diff options
Diffstat (limited to 'crates')
5 files changed, 157 insertions, 6 deletions
diff --git a/crates/atuin/src/command/client/search/cursor.rs b/crates/atuin/src/command/client/search/cursor.rs index ebe8bbe5..c1cdfee4 100644 --- a/crates/atuin/src/command/client/search/cursor.rs +++ b/crates/atuin/src/command/client/search/cursor.rs @@ -151,6 +151,68 @@ impl Cursor { self.index = word_jumper.get_prev_word_pos(&self.source, self.index); } + /// Move cursor to the end of the current/next word (vim `e` motion). + /// + /// If cursor is in the middle of a word, moves to the end of that word. + /// If cursor is at the end of a word (or on whitespace), moves to the + /// end of the next word. + pub fn word_end(&mut self, word_chars: &str) { + let len = self.source.len(); + if self.index >= len { + return; + } + + let chars: Vec<char> = self.source.chars().collect(); + let mut char_idx = self.source[..self.index].chars().count(); + + if char_idx >= chars.len() { + return; + } + + let current = chars[char_idx]; + + // Check if we're at a word boundary (end of current word or on whitespace) + let at_word_boundary = current.is_whitespace() || char_idx + 1 >= chars.len() || { + let next = chars[char_idx + 1]; + next.is_whitespace() || (word_chars.contains(current) != word_chars.contains(next)) + }; + + // If at word boundary, advance past it and skip whitespace to find next word + if at_word_boundary { + char_idx += 1; + while char_idx < chars.len() && chars[char_idx].is_whitespace() { + char_idx += 1; + } + } + + // If we've gone past end, go to end of string + if char_idx >= chars.len() { + self.index = len; + return; + } + + // Find end of word: advance until next char is whitespace or different word type + let in_word_chars = word_chars.contains(chars[char_idx]); + while char_idx < chars.len() { + let next_idx = char_idx + 1; + if next_idx >= chars.len() { + // At last char, move past it + char_idx = next_idx; + break; + } + let next_c = chars[next_idx]; + if next_c.is_whitespace() || (word_chars.contains(next_c) != in_word_chars) { + // Next char is start of new word/whitespace, so current char is end + char_idx = next_idx; + break; + } + char_idx += 1; + } + + // Convert char index back to byte index + self.index = chars.iter().take(char_idx).map(|c| c.len_utf8()).sum(); + } + pub fn insert(&mut self, c: char) { self.source.insert(self.index, c); self.index += c.len_utf8(); diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs index 32110144..7cddb163 100644 --- a/crates/atuin/src/command/client/search/interactive.rs +++ b/crates/atuin/src/command/client/search/interactive.rs @@ -438,6 +438,10 @@ impl State { .next_word(&settings.word_chars, settings.word_jump_mode); InputAction::Continue } + Action::CursorWordEnd => { + self.search.input.word_end(&settings.word_chars); + InputAction::Continue + } Action::CursorStart => { self.search.input.start(); InputAction::Continue @@ -690,6 +694,12 @@ impl State { self.keymap_mode = KeymapMode::VimInsert; InputAction::Continue } + Action::VimChangeToEnd => { + self.search.input.clear_to_end(); + self.set_keymap_cursor(settings, "vim_insert"); + self.keymap_mode = KeymapMode::VimInsert; + InputAction::Continue + } Action::EnterPrefixMode => { self.prefix = true; InputAction::Continue diff --git a/crates/atuin/src/command/client/search/keybindings/actions.rs b/crates/atuin/src/command/client/search/keybindings/actions.rs index 5242b383..fae811d6 100644 --- a/crates/atuin/src/command/client/search/keybindings/actions.rs +++ b/crates/atuin/src/command/client/search/keybindings/actions.rs @@ -10,6 +10,7 @@ pub enum Action { CursorRight, CursorWordLeft, CursorWordRight, + CursorWordEnd, CursorStart, CursorEnd, @@ -60,6 +61,7 @@ pub enum Action { VimEnterInsertAtStart, VimEnterInsertAtEnd, VimSearchInsert, + VimChangeToEnd, EnterPrefixMode, // Inspector @@ -92,6 +94,7 @@ impl Action { "cursor-right" => Ok(Action::CursorRight), "cursor-word-left" => Ok(Action::CursorWordLeft), "cursor-word-right" => Ok(Action::CursorWordRight), + "cursor-word-end" => Ok(Action::CursorWordEnd), "cursor-start" => Ok(Action::CursorStart), "cursor-end" => Ok(Action::CursorEnd), @@ -134,6 +137,7 @@ impl Action { "vim-enter-insert-at-start" => Ok(Action::VimEnterInsertAtStart), "vim-enter-insert-at-end" => Ok(Action::VimEnterInsertAtEnd), "vim-search-insert" => Ok(Action::VimSearchInsert), + "vim-change-to-end" => Ok(Action::VimChangeToEnd), "enter-prefix-mode" => Ok(Action::EnterPrefixMode), "inspect-previous" => Ok(Action::InspectPrevious), @@ -152,6 +156,7 @@ impl Action { Action::CursorRight => "cursor-right".to_string(), Action::CursorWordLeft => "cursor-word-left".to_string(), Action::CursorWordRight => "cursor-word-right".to_string(), + Action::CursorWordEnd => "cursor-word-end".to_string(), Action::CursorStart => "cursor-start".to_string(), Action::CursorEnd => "cursor-end".to_string(), @@ -196,6 +201,7 @@ impl Action { Action::VimEnterInsertAtStart => "vim-enter-insert-at-start".to_string(), Action::VimEnterInsertAtEnd => "vim-enter-insert-at-end".to_string(), Action::VimSearchInsert => "vim-search-insert".to_string(), + Action::VimChangeToEnd => "vim-change-to-end".to_string(), Action::EnterPrefixMode => "enter-prefix-mode".to_string(), Action::InspectPrevious => "inspect-previous".to_string(), diff --git a/crates/atuin/src/command/client/search/keybindings/defaults.rs b/crates/atuin/src/command/client/search/keybindings/defaults.rs index e6d36b80..121c59fe 100644 --- a/crates/atuin/src/command/client/search/keybindings/defaults.rs +++ b/crates/atuin/src/command/client/search/keybindings/defaults.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use atuin_client::settings::{KeyBindingConfig, Settings}; +use tracing::warn; use super::actions::Action; use super::conditions::{ConditionAtom, ConditionExpr}; @@ -272,6 +273,19 @@ pub fn default_vim_normal_keymap(settings: &Settings) -> Keymap { km.bind(key("h"), Action::CursorLeft); km.bind(key("l"), Action::CursorRight); + // --- Vim cursor movement --- + km.bind(key("0"), Action::CursorStart); + km.bind(key("$"), Action::CursorEnd); + km.bind(key("w"), Action::CursorWordRight); + km.bind(key("b"), Action::CursorWordLeft); + km.bind(key("e"), Action::CursorWordEnd); + + // --- Vim editing --- + km.bind(key("x"), Action::DeleteCharAfter); + km.bind(key("d d"), Action::ClearLine); + km.bind(key("D"), Action::ClearToEnd); + km.bind(key("C"), Action::VimChangeToEnd); + // --- Mode switching --- km.bind(key("?"), Action::VimSearchInsert); km.bind(key("/"), Action::VimSearchInsert); @@ -334,10 +348,17 @@ pub fn default_vim_insert_keymap(settings: &Settings) -> Keymap { // --------------------------------------------------------------------------- /// Build the default inspector keymap (tab index 1). -pub fn default_inspector_keymap(_settings: &Settings) -> Keymap { +/// +/// The inspector shows details about the selected history item and has no +/// text input, so we build a minimal keymap with only inspector-relevant +/// bindings. We respect the user's `keymap_mode` to provide vim-style j/k +/// navigation for vim users. +pub fn default_inspector_keymap(settings: &Settings) -> Keymap { + use atuin_client::settings::KeymapMode; + let mut km = Keymap::new(); - // Common bindings + // Common bindings (same as search tab) km.bind(key("ctrl-c"), Action::ReturnOriginal); km.bind(key("ctrl-g"), Action::ReturnOriginal); km.bind(key("esc"), Action::Exit); @@ -345,10 +366,31 @@ pub fn default_inspector_keymap(_settings: &Settings) -> Keymap { km.bind(key("tab"), Action::ReturnSelection); km.bind(key("ctrl-o"), Action::ToggleTab); - // Inspector-specific + // Accept behavior respects enter_accept setting + let accept = if settings.enter_accept { + Action::Accept + } else { + Action::ReturnSelection + }; + km.bind(key("enter"), accept); + + // Inspector-specific: delete history entry km.bind(key("ctrl-d"), Action::Delete); + + // Inspector navigation km.bind(key("up"), Action::InspectPrevious); km.bind(key("down"), Action::InspectNext); + km.bind(key("pageup"), Action::InspectPrevious); + km.bind(key("pagedown"), Action::InspectNext); + + // For vim users, add j/k navigation + if matches!( + settings.keymap_mode, + KeymapMode::VimNormal | KeymapMode::VimInsert + ) { + km.bind(key("j"), Action::InspectNext); + km.bind(key("k"), Action::InspectPrevious); + } km } @@ -409,7 +451,7 @@ fn apply_config_to_keymap(keymap: &mut Keymap, overrides: &HashMap<String, KeyBi let key = match KeyInput::parse(key_str) { Ok(k) => k, Err(e) => { - eprintln!("[atuin] warning: invalid key in keymap config: {key_str:?}: {e}"); + warn!("invalid key in keymap config: {key_str:?}: {e}"); continue; } }; @@ -418,7 +460,7 @@ fn apply_config_to_keymap(keymap: &mut Keymap, overrides: &HashMap<String, KeyBi keymap.bindings.insert(key, binding); } Err(e) => { - eprintln!("[atuin] warning: invalid binding for {key_str:?} in keymap config: {e}"); + warn!("invalid binding for {key_str:?} in keymap config: {e}"); } } } diff --git a/crates/atuin/src/command/client/search/keybindings/key.rs b/crates/atuin/src/command/client/search/keybindings/key.rs index e9fb777b..717b406d 100644 --- a/crates/atuin/src/command/client/search/keybindings/key.rs +++ b/crates/atuin/src/command/client/search/keybindings/key.rs @@ -1,6 +1,6 @@ use std::fmt; -use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MediaKeyCode}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// A single key press with modifiers (e.g. `ctrl-c`, `alt-f`, `enter`). @@ -34,6 +34,7 @@ pub enum KeyCodeValue { PageDown, Space, F(u8), + Media(MediaKeyCode), } /// A key input that may be a single key or a multi-key sequence (e.g. `g g`). @@ -93,6 +94,7 @@ impl SingleKey { KeyCode::PageUp => KeyCodeValue::PageUp, KeyCode::PageDown => KeyCodeValue::PageDown, KeyCode::F(n) => KeyCodeValue::F(n), + KeyCode::Media(m) => KeyCodeValue::Media(m), _ => return None, }; @@ -163,6 +165,20 @@ impl SingleKey { "]" => KeyCodeValue::Char(']'), "?" => KeyCodeValue::Char('?'), "/" => KeyCodeValue::Char('/'), + "$" => KeyCodeValue::Char('$'), + // Media keys (no dashes - the parser splits on dash for modifiers) + "play" => KeyCodeValue::Media(MediaKeyCode::Play), + "pause" => KeyCodeValue::Media(MediaKeyCode::Pause), + "playpause" => KeyCodeValue::Media(MediaKeyCode::PlayPause), + "stop" => KeyCodeValue::Media(MediaKeyCode::Stop), + "fastforward" => KeyCodeValue::Media(MediaKeyCode::FastForward), + "rewind" => KeyCodeValue::Media(MediaKeyCode::Rewind), + "tracknext" => KeyCodeValue::Media(MediaKeyCode::TrackNext), + "trackprevious" => KeyCodeValue::Media(MediaKeyCode::TrackPrevious), + "record" => KeyCodeValue::Media(MediaKeyCode::Record), + "lowervolume" => KeyCodeValue::Media(MediaKeyCode::LowerVolume), + "raisevolume" => KeyCodeValue::Media(MediaKeyCode::RaiseVolume), + "mutevolume" | "mute" => KeyCodeValue::Media(MediaKeyCode::MuteVolume), _ => { let chars: Vec<char> = key_part.chars().collect(); if chars.len() == 1 { @@ -226,6 +242,21 @@ impl fmt::Display for SingleKey { KeyCodeValue::PageDown => write!(f, "pagedown"), KeyCodeValue::Space => write!(f, "space"), KeyCodeValue::F(n) => write!(f, "f{n}"), + KeyCodeValue::Media(m) => match m { + MediaKeyCode::Play => write!(f, "play"), + MediaKeyCode::Pause => write!(f, "media-pause"), + MediaKeyCode::PlayPause => write!(f, "playpause"), + MediaKeyCode::Stop => write!(f, "stop"), + MediaKeyCode::FastForward => write!(f, "fastforward"), + MediaKeyCode::Rewind => write!(f, "rewind"), + MediaKeyCode::TrackNext => write!(f, "tracknext"), + MediaKeyCode::TrackPrevious => write!(f, "trackprevious"), + MediaKeyCode::Record => write!(f, "record"), + MediaKeyCode::LowerVolume => write!(f, "lowervolume"), + MediaKeyCode::RaiseVolume => write!(f, "raisevolume"), + MediaKeyCode::MuteVolume => write!(f, "mutevolume"), + MediaKeyCode::Reverse => write!(f, "reverse"), + }, } } } |
