Skip to main content

heatsync_core/
key.rs

1//! Face-independent key events.
2//!
3//! The editor and the vi model are the same logic whether the keystroke came
4//! from a terminal or a window, but each face has its own key type: the tui
5//! reads `crossterm::event::KeyEvent`, a gui reads whatever its toolkit hands
6//! it. Neither belongs in shared code — core takes no io dependencies.
7//!
8//! So core owns the vocabulary and each face maps into it. The shape here
9//! deliberately mirrors crossterm's (`code` + `modifiers`, a `contains` on the
10//! modifier set) so the terminal face's mapping is mechanical and the editor
11//! reads the same as it always did.
12
13/// Every key the editor and vi model actually distinguish. Closed on purpose —
14/// a face that sees something else drops it rather than widening this.
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum KeyCode {
17    Esc,
18    Enter,
19    Backspace,
20    Delete,
21    Left,
22    Right,
23    Up,
24    Down,
25    Home,
26    End,
27    Tab,
28    BackTab,
29    Char(char),
30}
31
32/// Held modifiers. `contains` asks "are all of the bits in `other` set here",
33/// matching the bitflags semantics the call sites were written against.
34#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
35pub struct KeyModifiers {
36    pub ctrl: bool,
37    pub alt: bool,
38    pub shift: bool,
39}
40
41impl KeyModifiers {
42    pub const NONE: Self = Self {
43        ctrl: false,
44        alt: false,
45        shift: false,
46    };
47    pub const CONTROL: Self = Self {
48        ctrl: true,
49        alt: false,
50        shift: false,
51    };
52    pub const ALT: Self = Self {
53        ctrl: false,
54        alt: true,
55        shift: false,
56    };
57    pub const SHIFT: Self = Self {
58        ctrl: false,
59        alt: false,
60        shift: true,
61    };
62
63    pub fn contains(self, other: Self) -> bool {
64        (!other.ctrl || self.ctrl) && (!other.alt || self.alt) && (!other.shift || self.shift)
65    }
66}
67
68/// One keystroke.
69#[derive(Clone, Copy, PartialEq, Eq, Debug)]
70pub struct KeyEvent {
71    pub code: KeyCode,
72    pub modifiers: KeyModifiers,
73}
74
75impl KeyEvent {
76    pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
77        Self { code, modifiers }
78    }
79
80    /// An unmodified keystroke — the common case in tests and in plain typing.
81    pub fn bare(code: KeyCode) -> Self {
82        Self {
83            code,
84            modifiers: KeyModifiers::NONE,
85        }
86    }
87
88    pub fn ctrl(code: KeyCode) -> Self {
89        Self {
90            code,
91            modifiers: KeyModifiers::CONTROL,
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn none_contains_nothing_and_is_contained_by_everything() {
102        assert!(KeyModifiers::NONE.contains(KeyModifiers::NONE));
103        assert!(!KeyModifiers::NONE.contains(KeyModifiers::CONTROL));
104        assert!(KeyModifiers::CONTROL.contains(KeyModifiers::NONE));
105    }
106
107    #[test]
108    fn contains_asks_for_a_subset_not_equality() {
109        let ctrl_shift = KeyModifiers {
110            ctrl: true,
111            alt: false,
112            shift: true,
113        };
114        assert!(ctrl_shift.contains(KeyModifiers::CONTROL));
115        assert!(ctrl_shift.contains(KeyModifiers::SHIFT));
116        assert!(!ctrl_shift.contains(KeyModifiers::ALT));
117    }
118
119    #[test]
120    fn constructors_agree_with_literals() {
121        assert_eq!(
122            KeyEvent::bare(KeyCode::Enter),
123            KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)
124        );
125        assert_eq!(
126            KeyEvent::ctrl(KeyCode::Char('w')),
127            KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL)
128        );
129    }
130}