1use crate::event::{Key, KeyBinding};
23use std::collections::HashMap;
24
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
27pub enum Mode {
28 #[default]
30 Normal,
31 Insert,
33 Visual,
35 Command,
37 Search,
39 Custom(u8),
41}
42
43impl Mode {
44 pub fn name(&self) -> &'static str {
46 match self {
47 Mode::Normal => "NORMAL",
48 Mode::Insert => "INSERT",
49 Mode::Visual => "VISUAL",
50 Mode::Command => "COMMAND",
51 Mode::Search => "SEARCH",
52 Mode::Custom(_) => "CUSTOM",
53 }
54 }
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, Hash)]
59pub struct KeyChord {
60 pub keys: Vec<KeyBinding>,
62}
63
64impl KeyChord {
65 pub fn single(key: KeyBinding) -> Self {
67 Self { keys: vec![key] }
68 }
69
70 pub fn multi(keys: Vec<KeyBinding>) -> Self {
72 Self { keys }
73 }
74
75 pub fn parse(s: &str) -> Option<Self> {
77 let parts: Vec<&str> = s.split_whitespace().collect();
78 if parts.is_empty() {
79 return None;
80 }
81
82 let keys: Option<Vec<KeyBinding>> = parts.iter().map(|p| parse_key_binding(p)).collect();
83 keys.map(|k| Self { keys: k })
84 }
85}
86
87pub fn parse_key_binding(s: &str) -> Option<KeyBinding> {
89 let s = s.trim();
90 if s.is_empty() {
91 return None;
92 }
93
94 let mut ctrl = false;
95 let mut alt = false;
96 let mut shift = false;
97 let mut key_part = s;
98
99 loop {
101 let lower = key_part.to_lowercase();
102 if lower.starts_with("ctrl-") || lower.starts_with("c-") {
103 ctrl = true;
104 key_part = if lower.starts_with("ctrl-") {
105 &key_part[5..]
106 } else {
107 &key_part[2..]
108 };
109 } else if lower.starts_with("alt-") || lower.starts_with("m-") {
110 alt = true;
111 key_part = if lower.starts_with("alt-") {
112 &key_part[4..]
113 } else {
114 &key_part[2..]
115 };
116 } else if lower.starts_with("shift-") || lower.starts_with("s-") {
117 shift = true;
118 key_part = if lower.starts_with("shift-") {
119 &key_part[6..]
120 } else {
121 &key_part[2..]
122 };
123 } else {
124 break;
125 }
126 }
127
128 let key = parse_key(key_part)?;
129
130 Some(KeyBinding {
131 key,
132 ctrl,
133 alt,
134 shift,
135 })
136}
137
138fn parse_key(s: &str) -> Option<Key> {
140 let lower = s.to_lowercase();
141 match lower.as_str() {
142 "enter" | "return" | "cr" => Some(Key::Enter),
143 "escape" | "esc" => Some(Key::Escape),
144 "tab" => Some(Key::Tab),
145 "backtab" | "s-tab" => Some(Key::BackTab),
146 "backspace" | "bs" => Some(Key::Backspace),
147 "delete" | "del" => Some(Key::Delete),
148 "up" => Some(Key::Up),
149 "down" => Some(Key::Down),
150 "left" => Some(Key::Left),
151 "right" => Some(Key::Right),
152 "home" => Some(Key::Home),
153 "end" => Some(Key::End),
154 "pageup" | "pgup" => Some(Key::PageUp),
155 "pagedown" | "pgdn" => Some(Key::PageDown),
156 "insert" | "ins" => Some(Key::Insert),
157 "space" => Some(Key::Char(' ')),
158 "f1" => Some(Key::F(1)),
159 "f2" => Some(Key::F(2)),
160 "f3" => Some(Key::F(3)),
161 "f4" => Some(Key::F(4)),
162 "f5" => Some(Key::F(5)),
163 "f6" => Some(Key::F(6)),
164 "f7" => Some(Key::F(7)),
165 "f8" => Some(Key::F(8)),
166 "f9" => Some(Key::F(9)),
167 "f10" => Some(Key::F(10)),
168 "f11" => Some(Key::F(11)),
169 "f12" => Some(Key::F(12)),
170 _ => {
171 let chars: Vec<char> = s.chars().collect();
173 if chars.len() == 1 {
174 Some(Key::Char(chars[0]))
175 } else {
176 None
177 }
178 }
179 }
180}
181
182pub fn format_key_binding(binding: &KeyBinding) -> String {
184 let mut parts = Vec::new();
185
186 if binding.ctrl {
187 parts.push("Ctrl");
188 }
189 if binding.alt {
190 parts.push("Alt");
191 }
192 if binding.shift {
193 parts.push("Shift");
194 }
195
196 let key_str = match binding.key {
197 Key::Char(' ') => "Space".to_string(),
198 Key::Char(c) => c.to_string(),
199 Key::Enter => "Enter".to_string(),
200 Key::Escape => "Esc".to_string(),
201 Key::Tab => "Tab".to_string(),
202 Key::BackTab => "BackTab".to_string(),
203 Key::Backspace => "Backspace".to_string(),
204 Key::Delete => "Del".to_string(),
205 Key::Up => "↑".to_string(),
206 Key::Down => "↓".to_string(),
207 Key::Left => "←".to_string(),
208 Key::Right => "→".to_string(),
209 Key::Home => "Home".to_string(),
210 Key::End => "End".to_string(),
211 Key::PageUp => "PgUp".to_string(),
212 Key::PageDown => "PgDn".to_string(),
213 Key::Insert => "Ins".to_string(),
214 Key::F(n) => format!("F{}", n),
215 Key::Null => "Null".to_string(),
216 Key::Unknown => "Unknown".to_string(),
217 };
218
219 parts.push(&key_str);
220 parts.join("-")
221}
222
223#[derive(Clone, Debug)]
225pub struct KeymapConfig {
226 bindings: HashMap<Mode, HashMap<KeyChord, String>>,
228 current_mode: Mode,
230 pending: Vec<KeyBinding>,
232 chord_timeout: u64,
234 global_bindings: HashMap<KeyChord, String>,
236}
237
238impl KeymapConfig {
239 pub fn new() -> Self {
241 Self {
242 bindings: HashMap::new(),
243 current_mode: Mode::Normal,
244 pending: Vec::new(),
245 chord_timeout: 1000,
246 global_bindings: HashMap::new(),
247 }
248 }
249
250 pub fn set_mode(&mut self, mode: Mode) {
252 self.current_mode = mode;
253 self.pending.clear();
254 }
255
256 pub fn mode(&self) -> Mode {
258 self.current_mode
259 }
260
261 pub fn bind(&mut self, mode: Mode, keys: &str, action: impl Into<String>) {
263 if let Some(chord) = KeyChord::parse(keys) {
264 self.bindings
265 .entry(mode)
266 .or_default()
267 .insert(chord, action.into());
268 }
269 }
270
271 pub fn bind_global(&mut self, keys: &str, action: impl Into<String>) {
273 if let Some(chord) = KeyChord::parse(keys) {
274 self.global_bindings.insert(chord, action.into());
275 }
276 }
277
278 pub fn unbind(&mut self, mode: Mode, keys: &str) {
280 if let Some(chord) = KeyChord::parse(keys) {
281 if let Some(mode_bindings) = self.bindings.get_mut(&mode) {
282 mode_bindings.remove(&chord);
283 }
284 }
285 }
286
287 pub fn lookup(&mut self, key: KeyBinding) -> LookupResult {
289 self.pending.push(key);
290
291 let chord = KeyChord {
292 keys: self.pending.clone(),
293 };
294
295 if let Some(action) = self.global_bindings.get(&chord) {
297 self.pending.clear();
298 return LookupResult::Action(action.clone());
299 }
300
301 if let Some(mode_bindings) = self.bindings.get(&self.current_mode) {
303 if let Some(action) = mode_bindings.get(&chord) {
304 self.pending.clear();
305 return LookupResult::Action(action.clone());
306 }
307
308 for existing_chord in mode_bindings.keys() {
310 if existing_chord.keys.len() > self.pending.len()
311 && existing_chord.keys.starts_with(&self.pending)
312 {
313 return LookupResult::Pending;
314 }
315 }
316 }
317
318 self.pending.clear();
320 LookupResult::None
321 }
322
323 pub fn clear_pending(&mut self) {
325 self.pending.clear();
326 }
327
328 pub fn pending_keys(&self) -> &[KeyBinding] {
330 &self.pending
331 }
332
333 pub fn has_pending(&self) -> bool {
335 !self.pending.is_empty()
336 }
337
338 pub fn chord_timeout(&mut self, ms: u64) {
340 self.chord_timeout = ms;
341 }
342
343 pub fn bindings_for_mode(&self, mode: Mode) -> Vec<(&KeyChord, &str)> {
345 self.bindings
346 .get(&mode)
347 .map(|m| m.iter().map(|(k, v)| (k, v.as_str())).collect())
348 .unwrap_or_default()
349 }
350
351 pub fn global_bindings(&self) -> Vec<(&KeyChord, &str)> {
353 self.global_bindings
354 .iter()
355 .map(|(k, v)| (k, v.as_str()))
356 .collect()
357 }
358}
359
360impl Default for KeymapConfig {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366#[derive(Clone, Debug, PartialEq, Eq)]
368pub enum LookupResult {
369 None,
371 Action(String),
373 Pending,
375}
376
377pub fn vim_preset() -> KeymapConfig {
379 let mut config = KeymapConfig::new();
380
381 config.bind(Mode::Normal, "h", "move_left");
383 config.bind(Mode::Normal, "j", "move_down");
384 config.bind(Mode::Normal, "k", "move_up");
385 config.bind(Mode::Normal, "l", "move_right");
386 config.bind(Mode::Normal, "i", "enter_insert");
387 config.bind(Mode::Normal, "a", "append");
388 config.bind(Mode::Normal, "A", "append_end");
389 config.bind(Mode::Normal, "o", "open_below");
390 config.bind(Mode::Normal, "O", "open_above");
391 config.bind(Mode::Normal, "v", "enter_visual");
392 config.bind(Mode::Normal, ":", "enter_command");
393 config.bind(Mode::Normal, "/", "search_forward");
394 config.bind(Mode::Normal, "?", "search_backward");
395 config.bind(Mode::Normal, "n", "search_next");
396 config.bind(Mode::Normal, "N", "search_prev");
397 config.bind(Mode::Normal, "g g", "goto_first");
398 config.bind(Mode::Normal, "G", "goto_last");
399 config.bind(Mode::Normal, "Ctrl-u", "page_up");
400 config.bind(Mode::Normal, "Ctrl-d", "page_down");
401 config.bind(Mode::Normal, "d d", "delete_line");
402 config.bind(Mode::Normal, "y y", "yank_line");
403 config.bind(Mode::Normal, "p", "paste_after");
404 config.bind(Mode::Normal, "P", "paste_before");
405 config.bind(Mode::Normal, "u", "undo");
406 config.bind(Mode::Normal, "Ctrl-r", "redo");
407
408 config.bind(Mode::Insert, "Escape", "exit_insert");
410 config.bind(Mode::Insert, "Ctrl-c", "exit_insert");
411
412 config.bind(Mode::Visual, "Escape", "exit_visual");
414 config.bind(Mode::Visual, "h", "extend_left");
415 config.bind(Mode::Visual, "j", "extend_down");
416 config.bind(Mode::Visual, "k", "extend_up");
417 config.bind(Mode::Visual, "l", "extend_right");
418 config.bind(Mode::Visual, "y", "yank_selection");
419 config.bind(Mode::Visual, "d", "delete_selection");
420
421 config.bind(Mode::Command, "Escape", "exit_command");
423 config.bind(Mode::Command, "Enter", "execute_command");
424
425 config.bind_global("Ctrl-c", "quit");
427 config.bind_global("Ctrl-z", "suspend");
428
429 config
430}
431
432pub fn emacs_preset() -> KeymapConfig {
434 let mut config = KeymapConfig::new();
435
436 config.bind(Mode::Normal, "Ctrl-p", "move_up");
438 config.bind(Mode::Normal, "Ctrl-n", "move_down");
439 config.bind(Mode::Normal, "Ctrl-b", "move_left");
440 config.bind(Mode::Normal, "Ctrl-f", "move_right");
441 config.bind(Mode::Normal, "Ctrl-a", "line_start");
442 config.bind(Mode::Normal, "Ctrl-e", "line_end");
443 config.bind(Mode::Normal, "Alt-<", "goto_first");
444 config.bind(Mode::Normal, "Alt->", "goto_last");
445 config.bind(Mode::Normal, "Ctrl-v", "page_down");
446 config.bind(Mode::Normal, "Alt-v", "page_up");
447
448 config.bind(Mode::Normal, "Ctrl-d", "delete_char");
450 config.bind(Mode::Normal, "Ctrl-k", "kill_line");
451 config.bind(Mode::Normal, "Ctrl-y", "yank");
452 config.bind(Mode::Normal, "Ctrl-w", "cut_region");
453 config.bind(Mode::Normal, "Alt-w", "copy_region");
454
455 config.bind(Mode::Normal, "Ctrl-s", "search_forward");
457 config.bind(Mode::Normal, "Ctrl-r", "search_backward");
458
459 config.bind(Mode::Normal, "Ctrl-/", "undo");
461 config.bind(Mode::Normal, "Ctrl-x u", "undo");
462
463 config.bind(Mode::Normal, "Ctrl-x Ctrl-s", "save");
465 config.bind(Mode::Normal, "Ctrl-x Ctrl-c", "quit");
466 config.bind(Mode::Normal, "Ctrl-x Ctrl-f", "open_file");
467
468 config
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn test_parse_key_binding() {
477 let binding = parse_key_binding("j").unwrap();
478 assert_eq!(binding.key, Key::Char('j'));
479 assert!(!binding.ctrl);
480
481 let binding = parse_key_binding("Ctrl-c").unwrap();
482 assert_eq!(binding.key, Key::Char('c'));
483 assert!(binding.ctrl);
484
485 let binding = parse_key_binding("Ctrl-Alt-Delete").unwrap();
486 assert_eq!(binding.key, Key::Delete);
487 assert!(binding.ctrl);
488 assert!(binding.alt);
489 }
490
491 #[test]
492 fn test_format_key_binding() {
493 let binding = KeyBinding {
494 key: Key::Char('c'),
495 ctrl: true,
496 alt: false,
497 shift: false,
498 };
499 assert_eq!(format_key_binding(&binding), "Ctrl-c");
500
501 let binding = KeyBinding {
502 key: Key::Enter,
503 ctrl: false,
504 alt: false,
505 shift: false,
506 };
507 assert_eq!(format_key_binding(&binding), "Enter");
508 }
509
510 #[test]
511 fn test_key_chord_parse() {
512 let chord = KeyChord::parse("Ctrl-x Ctrl-s").unwrap();
513 assert_eq!(chord.keys.len(), 2);
514 assert!(chord.keys[0].ctrl);
515 assert!(chord.keys[1].ctrl);
516 }
517
518 #[test]
519 fn test_keymap_single_key() {
520 let mut keymap = KeymapConfig::new();
521 keymap.bind(Mode::Normal, "j", "move_down");
522
523 let binding = parse_key_binding("j").unwrap();
524 let result = keymap.lookup(binding);
525 assert_eq!(result, LookupResult::Action("move_down".to_string()));
526 }
527
528 #[test]
529 fn test_keymap_multi_key() {
530 let mut keymap = KeymapConfig::new();
531 keymap.bind(Mode::Normal, "g g", "goto_first");
532
533 let g1 = parse_key_binding("g").unwrap();
534 let result = keymap.lookup(g1.clone());
535 assert_eq!(result, LookupResult::Pending);
536
537 let result = keymap.lookup(g1);
538 assert_eq!(result, LookupResult::Action("goto_first".to_string()));
539 }
540
541 #[test]
542 fn test_keymap_no_match() {
543 let mut keymap = KeymapConfig::new();
544 keymap.bind(Mode::Normal, "j", "move_down");
545
546 let binding = parse_key_binding("x").unwrap();
547 let result = keymap.lookup(binding);
548 assert_eq!(result, LookupResult::None);
549 }
550
551 #[test]
552 fn test_keymap_modes() {
553 let mut keymap = KeymapConfig::new();
554 keymap.bind(Mode::Normal, "i", "enter_insert");
555 keymap.bind(Mode::Insert, "Escape", "exit_insert");
556
557 keymap.set_mode(Mode::Normal);
558 let i = parse_key_binding("i").unwrap();
559 let result = keymap.lookup(i);
560 assert_eq!(result, LookupResult::Action("enter_insert".to_string()));
561
562 keymap.set_mode(Mode::Insert);
563 let esc = parse_key_binding("Escape").unwrap();
564 let result = keymap.lookup(esc);
565 assert_eq!(result, LookupResult::Action("exit_insert".to_string()));
566 }
567
568 #[test]
569 fn test_vim_preset() {
570 let mut keymap = vim_preset();
571
572 let j = parse_key_binding("j").unwrap();
573 let result = keymap.lookup(j);
574 assert_eq!(result, LookupResult::Action("move_down".to_string()));
575 }
576
577 #[test]
578 fn test_emacs_preset() {
579 let mut keymap = emacs_preset();
580
581 let ctrl_n = parse_key_binding("Ctrl-n").unwrap();
582 let result = keymap.lookup(ctrl_n);
583 assert_eq!(result, LookupResult::Action("move_down".to_string()));
584 }
585
586 #[test]
587 fn test_global_bindings() {
588 let mut keymap = KeymapConfig::new();
589 keymap.bind_global("Ctrl-c", "quit");
590
591 keymap.set_mode(Mode::Normal);
592 let ctrl_c = parse_key_binding("Ctrl-c").unwrap();
593 let result = keymap.lookup(ctrl_c.clone());
594 assert_eq!(result, LookupResult::Action("quit".to_string()));
595
596 keymap.set_mode(Mode::Insert);
597 let result = keymap.lookup(ctrl_c);
598 assert_eq!(result, LookupResult::Action("quit".to_string()));
599 }
600}