From 6afc3895b2865bdc39b4ae4d002175282b6db831 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 25 Apr 2022 17:23:40 -0400 Subject: [PATCH] feat: display repo commits --- server/session.go | 2 +- ui/common/common.go | 2 +- ui/components/selector/selector.go | 75 +++++++++----- ui/pages/repo/log.go | 154 ++++++++++++++++++++--------- ui/pages/repo/repo.go | 33 +++++-- ui/pages/selection/selection.go | 9 +- ui/ui.go | 29 +++++- 7 files changed, 215 insertions(+), 89 deletions(-) diff --git a/server/session.go b/server/session.go index bbec7217512b15039e57e1341b05e0f1adeed8c0..9d829c2a0f6f996d297b4f29492759b5ac8732f1 100644 --- a/server/session.go +++ b/server/session.go @@ -57,7 +57,7 @@ func SessionHandler(ac *appCfg.Config) bm.ProgramHandler { } c := common.Common{ Styles: styles.DefaultStyles(), - Keymap: keymap.DefaultKeyMap(), + KeyMap: keymap.DefaultKeyMap(), Width: pty.Window.Width, Height: pty.Window.Height, } diff --git a/ui/common/common.go b/ui/common/common.go index 7f10fe91d52dea154250638501dfab8e533f4a13..ef54b33eb5b8cf0661e46e1b05e356509da15527 100644 --- a/ui/common/common.go +++ b/ui/common/common.go @@ -8,7 +8,7 @@ import ( // Common is a struct all components should embed. type Common struct { Styles *styles.Styles - Keymap *keymap.KeyMap + KeyMap *keymap.KeyMap Width int Height int } diff --git a/ui/components/selector/selector.go b/ui/components/selector/selector.go index 8977081ec2f389b2279964d0985f7d8bef72b656..50a7226ed17da2f51efa33b6650355bfaa9bdad2 100644 --- a/ui/components/selector/selector.go +++ b/ui/components/selector/selector.go @@ -9,8 +9,7 @@ import ( // Selector is a list of items that can be selected. type Selector struct { - KeyMap list.KeyMap - list list.Model + list.Model common common.Common active int filterState list.FilterState @@ -36,63 +35,87 @@ func New(common common.Common, items []IdentifiableItem, delegate list.ItemDeleg } l := list.New(itms, delegate, common.Width, common.Height) s := &Selector{ - list: l, + Model: l, common: common, } - s.KeyMap = list.DefaultKeyMap() s.SetSize(common.Width, common.Height) return s } +// PerPage returns the number of items per page. +func (s *Selector) PerPage() int { + return s.Model.Paginator.PerPage +} + +// SetPage sets the current page. +func (s *Selector) SetPage(page int) { + s.Model.Paginator.Page = page +} + +// Page returns the current page. +func (s *Selector) Page() int { + return s.Model.Paginator.Page +} + +// TotalPages returns the total number of pages. +func (s *Selector) TotalPages() int { + return s.Model.Paginator.TotalPages +} + +// Select selects the item at the given index. +func (s *Selector) Select(index int) { + s.Model.Select(index) +} + // SetShowTitle sets the show title flag. func (s *Selector) SetShowTitle(show bool) { - s.list.SetShowTitle(show) + s.Model.SetShowTitle(show) } // SetShowHelp sets the show help flag. func (s *Selector) SetShowHelp(show bool) { - s.list.SetShowHelp(show) + s.Model.SetShowHelp(show) } // SetShowStatusBar sets the show status bar flag. func (s *Selector) SetShowStatusBar(show bool) { - s.list.SetShowStatusBar(show) + s.Model.SetShowStatusBar(show) } // DisableQuitKeybindings disables the quit keybindings. func (s *Selector) DisableQuitKeybindings() { - s.list.DisableQuitKeybindings() + s.Model.DisableQuitKeybindings() } // SetShowFilter sets the show filter flag. func (s *Selector) SetShowFilter(show bool) { - s.list.SetShowFilter(show) + s.Model.SetShowFilter(show) } // SetShowPagination sets the show pagination flag. func (s *Selector) SetShowPagination(show bool) { - s.list.SetShowPagination(show) + s.Model.SetShowPagination(show) } // SetFilteringEnabled sets the filtering enabled flag. func (s *Selector) SetFilteringEnabled(enabled bool) { - s.list.SetFilteringEnabled(enabled) + s.Model.SetFilteringEnabled(enabled) } // SetSize implements common.Component. func (s *Selector) SetSize(width, height int) { s.common.SetSize(width, height) - s.list.SetSize(width, height) + s.Model.SetSize(width, height) } // SetItems sets the items in the selector. func (s *Selector) SetItems(items []list.Item) tea.Cmd { - return s.list.SetItems(items) + return s.Model.SetItems(items) } // Index returns the index of the selected item. func (s *Selector) Index() int { - return s.list.Index() + return s.Model.Index() } // Init implements tea.Model. @@ -107,44 +130,44 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.MouseMsg: switch msg.Type { case tea.MouseWheelUp: - s.list.CursorUp() + s.Model.CursorUp() case tea.MouseWheelDown: - s.list.CursorDown() + s.Model.CursorDown() } case tea.KeyMsg: switch { - case key.Matches(msg, s.common.Keymap.Select): + case key.Matches(msg, s.common.KeyMap.Select): cmds = append(cmds, s.selectCmd) } case list.FilterMatchesMsg: cmds = append(cmds, s.activeFilterCmd) } - m, cmd := s.list.Update(msg) - s.list = m + m, cmd := s.Model.Update(msg) + s.Model = m if cmd != nil { cmds = append(cmds, cmd) } // Track filter state and update active item when filter state changes. - filterState := s.list.FilterState() + filterState := s.Model.FilterState() if s.filterState != filterState { cmds = append(cmds, s.activeFilterCmd) } s.filterState = filterState // Send ActiveMsg when index change. - if s.active != s.list.Index() { + if s.active != s.Model.Index() { cmds = append(cmds, s.activeCmd) } - s.active = s.list.Index() + s.active = s.Model.Index() return s, tea.Batch(cmds...) } // View implements tea.Model. func (s *Selector) View() string { - return s.list.View() + return s.Model.View() } func (s *Selector) selectCmd() tea.Msg { - item := s.list.SelectedItem() + item := s.Model.SelectedItem() i, ok := item.(IdentifiableItem) if !ok { return SelectMsg("") @@ -153,7 +176,7 @@ func (s *Selector) selectCmd() tea.Msg { } func (s *Selector) activeCmd() tea.Msg { - item := s.list.SelectedItem() + item := s.Model.SelectedItem() i, ok := item.(IdentifiableItem) if !ok { return ActiveMsg("") @@ -165,7 +188,7 @@ func (s *Selector) activeFilterCmd() tea.Msg { // Here we use VisibleItems because when list.FilterMatchesMsg is sent, // VisibleItems is the only way to get the list of filtered items. The list // bubble should export something like list.FilterMatchesMsg.Items(). - items := s.list.VisibleItems() + items := s.Model.VisibleItems() if len(items) == 0 { return nil } diff --git a/ui/pages/repo/log.go b/ui/pages/repo/log.go index d431b1c245e2b55d1441cf36a94bdb1dec0a2391..77475e96d1d9b6990b5e4b1c506dbb7921b6b212 100644 --- a/ui/pages/repo/log.go +++ b/ui/pages/repo/log.go @@ -1,10 +1,15 @@ package repo import ( + "fmt" + + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + ggit "github.com/charmbracelet/soft-serve/git" "github.com/charmbracelet/soft-serve/ui/common" "github.com/charmbracelet/soft-serve/ui/components/selector" "github.com/charmbracelet/soft-serve/ui/components/viewport" + "github.com/charmbracelet/soft-serve/ui/git" ) type view int @@ -14,11 +19,19 @@ const ( commitView ) +type LogCountMsg int64 + +type LogItemsMsg []list.Item + type Log struct { common common.Common selector *selector.Selector vp *viewport.Viewport activeView view + repo git.GitRepo + ref *ggit.Reference + count int64 + nextPage int } func NewLog(common common.Common) *Log { @@ -30,13 +43,13 @@ func NewLog(common common.Common) *Log { selector := selector.New(common, []selector.IdentifiableItem{}, LogItemDelegate{common.Styles}) selector.SetShowFilter(false) selector.SetShowHelp(false) - selector.SetShowPagination(true) + selector.SetShowPagination(false) selector.SetShowStatusBar(false) selector.SetShowTitle(false) selector.SetFilteringEnabled(false) selector.DisableQuitKeybindings() - selector.KeyMap.NextPage = common.Keymap.NextPage - selector.KeyMap.PrevPage = common.Keymap.PrevPage + selector.KeyMap.NextPage = common.KeyMap.NextPage + selector.KeyMap.PrevPage = common.KeyMap.PrevPage l.selector = selector return l } @@ -47,53 +60,54 @@ func (l *Log) SetSize(width, height int) { l.vp.SetSize(width, height) } -// func (b *Bubble) countCommits() tea.Msg { -// if b.ref == nil { -// ref, err := b.repo.HEAD() -// if err != nil { -// return common.ErrMsg{Err: err} -// } -// b.ref = ref -// } -// count, err := b.repo.CountCommits(b.ref) -// if err != nil { -// return common.ErrMsg{Err: err} -// } -// return countMsg(count) -// } - -// func (b *Bubble) updateItems() tea.Msg { -// if b.count == 0 { -// b.count = int64(b.countCommits().(countMsg)) -// } -// count := b.count -// items := make([]list.Item, count) -// page := b.nextPage -// limit := b.list.Paginator.PerPage -// skip := page * limit -// // CommitsByPage pages start at 1 -// cc, err := b.repo.CommitsByPage(b.ref, page+1, limit) -// if err != nil { -// return common.ErrMsg{Err: err} -// } -// for i, c := range cc { -// idx := i + skip -// if int64(idx) >= count { -// break -// } -// items[idx] = item{c} -// } -// b.list.SetItems(items) -// b.SetSize(b.width, b.height) -// return itemsMsg{} -// } - func (l *Log) Init() tea.Cmd { - return nil + cmds := make([]tea.Cmd, 0) + cmds = append(cmds, l.updateCommitsCmd) + return tea.Batch(cmds...) } func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - return l, nil + cmds := make([]tea.Cmd, 0) + switch msg := msg.(type) { + case RepoMsg: + l.count = 0 + l.selector.Select(0) + l.nextPage = 0 + l.repo = git.GitRepo(msg) + case RefMsg: + l.ref = msg + l.count = 0 + cmds = append(cmds, l.countCommitsCmd) + case LogCountMsg: + l.count = int64(msg) + case LogItemsMsg: + cmds = append(cmds, l.selector.SetItems(msg)) + l.selector.SetPage(l.nextPage) + l.SetSize(l.common.Width, l.common.Height) + case tea.KeyMsg, tea.MouseMsg: + // This is a hack for loading commits on demand based on list.Pagination. + if l.activeView == logView { + curPage := l.selector.Page() + s, cmd := l.selector.Update(msg) + m := s.(*selector.Selector) + l.selector = m + if m.Page() != curPage { + l.nextPage = m.Page() + l.selector.SetPage(curPage) + cmds = append(cmds, l.updateCommitsCmd) + } + cmds = append(cmds, cmd) + } + } + switch l.activeView { + case commitView: + vp, cmd := l.vp.Update(msg) + l.vp = vp.(*viewport.Viewport) + if cmd != nil { + cmds = append(cmds, cmd) + } + } + return l, tea.Batch(cmds...) } func (l *Log) View() string { @@ -106,3 +120,51 @@ func (l *Log) View() string { return "" } } + +func (l *Log) StatusBarInfo() string { + switch l.activeView { + case logView: + // We're using l.nextPage instead of l.selector.Paginator.Page because + // of the paginator hack above. + return fmt.Sprintf("%d/%d", l.nextPage+1, l.selector.TotalPages()) + default: + return "" + } +} + +func (l *Log) countCommitsCmd() tea.Msg { + count, err := l.repo.CountCommits(l.ref) + if err != nil { + return common.ErrorMsg(err) + } + return LogCountMsg(count) +} + +func (l *Log) updateCommitsCmd() tea.Msg { + count := l.count + if l.count == 0 { + switch msg := l.countCommitsCmd().(type) { + case common.ErrorMsg: + return msg + case LogCountMsg: + count = int64(msg) + } + } + items := make([]list.Item, count) + page := l.nextPage + limit := l.selector.PerPage() + skip := page * limit + // CommitsByPage pages start at 1 + cc, err := l.repo.CommitsByPage(l.ref, page+1, limit) + if err != nil { + return common.ErrorMsg(err) + } + for i, c := range cc { + idx := i + skip + if int64(idx) >= count { + break + } + items[idx] = LogItem{c} + } + return LogItemsMsg(items) +} diff --git a/ui/pages/repo/repo.go b/ui/pages/repo/repo.go index 756bb9db04a6aba53a3be25410d167a0b29bedfc..69dbfe53c9545bd62744b1cc7313219090dc87e4 100644 --- a/ui/pages/repo/repo.go +++ b/ui/pages/repo/repo.go @@ -9,7 +9,6 @@ import ( ggit "github.com/charmbracelet/soft-serve/git" "github.com/charmbracelet/soft-serve/ui/common" "github.com/charmbracelet/soft-serve/ui/components/code" - "github.com/charmbracelet/soft-serve/ui/components/selector" "github.com/charmbracelet/soft-serve/ui/components/statusbar" "github.com/charmbracelet/soft-serve/ui/components/tabs" "github.com/charmbracelet/soft-serve/ui/git" @@ -79,9 +78,9 @@ func (r *Repo) SetSize(width, height int) { // ShortHelp implements help.KeyMap. func (r *Repo) ShortHelp() []key.Binding { b := make([]key.Binding, 0) - tab := r.common.Keymap.Section + tab := r.common.KeyMap.Section tab.SetHelp("tab", "switch tab") - b = append(b, r.common.Keymap.Back) + b = append(b, r.common.KeyMap.Back) b = append(b, tab) return b } @@ -101,28 +100,48 @@ func (r *Repo) Init() tea.Cmd { func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { - case selector.SelectMsg: - r.activeTab = 0 - cmds = append(cmds, r.tabs.Init(), r.setRepoCmd(string(msg))) case RepoMsg: + r.activeTab = 0 r.selectedRepo = git.GitRepo(msg) r.readme.GotoTop() cmds = append(cmds, + r.tabs.Init(), r.updateReadmeCmd, r.updateRefCmd, ) + // Pass msg to log. + l, cmd := r.log.Update(msg) + r.log = l.(*Log) + if cmd != nil { + cmds = append(cmds, cmd) + } case RefMsg: r.ref = msg cmds = append(cmds, r.updateStatusBarCmd, r.log.Init(), ) + // Pass msg to log. + l, cmd := r.log.Update(msg) + r.log = l.(*Log) + if cmd != nil { + cmds = append(cmds, cmd) + } case tabs.ActiveTabMsg: r.activeTab = tab(msg) + if r.selectedRepo != nil { + cmds = append(cmds, r.updateStatusBarCmd) + } case tea.KeyMsg, tea.MouseMsg: if r.selectedRepo != nil { cmds = append(cmds, r.updateStatusBarCmd) } + case LogCountMsg, LogItemsMsg: + l, cmd := r.log.Update(msg) + r.log = l.(*Log) + if cmd != nil { + cmds = append(cmds, cmd) + } } t, cmd := r.tabs.Update(msg) r.tabs = t.(*tabs.Tabs) @@ -213,6 +232,8 @@ func (r *Repo) updateStatusBarCmd() tea.Msg { switch r.activeTab { case readmeTab: info = fmt.Sprintf("%.f%%", r.readme.ScrollPercent()*100) + case commitsTab: + info = r.log.StatusBarInfo() } return statusbar.StatusBarMsg{ Key: r.selectedRepo.Name(), diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index c7f98696c70d5db7d828517ffc6bf6e4563956bf..9879edd23e9eeae3ab0fd7a273f4ec671bd07a22 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -74,12 +74,12 @@ func (s *Selection) ShortHelp() []key.Binding { k := s.selector.KeyMap kb := make([]key.Binding, 0) kb = append(kb, - s.common.Keymap.UpDown, - s.common.Keymap.Section, + s.common.KeyMap.UpDown, + s.common.KeyMap.Section, ) if s.activeBox == selectorBox { kb = append(kb, - s.common.Keymap.Select, + s.common.KeyMap.Select, k.Filter, k.ClearFilter, ) @@ -205,9 +205,10 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, s.changeActive(msg)) // reset readme position when active item change s.readme.GotoTop() + case selector.SelectMsg: case tea.KeyMsg: switch { - case key.Matches(msg, s.common.Keymap.Section): + case key.Matches(msg, s.common.KeyMap.Section): s.activeBox = (s.activeBox + 1) % 2 } } diff --git a/ui/ui.go b/ui/ui.go index ebfa93fda3835c995a9374ab96f828e32cfe09cd..b005bc3362a28892ae647ab4cebc9e0186a6cab2 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/soft-serve/ui/components/footer" "github.com/charmbracelet/soft-serve/ui/components/header" "github.com/charmbracelet/soft-serve/ui/components/selector" + "github.com/charmbracelet/soft-serve/ui/git" "github.com/charmbracelet/soft-serve/ui/pages/repo" "github.com/charmbracelet/soft-serve/ui/pages/selection" "github.com/charmbracelet/soft-serve/ui/session" @@ -64,7 +65,7 @@ func (ui *UI) getMargins() (wm, hm int) { func (ui *UI) ShortHelp() []key.Binding { b := make([]key.Binding, 0) b = append(b, ui.pages[ui.activePage].ShortHelp()...) - b = append(b, ui.common.Keymap.Quit) + b = append(b, ui.common.KeyMap.Quit) return b } @@ -72,7 +73,7 @@ func (ui *UI) ShortHelp() []key.Binding { func (ui *UI) FullHelp() [][]key.Binding { b := make([][]key.Binding, 0) b = append(b, ui.pages[ui.activePage].FullHelp()...) - b = append(b, []key.Binding{ui.common.Keymap.Quit}) + b = append(b, []key.Binding{ui.common.KeyMap.Quit}) return b } @@ -129,9 +130,12 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ui.SetSize(msg.Width, msg.Height) case tea.KeyMsg: switch { - case key.Matches(msg, ui.common.Keymap.Quit): + case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil: + ui.error = nil + ui.state = loadedState + case key.Matches(msg, ui.common.KeyMap.Quit): return ui, tea.Quit - case ui.activePage == 1 && key.Matches(msg, ui.common.Keymap.Back): + case ui.activePage == 1 && key.Matches(msg, ui.common.KeyMap.Back): ui.activePage = 0 } case common.ErrorMsg: @@ -139,7 +143,10 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ui.state = errorState return ui, nil case selector.SelectMsg: - ui.activePage = (ui.activePage + 1) % 2 + if ui.activePage == 0 { + ui.activePage = (ui.activePage + 1) % 2 + cmds = append(cmds, ui.setRepoCmd(string(msg))) + } } m, cmd := ui.pages[ui.activePage].Update(msg) ui.pages[ui.activePage] = m.(common.Page) @@ -171,3 +178,15 @@ func (ui *UI) View() string { } return ui.common.Styles.App.Render(s.String()) } + +func (ui *UI) setRepoCmd(rn string) tea.Cmd { + rs := ui.s.Config().Source + return func() tea.Msg { + for _, r := range rs.AllRepos() { + if r.Name() == rn { + return repo.RepoMsg(r) + } + } + return common.ErrorMsg(git.ErrMissingRepo) + } +}