diff --git a/go.mod b/go.mod index b5b61ba8467e815e10ddc4f04ea1beeda441c675..76276e27025cac6c280f9c7f1fb64153dda1272e 100755 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.17 require ( github.com/alecthomas/chroma v0.10.0 github.com/caarlos0/env/v6 v6.9.1 - github.com/charmbracelet/bubbles v0.10.4-0.20220302223835-88562515cf7b + github.com/charmbracelet/bubbles v0.10.4-0.20220412141214-292a1dd7ba97 github.com/charmbracelet/bubbletea v0.20.0 github.com/charmbracelet/glamour v0.4.0 github.com/charmbracelet/lipgloss v0.4.0 diff --git a/go.sum b/go.sum index f7bcd5cc562374a802e4bddd7bc1101ce0d85ff5..c1d1aed15ec05f1721c66dacd23c223ac40317a0 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,12 @@ github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k github.com/caarlos0/env/v6 v6.9.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3 h1:w2ANoiT4ubmh4Nssa3/QW1M7lj3FZkma8f8V5aBDxXM= github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA= +github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho= +github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= github.com/charmbracelet/bubbles v0.10.4-0.20220302223835-88562515cf7b h1:o+LFpRn1fXtu1hDJLtBFjp7tMZ8AqwSpl84w1TnUj0Y= github.com/charmbracelet/bubbles v0.10.4-0.20220302223835-88562515cf7b/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= +github.com/charmbracelet/bubbles v0.10.4-0.20220412141214-292a1dd7ba97 h1:NJqAUfS+JNHqodsbhLR0zD3sDkXI7skwjAwd77HXe/Q= +github.com/charmbracelet/bubbles v0.10.4-0.20220412141214-292a1dd7ba97/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc= github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM= diff --git a/server/session.go b/server/session.go index 92f7273007ceaecdcf6da9ee52ed0c5ab6465d4a..fc5bfb797e354d1bbdc74df99e850721d1264ec2 100644 --- a/server/session.go +++ b/server/session.go @@ -50,7 +50,7 @@ func SessionHandler(ac *appCfg.Config) bm.ProgramHandler { if ac.Cfg.Callbacks != nil { ac.Cfg.Callbacks.Tui("new session") } - c := &common.Common{ + c := common.Common{ Styles: styles.DefaultStyles(), Keymap: keymap.DefaultKeyMap(), Width: pty.Window.Width, diff --git a/ui/common/common.go b/ui/common/common.go index 9a17f389b4a45d302a1896368f912173b0b4a435..90379ef82e605b28a51f9ba5d74a39c66f5df10e 100644 --- a/ui/common/common.go +++ b/ui/common/common.go @@ -11,3 +11,8 @@ type Common struct { Width int Height int } + +func (c Common) SetSize(width, height int) { + c.Width = width + c.Height = height +} diff --git a/ui/common/component.go b/ui/common/component.go new file mode 100644 index 0000000000000000000000000000000000000000..ee6b6874160389c99ab242309451bf134a7c07c1 --- /dev/null +++ b/ui/common/component.go @@ -0,0 +1,16 @@ +package common + +import ( + "github.com/charmbracelet/bubbles/help" + tea "github.com/charmbracelet/bubbletea" +) + +type Component interface { + tea.Model + SetSize(width, height int) +} + +type Page interface { + Component + help.KeyMap +} diff --git a/ui/components/footer/footer.go b/ui/components/footer/footer.go new file mode 100644 index 0000000000000000000000000000000000000000..4a43be97d6d4a105bb3a108fec257b46f8add174 --- /dev/null +++ b/ui/components/footer/footer.go @@ -0,0 +1,46 @@ +package footer + +import ( + "github.com/charmbracelet/bubbles/help" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/soft-serve/ui/common" +) + +type Footer struct { + common common.Common + help help.Model + keymap help.KeyMap +} + +func New(c common.Common, keymap help.KeyMap) *Footer { + h := help.New() + h.Styles.ShortKey = c.Styles.HelpKey + h.Styles.FullKey = c.Styles.HelpKey + f := &Footer{ + common: c, + help: h, + keymap: keymap, + } + return f +} + +func (f *Footer) SetSize(width, height int) { + f.common.Width = width + f.common.Height = height +} + +func (f *Footer) Init() tea.Cmd { + return nil +} + +func (f *Footer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return f, nil +} + +func (f *Footer) View() string { + if f.keymap == nil { + return "" + } + s := f.common.Styles.Footer.Copy().Width(f.common.Width) + return s.Render(f.help.View(f.keymap)) +} diff --git a/ui/components/header/header.go b/ui/components/header/header.go new file mode 100644 index 0000000000000000000000000000000000000000..50fb715176ca945c59ff561def49b3c4b22c7653 --- /dev/null +++ b/ui/components/header/header.go @@ -0,0 +1,39 @@ +package header + +import ( + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/soft-serve/ui/common" +) + +type Header struct { + common common.Common + text string +} + +func New(c common.Common, text string) *Header { + h := &Header{ + common: c, + text: text, + } + return h +} + +func (h *Header) SetSize(width, height int) { + h.common.Width = width + h.common.Height = height +} + +func (h *Header) Init() tea.Cmd { + return nil +} + +func (h *Header) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return h, nil +} + +func (h *Header) View() string { + s := h.common.Styles.Header.Copy().Width(h.common.Width) + return s.Render(strings.TrimSpace(h.text)) +} diff --git a/ui/components/selector/item.go b/ui/components/selector/item.go index ce2f59a4c05c1c543a7f7b7de5e0b6825727814a..67a23e66c48796df6d0639eac74ec718eb30b912 100644 --- a/ui/components/selector/item.go +++ b/ui/components/selector/item.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/soft-serve/ui/components/yankable" "github.com/charmbracelet/soft-serve/ui/styles" "github.com/dustin/go-humanize" @@ -80,13 +81,23 @@ func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { i := listItem.(Item) s := strings.Builder{} - style := d.styles.MenuItem + style := d.styles.MenuItem.Copy() if index == m.Index() { - style = d.styles.SelectedMenuItem + style = d.styles.SelectedMenuItem.Copy() } - updated := d.styles.MenuLastUpdate.Render(fmt.Sprintf("Updated %s", humanize.Time(i.LastUpdate))) + style.Width(m.Width() - 2) // FIXME figure out where this "2" comes from + titleStr := i.Title + updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.LastUpdate)) + updated := d.styles.MenuLastUpdate. + Copy(). + Width(m.Width() - style.GetHorizontalFrameSize() - lipgloss.Width(titleStr)). + Render(updatedStr) + title := lipgloss.NewStyle(). + Align(lipgloss.Left). + Width(m.Width() - style.GetHorizontalFrameSize() - lipgloss.Width(updated)). + Render(titleStr) - s.WriteString(fmt.Sprintf("%s %s", i.Title, updated)) + s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated)) s.WriteString("\n") s.WriteString(i.Description) s.WriteString("\n\n") diff --git a/ui/components/selector/selector.go b/ui/components/selector/selector.go index 6971d06559c3818db3c26c18765bb8a596b91843..ee9ac8ba04b06bad70f8acdd870021fb50230275 100644 --- a/ui/components/selector/selector.go +++ b/ui/components/selector/selector.go @@ -8,10 +8,10 @@ import ( type Selector struct { list list.Model - common *common.Common + common common.Common } -func New(common *common.Common, items []list.Item) *Selector { +func New(common common.Common, items []list.Item) *Selector { l := list.New(items, ItemDelegate{common.Styles}, common.Width, common.Height) l.SetShowTitle(false) l.SetShowHelp(false) @@ -21,10 +21,16 @@ func New(common *common.Common, items []list.Item) *Selector { list: l, common: common, } + s.SetSize(common.Width, common.Height) return s } +func (s *Selector) KeyMap() list.KeyMap { + return s.list.KeyMap +} + func (s *Selector) SetSize(width, height int) { + s.common.SetSize(width, height) s.list.SetSize(width, height) } diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index ddcf5b9ca34d4de0c0b8ef9ae4e30179f9d06686..fe42467372100438a3ab69db1661647f3098875d 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -16,11 +17,11 @@ import ( type Selection struct { s session.Session - common *common.Common + common common.Common selector *selector.Selector } -func New(s session.Session, common *common.Common) *Selection { +func New(s session.Session, common common.Common) *Selection { sel := &Selection{ s: s, common: common, @@ -29,6 +30,46 @@ func New(s session.Session, common *common.Common) *Selection { return sel } +func (s *Selection) SetSize(width, height int) { + s.common.SetSize(width, height) + s.selector.SetSize(width, height) +} + +func (s *Selection) ShortHelp() []key.Binding { + k := s.selector.KeyMap() + return []key.Binding{ + s.common.Keymap.UpDown, + s.common.Keymap.Select, + k.Filter, + k.ClearFilter, + } +} + +func (s *Selection) FullHelp() [][]key.Binding { + k := s.selector.KeyMap() + return [][]key.Binding{ + { + k.CursorUp, + k.CursorDown, + k.NextPage, + k.PrevPage, + k.GoToStart, + k.GoToEnd, + }, + { + k.Filter, + k.ClearFilter, + k.CancelWhileFiltering, + k.AcceptWhileFiltering, + k.ShowFullHelp, + k.CloseFullHelp, + }, + // Ignore the following keys: + // k.Quit, + // k.ForceQuit, + } +} + func (s *Selection) Init() tea.Cmd { items := make([]list.Item, 0) cfg := s.s.Config() diff --git a/ui/styles/styles.go b/ui/styles/styles.go index 5a5d4951f87f1354ef90c1fb995cd5970db77b45..58837ad807f225541acc2e4c5d16132ce980062f 100644 --- a/ui/styles/styles.go +++ b/ui/styles/styles.go @@ -87,8 +87,9 @@ func DefaultStyles() *Styles { Margin(1, 2) s.Header = lipgloss.NewStyle(). - Foreground(lipgloss.Color("62")). + Foreground(lipgloss.Color("15")). Align(lipgloss.Right). + Height(1). Bold(true) s.Menu = lipgloss.NewStyle(). @@ -109,7 +110,8 @@ func DefaultStyles() *Styles { BorderForeground(lipgloss.Color("241")) s.MenuLastUpdate = lipgloss.NewStyle(). - Foreground(lipgloss.Color("241")) + Foreground(lipgloss.Color("241")). + Align(lipgloss.Right) s.SelectedMenuItem = s.MenuItem.Copy(). BorderForeground(s.ActiveBorderColor) @@ -172,7 +174,7 @@ func DefaultStyles() *Styles { PaddingRight(1) s.Footer = lipgloss.NewStyle(). - MarginTop(1) + Height(1) s.Branch = lipgloss.NewStyle(). Foreground(lipgloss.Color("203")). diff --git a/ui/ui.go b/ui/ui.go index 60243b327a49d8919f92a5e917ae49a7f3cb022c..57ae6aff6adc542254355b131b46ec7875bec5d6 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,9 +1,14 @@ package ui import ( + "strings" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/soft-serve/ui/common" + "github.com/charmbracelet/soft-serve/ui/components/footer" + "github.com/charmbracelet/soft-serve/ui/components/header" "github.com/charmbracelet/soft-serve/ui/pages/selection" "github.com/charmbracelet/soft-serve/ui/session" ) @@ -18,72 +23,108 @@ const ( type UI struct { s session.Session - common *common.Common - pages []tea.Model + common common.Common + pages []common.Page activePage int state sessionState + header *header.Header + footer *footer.Footer } -func New(s session.Session, common *common.Common, initialRepo string) *UI { +func New(s session.Session, c common.Common, initialRepo string) *UI { + h := header.New(c, s.Config().Name) ui := &UI{ s: s, - common: common, - pages: make([]tea.Model, 2), // selection & repo + common: c, + pages: make([]common.Page, 2), // selection & repo activePage: 0, state: startState, + header: h, } + ui.footer = footer.New(c, ui) + ui.SetSize(c.Width, c.Height) return ui } -func (ui *UI) Init() tea.Cmd { - items := make([]string, 0) - cfg := ui.s.Config() - for _, r := range cfg.Repos { - items = append(items, r.Name) - } - for _, r := range cfg.Source.AllRepos() { - exists := false - for _, i := range items { - if i == r.Name() { - exists = true - break - } - } - if !exists { - items = append(items, r.Name()) +func (ui *UI) getMargins() (wm, hm int) { + wm = ui.common.Styles.App.GetHorizontalFrameSize() + hm = ui.common.Styles.App.GetVerticalFrameSize() + + ui.common.Styles.Header.GetHeight() + + ui.common.Styles.Footer.GetHeight() + return +} + +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) + return b +} + +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}) + return b +} + +func (ui *UI) SetSize(width, height int) { + ui.common.SetSize(width, height) + wm, hm := ui.getMargins() + ui.header.SetSize(width-wm, height-hm) + ui.footer.SetSize(width-wm, height-hm) + for _, p := range ui.pages { + if p != nil { + p.SetSize(width-wm, height-hm) } } +} + +func (ui *UI) Init() tea.Cmd { ui.pages[0] = selection.New(ui.s, ui.common) ui.pages[1] = selection.New(ui.s, ui.common) + ui.SetSize(ui.common.Width, ui.common.Height) ui.state = loadedState return ui.pages[ui.activePage].Init() } +// TODO update help when page change. func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { case tea.WindowSizeMsg: + h, cmd := ui.header.Update(msg) + ui.header = h.(*header.Header) + if cmd != nil { + cmds = append(cmds, cmd) + } + f, cmd := ui.footer.Update(msg) + ui.footer = f.(*footer.Footer) + if cmd != nil { + cmds = append(cmds, cmd) + } for i, p := range ui.pages { m, cmd := p.Update(msg) - ui.pages[i] = m + ui.pages[i] = m.(common.Page) if cmd != nil { cmds = append(cmds, cmd) } } + ui.SetSize(msg.Width, msg.Height) case tea.KeyMsg: switch { case key.Matches(msg, ui.common.Keymap.Quit): return ui, tea.Quit default: m, cmd := ui.pages[ui.activePage].Update(msg) - ui.pages[ui.activePage] = m + ui.pages[ui.activePage] = m.(common.Page) if cmd != nil { cmds = append(cmds, cmd) } } default: m, cmd := ui.pages[ui.activePage].Update(msg) - ui.pages[ui.activePage] = m + ui.pages[ui.activePage] = m.(common.Page) if cmd != nil { cmds = append(cmds, cmd) } @@ -92,14 +133,21 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (ui *UI) View() string { + s := strings.Builder{} switch ui.state { case startState: return "Loading..." case errorState: return "Error" case loadedState: - return ui.common.Styles.App.Render(ui.pages[ui.activePage].View()) + s.WriteString(lipgloss.JoinVertical( + lipgloss.Bottom, + ui.header.View(), + ui.pages[ui.activePage].View(), + ui.footer.View(), + )) default: return "Unknown state :/ this is a bug!" } + return ui.common.Styles.App.Render(s.String()) }