Skip to main content

a3s_tui/
event.rs

1//! Terminal event types (keyboard, mouse, resize, focus).
2
3use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseEventKind as CtMouseEventKind};
4
5/// A terminal event.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum Event {
8    /// Keyboard input.
9    Key(KeyEvent),
10    /// Mouse input.
11    Mouse(MouseEvent),
12    /// Terminal window resized.
13    Resize { width: u16, height: u16 },
14    /// Terminal gained focus.
15    FocusGained,
16    /// Terminal lost focus.
17    FocusLost,
18    /// Bracketed paste: the full pasted text in one event, so a multi-line
19    /// paste lands in the input instead of submitting line by line.
20    Paste(String),
21}
22
23/// A keyboard event with key code and modifiers.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct KeyEvent {
26    pub code: KeyCode,
27    pub modifiers: KeyModifiers,
28}
29
30impl KeyEvent {
31    /// Check if this is a specific character (without modifiers).
32    pub fn is_char(&self, c: char) -> bool {
33        self.code == KeyCode::Char(c) && self.modifiers == KeyModifiers::NONE
34    }
35
36    /// Check if this is Ctrl+`char`.
37    pub fn is_ctrl(&self, c: char) -> bool {
38        self.code == KeyCode::Char(c) && self.modifiers.contains(KeyModifiers::CONTROL)
39    }
40
41    /// Check if this is Alt+`char`.
42    pub fn is_alt(&self, c: char) -> bool {
43        self.code == KeyCode::Char(c) && self.modifiers.contains(KeyModifiers::ALT)
44    }
45
46    /// Check if this matches a specific key code (ignoring modifiers).
47    pub fn is_key(&self, code: KeyCode) -> bool {
48        self.code == code
49    }
50
51    /// Check if Escape was pressed.
52    pub fn is_esc(&self) -> bool {
53        self.code == KeyCode::Esc
54    }
55
56    /// Check if Enter was pressed.
57    pub fn is_enter(&self) -> bool {
58        self.code == KeyCode::Enter
59    }
60
61    /// Check if any character key was pressed (returns the char).
62    pub fn char(&self) -> Option<char> {
63        match self.code {
64            KeyCode::Char(c) => Some(c),
65            _ => None,
66        }
67    }
68}
69
70/// A mouse event with position and button state.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct MouseEvent {
73    pub kind: MouseEventKind,
74    pub column: u16,
75    pub row: u16,
76    pub modifiers: KeyModifiers,
77}
78
79/// The type of mouse action.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum MouseEventKind {
82    Down(MouseButton),
83    Up(MouseButton),
84    Drag(MouseButton),
85    Moved,
86    ScrollDown,
87    ScrollUp,
88    ScrollLeft,
89    ScrollRight,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum MouseButton {
94    Left,
95    Right,
96    Middle,
97}
98
99impl From<crossterm::event::Event> for Event {
100    fn from(event: crossterm::event::Event) -> Self {
101        match event {
102            crossterm::event::Event::Key(key) => Event::Key(KeyEvent {
103                code: key.code,
104                modifiers: key.modifiers,
105            }),
106            crossterm::event::Event::Mouse(mouse) => Event::Mouse(MouseEvent {
107                kind: convert_mouse_kind(mouse.kind),
108                column: mouse.column,
109                row: mouse.row,
110                modifiers: mouse.modifiers,
111            }),
112            crossterm::event::Event::Resize(w, h) => Event::Resize {
113                width: w,
114                height: h,
115            },
116            crossterm::event::Event::FocusGained => Event::FocusGained,
117            crossterm::event::Event::FocusLost => Event::FocusLost,
118            crossterm::event::Event::Paste(text) => Event::Paste(text),
119        }
120    }
121}
122
123impl Event {
124    /// Convert a crossterm event into an app event, dropping terminal-only key
125    /// releases so Windows terminals do not replay a single key as two inputs.
126    pub fn from_crossterm(event: crossterm::event::Event) -> Option<Self> {
127        match event {
128            crossterm::event::Event::Key(key) if key.kind == KeyEventKind::Release => None,
129            event => Some(event.into()),
130        }
131    }
132}
133
134fn convert_mouse_button(button: crossterm::event::MouseButton) -> MouseButton {
135    match button {
136        crossterm::event::MouseButton::Left => MouseButton::Left,
137        crossterm::event::MouseButton::Right => MouseButton::Right,
138        crossterm::event::MouseButton::Middle => MouseButton::Middle,
139    }
140}
141
142fn convert_mouse_kind(kind: CtMouseEventKind) -> MouseEventKind {
143    match kind {
144        CtMouseEventKind::Down(b) => MouseEventKind::Down(convert_mouse_button(b)),
145        CtMouseEventKind::Up(b) => MouseEventKind::Up(convert_mouse_button(b)),
146        CtMouseEventKind::Drag(b) => MouseEventKind::Drag(convert_mouse_button(b)),
147        CtMouseEventKind::Moved => MouseEventKind::Moved,
148        CtMouseEventKind::ScrollDown => MouseEventKind::ScrollDown,
149        CtMouseEventKind::ScrollUp => MouseEventKind::ScrollUp,
150        CtMouseEventKind::ScrollLeft => MouseEventKind::ScrollLeft,
151        CtMouseEventKind::ScrollRight => MouseEventKind::ScrollRight,
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn crossterm_key_release_is_ignored() {
161        let event = crossterm::event::Event::Key(crossterm::event::KeyEvent::new_with_kind(
162            KeyCode::Char('a'),
163            KeyModifiers::NONE,
164            KeyEventKind::Release,
165        ));
166
167        assert_eq!(Event::from_crossterm(event), None);
168    }
169
170    #[test]
171    fn crossterm_key_press_and_repeat_are_kept() {
172        for kind in [KeyEventKind::Press, KeyEventKind::Repeat] {
173            let event = crossterm::event::Event::Key(crossterm::event::KeyEvent::new_with_kind(
174                KeyCode::Char('a'),
175                KeyModifiers::NONE,
176                kind,
177            ));
178
179            assert_eq!(
180                Event::from_crossterm(event),
181                Some(Event::Key(KeyEvent {
182                    code: KeyCode::Char('a'),
183                    modifiers: KeyModifiers::NONE,
184                }))
185            );
186        }
187    }
188
189    #[test]
190    fn is_char() {
191        let e = KeyEvent {
192            code: KeyCode::Char('q'),
193            modifiers: KeyModifiers::NONE,
194        };
195        assert!(e.is_char('q'));
196        assert!(!e.is_char('x'));
197    }
198
199    #[test]
200    fn is_ctrl() {
201        let e = KeyEvent {
202            code: KeyCode::Char('c'),
203            modifiers: KeyModifiers::CONTROL,
204        };
205        assert!(e.is_ctrl('c'));
206        assert!(!e.is_ctrl('x'));
207        let plain = KeyEvent {
208            code: KeyCode::Char('c'),
209            modifiers: KeyModifiers::NONE,
210        };
211        assert!(!plain.is_ctrl('c'));
212    }
213
214    #[test]
215    fn is_alt() {
216        let e = KeyEvent {
217            code: KeyCode::Char('x'),
218            modifiers: KeyModifiers::ALT,
219        };
220        assert!(e.is_alt('x'));
221    }
222
223    #[test]
224    fn is_key() {
225        let e = KeyEvent {
226            code: KeyCode::Enter,
227            modifiers: KeyModifiers::NONE,
228        };
229        assert!(e.is_key(KeyCode::Enter));
230        assert!(e.is_enter());
231        assert!(!e.is_esc());
232    }
233
234    #[test]
235    fn char_extraction() {
236        let e = KeyEvent {
237            code: KeyCode::Char('a'),
238            modifiers: KeyModifiers::NONE,
239        };
240        assert_eq!(e.char(), Some('a'));
241        let esc = KeyEvent {
242            code: KeyCode::Esc,
243            modifiers: KeyModifiers::NONE,
244        };
245        assert_eq!(esc.char(), None);
246    }
247}