From c0ca59979efe2ec91af7bbcfe9970ed27078bf16 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Mon, 13 Nov 2023 16:39:42 -0500 Subject: [PATCH] (bugfix): make tabs paginated Signed-off-by: Bryce Palmer --- go.mod | 6 +- go.sum | 6 ++ pkg/charm/models/dashboard.go | 65 ++++----------- pkg/charm/models/tabber.go | 147 ++++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 50 deletions(-) create mode 100644 pkg/charm/models/tabber.go diff --git a/go.mod b/go.mod index 6b695ff..7b05f2f 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,11 @@ require ( sigs.k8s.io/yaml v1.3.0 ) -require github.com/dlclark/regexp2 v1.4.0 // indirect +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/sahilm/fuzzy v0.1.0 // indirect +) require ( github.com/alecthomas/chroma v0.10.0 diff --git a/go.sum b/go.sum index 0579783..6b565c8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/calyptia/go-bubble-table v0.2.1 h1:NWcVRyGCLuP7QIA29uUFSY+IjmWcmUWHjy5J/CPb0Rk= @@ -73,6 +75,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= @@ -118,6 +122,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/pkg/charm/models/dashboard.go b/pkg/charm/models/dashboard.go index 86c0974..fa7a256 100644 --- a/pkg/charm/models/dashboard.go +++ b/pkg/charm/models/dashboard.go @@ -12,10 +12,9 @@ import ( ) type DashboardKeyMap struct { - Help key.Binding - Quit key.Binding - TabRight key.Binding - TabLeft key.Binding + Help key.Binding + Quit key.Binding + TabberKeys TabberKeyMap } // ShortHelp returns keybindings to be shown in the mini help view. It's part @@ -28,20 +27,12 @@ func (k DashboardKeyMap) ShortHelp() []key.Binding { // key.Map interface. func (k DashboardKeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ - {k.TabRight, k.TabLeft}, // first column - {k.Help, k.Quit}, // second column + {k.TabberKeys.TabLeft, k.TabberKeys.TabRight}, // first column + {k.Help, k.Quit}, // second column } } var DefaultDashboardKeys = DashboardKeyMap{ - TabRight: key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "change tabs to the right"), - ), - TabLeft: key.NewBinding( - key.WithKeys("shift+tab"), - key.WithHelp("shift+tab", "change tabs to the left"), - ), Help: key.NewBinding( key.WithKeys("?"), key.WithHelp("?", "toggle help"), @@ -50,6 +41,7 @@ var DefaultDashboardKeys = DashboardKeyMap{ key.WithKeys("q", "esc", "ctrl+c"), key.WithHelp("q, esc, ctrl+c", "quit"), ), + TabberKeys: DefaultTabberKeys, } type Namer interface { @@ -60,16 +52,21 @@ type Namer interface { // for viewing Kubernetes information based // on a declarative dashboard description type Dashboard struct { - Panels []tea.Model - state int + tabber *Tabber width int help help.Model keys DashboardKeyMap } func NewDashboard(keys DashboardKeyMap, panels ...tea.Model) *Dashboard { + tabs := []Tab{} + for _, panel := range panels { + if namer, ok := panel.(Namer); ok { + tabs = append(tabs, Tab{Name: namer.Name(), Model: panel}) + } + } return &Dashboard{ - Panels: panels, + tabber: NewTabber(DefaultTabberKeys, tabs...), help: help.New(), keys: keys, } @@ -90,16 +87,6 @@ func (d *Dashboard) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch { case key.Matches(msg, d.keys.Quit): return d, tea.Quit - case key.Matches(msg, d.keys.TabRight): - d.state++ - if d.state > len(d.Panels)-1 { - d.state = 0 - } - case key.Matches(msg, d.keys.TabLeft): - d.state-- - if d.state < 0 { - d.state = len(d.Panels) - 1 - } case key.Matches(msg, d.keys.Help): d.help.ShowAll = !d.help.ShowAll } @@ -107,31 +94,11 @@ func (d *Dashboard) Update(msg tea.Msg) (tea.Model, tea.Cmd) { d.width = msg.Width } - d.Panels[d.state], cmd = d.Panels[d.state].Update(msg) + d.tabber, cmd = d.tabber.Update(msg) return d, tea.Batch(d.tick(), cmd) } func (d *Dashboard) View() string { - tabs := []string{} - for i, panel := range d.Panels { - if namer, ok := panel.(Namer); ok { - if i == d.state { - tabs = append(tabs, styles.SelectedTabStyle().Render(namer.Name())) - continue - } - - tabs = append(tabs, styles.TabStyle().Render(namer.Name())) - } - } - // TODO: This is not scrollable, so once there are more tabs than there is - // terminal width it goes off screen. Might need to create a new model specifically. - // for the tabs that enables some sort of scrolling/pagination. - tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, tabs...) - // gap is a repeating of the spaces so that the bottom border continues the entire width - // of the terminal. This allows it to look like a proper set of tabs - gap := styles.TabGap().Render(strings.Repeat(" ", max(0, d.width-lipgloss.Width(tabBlock)-2))) - tabsWithBorder := lipgloss.JoinHorizontal(lipgloss.Bottom, tabBlock, gap) - content := styles.ContentStyle().Render(d.Panels[d.state].View()) div := styles.TabGap().Render(strings.Repeat(" ", max(0, d.width-2))) - return lipgloss.JoinVertical(0, tabsWithBorder, content, div, d.help.View(d.keys)) + return lipgloss.JoinVertical(0, d.tabber.View(), div, d.help.View(d.keys)) } diff --git a/pkg/charm/models/tabber.go b/pkg/charm/models/tabber.go new file mode 100644 index 0000000..9a3a292 --- /dev/null +++ b/pkg/charm/models/tabber.go @@ -0,0 +1,147 @@ +package models + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/everettraven/buoy/pkg/charm/styles" +) + +type Tab struct { + Name string + Model tea.Model +} + +type Tabber struct { + tabs []Tab + selected int + keyMap TabberKeyMap + width int +} + +func NewTabber(keyMap TabberKeyMap, tabs ...Tab) *Tabber { + return &Tabber{ + tabs: tabs, + keyMap: keyMap, + } +} + +func (t *Tabber) Init() tea.Cmd { + return nil +} + +func (t *Tabber) Update(msg tea.Msg) (*Tabber, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, t.keyMap.TabRight): + t.selected++ + if t.selected > len(t.tabs)-1 { + t.selected = 0 + } + case key.Matches(msg, t.keyMap.TabLeft): + t.selected-- + if t.selected < 0 { + t.selected = len(t.tabs) - 1 + } + } + case tea.WindowSizeMsg: + t.width = msg.Width + } + + t.tabs[t.selected].Model, cmd = t.tabs[t.selected].Model.Update(msg) + return t, cmd +} + +func (t *Tabber) View() string { + tabRightArrow := styles.TabGap().Render(" ▶ ") + tabLeftArrow := styles.TabGap().Render(" ◀ ") + + pager := &pager{ + tabRightArrow: tabRightArrow, + tabLeftArrow: tabLeftArrow, + pages: []page{}, + } + pager.setPages(t.tabs, t.selected, t.width) + + tabBlock := pager.renderForSelectedTab(t.selected) + // gap is a repeating of the spaces so that the bottom border continues the entire width + // of the terminal. This allows it to look like a proper set of tabs + gap := styles.TabGap().Render(strings.Repeat(" ", max(0, t.width-lipgloss.Width(tabBlock)-2))) + tabsWithBorder := lipgloss.JoinHorizontal(lipgloss.Bottom, tabBlock, gap) + content := styles.ContentStyle().Render(t.tabs[t.selected].Model.View()) + return lipgloss.JoinVertical(0, tabsWithBorder, content) +} + +type TabberKeyMap struct { + TabRight key.Binding + TabLeft key.Binding +} + +var DefaultTabberKeys = TabberKeyMap{ + TabRight: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "change tabs to the right"), + ), + TabLeft: key.NewBinding( + key.WithKeys("shift+tab"), + key.WithHelp("shift+tab", "change tabs to the left"), + ), +} + +type page struct { + tabs []string + start int + end int +} + +type pager struct { + pages []page + tabRightArrow string + tabLeftArrow string +} + +func (p *pager) renderForSelectedTab(selected int) string { + tabPage := page{} + for _, page := range p.pages { + if page.start <= selected && page.end >= selected { + tabPage = page + } + } + + tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, tabPage.tabs...) + if len(p.pages) > 1 { + tabBlock = lipgloss.JoinHorizontal(lipgloss.Bottom, p.tabLeftArrow, tabBlock, p.tabRightArrow) + } + + return tabBlock +} + +func (p *pager) setPages(tabs []Tab, selected int, width int) { + tabPages := []page{} + tempTab := "" + tempPage := page{start: 0, tabs: []string{}} + for i, tab := range tabs { + renderedTab := styles.TabStyle().Render(tab.Name) + if i == selected { + renderedTab = styles.SelectedTabStyle().Render(tab.Name) + } + tempTab = lipgloss.JoinHorizontal(lipgloss.Top, tempTab, renderedTab) + if lipgloss.Width(lipgloss.JoinHorizontal(lipgloss.Top, p.tabLeftArrow, tempTab, p.tabRightArrow)) > width-2 { + tempPage.end = i + tabPages = append(tabPages, tempPage) + tempPage = page{start: i, tabs: []string{}} + tempTab = "" + } + + tempPage.tabs = append(tempPage.tabs, renderedTab) + } + if tempTab != "" { + tempPage.end = len(tabs) - 1 + tabPages = append(tabPages, tempPage) + } + p.pages = tabPages +}