Compare commits
14 Commits
1209ca1310
...
master
Author | SHA1 | Date | |
---|---|---|---|
15ba984d76 | |||
be2c17662d | |||
fa9318812d | |||
2528fbe9eb | |||
d434ff273b | |||
df48225954 | |||
c5b82d5dbe | |||
e5b3edb460 | |||
2a8f2e46d3 | |||
3a0ff257f7 | |||
d63adde44b | |||
6c914738a2 | |||
98788aeede | |||
0a503a8a0c |
46
config.go
46
config.go
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/yuin/gopher-lua"
|
"github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
@ -12,14 +13,28 @@ type Server struct {
|
|||||||
url string
|
url string
|
||||||
username string
|
username string
|
||||||
token string
|
token string
|
||||||
|
primary bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Servers holds all of the server structures.
|
||||||
|
type Servers map[string]*Server
|
||||||
|
|
||||||
|
// getPrimary gets the primary server contained in this map. Returns an error if no primary server is defined.
|
||||||
|
func (servers *Servers) getPrimary() (*Server, error) {
|
||||||
|
for _, s := range *servers {
|
||||||
|
if s.primary {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Server{}, errors.New("No primary server found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration holds data retrieved from a Lua script.
|
// Configuration holds data retrieved from a Lua script.
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
servers []*Server
|
servers Servers
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseText parses a text value from the configuration file.
|
// getStringFromTable parses a text value from the configuration file.
|
||||||
func getStringFromTable(L *lua.LState, lobj lua.LValue, key string, out *string) error {
|
func getStringFromTable(L *lua.LState, lobj lua.LValue, key string, out *string) error {
|
||||||
lv := L.GetTable(lobj, lua.LString(key))
|
lv := L.GetTable(lobj, lua.LString(key))
|
||||||
if text, ok := lv.(lua.LString); ok {
|
if text, ok := lv.(lua.LString); ok {
|
||||||
@ -30,6 +45,17 @@ func getStringFromTable(L *lua.LState, lobj lua.LValue, key string, out *string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getBoolFromTable parses a boolean value from the configuration file.
|
||||||
|
func getBoolFromTable(L *lua.LState, lobj lua.LValue, key string, out *bool) error {
|
||||||
|
lv := L.GetTable(lobj, lua.LString(key))
|
||||||
|
if val, ok := lv.(lua.LBool); ok {
|
||||||
|
*out = bool(val)
|
||||||
|
} else {
|
||||||
|
return errors.New(fmt.Sprintf("Failed to get configuration value '%s'", key))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseServerFromTable parses a Lua table into a Server structure.
|
// parseServerFromTable parses a Lua table into a Server structure.
|
||||||
func parseServerFromTable(L *lua.LState, table lua.LValue) (Server, error) {
|
func parseServerFromTable(L *lua.LState, table lua.LValue) (Server, error) {
|
||||||
server := Server{}
|
server := Server{}
|
||||||
@ -46,21 +72,35 @@ func parseServerFromTable(L *lua.LState, table lua.LValue) (Server, error) {
|
|||||||
return server, err
|
return server, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lv := L.GetTable(table, lua.LString("primary"))
|
||||||
|
if val, ok := lv.(lua.LBool); ok {
|
||||||
|
server.primary = bool(val)
|
||||||
|
} else if _, ok := lv.(*lua.LNilType); !ok {
|
||||||
|
panic("Config error : " + server.servername + " : Primary is neither nil nor boolean")
|
||||||
|
}
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration.Parse parses a Lua configuration file into a Configuration struct.
|
// Configuration.Parse parses a Lua configuration file into a Configuration struct.
|
||||||
func (config *Configuration) Parse(fname string) error {
|
func (config *Configuration) Parse(fname string) error {
|
||||||
|
config.servers = make(map[string]*Server)
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
|
|
||||||
L.DoFile(fname)
|
L.DoFile(fname)
|
||||||
table := L.Get(-1)
|
table := L.Get(-1)
|
||||||
|
|
||||||
|
found_primary := false
|
||||||
for i := 1; i <= L.ObjLen(table); i++ {
|
for i := 1; i <= L.ObjLen(table); i++ {
|
||||||
lserver := L.GetTable(table, lua.LNumber(i))
|
lserver := L.GetTable(table, lua.LNumber(i))
|
||||||
if server, err := parseServerFromTable(L, lserver); err == nil {
|
if server, err := parseServerFromTable(L, lserver); err == nil {
|
||||||
config.servers = append(config.servers, &server)
|
if !found_primary && server.primary {
|
||||||
|
found_primary = true
|
||||||
|
} else if server.primary {
|
||||||
|
panic("Config error : " + server.servername + " : Cannot have multiple primary servers!")
|
||||||
|
}
|
||||||
|
config.servers[server.servername] = &server
|
||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
159
feed.go
159
feed.go
@ -4,8 +4,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/list"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Activity stores an entry in a gitea user's activity feed.
|
// Activity stores an entry in a gitea user's activity feed.
|
||||||
@ -29,7 +34,7 @@ type Activity struct {
|
|||||||
func getActivityFeed() []Activity {
|
func getActivityFeed() []Activity {
|
||||||
feed := []Activity{}
|
feed := []Activity{}
|
||||||
for _, server := range config.servers {
|
for _, server := range config.servers {
|
||||||
client := Servers[server.servername]
|
client := servers[server.servername]
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/api/v1/users/%s/activities/feeds?limit=15&page=1",
|
resp, err := http.Get(fmt.Sprintf("%s/api/v1/users/%s/activities/feeds?limit=15&page=1",
|
||||||
server.url,
|
server.url,
|
||||||
server.username,
|
server.username,
|
||||||
@ -84,4 +89,156 @@ func getActivityFeed() []Activity {
|
|||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type listKeyMap struct {
|
||||||
|
toggleSpinner key.Binding
|
||||||
|
toggleTitleBar key.Binding
|
||||||
|
toggleStatusBar key.Binding
|
||||||
|
togglePagination key.Binding
|
||||||
|
toggleHelpMenu key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListKeyMap() *listKeyMap {
|
||||||
|
return &listKeyMap{
|
||||||
|
toggleSpinner: key.NewBinding(
|
||||||
|
key.WithKeys("s"),
|
||||||
|
key.WithHelp("s", "toggle spinner"),
|
||||||
|
),
|
||||||
|
toggleTitleBar: key.NewBinding(
|
||||||
|
key.WithKeys("T"),
|
||||||
|
key.WithHelp("T", "toggle title"),
|
||||||
|
),
|
||||||
|
toggleStatusBar: key.NewBinding(
|
||||||
|
key.WithKeys("S"),
|
||||||
|
key.WithHelp("S", "toggle status"),
|
||||||
|
),
|
||||||
|
togglePagination: key.NewBinding(
|
||||||
|
key.WithKeys("P"),
|
||||||
|
key.WithHelp("P", "toggle pagination"),
|
||||||
|
),
|
||||||
|
toggleHelpMenu: key.NewBinding(
|
||||||
|
key.WithKeys("H"),
|
||||||
|
key.WithHelp("H", "toggle help"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type feedviewer struct {
|
||||||
|
list list.Model
|
||||||
|
feed []Activity
|
||||||
|
appStyle lipgloss.Style
|
||||||
|
keys listKeyMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m feedviewer) Init() tea.Cmd {
|
||||||
|
m.appStyle = lipgloss.NewStyle().Padding(1, 2)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m feedviewer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
h, v := m.appStyle.GetFrameSize()
|
||||||
|
m.list.SetSize(msg.Width-h, msg.Height-v)
|
||||||
|
case tea.KeyMsg:
|
||||||
|
if m.list.FilterState() == list.Filtering {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.toggleSpinner):
|
||||||
|
return m, m.list.ToggleSpinner()
|
||||||
|
case key.Matches(msg, m.keys.toggleTitleBar):
|
||||||
|
v := !m.list.ShowTitle()
|
||||||
|
m.list.SetShowTitle(v)
|
||||||
|
m.list.SetShowFilter(v)
|
||||||
|
m.list.SetFilteringEnabled(v)
|
||||||
|
return m, nil
|
||||||
|
case key.Matches(msg, m.keys.toggleStatusBar):
|
||||||
|
m.list.SetShowStatusBar(!m.list.ShowStatusBar())
|
||||||
|
return m, nil
|
||||||
|
case key.Matches(msg, m.keys.togglePagination):
|
||||||
|
m.list.SetShowPagination(!m.list.ShowPagination())
|
||||||
|
return m, nil
|
||||||
|
case key.Matches(msg, m.keys.toggleHelpMenu):
|
||||||
|
m.list.SetShowHelp(!m.list.ShowHelp())
|
||||||
|
return m, nil
|
||||||
|
default:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newListModel, cmd := m.list.Update(msg)
|
||||||
|
m.list = newListModel
|
||||||
|
return m, tea.Batch(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m feedviewer) View() string {
|
||||||
|
return m.appStyle.Render(m.list.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
Activity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i item) Title() string {
|
||||||
|
switch i.Activity.op_type {
|
||||||
|
case "create_issue":
|
||||||
|
return fmt.Sprintf("%s created an issue", i.act_user.UserName)
|
||||||
|
case "commit_repo":
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.NewDecoder(strings.NewReader(i.content)).Decode(&data); err != nil {
|
||||||
|
return "JSON decode error: " + fmt.Sprint(err)
|
||||||
|
}
|
||||||
|
commits := data["Commits"].([]interface{})
|
||||||
|
commits_text := fmt.Sprintf("%d commit", len(commits))
|
||||||
|
if len(commits) > 1 {
|
||||||
|
commits_text += "s"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s pushed %s to %s", i.act_user.UserName, commits_text, i.ref_name)
|
||||||
|
case "comment_issue":
|
||||||
|
split := strings.Split(i.content, "|")
|
||||||
|
issue_num := split[0]
|
||||||
|
// comment := strings.TrimPrefix(i.content, issue_num+"|")
|
||||||
|
return fmt.Sprintf("%s commented on #%s", i.act_user.UserName, issue_num)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s performed an unknown action. (%s)", i.act_user.UserName, i.op_type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (i item) Description() string {
|
||||||
|
switch i.op_type {
|
||||||
|
case "commit_repo":
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.NewDecoder(strings.NewReader(i.content)).Decode(&data); err != nil {
|
||||||
|
return "JSON decode error: " + fmt.Sprint(err)
|
||||||
|
}
|
||||||
|
s := ""
|
||||||
|
commits := data["Commits"].([]interface{})
|
||||||
|
commit := commits[0].(map[string]interface{})
|
||||||
|
s += styles.text.Render(fmt.Sprintf("[%s] ", commit["Sha1"].(string)[0:10])) +
|
||||||
|
commit["Message"].(string)
|
||||||
|
return s
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func (i item) FilterValue() string { return "" }
|
||||||
|
|
||||||
|
func feed() {
|
||||||
|
feed := getActivityFeed()
|
||||||
|
items := []list.Item{}
|
||||||
|
|
||||||
|
for _, activity := range feed {
|
||||||
|
items = append(items, item{
|
||||||
|
activity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
p := feedviewer{}
|
||||||
|
p.list = list.New(items, list.NewDefaultDelegate(), 0, 0)
|
||||||
|
// Setup list
|
||||||
|
if _, err := tea.NewProgram(p, tea.WithAltScreen()).Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
18
go.mod
18
go.mod
@ -3,13 +3,19 @@ module code.retroedge.tech/noah/gitivity
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/sdk/gitea v0.17.1 // indirect
|
code.gitea.io/sdk/gitea v0.17.1
|
||||||
github.com/akamensky/argparse v1.4.0 // indirect
|
github.com/akamensky/argparse v1.4.0
|
||||||
|
github.com/charmbracelet/bubbles v0.18.0
|
||||||
|
github.com/charmbracelet/bubbletea v0.25.0
|
||||||
|
github.com/charmbracelet/lipgloss v0.9.1
|
||||||
|
github.com/karrick/tparse v2.4.2+incompatible
|
||||||
|
github.com/yuin/gopher-lua v1.1.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.18.0 // indirect
|
|
||||||
github.com/charmbracelet/bubbletea v0.25.0 // indirect
|
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/lipgloss v0.9.1 // indirect
|
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
@ -23,7 +29,7 @@ require (
|
|||||||
github.com/muesli/reflow v0.3.0 // indirect
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
github.com/muesli/termenv v0.15.2 // indirect
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
github.com/rivo/uniseg v0.4.6 // indirect
|
github.com/rivo/uniseg v0.4.6 // indirect
|
||||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
|
||||||
golang.org/x/crypto v0.17.0 // indirect
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
|
6
go.sum
6
go.sum
@ -2,6 +2,8 @@ code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8=
|
|||||||
code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM=
|
code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM=
|
||||||
github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
|
github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
|
||||||
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
|
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||||
@ -24,6 +26,8 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
|||||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/karrick/tparse v2.4.2+incompatible h1:+cW306qKAzrASC5XieHkgN7/vPaGKIuK62Q7nI7DIRc=
|
||||||
|
github.com/karrick/tparse v2.4.2+incompatible/go.mod h1:ASPA+vrIcN1uEW6BZg8vfWbzm69ODPSYZPU6qJyfdK0=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
@ -49,6 +53,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
|
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
|
||||||
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
@ -44,6 +44,14 @@ func (m Indicator) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case error:
|
case error:
|
||||||
m.err = msg
|
m.err = msg
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
default:
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
case IndicatorInfo:
|
case IndicatorInfo:
|
||||||
m.info = msg
|
m.info = msg
|
||||||
m.quitting = msg.quitting
|
m.quitting = msg.quitting
|
||||||
|
59
main.go
59
main.go
@ -9,16 +9,45 @@ import (
|
|||||||
"github.com/akamensky/argparse"
|
"github.com/akamensky/argparse"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Arguments is a place to store arguments / options / flags passed from the command line.
|
||||||
|
type Arguments struct {
|
||||||
|
Times struct {
|
||||||
|
since *string
|
||||||
|
}
|
||||||
|
Summary struct {
|
||||||
|
}
|
||||||
|
Feed struct {
|
||||||
|
}
|
||||||
|
global struct {
|
||||||
|
config_path *string
|
||||||
|
server *string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var arguments Arguments
|
||||||
var config Configuration
|
var config Configuration
|
||||||
var Servers map[string]*gitea.Client
|
var servers map[string]*gitea.Client
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
parser := argparse.NewParser("gitivity", "Command line tool to get Gitea statistics")
|
parser := argparse.NewParser("gitivity", "Command line tool to get Gitea statistics")
|
||||||
|
arguments.global.config_path = parser.String("c", "config", &argparse.Options{
|
||||||
|
Required: false,
|
||||||
|
Help: "Configuration file",
|
||||||
|
Default: "./config.lua",
|
||||||
|
})
|
||||||
|
arguments.global.server = parser.String("s", "server", &argparse.Options{
|
||||||
|
Required: false,
|
||||||
|
Help: "Specific server to use",
|
||||||
|
})
|
||||||
|
|
||||||
Times := parser.NewCommand("times", "Get a user's tracked times.")
|
Times := parser.NewCommand("times", "Get a user's tracked times.")
|
||||||
|
arguments.Times.since = Times.String("d", "since", &argparse.Options{
|
||||||
|
Required: false,
|
||||||
|
Help: "Get all time logs since this time. Format: 3d / 1w / 2mo",
|
||||||
|
Default: "1w",
|
||||||
|
})
|
||||||
Summary := parser.NewCommand("summary", "Display a summary of a user's activity.")
|
Summary := parser.NewCommand("summary", "Display a summary of a user's activity.")
|
||||||
Feed := parser.NewCommand("feed", "Display the user's activity feed.")
|
Feed := parser.NewCommand("feed", "Display the user's activity feed.")
|
||||||
config_path := parser.String("c", "config", &argparse.Options{Required: false, Help: "Configuration file", Default: "./config.lua"})
|
|
||||||
server_option := parser.String("s", "server", &argparse.Options{Required: false, Help: "Specific server to use"})
|
|
||||||
|
|
||||||
parse_err := parser.Parse(os.Args)
|
parse_err := parser.Parse(os.Args)
|
||||||
if parse_err != nil {
|
if parse_err != nil {
|
||||||
@ -27,13 +56,24 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config = Configuration{}
|
config = Configuration{}
|
||||||
Servers = make(map[string]*gitea.Client)
|
servers = make(map[string]*gitea.Client)
|
||||||
if err := config.Parse(*config_path); err != nil {
|
if err := config.Parse(*arguments.global.config_path); err != nil {
|
||||||
panic("Failed to parse configuration file")
|
panic("Failed to parse configuration file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specific server is used to ONLY use one of the available servers configured.
|
||||||
|
specific_server := ""
|
||||||
|
if *arguments.global.server != "" { // If a server was passed from CLI,
|
||||||
|
specific_server = *arguments.global.server // use that server.
|
||||||
|
} else { // Otherwise,
|
||||||
|
server, err := config.servers.getPrimary() // (Attempt to) get the primary server.
|
||||||
|
if err == nil { // If no error occured, we got the primary server.
|
||||||
|
specific_server = server.servername // Use the primary server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, server := range config.servers {
|
for _, server := range config.servers {
|
||||||
if *server_option != "" && strings.ToLower(server.servername) != strings.ToLower(*server_option) {
|
if specific_server != "" && strings.ToLower(server.servername) != strings.ToLower(specific_server) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
client_opts := []gitea.ClientOption{
|
client_opts := []gitea.ClientOption{
|
||||||
@ -44,9 +84,10 @@ func main() {
|
|||||||
fmt.Printf("Failed to create Gitea client! (%s)\n", err)
|
fmt.Printf("Failed to create Gitea client! (%s)\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
Servers[server.servername] = client
|
servers[server.servername] = client
|
||||||
}
|
}
|
||||||
if len(Servers) == 0 {
|
|
||||||
|
if len(servers) == 0 {
|
||||||
println("No servers configured / specified")
|
println("No servers configured / specified")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
@ -56,6 +97,6 @@ func main() {
|
|||||||
} else if Summary.Happened() {
|
} else if Summary.Happened() {
|
||||||
summary()
|
summary()
|
||||||
} else if Feed.Happened() {
|
} else if Feed.Happened() {
|
||||||
//feed()
|
feed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
summary.go
12
summary.go
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
@ -9,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type summaryviewer struct {
|
type summaryviewer struct {
|
||||||
times []gitea.TrackedTime
|
times []TrackedTime
|
||||||
total_time time.Duration
|
total_time time.Duration
|
||||||
quitting bool
|
quitting bool
|
||||||
}
|
}
|
||||||
@ -41,10 +42,14 @@ func (m summaryviewer) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func summary() {
|
func summary() {
|
||||||
|
finished := false
|
||||||
p := tea.NewProgram(initialIndicator(("Fetching time logs...")))
|
p := tea.NewProgram(initialIndicator(("Fetching time logs...")))
|
||||||
go func() {
|
go func() {
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
panic("An error occured.")
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
if !finished {
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -61,9 +66,10 @@ func summary() {
|
|||||||
viewer.total_time += time.Duration(t.Time * int64(time.Second))
|
viewer.total_time += time.Duration(t.Time * int64(time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finished = true
|
||||||
p.Send(IndicatorInfo{info: "Done", quitting: true})
|
p.Send(IndicatorInfo{info: "Done", quitting: true})
|
||||||
p.RestoreTerminal()
|
|
||||||
p.Quit()
|
p.Quit()
|
||||||
|
p.Wait()
|
||||||
|
|
||||||
program := tea.NewProgram(viewer)
|
program := tea.NewProgram(viewer)
|
||||||
if _, err := program.Run(); err != nil {
|
if _, err := program.Run(); err != nil {
|
||||||
|
184
times.go
184
times.go
@ -3,19 +3,118 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/table"
|
"github.com/charmbracelet/bubbles/table"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/karrick/tparse"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func times() {
|
||||||
|
since, err := tparse.AddDuration(time.Now(), "-"+*arguments.Times.since)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to parse time string '%s'\n", *arguments.Times.since)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
finished := false
|
||||||
|
p := tea.NewProgram(initialIndicator("Fetching time logs..."))
|
||||||
|
go func() {
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
// If the indicator exited before we got all the time logs, it was either an error or the user press CTRL + C / q
|
||||||
|
if !finished {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
times := getTimeLogs(since, func(repo gitea.Repository, took time.Duration) {
|
||||||
|
p.Send(IndicatorInfo{
|
||||||
|
info: fmt.Sprintf("%s / %s", repo.Owner.UserName, repo.Name),
|
||||||
|
duration: took,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
finished = true
|
||||||
|
p.Send(IndicatorInfo{info: "Done", quitting: true})
|
||||||
|
p.Quit()
|
||||||
|
p.Wait()
|
||||||
|
|
||||||
|
if len(times) == 0 {
|
||||||
|
fmt.Printf("No time logs found for the specified date!\n")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total_time time.Duration
|
||||||
|
|
||||||
|
columns := []table.Column{
|
||||||
|
{Title: "Server", Width: 15},
|
||||||
|
{Title: "Repository", Width: 15},
|
||||||
|
{Title: "User", Width: 8},
|
||||||
|
{Title: "Time", Width: 8},
|
||||||
|
{Title: "Created at", Width: 11},
|
||||||
|
}
|
||||||
|
rows := []table.Row{}
|
||||||
|
for _, t := range times {
|
||||||
|
dur, err := time.ParseDuration(fmt.Sprint(t.Time) + "s")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
rows = append(rows, table.Row{
|
||||||
|
t.server.servername,
|
||||||
|
t.Issue.Repository.Name,
|
||||||
|
t.UserName,
|
||||||
|
dur.String(),
|
||||||
|
t.Created.Format(time.DateOnly),
|
||||||
|
})
|
||||||
|
total_time += dur
|
||||||
|
}
|
||||||
|
tv := timesviewer{
|
||||||
|
total_time: total_time,
|
||||||
|
length: 70,
|
||||||
|
}
|
||||||
|
tab := table.New(
|
||||||
|
table.WithColumns(columns),
|
||||||
|
table.WithRows(rows),
|
||||||
|
table.WithFocused(true),
|
||||||
|
table.WithHeight(10),
|
||||||
|
table.WithWidth(tv.length),
|
||||||
|
)
|
||||||
|
s := table.DefaultStyles()
|
||||||
|
s.Header = s.Header.
|
||||||
|
Foreground(colors.overlay[0]).
|
||||||
|
BorderStyle(lipgloss.DoubleBorder()).
|
||||||
|
BorderForeground(colors.green).
|
||||||
|
BorderBottom(true).
|
||||||
|
Bold(false)
|
||||||
|
s.Selected = s.Selected.
|
||||||
|
Foreground(colors.surface[1]).
|
||||||
|
Background(colors.green).
|
||||||
|
Bold(false)
|
||||||
|
tab.SetStyles(s)
|
||||||
|
tv.table = tab
|
||||||
|
|
||||||
|
if _, err := tea.NewProgram(tv).Run(); err != nil {
|
||||||
|
fmt.Println("Error running program:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackedTime is an extended gitea.TrackedTime struct
|
||||||
|
type TrackedTime struct {
|
||||||
|
gitea.TrackedTime
|
||||||
|
server Server
|
||||||
|
}
|
||||||
|
|
||||||
// getTimeLogs gets every single time log possible.
|
// getTimeLogs gets every single time log possible.
|
||||||
func getTimeLogs(since time.Time, on_process_repo func(repo gitea.Repository, took time.Duration)) []gitea.TrackedTime {
|
func getTimeLogs(since time.Time, on_process_repo func(repo gitea.Repository, took time.Duration)) []TrackedTime {
|
||||||
var times []gitea.TrackedTime
|
var times []TrackedTime
|
||||||
for _, client := range Servers {
|
for server_name, client := range servers {
|
||||||
page := 1
|
page := 1
|
||||||
user, _, err := client.GetMyUserInfo()
|
user, _, err := client.GetMyUserInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -49,13 +148,17 @@ func getTimeLogs(since time.Time, on_process_repo func(repo gitea.Repository, to
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
for _, t := range repo_times {
|
for _, t := range repo_times {
|
||||||
times = append(times, *t)
|
times = append(times, TrackedTime{TrackedTime: *t, server: *config.servers[server_name]})
|
||||||
}
|
}
|
||||||
on_process_repo(*repo, time.Now().Sub(duration_start))
|
on_process_repo(*repo, time.Now().Sub(duration_start))
|
||||||
}
|
}
|
||||||
page++
|
page++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Sort the times by Created At
|
||||||
|
sort.SliceStable(times, func(i, j int) bool {
|
||||||
|
return times[j].Created.Compare(times[i].Created) < 1
|
||||||
|
})
|
||||||
return times
|
return times
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,76 +197,3 @@ func (m timesviewer) View() string {
|
|||||||
styles.text.Render("\nUse Up and Down arrows to navigate") +
|
styles.text.Render("\nUse Up and Down arrows to navigate") +
|
||||||
totalTextStyle.Render(fmt.Sprintf("\nTotal - %s\n", m.total_time.String()))
|
totalTextStyle.Render(fmt.Sprintf("\nTotal - %s\n", m.total_time.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func times() {
|
|
||||||
p := tea.NewProgram(initialIndicator("Fetching time logs..."))
|
|
||||||
go func() {
|
|
||||||
if _, err := p.Run(); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
since := time.Now().AddDate(0, 0, -7)
|
|
||||||
times := getTimeLogs(since, func(repo gitea.Repository, took time.Duration) {
|
|
||||||
p.Send(IndicatorInfo{
|
|
||||||
info: fmt.Sprintf("%s / %s", repo.Owner.UserName, repo.Name),
|
|
||||||
duration: took,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
p.Send(IndicatorInfo{
|
|
||||||
info: "Done!",
|
|
||||||
quitting: true,
|
|
||||||
})
|
|
||||||
p.Quit()
|
|
||||||
|
|
||||||
var total_time time.Duration
|
|
||||||
|
|
||||||
columns := []table.Column{
|
|
||||||
{Title: "User", Width: 10},
|
|
||||||
{Title: "Time", Width: 8},
|
|
||||||
{Title: "Created at", Width: 15},
|
|
||||||
}
|
|
||||||
rows := []table.Row{}
|
|
||||||
for _, t := range times {
|
|
||||||
dur, err := time.ParseDuration(fmt.Sprint(t.Time) + "s")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
rows = append(rows, table.Row{
|
|
||||||
t.UserName,
|
|
||||||
dur.String(),
|
|
||||||
t.Created.String(),
|
|
||||||
})
|
|
||||||
total_time += dur
|
|
||||||
}
|
|
||||||
tv := timesviewer{
|
|
||||||
total_time: total_time,
|
|
||||||
length: 50,
|
|
||||||
}
|
|
||||||
tab := table.New(
|
|
||||||
table.WithColumns(columns),
|
|
||||||
table.WithRows(rows),
|
|
||||||
table.WithFocused(true),
|
|
||||||
table.WithHeight(10),
|
|
||||||
table.WithWidth(tv.length),
|
|
||||||
)
|
|
||||||
s := table.DefaultStyles()
|
|
||||||
s.Header = s.Header.
|
|
||||||
Foreground(colors.overlay[0]).
|
|
||||||
BorderStyle(lipgloss.DoubleBorder()).
|
|
||||||
BorderForeground(colors.green).
|
|
||||||
BorderBottom(true).
|
|
||||||
Bold(false)
|
|
||||||
s.Selected = s.Selected.
|
|
||||||
Foreground(colors.surface[1]).
|
|
||||||
Background(colors.green).
|
|
||||||
Bold(false)
|
|
||||||
tab.SetStyles(s)
|
|
||||||
tv.table = tab
|
|
||||||
|
|
||||||
if _, err := tea.NewProgram(tv).Run(); err != nil {
|
|
||||||
fmt.Println("Error running program:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user