mirror of
https://github.com/jorgerojas26/lazysql.git
synced 2026-05-06 08:56:58 -04:00
Merge pull request #274 from fjmoralesp/main
feat: add custom keymaps config
This commit is contained in:
@@ -374,56 +374,178 @@ Note: Undefined environment variables will be replaced with an empty string.
|
||||
|
||||
## Keybindings
|
||||
|
||||
### Global
|
||||
### Custom Keybindings
|
||||
|
||||
| Key | Action |
|
||||
| --------- | ------------------------------ |
|
||||
| q | Quit |
|
||||
| CTRL + e | Open SQL editor |
|
||||
| Backspace | Return to connection selection |
|
||||
| ? | Show keybindings popup |
|
||||
You can customize keybindings by adding a `[keymap.<Group>]` section to your `config.toml` file. Each entry maps a command name to a key.
|
||||
|
||||
### Table
|
||||
```toml
|
||||
[keymap.Home]
|
||||
SwitchToEditorView = "i"
|
||||
Quit = "Esc"
|
||||
|
||||
| Key | Action |
|
||||
| -------- | ------------------------------------ |
|
||||
| c | Edit table cell |
|
||||
| d | Delete row |
|
||||
| o | Add row |
|
||||
| / | Focus the filter input or SQL editor |
|
||||
| CTRL + s | Commit changes |
|
||||
| > | Next page |
|
||||
| < | Previous page |
|
||||
| K | Sort ASC |
|
||||
| J | Sort DESC |
|
||||
| H | Focus tree panel |
|
||||
| { | Focus previous tab |
|
||||
| } | Focus next tab |
|
||||
| X | Close current tab |
|
||||
| R | Refresh the current table |
|
||||
| E | Export to CSV |
|
||||
[keymap.Tree]
|
||||
GotoTop = "t"
|
||||
Search = "Ctrl-F"
|
||||
```
|
||||
|
||||
### Tree
|
||||
For single character keys, use the character directly (e.g., `"q"`, `"G"`, `"1"`, `"/"`). For special keys, use the [tcell key name](https://github.com/gdamore/tcell/blob/v2.7.4/key.go#L83) (e.g., `"Enter"`, `"Esc"`, `"Ctrl-S"`). Only key names defined in tcell are supported.
|
||||
|
||||
| Key | Action |
|
||||
| ------ | ------------------------------ |
|
||||
| L | Focus table panel |
|
||||
| G | Focus last database tree node |
|
||||
| g | Focus first database tree node |
|
||||
| CTRL+u | Scroll 5 items up |
|
||||
| CTRL+d | Scroll 5 items down |
|
||||
Group names are case-insensitive (`Home`, `home`, and `HOME` all work).
|
||||
|
||||
### SQL Editor
|
||||
Available groups: `Home`, `Connection`, `Tree`, `TreeFilter`, `Table`, `Editor`, `Sidebar`, `QueryPreview`, `QueryHistory`, `JSONViewer`.
|
||||
|
||||
| Key | Action |
|
||||
| ------------ | --------------------------------- |
|
||||
| CTRL + R | Run the SQL statement |
|
||||
| CTRL + Space | Open external editor (Linux only) |
|
||||
### Default Keybindings
|
||||
|
||||
#### Home
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| L | MoveRight | Focus table |
|
||||
| H | MoveLeft | Focus tree |
|
||||
| Ctrl-E | SwitchToEditorView | Open SQL editor |
|
||||
| Ctrl-S | Save | Execute pending changes |
|
||||
| q | Quit | Quit |
|
||||
| Backspace | SwitchToConnectionsView | Switch to connections list |
|
||||
| ? | HelpPopup | Help |
|
||||
| Ctrl-P | SearchGlobal | Global search |
|
||||
| Ctrl-_ | ToggleQueryHistory | Toggle query history modal |
|
||||
| T | ToggleTree | Toggle file tree |
|
||||
|
||||
#### Connection
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| n | NewConnection | Create a new database connection |
|
||||
| c | Connect | Connect to database |
|
||||
| Enter | Connect | Connect to database |
|
||||
| e | EditConnection | Edit a database connection |
|
||||
| d | DeleteConnection | Delete a database connection |
|
||||
| q | Quit | Quit |
|
||||
|
||||
#### Tree
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| g | GotoTop | Go to top |
|
||||
| G | GotoBottom | Go to bottom |
|
||||
| Enter | Execute | Open |
|
||||
| j | MoveDown | Go down |
|
||||
| Down | MoveDown | Go down |
|
||||
| Ctrl-U | PagePrev | Go page up |
|
||||
| Ctrl-D | PageNext | Go page down |
|
||||
| k | MoveUp | Go up |
|
||||
| Up | MoveUp | Go up |
|
||||
| / | Search | Search |
|
||||
| n | NextFoundNode | Go to next found node |
|
||||
| N | PreviousFoundNode | Go to previous found node |
|
||||
| p | PreviousFoundNode | Go to previous found node |
|
||||
| P | NextFoundNode | Go to next found node |
|
||||
| c | TreeCollapseAll | Collapse all |
|
||||
| e | ExpandAll | Expand all |
|
||||
| R | Refresh | Refresh tree |
|
||||
|
||||
#### Tree Filter
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| Esc | UnfocusTreeFilter | Unfocus tree filter |
|
||||
| Enter | CommitTreeFilter | Commit tree filter search |
|
||||
|
||||
#### Table
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| / | Search | Search |
|
||||
| c | Edit | Change cell |
|
||||
| d | Delete | Delete row |
|
||||
| w | GotoNext | Go to next cell |
|
||||
| b | GotoPrev | Go to previous cell |
|
||||
| $ | GotoEnd | Go to last cell |
|
||||
| 0 | GotoStart | Go to first cell |
|
||||
| y | Copy | Copy cell value to clipboard |
|
||||
| o | AppendNewRow | Append new row |
|
||||
| O | DuplicateRow | Duplicate row |
|
||||
| J | SortDesc | Sort descending |
|
||||
| R | Refresh | Refresh the current table |
|
||||
| K | SortAsc | Sort ascending |
|
||||
| C | SetValue | Toggle value menu (NULL, EMPTY, DEFAULT) |
|
||||
| [ | TabPrev | Switch to previous tab |
|
||||
| ] | TabNext | Switch to next tab |
|
||||
| { | TabFirst | Switch to first tab |
|
||||
| } | TabLast | Switch to last tab |
|
||||
| X | TabClose | Close tab |
|
||||
| > | PageNext | Switch to next page |
|
||||
| < | PagePrev | Switch to previous page |
|
||||
| 1 | RecordsMenu | Switch to records menu |
|
||||
| 2 | ColumnsMenu | Switch to columns menu |
|
||||
| 3 | ConstraintsMenu | Switch to constraints menu |
|
||||
| 4 | ForeignKeysMenu | Switch to foreign keys menu |
|
||||
| 5 | IndexesMenu | Switch to indexes menu |
|
||||
| S | ToggleSidebar | Toggle sidebar |
|
||||
| s | FocusSidebar | Focus sidebar |
|
||||
| Z | ShowRowJSONViewer | Toggle JSON viewer for row |
|
||||
| z | ShowCellJSONViewer | Toggle JSON viewer for cell |
|
||||
| E | ExportCSV | Export to CSV |
|
||||
|
||||
#### Editor
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| Ctrl-R | Execute | Execute query |
|
||||
| Esc | UnfocusEditor | Unfocus editor |
|
||||
| Ctrl-Space | OpenInExternalEditor | Open in external editor |
|
||||
|
||||
Specific editor for lazysql can be set by `$SQL_EDITOR`.
|
||||
|
||||
Specific terminal for opening editor can be set by `$SQL_TERMINAL`
|
||||
|
||||
#### Sidebar
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| s | UnfocusSidebar | Focus table |
|
||||
| S | ToggleSidebar | Toggle sidebar |
|
||||
| j | MoveDown | Focus next field |
|
||||
| k | MoveUp | Focus previous field |
|
||||
| g | GotoStart | Focus first field |
|
||||
| G | GotoEnd | Focus last field |
|
||||
| c | Edit | Edit field |
|
||||
| Enter | CommitEdit | Add edit to pending changes |
|
||||
| Esc | DiscardEdit | Discard edit |
|
||||
| C | SetValue | Toggle value menu (NULL, EMPTY, DEFAULT) |
|
||||
| y | Copy | Copy value to clipboard |
|
||||
|
||||
#### Query Preview
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| Ctrl-S | Save | Execute queries |
|
||||
| q | Quit | Quit |
|
||||
| y | Copy | Copy query to clipboard |
|
||||
| d | Delete | Delete query |
|
||||
|
||||
#### Query History
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| s | Save | Save query |
|
||||
| d | Delete | Delete query |
|
||||
| q | Quit | Quit |
|
||||
| y | Copy | Copy query to clipboard |
|
||||
| / | Search | Search |
|
||||
| Ctrl-_ | ToggleQueryHistory | Toggle query history modal |
|
||||
| [ | TabPrev | Switch to previous tab |
|
||||
| ] | TabNext | Switch to next tab |
|
||||
|
||||
#### JSON Viewer
|
||||
|
||||
| Default Key | Command | Description |
|
||||
| --- | --- | --- |
|
||||
| Z | ShowRowJSONViewer | Toggle JSON viewer |
|
||||
| z | ShowCellJSONViewer | Toggle JSON viewer |
|
||||
| y | Copy | Copy value to clipboard |
|
||||
| w | ToggleJSONViewerWrap | Toggle word wrap |
|
||||
|
||||
## Example connection URLs
|
||||
|
||||
```
|
||||
@@ -448,7 +570,7 @@ odbc+postgres://user:pass@localhost:port/dbname?option1=
|
||||
- [ ] Support for NoSQL databases
|
||||
- [ ] Columns and indexes creation through TUI
|
||||
- [x] Table tree input filter
|
||||
- [ ] Custom keybindings
|
||||
- [x] Custom keybindings
|
||||
- [x] Show keybindings on a modal
|
||||
- [x] Rewrite row `create`, `update` and `delete` logic
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ type Config struct {
|
||||
ConfigFile string
|
||||
AppConfig *models.AppConfig `toml:"application"`
|
||||
Connections []models.Connection `toml:"database"`
|
||||
Keymaps models.KeymapConfig `toml:"keymap"`
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
@@ -73,6 +74,10 @@ func LoadConfig(configFile string) error {
|
||||
App.config.Connections[i].URL = parseConfigURL(&conn)
|
||||
}
|
||||
|
||||
if err := ApplyKeymapConfig(App.config.Keymaps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
||||
cmd "github.com/jorgerojas26/lazysql/commands"
|
||||
"github.com/jorgerojas26/lazysql/keymap"
|
||||
"github.com/jorgerojas26/lazysql/models"
|
||||
)
|
||||
|
||||
// local alias added for clarity purpose
|
||||
@@ -176,3 +180,71 @@ var Keymaps = KeymapSystem{
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var keyNameToCode = func() map[string]tcell.Key {
|
||||
keys := make(map[string]tcell.Key, len(tcell.KeyNames))
|
||||
for code, name := range tcell.KeyNames {
|
||||
keys[name] = code
|
||||
}
|
||||
return keys
|
||||
}()
|
||||
|
||||
func parseKeyString(s string) (Key, error) {
|
||||
runes := []rune(s)
|
||||
if len(runes) == 1 {
|
||||
return Key{Char: runes[0]}, nil
|
||||
}
|
||||
|
||||
if code, ok := keyNameToCode[s]; ok {
|
||||
return Key{Code: code}, nil
|
||||
}
|
||||
|
||||
return Key{}, fmt.Errorf("unknown key: %s", s)
|
||||
}
|
||||
|
||||
func setBindings(bindings map[string]string, group Map, groupName string) (Map, error) {
|
||||
for cmdName, keyStr := range bindings {
|
||||
key, err := parseKeyString(keyStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid key %q for command %s in group %s: %w", keyStr, cmdName, groupName, err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for index, bind := range group {
|
||||
if bind.Cmd.String() == cmdName {
|
||||
group[index].Key = key
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("command %s not found in group %s", cmdName, groupName)
|
||||
}
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func ApplyKeymapConfig(keymaps models.KeymapConfig) error {
|
||||
if len(keymaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for groupName, bindings := range keymaps {
|
||||
groupKey := strings.ToLower(groupName)
|
||||
group, ok := Keymaps.Groups[groupKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown keymap group: %s", groupName)
|
||||
}
|
||||
|
||||
updated, err := setBindings(bindings, group, groupName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Keymaps.Groups[groupKey] = updated
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
||||
cmd "github.com/jorgerojas26/lazysql/commands"
|
||||
"github.com/jorgerojas26/lazysql/models"
|
||||
)
|
||||
|
||||
func TestParseKeyString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want Key
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single lowercase char",
|
||||
input: "q",
|
||||
want: Key{Char: 'q'},
|
||||
},
|
||||
{
|
||||
name: "single uppercase char",
|
||||
input: "G",
|
||||
want: Key{Char: 'G'},
|
||||
},
|
||||
{
|
||||
name: "single digit",
|
||||
input: "1",
|
||||
want: Key{Char: '1'},
|
||||
},
|
||||
{
|
||||
name: "special char slash",
|
||||
input: "/",
|
||||
want: Key{Char: '/'},
|
||||
},
|
||||
{
|
||||
name: "special key Ctrl+S",
|
||||
input: "Ctrl-S",
|
||||
want: Key{Code: tcell.KeyCtrlS},
|
||||
},
|
||||
{
|
||||
name: "special key Enter",
|
||||
input: "Enter",
|
||||
want: Key{Code: tcell.KeyEnter},
|
||||
},
|
||||
{
|
||||
name: "special key Esc",
|
||||
input: "Esc",
|
||||
want: Key{Code: tcell.KeyEscape},
|
||||
},
|
||||
{
|
||||
name: "unknown key returns error",
|
||||
input: "NonExistentKey",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseKeyString(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("parseKeyString(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||
}
|
||||
if !tt.wantErr && got != tt.want {
|
||||
t.Errorf("parseKeyString(%q) = %+v, want %+v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetBindings(t *testing.T) {
|
||||
t.Run("rebinds existing command", func(t *testing.T) {
|
||||
group := Map{
|
||||
{Key: Key{Char: 'q'}, Cmd: cmd.Quit},
|
||||
{Key: Key{Char: 'j'}, Cmd: cmd.MoveDown},
|
||||
}
|
||||
|
||||
bindings := map[string]string{
|
||||
"Quit": "x",
|
||||
}
|
||||
|
||||
updated, err := setBindings(bindings, group, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if updated[0].Key.Char != 'x' {
|
||||
t.Errorf("expected Quit to be rebound to 'x', got %+v", updated[0].Key)
|
||||
}
|
||||
|
||||
if updated[1].Key.Char != 'j' {
|
||||
t.Errorf("expected MoveDown to remain 'j', got %+v", updated[1].Key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rebinds to special key", func(t *testing.T) {
|
||||
group := Map{
|
||||
{Key: Key{Char: 'q'}, Cmd: cmd.Quit},
|
||||
}
|
||||
|
||||
bindings := map[string]string{
|
||||
"Quit": "Esc",
|
||||
}
|
||||
|
||||
updated, err := setBindings(bindings, group, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if updated[0].Key.Code != tcell.KeyEscape {
|
||||
t.Errorf("expected Quit to be rebound to Esc, got %+v", updated[0].Key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown command returns error", func(t *testing.T) {
|
||||
group := Map{
|
||||
{Key: Key{Char: 'q'}, Cmd: cmd.Quit},
|
||||
}
|
||||
|
||||
bindings := map[string]string{
|
||||
"FakeCommand": "x",
|
||||
}
|
||||
|
||||
_, err := setBindings(bindings, group, "test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown command, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid key returns error", func(t *testing.T) {
|
||||
group := Map{
|
||||
{Key: Key{Char: 'q'}, Cmd: cmd.Quit},
|
||||
}
|
||||
|
||||
bindings := map[string]string{
|
||||
"Quit": "BadKey",
|
||||
}
|
||||
|
||||
_, err := setBindings(bindings, group, "test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid key, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple bindings", func(t *testing.T) {
|
||||
group := Map{
|
||||
{Key: Key{Char: 'q'}, Cmd: cmd.Quit},
|
||||
{Key: Key{Char: 'j'}, Cmd: cmd.MoveDown},
|
||||
{Key: Key{Char: 'k'}, Cmd: cmd.MoveUp},
|
||||
}
|
||||
|
||||
bindings := map[string]string{
|
||||
"Quit": "x",
|
||||
"MoveUp": "p",
|
||||
}
|
||||
|
||||
updated, err := setBindings(bindings, group, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if updated[0].Key.Char != 'x' {
|
||||
t.Errorf("expected Quit rebound to 'x', got %+v", updated[0].Key)
|
||||
}
|
||||
if updated[1].Key.Char != 'j' {
|
||||
t.Errorf("expected MoveDown unchanged at 'j', got %+v", updated[1].Key)
|
||||
}
|
||||
if updated[2].Key.Char != 'p' {
|
||||
t.Errorf("expected MoveUp rebound to 'p', got %+v", updated[2].Key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func saveKeymaps() map[string]Map {
|
||||
saved := make(map[string]Map, len(Keymaps.Groups))
|
||||
for k, v := range Keymaps.Groups {
|
||||
cp := make(Map, len(v))
|
||||
copy(cp, v)
|
||||
saved[k] = cp
|
||||
}
|
||||
return saved
|
||||
}
|
||||
|
||||
func restoreKeymaps(saved map[string]Map) {
|
||||
for k, v := range saved {
|
||||
Keymaps.Groups[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyKeymapConfig(t *testing.T) {
|
||||
t.Run("nil config is no-op", func(t *testing.T) {
|
||||
err := ApplyKeymapConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty config is no-op", func(t *testing.T) {
|
||||
err := ApplyKeymapConfig(models.KeymapConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid config rebinds key", func(t *testing.T) {
|
||||
saved := saveKeymaps()
|
||||
defer restoreKeymaps(saved)
|
||||
|
||||
cfg := models.KeymapConfig{
|
||||
"home": {"Quit": "x"},
|
||||
}
|
||||
|
||||
err := ApplyKeymapConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
group := Keymaps.Groups[HomeGroup]
|
||||
for _, bind := range group {
|
||||
if bind.Cmd == cmd.Quit {
|
||||
if bind.Key.Char != 'x' {
|
||||
t.Errorf("expected Quit rebound to 'x', got %+v", bind.Key)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Error("Quit command not found in home group")
|
||||
})
|
||||
|
||||
t.Run("case insensitive group name", func(t *testing.T) {
|
||||
saved := saveKeymaps()
|
||||
defer restoreKeymaps(saved)
|
||||
|
||||
cfg := models.KeymapConfig{
|
||||
"Home": {"Quit": "x"},
|
||||
}
|
||||
|
||||
err := ApplyKeymapConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
group := Keymaps.Groups[HomeGroup]
|
||||
for _, bind := range group {
|
||||
if bind.Cmd == cmd.Quit {
|
||||
if bind.Key.Char != 'x' {
|
||||
t.Errorf("expected Quit rebound to 'x', got %+v", bind.Key)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Error("Quit command not found in home group")
|
||||
})
|
||||
|
||||
t.Run("unknown group returns error", func(t *testing.T) {
|
||||
cfg := models.KeymapConfig{
|
||||
"nonexistent": {"Quit": "x"},
|
||||
}
|
||||
|
||||
err := ApplyKeymapConfig(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown group, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown command in group returns error", func(t *testing.T) {
|
||||
cfg := models.KeymapConfig{
|
||||
"home": {"FakeCommand": "x"},
|
||||
}
|
||||
|
||||
err := ApplyKeymapConfig(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown command, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid key string returns error", func(t *testing.T) {
|
||||
cfg := models.KeymapConfig{
|
||||
"home": {"Quit": "SuperBadKey"},
|
||||
}
|
||||
|
||||
err := ApplyKeymapConfig(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid key, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rebinds special key in table group", func(t *testing.T) {
|
||||
saved := saveKeymaps()
|
||||
defer restoreKeymaps(saved)
|
||||
|
||||
cfg := models.KeymapConfig{
|
||||
"table": {"Search": "Ctrl-F"},
|
||||
}
|
||||
|
||||
err := ApplyKeymapConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
group := Keymaps.Groups[TableGroup]
|
||||
for _, bind := range group {
|
||||
if bind.Cmd == cmd.Search {
|
||||
if bind.Key.Code != tcell.KeyCtrlF {
|
||||
t.Errorf("expected Search rebound to Ctrl-F, got %+v", bind.Key)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Error("Search command not found in table group")
|
||||
})
|
||||
|
||||
t.Run("multiple groups and bindings", func(t *testing.T) {
|
||||
saved := saveKeymaps()
|
||||
defer restoreKeymaps(saved)
|
||||
|
||||
cfg := models.KeymapConfig{
|
||||
"home": {"Quit": "x"},
|
||||
"tree": {"GotoTop": "t"},
|
||||
}
|
||||
|
||||
err := ApplyKeymapConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
homeGroup := Keymaps.Groups[HomeGroup]
|
||||
for _, bind := range homeGroup {
|
||||
if bind.Cmd == cmd.Quit {
|
||||
if bind.Key.Char != 'x' {
|
||||
t.Errorf("home: expected Quit rebound to 'x', got %+v", bind.Key)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
treeGroup := Keymaps.Groups[TreeGroup]
|
||||
for _, bind := range treeGroup {
|
||||
if bind.Cmd == cmd.GotoTop {
|
||||
if bind.Key.Char != 't' {
|
||||
t.Errorf("tree: expected GotoTop rebound to 't', got %+v", bind.Key)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -39,6 +39,8 @@ type Connection struct {
|
||||
Commands []*Command
|
||||
}
|
||||
|
||||
type KeymapConfig map[string]map[string]string
|
||||
|
||||
type Command struct {
|
||||
Command string
|
||||
WaitForPort string
|
||||
|
||||
Reference in New Issue
Block a user