From 3dde78c3f6415962566d418a0e3eb78635b992e4 Mon Sep 17 00:00:00 2001 From: David Dworken Date: Sat, 27 Apr 2024 19:27:32 -0700 Subject: [PATCH 1/4] Add support for custom key bindings for #190 --- client/cmd/configKeyBindings.go | 88 +++++++ client/hctx/hctx.go | 4 + client/tui/keybindings/keybindings.go | 326 ++++++++++++++++++++++++++ client/tui/tui.go | 150 ++---------- 4 files changed, 438 insertions(+), 130 deletions(-) create mode 100644 client/cmd/configKeyBindings.go create mode 100644 client/tui/keybindings/keybindings.go diff --git a/client/cmd/configKeyBindings.go b/client/cmd/configKeyBindings.go new file mode 100644 index 00000000..37eb01a8 --- /dev/null +++ b/client/cmd/configKeyBindings.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var getKeyBindingsCmd = &cobra.Command{ + Use: "key-bindings", + Short: "Get the currently configured key bindings for the TUI", + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + fmt.Println("up: \t\t\t" + strings.Join(config.KeyBindings.Up, " ")) + fmt.Println("down: \t\t\t" + strings.Join(config.KeyBindings.Down, " ")) + fmt.Println("page-up: \t\t" + strings.Join(config.KeyBindings.PageUp, " ")) + fmt.Println("page-down: \t\t" + strings.Join(config.KeyBindings.PageDown, " ")) + fmt.Println("select-entry: \t\t" + strings.Join(config.KeyBindings.SelectEntry, " ")) + fmt.Println("select-entry-and-cd: \t" + strings.Join(config.KeyBindings.SelectEntryAndChangeDir, " ")) + fmt.Println("left: \t\t\t" + strings.Join(config.KeyBindings.Left, " ")) + fmt.Println("right: \t\t\t" + strings.Join(config.KeyBindings.Right, " ")) + fmt.Println("table-left: \t\t" + strings.Join(config.KeyBindings.TableLeft, " ")) + fmt.Println("table-right: \t\t" + strings.Join(config.KeyBindings.TableRight, " ")) + fmt.Println("delete-entry: \t\t" + strings.Join(config.KeyBindings.DeleteEntry, " ")) + fmt.Println("help: \t\t\t" + strings.Join(config.KeyBindings.Help, " ")) + fmt.Println("quit: \t\t\t" + strings.Join(config.KeyBindings.Quit, " ")) + fmt.Println("jump-start-of-input: \t" + strings.Join(config.KeyBindings.JumpStartOfInput, " ")) + fmt.Println("jump-end-of-input: \t" + strings.Join(config.KeyBindings.JumpEndOfInput, " ")) + fmt.Println("word-left: \t\t" + strings.Join(config.KeyBindings.WordLeft, " ")) + fmt.Println("word-right: \t\t" + strings.Join(config.KeyBindings.WordRight, " ")) + }, +} + +var setKeyBindingsCmd = &cobra.Command{ + Use: "key-bindings", + Short: "Set custom key bindings for the TUI", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + switch args[0] { + case "up": + config.KeyBindings.Up = args[1:] + case "down": + config.KeyBindings.Down = args[1:] + case "page-up": + config.KeyBindings.PageUp = args[1:] + case "page-down": + config.KeyBindings.PageDown = args[1:] + case "select-entry": + config.KeyBindings.SelectEntry = args[1:] + case "select-entry-and-cd": + config.KeyBindings.SelectEntryAndChangeDir = args[1:] + case "left": + config.KeyBindings.Left = args[1:] + case "right": + config.KeyBindings.Right = args[1:] + case "table-left": + config.KeyBindings.TableLeft = args[1:] + case "table-right": + config.KeyBindings.TableRight = args[1:] + case "delete-entry": + config.KeyBindings.DeleteEntry = args[1:] + case "help": + config.KeyBindings.Help = args[1:] + case "quit": + config.KeyBindings.Quit = args[1:] + case "jump-start-of-input": + config.KeyBindings.JumpStartOfInput = args[1:] + case "jump-end-of-input": + config.KeyBindings.JumpEndOfInput = args[1:] + case "word-left": + config.KeyBindings.WordLeft = args[1:] + case "word-right": + config.KeyBindings.WordRight = args[1:] + } + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + +func init() { + configGetCmd.AddCommand(getKeyBindingsCmd) + configSetCmd.AddCommand(setKeyBindingsCmd) +} diff --git a/client/hctx/hctx.go b/client/hctx/hctx.go index adcca78d..1e9ab7af 100644 --- a/client/hctx/hctx.go +++ b/client/hctx/hctx.go @@ -11,6 +11,7 @@ import ( "time" "github.com/ddworken/hishtory/client/data" + "github.com/ddworken/hishtory/client/tui/keybindings" "github.com/ddworken/hishtory/shared" "github.com/google/uuid" "github.com/sirupsen/logrus" @@ -208,6 +209,8 @@ type ClientConfig struct { DefaultFilter string `json:"default_filter"` // The endpoint to use for AI suggestions AiCompletionEndpoint string `json:"ai_completion_endpoint"` + // Custom key bindings for the TUI + KeyBindings keybindings.SerializableKeyMap `json:"key_bindings"` } type ColorScheme struct { @@ -260,6 +263,7 @@ func GetConfig() (ClientConfig, error) { if err != nil { return ClientConfig{}, fmt.Errorf("failed to parse config file: %w", err) } + config.KeyBindings = config.KeyBindings.WithDefaults() if config.DisplayedColumns == nil || len(config.DisplayedColumns) == 0 { config.DisplayedColumns = []string{"Hostname", "CWD", "Timestamp", "Runtime", "Exit Code", "Command"} } diff --git a/client/tui/keybindings/keybindings.go b/client/tui/keybindings/keybindings.go new file mode 100644 index 00000000..f16b00d2 --- /dev/null +++ b/client/tui/keybindings/keybindings.go @@ -0,0 +1,326 @@ +package keybindings + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" +) + +type SerializableKeyMap struct { + Up []string + Down []string + PageUp []string + PageDown []string + SelectEntry []string + SelectEntryAndChangeDir []string + Left []string + Right []string + TableLeft []string + TableRight []string + DeleteEntry []string + Help []string + Quit []string + JumpStartOfInput []string + JumpEndOfInput []string + WordLeft []string + WordRight []string +} + +func prettifyKeyBinding(kb string) string { + if kb == "up" { + return "↑ " + } + if kb == "down" { + return "↓ " + } + if kb == "left" { + return "←" + } + if kb == "right" { + return "→" + } + subs := [][]string{ + {"+left", "+← "}, + {"+right", "+→ "}, + {"+down", "+↓ "}, + {"+up", "+↑ "}, + {"pgdown", "pgdn"}, + } + for _, sub := range subs { + kb = strings.ReplaceAll(kb, sub[0], sub[1]) + } + return kb +} + +func (s SerializableKeyMap) ToKeyMap() KeyMap { + if len(s.Up) == 0 { + panic(fmt.Sprintf("%#v", s)) + } + return KeyMap{ + Up: key.NewBinding( + key.WithKeys(s.Up...), + key.WithHelp(prettifyKeyBinding(s.Up[0]), "scroll up "), + ), + Down: key.NewBinding( + key.WithKeys(s.Down...), + key.WithHelp(prettifyKeyBinding(s.Down[0]), "scroll down "), + ), + PageUp: key.NewBinding( + key.WithKeys(s.PageUp...), + key.WithHelp(prettifyKeyBinding(s.PageUp[0]), "page up "), + ), + PageDown: key.NewBinding( + key.WithKeys(s.PageDown...), + key.WithHelp(prettifyKeyBinding(s.PageDown[0]), "page down "), + ), + SelectEntry: key.NewBinding( + key.WithKeys(s.SelectEntry...), + key.WithHelp(prettifyKeyBinding(s.SelectEntry[0]), "select an entry "), + ), + SelectEntryAndChangeDir: key.NewBinding( + key.WithKeys(s.SelectEntryAndChangeDir...), + key.WithHelp(prettifyKeyBinding(s.SelectEntryAndChangeDir[0]), "select an entry and cd into that directory"), + ), + Left: key.NewBinding( + key.WithKeys(s.Left...), + key.WithHelp(prettifyKeyBinding(s.Left[0]), "move left "), + ), + Right: key.NewBinding( + key.WithKeys(s.Right...), + key.WithHelp(prettifyKeyBinding(s.Right[0]), "move right "), + ), + TableLeft: key.NewBinding( + key.WithKeys(s.TableLeft...), + key.WithHelp(prettifyKeyBinding(s.TableLeft[0]), "scroll the table left "), + ), + TableRight: key.NewBinding( + key.WithKeys(s.TableRight...), + key.WithHelp(prettifyKeyBinding(s.TableRight[0]), "scroll the table right "), + ), + DeleteEntry: key.NewBinding( + key.WithKeys(s.DeleteEntry...), + key.WithHelp(prettifyKeyBinding(s.DeleteEntry[0]), "delete the highlighted entry "), + ), + Help: key.NewBinding( + key.WithKeys(s.Help...), + key.WithHelp(prettifyKeyBinding(s.Help[0]), "help "), + ), + Quit: key.NewBinding( + key.WithKeys(s.Quit...), + key.WithHelp(prettifyKeyBinding(s.Quit[0]), "exit hiSHtory "), + ), + JumpStartOfInput: key.NewBinding( + key.WithKeys(s.JumpStartOfInput...), + key.WithHelp(prettifyKeyBinding(s.JumpStartOfInput[0]), "jump to the start of the input "), + ), + JumpEndOfInput: key.NewBinding( + key.WithKeys(s.JumpEndOfInput...), + key.WithHelp(prettifyKeyBinding(s.JumpEndOfInput[0]), "jump to the end of the input "), + ), + WordLeft: key.NewBinding( + key.WithKeys(s.WordLeft...), + key.WithHelp(prettifyKeyBinding(s.WordLeft[0]), "jump left one word "), + ), + WordRight: key.NewBinding( + key.WithKeys(s.WordRight...), + key.WithHelp(prettifyKeyBinding(s.WordRight[0]), "jump right one word "), + ), + } +} + +func (s SerializableKeyMap) WithDefaults() SerializableKeyMap { + if len(s.Up) == 0 { + s.Up = DefaultKeyMap.Up.Keys() + } + if len(s.Down) == 0 { + s.Down = DefaultKeyMap.Down.Keys() + } + if len(s.PageUp) == 0 { + s.PageUp = DefaultKeyMap.PageUp.Keys() + } + if len(s.PageDown) == 0 { + s.PageDown = DefaultKeyMap.PageDown.Keys() + } + if len(s.SelectEntry) == 0 { + s.SelectEntry = DefaultKeyMap.SelectEntry.Keys() + } + if len(s.SelectEntryAndChangeDir) == 0 { + s.SelectEntryAndChangeDir = DefaultKeyMap.SelectEntryAndChangeDir.Keys() + } + if len(s.Left) == 0 { + s.Left = DefaultKeyMap.Left.Keys() + } + if len(s.Right) == 0 { + s.Right = DefaultKeyMap.Right.Keys() + } + if len(s.TableLeft) == 0 { + s.TableLeft = DefaultKeyMap.TableLeft.Keys() + } + if len(s.TableRight) == 0 { + s.TableRight = DefaultKeyMap.TableRight.Keys() + } + if len(s.DeleteEntry) == 0 { + s.DeleteEntry = DefaultKeyMap.DeleteEntry.Keys() + } + if len(s.Help) == 0 { + s.Help = DefaultKeyMap.Help.Keys() + } + if len(s.Quit) == 0 { + s.Quit = DefaultKeyMap.Quit.Keys() + } + if len(s.JumpStartOfInput) == 0 { + s.JumpStartOfInput = DefaultKeyMap.JumpStartOfInput.Keys() + } + if len(s.JumpEndOfInput) == 0 { + s.JumpEndOfInput = DefaultKeyMap.JumpEndOfInput.Keys() + } + if len(s.WordLeft) == 0 { + s.WordLeft = DefaultKeyMap.WordLeft.Keys() + } + if len(s.WordRight) == 0 { + s.WordRight = DefaultKeyMap.WordRight.Keys() + } + return s +} + +type KeyMap struct { + Up key.Binding + Down key.Binding + PageUp key.Binding + PageDown key.Binding + SelectEntry key.Binding + SelectEntryAndChangeDir key.Binding + Left key.Binding + Right key.Binding + TableLeft key.Binding + TableRight key.Binding + DeleteEntry key.Binding + Help key.Binding + Quit key.Binding + JumpStartOfInput key.Binding + JumpEndOfInput key.Binding + WordLeft key.Binding + WordRight key.Binding +} + +func (k KeyMap) ToSerializable() SerializableKeyMap { + return SerializableKeyMap{ + Up: k.Up.Keys(), + Down: k.Down.Keys(), + PageUp: k.PageUp.Keys(), + PageDown: k.PageDown.Keys(), + SelectEntry: k.SelectEntry.Keys(), + SelectEntryAndChangeDir: k.SelectEntryAndChangeDir.Keys(), + Left: k.Left.Keys(), + Right: k.Right.Keys(), + TableLeft: k.TableLeft.Keys(), + TableRight: k.TableRight.Keys(), + DeleteEntry: k.DeleteEntry.Keys(), + Help: k.Help.Keys(), + Quit: k.Quit.Keys(), + JumpStartOfInput: k.JumpStartOfInput.Keys(), + JumpEndOfInput: k.JumpEndOfInput.Keys(), + WordLeft: k.WordLeft.Keys(), + WordRight: k.WordRight.Keys(), + } +} + +var fakeTitleKeyBinding key.Binding = key.NewBinding( + key.WithKeys(""), + key.WithHelp("hiSHtory: Search your shell history", ""), +) + +var fakeEmptyKeyBinding key.Binding = key.NewBinding( + key.WithKeys(""), + key.WithHelp("", ""), +) + +func (k KeyMap) ShortHelp() []key.Binding { + return []key.Binding{fakeTitleKeyBinding, k.Help} +} + +func (k KeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {fakeTitleKeyBinding, k.Up, k.Left, k.SelectEntry, k.SelectEntryAndChangeDir}, + {fakeEmptyKeyBinding, k.Down, k.Right, k.DeleteEntry}, + {fakeEmptyKeyBinding, k.PageUp, k.TableLeft, k.Quit}, + {fakeEmptyKeyBinding, k.PageDown, k.TableRight, k.Help}, + } +} + +type Binding struct { + Keys []string `json:"keys"` + Help key.Help `json:"help"` +} + +var DefaultKeyMap = KeyMap{ + Up: key.NewBinding( + key.WithKeys("up", "alt+OA", "ctrl+p"), + key.WithHelp("↑ ", "scroll up "), + ), + Down: key.NewBinding( + key.WithKeys("down", "alt+OB", "ctrl+n"), + key.WithHelp("↓ ", "scroll down "), + ), + PageUp: key.NewBinding( + key.WithKeys("pgup"), + key.WithHelp("pgup", "page up "), + ), + PageDown: key.NewBinding( + key.WithKeys("pgdown"), + key.WithHelp("pgdn", "page down "), + ), + SelectEntry: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "select an entry "), + ), + SelectEntryAndChangeDir: key.NewBinding( + key.WithKeys("ctrl+x"), + key.WithHelp("ctrl+x", "select an entry and cd into that directory"), + ), + Left: key.NewBinding( + key.WithKeys("left"), + key.WithHelp("← ", "move left "), + ), + Right: key.NewBinding( + key.WithKeys("right"), + key.WithHelp("→ ", "move right "), + ), + TableLeft: key.NewBinding( + key.WithKeys("shift+left"), + key.WithHelp("shift+← ", "scroll the table left "), + ), + TableRight: key.NewBinding( + key.WithKeys("shift+right"), + key.WithHelp("shift+→ ", "scroll the table right "), + ), + DeleteEntry: key.NewBinding( + key.WithKeys("ctrl+k"), + key.WithHelp("ctrl+k", "delete the highlighted entry "), + ), + Help: key.NewBinding( + key.WithKeys("ctrl+h"), + key.WithHelp("ctrl+h", "help "), + ), + Quit: key.NewBinding( + key.WithKeys("esc", "ctrl+c", "ctrl+d"), + key.WithHelp("esc", "exit hiSHtory "), + ), + JumpStartOfInput: key.NewBinding( + key.WithKeys("ctrl+a"), + key.WithHelp("ctrl+a", "jump to the start of the input "), + ), + JumpEndOfInput: key.NewBinding( + key.WithKeys("ctrl+e"), + key.WithHelp("ctrl+e", "jump to the end of the input "), + ), + WordLeft: key.NewBinding( + key.WithKeys("ctrl+left"), + key.WithHelp("ctrl+left", "jump left one word "), + ), + WordRight: key.NewBinding( + key.WithKeys("ctrl+right"), + key.WithHelp("ctrl+right", "jump right one word "), + ), +} diff --git a/client/tui/tui.go b/client/tui/tui.go index 72309b12..e210dda7 100644 --- a/client/tui/tui.go +++ b/client/tui/tui.go @@ -23,6 +23,7 @@ import ( "github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/lib" "github.com/ddworken/hishtory/client/table" + "github.com/ddworken/hishtory/client/tui/keybindings" "github.com/ddworken/hishtory/shared" "github.com/muesli/termenv" "golang.org/x/term" @@ -42,120 +43,6 @@ var LAST_DISPATCHED_QUERY_ID = 0 var LAST_DISPATCHED_QUERY_TIMESTAMP time.Time var LAST_PROCESSED_QUERY_ID = -1 -type keyMap struct { - Up key.Binding - Down key.Binding - PageUp key.Binding - PageDown key.Binding - SelectEntry key.Binding - SelectEntryAndChangeDir key.Binding - Left key.Binding - Right key.Binding - TableLeft key.Binding - TableRight key.Binding - DeleteEntry key.Binding - Help key.Binding - Quit key.Binding - JumpStartOfInput key.Binding - JumpEndOfInput key.Binding - JumpWordLeft key.Binding - JumpWordRight key.Binding -} - -var fakeTitleKeyBinding key.Binding = key.NewBinding( - key.WithKeys(""), - key.WithHelp("hiSHtory: Search your shell history", ""), -) - -var fakeEmptyKeyBinding key.Binding = key.NewBinding( - key.WithKeys(""), - key.WithHelp("", ""), -) - -func (k keyMap) ShortHelp() []key.Binding { - return []key.Binding{fakeTitleKeyBinding, k.Help} -} - -func (k keyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {fakeTitleKeyBinding, k.Up, k.Left, k.SelectEntry, k.SelectEntryAndChangeDir}, - {fakeEmptyKeyBinding, k.Down, k.Right, k.DeleteEntry}, - {fakeEmptyKeyBinding, k.PageUp, k.TableLeft, k.Quit}, - {fakeEmptyKeyBinding, k.PageDown, k.TableRight, k.Help}, - } -} - -var keys = keyMap{ - Up: key.NewBinding( - key.WithKeys("up", "alt+OA", "ctrl+p"), - key.WithHelp("↑ ", "scroll up "), - ), - Down: key.NewBinding( - key.WithKeys("down", "alt+OB", "ctrl+n"), - key.WithHelp("↓ ", "scroll down "), - ), - PageUp: key.NewBinding( - key.WithKeys("pgup"), - key.WithHelp("pgup", "page up "), - ), - PageDown: key.NewBinding( - key.WithKeys("pgdown"), - key.WithHelp("pgdn", "page down "), - ), - SelectEntry: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "select an entry "), - ), - SelectEntryAndChangeDir: key.NewBinding( - key.WithKeys("ctrl+x"), - key.WithHelp("ctrl+x", "select an entry and cd into that directory"), - ), - Left: key.NewBinding( - key.WithKeys("left"), - key.WithHelp("← ", "move left "), - ), - Right: key.NewBinding( - key.WithKeys("right"), - key.WithHelp("→ ", "move right "), - ), - TableLeft: key.NewBinding( - key.WithKeys("shift+left"), - key.WithHelp("shift+← ", "scroll the table left "), - ), - TableRight: key.NewBinding( - key.WithKeys("shift+right"), - key.WithHelp("shift+→ ", "scroll the table right "), - ), - DeleteEntry: key.NewBinding( - key.WithKeys("ctrl+k"), - key.WithHelp("ctrl+k", "delete the highlighted entry "), - ), - Help: key.NewBinding( - key.WithKeys("ctrl+h"), - key.WithHelp("ctrl+h", "help "), - ), - Quit: key.NewBinding( - key.WithKeys("esc", "ctrl+c", "ctrl+d"), - key.WithHelp("esc", "exit hiSHtory "), - ), - JumpStartOfInput: key.NewBinding( - key.WithKeys("ctrl+a"), - key.WithHelp("ctrl+a", "jump to the start of the input "), - ), - JumpEndOfInput: key.NewBinding( - key.WithKeys("ctrl+e"), - key.WithHelp("ctrl+e", "jump to the end of the input "), - ), - JumpWordLeft: key.NewBinding( - key.WithKeys("ctrl+left"), - key.WithHelp("ctrl+left", "jump left one word "), - ), - JumpWordRight: key.NewBinding( - key.WithKeys("ctrl+right"), - key.WithHelp("ctrl+right", "jump right one word "), - ), -} - type SelectStatus int64 const ( @@ -164,6 +51,8 @@ const ( SelectedWithChangeDir ) +var loadedKeyBindings keybindings.KeyMap = keybindings.DefaultKeyMap + type model struct { // context ctx context.Context @@ -330,20 +219,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { - case key.Matches(msg, keys.Quit): + case key.Matches(msg, loadedKeyBindings.Quit): m.quitting = true return m, tea.Quit - case key.Matches(msg, keys.SelectEntry): + case key.Matches(msg, loadedKeyBindings.SelectEntry): if len(m.tableEntries) != 0 && m.table != nil { m.selected = Selected } return m, tea.Quit - case key.Matches(msg, keys.SelectEntryAndChangeDir): + case key.Matches(msg, loadedKeyBindings.SelectEntryAndChangeDir): if len(m.tableEntries) != 0 && m.table != nil { m.selected = SelectedWithChangeDir } return m, tea.Quit - case key.Matches(msg, keys.DeleteEntry): + case key.Matches(msg, loadedKeyBindings.DeleteEntry): if m.table == nil { return m, nil } @@ -355,16 +244,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd := runQueryAndUpdateTable(m, true, true) preventTableOverscrolling(m) return m, cmd - case key.Matches(msg, keys.Help): + case key.Matches(msg, loadedKeyBindings.Help): m.help.ShowAll = !m.help.ShowAll return m, nil - case key.Matches(msg, keys.JumpStartOfInput): + case key.Matches(msg, loadedKeyBindings.JumpStartOfInput): m.queryInput.SetCursor(0) return m, nil - case key.Matches(msg, keys.JumpEndOfInput): + case key.Matches(msg, loadedKeyBindings.JumpEndOfInput): m.queryInput.SetCursor(len(m.queryInput.Value())) return m, nil - case key.Matches(msg, keys.JumpWordLeft): + case key.Matches(msg, loadedKeyBindings.WordLeft): wordBoundaries := calculateWordBoundaries(m.queryInput.Value()) lastBoundary := 0 for _, boundary := range wordBoundaries { @@ -375,7 +264,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { lastBoundary = boundary } return m, nil - case key.Matches(msg, keys.JumpWordRight): + case key.Matches(msg, loadedKeyBindings.WordRight): wordBoundaries := calculateWordBoundaries(m.queryInput.Value()) for _, boundary := range wordBoundaries { if boundary > m.queryInput.Position() { @@ -509,7 +398,7 @@ func (m model) View() string { if isExtraCompactHeightMode() { additionalMessagesStr = "\n" } - helpView := m.help.View(keys) + helpView := m.help.View(loadedKeyBindings) if isExtraCompactHeightMode() { helpView = "" } @@ -751,10 +640,10 @@ func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.M return table.Model{}, err } km := table.KeyMap{ - LineUp: keys.Up, - LineDown: keys.Down, - PageUp: keys.PageUp, - PageDown: keys.PageDown, + LineUp: loadedKeyBindings.Up, + LineDown: loadedKeyBindings.Down, + PageUp: loadedKeyBindings.PageUp, + PageDown: loadedKeyBindings.PageDown, GotoTop: key.NewBinding( key.WithKeys("home"), key.WithHelp("home", "go to start"), @@ -763,8 +652,8 @@ func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.M key.WithKeys("end"), key.WithHelp("end", "go to end"), ), - MoveLeft: keys.TableLeft, - MoveRight: keys.TableRight, + MoveLeft: loadedKeyBindings.TableLeft, + MoveRight: loadedKeyBindings.TableRight, } _, terminalHeight, err := getTerminalSize() if err != nil { @@ -954,6 +843,7 @@ func configureColorProfile(ctx context.Context) { } func TuiQuery(ctx context.Context, shellName, initialQuery string) error { + loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap() configureColorProfile(ctx) p := tea.NewProgram(initialModel(ctx, shellName, initialQuery), tea.WithOutput(os.Stderr)) // Async: Get the initial set of rows From 0c3d46924e4e88db2b78b3a55f5cbc2ca2593944 Mon Sep 17 00:00:00 2001 From: David Dworken Date: Sat, 27 Apr 2024 21:08:09 -0700 Subject: [PATCH 2/4] Add tests for configuring custom key bindings --- client/client_test.go | 41 +++++++++++++++++++ .../testdata/TestTui-KeyBindings-Configured | 17 ++++++++ client/testdata/TestTui-KeyBindings-Default | 17 ++++++++ client/testdata/TestTui-KeyBindings-Help | 31 ++++++++++++++ client/testdata/TestTui-KeyBindings-Selected | 2 + 5 files changed, 108 insertions(+) create mode 100644 client/testdata/TestTui-KeyBindings-Configured create mode 100644 client/testdata/TestTui-KeyBindings-Default create mode 100644 client/testdata/TestTui-KeyBindings-Help create mode 100644 client/testdata/TestTui-KeyBindings-Selected diff --git a/client/client_test.go b/client/client_test.go index 7f9174cd..98eb8f11 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -114,6 +114,7 @@ func TestParam(t *testing.T) { t.Run("testTui/delete", wrapTestForSharding(testTui_delete)) t.Run("testTui/color", wrapTestForSharding(testTui_color)) t.Run("testTui/errors", wrapTestForSharding(testTui_errors)) + t.Run("testTui/keybindings", wrapTestForSharding(testTui_keybindings)) t.Run("testTui/ai", wrapTestForSharding(testTui_ai)) t.Run("testTui/defaultFilter", wrapTestForSharding(testTui_defaultFilter)) @@ -2140,6 +2141,46 @@ func testTui_general(t *testing.T, onlineStatus OnlineStatus) { assertNoLeakedConnections(t) } +func testTui_keybindings(t *testing.T) { + // Setup + defer testutils.BackupAndRestore(t)() + tester, _, _ := setupTestTui(t, Online) + + // Check the default config + testutils.CompareGoldens(t, + tester.RunInteractiveShell(t, `hishtory config-get key-bindings`), + "TestTui-KeyBindings-Default", + ) + + // Configure some custom key bindings + tester.RunInteractiveShell(t, `hishtory config-set key-bindings down '?'`) + tester.RunInteractiveShell(t, `hishtory config-set key-bindings help ctrl+j`) + + // Check that they got configured + testutils.CompareGoldens(t, + tester.RunInteractiveShell(t, `hishtory config-get key-bindings`), + "TestTui-KeyBindings-Configured", + ) + + // Record a command and demo searching for it + tester.RunInteractiveShell(t, `echo 1`) + tester.RunInteractiveShell(t, `echo 2`) + out := captureTerminalOutput(t, tester, []string{ + "hishtory SPACE tquery ENTER", + "C-j", + }) + out = stripTuiCommandPrefix(t, out) + testutils.CompareGoldens(t, out, "TestTui-KeyBindings-Help") + + // Use the custom key binding for scrolling down + out = captureTerminalOutput(t, tester, []string{ + "hishtory SPACE tquery ENTER", + "'?' Enter", + }) + out = stripTuiCommandPrefix(t, out) + testutils.CompareGoldens(t, out, "TestTui-KeyBindings-Selected") +} + func testTui_errors(t *testing.T) { // Setup defer testutils.BackupAndRestore(t)() diff --git a/client/testdata/TestTui-KeyBindings-Configured b/client/testdata/TestTui-KeyBindings-Configured new file mode 100644 index 00000000..922ed1a5 --- /dev/null +++ b/client/testdata/TestTui-KeyBindings-Configured @@ -0,0 +1,17 @@ +up: up alt+OA ctrl+p +down: ? +page-up: pgup +page-down: pgdown +select-entry: enter +select-entry-and-cd: ctrl+x +left: left +right: right +table-left: shift+left +table-right: shift+right +delete-entry: ctrl+k +help: ctrl+j +quit: esc ctrl+c ctrl+d +jump-start-of-input: ctrl+a +jump-end-of-input: ctrl+e +word-left: ctrl+left +word-right: ctrl+right diff --git a/client/testdata/TestTui-KeyBindings-Default b/client/testdata/TestTui-KeyBindings-Default new file mode 100644 index 00000000..ecd229ac --- /dev/null +++ b/client/testdata/TestTui-KeyBindings-Default @@ -0,0 +1,17 @@ +up: up alt+OA ctrl+p +down: down alt+OB ctrl+n +page-up: pgup +page-down: pgdown +select-entry: enter +select-entry-and-cd: ctrl+x +left: left +right: right +table-left: shift+left +table-right: shift+right +delete-entry: ctrl+k +help: ctrl+h +quit: esc ctrl+c ctrl+d +jump-start-of-input: ctrl+a +jump-end-of-input: ctrl+e +word-left: ctrl+left +word-right: ctrl+right diff --git a/client/testdata/TestTui-KeyBindings-Help b/client/testdata/TestTui-KeyBindings-Help new file mode 100644 index 00000000..e2433b7a --- /dev/null +++ b/client/testdata/TestTui-KeyBindings-Help @@ -0,0 +1,31 @@ +Search Query: > ls + +┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Hostname CWD Timestamp Runtime Exit Code Command │ +│────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ localhost /tmp/ Oct 17 2022 21:43:21 PDT 3s 2 echo 'aaaaaa bbbb' │ +│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 ls ~/ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +hiSHtory: Search your shell history +↑ scroll up ? scroll down pgup page up pgdn page down +← move left → move right shift+← scroll the table left shift+→ scroll the table right +enter select an entry ctrl+k delete the highlighted entry esc exit hiSHtory ctrl+j help +ctrl+x select an entry and cd into that directory \ No newline at end of file diff --git a/client/testdata/TestTui-KeyBindings-Selected b/client/testdata/TestTui-KeyBindings-Selected new file mode 100644 index 00000000..c6bc6f69 --- /dev/null +++ b/client/testdata/TestTui-KeyBindings-Selected @@ -0,0 +1,2 @@ +ls ~/ +david@ghaction-runner-hostname hishtory % \ No newline at end of file From aeaf1ad005f80b6adbc8b0a10e2f200ea8d9fe3b Mon Sep 17 00:00:00 2001 From: David Dworken Date: Sun, 28 Apr 2024 11:27:17 -0700 Subject: [PATCH 3/4] Simplify key bindings test --- client/client_test.go | 2 +- client/testdata/TestTui-KeyBindings-Selected | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 client/testdata/TestTui-KeyBindings-Selected diff --git a/client/client_test.go b/client/client_test.go index 98eb8f11..19589919 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2178,7 +2178,7 @@ func testTui_keybindings(t *testing.T) { "'?' Enter", }) out = stripTuiCommandPrefix(t, out) - testutils.CompareGoldens(t, out, "TestTui-KeyBindings-Selected") + require.Regexp(t, regexp.MustCompile(`^ls ~/\n`), out) } func testTui_errors(t *testing.T) { diff --git a/client/testdata/TestTui-KeyBindings-Selected b/client/testdata/TestTui-KeyBindings-Selected deleted file mode 100644 index c6bc6f69..00000000 --- a/client/testdata/TestTui-KeyBindings-Selected +++ /dev/null @@ -1,2 +0,0 @@ -ls ~/ -david@ghaction-runner-hostname hishtory % \ No newline at end of file From f3d467b4a76a413fbce91f3ff430cda5f32d7be3 Mon Sep 17 00:00:00 2001 From: David Dworken Date: Sun, 28 Apr 2024 11:33:27 -0700 Subject: [PATCH 4/4] Add docs on custom key bindings + error message for unhandled actions --- README.md | 8 +++++++- client/cmd/configKeyBindings.go | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c609121..396c8226 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,13 @@ You can customize hishtory's color scheme for the TUI. Run `hishtory config-set +
+Custom Key Bindings
+ +You can customize hishtory's key bindings for the TUI. Run `hishtory config-get key-bindings` to see the current key bindings. You can then run `hishtory config-set key-bindings $action $keybinding` to configure custom key bindings. + +
+
Disabling Control+R integration
@@ -195,7 +202,6 @@ Note that this uses [HTTP Basic Auth](https://en.wikipedia.org/wiki/Basic_access
-
Customizing the install folder
diff --git a/client/cmd/configKeyBindings.go b/client/cmd/configKeyBindings.go index 37eb01a8..dec7d4e9 100644 --- a/client/cmd/configKeyBindings.go +++ b/client/cmd/configKeyBindings.go @@ -77,6 +77,8 @@ var setKeyBindingsCmd = &cobra.Command{ config.KeyBindings.WordLeft = args[1:] case "word-right": config.KeyBindings.WordRight = args[1:] + default: + lib.CheckFatalError(fmt.Errorf("unknown action %q, run `hishtory config-get keybindings` to see the list of currently configured key bindings", args[0])) } lib.CheckFatalError(hctx.SetConfig(config)) },