Skip to main content

atomcode_tuix/input/
key_action.rs

1// crates/atomcode-tuix/src/input/key_action.rs
2use crossterm::event::{KeyCode, KeyModifiers};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Action {
6    Submit,
7    InsertNewline,
8    Cancel,
9    ClearLine,
10    DeleteWordBackward,
11    DeleteToEnd,
12    Insert(char),
13    Complete,
14    CursorLeft,
15    CursorRight,
16    LineStart,
17    LineEnd,
18    HistoryPrev,
19    HistoryNext,
20    Backspace,
21    DeleteForward,
22    ToggleToolOutput,
23    NoOp,
24}
25
26pub fn classify(code: KeyCode, modifiers: KeyModifiers) -> Action {
27    let ctrl = modifiers.contains(KeyModifiers::CONTROL);
28    let shift = modifiers.contains(KeyModifiers::SHIFT);
29    let alt = modifiers.contains(KeyModifiers::ALT);
30
31    match (code, ctrl) {
32        (KeyCode::Enter, ctrl) if ctrl || shift || alt => Action::InsertNewline,
33        (KeyCode::Enter, _) => Action::Submit,
34        // Ctrl+J = ASCII LF. On Kitty-aware terminals it arrives disambiguated
35        // from Enter and gives users another newline chord when their primary
36        // one is intercepted by the host terminal (e.g. Windows Terminal binds
37        // Alt+Enter to toggleFullscreen by default).
38        (KeyCode::Char('j'), true) => Action::InsertNewline,
39        (KeyCode::Char('c'), true) => Action::Cancel,
40        (KeyCode::Char('u'), true) => Action::ClearLine,
41        (KeyCode::Char('w'), true) => Action::DeleteWordBackward,
42        (KeyCode::Char('k'), true) => Action::DeleteToEnd,
43        // Emacs-style line navigation — Home/End aliases. Docs already
44        // promise these in site/docs/keybindings.html.
45        (KeyCode::Char('a'), true) => Action::LineStart,
46        (KeyCode::Char('e'), true) => Action::LineEnd,
47        // Ctrl+O toggles real-time tool output visibility.
48        (KeyCode::Char('o'), true) => Action::ToggleToolOutput,
49        // Ctrl+H is the POSIX / readline alias for Backspace. MobaXterm,
50        // PuTTY and other Windows SSH clients often ship with "Backspace
51        // sends ^H" turned on by default, so the physical Backspace key
52        // arrives here as `Ctrl+Char('h')` rather than `KeyCode::Backspace`.
53        // Without this arm the key is a no-op on those terminals — the
54        // user sees their input line accumulate characters they can't
55        // erase.
56        (KeyCode::Char('h'), true) => Action::Backspace,
57        // Ctrl+? (ASCII 0x7F with modifier coerced) — some xterm-family
58        // terminals emit this for the literal Delete key. Keep the
59        // behaviour symmetric with the bare `KeyCode::Delete` arm below.
60        (KeyCode::Char('?'), true) => Action::DeleteForward,
61        (KeyCode::Char(c), false) => Action::Insert(c),
62        (KeyCode::Tab, _) => Action::Complete,
63        (KeyCode::Left, _) => Action::CursorLeft,
64        (KeyCode::Right, _) => Action::CursorRight,
65        (KeyCode::Home, _) => Action::LineStart,
66        (KeyCode::End, _) => Action::LineEnd,
67        (KeyCode::Up, _) => Action::HistoryPrev,
68        (KeyCode::Down, _) => Action::HistoryNext,
69        (KeyCode::Backspace, _) => Action::Backspace,
70        (KeyCode::Delete, _) => Action::DeleteForward,
71        _ => Action::NoOp,
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crossterm::event::{KeyCode, KeyModifiers};
79
80    fn k(code: KeyCode, modifiers: KeyModifiers) -> Action {
81        classify(code, modifiers)
82    }
83
84    #[test]
85    fn enter_submits() {
86        assert_eq!(k(KeyCode::Enter, KeyModifiers::NONE), Action::Submit);
87    }
88
89    #[test]
90    fn shift_enter_inserts_newline() {
91        assert_eq!(
92            k(KeyCode::Enter, KeyModifiers::SHIFT),
93            Action::InsertNewline
94        );
95    }
96
97    #[test]
98    fn alt_enter_inserts_newline() {
99        assert_eq!(k(KeyCode::Enter, KeyModifiers::ALT), Action::InsertNewline);
100    }
101
102    #[test]
103    fn alt_shift_enter_inserts_newline() {
104        assert_eq!(
105            k(KeyCode::Enter, KeyModifiers::ALT | KeyModifiers::SHIFT),
106            Action::InsertNewline
107        );
108    }
109
110    #[test]
111    fn ctrl_j_inserts_newline() {
112        // Ctrl+J = ASCII 0x0A (LF). On terminals that negotiate the Kitty
113        // keyboard protocol, crossterm reports it as `Char('j'), CONTROL`
114        // — give it the same role as Shift/Ctrl/Alt+Enter so users on
115        // Kitty-aware terminals (kitty, wezterm, alacritty, WT ≥1.21) have
116        // an extra fallback when their main chord is intercepted by the
117        // host terminal (e.g. Windows Terminal eats Alt+Enter for full-
118        // screen toggle by default).
119        assert_eq!(
120            k(KeyCode::Char('j'), KeyModifiers::CONTROL),
121            Action::InsertNewline
122        );
123    }
124
125    #[test]
126    fn ctrl_c_cancels() {
127        assert_eq!(k(KeyCode::Char('c'), KeyModifiers::CONTROL), Action::Cancel);
128    }
129
130    #[test]
131    fn ctrl_u_clears_line() {
132        assert_eq!(
133            k(KeyCode::Char('u'), KeyModifiers::CONTROL),
134            Action::ClearLine
135        );
136    }
137
138    #[test]
139    fn ctrl_w_deletes_word() {
140        assert_eq!(
141            k(KeyCode::Char('w'), KeyModifiers::CONTROL),
142            Action::DeleteWordBackward
143        );
144    }
145
146    #[test]
147    fn ctrl_k_deletes_to_end() {
148        assert_eq!(
149            k(KeyCode::Char('k'), KeyModifiers::CONTROL),
150            Action::DeleteToEnd
151        );
152    }
153
154    #[test]
155    fn ctrl_a_jumps_to_line_start() {
156        assert_eq!(
157            k(KeyCode::Char('a'), KeyModifiers::CONTROL),
158            Action::LineStart
159        );
160    }
161
162    #[test]
163    fn ctrl_e_jumps_to_line_end() {
164        assert_eq!(
165            k(KeyCode::Char('e'), KeyModifiers::CONTROL),
166            Action::LineEnd
167        );
168    }
169
170    #[test]
171    fn ctrl_h_acts_as_backspace() {
172        // MobaXterm / PuTTY default: Backspace key sends ^H. Must delete
173        // a char, not be a silent no-op.
174        assert_eq!(
175            k(KeyCode::Char('h'), KeyModifiers::CONTROL),
176            Action::Backspace
177        );
178    }
179
180    #[test]
181    fn ctrl_questionmark_acts_as_delete_forward() {
182        assert_eq!(
183            k(KeyCode::Char('?'), KeyModifiers::CONTROL),
184            Action::DeleteForward,
185        );
186    }
187
188    #[test]
189    fn plain_letter_inserts() {
190        assert_eq!(
191            k(KeyCode::Char('a'), KeyModifiers::NONE),
192            Action::Insert('a')
193        );
194    }
195
196    #[test]
197    fn shifted_letter_inserts() {
198        assert_eq!(
199            k(KeyCode::Char('A'), KeyModifiers::SHIFT),
200            Action::Insert('A')
201        );
202    }
203
204    #[test]
205    fn tab_completes() {
206        assert_eq!(k(KeyCode::Tab, KeyModifiers::NONE), Action::Complete);
207    }
208
209    #[test]
210    fn arrow_navigation() {
211        assert_eq!(k(KeyCode::Left, KeyModifiers::NONE), Action::CursorLeft);
212        assert_eq!(k(KeyCode::Right, KeyModifiers::NONE), Action::CursorRight);
213        assert_eq!(k(KeyCode::Up, KeyModifiers::NONE), Action::HistoryPrev);
214        assert_eq!(k(KeyCode::Down, KeyModifiers::NONE), Action::HistoryNext);
215        assert_eq!(k(KeyCode::Home, KeyModifiers::NONE), Action::LineStart);
216        assert_eq!(k(KeyCode::End, KeyModifiers::NONE), Action::LineEnd);
217    }
218
219    #[test]
220    fn backspace_and_delete() {
221        assert_eq!(k(KeyCode::Backspace, KeyModifiers::NONE), Action::Backspace);
222        assert_eq!(
223            k(KeyCode::Delete, KeyModifiers::NONE),
224            Action::DeleteForward
225        );
226    }
227
228    #[test]
229    fn unknown_key_is_noop() {
230        assert_eq!(k(KeyCode::F(5), KeyModifiers::NONE), Action::NoOp);
231    }
232}