1use crate::{AppKey, AppKeyCode, AppModifiers, EditorAction, Mode, MoveDir};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum KeyTrigger {
5 Key(AppKey),
6 AnyChar(AppModifiers),
7 HomeRowChar(AppModifiers),
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ActionSpec {
12 Fixed(EditorAction),
13 InsertMatchedChar,
14 FillWithMatchedChar,
15 ActivateSwatchFromChar,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum BindingContext {
20 Always,
21 WhenSelecting,
22 WhenNotSelecting,
23 WhenFloating,
24 WhenNotFloating,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub struct EditorContext {
29 pub mode: Mode,
30 pub has_selection_anchor: bool,
31 pub is_floating: bool,
32}
33
34#[derive(Debug, Clone, Copy)]
35pub struct KeyBinding {
36 pub trigger: KeyTrigger,
37 pub action: ActionSpec,
38 pub context: BindingContext,
39 pub description: &'static str,
40 help: Option<BindingHelp>,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub enum HelpSection {
45 Drawing,
46 Selection,
47 Clipboard,
48 Transform,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct HelpEntry {
53 pub section: HelpSection,
54 pub keys: &'static str,
55 pub description: &'static str,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59struct BindingHelp {
60 section: HelpSection,
61 keys: &'static str,
62 description: &'static str,
63 order: u16,
64}
65
66#[derive(Debug, Clone)]
67pub struct KeyMap {
68 bindings: Vec<KeyBinding>,
69}
70
71impl KeyMap {
72 pub fn new(bindings: Vec<KeyBinding>) -> Self {
73 Self { bindings }
74 }
75
76 pub fn default_standalone() -> Self {
77 Self::new(default_standalone_bindings())
78 }
79
80 pub fn bindings(&self) -> &[KeyBinding] {
81 &self.bindings
82 }
83
84 pub fn help_entries(&self) -> Vec<HelpEntry> {
85 let mut rows: Vec<BindingHelp> = self.bindings.iter().filter_map(|b| b.help).collect();
86 rows.sort_by_key(|row| (row.section, row.order, row.keys, row.description));
87 rows.dedup();
88 rows.into_iter()
89 .map(|row| HelpEntry {
90 section: row.section,
91 keys: row.keys,
92 description: row.description,
93 })
94 .collect()
95 }
96
97 pub fn resolve(&self, key: AppKey, ctx: EditorContext) -> Option<EditorAction> {
98 for binding in &self.bindings {
99 if !context_matches(binding.context, ctx) {
100 continue;
101 }
102 if let Some(action) = resolve_binding(binding, key) {
103 return Some(action);
104 }
105 }
106 None
107 }
108}
109
110fn context_matches(binding_ctx: BindingContext, ctx: EditorContext) -> bool {
111 let selecting = ctx.mode.is_selecting() && ctx.has_selection_anchor;
112 match binding_ctx {
113 BindingContext::Always => true,
114 BindingContext::WhenSelecting => selecting,
115 BindingContext::WhenNotSelecting => !selecting,
116 BindingContext::WhenFloating => ctx.is_floating,
117 BindingContext::WhenNotFloating => !ctx.is_floating,
118 }
119}
120
121fn resolve_binding(binding: &KeyBinding, key: AppKey) -> Option<EditorAction> {
122 match binding.trigger {
123 KeyTrigger::Key(expected) => {
124 if expected == key {
125 build_action(binding.action, key)
126 } else {
127 None
128 }
129 }
130 KeyTrigger::AnyChar(mods) => match key.code {
131 AppKeyCode::Char(_) if key.modifiers == mods => build_action(binding.action, key),
132 _ => None,
133 },
134 KeyTrigger::HomeRowChar(mods) => match key.code {
135 AppKeyCode::Char(ch)
136 if key.modifiers == mods && swatch_home_row_index(ch).is_some() =>
137 {
138 build_action(binding.action, key)
139 }
140 _ => None,
141 },
142 }
143}
144
145fn build_action(spec: ActionSpec, key: AppKey) -> Option<EditorAction> {
146 match spec {
147 ActionSpec::Fixed(action) => Some(action),
148 ActionSpec::InsertMatchedChar => match key.code {
149 AppKeyCode::Char(ch) => Some(EditorAction::InsertChar(ch)),
150 _ => None,
151 },
152 ActionSpec::FillWithMatchedChar => match key.code {
153 AppKeyCode::Char(ch) => Some(EditorAction::FillSelectionOrCell(ch)),
154 _ => None,
155 },
156 ActionSpec::ActivateSwatchFromChar => match key.code {
157 AppKeyCode::Char(ch) => swatch_home_row_index(ch).map(EditorAction::ActivateSwatch),
158 _ => None,
159 },
160 }
161}
162
163pub(crate) fn swatch_home_row_index(ch: char) -> Option<usize> {
164 match ch {
165 'a' | 'A' => Some(0),
166 's' | 'S' => Some(1),
167 'd' | 'D' => Some(2),
168 'f' | 'F' => Some(3),
169 'g' | 'G' => Some(4),
170 _ => None,
171 }
172}
173
174fn default_standalone_bindings() -> Vec<KeyBinding> {
175 let mut out = Vec::new();
176 let help = |section, keys, description, order| {
177 Some(BindingHelp {
178 section,
179 keys,
180 description,
181 order,
182 })
183 };
184
185 let none = AppModifiers::default();
186 let shift = AppModifiers {
187 shift: true,
188 ..Default::default()
189 };
190 let ctrl = AppModifiers {
191 ctrl: true,
192 ..Default::default()
193 };
194 let ctrl_shift = AppModifiers {
195 ctrl: true,
196 shift: true,
197 ..Default::default()
198 };
199 let alt = AppModifiers {
200 alt: true,
201 ..Default::default()
202 };
203 let meta = AppModifiers {
204 meta: true,
205 ..Default::default()
206 };
207
208 let ctrl_shift_help = help(HelpSection::Drawing, "^⇧+←↑↓→", "pan / stroke floating", 80);
211 for (code, dx, dy) in [
212 (AppKeyCode::Left, -1_isize, 0_isize),
213 (AppKeyCode::Right, 1, 0),
214 (AppKeyCode::Up, 0, -1),
215 (AppKeyCode::Down, 0, 1),
216 ] {
217 out.push(KeyBinding {
218 trigger: KeyTrigger::Key(AppKey {
219 code,
220 modifiers: ctrl_shift,
221 }),
222 action: ActionSpec::Fixed(EditorAction::StrokeFloating {
223 dir: match code {
224 AppKeyCode::Left => MoveDir::Left,
225 AppKeyCode::Right => MoveDir::Right,
226 AppKeyCode::Up => MoveDir::Up,
227 AppKeyCode::Down => MoveDir::Down,
228 _ => unreachable!(),
229 },
230 }),
231 context: BindingContext::WhenFloating,
232 description: "stroke floating",
233 help: ctrl_shift_help,
234 });
235 out.push(KeyBinding {
236 trigger: KeyTrigger::Key(AppKey {
237 code,
238 modifiers: ctrl_shift,
239 }),
240 action: ActionSpec::Fixed(EditorAction::Pan { dx, dy }),
241 context: BindingContext::WhenNotFloating,
242 description: "pan viewport",
243 help: ctrl_shift_help,
244 });
245 }
246
247 out.push(KeyBinding {
250 trigger: KeyTrigger::Key(AppKey {
251 code: AppKeyCode::Char('t'),
252 modifiers: ctrl,
253 }),
254 action: ActionSpec::Fixed(EditorAction::ToggleFloatingTransparency),
255 context: BindingContext::WhenFloating,
256 description: "toggle float transparency",
257 help: help(HelpSection::Selection, "^T", "flip corner / see-thru", 80),
258 });
259
260 for (code, action, desc, binding_help) in [
262 (
263 AppKeyCode::Backspace,
264 EditorAction::PushLeft,
265 "push column left",
266 help(HelpSection::Transform, "^H / ^⌫", "push column ←", 10),
267 ),
268 (
269 AppKeyCode::Char('h'),
270 EditorAction::PushLeft,
271 "push column left",
272 help(HelpSection::Transform, "^H / ^⌫", "push column ←", 10),
273 ),
274 (
275 AppKeyCode::Char('j'),
276 EditorAction::PushDown,
277 "push row down",
278 help(HelpSection::Transform, "^J", "push row ↓", 20),
279 ),
280 (
281 AppKeyCode::Char('k'),
282 EditorAction::PushUp,
283 "push row up",
284 help(HelpSection::Transform, "^K", "push row ↑", 30),
285 ),
286 (
287 AppKeyCode::Char('l'),
288 EditorAction::PushRight,
289 "push column right",
290 help(HelpSection::Transform, "^L", "push column →", 40),
291 ),
292 (
293 AppKeyCode::Char('y'),
294 EditorAction::PullFromLeft,
295 "pull from left",
296 help(HelpSection::Transform, "^Y", "pull from ←", 50),
297 ),
298 (
299 AppKeyCode::Char('u'),
300 EditorAction::PullFromDown,
301 "pull from below",
302 help(HelpSection::Transform, "^U", "pull from ↓", 60),
303 ),
304 (
305 AppKeyCode::Tab,
306 EditorAction::PullFromUp,
307 "pull from above",
308 help(HelpSection::Transform, "^I / tab", "pull from ↑", 70),
309 ),
310 (
311 AppKeyCode::Char('i'),
312 EditorAction::PullFromUp,
313 "pull from above",
314 help(HelpSection::Transform, "^I / tab", "pull from ↑", 70),
315 ),
316 (
317 AppKeyCode::Char('o'),
318 EditorAction::PullFromRight,
319 "pull from right",
320 help(HelpSection::Transform, "^O", "pull from →", 80),
321 ),
322 (
323 AppKeyCode::Char('c'),
324 EditorAction::CopySelection,
325 "copy selection",
326 help(HelpSection::Clipboard, "^C", "copy → swatch", 10),
327 ),
328 (
329 AppKeyCode::Char('x'),
330 EditorAction::CutSelection,
331 "cut selection",
332 help(HelpSection::Clipboard, "^X", "cut → swatch", 20),
333 ),
334 (
335 AppKeyCode::Char('v'),
336 EditorAction::PastePrimarySwatch,
337 "paste primary swatch",
338 help(HelpSection::Clipboard, "^V", "paste / stamp", 30),
339 ),
340 (
341 AppKeyCode::Char('b'),
342 EditorAction::DrawBorder,
343 "draw selection border",
344 help(HelpSection::Transform, "^B", "draw selection border", 90),
345 ),
346 (
347 AppKeyCode::Char('t'),
348 EditorAction::TransposeSelectionCorner,
349 "transpose selection corner",
350 help(HelpSection::Selection, "^T", "flip corner / see-thru", 80),
351 ),
352 (
353 AppKeyCode::Char(' '),
354 EditorAction::SmartFill,
355 "smart-fill selection",
356 help(
357 HelpSection::Transform,
358 "^space",
359 "fill selection or cell",
360 100,
361 ),
362 ),
363 ] {
364 out.push(KeyBinding {
365 trigger: KeyTrigger::Key(AppKey {
366 code,
367 modifiers: ctrl,
368 }),
369 action: ActionSpec::Fixed(action),
370 context: BindingContext::Always,
371 description: desc,
372 help: binding_help,
373 });
374 }
375
376 out.push(KeyBinding {
378 trigger: KeyTrigger::HomeRowChar(ctrl),
379 action: ActionSpec::ActivateSwatchFromChar,
380 context: BindingContext::Always,
381 description: "activate swatch slot",
382 help: help(
383 HelpSection::Clipboard,
384 "^A/^S/^D/^F/^G",
385 "lift swatch 1..5",
386 50,
387 ),
388 });
389
390 for mods in [alt, meta] {
392 out.push(KeyBinding {
393 trigger: KeyTrigger::Key(AppKey {
394 code: AppKeyCode::Char('c'),
395 modifiers: mods,
396 }),
397 action: ActionSpec::Fixed(EditorAction::ExportSystemClipboard),
398 context: BindingContext::Always,
399 description: "copy to system clipboard",
400 help: help(HelpSection::Clipboard, "alt/meta+c", "os copy", 40),
401 });
402 for (code, dx, dy) in [
403 (AppKeyCode::Left, -1_isize, 0_isize),
404 (AppKeyCode::Right, 1, 0),
405 (AppKeyCode::Up, 0, -1),
406 (AppKeyCode::Down, 0, 1),
407 ] {
408 out.push(KeyBinding {
409 trigger: KeyTrigger::Key(AppKey {
410 code,
411 modifiers: mods,
412 }),
413 action: ActionSpec::Fixed(EditorAction::Pan { dx, dy }),
414 context: BindingContext::Always,
415 description: "pan viewport",
416 help: help(HelpSection::Drawing, "alt/meta+←↑↓→", "pan viewport", 90),
417 });
418 }
419 }
420
421 for (code, dir, move_help, extend_help) in [
423 (
424 AppKeyCode::Up,
425 MoveDir::Up,
426 help(HelpSection::Drawing, "←↑↓→", "move cursor", 40),
427 help(
428 HelpSection::Selection,
429 "shift+←↑↓→",
430 "create/extend selection",
431 10,
432 ),
433 ),
434 (
435 AppKeyCode::Down,
436 MoveDir::Down,
437 help(HelpSection::Drawing, "←↑↓→", "move cursor", 40),
438 help(
439 HelpSection::Selection,
440 "shift+←↑↓→",
441 "create/extend selection",
442 10,
443 ),
444 ),
445 (
446 AppKeyCode::Left,
447 MoveDir::Left,
448 help(HelpSection::Drawing, "←↑↓→", "move cursor", 40),
449 help(
450 HelpSection::Selection,
451 "shift+←↑↓→",
452 "create/extend selection",
453 10,
454 ),
455 ),
456 (
457 AppKeyCode::Right,
458 MoveDir::Right,
459 help(HelpSection::Drawing, "←↑↓→", "move cursor", 40),
460 help(
461 HelpSection::Selection,
462 "shift+←↑↓→",
463 "create/extend selection",
464 10,
465 ),
466 ),
467 (
468 AppKeyCode::Home,
469 MoveDir::LineStart,
470 help(HelpSection::Drawing, "home / end", "← / → edge", 50),
471 help(
472 HelpSection::Selection,
473 "shift+home / end",
474 "extend to ← / → edge",
475 20,
476 ),
477 ),
478 (
479 AppKeyCode::End,
480 MoveDir::LineEnd,
481 help(HelpSection::Drawing, "home / end", "← / → edge", 50),
482 help(
483 HelpSection::Selection,
484 "shift+home / end",
485 "extend to ← / → edge",
486 20,
487 ),
488 ),
489 (
490 AppKeyCode::PageUp,
491 MoveDir::PageUp,
492 help(HelpSection::Drawing, "pgup / pgdn", "↑ / ↓ edge", 60),
493 help(
494 HelpSection::Selection,
495 "shift+pgup / pgdn",
496 "extend to ↑ / ↓ edge",
497 30,
498 ),
499 ),
500 (
501 AppKeyCode::PageDown,
502 MoveDir::PageDown,
503 help(HelpSection::Drawing, "pgup / pgdn", "↑ / ↓ edge", 60),
504 help(
505 HelpSection::Selection,
506 "shift+pgup / pgdn",
507 "extend to ↑ / ↓ edge",
508 30,
509 ),
510 ),
511 ] {
512 out.push(KeyBinding {
513 trigger: KeyTrigger::Key(AppKey {
514 code,
515 modifiers: shift,
516 }),
517 action: ActionSpec::Fixed(EditorAction::Move {
518 dir,
519 extend_selection: true,
520 }),
521 context: BindingContext::Always,
522 description: "extend selection",
523 help: extend_help,
524 });
525 out.push(KeyBinding {
526 trigger: KeyTrigger::Key(AppKey {
527 code,
528 modifiers: none,
529 }),
530 action: ActionSpec::Fixed(EditorAction::Move {
531 dir,
532 extend_selection: false,
533 }),
534 context: BindingContext::Always,
535 description: "move cursor",
536 help: move_help,
537 });
538 }
539
540 out.push(KeyBinding {
542 trigger: KeyTrigger::Key(AppKey {
543 code: AppKeyCode::Enter,
544 modifiers: none,
545 }),
546 action: ActionSpec::Fixed(EditorAction::PastePrimarySwatch),
547 context: BindingContext::WhenFloating,
548 description: "stamp floating",
549 help: help(HelpSection::Clipboard, "enter", "stamp floating", 35),
550 });
551 out.push(KeyBinding {
552 trigger: KeyTrigger::Key(AppKey {
553 code: AppKeyCode::Enter,
554 modifiers: none,
555 }),
556 action: ActionSpec::Fixed(EditorAction::MoveDownLine),
557 context: BindingContext::Always,
558 description: "move to next row",
559 help: help(HelpSection::Drawing, "enter", "move down", 70),
560 });
561 out.push(KeyBinding {
562 trigger: KeyTrigger::Key(AppKey {
563 code: AppKeyCode::Esc,
564 modifiers: none,
565 }),
566 action: ActionSpec::Fixed(EditorAction::ClearSelection),
567 context: BindingContext::Always,
568 description: "clear selection",
569 help: None,
570 });
571
572 for mods in [none, shift] {
574 out.push(KeyBinding {
575 trigger: KeyTrigger::AnyChar(mods),
576 action: ActionSpec::FillWithMatchedChar,
577 context: BindingContext::WhenSelecting,
578 description: "fill selection with character",
579 help: help(HelpSection::Selection, "<type>", "fill selection", 40),
580 });
581 }
582 for mods in [none, shift] {
583 for code in [AppKeyCode::Backspace, AppKeyCode::Delete] {
584 out.push(KeyBinding {
585 trigger: KeyTrigger::Key(AppKey {
586 code,
587 modifiers: mods,
588 }),
589 action: ActionSpec::Fixed(EditorAction::FillSelectionOrCell(' ')),
590 context: BindingContext::WhenSelecting,
591 description: "erase selection",
592 help: help(
593 HelpSection::Selection,
594 "backspace / delete",
595 "clear selection",
596 50,
597 ),
598 });
599 }
600 }
601
602 for mods in [none, shift] {
604 out.push(KeyBinding {
605 trigger: KeyTrigger::AnyChar(mods),
606 action: ActionSpec::InsertMatchedChar,
607 context: BindingContext::WhenNotSelecting,
608 description: "insert character",
609 help: help(HelpSection::Drawing, "<type>", "draw character", 10),
610 });
611 }
612 out.push(KeyBinding {
613 trigger: KeyTrigger::Key(AppKey {
614 code: AppKeyCode::Backspace,
615 modifiers: none,
616 }),
617 action: ActionSpec::Fixed(EditorAction::Backspace),
618 context: BindingContext::WhenNotSelecting,
619 description: "delete previous character",
620 help: help(HelpSection::Drawing, "backspace", "erase backward", 20),
621 });
622 out.push(KeyBinding {
623 trigger: KeyTrigger::Key(AppKey {
624 code: AppKeyCode::Delete,
625 modifiers: none,
626 }),
627 action: ActionSpec::Fixed(EditorAction::Delete),
628 context: BindingContext::WhenNotSelecting,
629 description: "delete character at cursor",
630 help: help(HelpSection::Drawing, "delete", "erase at cursor", 30),
631 });
632
633 out
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 fn map() -> KeyMap {
641 KeyMap::default_standalone()
642 }
643
644 fn resolve(key: AppKey) -> Option<EditorAction> {
645 map().resolve(key, EditorContext::default())
646 }
647
648 fn resolve_selecting(key: AppKey) -> Option<EditorAction> {
649 map().resolve(
650 key,
651 EditorContext {
652 mode: Mode::Select,
653 has_selection_anchor: true,
654 is_floating: false,
655 },
656 )
657 }
658
659 fn resolve_floating(key: AppKey) -> Option<EditorAction> {
660 map().resolve(
661 key,
662 EditorContext {
663 is_floating: true,
664 ..Default::default()
665 },
666 )
667 }
668
669 fn key(code: AppKeyCode, mods: AppModifiers) -> AppKey {
670 AppKey {
671 code,
672 modifiers: mods,
673 }
674 }
675
676 #[test]
677 fn ctrl_shift_arrow_pans_when_not_floating() {
678 let mods = AppModifiers {
679 ctrl: true,
680 shift: true,
681 ..Default::default()
682 };
683 assert_eq!(
684 resolve(key(AppKeyCode::Left, mods)),
685 Some(EditorAction::Pan { dx: -1, dy: 0 })
686 );
687 }
688
689 #[test]
690 fn ctrl_shift_arrow_strokes_when_floating() {
691 let mods = AppModifiers {
692 ctrl: true,
693 shift: true,
694 ..Default::default()
695 };
696 assert_eq!(
697 resolve_floating(key(AppKeyCode::Left, mods)),
698 Some(EditorAction::StrokeFloating { dir: MoveDir::Left })
699 );
700 }
701
702 #[test]
703 fn ctrl_char_maps_to_editor_command() {
704 let mods = AppModifiers {
705 ctrl: true,
706 ..Default::default()
707 };
708 assert_eq!(
709 resolve(key(AppKeyCode::Char('h'), mods)),
710 Some(EditorAction::PushLeft)
711 );
712 assert_eq!(
713 resolve(key(AppKeyCode::Char('v'), mods)),
714 Some(EditorAction::PastePrimarySwatch)
715 );
716 }
717
718 #[test]
719 fn enter_stamps_when_floating() {
720 assert_eq!(
721 resolve_floating(key(AppKeyCode::Enter, AppModifiers::default())),
722 Some(EditorAction::PastePrimarySwatch)
723 );
724 assert_eq!(
725 resolve(key(AppKeyCode::Enter, AppModifiers::default())),
726 Some(EditorAction::MoveDownLine)
727 );
728 }
729
730 #[test]
731 fn ctrl_home_row_activates_swatch() {
732 let mods = AppModifiers {
733 ctrl: true,
734 ..Default::default()
735 };
736 assert_eq!(
737 resolve(key(AppKeyCode::Char('d'), mods)),
738 Some(EditorAction::ActivateSwatch(2))
739 );
740 assert_eq!(resolve(key(AppKeyCode::Char('z'), mods)), None);
742 }
743
744 #[test]
745 fn shift_move_extends_selection() {
746 let mods = AppModifiers {
747 shift: true,
748 ..Default::default()
749 };
750 assert_eq!(
751 resolve(key(AppKeyCode::Right, mods)),
752 Some(EditorAction::Move {
753 dir: MoveDir::Right,
754 extend_selection: true,
755 })
756 );
757 }
758
759 #[test]
760 fn plain_move_does_not_extend() {
761 assert_eq!(
762 resolve(key(AppKeyCode::Right, AppModifiers::default())),
763 Some(EditorAction::Move {
764 dir: MoveDir::Right,
765 extend_selection: false,
766 })
767 );
768 }
769
770 #[test]
771 fn char_inserts_when_not_selecting_and_fills_when_selecting() {
772 let k = key(AppKeyCode::Char('q'), AppModifiers::default());
773 assert_eq!(resolve(k), Some(EditorAction::InsertChar('q')));
774 assert_eq!(
775 resolve_selecting(k),
776 Some(EditorAction::FillSelectionOrCell('q'))
777 );
778 }
779
780 #[test]
781 fn backspace_and_delete_switch_action_by_context() {
782 let bs = key(AppKeyCode::Backspace, AppModifiers::default());
783 let del = key(AppKeyCode::Delete, AppModifiers::default());
784 assert_eq!(resolve(bs), Some(EditorAction::Backspace));
785 assert_eq!(resolve(del), Some(EditorAction::Delete));
786 assert_eq!(
787 resolve_selecting(bs),
788 Some(EditorAction::FillSelectionOrCell(' '))
789 );
790 assert_eq!(
791 resolve_selecting(del),
792 Some(EditorAction::FillSelectionOrCell(' '))
793 );
794 }
795
796 #[test]
797 fn alt_c_exports_clipboard() {
798 let mods = AppModifiers {
799 alt: true,
800 ..Default::default()
801 };
802 assert_eq!(
803 resolve(key(AppKeyCode::Char('c'), mods)),
804 Some(EditorAction::ExportSystemClipboard)
805 );
806 }
807
808 #[test]
809 fn unmapped_key_returns_none() {
810 let mods = AppModifiers {
811 ctrl: true,
812 alt: true,
813 ..Default::default()
814 };
815 assert_eq!(resolve(key(AppKeyCode::Char('z'), mods)), None);
816 }
817
818 #[test]
819 fn ctrl_t_depends_on_floating_context() {
820 let ctrl = AppModifiers {
821 ctrl: true,
822 ..Default::default()
823 };
824 assert_eq!(
825 resolve(key(AppKeyCode::Char('t'), ctrl)),
826 Some(EditorAction::TransposeSelectionCorner)
827 );
828 assert_eq!(
829 resolve_floating(key(AppKeyCode::Char('t'), ctrl)),
830 Some(EditorAction::ToggleFloatingTransparency)
831 );
832 }
833
834 #[test]
835 fn shift_backspace_while_selecting_still_erases() {
836 let mods = AppModifiers {
837 shift: true,
838 ..Default::default()
839 };
840 assert_eq!(
841 resolve_selecting(key(AppKeyCode::Backspace, mods)),
842 Some(EditorAction::FillSelectionOrCell(' '))
843 );
844 assert_eq!(
845 resolve_selecting(key(AppKeyCode::Delete, mods)),
846 Some(EditorAction::FillSelectionOrCell(' '))
847 );
848 }
849
850 #[test]
851 fn bindings_include_descriptions() {
852 let m = map();
853 assert!(m.bindings().iter().all(|b| !b.description.is_empty()));
854 assert!(!m.bindings().is_empty());
855 }
856
857 #[test]
858 fn help_entries_are_sorted_and_deduped() {
859 let rows = map().help_entries();
860 assert!(!rows.is_empty());
861 assert_eq!(
862 rows.iter()
863 .filter(|row| row.keys == "^T" && row.description == "flip corner / see-thru")
864 .count(),
865 1
866 );
867 assert!(rows.iter().any(|row| {
868 row.section == HelpSection::Clipboard
869 && row.keys == "^C"
870 && row.description == "copy → swatch"
871 }));
872 }
873}