4fab795d33e3fda62eca59bc410bfe2b931bdde8
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/go.mod b/go.mod
2index 6f6ef1df4b09317e733a0b16164bfad1ed0705d7..fad69374307dd3260c3b4900262899e57d855e71 100644
3--- a/go.mod
4+++ b/go.mod
5@@ -3,8 +3,9 @@ module github.com/charmbracelet/soft-serve
6 go 1.17
7
8 require (
9+ github.com/alecthomas/chroma v0.10.0
10 github.com/caarlos0/env/v6 v6.9.1
11- github.com/charmbracelet/bubbles v0.10.0
12+ github.com/charmbracelet/bubbles v0.10.3-0.20220208194203-1d489252fe50
13 github.com/charmbracelet/bubbletea v0.19.4-0.20220208181305-42cd4c31919c
14 github.com/charmbracelet/glamour v0.4.0
15 github.com/charmbracelet/lipgloss v0.4.0
16@@ -15,6 +16,8 @@ require (
17 github.com/go-git/go-git/v5 v5.4.2
18 github.com/matryer/is v1.2.0
19 github.com/muesli/reflow v0.3.0
20+ github.com/muesli/termenv v0.11.0
21+ github.com/sergi/go-diff v1.1.0 // indirect
22 golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
23 gopkg.in/yaml.v2 v2.4.0
24 )
25@@ -23,8 +26,8 @@ require (
26 github.com/Microsoft/go-winio v0.5.1 // indirect
27 github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f // indirect
28 github.com/acomagu/bufpipe v1.0.3 // indirect
29- github.com/alecthomas/chroma v0.10.0 // indirect
30 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
31+ github.com/atotto/clipboard v0.1.4 // indirect
32 github.com/aymerick/douceur v0.2.0 // indirect
33 github.com/charmbracelet/keygen v0.1.2 // indirect
34 github.com/containerd/console v1.0.3 // indirect
35@@ -42,10 +45,9 @@ require (
36 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
37 github.com/mitchellh/go-homedir v1.1.0 // indirect
38 github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
39- github.com/muesli/termenv v0.11.0 // indirect
40 github.com/olekukonko/tablewriter v0.0.5 // indirect
41 github.com/rivo/uniseg v0.2.0 // indirect
42- github.com/sergi/go-diff v1.2.0 // indirect
43+ github.com/sahilm/fuzzy v0.1.0 // indirect
44 github.com/xanzy/ssh-agent v0.3.1 // indirect
45 github.com/yuin/goldmark v1.4.4 // indirect
46 github.com/yuin/goldmark-emoji v1.0.1 // indirect
47diff --git a/go.sum b/go.sum
48index 5749f90ecd1569e09abab465c29ff7b08ecad062..12198bc644c5288da3d066fad5236b1f5a2240db 100644
49--- a/go.sum
50+++ b/go.sum
51@@ -15,13 +15,16 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
52 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
53 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
54 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
55-github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
56+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
57+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
58 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
59 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
60 github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k=
61 github.com/caarlos0/env/v6 v6.9.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
62-github.com/charmbracelet/bubbles v0.10.0 h1:ZYqBwnmFGp91HSRRbhxKq5jr6bUPsVUBdkrGGWtv0Wk=
63-github.com/charmbracelet/bubbles v0.10.0/go.mod h1:4tiDrWzH1MTD4t5NnrcthaedmI3MxU0FIutax7//dvk=
64+github.com/charmbracelet/bubbles v0.10.3-0.20220206060452-06358c35f974 h1:mdlhYomJ2+TRS+py0WxxqjDdyjvX5ox+CyrCjV2HaUM=
65+github.com/charmbracelet/bubbles v0.10.3-0.20220206060452-06358c35f974/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
66+github.com/charmbracelet/bubbles v0.10.3-0.20220208194203-1d489252fe50 h1:hAsXGdqKHVoEbBlvReSfz8X605xddHMBFSxSrCaSSO4=
67+github.com/charmbracelet/bubbles v0.10.3-0.20220208194203-1d489252fe50/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
68 github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
69 github.com/charmbracelet/bubbletea v0.19.4-0.20220208181305-42cd4c31919c h1:hcS4xdVQwblKo8xuA5gRO/jql+yCVfnBlOwWcZrxOmA=
70 github.com/charmbracelet/bubbletea v0.19.4-0.20220208181305-42cd4c31919c/go.mod h1:5nPeULOIxbAMykb3ggwhw1kruS7nP+Y4Za9yEH4J27U=
71@@ -30,7 +33,6 @@ github.com/charmbracelet/glamour v0.4.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5Uy
72 github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
73 github.com/charmbracelet/keygen v0.1.2 h1:Gr/gdIOjDIxCTRVXpwa9tsXPoJPS2eGNehPoMnZLvTQ=
74 github.com/charmbracelet/keygen v0.1.2/go.mod h1:kFQ3Cvop12fXWX1K29vxDxV9x8ujG4wBSXq//GySSSk=
75-github.com/charmbracelet/lipgloss v0.3.0/go.mod h1:VkhdBS2eNAmRkTwRKLJCFhCOVkjntMusBDxv7TXahuk=
76 github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
77 github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
78 github.com/charmbracelet/wish v0.2.1-0.20220208182816-534842b53d2a h1:dDdOcIedpXZ13xGfwFDd1ZlTUXotX945xXtz+7rHBK8=
79@@ -81,12 +83,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
80 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
81 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
82 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
83+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
84 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
85 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
86 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
87 github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
88 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
89-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
90 github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
91 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
92 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
93@@ -107,7 +109,6 @@ github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBc
94 github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
95 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
96 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
97-github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
98 github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
99 github.com/muesli/termenv v0.11.0 h1:fwNUbu2mfWlgicwG7qYzs06aOI8Z/zKPAv8J4uKbT+o=
100 github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
101@@ -122,10 +123,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
102 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
103 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
104 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
105+github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
106 github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
107+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
108 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
109-github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
110-github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
111 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
112 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
113 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
114diff --git a/internal/git/git.go b/internal/git/git.go
115index 936cf080cac33e780cf487639679b0b7fc1b2f5e..16931421dbddafa113dff031539d5b14fc901f1a 100644
116--- a/internal/git/git.go
117+++ b/internal/git/git.go
118@@ -25,6 +25,7 @@ type Repo struct {
119 Repository *git.Repository
120 Readme string
121 LastUpdated *time.Time
122+ commits CommitLog
123 }
124
125 // RepoCommit contains metadata for a Git commit.
126@@ -44,10 +45,9 @@ func (cl CommitLog) Less(i, j int) bool {
127
128 // RepoSource is a reference to an on-disk repositories.
129 type RepoSource struct {
130- Path string
131- mtx sync.Mutex
132- repos []*Repo
133- commits CommitLog
134+ Path string
135+ mtx sync.Mutex
136+ repos []*Repo
137 }
138
139 // NewRepoSource creates a new RepoSource.
140@@ -106,14 +106,14 @@ func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) {
141 return r, nil
142 }
143
144-// GetCommits returns commits for the repository.
145-func (rs *RepoSource) GetCommits(limit int) []RepoCommit {
146- rs.mtx.Lock()
147- defer rs.mtx.Unlock()
148- if limit > len(rs.commits) {
149- limit = len(rs.commits)
150+func (r *Repo) GetCommits(limit int) CommitLog {
151+ if limit <= 0 {
152+ return r.commits
153+ }
154+ if limit > len(r.commits) {
155+ limit = len(r.commits)
156 }
157- return rs.commits[:limit]
158+ return r.commits[:limit]
159 }
160
161 // LoadRepos opens Git repositories.
162@@ -125,7 +125,6 @@ func (rs *RepoSource) LoadRepos() error {
163 return err
164 }
165 rs.repos = make([]*Repo, 0)
166- rs.commits = make([]RepoCommit, 0)
167 for _, de := range rd {
168 rn := de.Name()
169 rg, err := git.PlainOpen(filepath.Join(rs.Path, rn))
170@@ -143,6 +142,7 @@ func (rs *RepoSource) LoadRepos() error {
171
172 func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
173 r := &Repo{Name: name}
174+ r.commits = make([]RepoCommit, 0)
175 r.Repository = rg
176 l, err := rg.Log(&git.LogOptions{All: true})
177 if err != nil {
178@@ -159,13 +159,13 @@ func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
179 }
180 }
181 }
182- rs.commits = append(rs.commits, RepoCommit{Name: name, Commit: c})
183+ r.commits = append(r.commits, RepoCommit{Name: name, Commit: c})
184 return nil
185 })
186 if err != nil {
187 return nil, err
188 }
189- sort.Sort(rs.commits)
190+ sort.Sort(r.commits)
191 return r, nil
192 }
193
194diff --git a/internal/tui/bubble.go b/internal/tui/bubble.go
195index c5071c4b836ed6e54388cb6238e42d29cab6a7ad..1adf41728ba4b9d584e30e8fb42fcf212be2a54b 100644
196--- a/internal/tui/bubble.go
197+++ b/internal/tui/bubble.go
198@@ -7,12 +7,17 @@ import (
199 tea "github.com/charmbracelet/bubbletea"
200 "github.com/charmbracelet/lipgloss"
201 "github.com/charmbracelet/soft-serve/internal/config"
202+ gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
203 "github.com/charmbracelet/soft-serve/internal/tui/bubbles/repo"
204 "github.com/charmbracelet/soft-serve/internal/tui/bubbles/selection"
205 "github.com/charmbracelet/soft-serve/internal/tui/style"
206 "github.com/gliderlabs/ssh"
207 )
208
209+const (
210+ repoNameMaxWidth = 32
211+)
212+
213 type sessionState int
214
215 const (
216@@ -83,14 +88,6 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
217 return b, tea.Quit
218 case "tab", "shift+tab":
219 b.activeBox = (b.activeBox + 1) % 2
220- case "h", "left":
221- if b.activeBox > 0 {
222- b.activeBox--
223- }
224- case "l", "right":
225- if b.activeBox < len(b.boxes)-1 {
226- b.activeBox++
227- }
228 }
229 case errMsg:
230 b.error = msg.Error()
231@@ -112,11 +109,8 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
232 case selection.SelectedMsg:
233 b.activeBox = 1
234 rb := b.repoMenu[msg.Index].bubble
235- rb.GotoTop()
236 b.boxes[1] = rb
237 case selection.ActiveMsg:
238- rb := b.repoMenu[msg.Index].bubble
239- rb.GotoTop()
240 b.boxes[1] = b.repoMenu[msg.Index].bubble
241 cmds = append(cmds, func() tea.Msg {
242 return b.lastResize
243@@ -163,27 +157,42 @@ func (b Bubble) headerView() string {
244
245 func (b Bubble) footerView() string {
246 w := &strings.Builder{}
247- var h []helpEntry
248- switch b.state {
249- case errorState:
250- h = []helpEntry{{"q", "quit"}}
251- default:
252- h = []helpEntry{
253+ var h []gittypes.HelpEntry
254+ if b.state != errorState {
255+ h = []gittypes.HelpEntry{
256 {"tab", "section"},
257- {"↑/↓", "navigate"},
258- {"q", "quit"},
259 }
260- if _, ok := b.boxes[b.activeBox].(*repo.Bubble); ok {
261- h = append(h[:2], helpEntry{"f/b", "pgdown/pgup"}, h[2])
262+ if box, ok := b.boxes[b.activeBox].(gittypes.HelpableBubble); ok {
263+ help := box.Help()
264+ for _, he := range help {
265+ h = append(h, he)
266+ }
267 }
268 }
269+ h = append(h, gittypes.HelpEntry{"q", "quit"})
270 for i, v := range h {
271- fmt.Fprint(w, v.Render(b.styles))
272+ fmt.Fprint(w, helpEntryRender(v, b.styles))
273 if i != len(h)-1 {
274 fmt.Fprint(w, b.styles.HelpDivider)
275 }
276 }
277- return b.styles.Footer.Copy().Width(b.width).Render(w.String())
278+ branch := ""
279+ if b.state == loadedState {
280+ branch = b.boxes[1].(*repo.Bubble).Reference().Short()
281+ }
282+ help := w.String()
283+ branchMaxWidth := b.width - // bubble width
284+ lipgloss.Width(help) - // help width
285+ b.styles.App.GetHorizontalFrameSize() // App paddings
286+ branch = b.styles.Branch.Render(gittypes.TruncateString(branch, branchMaxWidth-1, "…"))
287+ gap := lipgloss.NewStyle().
288+ Width(b.width -
289+ lipgloss.Width(help) -
290+ lipgloss.Width(branch) -
291+ b.styles.App.GetHorizontalFrameSize()).
292+ Render("")
293+ footer := lipgloss.JoinHorizontal(lipgloss.Top, help, gap, branch)
294+ return b.styles.Footer.Render(footer)
295 }
296
297 func (b Bubble) errorView() string {
298diff --git a/internal/tui/bubbles/commits/bubble.go b/internal/tui/bubbles/commits/bubble.go
299deleted file mode 100644
300index 4edc6a6c21e6a0a59c90695dfc6b1dc76e7aea2c..0000000000000000000000000000000000000000
301--- a/internal/tui/bubbles/commits/bubble.go
302+++ /dev/null
303@@ -1,67 +0,0 @@
304-package commits
305-
306-import (
307- "strings"
308-
309- "github.com/charmbracelet/bubbles/viewport"
310- tea "github.com/charmbracelet/bubbletea"
311- "github.com/charmbracelet/soft-serve/internal/git"
312- "github.com/dustin/go-humanize"
313-)
314-
315-type Bubble struct {
316- Commits []git.RepoCommit
317- Height int
318- Width int
319- viewport viewport.Model
320-}
321-
322-func NewBubble(height int, width int, rcs []git.RepoCommit) *Bubble {
323- b := &Bubble{
324- Commits: rcs,
325- viewport: viewport.Model{Height: height, Width: width},
326- }
327- s := ""
328- for _, rc := range rcs {
329- s += b.renderCommit(rc) + "\n"
330- }
331- b.viewport.SetContent(s)
332- return b
333-}
334-
335-func (b *Bubble) Init() tea.Cmd {
336- return nil
337-}
338-
339-func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
340- cmds := make([]tea.Cmd, 0)
341- switch msg := msg.(type) {
342- case tea.KeyMsg:
343- switch msg.String() {
344- case "up", "k":
345- b.viewport.LineUp(1)
346- case "down", "j":
347- b.viewport.LineDown(1)
348- }
349- }
350- return b, tea.Batch(cmds...)
351-}
352-
353-func (b *Bubble) renderCommit(rc git.RepoCommit) string {
354- s := ""
355- s += commitRepoNameStyle.Render(rc.Name)
356- s += " "
357- s += commitDateStyle.Render(humanize.Time(rc.Commit.Author.When))
358- s += "\n"
359- s += commitCommentStyle.Render(strings.TrimSpace(rc.Commit.Message))
360- s += "\n"
361- s += commitAuthorStyle.Render(rc.Commit.Author.Name)
362- s += " "
363- s += commitAuthorEmailStyle.Render(rc.Commit.Author.Email)
364- s += " "
365- return commitBoxStyle.Width(b.viewport.Width).Render(s)
366-}
367-
368-func (b *Bubble) View() string {
369- return b.viewport.View()
370-}
371diff --git a/internal/tui/bubbles/commits/style.go b/internal/tui/bubbles/commits/style.go
372deleted file mode 100644
373index dd2543acb875d62ff769efb5ab39bc204b183d22..0000000000000000000000000000000000000000
374--- a/internal/tui/bubbles/commits/style.go
375+++ /dev/null
376@@ -1,26 +0,0 @@
377-package commits
378-
379-import (
380- "github.com/charmbracelet/lipgloss"
381-)
382-
383-var commitBoxStyle = lipgloss.NewStyle().
384- Foreground(lipgloss.Color("#FFFFFF")).
385- BorderStyle(lipgloss.RoundedBorder()).
386- BorderForeground(lipgloss.Color("#670083")).
387- Padding(1)
388-var commitRepoNameStyle = lipgloss.NewStyle().
389- Foreground(lipgloss.Color("#8922A5"))
390-var commitAuthorStyle = lipgloss.NewStyle().
391- Foreground(lipgloss.Color("#670083"))
392-var commitAuthorEmailStyle = lipgloss.NewStyle().
393- Foreground(lipgloss.Color("#781194"))
394-var commitDateStyle = lipgloss.NewStyle().
395- Foreground(lipgloss.Color("#781194"))
396-var commitCommentStyle = lipgloss.NewStyle().
397- Foreground(lipgloss.Color("#A0A0A0")).
398- BorderStyle(lipgloss.Border{Left: ">"}).
399- BorderForeground(lipgloss.Color("#606060")).
400- PaddingLeft(1).
401- PaddingBottom(0).
402- Margin(0)
403diff --git a/internal/tui/bubbles/git/about/bubble.go b/internal/tui/bubbles/git/about/bubble.go
404new file mode 100644
405index 0000000000000000000000000000000000000000..07fa8a8c1775e571f0266437e7152d90e1af767a
406--- /dev/null
407+++ b/internal/tui/bubbles/git/about/bubble.go
408@@ -0,0 +1,121 @@
409+package about
410+
411+import (
412+ "github.com/charmbracelet/bubbles/viewport"
413+ tea "github.com/charmbracelet/bubbletea"
414+ "github.com/charmbracelet/glamour"
415+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
416+ vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
417+ "github.com/charmbracelet/soft-serve/internal/tui/style"
418+ "github.com/muesli/reflow/wrap"
419+)
420+
421+type Bubble struct {
422+ readmeViewport *vp.ViewportBubble
423+ repo types.Repo
424+ styles *style.Styles
425+ height int
426+ heightMargin int
427+ width int
428+ widthMargin int
429+}
430+
431+func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
432+ b := &Bubble{
433+ readmeViewport: &vp.ViewportBubble{
434+ Viewport: &viewport.Model{},
435+ },
436+ repo: repo,
437+ styles: styles,
438+ widthMargin: wm,
439+ heightMargin: hm,
440+ }
441+ b.SetSize(width, height)
442+ return b
443+}
444+func (b *Bubble) Init() tea.Cmd {
445+ return b.setupCmd
446+}
447+
448+func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
449+ var cmds []tea.Cmd
450+ switch msg := msg.(type) {
451+ case tea.WindowSizeMsg:
452+ b.SetSize(msg.Width, msg.Height)
453+ // XXX: if we find that longer readmes take more than a few
454+ // milliseconds to render we may need to move Glamour rendering into a
455+ // command.
456+ md, err := b.glamourize(b.repo.GetReadme())
457+ if err != nil {
458+ return b, nil
459+ }
460+ b.readmeViewport.Viewport.SetContent(md)
461+ case tea.KeyMsg:
462+ switch msg.String() {
463+ case "A":
464+ b.GotoTop()
465+ }
466+ }
467+ rv, cmd := b.readmeViewport.Update(msg)
468+ b.readmeViewport = rv.(*vp.ViewportBubble)
469+ cmds = append(cmds, cmd)
470+ return b, tea.Batch(cmds...)
471+}
472+
473+func (b *Bubble) SetSize(w, h int) {
474+ b.width = w
475+ b.height = h
476+ b.readmeViewport.Viewport.Width = w - b.widthMargin
477+ b.readmeViewport.Viewport.Height = h - b.heightMargin
478+}
479+
480+func (b *Bubble) GotoTop() {
481+ b.readmeViewport.Viewport.GotoTop()
482+}
483+
484+func (b *Bubble) View() string {
485+ return b.readmeViewport.View()
486+}
487+
488+func (b *Bubble) Help() []types.HelpEntry {
489+ return []types.HelpEntry{
490+ {"f/b", "pgup/pgdown"},
491+ }
492+}
493+
494+func (b *Bubble) setupCmd() tea.Msg {
495+ md, err := b.glamourize(b.repo.GetReadme())
496+ if err != nil {
497+ return types.ErrMsg{err}
498+ }
499+ b.readmeViewport.Viewport.SetContent(md)
500+ b.GotoTop()
501+ return nil
502+}
503+
504+func (b *Bubble) glamourize(md string) (string, error) {
505+ w := b.width - b.widthMargin - b.styles.RepoBody.GetHorizontalFrameSize()
506+ if w > types.GlamourMaxWidth {
507+ w = types.GlamourMaxWidth
508diff --git a/internal/tui/bubbles/git/bubble.go b/internal/tui/bubbles/git/bubble.go
509new file mode 100644
510index 0000000000000000000000000000000000000000..c6780f26ab56b0015159abb9bc8772fdab59da1c
511--- /dev/null
512+++ b/internal/tui/bubbles/git/bubble.go
513@@ -0,0 +1,157 @@
514+package git
515+
516+import (
517+ tea "github.com/charmbracelet/bubbletea"
518+ "github.com/charmbracelet/lipgloss"
519+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/about"
520+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/log"
521+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
522+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/tree"
523+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
524+ "github.com/charmbracelet/soft-serve/internal/tui/style"
525+ "github.com/go-git/go-git/v5/plumbing"
526+)
527+
528+const (
529+ repoNameMaxWidth = 32
530+)
531+
532+type pageState int
533+
534+const (
535+ aboutPage pageState = iota
536+ refsPage
537+ logPage
538+ treePage
539+)
540+
541+type Bubble struct {
542+ state pageState
543+ repo types.Repo
544+ height int
545+ heightMargin int
546+ width int
547+ widthMargin int
548+ style *style.Styles
549+ boxes []tea.Model
550+}
551+
552+func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
553+ b := &Bubble{
554+ repo: repo,
555+ state: aboutPage,
556+ width: width,
557+ widthMargin: wm,
558+ height: height,
559+ heightMargin: hm,
560+ style: styles,
561+ boxes: make([]tea.Model, 4),
562+ }
563+ heightMargin := hm + lipgloss.Height(b.headerView())
564+ b.boxes[aboutPage] = about.NewBubble(repo, b.style, b.width, wm, b.height, heightMargin)
565+ b.boxes[refsPage] = refs.NewBubble(repo, b.style, b.width, wm, b.height, heightMargin)
566+ b.boxes[logPage] = log.NewBubble(repo, b.style, width, wm, height, heightMargin)
567+ b.boxes[treePage] = tree.NewBubble(repo, b.style, width, wm, height, heightMargin)
568+ return b
569+}
570+
571+func (b *Bubble) Init() tea.Cmd {
572+ return b.setupCmd
573+}
574+
575+func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
576+ cmds := make([]tea.Cmd, 0)
577+ switch msg := msg.(type) {
578+ case tea.KeyMsg:
579+ switch msg.String() {
580+ case "A":
581+ b.state = aboutPage
582+ case "R":
583+ b.state = refsPage
584+ case "L":
585+ b.state = logPage
586+ case "T":
587+ b.state = treePage
588+ }
589+ case tea.WindowSizeMsg:
590+ b.width = msg.Width
591+ b.height = msg.Height
592+ for i, bx := range b.boxes {
593+ m, cmd := bx.Update(msg)
594+ b.boxes[i] = m
595+ if cmd != nil {
596+ cmds = append(cmds, cmd)
597+ }
598+ }
599+ }
600+ m, cmd := b.boxes[b.state].Update(msg)
601+ b.boxes[b.state] = m
602+ if cmd != nil {
603+ cmds = append(cmds, cmd)
604+ }
605+
606+ switch msg := msg.(type) {
607+ case tea.KeyMsg:
608+ switch msg.String() {
609+ case "enter":
610+ if b.state == refsPage {
611+ b.state = treePage
612+ cmds = append(cmds, b.boxes[b.state].Init())
613diff --git a/internal/tui/bubbles/git/log/bubble.go b/internal/tui/bubbles/git/log/bubble.go
614new file mode 100644
615index 0000000000000000000000000000000000000000..ccee7889ceb2c211982faf8815dccbd74f2c691a
616--- /dev/null
617+++ b/internal/tui/bubbles/git/log/bubble.go
618@@ -0,0 +1,394 @@
619+package log
620+
621+import (
622+ "context"
623+ "fmt"
624+ "io"
625+ "math"
626+ "strings"
627+ "time"
628+
629+ "github.com/charmbracelet/bubbles/list"
630+ "github.com/charmbracelet/bubbles/viewport"
631+ tea "github.com/charmbracelet/bubbletea"
632+ gansi "github.com/charmbracelet/glamour/ansi"
633+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
634+ vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
635+ "github.com/charmbracelet/soft-serve/internal/tui/style"
636+ "github.com/dustin/go-humanize/english"
637+ "github.com/go-git/go-git/v5/plumbing/object"
638+)
639+
640+var (
641+ diffChroma = &gansi.CodeBlockElement{
642+ Code: "",
643+ Language: "diff",
644+ }
645+)
646+
647+type commitMsg struct {
648+ commit *object.Commit
649+ parent *object.Commit
650+ tree *object.Tree
651+ parentTree *object.Tree
652+ patch *object.Patch
653+}
654+
655+type sessionState int
656+
657+const (
658+ logState sessionState = iota
659+ commitState
660+ errorState
661+)
662+
663+type item struct {
664+ *types.Commit
665+}
666+
667+func (i item) Title() string {
668+ lines := strings.Split(i.Message, "\n")
669+ if len(lines) > 0 {
670+ return lines[0]
671+ }
672+ return ""
673+}
674+
675+func (i item) FilterValue() string { return i.Title() }
676+
677+type itemDelegate struct {
678+ style *style.Styles
679+}
680+
681+func (d itemDelegate) Height() int { return 1 }
682+func (d itemDelegate) Spacing() int { return 0 }
683+func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
684+func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
685+ i, ok := listItem.(item)
686+ if !ok {
687+ return
688+ }
689+
690+ leftMargin := d.style.LogItemSelector.GetMarginLeft() +
691+ d.style.LogItemSelector.GetWidth() +
692+ d.style.LogItemHash.GetMarginLeft() +
693+ d.style.LogItemHash.GetWidth() +
694+ d.style.LogItemInactive.GetMarginLeft()
695+ title := types.TruncateString(i.Title(), m.Width()-leftMargin, "…")
696+ if index == m.Index() {
697+ fmt.Fprint(w, d.style.LogItemSelector.Render(">")+
698+ d.style.LogItemHash.Bold(true).Render(i.Hash.String()[:7])+
699+ d.style.LogItemActive.Render(title))
700+ } else {
701+ fmt.Fprint(w, d.style.LogItemSelector.Render(" ")+
702+ d.style.LogItemHash.Render(i.Hash.String()[:7])+
703+ d.style.LogItemInactive.Render(title))
704+ }
705+}
706+
707+type Bubble struct {
708+ repo types.Repo
709+ list list.Model
710+ state sessionState
711+ commitViewport *vp.ViewportBubble
712+ style *style.Styles
713+ width int
714+ widthMargin int
715+ height int
716+ heightMargin int
717+ error types.ErrMsg
718diff --git a/internal/tui/bubbles/git/refs/bubble.go b/internal/tui/bubbles/git/refs/bubble.go
719new file mode 100644
720index 0000000000000000000000000000000000000000..543410b4741d81b021ddf09b90b794eb0fa24460
721--- /dev/null
722+++ b/internal/tui/bubbles/git/refs/bubble.go
723@@ -0,0 +1,174 @@
724+package refs
725+
726+import (
727+ "fmt"
728+ "io"
729+ "sort"
730+
731+ "github.com/charmbracelet/bubbles/list"
732+ tea "github.com/charmbracelet/bubbletea"
733+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
734+ "github.com/charmbracelet/soft-serve/internal/tui/style"
735+ "github.com/go-git/go-git/v5/plumbing"
736+)
737+
738+type item struct {
739+ *plumbing.Reference
740+}
741+
742+func (i item) Short() string {
743+ return i.Name().Short()
744+}
745+
746+func (i item) FilterValue() string { return i.Short() }
747+
748+type items []item
749+
750+func (cl items) Len() int { return len(cl) }
751+func (cl items) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
752+func (cl items) Less(i, j int) bool {
753+ return cl[i].Name().Short() < cl[j].Name().Short()
754+}
755+
756+type itemDelegate struct {
757+ style *style.Styles
758+}
759+
760+func (d itemDelegate) Height() int { return 1 }
761+func (d itemDelegate) Spacing() int { return 0 }
762+func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
763+func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
764+ s := d.style
765+ i, ok := listItem.(item)
766+ if !ok {
767+ return
768+ }
769+
770+ ref := i.Short()
771+ if i.Name().IsTag() {
772+ ref = s.RefItemTag.Render(ref)
773+ }
774+ ref = s.RefItemBranch.Render(ref)
775+ refMaxWidth := m.Width() -
776+ s.RefItemSelector.GetMarginLeft() -
777+ s.RefItemSelector.GetWidth() -
778+ s.RefItemInactive.GetMarginLeft()
779+ ref = types.TruncateString(ref, refMaxWidth, "…")
780+ if index == m.Index() {
781+ fmt.Fprint(w, s.RefItemSelector.Render(">")+
782+ s.RefItemActive.Render(ref))
783+ } else {
784+ fmt.Fprint(w, s.LogItemSelector.Render(" ")+
785+ s.RefItemInactive.Render(ref))
786+ }
787+}
788+
789+type Bubble struct {
790+ repo types.Repo
791+ list list.Model
792+ style *style.Styles
793+ width int
794+ widthMargin int
795+ height int
796+ heightMargin int
797+}
798+
799+func NewBubble(repo types.Repo, style *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
800+ l := list.NewModel([]list.Item{}, itemDelegate{style}, width-widthMargin, height-heightMargin)
801+ l.SetShowFilter(false)
802+ l.SetShowHelp(false)
803+ l.SetShowPagination(false)
804+ l.SetShowStatusBar(false)
805+ l.SetShowTitle(false)
806+ l.SetFilteringEnabled(false)
807+ l.DisableQuitKeybindings()
808+ b := &Bubble{
809+ repo: repo,
810+ style: style,
811+ width: width,
812+ height: height,
813+ widthMargin: widthMargin,
814+ heightMargin: heightMargin,
815+ list: l,
816+ }
817+ b.SetSize(width, height)
818+ return b
819+}
820+
821+func (b *Bubble) SetBranch(ref *plumbing.Reference) {
822+ b.repo.SetReference(ref)
823diff --git a/internal/tui/bubbles/git/tree/bubble.go b/internal/tui/bubbles/git/tree/bubble.go
824new file mode 100644
825index 0000000000000000000000000000000000000000..e69bb28d30bde231ec530e1abf68e8121f1553c6
826--- /dev/null
827+++ b/internal/tui/bubbles/git/tree/bubble.go
828@@ -0,0 +1,355 @@
829+package tree
830+
831+import (
832+ "fmt"
833+ "io"
834+ "path/filepath"
835+ "sort"
836+ "strings"
837+
838+ "github.com/alecthomas/chroma/lexers"
839+ "github.com/charmbracelet/bubbles/list"
840+ "github.com/charmbracelet/bubbles/viewport"
841+ tea "github.com/charmbracelet/bubbletea"
842+ gansi "github.com/charmbracelet/glamour/ansi"
843+ "github.com/charmbracelet/lipgloss"
844+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
845+ vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
846+ "github.com/charmbracelet/soft-serve/internal/tui/style"
847+ "github.com/dustin/go-humanize"
848+ "github.com/go-git/go-git/v5/plumbing"
849+ "github.com/go-git/go-git/v5/plumbing/filemode"
850+ "github.com/go-git/go-git/v5/plumbing/object"
851+)
852+
853+type fileMsg struct {
854+ content string
855+}
856+
857+type sessionState int
858+
859+const (
860+ treeState sessionState = iota
861+ fileState
862+ errorState
863+)
864+
865+type item struct {
866+ *object.TreeEntry
867+ *object.File
868+}
869+
870+func (i item) Name() string {
871+ return i.TreeEntry.Name
872+}
873+
874+func (i item) Mode() filemode.FileMode {
875+ return i.TreeEntry.Mode
876+}
877+
878+func (i item) FilterValue() string { return i.Name() }
879+
880+type items []item
881+
882+func (cl items) Len() int { return len(cl) }
883+func (cl items) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
884+func (cl items) Less(i, j int) bool {
885+ if cl[i].Mode() == filemode.Dir && cl[j].Mode() == filemode.Dir {
886+ return cl[i].Name() < cl[j].Name()
887+ } else if cl[i].Mode() == filemode.Dir {
888+ return true
889+ } else if cl[j].Mode() == filemode.Dir {
890+ return false
891+ } else {
892+ return cl[i].Name() < cl[j].Name()
893+ }
894+}
895+
896+type itemDelegate struct {
897+ style *style.Styles
898+}
899+
900+func (d itemDelegate) Height() int { return 1 }
901+func (d itemDelegate) Spacing() int { return 0 }
902+func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
903+func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
904+ s := d.style
905+ i, ok := listItem.(item)
906+ if !ok {
907+ return
908+ }
909+
910+ name := i.Name()
911+ if i.Mode() == filemode.Dir {
912+ name = s.TreeFileDir.Render(name)
913+ }
914+ size := ""
915+ if i.File != nil {
916+ size = humanize.Bytes(uint64(i.File.Size))
917+ }
918+ var cs lipgloss.Style
919+ mode, _ := i.Mode().ToOSFileMode()
920+ if index == m.Index() {
921+ cs = s.TreeItemActive
922+ fmt.Fprint(w, s.TreeItemSelector.Render(">"))
923+ } else {
924+ cs = s.TreeItemInactive
925+ fmt.Fprint(w, s.TreeItemSelector.Render(" "))
926+ }
927+ leftMargin := s.TreeItemSelector.GetMarginLeft() +
928diff --git a/internal/tui/bubbles/git/types/consts.go b/internal/tui/bubbles/git/types/consts.go
929new file mode 100644
930index 0000000000000000000000000000000000000000..940938638aa3082404ce339b8cdfbad01c52f7a2
931--- /dev/null
932+++ b/internal/tui/bubbles/git/types/consts.go
933@@ -0,0 +1,28 @@
934+package types
935+
936+import (
937+ "time"
938+
939+ "github.com/charmbracelet/bubbles/key"
940+)
941+
942+// Some constants were copied from https://docs.gitea.io/en-us/config-cheat-sheet/#git-git
943+
944+const (
945+ GlamourMaxWidth = 120
946+ RepoNameMaxWidth = 32
947+ MaxDiffLines = 1000
948+ MaxDiffFiles = 100
949+ MaxPatchWait = time.Second * 3
950+)
951+
952+var (
953+ PrevPage = key.NewBinding(
954+ key.WithKeys("pgup", "b", "u"),
955+ key.WithHelp("pgup", "prev page"),
956+ )
957+ NextPage = key.NewBinding(
958+ key.WithKeys("pgdown", "f", "d"),
959+ key.WithHelp("pgdn", "next page"),
960+ )
961+)
962diff --git a/internal/tui/bubbles/git/types/error.go b/internal/tui/bubbles/git/types/error.go
963new file mode 100644
964index 0000000000000000000000000000000000000000..0e2cf1e4621a00626bba67d2eda23e5ab231c158
965--- /dev/null
966+++ b/internal/tui/bubbles/git/types/error.go
967@@ -0,0 +1,36 @@
968+package types
969+
970+import (
971+ "errors"
972+
973+ "github.com/charmbracelet/lipgloss"
974+ "github.com/charmbracelet/soft-serve/internal/tui/style"
975+)
976+
977+var (
978+ ErrDiffTooLong = errors.New("diff is too long")
979+ ErrDiffFilesTooLong = errors.New("diff files are too long")
980+ ErrBinaryFile = errors.New("binary file")
981+ ErrFileTooLarge = errors.New("file is too large")
982+ ErrInvalidFile = errors.New("invalid file")
983+)
984+
985+type ErrMsg struct {
986+ Err error
987+}
988+
989+func (e ErrMsg) Error() string {
990+ return e.Err.Error()
991+}
992+
993+func (e ErrMsg) View(s *style.Styles) string {
994+ return e.ViewWithPrefix(s, "")
995+}
996+
997+func (e ErrMsg) ViewWithPrefix(s *style.Styles, prefix string) string {
998+ return lipgloss.JoinHorizontal(
999+ lipgloss.Top,
1000+ s.ErrorTitle.Render(prefix),
1001+ s.ErrorBody.Render(e.Error()),
1002+ )
1003+}
1004diff --git a/internal/tui/bubbles/git/types/formatter.go b/internal/tui/bubbles/git/types/formatter.go
1005new file mode 100644
1006index 0000000000000000000000000000000000000000..7356581d19a61ada2c184b25d364e8c7c8b85ccb
1007--- /dev/null
1008+++ b/internal/tui/bubbles/git/types/formatter.go
1009@@ -0,0 +1,36 @@
1010+package types
1011+
1012+import (
1013+ "github.com/charmbracelet/glamour"
1014+ gansi "github.com/charmbracelet/glamour/ansi"
1015+ "github.com/muesli/termenv"
1016+)
1017+
1018+var (
1019+ RenderCtx = DefaultRenderCtx()
1020+ Styles = DefaultStyles()
1021+)
1022+
1023+func DefaultStyles() gansi.StyleConfig {
1024+ noColor := ""
1025+ s := glamour.DarkStyleConfig
1026+ s.Document.StylePrimitive.Color = &noColor
1027+ s.CodeBlock.Chroma.Text.Color = &noColor
1028+ s.CodeBlock.Chroma.Name.Color = &noColor
1029+ return s
1030+}
1031+
1032+func DefaultRenderCtx() gansi.RenderContext {
1033+ return gansi.NewRenderContext(gansi.Options{
1034+ ColorProfile: termenv.TrueColor,
1035+ Styles: DefaultStyles(),
1036+ })
1037+}
1038+
1039+func NewRenderCtx(worldwrap int) gansi.RenderContext {
1040+ return gansi.NewRenderContext(gansi.Options{
1041+ ColorProfile: termenv.TrueColor,
1042+ Styles: DefaultStyles(),
1043+ WordWrap: worldwrap,
1044+ })
1045+}
1046diff --git a/internal/tui/bubbles/git/types/git.go b/internal/tui/bubbles/git/types/git.go
1047new file mode 100644
1048index 0000000000000000000000000000000000000000..8e0213871b9bc7e868e15dfa9fd87b5a44fb88e7
1049--- /dev/null
1050+++ b/internal/tui/bubbles/git/types/git.go
1051@@ -0,0 +1,29 @@
1052+package types
1053+
1054+import (
1055+ "github.com/go-git/go-git/v5"
1056+ "github.com/go-git/go-git/v5/plumbing"
1057+ "github.com/go-git/go-git/v5/plumbing/object"
1058+)
1059+
1060+type Repo interface {
1061+ Name() string
1062+ GetReference() *plumbing.Reference
1063+ SetReference(*plumbing.Reference) error
1064+ GetReadme() string
1065+ GetCommits(limit int) (Commits, error)
1066+ Repository() *git.Repository
1067+ Tree(path string) (*object.Tree, error)
1068+}
1069+
1070+type Commit struct {
1071+ *object.Commit
1072+}
1073+
1074+type Commits []*Commit
1075+
1076+func (cl Commits) Len() int { return len(cl) }
1077+func (cl Commits) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
1078+func (cl Commits) Less(i, j int) bool {
1079+ return cl[i].Author.When.After(cl[j].Author.When)
1080+}
1081diff --git a/internal/tui/bubbles/git/types/help.go b/internal/tui/bubbles/git/types/help.go
1082new file mode 100644
1083index 0000000000000000000000000000000000000000..ae04818c76b15bd1914478fb5f2a45e7f23f0b04
1084--- /dev/null
1085+++ b/internal/tui/bubbles/git/types/help.go
1086@@ -0,0 +1,10 @@
1087+package types
1088+
1089+type HelpableBubble interface {
1090+ Help() []HelpEntry
1091+}
1092+
1093+type HelpEntry struct {
1094+ Key string
1095+ Value string
1096+}
1097diff --git a/internal/tui/bubbles/git/types/utils.go b/internal/tui/bubbles/git/types/utils.go
1098new file mode 100644
1099index 0000000000000000000000000000000000000000..c1758097d235186a24d120676b42b667604f8fff
1100--- /dev/null
1101+++ b/internal/tui/bubbles/git/types/utils.go
1102@@ -0,0 +1,17 @@
1103+package types
1104+
1105+import "github.com/muesli/reflow/truncate"
1106+
1107+func TruncateString(s string, max int, tail string) string {
1108+ if max < 0 {
1109+ max = 0
1110+ }
1111+ return truncate.StringWithTail(s, uint(max), tail)
1112+}
1113+
1114+func Max(a, b int) int {
1115+ if a > b {
1116+ return a
1117+ }
1118+ return b
1119+}
1120diff --git a/internal/tui/bubbles/repo/viewport_patch.go b/internal/tui/bubbles/git/viewport/viewport_patch.go
1121rename from internal/tui/bubbles/repo/viewport_patch.go
1122rename to internal/tui/bubbles/git/viewport/viewport_patch.go
1123index df176ebebe429decac912802b7e98c21476a8d64..4163b5c40ee90025690f1f4d97d01ac12d310584 100644
1124--- a/internal/tui/bubbles/repo/viewport_patch.go
1125+++ b/internal/tui/bubbles/git/viewport/viewport_patch.go
1126@@ -1,4 +1,4 @@
1127-package repo
1128+package viewport
1129
1130 import (
1131 "github.com/charmbracelet/bubbles/viewport"
1132diff --git a/internal/tui/bubbles/repo/bubble.go b/internal/tui/bubbles/repo/bubble.go
1133index 12f0ebbc237961c2510c3e6a0f72b07429e5bf82..d96d3b10ca718fa5689fe5307fffd32c1bf7c8d7 100644
1134--- a/internal/tui/bubbles/repo/bubble.go
1135+++ b/internal/tui/bubbles/repo/bubble.go
1136@@ -1,111 +1,71 @@
1137 package repo
1138
1139 import (
1140- "bytes"
1141 "fmt"
1142 "strconv"
1143- "text/template"
1144
1145- "github.com/charmbracelet/bubbles/viewport"
1146 tea "github.com/charmbracelet/bubbletea"
1147- "github.com/charmbracelet/glamour"
1148- "github.com/charmbracelet/glamour/ansi"
1149 "github.com/charmbracelet/lipgloss"
1150- "github.com/charmbracelet/soft-serve/internal/git"
1151+ gitui "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git"
1152+ gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1153 "github.com/charmbracelet/soft-serve/internal/tui/style"
1154+ "github.com/go-git/go-git/v5/plumbing"
1155 "github.com/muesli/reflow/truncate"
1156 "github.com/muesli/reflow/wrap"
1157 )
1158
1159 const (
1160- glamourMaxWidth = 120
1161 repoNameMaxWidth = 32
1162 )
1163
1164-var glamourStyle = func() ansi.StyleConfig {
1165- noColor := ""
1166- s := glamour.DarkStyleConfig
1167- s.Document.StylePrimitive.Color = &noColor
1168- s.CodeBlock.Chroma.Text.Color = &noColor
1169- s.CodeBlock.Chroma.Name.Color = &noColor
1170- return s
1171-}()
1172-
1173-type ErrMsg struct {
1174- Error error
1175-}
1176-
1177 type Bubble struct {
1178- templateObject interface{}
1179- repoSource *git.RepoSource
1180- name string
1181- repo *git.Repo
1182- styles *style.Styles
1183- readmeViewport *ViewportBubble
1184- readme string
1185- height int
1186- heightMargin int
1187- width int
1188- widthMargin int
1189- Active bool
1190-
1191- // XXX: ideally, we get these from the parent as a pointer. Currently, we
1192- // can't add a *tui.Config because it's an illegal import cycle. One
1193- // solution would be to (rename and) move this Bubble into the parent
1194- // package.
1195- Host string
1196- Port int
1197+ name string
1198+ host string
1199+ port int
1200+ repo gittypes.Repo
1201+ styles *style.Styles
1202+ width int
1203+ widthMargin int
1204+ height int
1205+ heightMargin int
1206+ box *gitui.Bubble
1207+
1208+ Active bool
1209 }
1210
1211-func NewBubble(rs *git.RepoSource, name string, styles *style.Styles, width, wm, height, hm int, tmp interface{}) *Bubble {
1212+func NewBubble(name, host string, port int, repo gittypes.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
1213 b := &Bubble{
1214- templateObject: tmp,
1215- repoSource: rs,
1216- name: name,
1217- styles: styles,
1218- heightMargin: hm,
1219- widthMargin: wm,
1220- readmeViewport: &ViewportBubble{
1221- Viewport: &viewport.Model{},
1222- },
1223- }
1224- b.SetSize(width, height)
1225+ name: name,
1226+ host: host,
1227+ port: port,
1228+ repo: repo,
1229+ width: width,
1230+ widthMargin: wm,
1231+ height: height,
1232+ heightMargin: hm,
1233+ styles: styles,
1234+ }
1235+ b.box = gitui.NewBubble(repo, styles, width, wm+styles.RepoBody.GetHorizontalBorderSize(), height, hm+lipgloss.Height(b.headerView())-styles.RepoBody.GetVerticalBorderSize())
1236diff --git a/internal/tui/bubbles/selection/bubble.go b/internal/tui/bubbles/selection/bubble.go
1237index 6a8a59342651ac3ebfc0056a9328b17a0b75beb0..26730dd214f19f720e65b15cada8d88a3becb44f 100644
1238--- a/internal/tui/bubbles/selection/bubble.go
1239+++ b/internal/tui/bubbles/selection/bubble.go
1240@@ -5,6 +5,7 @@ import (
1241
1242 tea "github.com/charmbracelet/bubbletea"
1243 "github.com/charmbracelet/lipgloss"
1244+ gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1245 "github.com/charmbracelet/soft-serve/internal/tui/style"
1246 "github.com/muesli/reflow/truncate"
1247 )
1248@@ -79,6 +80,12 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
1249 return b, tea.Batch(cmds...)
1250 }
1251
1252+func (b *Bubble) Help() []gittypes.HelpEntry {
1253+ return []gittypes.HelpEntry{
1254+ {"↑/↓", "navigate"},
1255+ }
1256+}
1257+
1258 func (b *Bubble) sendActiveMessage() tea.Msg {
1259 if b.SelectedItem >= 0 && b.SelectedItem < len(b.Items) {
1260 return ActiveMsg{
1261diff --git a/internal/tui/commands.go b/internal/tui/commands.go
1262index 6b98ca7d6d32279c8c27ab7e0826c1d3dde70e2a..d8879e0873903f25884d9d6a1f33a66b191deeeb 100644
1263--- a/internal/tui/commands.go
1264+++ b/internal/tui/commands.go
1265@@ -1,12 +1,14 @@
1266 package tui
1267
1268 import (
1269+ "bytes"
1270 "fmt"
1271+ "text/template"
1272
1273 tea "github.com/charmbracelet/bubbletea"
1274 "github.com/charmbracelet/lipgloss"
1275- "github.com/charmbracelet/soft-serve/internal/config"
1276- br "github.com/charmbracelet/soft-serve/internal/tui/bubbles/repo"
1277+ gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1278+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/repo"
1279 "github.com/charmbracelet/soft-serve/internal/tui/bubbles/selection"
1280 gm "github.com/charmbracelet/wish/git"
1281 )
1282@@ -93,37 +95,54 @@ func (b *Bubble) menuEntriesFromSource() ([]MenuEntry, error) {
1283 return mes, nil
1284 }
1285
1286-func (b *Bubble) newMenuEntry(name string, repo string) (MenuEntry, error) {
1287- var tmplConfig *config.Config
1288- if repo == "config" {
1289- tmplConfig = b.config
1290+func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
1291+ gr := &Repo{
1292+ name: rn,
1293+ }
1294+ me := MenuEntry{Name: name, Repo: rn}
1295+ r, err := b.config.Source.GetRepo(rn)
1296+ if err != nil {
1297+ return me, err
1298+ }
1299+ if rn == "config" {
1300+ md, err := templatize(r.Readme, b.config)
1301+ if err != nil {
1302+ return me, err
1303+ }
1304+ r.Readme = md
1305+ }
1306+ gr.repo = r.Repository
1307+ gr.readme = r.Readme
1308+ gr.ref, err = r.Repository.Head()
1309+ if err != nil {
1310+ return me, err
1311 }
1312- me := MenuEntry{Name: name, Repo: repo}
1313- width := b.width
1314 boxLeftWidth := b.styles.Menu.GetWidth() + b.styles.Menu.GetHorizontalFrameSize()
1315 // TODO: also send this along with a tea.WindowSizeMsg
1316 var heightMargin = lipgloss.Height(b.headerView()) +
1317 lipgloss.Height(b.footerView()) +
1318 b.styles.RepoBody.GetVerticalFrameSize() +
1319 b.styles.App.GetVerticalMargins()
1320- rb := br.NewBubble(
1321- b.config.Source,
1322- me.Repo,
1323- b.styles,
1324- width,
1325- boxLeftWidth,
1326- b.height,
1327- heightMargin,
1328- tmplConfig,
1329- )
1330- rb.Host = b.config.Host
1331- rb.Port = b.config.Port
1332+ rb := repo.NewBubble(rn, b.config.Host, b.config.Port, gr, b.styles, b.width, boxLeftWidth, b.height, heightMargin)
1333 initCmd := rb.Init()
1334 msg := initCmd()
1335 switch msg := msg.(type) {
1336- case br.ErrMsg:
1337- return me, fmt.Errorf("missing %s: %s", me.Repo, msg.Error)
1338+ case gitypes.ErrMsg:
1339+ return me, fmt.Errorf("missing %s: %s", me.Repo, msg.Err.Error())
1340 }
1341 me.bubble = rb
1342 return me, nil
1343 }
1344+
1345+func templatize(mdt string, tmpl interface{}) (string, error) {
1346+ t, err := template.New("readme").Parse(mdt)
1347+ if err != nil {
1348+ return "", err
1349+ }
1350+ buf := &bytes.Buffer{}
1351+ err = t.Execute(buf, tmpl)
1352+ if err != nil {
1353+ return "", err
1354+ }
1355+ return buf.String(), nil
1356+}
1357diff --git a/internal/tui/git.go b/internal/tui/git.go
1358new file mode 100644
1359index 0000000000000000000000000000000000000000..f8105a695317d734ddb4502ee04cb34dd3253968
1360--- /dev/null
1361+++ b/internal/tui/git.go
1362@@ -0,0 +1,105 @@
1363+package tui
1364+
1365+import (
1366+ "path/filepath"
1367+
1368+ gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1369+ "github.com/go-git/go-git/v5"
1370+ "github.com/go-git/go-git/v5/plumbing"
1371+ "github.com/go-git/go-git/v5/plumbing/object"
1372+)
1373+
1374+type Repo struct {
1375+ name string
1376+ repo *git.Repository
1377+ readme string
1378+ ref *plumbing.Reference
1379+}
1380+
1381+func (r *Repo) Name() string {
1382+ return r.name
1383+}
1384+
1385+func (r *Repo) GetReference() *plumbing.Reference {
1386+ return r.ref
1387+}
1388+
1389+func (r *Repo) SetReference(ref *plumbing.Reference) error {
1390+ r.ref = ref
1391+ return nil
1392+}
1393+
1394+func (r *Repo) Repository() *git.Repository {
1395+ return r.repo
1396+}
1397+
1398+func (r *Repo) Tree(path string) (*object.Tree, error) {
1399+ path = filepath.Clean(path)
1400+ c, err := r.repo.CommitObject(r.ref.Hash())
1401+ if err != nil {
1402+ return nil, err
1403+ }
1404+ t, err := c.Tree()
1405+ if err != nil {
1406+ return nil, err
1407+ }
1408+ if path == "." {
1409+ return t, nil
1410+ }
1411+ return t.Tree(path)
1412+}
1413+
1414+func (r *Repo) GetCommits(limit int) (gitypes.Commits, error) {
1415+ commits := gitypes.Commits{}
1416+ l, err := r.repo.Log(&git.LogOptions{
1417+ Order: git.LogOrderCommitterTime,
1418+ From: r.ref.Hash(),
1419+ })
1420+ if err != nil {
1421+ return nil, err
1422+ }
1423+ err = l.ForEach(func(c *object.Commit) error {
1424+ commits = append(commits, &gitypes.Commit{c})
1425+ return nil
1426+ })
1427+ if err != nil {
1428+ return nil, err
1429+ }
1430+ if limit <= 0 || limit > len(commits) {
1431+ limit = len(commits)
1432+ }
1433+ return commits[:limit], nil
1434+}
1435+
1436+func (r *Repo) GetReadme() string {
1437+ if r.readme != "" {
1438+ return r.readme
1439+ }
1440+ md, err := r.readFile("README.md")
1441+ if err != nil {
1442+ return ""
1443+ }
1444+ return md
1445+}
1446+
1447+func (r *Repo) readFile(path string) (string, error) {
1448+ lg, err := r.repo.Log(&git.LogOptions{
1449+ From: r.ref.Hash(),
1450+ })
1451+ if err != nil {
1452+ return "", err
1453+ }
1454+ c, err := lg.Next()
1455+ if err != nil {
1456+ return "", err
1457+ }
1458+ f, err := c.File(path)
1459+ if err != nil {
1460+ return "", err
1461+ }
1462diff --git a/internal/tui/session.go b/internal/tui/session.go
1463index c322d01c19d1ef53d6fba06442ffbd692caa6e35..2b6db15b1336a2742aac58444f4ff54c146a3b1e 100644
1464--- a/internal/tui/session.go
1465+++ b/internal/tui/session.go
1466@@ -2,6 +2,8 @@ package tui
1467
1468 import (
1469 "fmt"
1470+ "net/url"
1471+ "strings"
1472
1473 tea "github.com/charmbracelet/bubbletea"
1474 "github.com/charmbracelet/soft-serve/internal/config"
1475@@ -16,6 +18,10 @@ func SessionHandler(cfg *config.Config) func(ssh.Session) (tea.Model, []tea.Prog
1476 case 0:
1477 scfg.InitialRepo = ""
1478 case 1:
1479+ p, err := url.Parse(cmd[0])
1480+ if err != nil || strings.Contains(p.Path, "/") {
1481+ return nil, nil
1482+ }
1483 scfg.InitialRepo = cmd[0]
1484 default:
1485 return nil, nil
1486diff --git a/internal/tui/style/style.go b/internal/tui/style/style.go
1487index ec09bd832dbd1cba88d8abdb3f69666e0c57ba75..1f57cb5b763cd83ea39c5add97521545ab2ac6eb 100644
1488--- a/internal/tui/style/style.go
1489+++ b/internal/tui/style/style.go
1490@@ -31,6 +31,7 @@ type Styles struct {
1491 RepoBody lipgloss.Style
1492
1493 Footer lipgloss.Style
1494+ Branch lipgloss.Style
1495 HelpKey lipgloss.Style
1496 HelpValue lipgloss.Style
1497 HelpDivider lipgloss.Style
1498@@ -38,6 +39,32 @@ type Styles struct {
1499 Error lipgloss.Style
1500 ErrorTitle lipgloss.Style
1501 ErrorBody lipgloss.Style
1502+
1503+ LogItemSelector lipgloss.Style
1504+ LogItemActive lipgloss.Style
1505+ LogItemInactive lipgloss.Style
1506+ LogItemHash lipgloss.Style
1507+ LogCommit lipgloss.Style
1508+ LogCommitHash lipgloss.Style
1509+ LogCommitAuthor lipgloss.Style
1510+ LogCommitDate lipgloss.Style
1511+ LogCommitBody lipgloss.Style
1512+ LogCommitStatsAdd lipgloss.Style
1513+ LogCommitStatsDel lipgloss.Style
1514+
1515+ RefItemSelector lipgloss.Style
1516+ RefItemActive lipgloss.Style
1517+ RefItemInactive lipgloss.Style
1518+ RefItemBranch lipgloss.Style
1519+ RefItemTag lipgloss.Style
1520+
1521+ TreeItemSelector lipgloss.Style
1522+ TreeItemActive lipgloss.Style
1523+ TreeItemInactive lipgloss.Style
1524+ TreeFileDir lipgloss.Style
1525+ TreeFileMode lipgloss.Style
1526+ TreeFileSize lipgloss.Style
1527+ TreeFileContent lipgloss.Style
1528 }
1529
1530 // DefaultStyles returns default styles for the TUI.
1531@@ -133,6 +160,11 @@ func DefaultStyles() *Styles {
1532 s.Footer = lipgloss.NewStyle().
1533 MarginTop(1)
1534
1535+ s.Branch = lipgloss.NewStyle().
1536+ Foreground(lipgloss.Color("203")).
1537+ Background(lipgloss.Color("236")).
1538+ Padding(0, 1)
1539+
1540 s.HelpKey = lipgloss.NewStyle().
1541 Foreground(lipgloss.Color("241"))
1542
1543@@ -157,5 +189,68 @@ func DefaultStyles() *Styles {
1544 MarginLeft(2).
1545 Width(52) // for now
1546
1547+ s.LogItemInactive = lipgloss.NewStyle().
1548+ MarginLeft(1)
1549+
1550+ s.LogItemSelector = s.LogItemInactive.Copy().
1551+ Width(1).
1552+ Foreground(lipgloss.Color("#B083EA"))
1553+
1554+ s.LogItemActive = s.LogItemInactive.Copy().
1555+ Bold(true)
1556+
1557+ s.LogItemHash = s.LogItemInactive.Copy().
1558+ Width(7).
1559+ Foreground(lipgloss.Color("#A3A322"))
1560+
1561+ s.LogCommit = lipgloss.NewStyle().
1562+ Margin(0, 2)
1563+
1564+ s.LogCommitHash = s.LogItemHash.Copy().
1565+ UnsetMarginLeft().
1566+ UnsetWidth().
1567+ Bold(true)
1568+
1569+ s.LogCommitBody = lipgloss.NewStyle().
1570+ MarginTop(1).
1571+ MarginLeft(2)
1572+
1573+ s.LogCommitStatsAdd = lipgloss.NewStyle().
1574+ Foreground(lipgloss.Color("#00D787")).
1575+ Bold(true)
1576+
1577+ s.LogCommitStatsDel = lipgloss.NewStyle().
1578+ Foreground(lipgloss.Color("#FD5B5B")).
1579+ Bold(true)
1580+
1581+ s.RefItemSelector = s.LogItemSelector.Copy()
1582+
1583+ s.RefItemActive = s.LogItemActive.Copy()
1584+
1585+ s.RefItemInactive = s.LogItemInactive.Copy()
1586+
1587+ s.RefItemBranch = lipgloss.NewStyle()
1588+
1589+ s.RefItemTag = lipgloss.NewStyle().