Skip to content

Commit

Permalink
(bugfix): make tabs paginated
Browse files Browse the repository at this point in the history
Signed-off-by: Bryce Palmer <everettraven@gmail.com>
  • Loading branch information
everettraven committed Nov 13, 2023
1 parent 752d856 commit c0ca599
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 50 deletions.
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
65 changes: 16 additions & 49 deletions pkg/charm/models/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"),
Expand All @@ -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 {
Expand All @@ -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,
}
Expand All @@ -90,48 +87,18 @@ 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
}
case tea.WindowSizeMsg:
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))
}
147 changes: 147 additions & 0 deletions pkg/charm/models/tabber.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit c0ca599

Please sign in to comment.