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
102/// A keyboard event with key code and modifiers.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct KeyEvent {
105    /// The key that was pressed.
106    pub code: KeyCode,
107    /// Modifier keys held at the time of the press.
108    pub modifiers: KeyModifiers,
109    /// The type of key event. Always `Press` without Kitty keyboard protocol.
110    pub kind: KeyEventKind,
111}
112
113/// The type of key event.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum KeyEventKind {
116    /// Key was pressed.
117    Press,
118    /// Key was released (requires Kitty keyboard protocol).
119    Release,
120    /// Key is being held/repeated (requires Kitty keyboard protocol).
121    Repeat,
122}
123
124/// Key identifier.
125///
126/// Covers printable characters, control keys, arrow keys, function keys,
127/// and navigation keys. Unrecognized keys are silently dropped by the
128/// crossterm conversion layer.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum KeyCode {
131    /// A printable character (letter, digit, symbol, space, etc.).
132    Char(char),
133    /// Enter / Return key.
134    Enter,
135    /// Backspace key.
136    Backspace,
137    /// Tab key (forward tab).
138    Tab,
139    /// Shift+Tab (back tab).
140    BackTab,
141    /// Escape key.
142    Esc,
143    /// Up arrow key.
144    Up,
145    /// Down arrow key.
146    Down,
147    /// Left arrow key.
148    Left,
149    /// Right arrow key.
150    Right,
151    /// Home key.
152    Home,
153    /// End key.
154    End,
155    /// Page Up key.
156    PageUp,
157    /// Page Down key.
158    PageDown,
159    /// Delete (forward delete) key.
160    Delete,
161    /// Function key `F1`..`F12` (and beyond). The inner `u8` is the number.
162    F(u8),
163}
164
165/// Modifier keys held during a key press.
166///
167/// Stored as bitflags in a `u8`. Check individual modifiers with
168/// [`KeyModifiers::contains`].
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
170pub struct KeyModifiers(pub u8);
171
172impl KeyModifiers {
173    /// No modifier keys held.
174    pub const NONE: Self = Self(0);
175    /// Shift key held.
176    pub const SHIFT: Self = Self(1 << 0);
177    /// Control key held.
178    pub const CONTROL: Self = Self(1 << 1);
179    /// Alt / Option key held.
180    pub const ALT: Self = Self(1 << 2);
181
182    /// Returns `true` if all bits in `other` are set in `self`.
183    #[inline]
184    pub fn contains(self, other: Self) -> bool {
185        (self.0 & other.0) == other.0
186    }
187}
188
189/// A mouse event with position and kind.
190///
191/// Coordinates are zero-based terminal columns (`x`) and rows (`y`).
192/// Mouse events are only produced when `mouse: true` is set in
193/// [`crate::RunConfig`].
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct MouseEvent {
196    /// The type of mouse action that occurred.
197    pub kind: MouseKind,
198    /// Column (horizontal position), zero-based.
199    pub x: u32,
200    /// Row (vertical position), zero-based.
201    pub y: u32,
202    /// Modifier keys held at the time of the event.
203    pub modifiers: KeyModifiers,
204}
205
206/// The type of mouse event.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum MouseKind {
209    /// A mouse button was pressed.
210    Down(MouseButton),
211    /// A mouse button was released.
212    Up(MouseButton),
213    /// The mouse was moved while a button was held.
214    Drag(MouseButton),
215    /// The scroll wheel was rotated upward.
216    ScrollUp,
217    /// The scroll wheel was rotated downward.
218    ScrollDown,
219    /// The mouse was moved without any button held.
220    Moved,
221}
222
223/// Mouse button identifier.
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum MouseButton {
226    /// Primary (left) mouse button.
227    Left,
228    /// Secondary (right) mouse button.
229    Right,
230    /// Middle mouse button (scroll wheel click).
231    Middle,
232}
233
234#[cfg(feature = "crossterm")]
235fn convert_modifiers(modifiers: crossterm_event::KeyModifiers) -> KeyModifiers {
236    let mut out = KeyModifiers::NONE;
237    if modifiers.contains(crossterm_event::KeyModifiers::SHIFT) {
238        out.0 |= KeyModifiers::SHIFT.0;
239    }
240    if modifiers.contains(crossterm_event::KeyModifiers::CONTROL) {
241        out.0 |= KeyModifiers::CONTROL.0;
242    }
243    if modifiers.contains(crossterm_event::KeyModifiers::ALT) {
244        out.0 |= KeyModifiers::ALT.0;
245    }
246    out
247}
248
249#[cfg(feature = "crossterm")]
250fn convert_button(button: crossterm_event::MouseButton) -> MouseButton {
251    match button {
252        crossterm_event::MouseButton::Left => MouseButton::Left,
253        crossterm_event::MouseButton::Right => MouseButton::Right,
254        crossterm_event::MouseButton::Middle => MouseButton::Middle,
255    }
256}
257
258// ── crossterm conversions ────────────────────────────────────────────
259
260/// Convert a raw crossterm event into our lightweight [`Event`].
261/// Returns `None` for event kinds we don't handle.
262#[cfg(feature = "crossterm")]
263pub(crate) fn from_crossterm(raw: crossterm_event::Event) -> Option<Event> {
264    match raw {
265        crossterm_event::Event::Key(k) => {
266            let code = match k.code {
267                crossterm_event::KeyCode::Char(c) => KeyCode::Char(c),
268                crossterm_event::KeyCode::Enter => KeyCode::Enter,
269                crossterm_event::KeyCode::Backspace => KeyCode::Backspace,
270                crossterm_event::KeyCode::Tab => KeyCode::Tab,
271                crossterm_event::KeyCode::BackTab => KeyCode::BackTab,
272                crossterm_event::KeyCode::Esc => KeyCode::Esc,
273                crossterm_event::KeyCode::Up => KeyCode::Up,
274                crossterm_event::KeyCode::Down => KeyCode::Down,
275                crossterm_event::KeyCode::Left => KeyCode::Left,
276                crossterm_event::KeyCode::Right => KeyCode::Right,
277                crossterm_event::KeyCode::Home => KeyCode::Home,
278                crossterm_event::KeyCode::End => KeyCode::End,
279                crossterm_event::KeyCode::PageUp => KeyCode::PageUp,
280                crossterm_event::KeyCode::PageDown => KeyCode::PageDown,
281                crossterm_event::KeyCode::Delete => KeyCode::Delete,
282                crossterm_event::KeyCode::F(n) => KeyCode::F(n),
283                _ => return None,
284            };
285            let modifiers = convert_modifiers(k.modifiers);
286            let kind = match k.kind {
287                crossterm_event::KeyEventKind::Press => KeyEventKind::Press,
288                crossterm_event::KeyEventKind::Repeat => KeyEventKind::Repeat,
289                crossterm_event::KeyEventKind::Release => KeyEventKind::Release,
290            };
291            Some(Event::Key(KeyEvent {
292                code,
293                modifiers,
294                kind,
295            }))
296        }
297        crossterm_event::Event::Mouse(m) => {
298            let kind = match m.kind {
299                crossterm_event::MouseEventKind::Down(btn) => MouseKind::Down(convert_button(btn)),
300                crossterm_event::MouseEventKind::Up(btn) => MouseKind::Up(convert_button(btn)),
301                crossterm_event::MouseEventKind::Drag(btn) => MouseKind::Drag(convert_button(btn)),
302                crossterm_event::MouseEventKind::Moved => MouseKind::Moved,
303                crossterm_event::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
304                crossterm_event::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
305                _ => return None,
306            };
307
308            Some(Event::Mouse(MouseEvent {
309                kind,
310                x: m.column as u32,
311                y: m.row as u32,
312                modifiers: convert_modifiers(m.modifiers),
313            }))
314        }
315        crossterm_event::Event::Resize(cols, rows) => Some(Event::Resize(cols as u32, rows as u32)),
316        crossterm_event::Event::Paste(s) => Some(Event::Paste(s)),
317        crossterm_event::Event::FocusGained => Some(Event::FocusGained),
318        crossterm_event::Event::FocusLost => Some(Event::FocusLost),
319    }
320}
321
322#[cfg(test)]
323mod event_constructor_tests {
324    use super::*;
325
326    #[test]
327    fn test_key_char() {
328        let e = Event::key_char('q');
329        if let Event::Key(k) = e {
330            assert!(matches!(k.code, KeyCode::Char('q')));
331            assert_eq!(k.modifiers, KeyModifiers::NONE);
332            assert!(matches!(k.kind, KeyEventKind::Press));
333        } else {
334            panic!("Expected Key event");
335        }
336    }
337
338    #[test]
339    fn test_key() {
340        let e = Event::key(KeyCode::Enter);
341        if let Event::Key(k) = e {
342            assert!(matches!(k.code, KeyCode::Enter));
343            assert_eq!(k.modifiers, KeyModifiers::NONE);
344            assert!(matches!(k.kind, KeyEventKind::Press));
345        } else {
346            panic!("Expected Key event");
347        }
348    }
349
350    #[test]
351    fn test_key_ctrl() {
352        let e = Event::key_ctrl('s');
353        if let Event::Key(k) = e {
354            assert!(matches!(k.code, KeyCode::Char('s')));
355            assert_eq!(k.modifiers, KeyModifiers::CONTROL);
356            assert!(matches!(k.kind, KeyEventKind::Press));
357        } else {
358            panic!("Expected Key event");
359        }
360    }
361
362    #[test]
363    fn test_key_mod() {
364        let modifiers = KeyModifiers(KeyModifiers::SHIFT.0 | KeyModifiers::ALT.0);
365        let e = Event::key_mod(KeyCode::Tab, modifiers);
366        if let Event::Key(k) = e {
367            assert!(matches!(k.code, KeyCode::Tab));
368            assert_eq!(k.modifiers, modifiers);
369            assert!(matches!(k.kind, KeyEventKind::Press));
370        } else {
371            panic!("Expected Key event");
372        }
373    }
374
375    #[test]
376    fn test_resize() {
377        let e = Event::resize(80, 24);
378        assert!(matches!(e, Event::Resize(80, 24)));
379    }
380
381    #[test]
382    fn test_mouse_click() {
383        let e = Event::mouse_click(10, 5);
384        if let Event::Mouse(m) = e {
385            assert!(matches!(m.kind, MouseKind::Down(MouseButton::Left)));
386            assert_eq!(m.x, 10);
387            assert_eq!(m.y, 5);
388            assert_eq!(m.modifiers, KeyModifiers::NONE);
389        } else {
390            panic!("Expected Mouse event");
391        }
392    }
393
394    #[test]
395    fn test_mouse_move() {
396        let e = Event::mouse_move(10, 5);
397        if let Event::Mouse(m) = e {
398            assert!(matches!(m.kind, MouseKind::Moved));
399            assert_eq!(m.x, 10);
400            assert_eq!(m.y, 5);
401            assert_eq!(m.modifiers, KeyModifiers::NONE);
402        } else {
403            panic!("Expected Mouse event");
404        }
405    }
406
407    #[test]
408    fn test_paste() {
409        let e = Event::paste("hello");
410        assert!(matches!(e, Event::Paste(s) if s == "hello"));
411    }
412}