Detailed changes
@@ -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
@@ -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=
@@ -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,
@@ -11,3 +11,8 @@ type Common struct {
Width int
Height int
}
+
+func (c Common) SetSize(width, height int) {
+ c.Width = width
+ c.Height = height
+}
@@ -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
+}
@@ -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))
+}
@@ -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))
+}
@@ -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")
@@ -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)
}
@@ -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()
@@ -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")).
@@ -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())
}