1use std::collections::HashMap;
7
8use crossterm::event::{KeyCode, KeyModifiers};
9
10pub type KeybindingId = &'static str;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct KeyCombo {
16 pub code: KeyCode,
17 pub modifiers: KeyModifiers,
18}
19
20impl KeyCombo {
21 pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
23 Self { code, modifiers }
24 }
25
26 pub fn from_code(code: KeyCode) -> Self {
28 Self {
29 code,
30 modifiers: KeyModifiers::NONE,
31 }
32 }
33
34 pub fn ctrl(self) -> Self {
36 Self {
37 modifiers: self.modifiers | KeyModifiers::CONTROL,
38 ..self
39 }
40 }
41
42 pub fn shift(self) -> Self {
44 Self {
45 modifiers: self.modifiers | KeyModifiers::SHIFT,
46 ..self
47 }
48 }
49
50 pub fn alt(self) -> Self {
52 Self {
53 modifiers: self.modifiers | KeyModifiers::ALT,
54 ..self
55 }
56 }
57
58 pub fn super_key(self) -> Self {
60 Self {
61 modifiers: self.modifiers | KeyModifiers::SUPER,
62 ..self
63 }
64 }
65
66 pub fn display(&self) -> String {
70 let mut parts: Vec<String> = Vec::new();
71 if self.modifiers.contains(KeyModifiers::CONTROL) {
72 parts.push("Ctrl".into());
73 }
74 if self.modifiers.contains(KeyModifiers::ALT) {
75 parts.push("Alt".into());
76 }
77 if self.modifiers.contains(KeyModifiers::SHIFT) && self.code != KeyCode::BackTab {
79 parts.push("Shift".into());
80 }
81 if self.modifiers.contains(KeyModifiers::SUPER) {
82 parts.push("Super".into());
83 }
84 let key = match self.code {
85 KeyCode::Char(c) => c.to_uppercase().to_string(),
86 KeyCode::Enter => "Enter".into(),
87 KeyCode::Esc => "Esc".into(),
88 KeyCode::Tab | KeyCode::BackTab => "Tab".into(),
89 KeyCode::Backspace => "Backspace".into(),
90 KeyCode::Delete => "Delete".into(),
91 KeyCode::Insert => "Insert".into(),
92 KeyCode::Up => "Up".into(),
93 KeyCode::Down => "Down".into(),
94 KeyCode::Left => "Left".into(),
95 KeyCode::Right => "Right".into(),
96 KeyCode::Home => "Home".into(),
97 KeyCode::End => "End".into(),
98 KeyCode::PageUp => "PageUp".into(),
99 KeyCode::PageDown => "PageDown".into(),
100 other => format!("{other:?}"),
101 };
102 parts.push(key);
103 parts.join("+")
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct KeybindingDefinition {
110 pub default_keys: Vec<KeyCombo>,
111 pub description: Option<&'static str>,
112}
113
114#[derive(Debug, Clone)]
116pub struct KeybindingConflict {
117 pub key: KeyCombo,
118 pub keybindings: Vec<KeybindingId>,
119}
120
121#[derive(Clone)]
123pub struct Keybindings {
124 definitions: HashMap<KeybindingId, KeybindingDefinition>,
125 user_bindings: HashMap<KeybindingId, Vec<KeyCombo>>,
126 keys_by_id: HashMap<KeybindingId, Vec<KeyCombo>>,
127 conflicts: Vec<KeybindingConflict>,
128}
129
130impl Keybindings {
131 pub fn new() -> Self {
133 let definitions = Self::default_definitions();
134 let keys_by_id = definitions
135 .iter()
136 .map(|(id, def)| (*id, def.default_keys.clone()))
137 .collect();
138 Self {
139 definitions,
140 user_bindings: HashMap::new(),
141 keys_by_id,
142 conflicts: Vec::new(),
143 }
144 }
145
146 fn default_definitions() -> HashMap<KeybindingId, KeybindingDefinition> {
148 use KeyCode::*;
149 use KeyModifiers as M;
150
151 let mut map = HashMap::new();
152
153 map.insert(
156 "app.interrupt",
157 KeybindingDefinition {
158 default_keys: vec![KeyCombo::new(Esc, M::NONE)],
159 description: Some("Cancel or abort"),
160 },
161 );
162 map.insert(
163 "app.clear",
164 KeybindingDefinition {
165 default_keys: vec![KeyCombo::new(Char('c'), M::CONTROL)],
166 description: Some("Clear editor"),
167 },
168 );
169 map.insert(
170 "app.exit",
171 KeybindingDefinition {
172 default_keys: vec![KeyCombo::new(Char('d'), M::CONTROL)],
173 description: Some("Exit when editor is empty"),
174 },
175 );
176 map.insert(
177 "app.model.select",
178 KeybindingDefinition {
179 default_keys: vec![KeyCombo::new(Char('l'), M::CONTROL)],
180 description: Some("Open model selector"),
181 },
182 );
183 map.insert(
184 "app.model.cycleForward",
185 KeybindingDefinition {
186 default_keys: vec![KeyCombo::new(Char('p'), M::CONTROL)],
187 description: Some("Cycle to next model"),
188 },
189 );
190 map.insert(
191 "app.model.cycleBackward",
192 KeybindingDefinition {
193 default_keys: vec![if cfg!(windows) {
195 KeyCombo::new(Char('p'), M::ALT)
196 } else {
197 KeyCombo::new(Char('p'), M::CONTROL | M::SHIFT)
198 }],
199 description: Some("Cycle to previous model"),
200 },
201 );
202 map.insert(
203 "app.tools.expand",
204 KeybindingDefinition {
205 default_keys: vec![KeyCombo::new(Char('o'), M::CONTROL)],
206 description: Some("Toggle tool output"),
207 },
208 );
209 map.insert(
210 "app.thinking.toggle",
211 KeybindingDefinition {
212 default_keys: vec![KeyCombo::new(Char('t'), M::CONTROL)],
213 description: Some("Toggle thinking blocks"),
214 },
215 );
216 map.insert(
217 "app.editor.external",
218 KeybindingDefinition {
219 default_keys: vec![KeyCombo::new(Char('g'), M::CONTROL)],
220 description: Some("Open external editor"),
221 },
222 );
223 map.insert(
224 "app.thinking.cycle",
225 KeybindingDefinition {
226 default_keys: vec![KeyCombo::new(Tab, M::SHIFT)],
227 description: Some("Cycle thinking level"),
228 },
229 );
230 map.insert(
231 "app.clipboard.pasteImage",
232 KeybindingDefinition {
233 default_keys: if cfg!(windows) {
238 vec![
239 KeyCombo::new(Char('v'), M::CONTROL),
240 KeyCombo::new(Char('v'), M::ALT),
241 ]
242 } else {
243 vec![KeyCombo::new(Char('v'), M::CONTROL)]
244 },
245 description: Some("Paste image from clipboard"),
246 },
247 );
248 map.insert(
249 "app.suspend",
250 KeybindingDefinition {
251 default_keys: if cfg!(windows) {
253 vec![]
254 } else {
255 vec![KeyCombo::new(Char('z'), M::CONTROL)]
256 },
257 description: Some("Suspend to background"),
258 },
259 );
260 map.insert(
261 "app.message.followUp",
262 KeybindingDefinition {
263 default_keys: if cfg!(windows) {
267 vec![
268 KeyCombo::new(Char('q'), M::CONTROL),
269 KeyCombo::new(Enter, M::ALT),
270 ]
271 } else {
272 vec![KeyCombo::new(Enter, M::ALT)]
273 },
274 description: Some("Queue follow-up message"),
275 },
276 );
277 map.insert(
278 "app.message.dequeue",
279 KeybindingDefinition {
280 default_keys: vec![if cfg!(windows) {
282 KeyCombo::new(Char('q'), M::ALT)
283 } else {
284 KeyCombo::new(Up, M::ALT)
285 }],
286 description: Some("Restore queued messages"),
287 },
288 );
289 map.insert(
290 "app.message.copy",
291 KeybindingDefinition {
292 default_keys: vec![KeyCombo::new(Char('x'), M::CONTROL)],
293 description: Some("Copy selection or last assistant message"),
294 },
295 );
296
297 map.insert(
299 "tui.editor.cursorUp",
300 KeybindingDefinition {
301 default_keys: vec![KeyCombo::new(Up, M::NONE)],
302 description: Some("Move cursor up"),
303 },
304 );
305 map.insert(
306 "tui.editor.cursorDown",
307 KeybindingDefinition {
308 default_keys: vec![KeyCombo::new(Down, M::NONE)],
309 description: Some("Move cursor down"),
310 },
311 );
312 map.insert(
313 "tui.editor.historyPrevious",
314 KeybindingDefinition {
315 default_keys: vec![],
316 description: Some("Select previous prompt history entry"),
317 },
318 );
319 map.insert(
320 "tui.editor.historyNext",
321 KeybindingDefinition {
322 default_keys: vec![],
323 description: Some("Select next prompt history entry"),
324 },
325 );
326 map.insert(
327 "tui.editor.cursorLeft",
328 KeybindingDefinition {
329 default_keys: vec![
330 KeyCombo::new(Left, M::NONE),
331 KeyCombo::new(Char('b'), M::CONTROL),
332 ],
333 description: Some("Move cursor left"),
334 },
335 );
336 map.insert(
337 "tui.editor.cursorRight",
338 KeybindingDefinition {
339 default_keys: vec![
340 KeyCombo::new(Right, M::NONE),
341 KeyCombo::new(Char('f'), M::CONTROL),
342 ],
343 description: Some("Move cursor right"),
344 },
345 );
346 map.insert(
347 "tui.editor.cursorWordLeft",
348 KeybindingDefinition {
349 default_keys: vec![
350 KeyCombo::new(Left, M::ALT),
351 KeyCombo::new(Left, M::CONTROL),
352 KeyCombo::new(Char('b'), M::ALT),
353 ],
354 description: Some("Move cursor word left"),
355 },
356 );
357 map.insert(
358 "tui.editor.cursorWordRight",
359 KeybindingDefinition {
360 default_keys: vec![
361 KeyCombo::new(Right, M::ALT),
362 KeyCombo::new(Right, M::CONTROL),
363 KeyCombo::new(Char('f'), M::ALT),
364 ],
365 description: Some("Move cursor word right"),
366 },
367 );
368 map.insert(
369 "tui.editor.cursorLineStart",
370 KeybindingDefinition {
371 default_keys: vec![
372 KeyCombo::new(Home, M::NONE),
373 KeyCombo::new(Home, M::CONTROL),
374 KeyCombo::new(Char('a'), M::CONTROL),
375 ],
376 description: Some("Move to line start"),
377 },
378 );
379 map.insert(
380 "tui.editor.cursorLineEnd",
381 KeybindingDefinition {
382 default_keys: vec![
383 KeyCombo::new(End, M::NONE),
384 KeyCombo::new(End, M::CONTROL),
385 KeyCombo::new(Char('e'), M::CONTROL),
386 ],
387 description: Some("Move to line end"),
388 },
389 );
390 map.insert(
391 "tui.editor.jumpForward",
392 KeybindingDefinition {
393 default_keys: vec![KeyCombo::new(Char(']'), M::CONTROL)],
394 description: Some("Jump forward to character"),
395 },
396 );
397 map.insert(
398 "tui.editor.jumpBackward",
399 KeybindingDefinition {
400 default_keys: vec![KeyCombo::new(Char(']'), M::CONTROL | M::ALT)],
401 description: Some("Jump backward to character"),
402 },
403 );
404 map.insert(
405 "tui.editor.pageUp",
406 KeybindingDefinition {
407 default_keys: vec![
408 KeyCombo::new(PageUp, M::NONE),
409 KeyCombo::new(PageUp, M::CONTROL),
410 ],
411 description: Some("Page up"),
412 },
413 );
414 map.insert(
415 "tui.editor.pageDown",
416 KeybindingDefinition {
417 default_keys: vec![
418 KeyCombo::new(PageDown, M::NONE),
419 KeyCombo::new(PageDown, M::CONTROL),
420 ],
421 description: Some("Page down"),
422 },
423 );
424 map.insert(
425 "tui.editor.deleteCharBackward",
426 KeybindingDefinition {
427 default_keys: vec![KeyCombo::new(Backspace, M::NONE)],
428 description: Some("Delete character backward"),
429 },
430 );
431 map.insert(
432 "tui.editor.deleteCharForward",
433 KeybindingDefinition {
434 default_keys: vec![
435 KeyCombo::new(Delete, M::NONE),
436 KeyCombo::new(Char('d'), M::CONTROL),
437 ],
438 description: Some("Delete character forward"),
439 },
440 );
441 map.insert(
442 "tui.editor.deleteWordBackward",
443 KeybindingDefinition {
444 default_keys: vec![
445 KeyCombo::new(Char('w'), M::CONTROL),
446 KeyCombo::new(Backspace, M::ALT),
447 ],
448 description: Some("Delete word backward"),
449 },
450 );
451 map.insert(
452 "tui.editor.deleteWordForward",
453 KeybindingDefinition {
454 default_keys: vec![
455 KeyCombo::new(Char('d'), M::ALT),
456 KeyCombo::new(Delete, M::ALT),
457 ],
458 description: Some("Delete word forward"),
459 },
460 );
461 map.insert(
462 "tui.editor.deleteToLineStart",
463 KeybindingDefinition {
464 default_keys: vec![KeyCombo::new(Char('u'), M::CONTROL)],
465 description: Some("Delete to line start"),
466 },
467 );
468 map.insert(
469 "tui.editor.deleteToLineEnd",
470 KeybindingDefinition {
471 default_keys: vec![KeyCombo::new(Char('k'), M::CONTROL)],
472 description: Some("Delete to line end"),
473 },
474 );
475 map.insert(
476 "tui.editor.yank",
477 KeybindingDefinition {
478 default_keys: vec![KeyCombo::new(Char('y'), M::CONTROL)],
479 description: Some("Yank"),
480 },
481 );
482 map.insert(
483 "tui.editor.yankPop",
484 KeybindingDefinition {
485 default_keys: vec![KeyCombo::new(Char('y'), M::ALT)],
486 description: Some("Yank pop"),
487 },
488 );
489 map.insert(
490 "tui.editor.undo",
491 KeybindingDefinition {
492 default_keys: vec![KeyCombo::new(Char('-'), M::CONTROL)],
493 description: Some("Undo"),
494 },
495 );
496
497 map.insert(
499 "tui.input.newLine",
500 KeybindingDefinition {
501 default_keys: vec![
502 KeyCombo::new(Enter, M::SHIFT),
503 KeyCombo::new(Char('j'), M::CONTROL),
504 ],
505 description: Some("Insert newline"),
506 },
507 );
508 map.insert(
509 "tui.input.submit",
510 KeybindingDefinition {
511 default_keys: vec![KeyCombo::new(Enter, M::NONE)],
512 description: Some("Submit input"),
513 },
514 );
515 map.insert(
516 "tui.input.tab",
517 KeybindingDefinition {
518 default_keys: vec![KeyCombo::new(Tab, M::NONE)],
519 description: Some("Tab / autocomplete"),
520 },
521 );
522 map.insert(
523 "tui.input.copy",
524 KeybindingDefinition {
525 default_keys: vec![KeyCombo::new(Char('c'), M::CONTROL)],
526 description: Some("Copy selection"),
527 },
528 );
529
530 map.insert(
532 "tui.select.up",
533 KeybindingDefinition {
534 default_keys: vec![KeyCombo::new(Up, M::NONE)],
535 description: Some("Move selection up"),
536 },
537 );
538 map.insert(
539 "tui.select.down",
540 KeybindingDefinition {
541 default_keys: vec![KeyCombo::new(Down, M::NONE)],
542 description: Some("Move selection down"),
543 },
544 );
545 map.insert(
546 "tui.select.pageUp",
547 KeybindingDefinition {
548 default_keys: vec![KeyCombo::new(PageUp, M::NONE)],
549 description: Some("Selection page up"),
550 },
551 );
552 map.insert(
553 "tui.select.pageDown",
554 KeybindingDefinition {
555 default_keys: vec![KeyCombo::new(PageDown, M::NONE)],
556 description: Some("Selection page down"),
557 },
558 );
559 map.insert(
560 "tui.select.confirm",
561 KeybindingDefinition {
562 default_keys: vec![KeyCombo::new(Enter, M::NONE)],
563 description: Some("Confirm selection"),
564 },
565 );
566 map.insert(
567 "tui.select.cancel",
568 KeybindingDefinition {
569 default_keys: vec![
570 KeyCombo::new(Esc, M::NONE),
571 KeyCombo::new(Char('c'), M::CONTROL),
572 ],
573 description: Some("Cancel selection"),
574 },
575 );
576
577 map.insert(
579 "tui.altScreen.pageUp",
580 KeybindingDefinition {
581 default_keys: vec![KeyCombo::new(PageUp, M::NONE)],
582 description: Some("Scroll viewport up one page"),
583 },
584 );
585 map.insert(
586 "tui.altScreen.pageDown",
587 KeybindingDefinition {
588 default_keys: vec![KeyCombo::new(PageDown, M::NONE)],
589 description: Some("Scroll viewport down one page"),
590 },
591 );
592 map.insert(
593 "tui.altScreen.halfPageUp",
594 KeybindingDefinition {
595 default_keys: vec![],
596 description: Some("Scroll viewport up half a page"),
597 },
598 );
599 map.insert(
600 "tui.altScreen.halfPageDown",
601 KeybindingDefinition {
602 default_keys: vec![],
603 description: Some("Scroll viewport down half a page"),
604 },
605 );
606 map.insert(
607 "tui.altScreen.lineUp",
608 KeybindingDefinition {
609 default_keys: vec![],
610 description: Some("Scroll viewport up one line"),
611 },
612 );
613 map.insert(
614 "tui.altScreen.lineDown",
615 KeybindingDefinition {
616 default_keys: vec![],
617 description: Some("Scroll viewport down one line"),
618 },
619 );
620 map.insert(
621 "tui.altScreen.previousPrompt",
622 KeybindingDefinition {
623 default_keys: vec![KeyCombo::new(Up, M::CONTROL | M::SHIFT)],
624 description: Some("Jump to previous semantic prompt"),
625 },
626 );
627 map.insert(
628 "tui.altScreen.nextPrompt",
629 KeybindingDefinition {
630 default_keys: vec![KeyCombo::new(Down, M::CONTROL | M::SHIFT)],
631 description: Some("Jump to next semantic prompt"),
632 },
633 );
634 map.insert(
635 "tui.altScreen.search",
636 KeybindingDefinition {
637 default_keys: vec![KeyCombo::new(Char('f'), M::CONTROL | M::SHIFT)],
638 description: Some("Search the primary scroll view"),
639 },
640 );
641 map.insert(
642 "tui.altScreen.searchNext",
643 KeybindingDefinition {
644 default_keys: vec![
645 KeyCombo::new(Enter, M::NONE),
646 KeyCombo::new(Char('g'), M::CONTROL),
647 ],
648 description: Some("Select the next search match"),
649 },
650 );
651 map.insert(
652 "tui.altScreen.searchPrevious",
653 KeybindingDefinition {
654 default_keys: vec![
655 KeyCombo::new(Enter, M::SHIFT),
656 KeyCombo::new(Char('g'), M::CONTROL | M::SHIFT),
657 ],
658 description: Some("Select the previous search match"),
659 },
660 );
661 map.insert(
662 "tui.altScreen.searchClose",
663 KeybindingDefinition {
664 default_keys: vec![KeyCombo::new(Esc, M::NONE)],
665 description: Some("Close transcript search"),
666 },
667 );
668 map.insert(
669 "tui.altScreen.top",
670 KeybindingDefinition {
671 default_keys: vec![KeyCombo::new(Home, M::NONE)],
672 description: Some("Scroll viewport to top"),
673 },
674 );
675 map.insert(
676 "tui.altScreen.bottom",
677 KeybindingDefinition {
678 default_keys: vec![KeyCombo::new(End, M::NONE)],
679 description: Some("Scroll viewport to bottom"),
680 },
681 );
682
683 map
684 }
685
686 pub fn matches(&self, event: &crossterm::event::KeyEvent, binding_id: KeybindingId) -> bool {
688 if let Some(combos) = self.keys_by_id.get(binding_id) {
689 let combo = KeyCombo::new(event.code, event.modifiers);
690 return combos.contains(&combo);
691 }
692 false
693 }
694
695 pub fn get_binding(&self, event: &crossterm::event::KeyEvent) -> Option<KeybindingId> {
697 let combo = KeyCombo::new(event.code, event.modifiers);
698 for (id, combos) in &self.keys_by_id {
699 if combos.contains(&combo) {
700 return Some(*id);
701 }
702 }
703 None
704 }
705
706 pub fn get_keys(&self, binding_id: KeybindingId) -> Vec<KeyCombo> {
708 self.keys_by_id.get(binding_id).cloned().unwrap_or_default()
709 }
710
711 pub fn get_definition(&self, binding_id: KeybindingId) -> Option<&KeybindingDefinition> {
713 self.definitions.get(binding_id)
714 }
715
716 pub fn get_conflicts(&self) -> &[KeybindingConflict] {
718 &self.conflicts
719 }
720
721 pub fn set_user_bindings(&mut self, user_bindings: HashMap<KeybindingId, Vec<KeyCombo>>) {
723 self.user_bindings = user_bindings;
724 self.rebuild();
725 }
726
727 fn rebuild(&mut self) {
729 self.keys_by_id.clear();
730 self.conflicts.clear();
731
732 let mut key_claims: HashMap<KeyCombo, Vec<KeybindingId>> = HashMap::new();
734
735 for (id, def) in &self.definitions {
736 let keys = if let Some(user_keys) = self.user_bindings.get(id) {
737 user_keys.clone()
738 } else {
739 def.default_keys.clone()
740 };
741
742 for key in &keys {
744 key_claims.entry(*key).or_default().push(*id);
745 }
746
747 self.keys_by_id.insert(*id, keys);
748 }
749
750 for (key, claimants) in key_claims {
752 if claimants.len() > 1 {
753 self.conflicts.push(KeybindingConflict {
754 key,
755 keybindings: claimants,
756 });
757 }
758 }
759 }
760
761 pub fn set(&mut self, binding_id: KeybindingId, combos: Vec<KeyCombo>) {
763 self.user_bindings.insert(binding_id, combos);
764 self.rebuild();
765 }
766}
767
768impl Default for Keybindings {
769 fn default() -> Self {
770 Self::new()
771 }
772}
773
774static GLOBAL_KEYBINDINGS: std::sync::OnceLock<std::sync::Mutex<Keybindings>> =
776 std::sync::OnceLock::new();
777
778pub fn set_keybindings(keybindings: Keybindings) {
780 let guard = GLOBAL_KEYBINDINGS.get_or_init(|| std::sync::Mutex::new(Keybindings::new()));
781 if let Ok(mut kb) = guard.lock() {
782 *kb = keybindings;
783 }
784}
785
786pub fn get_keybindings() -> std::sync::MutexGuard<'static, Keybindings> {
788 let guard = GLOBAL_KEYBINDINGS.get_or_init(|| std::sync::Mutex::new(Keybindings::new()));
789 guard.lock().expect("Failed to lock global keybindings")
790}
791
792pub mod keys {
794 use super::KeybindingId;
795
796 pub const INTERRUPT: KeybindingId = "app.interrupt";
797 pub const CLEAR: KeybindingId = "app.clear";
798 pub const EXIT: KeybindingId = "app.exit";
799 pub const MODEL_SELECT: KeybindingId = "app.model.select";
800 pub const MODEL_CYCLE_FORWARD: KeybindingId = "app.model.cycleForward";
801 pub const MODEL_CYCLE_BACKWARD: KeybindingId = "app.model.cycleBackward";
802 pub const TOOLS_EXPAND: KeybindingId = "app.tools.expand";
803 pub const THINKING_TOGGLE: KeybindingId = "app.thinking.toggle";
804 pub const EXTERNAL_EDITOR: KeybindingId = "app.editor.external";
805 pub const THINKING_CYCLE: KeybindingId = "app.thinking.cycle";
806 pub const PASTE_IMAGE: KeybindingId = "app.clipboard.pasteImage";
807 pub const SUSPEND: KeybindingId = "app.suspend";
808 pub const MESSAGE_FOLLOW_UP: KeybindingId = "app.message.followUp";
809 pub const DEQUEUE: KeybindingId = "app.message.dequeue";
810 pub const MESSAGE_COPY: KeybindingId = "app.message.copy";
811
812 pub const CURSOR_UP: KeybindingId = "tui.editor.cursorUp";
814 pub const CURSOR_DOWN: KeybindingId = "tui.editor.cursorDown";
815 pub const CURSOR_LEFT: KeybindingId = "tui.editor.cursorLeft";
816 pub const CURSOR_RIGHT: KeybindingId = "tui.editor.cursorRight";
817 pub const CURSOR_WORD_LEFT: KeybindingId = "tui.editor.cursorWordLeft";
818 pub const CURSOR_WORD_RIGHT: KeybindingId = "tui.editor.cursorWordRight";
819 pub const CURSOR_LINE_START: KeybindingId = "tui.editor.cursorLineStart";
820 pub const CURSOR_LINE_END: KeybindingId = "tui.editor.cursorLineEnd";
821 pub const DELETE_CHAR_BACKWARD: KeybindingId = "tui.editor.deleteCharBackward";
822 pub const DELETE_CHAR_FORWARD: KeybindingId = "tui.editor.deleteCharForward";
823 pub const DELETE_WORD_BACKWARD: KeybindingId = "tui.editor.deleteWordBackward";
824 pub const DELETE_WORD_FORWARD: KeybindingId = "tui.editor.deleteWordForward";
825 pub const DELETE_TO_LINE_START: KeybindingId = "tui.editor.deleteToLineStart";
826 pub const DELETE_TO_LINE_END: KeybindingId = "tui.editor.deleteToLineEnd";
827 pub const YANK: KeybindingId = "tui.editor.yank";
828 pub const YANK_POP: KeybindingId = "tui.editor.yankPop";
829 pub const UNDO: KeybindingId = "tui.editor.undo";
830
831 pub const INPUT_SUBMIT: KeybindingId = "tui.input.submit";
833 pub const INPUT_NEW_LINE: KeybindingId = "tui.input.newLine";
834 pub const INPUT_TAB: KeybindingId = "tui.input.tab";
835 pub const INPUT_COPY: KeybindingId = "tui.input.copy";
836
837 pub const SELECT_UP: KeybindingId = "tui.select.up";
839 pub const SELECT_DOWN: KeybindingId = "tui.select.down";
840 pub const SELECT_PAGE_UP: KeybindingId = "tui.select.pageUp";
841 pub const SELECT_PAGE_DOWN: KeybindingId = "tui.select.pageDown";
842 pub const SELECT_CONFIRM: KeybindingId = "tui.select.confirm";
843 pub const SELECT_CANCEL: KeybindingId = "tui.select.cancel";
844
845 pub const PAGE_UP: KeybindingId = "tui.altScreen.pageUp";
847 pub const PAGE_DOWN: KeybindingId = "tui.altScreen.pageDown";
848 pub const HALF_PAGE_UP: KeybindingId = "tui.altScreen.halfPageUp";
849 pub const HALF_PAGE_DOWN: KeybindingId = "tui.altScreen.halfPageDown";
850 pub const LINE_UP: KeybindingId = "tui.altScreen.lineUp";
851 pub const LINE_DOWN: KeybindingId = "tui.altScreen.lineDown";
852 pub const TOP: KeybindingId = "tui.altScreen.top";
853 pub const BOTTOM: KeybindingId = "tui.altScreen.bottom";
854 pub const SEARCH: KeybindingId = "tui.altScreen.search";
855 pub const SEARCH_NEXT: KeybindingId = "tui.altScreen.searchNext";
856 pub const SEARCH_PREVIOUS: KeybindingId = "tui.altScreen.searchPrevious";
857 pub const SEARCH_CLOSE: KeybindingId = "tui.altScreen.searchClose";
858 pub const PREVIOUS_PROMPT: KeybindingId = "tui.altScreen.previousPrompt";
859 pub const NEXT_PROMPT: KeybindingId = "tui.altScreen.nextPrompt";
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn test_keybindings_new() {
868 let kb = Keybindings::new();
869 assert!(kb.definitions.len() > 0);
870 }
871
872 #[test]
873 fn test_matches() {
874 let kb = Keybindings::new();
875 let event = crossterm::event::KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
876 assert!(kb.matches(&event, keys::INPUT_SUBMIT));
877 }
878
879 #[test]
880 fn test_get_keys() {
881 let kb = Keybindings::new();
882 let keys = kb.get_keys(keys::CURSOR_LEFT);
883 assert!(!keys.is_empty());
884 }
885
886 #[test]
887 fn key_combo_display_formats_hints() {
888 assert_eq!(
889 KeyCombo::new(KeyCode::Char('q'), KeyModifiers::ALT).display(),
890 "Alt+Q"
891 );
892 assert_eq!(
893 KeyCombo::new(
894 KeyCode::Char('f'),
895 KeyModifiers::CONTROL | KeyModifiers::SHIFT
896 )
897 .display(),
898 "Ctrl+Shift+F"
899 );
900 assert_eq!(
901 KeyCombo::new(KeyCode::Enter, KeyModifiers::NONE).display(),
902 "Enter"
903 );
904 assert_eq!(
905 KeyCombo::new(KeyCode::Up, KeyModifiers::ALT).display(),
906 "Alt+Up"
907 );
908 }
909
910 #[test]
911 fn dequeue_binding_has_a_platform_default() {
912 let kb = Keybindings::new();
913 let keys = kb.get_keys(keys::DEQUEUE);
914 assert_eq!(keys.len(), 1);
915 if cfg!(windows) {
916 assert_eq!(keys[0].display(), "Alt+Q");
917 } else {
918 assert_eq!(keys[0].display(), "Alt+Up");
919 }
920 }
921
922 #[test]
923 fn dequeue_matches_its_platform_default_event() {
924 let kb = Keybindings::new();
928 let event = if cfg!(windows) {
929 crossterm::event::KeyEvent::new(KeyCode::Char('q'), KeyModifiers::ALT)
930 } else {
931 crossterm::event::KeyEvent::new(KeyCode::Up, KeyModifiers::ALT)
932 };
933 assert!(kb.matches(&event, keys::DEQUEUE));
934 }
935}