From 52851caa12018baecc943a3563ee60c655377b65 Mon Sep 17 00:00:00 2001 From: kencx Date: Sat, 3 Jun 2023 21:12:30 +0800 Subject: [PATCH] feat: allow customization of search bar key bindings --- README.md | 2 +- config/config.go | 208 ++++++++++++++++++++---------------- config/config_test.go | 45 ++++---- examples/config/README.md | 16 ++- examples/config/default.yml | 9 ++ testdata/testConfig.yml | 9 ++ ui/list/keymap.go | 36 +++---- ui/list/list.go | 19 ++-- ui/list/update.go | 6 +- 9 files changed, 201 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index a4753e0..70e681c 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ See [config](examples/config/README.md) for all configuration options. - [x] Ability to customize keyb hotkeys - [x] `a, add` subcommand to quickly add a single hotkey entry from the CLI - [ ] Export to additional file formats (`json, toml, conf/ini` etc.) -- [ ] Allow customization of search bar key bindings +- [ ] Support multiple keyb files or directories ## Contributing diff --git a/config/config.go b/config/config.go index 1204535..8bd54bb 100644 --- a/config/config.go +++ b/config/config.go @@ -51,81 +51,99 @@ type Color struct { } type Keys struct { - Quit string - Up string - Down string - UpFocus string `yaml:"up_focus"` - DownFocus string `yaml:"down_focus"` - HalfUp string `yaml:"half_up"` - HalfDown string `yaml:"half_down"` - FullUp string `yaml:"full_up"` - FullDown string `yaml:"full_bottom"` - GoToFirstLine string `yaml:"first_line"` - GoToLastLine string `yaml:"last_line"` - GoToTop string `yaml:"top"` - GoToMiddle string `yaml:"middle"` - GoToBottom string `yaml:"bottom"` - Search string - ClearSearch string `yaml:"clear_search"` - Normal string + Quit string + Up string + Down string + UpFocus string `yaml:"up_focus"` + DownFocus string `yaml:"down_focus"` + HalfUp string `yaml:"half_up"` + HalfDown string `yaml:"half_down"` + FullUp string `yaml:"full_up"` + FullDown string `yaml:"full_bottom"` + GoToFirstLine string `yaml:"first_line"` + GoToLastLine string `yaml:"last_line"` + GoToTop string `yaml:"top"` + GoToMiddle string `yaml:"middle"` + GoToBottom string `yaml:"bottom"` + Search string + ClearSearch string `yaml:"clear_search"` + Normal string + CursorWordForward string `yaml:"cursor_word_forward"` + CursorWordBackward string `yaml:"cursor_word_backward"` + CursorDeleteWordBackward string `yaml:"cursor_delete_word_backward"` + CursorDeleteWordForward string `yaml:"cursor_delete_word_forward"` + CursorDeleteAfterCursor string `yaml:"cursor_delete_after_cursor"` + CursorDeleteBeforeCursor string `yaml:"cursor_delete_before_cursor"` + CursorLineStart string `yaml:"cursor_line_start"` + CursorLineEnd string `yaml:"cursor_line_end"` + CursorPaste string `yaml:"cursor_paste"` } var DefaultConfig = &Config{ - Settings: Settings{ - Debug: false, - Reverse: false, - Mouse: true, - SearchMode: false, - SortKeys: false, - Title: "", - Prompt: "keys > ", - PromptLocation: "top", - Placeholder: "...", - PrefixSep: ";", - SepWidth: 4, - Margin: 0, - Padding: 1, - BorderStyle: "hidden", - }, - Color: Color{ - FilterFg: "#FFA066", - }, - Keys: Keys{ - Quit: "q, ctrl+c", - Up: "k, up", - Down: "j, down", - UpFocus: "ctrl+k", - DownFocus: "ctrl+j", - HalfUp: "ctrl+u", - HalfDown: "ctrl+d", - FullUp: "ctrl+b", - FullDown: "ctrl+f", - GoToFirstLine: "g", - GoToLastLine: "G", - GoToTop: "H", - GoToMiddle: "M", - GoToBottom: "L", - Search: "/", - ClearSearch: "alt+d", - Normal: "esc", - }, - } + Settings: Settings{ + Debug: false, + Reverse: false, + Mouse: true, + SearchMode: false, + SortKeys: false, + Title: "", + Prompt: "keys > ", + PromptLocation: "top", + Placeholder: "...", + PrefixSep: ";", + SepWidth: 4, + Margin: 0, + Padding: 1, + BorderStyle: "hidden", + }, + Color: Color{ + FilterFg: "#FFA066", + }, + Keys: Keys{ + Quit: "q, ctrl+c", + Up: "k, up", + Down: "j, down", + UpFocus: "ctrl+k", + DownFocus: "ctrl+j", + HalfUp: "ctrl+u", + HalfDown: "ctrl+d", + FullUp: "ctrl+b", + FullDown: "ctrl+f", + GoToFirstLine: "g", + GoToLastLine: "G", + GoToTop: "H", + GoToMiddle: "M", + GoToBottom: "L", + Search: "/", + ClearSearch: "alt+d", + Normal: "esc", + CursorWordForward: "alt+right, alt+f", + CursorWordBackward: "alt+left, alt+b", + CursorDeleteWordBackward: "alt+backspace", + CursorDeleteWordForward: "alt+delete", + CursorDeleteAfterCursor: "alt+k", + CursorDeleteBeforeCursor: "alt+u", + CursorLineStart: "home, ctrl+a", + CursorLineEnd: "end, ctrl+e", + CursorPaste: "ctrl+v", + }, +} func Parse(flagKPath, configPath string) (Apps, *Config, error) { - var ( - config *Config - err error - ) - - switch configPath { - case "": - config, err = ReadDefaultConfigFile() - default: - config, err = ReadConfigFile(configPath) - } - if err != nil { - return nil, nil, err - } + var ( + config *Config + err error + ) + + switch configPath { + case "": + config, err = ReadDefaultConfigFile() + default: + config, err = ReadConfigFile(configPath) + } + if err != nil { + return nil, nil, err + } // priority: flag > file var kPath string @@ -133,7 +151,7 @@ func Parse(flagKPath, configPath string) (Apps, *Config, error) { kPath = flagKPath } - // If no keyb file present, create a default file and set it as kPath + // If no keyb file present, create a default file and set it as kPath if kPath == "" { kPath = config.KeybPath if !pathExists(kPath) { @@ -152,29 +170,29 @@ func Parse(flagKPath, configPath string) (Apps, *Config, error) { // Read config file at default path if exist. Otherwise, return default config func ReadDefaultConfigFile() (*Config, error) { - baseDir, err := getBaseDir() - if err != nil { - return nil, err - } - configDir, err := getConfigDir(baseDir) - if err != nil { - return nil, err - } - - var config *Config - defaultConfigFilePath := filepath.Join(configDir, configFileName) - if !pathExists(defaultConfigFilePath) { - config, err = newDefaultConfig() - if err != nil { - return nil, err - } - } else { - config, err = ReadConfigFile(defaultConfigFilePath) - if err != nil { - return nil, err - } - } - return config, nil + baseDir, err := getBaseDir() + if err != nil { + return nil, err + } + configDir, err := getConfigDir(baseDir) + if err != nil { + return nil, err + } + + var config *Config + defaultConfigFilePath := filepath.Join(configDir, configFileName) + if !pathExists(defaultConfigFilePath) { + config, err = newDefaultConfig() + if err != nil { + return nil, err + } + } else { + config, err = ReadConfigFile(defaultConfigFilePath) + if err != nil { + return nil, err + } + } + return config, nil } // Read given config file and merge with default config @@ -201,7 +219,7 @@ func ReadConfigFile(path string) (*Config, error) { } func newDefaultConfig() (*Config, error) { - res := DefaultConfig + res := DefaultConfig baseDir, err := getBaseDir() if err != nil { diff --git a/config/config_test.go b/config/config_test.go index 8502f82..3f73d71 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -9,7 +9,7 @@ import ( ) const ( - testDirPath = "../testdata" + testDirPath = "../testdata" testConfigDir = keybDirPath ) @@ -71,23 +71,32 @@ func TestReadConfigFile(t *testing.T) { FilterFg: "#FFA066", }, Keys: Keys{ - Quit: "q, ctrl+c", - Up: "k, up", - Down: "j, down", - UpFocus: "alt+k", - DownFocus: "alt+j", - HalfUp: "ctrl+u", - HalfDown: "ctrl+d", - FullUp: "ctrl+b", - FullDown: "ctrl+f", - GoToFirstLine: "g", - GoToLastLine: "G", - GoToTop: "H", - GoToMiddle: "M", - GoToBottom: "L", - Search: "/", - ClearSearch: "alt+d", - Normal: "esc", + Quit: "q, ctrl+c", + Up: "k, up", + Down: "j, down", + UpFocus: "alt+k", + DownFocus: "alt+j", + HalfUp: "ctrl+u", + HalfDown: "ctrl+d", + FullUp: "ctrl+b", + FullDown: "ctrl+f", + GoToFirstLine: "g", + GoToLastLine: "G", + GoToTop: "H", + GoToMiddle: "M", + GoToBottom: "L", + Search: "/", + ClearSearch: "alt+d", + Normal: "esc", + CursorWordForward: "alt+right, alt+f", + CursorWordBackward: "alt+left, alt+b", + CursorDeleteWordBackward: "alt+backspace", + CursorDeleteWordForward: "alt+delete", + CursorDeleteAfterCursor: "alt+k", + CursorDeleteBeforeCursor: "alt+u", + CursorLineStart: "home, ctrl+a", + CursorLineEnd: "end, ctrl+e", + CursorPaste: "ctrl+v", }, } got, err := ReadConfigFile(filepath.Join(testDirPath, "testConfig.yml")) diff --git a/examples/config/README.md b/examples/config/README.md index aa9500a..68b950f 100644 --- a/examples/config/README.md +++ b/examples/config/README.md @@ -66,8 +66,16 @@ Multiple keys may be set for a single binding, separated by commas. | `normal` | Esc | Exit search mode | | `quit` | Ctrl + c, q | Quit | ->The ->[default](https://github.com/charmbracelet/bubbles/blob/afd7868712d4a4f817829dc7e1868c337c5e5cff/textinput/textinput.go#L61) ->key bindings for the search bar have been reset temporarily. Customization of ->these key bindings are coming soon. +These hotkeys configure the cursor behaviour in the search bar only: +| Hotkey | Default | Description | +| ----------------------- | --------------------------- | ---------------- | +| `cursor_word_forward` | alt+right, alt+f | Move forward by word | +| `cursor_word_backward` | alt+left, alt+b | Move backward by word | +| `cursor_delete_word_backward` | alt+backspace | Delete word backward | +| `cursor_delete_word_forward` | alt+delete | Delete word forward | +| `cursor_delete_after_cursor` | alt+k | Delete after cursor | +| `cursor_delete_before_cursor` | alt+u | Delete before cursor | +| `cursor_line_start` | home, ctrl+a | Move cursor to start | +| `cursor_line_end` | end, ctrl+e | Move cursor to end | +| `cursor_paste` | ctrl+v | Paste into search bar| diff --git a/examples/config/default.yml b/examples/config/default.yml index f5c63a8..26dbf18 100644 --- a/examples/config/default.yml +++ b/examples/config/default.yml @@ -39,3 +39,12 @@ keys: search: / clear_search: alt+d normal: esc + cursor_word_forward: "alt+right, alt+f" + cursor_word_backward: "alt+left, alt+b" + cursor_delete_word_backward: "alt+backspace" + cursor_delete_word_forward: "alt+delete" + cursor_delete_after_cursor: "alt+k" + cursor_delete_before_cursor: "alt+u" + cursor_line_start: "home, ctrl+a" + cursor_line_end: "end, ctrl+e" + cursor_paste: "ctrl+v" diff --git a/testdata/testConfig.yml b/testdata/testConfig.yml index 2602105..1fe0ecc 100644 --- a/testdata/testConfig.yml +++ b/testdata/testConfig.yml @@ -39,3 +39,12 @@ keys: search: / clear_search: alt+d normal: esc + cursor_word_forward: "alt+right, alt+f" + cursor_word_backward: "alt+left, alt+b" + cursor_delete_word_backward: "alt+backspace" + cursor_delete_word_forward: "alt+delete" + cursor_delete_after_cursor: "alt+k" + cursor_delete_before_cursor: "alt+u" + cursor_line_start: "home, ctrl+a" + cursor_line_end: "end, ctrl+e" + cursor_paste: "ctrl+v" diff --git a/ui/list/keymap.go b/ui/list/keymap.go index e3679c9..e2b22a7 100644 --- a/ui/list/keymap.go +++ b/ui/list/keymap.go @@ -29,6 +29,8 @@ type KeyMap struct { Search key.Binding ClearSearch key.Binding Normal key.Binding + + TextInputKeyMap } type TextInputKeyMap struct { @@ -67,25 +69,23 @@ func CreateKeyMap(keys config.Keys) KeyMap { Search: SetKey(keys.Search), ClearSearch: SetKey(keys.ClearSearch), Normal: SetKey(keys.Normal), - } -} -func CreateTextInputKeyMap() TextInputKeyMap { - return TextInputKeyMap { - CharacterForward: SetKey("right"), - CharacterBackward: SetKey("left"), - WordForward: SetKey("alt+right, alt+f"), - WordBackward: SetKey("alt+left, alt+b"), - DeleteWordBackward: SetKey("alt+backspace"), - DeleteWordForward: SetKey("alt+delete"), - DeleteAfterCursor: SetKey("alt+k"), - DeleteBeforeCursor: SetKey("alt+u"), - DeleteCharacterBackward: SetKey("backspace"), - DeleteCharacterForward: SetKey("delete"), - LineStart: SetKey("home, ctrl+a"), - LineEnd: SetKey("end, ctrl+e"), - Paste: SetKey("ctrl+v"), - } + TextInputKeyMap: TextInputKeyMap{ + CharacterForward: SetKey("right"), + CharacterBackward: SetKey("left"), + WordForward: SetKey(keys.CursorWordForward), + WordBackward: SetKey(keys.CursorWordBackward), + DeleteWordBackward: SetKey(keys.CursorDeleteWordBackward), + DeleteWordForward: SetKey(keys.CursorDeleteWordForward), + DeleteAfterCursor: SetKey(keys.CursorDeleteAfterCursor), + DeleteBeforeCursor: SetKey(keys.CursorDeleteBeforeCursor), + DeleteCharacterBackward: SetKey("backspace"), + DeleteCharacterForward: SetKey("delete"), + LineStart: SetKey(keys.CursorLineStart), + LineEnd: SetKey(keys.CursorLineEnd), + Paste: SetKey(keys.CursorPaste), + }, + } } func SetKey(s string) key.Binding { diff --git a/ui/list/list.go b/ui/list/list.go index c50ce0a..de565dc 100644 --- a/ui/list/list.go +++ b/ui/list/list.go @@ -45,8 +45,10 @@ type Model struct { } func New(t *table.Model, c *config.Config) Model { + keyMap := CreateKeyMap(c.Keys) + m := Model{ - keys: CreateKeyMap(c.Keys), + keys: keyMap, viewport: viewport.Model{ YOffset: 0, MouseWheelDelta: 3, @@ -62,7 +64,7 @@ func New(t *table.Model, c *config.Config) Model { EchoCharacter: '*', CharLimit: 0, Cursor: cursor.New(), - KeyMap: textinput.KeyMap(CreateTextInputKeyMap()), + KeyMap: textinput.KeyMap(keyMap.TextInputKeyMap), }, startInSearchMode: c.SearchMode, @@ -80,7 +82,10 @@ func New(t *table.Model, c *config.Config) Model { promptLocation: c.PromptLocation, } - m.configure(c) + m.table.SepWidth = c.SepWidth + m.filteredTable.SepWidth = c.SepWidth + m.scrollOffset += (m.margin * 2) + (m.padding * 2) + m.style(c) if m.startInSearchMode { m.startSearch() @@ -89,8 +94,7 @@ func New(t *table.Model, c *config.Config) Model { return m } -func (m *Model) configure(c *config.Config) { - +func (m *Model) style(c *config.Config) { if c.PlaceholderFg != "" || c.PlaceholderBg != "" { m.searchBar.PlaceholderStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(c.PlaceholderFg)). @@ -101,11 +105,6 @@ func (m *Model) configure(c *config.Config) { m.counterStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(c.CounterFg)).Background(lipgloss.Color(c.CounterBg)).Margin(0, 1) } - m.table.SepWidth = c.SepWidth - m.filteredTable.SepWidth = c.SepWidth - - m.scrollOffset += (m.margin * 2) + (m.padding * 2) - var b lipgloss.Border switch c.BorderStyle { case "normal": diff --git a/ui/list/update.go b/ui/list/update.go index da79a62..b670ac9 100644 --- a/ui/list/update.go +++ b/ui/list/update.go @@ -197,19 +197,19 @@ func (m *Model) handleSearch(msg tea.Msg) tea.Cmd { m.searchBar.Reset() return m.startSearch() - // scrolling in search mode + // scrolling in search mode case key.Matches(msg, m.keys.UpFocus): m.cursor-- if m.cursorPastViewTop() { m.viewport.LineUp(1) } - return nil + return nil case key.Matches(msg, m.keys.DownFocus): m.cursor++ if m.cursorPastViewBottom() { m.viewport.LineDown(1) } - return nil + return nil case key.Matches(msg, m.keys.HalfUp): m.cursor -= m.viewport.Height / 2