Skip to main content

saorsa_core/
event.rs

1//! Event types for terminal input handling.
2
3use std::fmt;
4
5/// A terminal event.
6#[derive(Clone, Debug, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum Event {
9    /// A key was pressed.
10    Key(KeyEvent),
11    /// A mouse event occurred.
12    Mouse(MouseEvent),
13    /// The terminal was resized.
14    Resize(u16, u16),
15    /// Text was pasted (bracketed paste mode).
16    Paste(String),
17}
18
19/// A keyboard event.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct KeyEvent {
22    /// The key code.
23    pub code: KeyCode,
24    /// Active modifiers.
25    pub modifiers: Modifiers,
26}
27
28impl KeyEvent {
29    /// Create a new key event.
30    pub fn new(code: KeyCode, modifiers: Modifiers) -> Self {
31        Self { code, modifiers }
32    }
33
34    /// Create a plain key event with no modifiers.
35    pub fn plain(code: KeyCode) -> Self {
36        Self {
37            code,
38            modifiers: Modifiers::NONE,
39        }
40    }
41
42    /// Check if Ctrl is held.
43    pub fn ctrl(&self) -> bool {
44        self.modifiers.contains(Modifiers::CTRL)
45    }
46
47    /// Check if Alt is held.
48    pub fn alt(&self) -> bool {
49        self.modifiers.contains(Modifiers::ALT)
50    }
51
52    /// Check if Shift is held.
53    pub fn shift(&self) -> bool {
54        self.modifiers.contains(Modifiers::SHIFT)
55    }
56}
57
58/// A key code.
59#[derive(Clone, Debug, PartialEq, Eq, Hash)]
60#[non_exhaustive]
61pub enum KeyCode {
62    /// A character key.
63    Char(char),
64    /// Enter / Return.
65    Enter,
66    /// Tab.
67    Tab,
68    /// Backspace.
69    Backspace,
70    /// Delete.
71    Delete,
72    /// Escape.
73    Escape,
74    /// Arrow up.
75    Up,
76    /// Arrow down.
77    Down,
78    /// Arrow left.
79    Left,
80    /// Arrow right.
81    Right,
82    /// Home.
83    Home,
84    /// End.
85    End,
86    /// Page up.
87    PageUp,
88    /// Page down.
89    PageDown,
90    /// Insert.
91    Insert,
92    /// Function key (F1-F12).
93    F(u8),
94}
95
96/// Keyboard modifier flags.
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub struct Modifiers(u8);
99
100impl Modifiers {
101    /// No modifiers.
102    pub const NONE: Self = Self(0);
103    /// Shift modifier.
104    pub const SHIFT: Self = Self(1);
105    /// Ctrl modifier.
106    pub const CTRL: Self = Self(2);
107    /// Alt/Option modifier.
108    pub const ALT: Self = Self(4);
109    /// Super/Command modifier.
110    pub const SUPER: Self = Self(8);
111
112    /// Check if this modifier set contains the given modifier.
113    pub const fn contains(self, other: Self) -> bool {
114        (self.0 & other.0) == other.0 && other.0 != 0
115    }
116
117    /// Combine two modifier sets.
118    pub const fn union(self, other: Self) -> Self {
119        Self(self.0 | other.0)
120    }
121}
122
123impl std::ops::BitOr for Modifiers {
124    type Output = Self;
125    fn bitor(self, rhs: Self) -> Self {
126        Self(self.0 | rhs.0)
127    }
128}
129
130/// The kind of mouse event.
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132#[non_exhaustive]
133pub enum MouseEventKind {
134    /// A button was pressed.
135    Press,
136    /// A button was released.
137    Release,
138    /// The mouse was moved (while a button is held).
139    Drag,
140    /// The mouse was moved (no button held).
141    Move,
142    /// Scroll up.
143    ScrollUp,
144    /// Scroll down.
145    ScrollDown,
146}
147
148/// A mouse event.
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct MouseEvent {
151    /// The kind of mouse event.
152    pub kind: MouseEventKind,
153    /// Column position (0-based).
154    pub x: u16,
155    /// Row position (0-based).
156    pub y: u16,
157    /// Active modifiers.
158    pub modifiers: Modifiers,
159}
160
161// Crossterm conversions
162
163impl From<crossterm::event::Event> for Event {
164    fn from(ct: crossterm::event::Event) -> Self {
165        match ct {
166            crossterm::event::Event::Key(key) => Event::Key(key.into()),
167            crossterm::event::Event::Mouse(mouse) => Event::Mouse(mouse.into()),
168            crossterm::event::Event::Resize(w, h) => Event::Resize(w, h),
169            crossterm::event::Event::Paste(text) => Event::Paste(text),
170            _ => Event::Key(KeyEvent::plain(KeyCode::Escape)), // fallback for FocusGained/Lost
171        }
172    }
173}
174
175impl From<crossterm::event::KeyEvent> for KeyEvent {
176    fn from(ct: crossterm::event::KeyEvent) -> Self {
177        Self {
178            code: ct.code.into(),
179            modifiers: ct.modifiers.into(),
180        }
181    }
182}
183
184impl From<crossterm::event::KeyCode> for KeyCode {
185    fn from(ct: crossterm::event::KeyCode) -> Self {
186        match ct {
187            crossterm::event::KeyCode::Char(c) => KeyCode::Char(c),
188            crossterm::event::KeyCode::Enter => KeyCode::Enter,
189            crossterm::event::KeyCode::Tab => KeyCode::Tab,
190            crossterm::event::KeyCode::Backspace => KeyCode::Backspace,
191            crossterm::event::KeyCode::Delete => KeyCode::Delete,
192            crossterm::event::KeyCode::Esc => KeyCode::Escape,
193            crossterm::event::KeyCode::Up => KeyCode::Up,
194            crossterm::event::KeyCode::Down => KeyCode::Down,
195            crossterm::event::KeyCode::Left => KeyCode::Left,
196            crossterm::event::KeyCode::Right => KeyCode::Right,
197            crossterm::event::KeyCode::Home => KeyCode::Home,
198            crossterm::event::KeyCode::End => KeyCode::End,
199            crossterm::event::KeyCode::PageUp => KeyCode::PageUp,
200            crossterm::event::KeyCode::PageDown => KeyCode::PageDown,
201            crossterm::event::KeyCode::Insert => KeyCode::Insert,
202            crossterm::event::KeyCode::F(n) => KeyCode::F(n),
203            _ => KeyCode::Escape, // fallback
204        }
205    }
206}
207
208impl From<crossterm::event::KeyModifiers> for Modifiers {
209    fn from(ct: crossterm::event::KeyModifiers) -> Self {
210        let mut m = Modifiers::NONE;
211        if ct.contains(crossterm::event::KeyModifiers::SHIFT) {
212            m = m | Modifiers::SHIFT;
213        }
214        if ct.contains(crossterm::event::KeyModifiers::CONTROL) {
215            m = m | Modifiers::CTRL;
216        }
217        if ct.contains(crossterm::event::KeyModifiers::ALT) {
218            m = m | Modifiers::ALT;
219        }
220        if ct.contains(crossterm::event::KeyModifiers::SUPER) {
221            m = m | Modifiers::SUPER;
222        }
223        m
224    }
225}
226
227impl From<crossterm::event::MouseEvent> for MouseEvent {
228    fn from(ct: crossterm::event::MouseEvent) -> Self {
229        Self {
230            kind: ct.kind.into(),
231            x: ct.column,
232            y: ct.row,
233            modifiers: ct.modifiers.into(),
234        }
235    }
236}
237
238impl From<crossterm::event::MouseEventKind> for MouseEventKind {
239    fn from(ct: crossterm::event::MouseEventKind) -> Self {
240        match ct {
241            crossterm::event::MouseEventKind::Down(_) => MouseEventKind::Press,
242            crossterm::event::MouseEventKind::Up(_) => MouseEventKind::Release,
243            crossterm::event::MouseEventKind::Drag(_) => MouseEventKind::Drag,
244            crossterm::event::MouseEventKind::Moved => MouseEventKind::Move,
245            crossterm::event::MouseEventKind::ScrollUp => MouseEventKind::ScrollUp,
246            crossterm::event::MouseEventKind::ScrollDown => MouseEventKind::ScrollDown,
247            _ => MouseEventKind::Move, // fallback for ScrollLeft/ScrollRight
248        }
249    }
250}
251
252impl fmt::Display for KeyCode {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        match self {
255            KeyCode::Char(c) => write!(f, "{c}"),
256            KeyCode::Enter => write!(f, "Enter"),
257            KeyCode::Tab => write!(f, "Tab"),
258            KeyCode::Backspace => write!(f, "Backspace"),
259            KeyCode::Delete => write!(f, "Delete"),
260            KeyCode::Escape => write!(f, "Escape"),
261            KeyCode::Up => write!(f, "Up"),
262            KeyCode::Down => write!(f, "Down"),
263            KeyCode::Left => write!(f, "Left"),
264            KeyCode::Right => write!(f, "Right"),
265            KeyCode::Home => write!(f, "Home"),
266            KeyCode::End => write!(f, "End"),
267            KeyCode::PageUp => write!(f, "PageUp"),
268            KeyCode::PageDown => write!(f, "PageDown"),
269            KeyCode::Insert => write!(f, "Insert"),
270            KeyCode::F(n) => write!(f, "F{n}"),
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn key_event_plain() {
281        let k = KeyEvent::plain(KeyCode::Char('a'));
282        assert!(!k.ctrl());
283        assert!(!k.alt());
284        assert!(!k.shift());
285    }
286
287    #[test]
288    fn key_event_with_modifiers() {
289        let k = KeyEvent::new(KeyCode::Char('c'), Modifiers::CTRL);
290        assert!(k.ctrl());
291        assert!(!k.alt());
292    }
293
294    #[test]
295    fn modifier_union() {
296        let m = Modifiers::CTRL | Modifiers::SHIFT;
297        assert!(m.contains(Modifiers::CTRL));
298        assert!(m.contains(Modifiers::SHIFT));
299        assert!(!m.contains(Modifiers::ALT));
300    }
301
302    #[test]
303    fn resize_event() {
304        let e = Event::Resize(80, 24);
305        assert!(matches!(e, Event::Resize(80, 24)));
306    }
307
308    #[test]
309    fn paste_event() {
310        let e = Event::Paste("hello".into());
311        assert!(matches!(e, Event::Paste(ref s) if s == "hello"));
312    }
313
314    #[test]
315    fn mouse_event() {
316        let m = MouseEvent {
317            kind: MouseEventKind::Press,
318            x: 10,
319            y: 5,
320            modifiers: Modifiers::NONE,
321        };
322        assert_eq!(m.kind, MouseEventKind::Press);
323        assert_eq!(m.x, 10);
324        assert_eq!(m.y, 5);
325    }
326
327    #[test]
328    fn keycode_display() {
329        assert_eq!(format!("{}", KeyCode::Char('a')), "a");
330        assert_eq!(format!("{}", KeyCode::Enter), "Enter");
331        assert_eq!(format!("{}", KeyCode::F(1)), "F1");
332    }
333
334    #[test]
335    fn crossterm_key_conversion() {
336        let ct = crossterm::event::KeyEvent::new(
337            crossterm::event::KeyCode::Char('x'),
338            crossterm::event::KeyModifiers::CONTROL,
339        );
340        let k: KeyEvent = ct.into();
341        assert_eq!(k.code, KeyCode::Char('x'));
342        assert!(k.ctrl());
343    }
344
345    #[test]
346    fn crossterm_resize_conversion() {
347        let ct = crossterm::event::Event::Resize(120, 40);
348        let e: Event = ct.into();
349        assert!(matches!(e, Event::Resize(120, 40)));
350    }
351}