diff --git a/ui/components/code/code.go b/ui/components/code/code.go index cf0bf45612d7eb72d36e53bb1519fc06c47e96a6..942e2ccf63af81896e8d1ab2aa8397812d85c6bb 100644 --- a/ui/components/code/code.go +++ b/ui/components/code/code.go @@ -49,11 +49,6 @@ func (r *Code) SetContent(c, ext string) tea.Cmd { return r.Init() } -// GotoTop reset the viewport to the top. -func (r *Code) GotoTop() { - r.viewport.Viewport.GotoTop() -} - // Init implements tea.Model. func (r *Code) Init() tea.Cmd { w := r.common.Width @@ -96,6 +91,51 @@ func (r *Code) View() string { return r.viewport.View() } +// GotoTop moves the viewport to the top of the log. +func (r *Code) GotoTop() { + r.viewport.GotoTop() +} + +// GotoBottom moves the viewport to the bottom of the log. +func (r *Code) GotoBottom() { + r.viewport.GotoBottom() +} + +// HalfViewDown moves the viewport down by half the viewport height. +func (r *Code) HalfViewDown() { + r.viewport.HalfViewDown() +} + +// HalfViewUp moves the viewport up by half the viewport height. +func (r *Code) HalfViewUp() { + r.viewport.HalfViewUp() +} + +// ViewUp moves the viewport up by a page. +func (r *Code) ViewUp() []string { + return r.viewport.ViewUp() +} + +// ViewDown moves the viewport down by a page. +func (r *Code) ViewDown() []string { + return r.viewport.ViewDown() +} + +// LineUp moves the viewport up by the given number of lines. +func (r *Code) LineUp(n int) []string { + return r.viewport.LineUp(n) +} + +// LineDown moves the viewport down by the given number of lines. +func (r *Code) LineDown(n int) []string { + return r.viewport.LineDown(n) +} + +// ScrollPercent returns the viewport's scroll percentage. +func (r *Code) ScrollPercent() float64 { + return r.viewport.ScrollPercent() +} + func styleConfig() gansi.StyleConfig { noColor := "" s := glamour.DarkStyleConfig diff --git a/ui/components/footer/footer.go b/ui/components/footer/footer.go index 70e87defa5d29f44b22fde452d1ccf0036ef7ae9..5a8e10aa5a275f193c33801baa71a4cb2795bac0 100644 --- a/ui/components/footer/footer.go +++ b/ui/components/footer/footer.go @@ -17,7 +17,9 @@ type Footer struct { func New(c common.Common, keymap help.KeyMap) *Footer { h := help.New() h.Styles.ShortKey = c.Styles.HelpKey + h.Styles.ShortDesc = c.Styles.HelpValue h.Styles.FullKey = c.Styles.HelpKey + h.Styles.FullDesc = c.Styles.HelpValue f := &Footer{ common: c, help: h, diff --git a/ui/components/selector/selector.go b/ui/components/selector/selector.go index 9e9868e81bfdcb5a744cc6c8a60d93a4e812bbce..50a34c366d5a7311a12ea995e012c960a7354b0f 100644 --- a/ui/components/selector/selector.go +++ b/ui/components/selector/selector.go @@ -9,6 +9,7 @@ import ( // Selector is a list of items that can be selected. type Selector struct { + KeyMap list.KeyMap list list.Model common common.Common active int @@ -34,10 +35,7 @@ func New(common common.Common, items []IdentifiableItem, delegate list.ItemDeleg itms[i] = item } l := list.New(itms, delegate, common.Width, common.Height) - l.SetShowTitle(false) - l.SetShowHelp(false) - l.SetShowStatusBar(false) - l.DisableQuitKeybindings() + l.KeyMap = list.DefaultKeyMap() s := &Selector{ list: l, common: common, @@ -46,9 +44,39 @@ func New(common common.Common, items []IdentifiableItem, delegate list.ItemDeleg return s } -// KeyMap returns the underlying list's keymap. -func (s *Selector) KeyMap() list.KeyMap { - return s.list.KeyMap +// SetShowTitle sets the show title flag. +func (s *Selector) SetShowTitle(show bool) { + s.list.SetShowTitle(show) +} + +// SetShowHelp sets the show help flag. +func (s *Selector) SetShowHelp(show bool) { + s.list.SetShowHelp(show) +} + +// SetShowStatusBar sets the show status bar flag. +func (s *Selector) SetShowStatusBar(show bool) { + s.list.SetShowStatusBar(show) +} + +// DisableQuitKeybindings disables the quit keybindings. +func (s *Selector) DisableQuitKeybindings() { + s.list.DisableQuitKeybindings() +} + +// SetShowFilter sets the show filter flag. +func (s *Selector) SetShowFilter(show bool) { + s.list.SetShowFilter(show) +} + +// SetShowPagination sets the show pagination flag. +func (s *Selector) SetShowPagination(show bool) { + s.list.SetShowPagination(show) +} + +// SetFilteringEnabled sets the filtering enabled flag. +func (s *Selector) SetFilteringEnabled(enabled bool) { + s.list.SetFilteringEnabled(enabled) } // SetSize implements common.Component. diff --git a/ui/components/statusbar/statusbar.go b/ui/components/statusbar/statusbar.go index dce54deffb0f7e46c241d083ebb03ee4f93dbe7a..fc03f00ca1a422318de7194a4d898002bfc8265c 100644 --- a/ui/components/statusbar/statusbar.go +++ b/ui/components/statusbar/statusbar.go @@ -46,7 +46,10 @@ func (s *StatusBar) View() string { st := s.common.Styles w := lipgloss.Width key := st.StatusBarKey.Render(s.msg.Key) - info := st.StatusBarInfo.Render(s.msg.Info) + info := "" + if s.msg.Info != "" { + info = st.StatusBarInfo.Render(s.msg.Info) + } branch := st.StatusBarBranch.Render(s.msg.Branch) value := st.StatusBarValue. Width(s.common.Width - w(key) - w(info) - w(branch)). diff --git a/ui/components/tabs/tabs.go b/ui/components/tabs/tabs.go index ae4bd3857cb069ed1b2210844085833a0e175329..63081a849f8f16f4a62edd59a9ae659c4ab8bd6a 100644 --- a/ui/components/tabs/tabs.go +++ b/ui/components/tabs/tabs.go @@ -53,7 +53,7 @@ func (t *Tabs) View() string { s := strings.Builder{} sep := t.common.Styles.TabSeparator for i, tab := range t.tabs { - style := t.common.Styles.Tab.Copy() + style := t.common.Styles.TabInactive.Copy() if i == t.activeTab { style = t.common.Styles.TabActive.Copy() } diff --git a/ui/components/viewport/viewport.go b/ui/components/viewport/viewport.go index 6de1359726c7c5dd707c5f2885098bdebbb2dad0..0521740eec7b79ea2ead6afe04119bbca9138afa 100644 --- a/ui/components/viewport/viewport.go +++ b/ui/components/viewport/viewport.go @@ -40,3 +40,48 @@ func (v *Viewport) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (v *Viewport) View() string { return v.Viewport.View() } + +// GotoTop moves the viewport to the top of the log. +func (v *Viewport) GotoTop() { + v.Viewport.GotoTop() +} + +// GotoBottom moves the viewport to the bottom of the log. +func (v *Viewport) GotoBottom() { + v.Viewport.GotoBottom() +} + +// HalfViewDown moves the viewport down by half the viewport height. +func (v *Viewport) HalfViewDown() { + v.Viewport.HalfViewDown() +} + +// HalfViewUp moves the viewport up by half the viewport height. +func (v *Viewport) HalfViewUp() { + v.Viewport.HalfViewUp() +} + +// ViewUp moves the viewport up by a page. +func (v *Viewport) ViewUp() []string { + return v.Viewport.ViewUp() +} + +// ViewDown moves the viewport down by a page. +func (v *Viewport) ViewDown() []string { + return v.Viewport.ViewDown() +} + +// LineUp moves the viewport up by the given number of lines. +func (v *Viewport) LineUp(n int) []string { + return v.Viewport.LineUp(n) +} + +// LineDown moves the viewport down by the given number of lines. +func (v *Viewport) LineDown(n int) []string { + return v.Viewport.LineDown(n) +} + +// ScrollPercent returns the viewport's scroll percentage. +func (v *Viewport) ScrollPercent() float64 { + return v.Viewport.ScrollPercent() +} diff --git a/ui/keymap/keymap.go b/ui/keymap/keymap.go index 9a3cac2b7dbd4c5942eecc7b687b471db8e55d4c..ceeb81eaf079d34232430098a52f114526500922 100644 --- a/ui/keymap/keymap.go +++ b/ui/keymap/keymap.go @@ -13,6 +13,8 @@ type KeyMap struct { Select key.Binding Section key.Binding Back key.Binding + PrevPage key.Binding + NextPage key.Binding } // DefaultKeyMap returns the default key map. @@ -126,5 +128,29 @@ func DefaultKeyMap() *KeyMap { ), ) + km.PrevPage = key.NewBinding( + key.WithKeys( + "pgup", + "b", + "u", + ), + key.WithHelp( + "pgup", + "prev page", + ), + ) + + km.NextPage = key.NewBinding( + key.WithKeys( + "pgdown", + "f", + "d", + ), + key.WithHelp( + "pgdn", + "next page", + ), + ) + return km } diff --git a/ui/pages/repo/log.go b/ui/pages/repo/log.go index 25d9522f54f802ab67b2378bfd29448f87e794cf..d431b1c245e2b55d1441cf36a94bdb1dec0a2391 100644 --- a/ui/pages/repo/log.go +++ b/ui/pages/repo/log.go @@ -24,10 +24,20 @@ type Log struct { func NewLog(common common.Common) *Log { l := &Log{ common: common, - selector: selector.New(common, []selector.IdentifiableItem{}, LogItemDelegate{common.Styles}), vp: viewport.New(), activeView: logView, } + selector := selector.New(common, []selector.IdentifiableItem{}, LogItemDelegate{common.Styles}) + selector.SetShowFilter(false) + selector.SetShowHelp(false) + selector.SetShowPagination(true) + selector.SetShowStatusBar(false) + selector.SetShowTitle(false) + selector.SetFilteringEnabled(false) + selector.DisableQuitKeybindings() + selector.KeyMap.NextPage = common.Keymap.NextPage + selector.KeyMap.PrevPage = common.Keymap.PrevPage + l.selector = selector return l } @@ -37,6 +47,47 @@ 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 } diff --git a/ui/pages/repo/repo.go b/ui/pages/repo/repo.go index e8c9d5e11110817afa20ea468bbbb07bf524dbc4..8fa689fe86d94d445867d68bea685b0e026fcd68 100644 --- a/ui/pages/repo/repo.go +++ b/ui/pages/repo/repo.go @@ -1,6 +1,8 @@ package repo import ( + "fmt" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -23,8 +25,13 @@ const ( tagsTab ) +// RepoMsg is a message that contains a git.Repository. type RepoMsg git.GitRepo +// RefMsg is a message that contains a git.Reference. +type RefMsg *ggit.Reference + +// Repo is a view for a git repository. type Repo struct { common common.Common rs git.GitRepoSource @@ -37,32 +44,39 @@ type Repo struct { ref *ggit.Reference } +// New returns a new Repo. func New(common common.Common, rs git.GitRepoSource) *Repo { sb := statusbar.New(common) tb := tabs.New(common, []string{"Readme", "Files", "Commits", "Branches", "Tags"}) readme := code.New(common, "", "") readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.") + log := NewLog(common) r := &Repo{ common: common, rs: rs, tabs: tb, statusbar: sb, readme: readme, + log: log, } return r } +// SetSize implements common.Component. func (r *Repo) SetSize(width, height int) { r.common.SetSize(width, height) - hm := 4 + hm := r.common.Styles.RepoBody.GetVerticalFrameSize() + + r.common.Styles.RepoHeader.GetHeight() + + r.common.Styles.RepoHeader.GetVerticalFrameSize() + + r.common.Styles.StatusBar.GetHeight() + + r.common.Styles.Tabs.GetHeight() r.tabs.SetSize(width, height-hm) r.statusbar.SetSize(width, height-hm) r.readme.SetSize(width, height-hm) - if r.log != nil { - r.log.SetSize(width, height-hm) - } + r.log.SetSize(width, height-hm) } +// ShortHelp implements help.KeyMap. func (r *Repo) ShortHelp() []key.Binding { b := make([]key.Binding, 0) tab := r.common.Keymap.Section @@ -72,15 +86,18 @@ func (r *Repo) ShortHelp() []key.Binding { return b } +// FullHelp implements help.KeyMap. func (r *Repo) FullHelp() [][]key.Binding { b := make([][]key.Binding, 0) return b } +// Init implements tea.View. func (r *Repo) Init() tea.Cmd { return nil } +// Update implements tea.Model. func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { @@ -89,9 +106,23 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, r.tabs.Init(), r.setRepoCmd(string(msg))) case RepoMsg: r.selectedRepo = git.GitRepo(msg) - cmds = append(cmds, r.updateStatusBarCmd, r.updateReadmeCmd) + r.readme.GotoTop() + cmds = append(cmds, + r.updateReadmeCmd, + r.updateRefCmd, + ) + case RefMsg: + r.ref = msg + cmds = append(cmds, + r.updateStatusBarCmd, + r.log.Init(), + ) case tabs.ActiveTabMsg: r.activeTab = tab(msg) + case tea.KeyMsg, tea.MouseMsg: + if r.selectedRepo != nil { + cmds = append(cmds, r.updateStatusBarCmd) + } } t, cmd := r.tabs.Update(msg) r.tabs = t.(*tabs.Tabs) @@ -112,10 +143,6 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case filesTab: case commitsTab: - if r.log == nil { - r.log = NewLog(r.common) - cmds = append(cmds, r.log.Init()) - } l, cmd := r.log.Update(msg) r.log = l.(*Log) if cmd != nil { @@ -127,31 +154,49 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return r, tea.Batch(cmds...) } +// View implements tea.Model. func (r *Repo) View() string { - s := r.common.Styles.RepoBody.Copy(). + s := r.common.Styles.Repo.Copy(). Width(r.common.Width). Height(r.common.Height) - mainStyle := lipgloss.NewStyle(). - Height(r.common.Height-4). - Margin(1, 0) + repoBodyStyle := r.common.Styles.RepoBody.Copy() + hm := repoBodyStyle.GetVerticalFrameSize() + + r.common.Styles.RepoHeader.GetHeight() + + r.common.Styles.RepoHeader.GetVerticalFrameSize() + + r.common.Styles.StatusBar.GetHeight() + + r.common.Styles.Tabs.GetHeight() + mainStyle := repoBodyStyle. + Height(r.common.Height - hm) main := mainStyle.Render("") switch r.activeTab { case readmeTab: main = mainStyle.Render(r.readme.View()) case filesTab: case commitsTab: - if r.log != nil { - main = mainStyle.Render(r.log.View()) - } + main = mainStyle.Render(r.log.View()) } view := lipgloss.JoinVertical(lipgloss.Top, - r.tabs.View(), + r.headerView(), main, r.statusbar.View(), ) return s.Render(view) } +func (r *Repo) headerView() string { + if r.selectedRepo == nil { + return "" + } + name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name()) + style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width) + return style.Render( + lipgloss.JoinVertical(lipgloss.Top, + name, + r.tabs.View(), + ), + ) +} + func (r *Repo) setRepoCmd(repo string) tea.Cmd { return func() tea.Msg { for _, r := range r.rs.AllRepos() { @@ -164,15 +209,16 @@ func (r *Repo) setRepoCmd(repo string) tea.Cmd { } func (r *Repo) updateStatusBarCmd() tea.Msg { - branch, err := r.selectedRepo.HEAD() - if err != nil { - return common.ErrorMsg(err) + info := "" + switch r.activeTab { + case readmeTab: + info = fmt.Sprintf("%.f%%", r.readme.ScrollPercent()*100) } return statusbar.StatusBarMsg{ Key: r.selectedRepo.Name(), Value: "", - Info: "", - Branch: branch.Name().Short(), + Info: info, + Branch: r.ref.Name().Short(), } } @@ -183,3 +229,11 @@ func (r *Repo) updateReadmeCmd() tea.Msg { rm, rp := r.selectedRepo.Readme() return r.readme.SetContent(rm, rp) } + +func (r *Repo) updateRefCmd() tea.Msg { + head, err := r.selectedRepo.HEAD() + if err != nil { + return common.ErrorMsg(err) + } + return RefMsg(head) +} diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index 11a04c1a8b12bb2b512f3dd818495a0340ff4e9d..c7f98696c70d5db7d828517ffc6bf6e4563956bf 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -41,10 +41,15 @@ func New(s session.Session, common common.Common) *Selection { } readme := code.New(common, "", "") readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.") - sel.readme = readme - sel.selector = selector.New(common, + selector := selector.New(common, []selector.IdentifiableItem{}, ItemDelegate{common.Styles, &sel.activeBox}) + selector.SetShowTitle(false) + selector.SetShowHelp(false) + selector.SetShowStatusBar(false) + selector.DisableQuitKeybindings() + sel.selector = selector + sel.readme = readme return sel } @@ -66,7 +71,7 @@ func (s *Selection) SetSize(width, height int) { // ShortHelp implements help.KeyMap. func (s *Selection) ShortHelp() []key.Binding { - k := s.selector.KeyMap() + k := s.selector.KeyMap kb := make([]key.Binding, 0) kb = append(kb, s.common.Keymap.UpDown, @@ -85,7 +90,7 @@ func (s *Selection) ShortHelp() []key.Binding { // FullHelp implements help.KeyMap. // TODO implement full help on ? func (s *Selection) FullHelp() [][]key.Binding { - k := s.selector.KeyMap() + k := s.selector.KeyMap return [][]key.Binding{ { k.CursorUp, diff --git a/ui/styles/styles.go b/ui/styles/styles.go index 33f2003a0689a915d855e29177c486afcc6b84fe..4b1a3d9b2204e9836adc7417e3fa6fc5f4bb4d1f 100644 --- a/ui/styles/styles.go +++ b/ui/styles/styles.go @@ -28,11 +28,14 @@ type Styles struct { RepoNoteBorder lipgloss.Border RepoBodyBorder lipgloss.Border - RepoTitle lipgloss.Style - RepoTitleBox lipgloss.Style - RepoNote lipgloss.Style - RepoNoteBox lipgloss.Style - RepoBody lipgloss.Style + Repo lipgloss.Style + RepoTitle lipgloss.Style + RepoTitleBox lipgloss.Style + RepoNote lipgloss.Style + RepoNoteBox lipgloss.Style + RepoBody lipgloss.Style + RepoHeader lipgloss.Style + RepoHeaderName lipgloss.Style Footer lipgloss.Style Branch lipgloss.Style @@ -80,12 +83,14 @@ type Styles struct { CodeNoContent lipgloss.Style + StatusBar lipgloss.Style StatusBarKey lipgloss.Style StatusBarValue lipgloss.Style StatusBarInfo lipgloss.Style StatusBarBranch lipgloss.Style - Tab lipgloss.Style + Tabs lipgloss.Style + TabInactive lipgloss.Style TabActive lipgloss.Style TabSeparator lipgloss.Style } @@ -166,6 +171,8 @@ func DefaultStyles() *Styles { BottomRight: "╯", } + s.Repo = lipgloss.NewStyle() + s.RepoTitle = lipgloss.NewStyle(). Padding(0, 2) @@ -185,7 +192,16 @@ func DefaultStyles() *Styles { BorderBottom(true). BorderLeft(false) - s.RepoBody = lipgloss.NewStyle() + s.RepoBody = lipgloss.NewStyle(). + Margin(1, 0) + + s.RepoHeader = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, true, false). + BorderForeground(lipgloss.Color("241")) + + s.RepoHeaderName = lipgloss.NewStyle(). + Foreground(lipgloss.Color("15")). + Bold(true) s.Footer = lipgloss.NewStyle(). Height(1) @@ -308,6 +324,9 @@ func DefaultStyles() *Styles { MarginLeft(2). Foreground(lipgloss.Color("#626262")) + s.StatusBar = lipgloss.NewStyle(). + Height(1) + s.StatusBarKey = lipgloss.NewStyle(). Bold(true). Padding(0, 1). @@ -329,8 +348,11 @@ func DefaultStyles() *Styles { Background(lipgloss.Color("#6E6ED8")). Foreground(lipgloss.Color("#F1F1F1")) - s.Tab = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#F1F1F1")) + s.Tabs = lipgloss.NewStyle(). + Height(1) + + s.TabInactive = lipgloss.NewStyle(). + Foreground(lipgloss.Color("15")) s.TabActive = lipgloss.NewStyle(). Foreground(lipgloss.Color("#6E6ED8")). @@ -339,7 +361,7 @@ func DefaultStyles() *Styles { s.TabSeparator = lipgloss.NewStyle(). SetString("│"). Padding(0, 1). - Foreground(lipgloss.Color("#777777")) + Foreground(lipgloss.Color("241")) return s } diff --git a/ui/ui.go b/ui/ui.go index 4c024aabbc680f5e436706353d448448041738fc..ebfa93fda3835c995a9374ab96f828e32cfe09cd 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,6 +1,7 @@ package ui import ( + "log" "strings" "github.com/charmbracelet/bubbles/key" @@ -102,8 +103,9 @@ func (ui *UI) Init() tea.Cmd { } // Update implements tea.Model. -// TODO update help when page change. +// TODO show full help func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Printf("%T", msg) cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { case tea.WindowSizeMsg: