package main import ( "encoding/json" "fmt" "net/http" "strings" "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. type Activity struct { act_user *gitea.User act_user_id int64 comment gitea.Comment comment_id int64 content string created string id int64 is_private bool op_type string ref_name string repo *gitea.Repository repo_id int64 user_id int64 } // getActivityFeed returns the authenticated user's activity feed. func getActivityFeed() []Activity { feed := []Activity{} for _, server := range config.servers { client := servers[server.servername] resp, err := http.Get(fmt.Sprintf("%s/api/v1/users/%s/activities/feeds?limit=15&page=1", server.url, server.username, )) if err != nil { println("Failed to make HTTP request") continue } defer resp.Body.Close() var data []map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&data) if err != nil { fmt.Println("Error decoding JSON: " + fmt.Sprint(err)) continue } for _, obj := range data { act_user, _, err := client.GetUserInfo(obj["act_user"].(map[string]interface{})["login"].(string)) if err != nil { fmt.Println("Error getting user: " + fmt.Sprint(err)) continue } var comment gitea.Comment if obj["comment"] != nil { raw_comment := obj["comment"].(map[string]interface{}) comment = gitea.Comment{ Body: raw_comment["body"].(string), HTMLURL: raw_comment["html_url"].(string), ID: int64(raw_comment["id"].(float64)), IssueURL: raw_comment["issue_url"].(string), OriginalAuthor: raw_comment["original_author"].(string), OriginalAuthorID: int64(raw_comment["original_author_id"].(float64)), } } feed = append(feed, Activity{ // repo: repo, act_user: act_user, act_user_id: int64(obj["act_user_id"].(float64)), comment: comment, comment_id: int64(obj["comment_id"].(float64)), content: obj["content"].(string), created: obj["created"].(string), id: int64(obj["id"].(float64)), is_private: obj["is_private"].(bool), op_type: obj["op_type"].(string), ref_name: obj["ref_name"].(string), repo_id: int64(obj["repo_id"].(float64)), user_id: int64(obj["user_id"].(float64)), }) } } 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) } }