From f9795dba060c16dce7d3fee562ae690b5aef670c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20S=C3=B6ren=20Pollak?= Date: Thu, 31 Oct 2024 14:57:22 +0100 Subject: [PATCH] Added functionality to implement help.KeyMap interface (#185) * Added functionality to implement help.KeyMap interface * Formatting * Made additionalHelp... private. Integrated implementation with 'With..' * Added short comments and fixed logic error in Help string * Fixing linter issues * Fixing linter issues * Fix linter errors * Fixing linter error --- table/keys.go | 49 ++++++++++++++++++++++++- table/keys_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++ table/model.go | 18 ++++++++-- table/options.go | 21 +++++++++-- 4 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 table/keys_test.go diff --git a/table/keys.go b/table/keys.go index bc7d119..fe2eccc 100644 --- a/table/keys.go +++ b/table/keys.go @@ -30,44 +30,91 @@ type KeyMap struct { ScrollLeft key.Binding } -// DefaultKeyMap returns a set of sensible defaults for controlling a focused table. +// DefaultKeyMap returns a set of sensible defaults for controlling a focused table with help text. func DefaultKeyMap() KeyMap { return KeyMap{ RowDown: key.NewBinding( key.WithKeys("down", "j"), + key.WithHelp("↓/j", "move down"), ), RowUp: key.NewBinding( key.WithKeys("up", "k"), + key.WithHelp("↑/k", "move up"), ), RowSelectToggle: key.NewBinding( key.WithKeys(" ", "enter"), + key.WithHelp("/enter", "select row"), ), PageDown: key.NewBinding( key.WithKeys("right", "l", "pgdown"), + key.WithHelp("→/h/page down", "next page"), ), PageUp: key.NewBinding( key.WithKeys("left", "h", "pgup"), + key.WithHelp("←/h/page up", "previous page"), ), PageFirst: key.NewBinding( key.WithKeys("home", "g"), + key.WithHelp("home/g", "first page"), ), PageLast: key.NewBinding( key.WithKeys("end", "G"), + key.WithHelp("end/G", "last page"), ), Filter: key.NewBinding( key.WithKeys("/"), + key.WithHelp("/", "filter"), ), FilterBlur: key.NewBinding( key.WithKeys("enter", "esc"), + key.WithHelp("enter/esc", "unfocus"), ), FilterClear: key.NewBinding( key.WithKeys("esc"), + key.WithHelp("esc", "clear filter"), ), ScrollRight: key.NewBinding( key.WithKeys("shift+right"), + key.WithHelp("shift+→", "scroll right"), ), ScrollLeft: key.NewBinding( key.WithKeys("shift+left"), + key.WithHelp("shift+←", "scroll left"), ), } } + +// FullHelp returns a multi row view of all the helpkeys that are defined. Needed to fullfil the 'help.Model' interface. +// Also appends all user defined extra keys to the help. +func (m Model) FullHelp() [][]key.Binding { + keyBinds := [][]key.Binding{ + {m.keyMap.RowDown, m.keyMap.RowUp, m.keyMap.RowSelectToggle}, + {m.keyMap.PageDown, m.keyMap.PageUp, m.keyMap.PageFirst, m.keyMap.PageLast}, + {m.keyMap.Filter, m.keyMap.FilterBlur, m.keyMap.FilterClear, m.keyMap.ScrollRight, m.keyMap.ScrollLeft}, + } + if m.additionalFullHelpKeys != nil { + keyBinds = append(keyBinds, m.additionalFullHelpKeys()) + } + + return keyBinds +} + +// ShortHelp just returns a single row of help views. Needed to fullfil the 'help.Model' interface. +// Also appends all user defined extra keys to the help. +func (m Model) ShortHelp() []key.Binding { + keyBinds := []key.Binding{ + m.keyMap.RowDown, + m.keyMap.RowUp, + m.keyMap.RowSelectToggle, + m.keyMap.PageDown, + m.keyMap.PageUp, + m.keyMap.Filter, + m.keyMap.FilterBlur, + m.keyMap.FilterClear, + } + if m.additionalShortHelpKeys != nil { + keyBinds = append(keyBinds, m.additionalShortHelpKeys()...) + } + + return keyBinds +} diff --git a/table/keys_test.go b/table/keys_test.go new file mode 100644 index 0000000..8831562 --- /dev/null +++ b/table/keys_test.go @@ -0,0 +1,89 @@ +package table + +import ( + "testing" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/stretchr/testify/assert" +) + +func TestKeyMapShortHelp(t *testing.T) { + columns := []Column{NewColumn("c1", "Column1", 10)} + model := New(columns) + km := DefaultKeyMap() + model.WithKeyMap(km) + assert.Nil(t, model.additionalShortHelpKeys) + assert.Equal(t, model.ShortHelp(), []key.Binding{ + model.keyMap.RowDown, + model.keyMap.RowUp, + model.keyMap.RowSelectToggle, + model.keyMap.PageDown, + model.keyMap.PageUp, + model.keyMap.Filter, + model.keyMap.FilterBlur, + model.keyMap.FilterClear}, + ) + // Testing if the 'adding of keys' works too. + keys := []key.Binding{key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))} + model = model.WithAdditionalShortHelpKeys(keys) + assert.NotNil(t, model.additionalShortHelpKeys) + assert.Equal(t, model.ShortHelp(), []key.Binding{ + model.keyMap.RowDown, + model.keyMap.RowUp, + model.keyMap.RowSelectToggle, + model.keyMap.PageDown, + model.keyMap.PageUp, + model.keyMap.Filter, + model.keyMap.FilterBlur, + model.keyMap.FilterClear, + key.NewBinding( + key.WithKeys("t"), + key.WithHelp("t", + "Testing additional keybinds"), + ), + }) +} + +func TestKeyMapFullHelp(t *testing.T) { + columns := []Column{NewColumn("c1", "Column1", 10)} + model := New(columns) + km := DefaultKeyMap() + model.WithKeyMap(km) + assert.Nil(t, model.additionalFullHelpKeys) + assert.Equal(t, + model.FullHelp(), + [][]key.Binding{ + {model.keyMap.RowDown, model.keyMap.RowUp, model.keyMap.RowSelectToggle}, + {model.keyMap.PageDown, model.keyMap.PageUp, model.keyMap.PageFirst, model.keyMap.PageLast}, + { + model.keyMap.Filter, + model.keyMap.FilterBlur, + model.keyMap.FilterClear, + model.keyMap.ScrollRight, + model.keyMap.ScrollLeft, + }, + }, + ) + // Testing if the 'adding of keys' works too. + keys := []key.Binding{key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))} + model = model.WithAdditionalFullHelpKeys(keys) + assert.NotNil(t, model.additionalFullHelpKeys) + assert.Equal(t, + model.FullHelp(), + [][]key.Binding{ + {model.keyMap.RowDown, model.keyMap.RowUp, model.keyMap.RowSelectToggle}, + {model.keyMap.PageDown, model.keyMap.PageUp, model.keyMap.PageFirst, model.keyMap.PageLast}, + {model.keyMap.Filter, model.keyMap.FilterBlur, + model.keyMap.FilterClear, + model.keyMap.ScrollRight, + model.keyMap.ScrollLeft}, + {key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))}}, + ) +} + +// Testing if Model actually implements the 'help.KeyMap' interface. +func TestKeyMapInterface(t *testing.T) { + model := New(nil) + assert.Implements(t, (*help.KeyMap)(nil), model) +} diff --git a/table/model.go b/table/model.go index d5e8937..d39b660 100644 --- a/table/model.go +++ b/table/model.go @@ -1,6 +1,7 @@ package table import ( + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -28,8 +29,21 @@ type Model struct { missingDataIndicator interface{} // Interaction - focused bool - keyMap KeyMap + focused bool + keyMap KeyMap + + // Taken from: 'Bubbles/List' + // Additional key mappings for the short and full help views. This allows + // you to add additional key mappings to the help menu without + // re-implementing the help component. Of course, you can also disable the + // list's help component and implement a new one if you need more + // flexibility. + // You have to supply a keybinding like this: + // key.NewBinding( key.WithKeys("shift+left"), key.WithHelp("shift+←", "scroll left")) + // It needs both 'WithKeys' and 'WithHelp' + additionalShortHelpKeys func() []key.Binding + additionalFullHelpKeys func() []key.Binding + selectableRows bool rowCursorIndex int diff --git a/table/options.go b/table/options.go index 9db863c..9511954 100644 --- a/table/options.go +++ b/table/options.go @@ -1,6 +1,7 @@ package table import ( + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/lipgloss" ) @@ -302,7 +303,6 @@ func (m Model) WithCurrentPage(currentPage int) Model { if m.pageSize == 0 || currentPage == m.CurrentPage() { return m } - if currentPage < 1 { currentPage = 1 } else { @@ -312,7 +312,6 @@ func (m Model) WithCurrentPage(currentPage int) Model { currentPage = maxPages } } - m.currentPage = currentPage - 1 m.rowCursorIndex = m.currentPage * m.pageSize @@ -465,3 +464,21 @@ func (m Model) WithMultiline(multiline bool) Model { return m } + +// WithAdditionalShortHelpKeys enables you to add more keybindings to the 'short help' view. +func (m Model) WithAdditionalShortHelpKeys(keys []key.Binding) Model { + m.additionalShortHelpKeys = func() []key.Binding { + return keys + } + + return m +} + +// WithAdditionalFullHelpKeys enables you to add more keybindings to the 'full help' view. +func (m Model) WithAdditionalFullHelpKeys(keys []key.Binding) Model { + m.additionalFullHelpKeys = func() []key.Binding { + return keys + } + + return m +}