224 lines
7.7 KiB
Rust
224 lines
7.7 KiB
Rust
//! Keybinding abstraction — Action enum + KeyMap with presets (Emacs/Vim/Windows).
|
|
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum Action {
|
|
Submit,
|
|
Cancel,
|
|
Newline,
|
|
ScrollUp,
|
|
ScrollDown,
|
|
ScrollHalfUp,
|
|
ScrollHalfDown,
|
|
ScrollTop,
|
|
ScrollBottom,
|
|
Exit,
|
|
ProviderSwap,
|
|
TeamToggle,
|
|
CommandPalette,
|
|
ToggleSidebar,
|
|
ToggleAgentView,
|
|
Help,
|
|
ClearConversation,
|
|
FocusInput,
|
|
FocusConversation,
|
|
CycleChatMode,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum KeyPreset {
|
|
Emacs,
|
|
Vim,
|
|
Windows,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum VimMode {
|
|
Normal,
|
|
Insert,
|
|
}
|
|
|
|
pub struct KeyMap {
|
|
preset: KeyPreset,
|
|
bindings: HashMap<(KeyModifiers, KeyCode), Action>,
|
|
vim_mode: VimMode,
|
|
}
|
|
|
|
impl KeyMap {
|
|
pub fn new(preset: KeyPreset) -> Self {
|
|
let mut map = Self {
|
|
preset,
|
|
bindings: HashMap::new(),
|
|
vim_mode: VimMode::Insert,
|
|
};
|
|
map.load_preset(preset);
|
|
map
|
|
}
|
|
|
|
fn load_preset(&mut self, preset: KeyPreset) {
|
|
self.bindings.clear();
|
|
match preset {
|
|
KeyPreset::Emacs => self.load_emacs(),
|
|
KeyPreset::Vim => self.load_emacs(), // Vim overrides handled in resolve()
|
|
KeyPreset::Windows => self.load_windows(),
|
|
}
|
|
}
|
|
|
|
fn load_emacs(&mut self) {
|
|
self.bind(KeyModifiers::NONE, KeyCode::Enter, Action::Submit);
|
|
self.bind(KeyModifiers::SHIFT, KeyCode::Enter, Action::Newline);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('c'), Action::Cancel);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('d'), Action::Exit);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('p'), Action::ProviderSwap);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('t'), Action::TeamToggle);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('k'), Action::CommandPalette);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('b'), Action::ToggleSidebar);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('a'), Action::ToggleAgentView);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('l'), Action::ClearConversation);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('h'), Action::Help);
|
|
self.bind(KeyModifiers::NONE, KeyCode::PageUp, Action::ScrollHalfUp);
|
|
self.bind(KeyModifiers::NONE, KeyCode::PageDown, Action::ScrollHalfDown);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Home, Action::ScrollTop);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::End, Action::ScrollBottom);
|
|
self.bind(KeyModifiers::NONE, KeyCode::F(1), Action::Help);
|
|
}
|
|
|
|
fn load_windows(&mut self) {
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Enter, Action::Submit);
|
|
self.bind(KeyModifiers::NONE, KeyCode::Enter, Action::Newline);
|
|
self.bind(KeyModifiers::NONE, KeyCode::Esc, Action::Cancel);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('d'), Action::Exit);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('p'), Action::ProviderSwap);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('k'), Action::CommandPalette);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('a'), Action::ToggleAgentView);
|
|
self.bind(KeyModifiers::CONTROL, KeyCode::Char('l'), Action::ClearConversation);
|
|
self.bind(KeyModifiers::NONE, KeyCode::F(1), Action::Help);
|
|
self.bind(KeyModifiers::NONE, KeyCode::PageUp, Action::ScrollHalfUp);
|
|
self.bind(KeyModifiers::NONE, KeyCode::PageDown, Action::ScrollHalfDown);
|
|
}
|
|
|
|
fn bind(&mut self, modifiers: KeyModifiers, code: KeyCode, action: Action) {
|
|
self.bindings.insert((modifiers, code), action);
|
|
}
|
|
|
|
pub fn resolve(&self, key: KeyEvent) -> Option<Action> {
|
|
// Vim normal mode overrides
|
|
if self.preset == KeyPreset::Vim && self.vim_mode == VimMode::Normal {
|
|
return match (key.modifiers, key.code) {
|
|
(KeyModifiers::NONE, KeyCode::Char('j')) => Some(Action::ScrollDown),
|
|
(KeyModifiers::NONE, KeyCode::Char('k')) => Some(Action::ScrollUp),
|
|
(KeyModifiers::NONE, KeyCode::Char('g')) => Some(Action::ScrollTop),
|
|
(KeyModifiers::NONE, KeyCode::Char('G')) => Some(Action::ScrollBottom),
|
|
(KeyModifiers::NONE, KeyCode::Char('i')) => Some(Action::FocusInput),
|
|
(KeyModifiers::NONE, KeyCode::Char(':')) => Some(Action::CommandPalette),
|
|
(KeyModifiers::NONE, KeyCode::Char('q')) => Some(Action::Exit),
|
|
(KeyModifiers::NONE, KeyCode::Esc) => Some(Action::Cancel),
|
|
_ => self.bindings.get(&(key.modifiers, key.code)).copied(),
|
|
};
|
|
}
|
|
self.bindings.get(&(key.modifiers, key.code)).copied()
|
|
}
|
|
|
|
pub fn set_vim_mode(&mut self, mode: VimMode) {
|
|
self.vim_mode = mode;
|
|
}
|
|
|
|
pub fn vim_mode(&self) -> VimMode {
|
|
self.vim_mode
|
|
}
|
|
|
|
pub fn preset(&self) -> KeyPreset {
|
|
self.preset
|
|
}
|
|
|
|
pub fn set_preset(&mut self, preset: KeyPreset) {
|
|
self.preset = preset;
|
|
self.load_preset(preset);
|
|
self.vim_mode = VimMode::Insert;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_emacs_submit() {
|
|
let km = KeyMap::new(KeyPreset::Emacs);
|
|
let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
|
|
assert_eq!(km.resolve(key), Some(Action::Submit));
|
|
}
|
|
|
|
#[test]
|
|
fn test_emacs_ctrl_d() {
|
|
let km = KeyMap::new(KeyPreset::Emacs);
|
|
let key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
|
|
assert_eq!(km.resolve(key), Some(Action::Exit));
|
|
}
|
|
|
|
#[test]
|
|
fn test_emacs_ctrl_k() {
|
|
let km = KeyMap::new(KeyPreset::Emacs);
|
|
let key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
|
|
assert_eq!(km.resolve(key), Some(Action::CommandPalette));
|
|
}
|
|
|
|
#[test]
|
|
fn test_vim_normal_j() {
|
|
let mut km = KeyMap::new(KeyPreset::Vim);
|
|
km.set_vim_mode(VimMode::Normal);
|
|
let key = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE);
|
|
assert_eq!(km.resolve(key), Some(Action::ScrollDown));
|
|
}
|
|
|
|
#[test]
|
|
fn test_vim_normal_k() {
|
|
let mut km = KeyMap::new(KeyPreset::Vim);
|
|
km.set_vim_mode(VimMode::Normal);
|
|
let key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE);
|
|
assert_eq!(km.resolve(key), Some(Action::ScrollUp));
|
|
}
|
|
|
|
#[test]
|
|
fn test_vim_normal_i() {
|
|
let mut km = KeyMap::new(KeyPreset::Vim);
|
|
km.set_vim_mode(VimMode::Normal);
|
|
let key = KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE);
|
|
assert_eq!(km.resolve(key), Some(Action::FocusInput));
|
|
}
|
|
|
|
#[test]
|
|
fn test_vim_normal_colon() {
|
|
let mut km = KeyMap::new(KeyPreset::Vim);
|
|
km.set_vim_mode(VimMode::Normal);
|
|
let key = KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE);
|
|
assert_eq!(km.resolve(key), Some(Action::CommandPalette));
|
|
}
|
|
|
|
#[test]
|
|
fn test_unbound_key_returns_none() {
|
|
let km = KeyMap::new(KeyPreset::Emacs);
|
|
let key = KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE);
|
|
assert_eq!(km.resolve(key), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_preset_switch() {
|
|
let mut km = KeyMap::new(KeyPreset::Emacs);
|
|
assert_eq!(km.preset(), KeyPreset::Emacs);
|
|
km.set_preset(KeyPreset::Vim);
|
|
assert_eq!(km.preset(), KeyPreset::Vim);
|
|
// Should reset to Insert mode
|
|
assert_eq!(km.vim_mode(), VimMode::Insert);
|
|
}
|
|
|
|
#[test]
|
|
fn test_windows_ctrl_enter() {
|
|
let km = KeyMap::new(KeyPreset::Windows);
|
|
let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL);
|
|
assert_eq!(km.resolve(key), Some(Action::Submit));
|
|
}
|
|
}
|