Skip to main content

slt/
event.rs

1//! Terminal input events.
2//!
3//! This module defines the event types that SLT delivers to your UI closure
4//! each frame: keyboard, mouse, resize, paste, and focus events. In most
5//! cases you'll use the convenience methods on [`crate::Context`] (e.g.,
6//! [`Context::key`](crate::Context::key),
7//! [`Context::mouse_down`](crate::Context::mouse_down)) instead of matching
8//! on these types directly.
9
10#[cfg(feature = "crossterm")]
11use crossterm::event as crossterm_event;
12
13/// A terminal input event.
14///
15/// Produced each frame by the run loop and passed to your UI closure via
16/// [`crate::Context`]. Use the helper methods on `Context` (e.g., `key()`,
17/// `key_code()`) rather than matching on this type directly.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum Event {
20    /// A keyboard event.
21    Key(KeyEvent),
22    /// A mouse event (requires `mouse: true` in [`crate::RunConfig`]).
23    Mouse(MouseEvent),
24    /// The terminal was resized to the given `(columns, rows)`.
25    Resize(u32, u32),
26    /// Pasted text (bracketed paste). May contain newlines.
27    Paste(String),
28    /// The terminal window gained focus.
29    FocusGained,
30    /// The terminal window lost focus. Used to clear hover state.
31    FocusLost,
32}
33
34impl Event {
35    /// Create a key press event for a character.
36    pub fn key_char(c: char) -> Self {
37        Event::Key(KeyEvent {
38            code: KeyCode::Char(c),
39            modifiers: KeyModifiers::NONE,
40            kind: KeyEventKind::Press,
41        })
42    }
43
44    /// Create a key press event for a special key.
45    pub fn key(code: KeyCode) -> Self {
46        Event::Key(KeyEvent {
47            code,
48            modifiers: KeyModifiers::NONE,
49            kind: KeyEventKind::Press,
50        })
51    }
52
53    /// Create a key press event with Ctrl modifier.
54    pub fn key_ctrl(c: char) -> Self {
55        Event::Key(KeyEvent {
56            code: KeyCode::Char(c),
57            modifiers: KeyModifiers::CONTROL,
58            kind: KeyEventKind::Press,
59        })
60    }
61
62    /// Create a key press event with custom modifiers.
63    pub fn key_mod(code: KeyCode, modifiers: KeyModifiers) -> Self {
64        Event::Key(KeyEvent {
65            code,
66            modifiers,
67            kind: KeyEventKind::Press,
68        })
69    }
70
71    /// Create a terminal resize event.
72    pub fn resize(width: u32, height: u32) -> Self {
73        Event::Resize(width, height)
74    }
75
76    /// Create a left mouse click event at (x, y).
77    pub fn mouse_click(x: u32, y: u32) -> Self {
78        Event::Mouse(MouseEvent {
79            kind: MouseKind::Down(MouseButton::Left),
80            x,
81            y,
82            modifiers: KeyModifiers::NONE,
83        })
84    }
85
86    /// Create a mouse move event to (x, y).
87    pub fn mouse_move(x: u32, y: u32) -> Self {
88        Event::Mouse(MouseEvent {
89            kind: MouseKind::Moved,
90            x,
91            y,
92            modifiers: KeyModifiers::NONE,
93        })
94    }
95
96    /// Create a paste event with the given text.
97    pub fn paste(text: impl Into<String>) -> Self {
98        Event::Paste(text.into())
99    }
100
101    /// Returns the key event data if this is a `Key` variant.
102    pub fn as_key(&self) -> Option<&KeyEvent> {
103        match self {
104            Event::Key(k) => Some(k),
105            _ => None,
106        }
107    }
108
109    /// Returns the mouse event data if this is a `Mouse` variant.
110    pub fn as_mouse(&self) -> Option<&MouseEvent> {
111        match self {
112            Event::Mouse(m) => Some(m),
113            _ => None,
114        }
115    }
116
117    /// Returns `(columns, rows)` if this is a `Resize` variant.
118    pub fn as_resize(&self) -> Option<(u32, u32)> {
119        match self {
120            Event::Resize(w, h) => Some((*w, *h)),
121            _ => None,
122        }
123    }
124
125    /// Returns the pasted text if this is a `Paste` variant.
126    pub fn as_paste(&self) -> Option<&str> {
127        match self {
128            Event::Paste(s) => Some(s),
129            _ => None,
130        }
131    }
132}
133
134/// A keyboard event with key code and modifiers.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct KeyEvent {
137    /// The key that was pressed.
138    pub code: KeyCode,
139    /// Modifier keys held at the time of the press.
140    pub modifiers: KeyModifiers,
141    /// The type of key event. Always `Press` without Kitty keyboard protocol.
142    pub kind: KeyEventKind,
143}
144
145/// The type of key event.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum KeyEventKind {
148    /// Key was pressed.
149    Press,
150    /// Key was released (requires Kitty keyboard protocol).
151    Release,
152    /// Key is being held/repeated (requires Kitty keyboard protocol).
153    Repeat,
154}
155
156/// Key identifier.
157///
158/// Covers printable characters, control keys, arrow keys, function keys,
159/// and navigation keys. Unrecognized keys are silently dropped by the
160/// crossterm conversion layer.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum KeyCode {
163    /// A printable character (letter, digit, symbol, space, etc.).
164    Char(char),
165    /// Enter / Return key.
166    Enter,
167    /// Backspace key.
168    Backspace,
169    /// Tab key (forward tab).
170    Tab,
171    /// Shift+Tab (back tab).
172    BackTab,
173    /// Escape key.
174    Esc,
175    /// Up arrow key.
176    Up,
177    /// Down arrow key.
178    Down,
179    /// Left arrow key.
180    Left,
181    /// Right arrow key.
182    Right,
183    /// Home key.
184    Home,
185    /// End key.
186    End,
187    /// Page Up key.
188    PageUp,
189    /// Page Down key.
190    PageDown,
191    /// Delete (forward delete) key.
192    Delete,
193    /// Function key `F1`..`F12` (and beyond). The inner `u8` is the number.
194    F(u8),
195}
196
197/// Modifier keys held during a key press.
198///
199/// Stored as bitflags in a `u8`. Check individual modifiers with
200/// [`KeyModifiers::contains`].
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
202pub struct KeyModifiers(pub u8);
203
204impl KeyModifiers {
205    /// No modifier keys held.
206    pub const NONE: Self = Self(0);
207    /// Shift key held.
208    pub const SHIFT: Self = Self(1 << 0);
209    /// Control key held.
210    pub const CONTROL: Self = Self(1 << 1);
211    /// Alt / Option key held.
212    pub const ALT: Self = Self(1 << 2);
213
214    /// Returns `true` if all bits in `other` are set in `self`.
215    #[inline]
216    pub fn contains(self, other: Self) -> bool {
217        (self.0 & other.0) == other.0
218    }
219}
220
221/// A mouse event with position and kind.
222///
223/// Coordinates are zero-based terminal columns (`x`) and rows (`y`).
224/// Mouse events are only produced when `mouse: true` is set in
225/// [`crate::RunConfig`].
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct MouseEvent {
228    /// The type of mouse action that occurred.
229    pub kind: MouseKind,
230    /// Column (horizontal position), zero-based.
231    pub x: u32,
232    /// Row (vertical position), zero-based.
233    pub y: u32,
234    /// Modifier keys held at the time of the event.
235    pub modifiers: KeyModifiers,
236}
237
238/// The type of mouse event.
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub enum MouseKind {
241    /// A mouse button was pressed.
242    Down(MouseButton),
243    /// A mouse button was released.
244    Up(MouseButton),
245    /// The mouse was moved while a button was held.
246    Drag(MouseButton),
247    /// The scroll wheel was rotated upward.
248    ScrollUp,
249    /// The scroll wheel was rotated downward.
250    ScrollDown,
251    /// The mouse was moved without any button held.
252    Moved,
253}
254
255/// Mouse button identifier.
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum MouseButton {
258    /// Primary (left) mouse button.
259    Left,
260    /// Secondary (right) mouse button.
261    Right,
262    /// Middle mouse button (scroll wheel click).
263    Middle,
264}
265
266#[cfg(feature = "crossterm")]
267fn convert_modifiers(modifiers: crossterm_event::KeyModifiers) -> KeyModifiers {
268    let mut out = KeyModifiers::NONE;
269    if modifiers.contains(crossterm_event::KeyModifiers::SHIFT) {
270        out.0 |= KeyModifiers::SHIFT.0;
271    }
272    if modifiers.contains(crossterm_event::KeyModifiers::CONTROL) {
273        out.0 |= KeyModifiers::CONTROL.0;
274    }
275    if modifiers.contains(crossterm_event::KeyModifiers::ALT) {
276        out.0 |= KeyModifiers::ALT.0;
277    }
278    out
279}
280
281#[cfg(feature = "crossterm")]
282fn convert_button(button: crossterm_event::MouseButton) -> MouseButton {
283    match button {
284        crossterm_event::MouseButton::Left => MouseButton::Left,
285        crossterm_event::MouseButton::Right => MouseButton::Right,
286        crossterm_event::MouseButton::Middle => MouseButton::Middle,
287    }
288}
289
290// ── crossterm conversions ────────────────────────────────────────────
291
292/// Convert a raw crossterm event into our lightweight [`Event`].
293/// Returns `None` for event kinds we don't handle.
294#[cfg(feature = "crossterm")]
295pub(crate) fn from_crossterm(raw: crossterm_event::Event) -> Option<Event> {
296    match raw {
297        crossterm_event::Event::Key(k) => {
298            let code = match k.code {
299                crossterm_event::KeyCode::Char(c) => KeyCode::Char(c),
300                crossterm_event::KeyCode::Enter => KeyCode::Enter,
301                crossterm_event::KeyCode::Backspace => KeyCode::Backspace,
302                crossterm_event::KeyCode::Tab => KeyCode::Tab,
303                crossterm_event::KeyCode::BackTab => KeyCode::BackTab,
304                crossterm_event::KeyCode::Esc => KeyCode::Esc,
305                crossterm_event::KeyCode::Up => KeyCode::Up,
306                crossterm_event::KeyCode::Down => KeyCode::Down,
307                crossterm_event::KeyCode::Left => KeyCode::Left,
308                crossterm_event::KeyCode::Right => KeyCode::Right,
309                crossterm_event::KeyCode::Home => KeyCode::Home,
310                crossterm_event::KeyCode::End => KeyCode::End,
311                crossterm_event::KeyCode::PageUp => KeyCode::PageUp,
312                crossterm_event::KeyCode::PageDown => KeyCode::PageDown,
313                crossterm_event::KeyCode::Delete => KeyCode::Delete,
314                crossterm_event::KeyCode::F(n) => KeyCode::F(n),
315                _ => return None,
316            };
317            let modifiers = convert_modifiers(k.modifiers);
318            let kind = match k.kind {
319                crossterm_event::KeyEventKind::Press => KeyEventKind::Press,
320                crossterm_event::KeyEventKind::Repeat => KeyEventKind::Repeat,
321                crossterm_event::KeyEventKind::Release => KeyEventKind::Release,
322            };
323            Some(Event::Key(KeyEvent {
324                code,
325                modifiers,
326                kind,
327            }))
328        }
329        crossterm_event::Event::Mouse(m) => {
330            let kind = match m.kind {
331                crossterm_event::MouseEventKind::Down(btn) => MouseKind::Down(convert_button(btn)),
332                crossterm_event::MouseEventKind::Up(btn) => MouseKind::Up(convert_button(btn)),
333                crossterm_event::MouseEventKind::Drag(btn) => MouseKind::Drag(convert_button(btn)),
334                crossterm_event::MouseEventKind::Moved => MouseKind::Moved,
335                crossterm_event::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
336                crossterm_event::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
337                _ => return None,
338            };
339
340            Some(Event::Mouse(MouseEvent {
341                kind,
342                x: m.column as u32,
343                y: m.row as u32,
344                modifiers: convert_modifiers(m.modifiers),
345            }))
346        }
347        crossterm_event::Event::Resize(cols, rows) => Some(Event::Resize(cols as u32, rows as u32)),
348        crossterm_event::Event::Paste(s) => Some(Event::Paste(s)),
349        crossterm_event::Event::FocusGained => Some(Event::FocusGained),
350        crossterm_event::Event::FocusLost => Some(Event::FocusLost),
351    }
352}
353
354#[cfg(test)]
355mod event_constructor_tests {
356    use super::*;
357
358    #[test]
359    fn test_key_char() {
360        let e = Event::key_char('q');
361        if let Event::Key(k) = e {
362            assert!(matches!(k.code, KeyCode::Char('q')));
363            assert_eq!(k.modifiers, KeyModifiers::NONE);
364            assert!(matches!(k.kind, KeyEventKind::Press));
365        } else {
366            panic!("Expected Key event");
367        }
368    }
369
370    #[test]
371    fn test_key() {
372        let e = Event::key(KeyCode::Enter);
373        if let Event::Key(k) = e {
374            assert!(matches!(k.code, KeyCode::Enter));
375            assert_eq!(k.modifiers, KeyModifiers::NONE);
376            assert!(matches!(k.kind, KeyEventKind::Press));
377        } else {
378            panic!("Expected Key event");
379        }
380    }
381
382    #[test]
383    fn test_key_ctrl() {
384        let e = Event::key_ctrl('s');
385        if let Event::Key(k) = e {
386            assert!(matches!(k.code, KeyCode::Char('s')));
387            assert_eq!(k.modifiers, KeyModifiers::CONTROL);
388            assert!(matches!(k.kind, KeyEventKind::Press));
389        } else {
390            panic!("Expected Key event");
391        }
392    }
393
394    #[test]
395    fn test_key_mod() {
396        let modifiers = KeyModifiers(KeyModifiers::SHIFT.0 | KeyModifiers::ALT.0);
397        let e = Event::key_mod(KeyCode::Tab, modifiers);
398        if let Event::Key(k) = e {
399            assert!(matches!(k.code, KeyCode::Tab));
400            assert_eq!(k.modifiers, modifiers);
401            assert!(matches!(k.kind, KeyEventKind::Press));
402        } else {
403            panic!("Expected Key event");
404        }
405    }
406
407    #[test]
408    fn test_resize() {
409        let e = Event::resize(80, 24);
410        assert!(matches!(e, Event::Resize(80, 24)));
411    }
412
413    #[test]
414    fn test_mouse_click() {
415        let e = Event::mouse_click(10, 5);
416        if let Event::Mouse(m) = e {
417            assert!(matches!(m.kind, MouseKind::Down(MouseButton::Left)));
418            assert_eq!(m.x, 10);
419            assert_eq!(m.y, 5);
420            assert_eq!(m.modifiers, KeyModifiers::NONE);
421        } else {
422            panic!("Expected Mouse event");
423        }
424    }
425
426    #[test]
427    fn test_mouse_move() {
428        let e = Event::mouse_move(10, 5);
429        if let Event::Mouse(m) = e {
430            assert!(matches!(m.kind, MouseKind::Moved));
431            assert_eq!(m.x, 10);
432            assert_eq!(m.y, 5);
433            assert_eq!(m.modifiers, KeyModifiers::NONE);
434        } else {
435            panic!("Expected Mouse event");
436        }
437    }
438
439    #[test]
440    fn test_paste() {
441        let e = Event::paste("hello");
442        assert!(matches!(e, Event::Paste(s) if s == "hello"));
443    }
444}