From ae2a71ad62d94ab393d69417fed4d360c7d6ea29 Mon Sep 17 00:00:00 2001 From: Toby Padilla Date: Tue, 3 Aug 2021 18:25:16 -0500 Subject: [PATCH] Scrollable timeline for latest commits across all repos --- {tui => git}/git.go | 36 +++++++++++------- go.mod | 2 + go.sum | 2 + tui/{model.go => bubble.go} | 47 ++++++++++++----------- tui/bubbles/commits/bubble.go | 70 +++++++++++++++++++++++++++++++++++ tui/bubbles/commits/style.go | 26 +++++++++++++ tui/bubbles/repo/bubble.go | 22 +++++++++++ tui/bubbles/repo/style.go | 25 +++++++++++++ tui/commands.go | 6 ++- 9 files changed, 196 insertions(+), 40 deletions(-) rename {tui => git}/git.go (55%) rename tui/{model.go => bubble.go} (69%) create mode 100644 tui/bubbles/commits/bubble.go create mode 100644 tui/bubbles/commits/style.go create mode 100644 tui/bubbles/repo/bubble.go create mode 100644 tui/bubbles/repo/style.go diff --git a/tui/git.go b/git/git.go similarity index 55% rename from tui/git.go rename to git/git.go index 59b7c6dfebbb77982cc454978987540e71323c74..80d8c88c384c856d866477026d31a90d53aa6d92 100644 --- a/tui/git.go +++ b/git/git.go @@ -1,4 +1,4 @@ -package tui +package git import ( "log" @@ -11,21 +11,29 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -type commitLog []*object.Commit +type RepoCommit struct { + Name string + Repo *git.Repository + Commit *object.Commit +} + +type CommitLog []RepoCommit -func (cl commitLog) Len() int { return len(cl) } -func (cl commitLog) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] } -func (cl commitLog) Less(i, j int) bool { return cl[i].Author.When.After(cl[j].Author.When) } +func (cl CommitLog) Len() int { return len(cl) } +func (cl CommitLog) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] } +func (cl CommitLog) Less(i, j int) bool { + return cl[i].Commit.Author.When.After(cl[j].Commit.Author.When) +} -type repoSource struct { +type RepoSource struct { mtx sync.Mutex path string repos []*git.Repository - commits commitLog + commits CommitLog } -func newRepoSource(repoPath string) *repoSource { - rs := &repoSource{path: repoPath} +func NewRepoSource(repoPath string) *RepoSource { + rs := &RepoSource{path: repoPath} go func() { for { rs.loadRepos() @@ -35,13 +43,13 @@ func newRepoSource(repoPath string) *repoSource { return rs } -func (rs *repoSource) allRepos() []*git.Repository { +func (rs *RepoSource) AllRepos() []*git.Repository { rs.mtx.Lock() defer rs.mtx.Unlock() return rs.repos } -func (rs *repoSource) getCommits(limit int) []*object.Commit { +func (rs *RepoSource) GetCommits(limit int) []RepoCommit { rs.mtx.Lock() defer rs.mtx.Unlock() if limit > len(rs.commits) { @@ -50,7 +58,7 @@ func (rs *repoSource) getCommits(limit int) []*object.Commit { return rs.commits[:limit] } -func (rs *repoSource) loadRepos() { +func (rs *RepoSource) loadRepos() { rs.mtx.Lock() defer rs.mtx.Unlock() rd, err := os.ReadDir(rs.path) @@ -58,7 +66,7 @@ func (rs *repoSource) loadRepos() { return } rs.repos = make([]*git.Repository, 0) - rs.commits = make([]*object.Commit, 0) + rs.commits = make([]RepoCommit, 0) for _, rd := range rd { r, err := git.PlainOpen(rs.path + string(os.PathSeparator) + rd.Name()) if err != nil { @@ -69,7 +77,7 @@ func (rs *repoSource) loadRepos() { log.Fatal(err) } l.ForEach(func(c *object.Commit) error { - rs.commits = append(rs.commits, c) + rs.commits = append(rs.commits, RepoCommit{Name: rd.Name(), Repo: r, Commit: c}) return nil }) sort.Sort(rs.commits) diff --git a/go.mod b/go.mod index 0153ec754931be032e03ea864c7d28e2473f8b5e..2a354f2d3dc6bcf7f1c1edad51858cb706237acc 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,11 @@ replace github.com/charmbracelet/charm => ../charm replace github.com/charmbracelet/bubbletea => ../bubbletea require ( + github.com/charmbracelet/bubbles v0.8.0 github.com/charmbracelet/bubbletea v0.14.0 github.com/charmbracelet/charm v0.8.6 github.com/charmbracelet/lipgloss v0.2.1 + github.com/dustin/go-humanize v1.0.0 github.com/gliderlabs/ssh v0.3.3 github.com/go-git/go-git/v5 v5.4.2 github.com/meowgorithm/babyenv v1.3.0 diff --git a/go.sum b/go.sum index 4b12c310808cb65917f1995b2c2c0edd2780b3a1..6a3755ebe0b4f409cccdf9b3995fd0314b5ba962 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/charmbracelet/bubbles v0.8.0 h1:+l2op90Ag37Vn+30O1hbg/0wBl+e+sxHhgY1F/rvdHs= github.com/charmbracelet/bubbles v0.8.0/go.mod h1:5WX1sSSjNCgCrzvRMN/z23HxvWaa+AI16Ch0KPZPeDs= github.com/charmbracelet/bubbletea v0.13.1/go.mod h1:tp9tr9Dadh0PLhgiwchE5zZJXm5543JYjHG9oY+5qSg= github.com/charmbracelet/bubbletea v0.14.0 h1:SBbtRPkZ/wGYyTAn2zrBERes/nwzxZR5QdJ5nvczmFw= @@ -53,6 +54,7 @@ github.com/dgraph-io/ristretto v0.0.4-0.20210122082011-bb5d392ed82d/go.mod h1:tv github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= diff --git a/tui/model.go b/tui/bubble.go similarity index 69% rename from tui/model.go rename to tui/bubble.go index 2c92291e67bf228ee0865be497de26271cc973b4..2f5b45bb343330cd47185912a8d5acbb8f085319 100644 --- a/tui/model.go +++ b/tui/bubble.go @@ -2,10 +2,11 @@ package tui import ( "fmt" + "smoothie/git" + "smoothie/tui/bubbles/commits" tea "github.com/charmbracelet/bubbletea" "github.com/gliderlabs/ssh" - "github.com/go-git/go-git/v5/plumbing/object" ) type sessionState int @@ -13,7 +14,7 @@ type sessionState int const ( startState sessionState = iota errorState - commitsLoadedState + loadedState quittingState quitState ) @@ -26,45 +27,44 @@ func (e errMsg) Error() string { return e.err.Error() } -func SessionHandler(repoPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) { - rs := newRepoSource(repoPath) +func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) { + rs := git.NewRepoSource(reposPath) return func(s ssh.Session) (tea.Model, []tea.ProgramOption) { if len(s.Command()) == 0 { pty, changes, active := s.Pty() if !active { return nil, nil } - return NewModel(pty.Window.Width, pty.Window.Height, changes, rs), nil + return NewModel(pty.Window.Width, pty.Window.Height, changes, rs), []tea.ProgramOption{tea.WithAltScreen()} } return nil, nil } } type Model struct { - state sessionState - error string - info string - width int - height int - windowChanges <-chan ssh.Window - repos *repoSource - commits []*object.Commit + state sessionState + error string + info string + width int + height int + windowChanges <-chan ssh.Window + repoSource *git.RepoSource + commitTimeline *commits.Bubble } -func NewModel(width int, height int, windowChanges <-chan ssh.Window, repos *repoSource) *Model { +func NewModel(width int, height int, windowChanges <-chan ssh.Window, repoSource *git.RepoSource) *Model { m := &Model{ width: width, height: height, windowChanges: windowChanges, - repos: repos, - commits: make([]*object.Commit, 0), + repoSource: repoSource, } m.state = startState return m } func (m *Model) Init() tea.Cmd { - return tea.Batch(m.windowChangesCmd, tea.HideCursor, m.getCommitsCmd) + return tea.Batch(m.windowChangesCmd, m.getCommitsCmd) } func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -77,6 +77,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "q", "ctrl+c": return m, tea.Quit + case "j", "k", "up", "down": + _, cmd := m.commitTimeline.Update(msg) + cmds = append(cmds, cmd) } case errMsg: m.error = msg.Error() @@ -89,6 +92,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height + m.commitTimeline.Height = msg.Height } return m, tea.Batch(cmds...) } @@ -100,13 +104,8 @@ func (m *Model) View() string { s := "" content := "" switch m.state { - case startState: - s += normalStyle.Render("Loading") - case commitsLoadedState: - for _, c := range m.commits { - msg := fmt.Sprintf("%s %s %s %s", c.Author.When, c.Author.Name, c.Author.Email, c.Message) - s += normalStyle.Render(msg) + "\n" - } + case loadedState: + s += m.commitTimeline.View() case errorState: s += errorStyle.Render(fmt.Sprintf("Bummer: %s", m.error)) default: diff --git a/tui/bubbles/commits/bubble.go b/tui/bubbles/commits/bubble.go new file mode 100644 index 0000000000000000000000000000000000000000..25aed72d2362919adcc5ffc8fe1d37cd81191abc --- /dev/null +++ b/tui/bubbles/commits/bubble.go @@ -0,0 +1,70 @@ +package commits + +import ( + "smoothie/git" + "strings" + + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/dustin/go-humanize" +) + +type Bubble struct { + Commits []git.RepoCommit + Margin int + Width int + Height int + viewport viewport.Model +} + +func NewBubble(height int, margin int, width int, rcs []git.RepoCommit) *Bubble { + b := &Bubble{ + Commits: rcs, + viewport: viewport.Model{Height: height - margin, Width: width}, + } + s := "" + for _, rc := range rcs { + s += b.renderCommit(rc) + "\n" + } + b.viewport.SetContent(s) + return b +} + +func (b *Bubble) Init() tea.Cmd { + return nil +} + +func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + cmds := make([]tea.Cmd, 0) + switch msg := msg.(type) { + case tea.WindowSizeMsg: + b.viewport.Height = msg.Height - b.Margin + case tea.KeyMsg: + switch msg.String() { + case "up", "k": + b.viewport.LineUp(1) + case "down", "j": + b.viewport.LineDown(1) + } + } + return b, tea.Batch(cmds...) +} + +func (b *Bubble) renderCommit(rc git.RepoCommit) string { + s := "" + s += commitRepoNameStyle.Render(rc.Name) + s += " " + s += commitDateStyle.Render(humanize.Time(rc.Commit.Author.When)) + s += "\n" + s += commitCommentStyle.Render(strings.TrimSpace(rc.Commit.Message)) + s += "\n" + s += commitAuthorStyle.Render(rc.Commit.Author.Name) + s += " " + s += commitAuthorEmailStyle.Render(rc.Commit.Author.Email) + s += " " + return commitBoxStyle.Width(b.viewport.Height).Render(s) +} + +func (b *Bubble) View() string { + return b.viewport.View() +} diff --git a/tui/bubbles/commits/style.go b/tui/bubbles/commits/style.go new file mode 100644 index 0000000000000000000000000000000000000000..dd2543acb875d62ff769efb5ab39bc204b183d22 --- /dev/null +++ b/tui/bubbles/commits/style.go @@ -0,0 +1,26 @@ +package commits + +import ( + "github.com/charmbracelet/lipgloss" +) + +var commitBoxStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFFFF")). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#670083")). + Padding(1) +var commitRepoNameStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#8922A5")) +var commitAuthorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#670083")) +var commitAuthorEmailStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#781194")) +var commitDateStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#781194")) +var commitCommentStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#A0A0A0")). + BorderStyle(lipgloss.Border{Left: ">"}). + BorderForeground(lipgloss.Color("#606060")). + PaddingLeft(1). + PaddingBottom(0). + Margin(0) diff --git a/tui/bubbles/repo/bubble.go b/tui/bubbles/repo/bubble.go new file mode 100644 index 0000000000000000000000000000000000000000..3d1f22da4d48b41eaa832b5e738084d9f0c87d3a --- /dev/null +++ b/tui/bubbles/repo/bubble.go @@ -0,0 +1,22 @@ +package repo + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/go-git/go-git/v5" +) + +type Bubble struct { + repo *git.Repository +} + +func (b *Bubble) Init() tea.Cmd { + return nil +} + +func (b *Bubble) Update(tea.Msg) (tea.Model, tea.Cmd) { + return nil, nil +} + +func (b *Bubble) View() string { + return "repo" +} diff --git a/tui/bubbles/repo/style.go b/tui/bubbles/repo/style.go new file mode 100644 index 0000000000000000000000000000000000000000..2789bdad3b05efe1bbcd4e77c8b416fc54847b4a --- /dev/null +++ b/tui/bubbles/repo/style.go @@ -0,0 +1,25 @@ +package repo + +import ( + "github.com/charmbracelet/lipgloss" +) + +var commitBoxStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFFFF")). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#670083")). + Padding(1) +var commitRepoNameStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#8922A5")) +var commitAuthorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#670083")) +var commitAuthorEmailStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#781194")) +var commitDateStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#781194")) +var commitCommentStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#606060")). + BorderStyle(lipgloss.Border{Left: ">"}). + PaddingLeft(1). + PaddingBottom(0). + Margin(0) diff --git a/tui/commands.go b/tui/commands.go index cd24944ead3f03f615f3153414d455cb75bd07b1..7189ed70236ee73eb15805add4e10373f535169f 100644 --- a/tui/commands.go +++ b/tui/commands.go @@ -1,6 +1,8 @@ package tui import ( + "smoothie/tui/bubbles/commits" + tea "github.com/charmbracelet/bubbletea" ) @@ -14,7 +16,7 @@ func (m *Model) windowChangesCmd() tea.Msg { } func (m *Model) getCommitsCmd() tea.Msg { - m.commits = m.repos.getCommits(20) - m.state = commitsLoadedState + m.commitTimeline = commits.NewBubble(m.height, 2, 80, m.repoSource.GetCommits(200)) + m.state = loadedState return nil }