diff --git a/README.md b/README.md
index 2c60912..396c822 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/client_test.go b/client/client_test.go
index 7f9174c..1958991 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)
+ require.Regexp(t, regexp.MustCompile(`^ls ~/\n`), out)
+}
+
func testTui_errors(t *testing.T) {
// Setup
defer testutils.BackupAndRestore(t)()
diff --git a/client/cmd/configKeyBindings.go b/client/cmd/configKeyBindings.go
new file mode 100644
index 0000000..dec7d4e
--- /dev/null
+++ b/client/cmd/configKeyBindings.go
@@ -0,0 +1,90 @@
+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:]
+ 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))
+ },
+}
+
+func init() {
+ configGetCmd.AddCommand(getKeyBindingsCmd)
+ configSetCmd.AddCommand(setKeyBindingsCmd)
+}
diff --git a/client/hctx/hctx.go b/client/hctx/hctx.go
index adcca78..1e9ab7a 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/testdata/TestTui-KeyBindings-Configured b/client/testdata/TestTui-KeyBindings-Configured
new file mode 100644
index 0000000..922ed1a
--- /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 0000000..ecd229a
--- /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 0000000..e2433b7
--- /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/tui/keybindings/keybindings.go b/client/tui/keybindings/keybindings.go
new file mode 100644
index 0000000..f16b00d
--- /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 72309b1..e210dda 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