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#[non_exhaustive]
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum Event {
21    /// A keyboard event.
22    Key(KeyEvent),
23    /// A mouse event (requires `mouse: true` in [`crate::RunConfig`]).
24    Mouse(MouseEvent),
25    /// The terminal was resized to the given `(columns, rows)`.
26    Resize(u32, u32),
27    /// Pasted text (bracketed paste). May contain newlines.
28    Paste(String),
29    /// The terminal window gained focus.
30    FocusGained,
31    /// The terminal window lost focus. Used to clear hover state.
32    FocusLost,
33}
34
35impl Event {
36    /// Create a key press event for a character.
37    pub fn key_char(c: char) -> Self {
38        Event::Key(KeyEvent {
39            code: KeyCode::Char(c),
40            modifiers: KeyModifiers::NONE,
41            kind: KeyEventKind::Press,
42        })
43    }
44
45    /// Create a key press event for a special key.
46    pub fn key(code: KeyCode) -> Self {
47        Event::Key(KeyEvent {
48            code,
49            modifiers: KeyModifiers::NONE,
50            kind: KeyEventKind::Press,
51        })
52    }
53
54    /// Create a key press event with Ctrl modifier.
55    pub fn key_ctrl(c: char) -> Self {
56        Event::Key(KeyEvent {
57            code: KeyCode::Char(c),
58            modifiers: KeyModifiers::CONTROL,
59            kind: KeyEventKind::Press,
60        })
61    }
62
63    /// Create a key press event with custom modifiers.
64    pub fn key_mod(code: KeyCode, modifiers: KeyModifiers) -> Self {
65        Event::Key(KeyEvent {
66            code,
67            modifiers,
68            kind: KeyEventKind::Press,
69        })
70    }
71
72    /// Create a terminal resize event.
73    pub fn resize(width: u32, height: u32) -> Self {
74        Event::Resize(width, height)
75    }
76
77    /// Create a left mouse click event at (x, y).
78    pub fn mouse_click(x: u32, y: u32) -> Self {
79        Event::Mouse(MouseEvent {
80            kind: MouseKind::Down(MouseButton::Left),
81            x,
82            y,
83            modifiers: KeyModifiers::NONE,
84            pixel_x: None,
85            pixel_y: None,
86        })
87    }
88
89    /// Create a mouse move event at the given position.
90    pub fn mouse_move(x: u32, y: u32) -> Self {
91        Event::Mouse(MouseEvent {
92            kind: MouseKind::Moved,
93            x,
94            y,
95            modifiers: KeyModifiers::NONE,
96            pixel_x: None,
97            pixel_y: None,
98        })
99    }
100
101    /// Create a left mouse drag event at (x, y).
102    pub fn mouse_drag(x: u32, y: u32) -> Self {
103        Event::Mouse(MouseEvent {
104            kind: MouseKind::Drag(MouseButton::Left),
105            x,
106            y,
107            modifiers: KeyModifiers::NONE,
108            pixel_x: None,
109            pixel_y: None,
110        })
111    }
112
113    /// Create a left mouse button release event at (x, y).
114    pub fn mouse_up(x: u32, y: u32) -> Self {
115        Event::Mouse(MouseEvent {
116            kind: MouseKind::Up(MouseButton::Left),
117            x,
118            y,
119            modifiers: KeyModifiers::NONE,
120            pixel_x: None,
121            pixel_y: None,
122        })
123    }
124
125    /// Create a scroll up event at the given position.
126    pub fn scroll_up(x: u32, y: u32) -> Self {
127        Event::Mouse(MouseEvent {
128            kind: MouseKind::ScrollUp,
129            x,
130            y,
131            modifiers: KeyModifiers::NONE,
132            pixel_x: None,
133            pixel_y: None,
134        })
135    }
136
137    /// Create a scroll down event at the given position.
138    pub fn scroll_down(x: u32, y: u32) -> Self {
139        Event::Mouse(MouseEvent {
140            kind: MouseKind::ScrollDown,
141            x,
142            y,
143            modifiers: KeyModifiers::NONE,
144            pixel_x: None,
145            pixel_y: None,
146        })
147    }
148
149    /// Create a key release event for a character.
150    pub fn key_release(c: char) -> Self {
151        Event::Key(KeyEvent {
152            code: KeyCode::Char(c),
153            modifiers: KeyModifiers::NONE,
154            kind: KeyEventKind::Release,
155        })
156    }
157
158    /// Create a paste event with the given text.
159    pub fn paste(text: impl Into<String>) -> Self {
160        Event::Paste(text.into())
161    }
162
163    /// Returns the key event data if this is a `Key` variant.
164    pub fn as_key(&self) -> Option<&KeyEvent> {
165        match self {
166            Event::Key(k) => Some(k),
167            _ => None,
168        }
169    }
170
171    /// Returns the mouse event data if this is a `Mouse` variant.
172    pub fn as_mouse(&self) -> Option<&MouseEvent> {
173        match self {
174            Event::Mouse(m) => Some(m),
175            _ => None,
176        }
177    }
178
179    /// Returns `(columns, rows)` if this is a `Resize` variant.
180    pub fn as_resize(&self) -> Option<(u32, u32)> {
181        match self {
182            Event::Resize(w, h) => Some((*w, *h)),
183            _ => None,
184        }
185    }
186
187    /// Returns the pasted text if this is a `Paste` variant.
188    pub fn as_paste(&self) -> Option<&str> {
189        match self {
190            Event::Paste(s) => Some(s),
191            _ => None,
192        }
193    }
194
195    /// Returns `true` if this is a `Key` event.
196    pub fn is_key(&self) -> bool {
197        matches!(self, Event::Key(_))
198    }
199
200    /// Returns `true` if this is a `Mouse` event.
201    pub fn is_mouse(&self) -> bool {
202        matches!(self, Event::Mouse(_))
203    }
204}
205
206/// A keyboard event with key code and modifiers.
207#[non_exhaustive]
208#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct KeyEvent {
210    /// The key that was pressed.
211    pub code: KeyCode,
212    /// Modifier keys held at the time of the press.
213    pub modifiers: KeyModifiers,
214    /// The type of key event. Always `Press` without Kitty keyboard protocol.
215    pub kind: KeyEventKind,
216}
217
218impl KeyEvent {
219    /// Returns `true` if this is a press of the given character (no modifiers).
220    pub fn is_char(&self, c: char) -> bool {
221        self.code == KeyCode::Char(c)
222            && self.modifiers == KeyModifiers::NONE
223            && self.kind == KeyEventKind::Press
224    }
225
226    /// Returns `true` if this is Ctrl+`c`.
227    pub fn is_ctrl_char(&self, c: char) -> bool {
228        self.code == KeyCode::Char(c)
229            && self.modifiers == KeyModifiers::CONTROL
230            && self.kind == KeyEventKind::Press
231    }
232
233    /// Returns `true` if this is a press of the given key code (no modifiers).
234    pub fn is_code(&self, code: KeyCode) -> bool {
235        self.code == code
236            && self.modifiers == KeyModifiers::NONE
237            && self.kind == KeyEventKind::Press
238    }
239}
240
241/// The type of key event.
242#[non_exhaustive]
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub enum KeyEventKind {
245    /// Key was pressed.
246    Press,
247    /// Key was released (requires Kitty keyboard protocol).
248    Release,
249    /// Key is being held/repeated (requires Kitty keyboard protocol).
250    Repeat,
251}
252
253/// Key identifier.
254///
255/// Covers printable characters, control keys, arrow keys, function keys,
256/// and navigation keys. Unrecognized keys are silently dropped by the
257/// crossterm conversion layer.
258#[non_exhaustive]
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub enum KeyCode {
261    /// A printable character (letter, digit, symbol, space, etc.).
262    Char(char),
263    /// Enter / Return key.
264    Enter,
265    /// Backspace key.
266    Backspace,
267    /// Tab key (forward tab).
268    Tab,
269    /// Shift+Tab (back tab).
270    BackTab,
271    /// Escape key.
272    Esc,
273    /// Up arrow key.
274    Up,
275    /// Down arrow key.
276    Down,
277    /// Left arrow key.
278    Left,
279    /// Right arrow key.
280    Right,
281    /// Home key.
282    Home,
283    /// End key.
284    End,
285    /// Page Up key.
286    PageUp,
287    /// Page Down key.
288    PageDown,
289    /// Delete (forward delete) key.
290    Delete,
291    /// Insert key.
292    Insert,
293    /// Null key (Ctrl+Space on some terminals).
294    Null,
295    /// Caps Lock key (Kitty keyboard protocol only).
296    CapsLock,
297    /// Scroll Lock key (Kitty keyboard protocol only).
298    ScrollLock,
299    /// Num Lock key (Kitty keyboard protocol only).
300    NumLock,
301    /// Print Screen key (Kitty keyboard protocol only).
302    PrintScreen,
303    /// Pause/Break key (Kitty keyboard protocol only).
304    Pause,
305    /// Menu / context menu key.
306    Menu,
307    /// Keypad center key (numpad 5 without NumLock).
308    KeypadBegin,
309    /// Function key `F1`..`F12` (and beyond). The inner `u8` is the number.
310    F(u8),
311}
312
313/// Modifier keys held during a key press.
314///
315/// Stored as bitflags in a `u8`. Check individual modifiers with
316/// [`KeyModifiers::contains`].
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
318pub struct KeyModifiers(pub u8);
319
320impl KeyModifiers {
321    /// No modifier keys held.
322    pub const NONE: Self = Self(0);
323    /// Shift key held.
324    pub const SHIFT: Self = Self(1 << 0);
325    /// Control key held.
326    pub const CONTROL: Self = Self(1 << 1);
327    /// Alt / Option key held.
328    pub const ALT: Self = Self(1 << 2);
329    /// Super key (Cmd on macOS, Win on Windows). Kitty keyboard protocol only.
330    pub const SUPER: Self = Self(1 << 3);
331    /// Hyper modifier. Kitty keyboard protocol only.
332    pub const HYPER: Self = Self(1 << 4);
333    /// Meta modifier. Kitty keyboard protocol only.
334    pub const META: Self = Self(1 << 5);
335
336    /// Returns `true` if all bits in `other` are set in `self`.
337    #[inline]
338    pub fn contains(self, other: Self) -> bool {
339        (self.0 & other.0) == other.0
340    }
341}
342
343/// A mouse event with position and kind.
344///
345/// Coordinates are zero-based terminal columns (`x`) and rows (`y`).
346/// `pixel_x` and `pixel_y` are reserved for future sub-cell precision
347/// (e.g. Kitty pixel mouse protocol, WASM backend); they are currently
348/// always `None` with the crossterm backend, since SGR 1006 only reports
349/// cell coordinates and crossterm 0.28 has no pixel mouse fields.
350/// Mouse events are only produced when `mouse: true` is set in
351/// [`crate::RunConfig`].
352#[non_exhaustive]
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub struct MouseEvent {
355    /// The type of mouse action that occurred.
356    pub kind: MouseKind,
357    /// Column (horizontal position), zero-based.
358    pub x: u32,
359    /// Row (vertical position), zero-based.
360    pub y: u32,
361    /// Modifier keys held at the time of the event.
362    pub modifiers: KeyModifiers,
363    /// Pixel-level x coordinate (reserved).
364    ///
365    /// Currently always `None` with the crossterm backend; populated only
366    /// when a Kitty-capable or WASM backend provides sub-cell precision.
367    pub pixel_x: Option<u16>,
368    /// Pixel-level y coordinate (reserved).
369    ///
370    /// Currently always `None` with the crossterm backend; populated only
371    /// when a Kitty-capable or WASM backend provides sub-cell precision.
372    pub pixel_y: Option<u16>,
373}
374
375impl MouseEvent {
376    /// Create a new MouseEvent with all fields.
377    pub fn new(
378        kind: MouseKind,
379        x: u32,
380        y: u32,
381        modifiers: KeyModifiers,
382        pixel_x: Option<u16>,
383        pixel_y: Option<u16>,
384    ) -> Self {
385        Self {
386            kind,
387            x,
388            y,
389            modifiers,
390            pixel_x,
391            pixel_y,
392        }
393    }
394
395    /// Returns true if this is a scroll event.
396    pub fn is_scroll(&self) -> bool {
397        matches!(
398            self.kind,
399            MouseKind::ScrollUp
400                | MouseKind::ScrollDown
401                | MouseKind::ScrollLeft
402                | MouseKind::ScrollRight
403        )
404    }
405}
406
407/// The type of mouse event.
408#[non_exhaustive]
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub enum MouseKind {
411    /// A mouse button was pressed.
412    Down(MouseButton),
413    /// A mouse button was released.
414    Up(MouseButton),
415    /// The mouse was moved while a button was held.
416    Drag(MouseButton),
417    /// The scroll wheel was rotated upward.
418    ScrollUp,
419    /// The scroll wheel was rotated downward.
420    ScrollDown,
421    /// The scroll wheel was rotated leftward (horizontal scroll).
422    ScrollLeft,
423    /// The scroll wheel was rotated rightward (horizontal scroll).
424    ScrollRight,
425    /// The mouse was moved without any button held.
426    Moved,
427}
428
429/// Mouse button identifier.
430#[non_exhaustive]
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432pub enum MouseButton {
433    /// Primary (left) mouse button.
434    Left,
435    /// Secondary (right) mouse button.
436    Right,
437    /// Middle mouse button (scroll wheel click).
438    Middle,
439}
440
441#[cfg(feature = "crossterm")]
442fn convert_modifiers(modifiers: crossterm_event::KeyModifiers) -> KeyModifiers {
443    let mut out = KeyModifiers::NONE;
444    if modifiers.contains(crossterm_event::KeyModifiers::SHIFT) {
445        out.0 |= KeyModifiers::SHIFT.0;
446    }
447    if modifiers.contains(crossterm_event::KeyModifiers::CONTROL) {
448        out.0 |= KeyModifiers::CONTROL.0;
449    }
450    if modifiers.contains(crossterm_event::KeyModifiers::ALT) {
451        out.0 |= KeyModifiers::ALT.0;
452    }
453    if modifiers.contains(crossterm_event::KeyModifiers::SUPER) {
454        out.0 |= KeyModifiers::SUPER.0;
455    }
456    if modifiers.contains(crossterm_event::KeyModifiers::HYPER) {
457        out.0 |= KeyModifiers::HYPER.0;
458    }
459    if modifiers.contains(crossterm_event::KeyModifiers::META) {
460        out.0 |= KeyModifiers::META.0;
461    }
462    out
463}
464
465#[cfg(feature = "crossterm")]
466fn convert_button(button: crossterm_event::MouseButton) -> MouseButton {
467    match button {
468        crossterm_event::MouseButton::Left => MouseButton::Left,
469        crossterm_event::MouseButton::Right => MouseButton::Right,
470        crossterm_event::MouseButton::Middle => MouseButton::Middle,
471    }
472}
473
474// ── crossterm conversions ────────────────────────────────────────────
475
476/// Convert a raw crossterm event into our lightweight [`Event`].
477/// Returns `None` for event kinds we don't handle.
478#[cfg(feature = "crossterm")]
479pub(crate) fn from_crossterm(raw: crossterm_event::Event) -> Option<Event> {
480    match raw {
481        crossterm_event::Event::Key(k) => {
482            let code = match k.code {
483                crossterm_event::KeyCode::Char(c) => KeyCode::Char(c),
484                crossterm_event::KeyCode::Enter => KeyCode::Enter,
485                crossterm_event::KeyCode::Backspace => KeyCode::Backspace,
486                crossterm_event::KeyCode::Tab => KeyCode::Tab,
487                crossterm_event::KeyCode::BackTab => KeyCode::BackTab,
488                crossterm_event::KeyCode::Esc => KeyCode::Esc,
489                crossterm_event::KeyCode::Up => KeyCode::Up,
490                crossterm_event::KeyCode::Down => KeyCode::Down,
491                crossterm_event::KeyCode::Left => KeyCode::Left,
492                crossterm_event::KeyCode::Right => KeyCode::Right,
493                crossterm_event::KeyCode::Home => KeyCode::Home,
494                crossterm_event::KeyCode::End => KeyCode::End,
495                crossterm_event::KeyCode::PageUp => KeyCode::PageUp,
496                crossterm_event::KeyCode::PageDown => KeyCode::PageDown,
497                crossterm_event::KeyCode::Delete => KeyCode::Delete,
498                crossterm_event::KeyCode::Insert => KeyCode::Insert,
499                crossterm_event::KeyCode::Null => KeyCode::Null,
500                crossterm_event::KeyCode::CapsLock => KeyCode::CapsLock,
501                crossterm_event::KeyCode::ScrollLock => KeyCode::ScrollLock,
502                crossterm_event::KeyCode::NumLock => KeyCode::NumLock,
503                crossterm_event::KeyCode::PrintScreen => KeyCode::PrintScreen,
504                crossterm_event::KeyCode::Pause => KeyCode::Pause,
505                crossterm_event::KeyCode::Menu => KeyCode::Menu,
506                crossterm_event::KeyCode::KeypadBegin => KeyCode::KeypadBegin,
507                crossterm_event::KeyCode::F(n) => KeyCode::F(n),
508                _ => return None,
509            };
510            let modifiers = convert_modifiers(k.modifiers);
511            let kind = match k.kind {
512                crossterm_event::KeyEventKind::Press => KeyEventKind::Press,
513                crossterm_event::KeyEventKind::Repeat => KeyEventKind::Repeat,
514                crossterm_event::KeyEventKind::Release => KeyEventKind::Release,
515            };
516            Some(Event::Key(KeyEvent {
517                code,
518                modifiers,
519                kind,
520            }))
521        }
522        crossterm_event::Event::Mouse(m) => {
523            let kind = match m.kind {
524                crossterm_event::MouseEventKind::Down(btn) => MouseKind::Down(convert_button(btn)),
525                crossterm_event::MouseEventKind::Up(btn) => MouseKind::Up(convert_button(btn)),
526                crossterm_event::MouseEventKind::Drag(btn) => MouseKind::Drag(convert_button(btn)),
527                crossterm_event::MouseEventKind::Moved => MouseKind::Moved,
528                crossterm_event::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
529                crossterm_event::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
530                crossterm_event::MouseEventKind::ScrollLeft => MouseKind::ScrollLeft,
531                crossterm_event::MouseEventKind::ScrollRight => MouseKind::ScrollRight,
532            };
533
534            Some(Event::Mouse(MouseEvent {
535                kind,
536                x: m.column as u32,
537                y: m.row as u32,
538                modifiers: convert_modifiers(m.modifiers),
539                pixel_x: None,
540                pixel_y: None,
541            }))
542        }
543        crossterm_event::Event::Resize(cols, rows) => Some(Event::Resize(cols as u32, rows as u32)),
544        crossterm_event::Event::Paste(mut s) => {
545            // Defense-in-depth: cap bracketed-paste payload to bound memory and
546            // prevent O(n²) handling in text widgets. 1 MiB is far more than any
547            // reasonable interactive paste; larger payloads are truncated on a
548            // char boundary with an ellipsis so the user sees that it was cut.
549            const MAX_PASTE_BYTES: usize = 1 << 20;
550            if s.len() > MAX_PASTE_BYTES {
551                let mut end = MAX_PASTE_BYTES;
552                while end > 0 && !s.is_char_boundary(end) {
553                    end -= 1;
554                }
555                s.truncate(end);
556                s.push('…');
557            }
558            Some(Event::Paste(s))
559        }
560        crossterm_event::Event::FocusGained => Some(Event::FocusGained),
561        crossterm_event::Event::FocusLost => Some(Event::FocusLost),
562    }
563}
564
565#[cfg(test)]
566mod event_constructor_tests {
567    use super::*;
568
569    #[test]
570    fn test_key_char() {
571        let e = Event::key_char('q');
572        if let Event::Key(k) = e {
573            assert!(matches!(k.code, KeyCode::Char('q')));
574            assert_eq!(k.modifiers, KeyModifiers::NONE);
575            assert!(matches!(k.kind, KeyEventKind::Press));
576        } else {
577            panic!("Expected Key event");
578        }
579    }
580
581    #[test]
582    fn test_key() {
583        let e = Event::key(KeyCode::Enter);
584        if let Event::Key(k) = e {
585            assert!(matches!(k.code, KeyCode::Enter));
586            assert_eq!(k.modifiers, KeyModifiers::NONE);
587            assert!(matches!(k.kind, KeyEventKind::Press));
588        } else {
589            panic!("Expected Key event");
590        }
591    }
592
593    #[test]
594    fn test_key_ctrl() {
595        let e = Event::key_ctrl('s');
596        if let Event::Key(k) = e {
597            assert!(matches!(k.code, KeyCode::Char('s')));
598            assert_eq!(k.modifiers, KeyModifiers::CONTROL);
599            assert!(matches!(k.kind, KeyEventKind::Press));
600        } else {
601            panic!("Expected Key event");
602        }
603    }
604
605    #[test]
606    fn test_key_mod() {
607        let modifiers = KeyModifiers(KeyModifiers::SHIFT.0 | KeyModifiers::ALT.0);
608        let e = Event::key_mod(KeyCode::Tab, modifiers);
609        if let Event::Key(k) = e {
610            assert!(matches!(k.code, KeyCode::Tab));
611            assert_eq!(k.modifiers, modifiers);
612            assert!(matches!(k.kind, KeyEventKind::Press));
613        } else {
614            panic!("Expected Key event");
615        }
616    }
617
618    #[test]
619    fn test_resize() {
620        let e = Event::resize(80, 24);
621        assert!(matches!(e, Event::Resize(80, 24)));
622    }
623
624    #[test]
625    fn test_mouse_click() {
626        let e = Event::mouse_click(10, 5);
627        if let Event::Mouse(m) = e {
628            assert!(matches!(m.kind, MouseKind::Down(MouseButton::Left)));
629            assert_eq!(m.x, 10);
630            assert_eq!(m.y, 5);
631            assert_eq!(m.modifiers, KeyModifiers::NONE);
632        } else {
633            panic!("Expected Mouse event");
634        }
635    }
636
637    #[test]
638    fn test_mouse_move() {
639        let e = Event::mouse_move(10, 5);
640        if let Event::Mouse(m) = e {
641            assert!(matches!(m.kind, MouseKind::Moved));
642            assert_eq!(m.x, 10);
643            assert_eq!(m.y, 5);
644            assert_eq!(m.modifiers, KeyModifiers::NONE);
645        } else {
646            panic!("Expected Mouse event");
647        }
648    }
649
650    #[test]
651    fn test_paste() {
652        let e = Event::paste("hello");
653        assert!(matches!(e, Event::Paste(s) if s == "hello"));
654    }
655}