ae2a71ad62d94ab393d69417fed4d360c7d6ea29

Author
Toby Padilla <toby@charm.sh>
Committer
Toby Padilla <toby@charm.sh>
Date

Message

Scrollable timeline for latest commits across all repos

Diff

This diff is truncated to protect this page.

  1diff --git a/tui/git.go b/git/git.go
  2rename from tui/git.go
  3rename to git/git.go
  4index 59b7c6dfebbb77982cc454978987540e71323c74..80d8c88c384c856d866477026d31a90d53aa6d92 100644
  5--- a/tui/git.go
  6+++ b/git/git.go
  7@@ -1,4 +1,4 @@
  8-package tui
  9+package git
 10 
 11 import (
 12 	"log"
 13@@ -11,21 +11,29 @@ import (
 14 	"github.com/go-git/go-git/v5/plumbing/object"
 15 )
 16 
 17-type commitLog []*object.Commit
 18+type RepoCommit struct {
 19+	Name   string
 20+	Repo   *git.Repository
 21+	Commit *object.Commit
 22+}
 23+
 24+type CommitLog []RepoCommit
 25 
 26-func (cl commitLog) Len() int           { return len(cl) }
 27-func (cl commitLog) Swap(i, j int)      { cl[i], cl[j] = cl[j], cl[i] }
 28-func (cl commitLog) Less(i, j int) bool { return cl[i].Author.When.After(cl[j].Author.When) }
 29+func (cl CommitLog) Len() int      { return len(cl) }
 30+func (cl CommitLog) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
 31+func (cl CommitLog) Less(i, j int) bool {
 32+	return cl[i].Commit.Author.When.After(cl[j].Commit.Author.When)
 33+}
 34 
 35-type repoSource struct {
 36+type RepoSource struct {
 37 	mtx     sync.Mutex
 38 	path    string
 39 	repos   []*git.Repository
 40-	commits commitLog
 41+	commits CommitLog
 42 }
 43 
 44-func newRepoSource(repoPath string) *repoSource {
 45-	rs := &repoSource{path: repoPath}
 46+func NewRepoSource(repoPath string) *RepoSource {
 47+	rs := &RepoSource{path: repoPath}
 48 	go func() {
 49 		for {
 50 			rs.loadRepos()
 51@@ -35,13 +43,13 @@ func newRepoSource(repoPath string) *repoSource {
 52 	return rs
 53 }
 54 
 55-func (rs *repoSource) allRepos() []*git.Repository {
 56+func (rs *RepoSource) AllRepos() []*git.Repository {
 57 	rs.mtx.Lock()
 58 	defer rs.mtx.Unlock()
 59 	return rs.repos
 60 }
 61 
 62-func (rs *repoSource) getCommits(limit int) []*object.Commit {
 63+func (rs *RepoSource) GetCommits(limit int) []RepoCommit {
 64 	rs.mtx.Lock()
 65 	defer rs.mtx.Unlock()
 66 	if limit > len(rs.commits) {
 67@@ -50,7 +58,7 @@ func (rs *repoSource) getCommits(limit int) []*object.Commit {
 68 	return rs.commits[:limit]
 69 }
 70 
 71-func (rs *repoSource) loadRepos() {
 72+func (rs *RepoSource) loadRepos() {
 73 	rs.mtx.Lock()
 74 	defer rs.mtx.Unlock()
 75 	rd, err := os.ReadDir(rs.path)
 76@@ -58,7 +66,7 @@ func (rs *repoSource) loadRepos() {
 77 		return
 78 	}
 79 	rs.repos = make([]*git.Repository, 0)
 80-	rs.commits = make([]*object.Commit, 0)
 81+	rs.commits = make([]RepoCommit, 0)
 82 	for _, rd := range rd {
 83 		r, err := git.PlainOpen(rs.path + string(os.PathSeparator) + rd.Name())
 84 		if err != nil {
 85@@ -69,7 +77,7 @@ func (rs *repoSource) loadRepos() {
 86 			log.Fatal(err)
 87 		}
 88 		l.ForEach(func(c *object.Commit) error {
 89-			rs.commits = append(rs.commits, c)
 90+			rs.commits = append(rs.commits, RepoCommit{Name: rd.Name(), Repo: r, Commit: c})
 91 			return nil
 92 		})
 93 		sort.Sort(rs.commits)
 94diff --git a/go.mod b/go.mod
 95index 0153ec754931be032e03ea864c7d28e2473f8b5e..2a354f2d3dc6bcf7f1c1edad51858cb706237acc 100644
 96--- a/go.mod
 97+++ b/go.mod
 98@@ -7,9 +7,11 @@ replace github.com/charmbracelet/charm => ../charm
 99 replace github.com/charmbracelet/bubbletea => ../bubbletea
100 
101 require (
102+	github.com/charmbracelet/bubbles v0.8.0
103 	github.com/charmbracelet/bubbletea v0.14.0
104 	github.com/charmbracelet/charm v0.8.6
105 	github.com/charmbracelet/lipgloss v0.2.1
106+	github.com/dustin/go-humanize v1.0.0
107 	github.com/gliderlabs/ssh v0.3.3
108 	github.com/go-git/go-git/v5 v5.4.2
109 	github.com/meowgorithm/babyenv v1.3.0
110diff --git a/go.sum b/go.sum
111index 4b12c310808cb65917f1995b2c2c0edd2780b3a1..6a3755ebe0b4f409cccdf9b3995fd0314b5ba962 100644
112--- a/go.sum
113+++ b/go.sum
114@@ -23,6 +23,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
115 github.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM=
116 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
117 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
118+github.com/charmbracelet/bubbles v0.8.0 h1:+l2op90Ag37Vn+30O1hbg/0wBl+e+sxHhgY1F/rvdHs=
119 github.com/charmbracelet/bubbles v0.8.0/go.mod h1:5WX1sSSjNCgCrzvRMN/z23HxvWaa+AI16Ch0KPZPeDs=
120 github.com/charmbracelet/bubbletea v0.13.1/go.mod h1:tp9tr9Dadh0PLhgiwchE5zZJXm5543JYjHG9oY+5qSg=
121 github.com/charmbracelet/bubbletea v0.14.0 h1:SBbtRPkZ/wGYyTAn2zrBERes/nwzxZR5QdJ5nvczmFw=
122@@ -53,6 +54,7 @@ github.com/dgraph-io/ristretto v0.0.4-0.20210122082011-bb5d392ed82d/go.mod h1:tv
123 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
124 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
125 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
126+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
127 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
128 github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
129 github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
130diff --git a/tui/model.go b/tui/bubble.go
131rename from tui/model.go
132rename to tui/bubble.go
133index 2c92291e67bf228ee0865be497de26271cc973b4..2f5b45bb343330cd47185912a8d5acbb8f085319 100644
134--- a/tui/model.go
135+++ b/tui/bubble.go
136@@ -2,10 +2,11 @@ package tui
137 
138 import (
139 	"fmt"
140+	"smoothie/git"
141+	"smoothie/tui/bubbles/commits"
142 
143 	tea "github.com/charmbracelet/bubbletea"
144 	"github.com/gliderlabs/ssh"
145-	"github.com/go-git/go-git/v5/plumbing/object"
146 )
147 
148 type sessionState int
149@@ -13,7 +14,7 @@ type sessionState int
150 const (
151 	startState sessionState = iota
152 	errorState
153-	commitsLoadedState
154+	loadedState
155 	quittingState
156 	quitState
157 )
158@@ -26,45 +27,44 @@ func (e errMsg) Error() string {
159 	return e.err.Error()
160 }
161 
162-func SessionHandler(repoPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
163-	rs := newRepoSource(repoPath)
164+func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
165+	rs := git.NewRepoSource(reposPath)
166 	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
167 		if len(s.Command()) == 0 {
168 			pty, changes, active := s.Pty()
169 			if !active {
170 				return nil, nil
171 			}
172-			return NewModel(pty.Window.Width, pty.Window.Height, changes, rs), nil
173+			return NewModel(pty.Window.Width, pty.Window.Height, changes, rs), []tea.ProgramOption{tea.WithAltScreen()}
174 		}
175 		return nil, nil
176 	}
177 }
178 
179 type Model struct {
180-	state         sessionState
181-	error         string
182-	info          string
183-	width         int
184-	height        int
185-	windowChanges <-chan ssh.Window
186-	repos         *repoSource
187-	commits       []*object.Commit
188+	state          sessionState
189+	error          string
190+	info           string
191+	width          int
192+	height         int
193+	windowChanges  <-chan ssh.Window
194+	repoSource     *git.RepoSource
195+	commitTimeline *commits.Bubble
196 }
197 
198-func NewModel(width int, height int, windowChanges <-chan ssh.Window, repos *repoSource) *Model {
199+func NewModel(width int, height int, windowChanges <-chan ssh.Window, repoSource *git.RepoSource) *Model {
200 	m := &Model{
201 		width:         width,
202 		height:        height,
203 		windowChanges: windowChanges,
204-		repos:         repos,
205-		commits:       make([]*object.Commit, 0),
206+		repoSource:    repoSource,
207 	}
208 	m.state = startState
209 	return m
210 }
211 
212 func (m *Model) Init() tea.Cmd {
213-	return tea.Batch(m.windowChangesCmd, tea.HideCursor, m.getCommitsCmd)
214+	return tea.Batch(m.windowChangesCmd, m.getCommitsCmd)
215 }
216 
217 func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
218@@ -77,6 +77,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
219 		switch msg.String() {
220 		case "q", "ctrl+c":
221 			return m, tea.Quit
222+		case "j", "k", "up", "down":
223+			_, cmd := m.commitTimeline.Update(msg)
224+			cmds = append(cmds, cmd)
225 		}
226 	case errMsg:
227 		m.error = msg.Error()
228@@ -89,6 +92,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
229 	case tea.WindowSizeMsg:
230 		m.width = msg.Width
231 		m.height = msg.Height
232+		m.commitTimeline.Height = msg.Height
233 	}
234 	return m, tea.Batch(cmds...)
235 }
236diff --git a/tui/bubbles/commits/bubble.go b/tui/bubbles/commits/bubble.go
237new file mode 100644
238index 0000000000000000000000000000000000000000..25aed72d2362919adcc5ffc8fe1d37cd81191abc
239--- /dev/null
240+++ b/tui/bubbles/commits/bubble.go
241@@ -0,0 +1,70 @@
242+package commits
243+
244+import (
245+	"smoothie/git"
246+	"strings"
247+
248+	"github.com/charmbracelet/bubbles/viewport"
249+	tea "github.com/charmbracelet/bubbletea"
250+	"github.com/dustin/go-humanize"
251+)
252+
253+type Bubble struct {
254+	Commits  []git.RepoCommit
255+	Margin   int
256+	Width    int
257+	Height   int
258+	viewport viewport.Model
259+}
260+
261+func NewBubble(height int, margin int, width int, rcs []git.RepoCommit) *Bubble {
262+	b := &Bubble{
263+		Commits:  rcs,
264+		viewport: viewport.Model{Height: height - margin, Width: width},
265+	}
266+	s := ""
267+	for _, rc := range rcs {
268+		s += b.renderCommit(rc) + "\n"
269+	}
270+	b.viewport.SetContent(s)
271+	return b
272+}
273+
274+func (b *Bubble) Init() tea.Cmd {
275+	return nil
276+}
277+
278+func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
279+	cmds := make([]tea.Cmd, 0)
280+	switch msg := msg.(type) {
281+	case tea.WindowSizeMsg:
282+		b.viewport.Height = msg.Height - b.Margin
283+	case tea.KeyMsg:
284+		switch msg.String() {
285+		case "up", "k":
286+			b.viewport.LineUp(1)
287+		case "down", "j":
288+			b.viewport.LineDown(1)
289+		}
290+	}
291+	return b, tea.Batch(cmds...)
292+}
293+
294+func (b *Bubble) renderCommit(rc git.RepoCommit) string {
295+	s := ""
296+	s += commitRepoNameStyle.Render(rc.Name)
297+	s += " "
298+	s += commitDateStyle.Render(humanize.Time(rc.Commit.Author.When))
299+	s += "\n"
300+	s += commitCommentStyle.Render(strings.TrimSpace(rc.Commit.Message))
301+	s += "\n"
302+	s += commitAuthorStyle.Render(rc.Commit.Author.Name)
303+	s += " "
304+	s += commitAuthorEmailStyle.Render(rc.Commit.Author.Email)
305+	s += " "
306+	return commitBoxStyle.Width(b.viewport.Height).Render(s)
307+}
308+
309+func (b *Bubble) View() string {
310+	return b.viewport.View()
311+}
312diff --git a/tui/bubbles/commits/style.go b/tui/bubbles/commits/style.go
313new file mode 100644
314index 0000000000000000000000000000000000000000..dd2543acb875d62ff769efb5ab39bc204b183d22
315--- /dev/null
316+++ b/tui/bubbles/commits/style.go
317@@ -0,0 +1,26 @@
318+package commits
319+
320+import (
321+	"github.com/charmbracelet/lipgloss"
322+)
323+
324+var commitBoxStyle = lipgloss.NewStyle().
325+	Foreground(lipgloss.Color("#FFFFFF")).
326+	BorderStyle(lipgloss.RoundedBorder()).
327+	BorderForeground(lipgloss.Color("#670083")).
328+	Padding(1)
329+var commitRepoNameStyle = lipgloss.NewStyle().
330+	Foreground(lipgloss.Color("#8922A5"))
331+var commitAuthorStyle = lipgloss.NewStyle().
332+	Foreground(lipgloss.Color("#670083"))
333+var commitAuthorEmailStyle = lipgloss.NewStyle().
334+	Foreground(lipgloss.Color("#781194"))
335+var commitDateStyle = lipgloss.NewStyle().
336+	Foreground(lipgloss.Color("#781194"))
337+var commitCommentStyle = lipgloss.NewStyle().
338+	Foreground(lipgloss.Color("#A0A0A0")).
339+	BorderStyle(lipgloss.Border{Left: ">"}).
340+	BorderForeground(lipgloss.Color("#606060")).
341+	PaddingLeft(1).
342+	PaddingBottom(0).
343+	Margin(0)
344diff --git a/tui/bubbles/repo/bubble.go b/tui/bubbles/repo/bubble.go
345new file mode 100644
346index 0000000000000000000000000000000000000000..3d1f22da4d48b41eaa832b5e738084d9f0c87d3a
347--- /dev/null
348+++ b/tui/bubbles/repo/bubble.go
349@@ -0,0 +1,22 @@
350+package repo
351+
352+import (
353+	tea "github.com/charmbracelet/bubbletea"
354+	"github.com/go-git/go-git/v5"
355+)
356+
357+type Bubble struct {
358+	repo *git.Repository
359+}
360+
361+func (b *Bubble) Init() tea.Cmd {
362+	return nil
363+}
364+
365+func (b *Bubble) Update(tea.Msg) (tea.Model, tea.Cmd) {
366+	return nil, nil
367+}
368+
369+func (b *Bubble) View() string {
370+	return "repo"
371+}
372diff --git a/tui/bubbles/repo/style.go b/tui/bubbles/repo/style.go
373new file mode 100644
374index 0000000000000000000000000000000000000000..2789bdad3b05efe1bbcd4e77c8b416fc54847b4a
375--- /dev/null
376+++ b/tui/bubbles/repo/style.go
377@@ -0,0 +1,25 @@
378+package repo
379+
380+import (
381+	"github.com/charmbracelet/lipgloss"
382+)
383+
384+var commitBoxStyle = lipgloss.NewStyle().
385+	Foreground(lipgloss.Color("#FFFFFF")).
386+	BorderStyle(lipgloss.RoundedBorder()).
387+	BorderForeground(lipgloss.Color("#670083")).
388+	Padding(1)
389+var commitRepoNameStyle = lipgloss.NewStyle().
390+	Foreground(lipgloss.Color("#8922A5"))
391+var commitAuthorStyle = lipgloss.NewStyle().
392+	Foreground(lipgloss.Color("#670083"))
393+var commitAuthorEmailStyle = lipgloss.NewStyle().
394+	Foreground(lipgloss.Color("#781194"))
395+var commitDateStyle = lipgloss.NewStyle().
396+	Foreground(lipgloss.Color("#781194"))
397+var commitCommentStyle = lipgloss.NewStyle().
398+	Foreground(lipgloss.Color("#606060")).
399+	BorderStyle(lipgloss.Border{Left: ">"}).
400+	PaddingLeft(1).
401+	PaddingBottom(0).
402+	Margin(0)
403diff --git a/tui/commands.go b/tui/commands.go
404index cd24944ead3f03f615f3153414d455cb75bd07b1..7189ed70236ee73eb15805add4e10373f535169f 100644
405--- a/tui/commands.go
406+++ b/tui/commands.go
407@@ -1,6 +1,8 @@
408 package tui
409 
410 import (
411+	"smoothie/tui/bubbles/commits"
412+
413 	tea "github.com/charmbracelet/bubbletea"
414 )
415 
416@@ -14,7 +16,7 @@ func (m *Model) windowChangesCmd() tea.Msg {
417 }
418 
419 func (m *Model) getCommitsCmd() tea.Msg {
420-	m.commits = m.repos.getCommits(20)
421-	m.state = commitsLoadedState
422+	m.commitTimeline = commits.NewBubble(m.height, 2, 80, m.repoSource.GetCommits(200))
423+	m.state = loadedState
424 	return nil
425 }