Diff
1diff --git a/main.go b/main.go
2index a2cc4c953ff6da2861835f430dd3e3acdce97553..4ee805c02ac114d61909c25aff150c047508b27e 100644
3--- a/main.go
4+++ b/main.go
5@@ -27,7 +27,7 @@ func main() {
6 s, err := server.NewServer(
7 cfg.Port,
8 cfg.KeyPath,
9- bm.Middleware(tui.SessionHandler),
10+ bm.Middleware(tui.SessionHandler(cfg.RepoPath)),
11 gm.Middleware(cfg.RepoPath, cfg.RepoAuthPath),
12 lm.Middleware(),
13 )
14diff --git a/tui/commands.go b/tui/commands.go
15index 2ca8f35995743e69a7bef225d931ac6bc990cfe2..cd24944ead3f03f615f3153414d455cb75bd07b1 100644
16--- a/tui/commands.go
17+++ b/tui/commands.go
18@@ -12,3 +12,9 @@ func (m *Model) windowChangesCmd() tea.Msg {
19 m.height = w.Height
20 return windowMsg{}
21 }
22+
23+func (m *Model) getCommitsCmd() tea.Msg {
24+ m.commits = m.repos.getCommits(20)
25+ m.state = commitsLoadedState
26+ return nil
27+}
28diff --git a/tui/git.go b/tui/git.go
29new file mode 100644
30index 0000000000000000000000000000000000000000..631ef7ede5d9172ffee779b52411609064b609ef
31--- /dev/null
32+++ b/tui/git.go
33@@ -0,0 +1,77 @@
34+package tui
35+
36+import (
37+ "log"
38+ "os"
39+ "sort"
40+ "sync"
41+ "time"
42+
43+ "github.com/go-git/go-git/v5"
44+ "github.com/go-git/go-git/v5/plumbing/object"
45+)
46+
47+type commitLog []*object.Commit
48+
49+func (cl commitLog) Len() int { return len(cl) }
50+func (cl commitLog) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
51+func (cl commitLog) Less(i, j int) bool { return cl[i].Author.When.After(cl[j].Author.When) }
52+
53+type repoSource struct {
54+ mtx sync.Mutex
55+ path string
56+ repos []*git.Repository
57+ commits commitLog
58+}
59+
60+func newRepoSource(repoPath string) *repoSource {
61+ rs := &repoSource{path: repoPath}
62+ go func() {
63+ for {
64+ rs.loadRepos()
65+ time.Sleep(time.Second * 10)
66+ }
67+ }()
68+ return rs
69+}
70+
71+func (rs *repoSource) allRepos() []*git.Repository {
72+ rs.mtx.Lock()
73+ defer rs.mtx.Unlock()
74+ return rs.repos
75+}
76+
77+func (rs *repoSource) getCommits(limit int) []*object.Commit {
78+ rs.mtx.Lock()
79+ defer rs.mtx.Unlock()
80+ if limit > len(rs.commits) {
81+ limit = len(rs.commits)
82+ }
83+ return rs.commits[:limit]
84+}
85+
86+func (rs *repoSource) loadRepos() {
87+ rs.mtx.Lock()
88+ defer rs.mtx.Unlock()
89+ rd, err := os.ReadDir(rs.path)
90+ if err != nil {
91+ return
92+ }
93+ rs.repos = make([]*git.Repository, 0)
94+ for _, rd := range rd {
95+ r, err := git.PlainOpen(rs.path + string(os.PathSeparator) + rd.Name())
96+ if err != nil {
97+ log.Fatal(err)
98+ }
99+ l, err := r.Log(&git.LogOptions{All: true})
100+ if err != nil {
101+ log.Fatal(err)
102+ }
103+ l.ForEach(func(c *object.Commit) error {
104+ rs.commits = append(rs.commits, c)
105+ return nil
106+ })
107+ sort.Sort(rs.commits)
108+ rs.repos = append(rs.repos, r)
109+ }
110+}
111diff --git a/tui/model.go b/tui/model.go
112index 9ed9028a608a7fa2fea953a65cd5839309e2081c..2c92291e67bf228ee0865be497de26271cc973b4 100644
113--- a/tui/model.go
114+++ b/tui/model.go
115@@ -5,6 +5,7 @@ import (
116
117 tea "github.com/charmbracelet/bubbletea"
118 "github.com/gliderlabs/ssh"
119+ "github.com/go-git/go-git/v5/plumbing/object"
120 )
121
122 type sessionState int
123@@ -12,6 +13,7 @@ type sessionState int
124 const (
125 startState sessionState = iota
126 errorState
127+ commitsLoadedState
128 quittingState
129 quitState
130 )
131@@ -24,15 +26,18 @@ func (e errMsg) Error() string {
132 return e.err.Error()
133 }
134
135-func SessionHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
136- if len(s.Command()) == 0 {
137- pty, changes, active := s.Pty()
138- if !active {
139- return nil, nil
140+func SessionHandler(repoPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
141+ rs := newRepoSource(repoPath)
142+ return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
143+ if len(s.Command()) == 0 {
144+ pty, changes, active := s.Pty()
145+ if !active {
146+ return nil, nil
147+ }
148+ return NewModel(pty.Window.Width, pty.Window.Height, changes, rs), nil
149 }
150- return NewModel(pty.Window.Width, pty.Window.Height, changes), []tea.ProgramOption{tea.WithAltScreen()}
151+ return nil, nil
152 }
153- return nil, nil
154 }
155
156 type Model struct {
157@@ -42,20 +47,24 @@ type Model struct {
158 width int
159 height int
160 windowChanges <-chan ssh.Window
161+ repos *repoSource
162+ commits []*object.Commit
163 }
164
165-func NewModel(width int, height int, windowChanges <-chan ssh.Window) *Model {
166+func NewModel(width int, height int, windowChanges <-chan ssh.Window, repos *repoSource) *Model {
167 m := &Model{
168 width: width,
169 height: height,
170 windowChanges: windowChanges,
171+ repos: repos,
172+ commits: make([]*object.Commit, 0),
173 }
174 m.state = startState
175 return m
176 }
177
178 func (m *Model) Init() tea.Cmd {
179- return tea.Batch(m.windowChangesCmd, tea.HideCursor)
180+ return tea.Batch(m.windowChangesCmd, tea.HideCursor, m.getCommitsCmd)
181 }
182
183 func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
184@@ -86,11 +95,18 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
185
186 func (m *Model) View() string {
187 pad := 6
188- h := headerStyle.Width(m.width - pad).Render("Smoothie")
189+ h := headerStyle.Width(m.width - pad).Render("Charm Beta")
190 f := footerStyle.Render(m.info)
191 s := ""
192 content := ""
193 switch m.state {
194+ case startState:
195+ s += normalStyle.Render("Loading")
196+ case commitsLoadedState:
197+ for _, c := range m.commits {
198+ msg := fmt.Sprintf("%s %s %s %s", c.Author.When, c.Author.Name, c.Author.Email, c.Message)
199+ s += normalStyle.Render(msg) + "\n"
200+ }
201 case errorState:
202 s += errorStyle.Render(fmt.Sprintf("Bummer: %s", m.error))
203 default:
204diff --git a/tui/style.go b/tui/style.go
205index 6c219b1117230fc47c63efd21d90d117151ca91a..dc0d2f4bd0efad5142f31dd2b30af385049aaf27 100644
206--- a/tui/style.go
207+++ b/tui/style.go
208@@ -18,12 +18,9 @@ var normalStyle = lipgloss.NewStyle().
209 Foreground(lipgloss.Color("#FFFFFF"))
210
211 var footerStyle = lipgloss.NewStyle().
212- BorderStyle(lipgloss.Border{Left: ">"}).
213 BorderForeground(lipgloss.Color("#6D6D6D")).
214 BorderLeft(true).
215 Foreground(lipgloss.Color("#373737")).
216- PaddingLeft(1).
217- MarginLeft(1).
218 Bold(true)
219
220 var errorStyle = lipgloss.NewStyle().