From 6bff8c8e1ad3a230f3cd8f5d7078ed2af3f43463 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Tue, 16 Jan 2024 22:35:10 +0900 Subject: [PATCH] feat(search): introduce keymap-dependent vim-mode (#1570) * feat(search): introduce keymap-dependent vim-mode * fix(zsh): provide widgets with specific keymaps * fix(settings): unify "vim" and "keymap_mode" --- atuin-client/config.toml | 9 +++-- atuin-client/src/settings.rs | 30 ++++++++++++++-- atuin/src/command/client/search.rs | 13 ++++++- .../src/command/client/search/interactive.rs | 35 ++++++++----------- atuin/src/command/init.rs | 14 ++++---- atuin/src/shell/atuin.bash | 24 ++++++------- atuin/src/shell/atuin.fish | 15 +++++++- atuin/src/shell/atuin.zsh | 18 +++++++++- 8 files changed, 112 insertions(+), 46 deletions(-) diff --git a/atuin-client/config.toml b/atuin-client/config.toml index 29581d1f..24366b60 100644 --- a/atuin-client/config.toml +++ b/atuin-client/config.toml @@ -126,8 +126,13 @@ enter_accept = true -## Defaults to false. If enabled you may use 'j' and 'k' to navigate the history list and 'i' to enter Insert mode. -# vim = false +## Defaults to "emacs". This specifies the keymap on the startup of `atuin +## search`. If this is set to "auto", the startup keymap mode in the Atuin +## search is automatically selected based on the shell's keymap where the +## keybinding is defined. If this is set to "emacs", "vim-insert", or +## "vim-normal", the startup keymap mode in the Atuin search is forced to be +## the specified one. +# keymap_mode = "auto" #[stats] # Set commands where we should consider the subcommand for statistics. Eg, kubectl get vs just kubectl diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs index ea01961c..36bbd826 100644 --- a/atuin-client/src/settings.rs +++ b/atuin-client/src/settings.rs @@ -143,6 +143,32 @@ pub enum WordJumpMode { Subl, } +#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)] +pub enum KeymapMode { + #[serde(rename = "emacs")] + Emacs, + + #[serde(rename = "vim-normal")] + VimNormal, + + #[serde(rename = "vim-insert")] + VimInsert, + + #[serde(rename = "auto")] + Auto, +} + +impl KeymapMode { + pub fn as_str(&self) -> &'static str { + match self { + KeymapMode::Emacs => "EMACS", + KeymapMode::VimNormal => "VIMNORMAL", + KeymapMode::VimInsert => "VIMINSERT", + KeymapMode::Auto => "AUTO", + } + } +} + #[derive(Clone, Debug, Deserialize)] pub struct Stats { #[serde(default = "Stats::common_prefix_default")] @@ -201,7 +227,7 @@ pub struct Settings { pub max_preview_height: u16, pub show_help: bool, pub exit_mode: ExitMode, - pub vim: bool, + pub keymap_mode: KeymapMode, pub word_jump_mode: WordJumpMode, pub word_chars: String, pub scroll_context_lines: usize, @@ -437,7 +463,7 @@ impl Settings { // New users will get the new default, that is more similar to what they are used to. .set_default("enter_accept", false)? .set_default("sync.records", false)? - .set_default("vim", false)? + .set_default("keymap_mode", "emacs")? .add_source( Environment::with_prefix("atuin") .prefix_separator("_") diff --git a/atuin/src/command/client/search.rs b/atuin/src/command/client/search.rs index 0875f3ad..726da348 100644 --- a/atuin/src/command/client/search.rs +++ b/atuin/src/command/client/search.rs @@ -6,7 +6,7 @@ use atuin_client::{ database::Database, database::{current_context, OptFilters}, history::History, - settings::{FilterMode, SearchMode, Settings}, + settings::{FilterMode, KeymapMode, SearchMode, Settings}, }; use super::history::ListMode; @@ -71,6 +71,10 @@ pub struct Cmd { #[arg(long = "shell-up-key-binding", hide = true)] shell_up_key_binding: bool, + /// Notify the keymap at the shell's side + #[arg(long = "keymap-mode", default_value = "auto")] + keymap_mode: KeymapMode, + /// Use human-readable formatting for time #[arg(long)] human: bool, @@ -142,6 +146,13 @@ impl Cmd { settings.shell_up_key_binding = self.shell_up_key_binding; + // `keymap_mode` specified in config.toml overrides the `--keymap-mode` + // option specified in the keybindings. + settings.keymap_mode = match settings.keymap_mode { + KeymapMode::Auto => self.keymap_mode, + value => value, + }; + if self.interactive { let item = interactive::history(&self.query, settings, db).await?; eprintln!("{item}"); diff --git a/atuin/src/command/client/search/interactive.rs b/atuin/src/command/client/search/interactive.rs index b47e5555..c838e873 100644 --- a/atuin/src/command/client/search/interactive.rs +++ b/atuin/src/command/client/search/interactive.rs @@ -21,7 +21,7 @@ use unicode_width::UnicodeWidthStr; use atuin_client::{ database::{current_context, Database}, history::{History, HistoryStats}, - settings::{ExitMode, FilterMode, SearchMode, Settings}, + settings::{ExitMode, FilterMode, KeymapMode, SearchMode, Settings}, }; use super::{ @@ -54,12 +54,6 @@ pub enum InputAction { Redraw, } -#[derive(PartialEq)] -enum VimMode { - Normal, - Insert, -} - #[allow(clippy::struct_field_names)] pub struct State { history_count: i64, @@ -69,7 +63,7 @@ pub struct State { search_mode: SearchMode, results_len: usize, accept: bool, - vim_mode: VimMode, + keymap_mode: KeymapMode, tab_index: usize, search: SearchState, @@ -159,9 +153,9 @@ impl State { // core input handling, common for all tabs match input.code { KeyCode::Char('c' | 'g') if ctrl => return InputAction::ReturnOriginal, - KeyCode::Esc if settings.vim && self.vim_mode == VimMode::Insert => { + KeyCode::Esc if self.keymap_mode == KeymapMode::VimInsert => { let _ = execute!(stdout(), SetCursorStyle::SteadyBlock); - self.vim_mode = VimMode::Normal; + self.keymap_mode = KeymapMode::VimNormal; return InputAction::Continue; } KeyCode::Esc => { @@ -311,8 +305,7 @@ impl State { KeyCode::Char('j') if !ctrl && !settings.invert - && settings.vim - && self.vim_mode == VimMode::Normal + && self.keymap_mode == KeymapMode::VimNormal && self.results_state.selected() == 0 => { do_exit!(); @@ -320,16 +313,15 @@ impl State { KeyCode::Char('k') if !ctrl && settings.invert - && settings.vim - && self.vim_mode == VimMode::Normal + && self.keymap_mode == KeymapMode::VimNormal && self.results_state.selected() == 0 => { do_exit!(); } - KeyCode::Char('k') if !ctrl && settings.vim && self.vim_mode == VimMode::Normal => { + KeyCode::Char('k') if !ctrl && self.keymap_mode == KeymapMode::VimNormal => { self.scroll_up(1); } - KeyCode::Char('j') if !ctrl && settings.vim && self.vim_mode == VimMode::Normal => { + KeyCode::Char('j') if !ctrl && self.keymap_mode == KeymapMode::VimNormal => { self.scroll_down(1); } KeyCode::Down if !settings.invert => { @@ -359,11 +351,11 @@ impl State { KeyCode::Char('l') if ctrl => { return InputAction::Redraw; } - KeyCode::Char('i') if settings.vim && self.vim_mode == VimMode::Normal => { + KeyCode::Char('i') if self.keymap_mode == KeymapMode::VimNormal => { let _ = execute!(stdout(), SetCursorStyle::BlinkingBlock); - self.vim_mode = VimMode::Insert; + self.keymap_mode = KeymapMode::VimInsert; } - KeyCode::Char(c) if !settings.vim || self.vim_mode == VimMode::Insert => { + KeyCode::Char(c) if self.keymap_mode != KeymapMode::VimNormal => { self.search.input.insert(c); } KeyCode::PageDown if !settings.invert => { @@ -832,7 +824,10 @@ pub async fn history( engine: engines::engine(search_mode), results_len: 0, accept: false, - vim_mode: VimMode::Normal, + keymap_mode: match settings.keymap_mode { + KeymapMode::Auto => KeymapMode::Emacs, + value => value, + }, }; let mut results = app.query_results(&mut db).await?; diff --git a/atuin/src/command/init.rs b/atuin/src/command/init.rs index 4eae98d3..ba501181 100644 --- a/atuin/src/command/init.rs +++ b/atuin/src/command/init.rs @@ -33,16 +33,16 @@ impl Cmd { if std::env::var("ATUIN_NOBIND").is_err() { const BIND_CTRL_R: &str = r"bindkey -M emacs '^r' _atuin_search_widget -bindkey -M vicmd '^r' _atuin_search_widget -bindkey -M viins '^r' _atuin_search_widget"; +bindkey -M vicmd '^r' _atuin_search_vicmd_widget +bindkey -M viins '^r' _atuin_search_viins_widget"; const BIND_UP_ARROW: &str = r"bindkey -M emacs '^[[A' _atuin_up_search_widget -bindkey -M vicmd '^[[A' _atuin_up_search_widget -bindkey -M viins '^[[A' _atuin_up_search_widget +bindkey -M vicmd '^[[A' _atuin_up_search_vicmd_widget +bindkey -M viins '^[[A' _atuin_up_search_viins_widget bindkey -M emacs '^[OA' _atuin_up_search_widget -bindkey -M vicmd '^[OA' _atuin_up_search_widget -bindkey -M viins '^[OA' _atuin_up_search_widget -bindkey -M vicmd 'k' _atuin_up_search_widget"; +bindkey -M vicmd '^[OA' _atuin_up_search_vicmd_widget +bindkey -M viins '^[OA' _atuin_up_search_viins_widget +bindkey -M vicmd 'k' _atuin_up_search_vicmd_widget"; if !self.disable_ctrl_r { println!("{BIND_CTRL_R}"); diff --git a/atuin/src/shell/atuin.bash b/atuin/src/shell/atuin.bash index 933416e6..490b7f71 100644 --- a/atuin/src/shell/atuin.bash +++ b/atuin/src/shell/atuin.bash @@ -227,33 +227,33 @@ if [[ $__atuin_bind_ctrl_r == true ]]; then # Note: We do not overwrite [C-r] in the vi-command keymap for Bash because # we do not want to overwrite "redo", which is already bound to [C-r] in # the vi_nmap keymap in ble.sh. - bind -m emacs -x '"\C-r": __atuin_history' - bind -m vi-insert -x '"\C-r": __atuin_history' + bind -m emacs -x '"\C-r": __atuin_history --keymap-mode=emacs' + bind -m vi-insert -x '"\C-r": __atuin_history --keymap-mode=vim-insert' fi # shellcheck disable=SC2154 if [[ $__atuin_bind_up_arrow == true ]]; then if ((BASH_VERSINFO[0] > 4 || BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 3)); then - bind -m emacs -x '"\e[A": __atuin_history --shell-up-key-binding' - bind -m emacs -x '"\eOA": __atuin_history --shell-up-key-binding' - bind -m vi-insert -x '"\e[A": __atuin_history --shell-up-key-binding' - bind -m vi-insert -x '"\eOA": __atuin_history --shell-up-key-binding' - bind -m vi-command -x '"\e[A": __atuin_history --shell-up-key-binding' - bind -m vi-command -x '"\eOA": __atuin_history --shell-up-key-binding' - bind -m vi-command -x '"k": __atuin_history --shell-up-key-binding' + bind -m emacs -x '"\e[A": __atuin_history --shell-up-key-binding --keymap-mode=emacs' + bind -m emacs -x '"\eOA": __atuin_history --shell-up-key-binding --keymap-mode=emacs' + bind -m vi-insert -x '"\e[A": __atuin_history --shell-up-key-binding --keymap-mode=vim-insert' + bind -m vi-insert -x '"\eOA": __atuin_history --shell-up-key-binding --keymap-mode=vim-insert' + bind -m vi-command -x '"\e[A": __atuin_history --shell-up-key-binding --keymap-mode=vim-normal' + bind -m vi-command -x '"\eOA": __atuin_history --shell-up-key-binding --keymap-mode=vim-normal' + bind -m vi-command -x '"k": __atuin_history --shell-up-key-binding --keymap-mode=vim-normal' else # In bash < 4.3, "bind -x" cannot bind a shell command to a keyseq # having more than two bytes. To work around this, we first translate # the keyseqs to the two-byte sequence \C-x\C-p (which is not used by # default) using string macros and run the shell command through the # keybinding to \C-x\C-p. - bind -m emacs -x '"\C-x\C-p": __atuin_history --shell-up-key-binding' + bind -m emacs -x '"\C-x\C-p": __atuin_history --shell-up-key-binding --keymap-mode=emacs' bind -m emacs '"\e[A": "\C-x\C-p"' bind -m emacs '"\eOA": "\C-x\C-p"' - bind -m vi-insert -x '"\C-x\C-p": __atuin_history --shell-up-key-binding' + bind -m vi-insert -x '"\C-x\C-p": __atuin_history --shell-up-key-binding --keymap-mode=vim-insert' bind -m vi-insert -x '"\e[A": "\C-x\C-p"' bind -m vi-insert -x '"\eOA": "\C-x\C-p"' - bind -m vi-command -x '"\C-x\C-p": __atuin_history --shell-up-key-binding' + bind -m vi-command -x '"\C-x\C-p": __atuin_history --shell-up-key-binding --keymap-mode=vim-normal' bind -m vi-command -x '"\e[A": "\C-x\C-p"' bind -m vi-command -x '"\eOA": "\C-x\C-p"' bind -m vi-command -x '"k": "\C-x\C-p"' diff --git a/atuin/src/shell/atuin.fish b/atuin/src/shell/atuin.fish index ee410058..b10d70ec 100644 --- a/atuin/src/shell/atuin.fish +++ b/atuin/src/shell/atuin.fish @@ -19,10 +19,23 @@ function _atuin_postexec --on-event fish_postexec end function _atuin_search + set -l keymap_mode + switch $fish_key_bindings + case fish_vi_key_bindings + switch $fish_bind_mode + case default + set keymap_mode vim-normal + case insert + set keymap_mode vim-insert + end + case '*' + set keymap_mode emacs + end + # In fish 3.4 and above we can use `"$(some command)"` to keep multiple lines separate; # but to support fish 3.3 we need to use `(some command | string collect)`. # https://fishshell.com/docs/current/relnotes.html#id24 (fish 3.4 "Notable improvements and fixes") - set -l ATUIN_H (ATUIN_SHELL_FISH=t ATUIN_LOG=error atuin search $argv -i -- (commandline -b) 3>&1 1>&2 2>&3 | string collect) + set -l ATUIN_H (ATUIN_SHELL_FISH=t ATUIN_LOG=error atuin search --keymap-mode=$keymap_mode $argv -i -- (commandline -b) 3>&1 1>&2 2>&3 | string collect) if test -n "$ATUIN_H" if string match --quiet '__atuin_accept__:*' "$ATUIN_H" diff --git a/atuin/src/shell/atuin.zsh b/atuin/src/shell/atuin.zsh index 85aba182..152d674f 100644 --- a/atuin/src/shell/atuin.zsh +++ b/atuin/src/shell/atuin.zsh @@ -71,18 +71,34 @@ _atuin_search() { fi fi } +_atuin_search_vicmd() { + _atuin_search --keymap-mode=vim-normal +} +_atuin_search_viins() { + _atuin_search --keymap-mode=vim-insert +} _atuin_up_search() { # Only trigger if the buffer is a single line if [[ ! $BUFFER == *$'\n'* ]]; then - _atuin_search --shell-up-key-binding + _atuin_search --shell-up-key-binding "$@" else zle up-line fi } +_atuin_up_search_vicmd() { + _atuin_up_search --keymap-mode=vim-normal +} +_atuin_up_search_viins() { + _atuin_up_search --keymap-mode=vim-insert +} add-zsh-hook preexec _atuin_preexec add-zsh-hook precmd _atuin_precmd zle -N _atuin_search_widget _atuin_search +zle -N _atuin_search_vicmd_widget _atuin_search_vicmd +zle -N _atuin_search_viins_widget _atuin_search_viins zle -N _atuin_up_search_widget _atuin_up_search +zle -N _atuin_up_search_vicmd_widget _atuin_up_search_vicmd +zle -N _atuin_up_search_viins_widget _atuin_up_search_viins