diff --git a/ui/components/statusbar/statusbar.go b/ui/components/statusbar/statusbar.go index fc03f00ca1a422318de7194a4d898002bfc8265c..47145c02883f8823fdd3954d00fd569377f9aeae 100644 --- a/ui/components/statusbar/statusbar.go +++ b/ui/components/statusbar/statusbar.go @@ -18,6 +18,11 @@ type StatusBar struct { msg StatusBarMsg } +type Model interface { + StatusBarValue() string + StatusBarInfo() string +} + func New(c common.Common) *StatusBar { s := &StatusBar{ common: c, diff --git a/ui/components/tabs/tabs.go b/ui/components/tabs/tabs.go index 63081a849f8f16f4a62edd59a9ae659c4ab8bd6a..f7be636f87720495219729a82fdb44cce93990e9 100644 --- a/ui/components/tabs/tabs.go +++ b/ui/components/tabs/tabs.go @@ -7,6 +7,8 @@ import ( "github.com/charmbracelet/soft-serve/ui/common" ) +type SelectTabMsg int + type ActiveTabMsg int type Tabs struct { @@ -45,6 +47,8 @@ func (t *Tabs) Update(msg tea.Msg) (tea.Model, tea.Cmd) { t.activeTab = (t.activeTab - 1 + len(t.tabs)) % len(t.tabs) cmds = append(cmds, t.activeTabCmd) } + case SelectTabMsg: + t.activeTab = int(msg) } return t, tea.Batch(cmds...) } @@ -68,3 +72,9 @@ func (t *Tabs) View() string { func (t *Tabs) activeTabCmd() tea.Msg { return ActiveTabMsg(t.activeTab) } + +func SelectTabCmd(tab int) tea.Cmd { + return func() tea.Msg { + return SelectTabMsg(tab) + } +} diff --git a/ui/pages/repo/log.go b/ui/pages/repo/log.go index f7c235c23cd3bf5e02fdb12651c7a751a7854382..20cedda7af85d6b9b2efda869edd08eba3b7d4cf 100644 --- a/ui/pages/repo/log.go +++ b/ui/pages/repo/log.go @@ -116,6 +116,10 @@ func (l *Log) ShortHelp() []key.Binding { } } +func (l Log) FullHelp() [][]key.Binding { + return [][]key.Binding{} +} + // Init implements tea.Model. func (l *Log) Init() tea.Cmd { cmds := make([]tea.Cmd, 0) diff --git a/ui/pages/repo/refs.go b/ui/pages/repo/refs.go new file mode 100644 index 0000000000000000000000000000000000000000..1a1f49207abf96910b8218bb041961447edaa568 --- /dev/null +++ b/ui/pages/repo/refs.go @@ -0,0 +1,126 @@ +package repo + +import ( + "sort" + "strings" + + 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/tabs" + "github.com/charmbracelet/soft-serve/ui/git" +) + +type RefItemsMsg struct { + prefix string + items []selector.IdentifiableItem +} + +type Refs struct { + common common.Common + selector *selector.Selector + repo git.GitRepo + ref *ggit.Reference + refPrefix string +} + +func NewRefs(common common.Common, refPrefix string) *Refs { + r := &Refs{ + common: common, + refPrefix: refPrefix, + } + s := selector.New(common, []selector.IdentifiableItem{}, RefItemDelegate{common.Styles}) + s.SetShowFilter(false) + s.SetShowHelp(false) + s.SetShowPagination(true) + s.SetShowStatusBar(false) + s.SetShowTitle(false) + s.SetFilteringEnabled(false) + s.DisableQuitKeybindings() + r.selector = s + return r +} + +func (r *Refs) SetSize(width, height int) { + r.common.SetSize(width, height) + r.selector.SetSize(width, height) +} + +func (r *Refs) Init() tea.Cmd { + return r.updateItemsCmd +} + +func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + cmds := make([]tea.Cmd, 0) + switch msg := msg.(type) { + case RepoMsg: + r.selector.Select(0) + r.repo = git.GitRepo(msg) + cmds = append(cmds, r.Init()) + case RefMsg: + r.ref = msg + cmds = append(cmds, r.Init()) + case RefItemsMsg: + cmds = append(cmds, r.selector.SetItems(msg.items)) + case selector.SelectMsg: + switch i := msg.IdentifiableItem.(type) { + case RefItem: + cmds = append(cmds, + switchRefCmd(i.Reference), + tabs.SelectTabCmd(int(filesTab)), + ) + } + case tea.KeyMsg: + switch msg.String() { + case "l", "right": + cmds = append(cmds, r.selector.SelectItem) + } + } + m, cmd := r.selector.Update(msg) + r.selector = m.(*selector.Selector) + if cmd != nil { + cmds = append(cmds, cmd) + } + return r, tea.Batch(cmds...) +} + +func (r *Refs) View() string { + return r.selector.View() +} + +func (r *Refs) StatusBarValue() string { + return "" +} + +func (r *Refs) StatusBarInfo() string { + return "" +} + +func (r *Refs) updateItemsCmd() tea.Msg { + its := make(RefItems, 0) + refs, err := r.repo.References() + if err != nil { + return common.ErrorMsg(err) + } + for _, ref := range refs { + if strings.HasPrefix(ref.Name().String(), r.refPrefix) { + its = append(its, RefItem{ref}) + } + } + sort.Sort(its) + items := make([]selector.IdentifiableItem, len(its)) + for i, it := range its { + items[i] = it + } + return RefItemsMsg{ + items: items, + prefix: r.refPrefix, + } +} + +func switchRefCmd(ref *ggit.Reference) tea.Cmd { + return func() tea.Msg { + return RefMsg(ref) + } +} diff --git a/ui/pages/repo/refsitem.go b/ui/pages/repo/refsitem.go new file mode 100644 index 0000000000000000000000000000000000000000..1a1d0d876fdd139554277b984209504f36fd9c33 --- /dev/null +++ b/ui/pages/repo/refsitem.go @@ -0,0 +1,75 @@ +package repo + +import ( + "fmt" + "io" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/soft-serve/git" + "github.com/charmbracelet/soft-serve/tui/common" + "github.com/charmbracelet/soft-serve/ui/styles" +) + +type RefItem struct { + *git.Reference +} + +func (i RefItem) ID() string { + return i.Reference.Name().String() +} + +func (i RefItem) Title() string { + return i.Reference.Name().Short() +} + +func (i RefItem) Description() string { + return "" +} + +func (i RefItem) Short() string { + return i.Reference.Name().Short() +} + +func (i RefItem) FilterValue() string { return i.Short() } + +type RefItems []RefItem + +func (cl RefItems) Len() int { return len(cl) } +func (cl RefItems) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] } +func (cl RefItems) Less(i, j int) bool { + return cl[i].Short() < cl[j].Short() +} + +type RefItemDelegate struct { + style *styles.Styles +} + +func (d RefItemDelegate) Height() int { return 1 } +func (d RefItemDelegate) Spacing() int { return 0 } +func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } +func (d RefItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + s := d.style + i, ok := listItem.(RefItem) + if !ok { + return + } + + ref := i.Short() + if i.Reference.IsTag() { + ref = s.RefItemTag.Render(ref) + } + ref = s.RefItemBranch.Render(ref) + refMaxWidth := m.Width() - + s.RefItemSelector.GetMarginLeft() - + s.RefItemSelector.GetWidth() - + s.RefItemInactive.GetMarginLeft() + ref = common.TruncateString(ref, refMaxWidth, "…") + if index == m.Index() { + fmt.Fprint(w, s.RefItemSelector.Render(">")+ + s.RefItemActive.Render(ref)) + } else { + fmt.Fprint(w, s.LogItemSelector.Render(" ")+ + s.RefItemInactive.Render(ref)) + } +} diff --git a/ui/pages/repo/repo.go b/ui/pages/repo/repo.go index 355fcd0eb9894e7f2dfffe342233e99fdf5412da..deddb798e597607600f98ece1b795714df8556fb 100644 --- a/ui/pages/repo/repo.go +++ b/ui/pages/repo/repo.go @@ -3,6 +3,7 @@ package repo import ( "fmt" + "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -26,6 +27,7 @@ const ( tagsTab ) +// UpdateStatusBarMsg updates the status bar. type UpdateStatusBarMsg struct{} // RepoMsg is a message that contains a git.Repository. @@ -43,28 +45,33 @@ type Repo struct { activeTab tab tabs *tabs.Tabs statusbar *statusbar.StatusBar - readme *code.Code - log *Log - files *Files + boxes []common.Component 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, "", "") +func New(c common.Common, rs git.GitRepoSource) *Repo { + sb := statusbar.New(c) + tb := tabs.New(c, []string{"Readme", "Files", "Commits", "Branches", "Tags"}) + readme := code.New(c, "", "") readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.") - log := NewLog(common) - files := NewFiles(common) + log := NewLog(c) + files := NewFiles(c) + branches := NewRefs(c, ggit.RefsHeads) + tags := NewRefs(c, ggit.RefsTags) + boxes := []common.Component{ + readme, + files, + log, + branches, + tags, + } r := &Repo{ - common: common, + common: c, rs: rs, tabs: tb, statusbar: sb, - readme: readme, - log: log, - files: files, + boxes: boxes, } return r } @@ -80,9 +87,9 @@ func (r *Repo) SetSize(width, height int) { r.common.Styles.Tabs.GetVerticalFrameSize() r.tabs.SetSize(width, height-hm) r.statusbar.SetSize(width, height-hm) - r.readme.SetSize(width, height-hm) - r.log.SetSize(width, height-hm) - r.files.SetSize(width, height-hm) + for _, b := range r.boxes { + b.SetSize(width, height-hm) + } } // ShortHelp implements help.KeyMap. @@ -98,7 +105,7 @@ func (r *Repo) ShortHelp() []key.Binding { case readmeTab: b = append(b, r.common.KeyMap.UpDown) case commitsTab: - b = append(b, r.log.ShortHelp()...) + b = append(b, r.boxes[commitsTab].(help.KeyMap).ShortHelp()...) } return b } @@ -126,7 +133,7 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case RepoMsg: r.activeTab = 0 r.selectedRepo = git.GitRepo(msg) - r.readme.GotoTop() + r.boxes[readmeTab].(*code.Code).GotoTop() cmds = append(cmds, r.tabs.Init(), r.updateReadmeCmd, @@ -135,74 +142,80 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ) case RefMsg: r.ref = msg + for _, b := range r.boxes { + cmds = append(cmds, b.Init()) + } cmds = append(cmds, r.updateStatusBarCmd, - r.log.Init(), - r.files.Init(), r.updateModels(msg), ) + case tabs.SelectTabMsg: + r.activeTab = tab(msg) + t, cmd := r.tabs.Update(msg) + r.tabs = t.(*tabs.Tabs) + 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: + t, cmd := r.tabs.Update(msg) + r.tabs = t.(*tabs.Tabs) + if cmd != nil { + cmds = append(cmds, cmd) + } if r.selectedRepo != nil { cmds = append(cmds, r.updateStatusBarCmd) } case FileItemsMsg: - f, cmd := r.files.Update(msg) - r.files = f.(*Files) + f, cmd := r.boxes[filesTab].Update(msg) + r.boxes[filesTab] = f.(*Files) if cmd != nil { cmds = append(cmds, cmd) } case LogCountMsg, LogItemsMsg: - l, cmd := r.log.Update(msg) - r.log = l.(*Log) + l, cmd := r.boxes[commitsTab].Update(msg) + r.boxes[commitsTab] = l.(*Log) if cmd != nil { cmds = append(cmds, cmd) } + case RefItemsMsg: + switch msg.prefix { + case ggit.RefsHeads: + b, cmd := r.boxes[branchesTab].Update(msg) + r.boxes[branchesTab] = b.(*Refs) + if cmd != nil { + cmds = append(cmds, cmd) + } + case ggit.RefsTags: + t, cmd := r.boxes[tagsTab].Update(msg) + r.boxes[tagsTab] = t.(*Refs) + if cmd != nil { + cmds = append(cmds, cmd) + } + } case UpdateStatusBarMsg: cmds = append(cmds, r.updateStatusBarCmd) case tea.WindowSizeMsg: - b, cmd := r.readme.Update(msg) - r.readme = b.(*code.Code) + b, cmd := r.boxes[readmeTab].Update(msg) + r.boxes[readmeTab] = b.(*code.Code) if cmd != nil { cmds = append(cmds, cmd) } cmds = append(cmds, r.updateModels(msg)) } - t, cmd := r.tabs.Update(msg) - r.tabs = t.(*tabs.Tabs) - if cmd != nil { - cmds = append(cmds, cmd) - } s, cmd := r.statusbar.Update(msg) r.statusbar = s.(*statusbar.StatusBar) if cmd != nil { cmds = append(cmds, cmd) } - switch r.activeTab { - case readmeTab: - b, cmd := r.readme.Update(msg) - r.readme = b.(*code.Code) - if cmd != nil { - cmds = append(cmds, cmd) - } - case filesTab: - f, cmd := r.files.Update(msg) - r.files = f.(*Files) - if cmd != nil { - cmds = append(cmds, cmd) - } - case commitsTab: - l, cmd := r.log.Update(msg) - r.log = l.(*Log) - if cmd != nil { - cmds = append(cmds, cmd) - } - case branchesTab: - case tagsTab: + m, cmd := r.boxes[r.activeTab].Update(msg) + r.boxes[r.activeTab] = m.(common.Component) + if cmd != nil { + cmds = append(cmds, cmd) } return r, tea.Batch(cmds...) } @@ -221,17 +234,7 @@ func (r *Repo) View() string { r.common.Styles.Tabs.GetVerticalFrameSize() mainStyle := repoBodyStyle. Height(r.common.Height - hm) - main := "" - switch r.activeTab { - case readmeTab: - main = r.readme.View() - case filesTab: - main = r.files.View() - case commitsTab: - main = r.log.View() - case branchesTab: - case tagsTab: - } + main := r.boxes[r.activeTab].View() view := lipgloss.JoinVertical(lipgloss.Top, r.headerView(), r.tabs.View(), @@ -277,17 +280,13 @@ func (r *Repo) setRepoCmd(repo string) tea.Cmd { } func (r *Repo) updateStatusBarCmd() tea.Msg { - value := "" - info := "" + var info, value string switch r.activeTab { case readmeTab: - info = fmt.Sprintf("%.f%%", r.readme.ScrollPercent()*100) - case commitsTab: - value = r.log.StatusBarValue() - info = r.log.StatusBarInfo() - case filesTab: - value = r.files.StatusBarValue() - info = r.files.StatusBarInfo() + info = fmt.Sprintf("%.f%%", r.boxes[readmeTab].(*code.Code).ScrollPercent()*100) + default: + value = r.boxes[r.activeTab].(statusbar.Model).StatusBarValue() + info = r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo() } return statusbar.StatusBarMsg{ Key: r.selectedRepo.Name(), @@ -302,7 +301,7 @@ func (r *Repo) updateReadmeCmd() tea.Msg { return common.ErrorCmd(git.ErrMissingRepo) } rm, rp := r.selectedRepo.Readme() - return r.readme.SetContent(rm, rp) + return r.boxes[readmeTab].(*code.Code).SetContent(rm, rp) } func (r *Repo) updateRefCmd() tea.Msg { @@ -315,15 +314,12 @@ func (r *Repo) updateRefCmd() tea.Msg { func (r *Repo) updateModels(msg tea.Msg) tea.Cmd { cmds := make([]tea.Cmd, 0) - l, cmd := r.log.Update(msg) - r.log = l.(*Log) - if cmd != nil { - cmds = append(cmds, cmd) - } - f, cmd := r.files.Update(msg) - r.files = f.(*Files) - if cmd != nil { - cmds = append(cmds, cmd) + for i, b := range r.boxes { + m, cmd := b.Update(msg) + r.boxes[i] = m.(common.Component) + if cmd != nil { + cmds = append(cmds, cmd) + } } return tea.Batch(cmds...) }