Add support for custom key bindings for #190 (#209)

* Add support for custom key bindings for #190

* Add tests for configuring custom key bindings

* Simplify key bindings test

* Add docs on custom key bindings + error message for unhandled actions
This commit is contained in:
David Dworken 2024-04-28 13:50:09 -07:00 committed by GitHub
parent d6a60214a2
commit c933cbc792
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 553 additions and 131 deletions

View File

@ -121,6 +121,13 @@ You can customize hishtory's color scheme for the TUI. Run `hishtory config-set
</blockquote></details>
<details>
<summary>Custom Key Bindings</summary><blockquote>
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.
</blockquote></details>
<details>
<summary>Disabling Control+R integration</summary><blockquote>
@ -195,7 +202,6 @@ Note that this uses [HTTP Basic Auth](https://en.wikipedia.org/wiki/Basic_access
</blockquote></details>
<details>
<summary>Customizing the install folder</summary><blockquote>

View File

@ -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)()

View File

@ -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)
}

View File

@ -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"}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 "),
),
}

View File

@ -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