Repository: Evertras/bubble-table
Pull request #185 (2024-10-09 13:24:55)

Added functionality to implement help.KeyMap interface
So as discussed in #184 this is a first attempt at including this. 

I didn´t really know where to put the tests so I made a new file, hope that structure works for you.

The naming, as well as testing / layout etc. is all changeable, just wanted to provide a first look at how this interface could be provided.

Let me know what you think. 

MaximilianSoerenPollak: I have run

- [x] make fmt 
- [x] make lint (it errors on things outside our package??)
- [x] make test `ok  	github.com/evertras/bubble-table/table	1.110s	coverage: 98.3% of statements`

Let me know if there is anything I have missed to run. 

Also let me know any feedback regarding the implementation. 
MaximilianSoerenPollak: I adapted the PR to the comments made. Let me know if anything else needs changing :)
Evertras: Thanks for the updates, just need to fix the page text a bit and then I think we're good to merge!
MaximilianSoerenPollak: All help text should be consitent now, I also fixed any Linting issues I found. Let me know if anything else needs fixing :) 
Evertras: Looks like a few more linting errors snuck in.
MaximilianSoerenPollak: Ok just fixed the latest ones. Hope that should be all :)
Evertras: The linter is still angry about the extra newlines. :(
MaximilianSoerenPollak: I tried again :D. 

I'm unsure what to do, Gitlint locally just checks everything in my local drive not just the directory. I don't know why...
Evertras: Are you running `make lint`?

https://github.com/Evertras/bubble-table/actions/runs/11559775491/job/32221732059?pr=185 still has the newline issues.
MaximilianSoerenPollak: Yep usiing run/lint sadly still same, see my output underneath. 
![image](https://github.com/user-attachments/assets/01734826-0bb4-4ea6-9877-8dd37e626ec0)

Others have said it might be cause I don´t have the same Golang version locally as is used in the linter. But not sure. 
It also does not show the errors the CI/CD makes, which makes it even more confusing Q_Q. 
I now have I think catched all remaining errors (4). 


MaximilianSoerenPollak: Is it unhappy with the newline in the comment or the one before the return?

but why then does it complain in other lines that it needs an empty line before a return?

![image](https://github.com/user-attachments/assets/04488815-3e53-4f03-90ee-bc2fb52d7c16)

Evertras: ```go
// Good
if something {
  return true
}

// Bad
if something {
  fmt.Println("Thing")
  return
}
```

Basically, a statement before it versus a `{`
MaximilianSoerenPollak: > ```go
> // Good
> if something {
>   return true
> }
> 
> // Bad
> if something {
>   fmt.Println("Thing")
>   return
> }
> ```
> 
> Basically, a statement before it versus a `{`

OKay that makes it much more clear. Will correct it and push again :)
Evertras: Thank you for the patience and fixes!

Patch:
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("<space>/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/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
+}


Test patch:
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)
+}