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/// When the terminal supports pixel-level reporting (e.g. Kitty, or WASM),
347/// `pixel_x` and `pixel_y` contain the sub-cell position in pixels.
348/// Mouse events are only produced when `mouse: true` is set in
349/// [`crate::RunConfig`].
350#[non_exhaustive]
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct MouseEvent {
353    /// The type of mouse action that occurred.
354    pub kind: MouseKind,
355    /// Column (horizontal position), zero-based.
356    pub x: u32,
357    /// Row (vertical position), zero-based.
358    pub y: u32,
359    /// Modifier keys held at the time of the event.
360    pub modifiers: KeyModifiers,
361    /// Pixel-level x coordinate, if available.
362    pub pixel_x: Option<u16>,
363    /// Pixel-level y coordinate, if available.
364    pub pixel_y: Option<u16>,
365}
366
367impl MouseEvent {
368    /// Create a new MouseEvent with all fields.
369    pub fn new(
370        kind: MouseKind,
371        x: u32,
372        y: u32,
373        modifiers: KeyModifiers,
374        pixel_x: Option<u16>,
375        pixel_y: Option<u16>,
376    ) -> Self {
377        Self {
378            kind,
379            x,
380            y,
381            modifiers,
382            pixel_x,
383            pixel_y,
384        }
385    }
386
387    /// Returns true if this is a scroll event.
388    pub fn is_scroll(&self) -> bool {
389        matches!(
390            self.kind,
391            MouseKind::ScrollUp
392                | MouseKind::ScrollDown
393                | MouseKind::ScrollLeft
394                | MouseKind::ScrollRight
395        )
396    }
397}
398
399/// The type of mouse event.
400#[non_exhaustive]
401#[derive(Debug, Clone, PartialEq, Eq)]
402pub enum MouseKind {
403    /// A mouse button was pressed.
404    Down(MouseButton),
405    /// A mouse button was released.
406    Up(MouseButton),
407    /// The mouse was moved while a button was held.
408    Drag(MouseButton),
409    /// The scroll wheel was rotated upward.
410    ScrollUp,
411    /// The scroll wheel was rotated downward.
412    ScrollDown,
413    /// The scroll wheel was rotated leftward (horizontal scroll).
414    ScrollLeft,
415    /// The scroll wheel was rotated rightward (horizontal scroll).
416    ScrollRight,
417    /// The mouse was moved without any button held.
418    Moved,
419}
420
421/// Mouse button identifier.
422#[non_exhaustive]
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424pub enum MouseButton {
425    /// Primary (left) mouse button.
426    Left,
427    /// Secondary (right) mouse button.
428    Right,
429    /// Middle mouse button (scroll wheel click).
430    Middle,
431}
432
433#[cfg(feature = "crossterm")]
434fn convert_modifiers(modifiers: crossterm_event::KeyModifiers) -> KeyModifiers {
435    let mut out = KeyModifiers::NONE;
436    if modifiers.contains(crossterm_event::KeyModifiers::SHIFT) {
437        out.0 |= KeyModifiers::SHIFT.0;
438    }
439    if modifiers.contains(crossterm_event::KeyModifiers::CONTROL) {
440        out.0 |= KeyModifiers::CONTROL.0;
441    }
442    if modifiers.contains(crossterm_event::KeyModifiers::ALT) {
443        out.0 |= KeyModifiers::ALT.0;
444    }
445    if modifiers.contains(crossterm_event::KeyModifiers::SUPER) {
446        out.0 |= KeyModifiers::SUPER.0;
447    }
448    if modifiers.contains(crossterm_event::KeyModifiers::HYPER) {
449        out.0 |= KeyModifiers::HYPER.0;
450    }
451    if modifiers.contains(crossterm_event::KeyModifiers::META) {
452        out.0 |= KeyModifiers::META.0;
453    }
454    out
455}
456
457#[cfg(feature = "crossterm")]
458fn convert_button(button: crossterm_event::MouseButton) -> MouseButton {
459    match button {
460        crossterm_event::MouseButton::Left => MouseButton::Left,
461        crossterm_event::MouseButton::Right => MouseButton::Right,
462        crossterm_event::MouseButton::Middle => MouseButton::Middle,
463    }
464}
465
466// ── crossterm conversions ────────────────────────────────────────────
467
468/// Convert a raw crossterm event into our lightweight [`Event`].
469/// Returns `None` for event kinds we don't handle.
470#[cfg(feature = "crossterm")]
471pub(crate) fn from_crossterm(raw: crossterm_event::Event) -> Option<Event> {
472    match raw {
473        crossterm_event::Event::Key(k) => {
474            let code = match k.code {
475                crossterm_event::KeyCode::Char(c) => KeyCode::Char(c),
476                crossterm_event::KeyCode::Enter => KeyCode::Enter,
477                crossterm_event::KeyCode::Backspace => KeyCode::Backspace,
478                crossterm_event::KeyCode::Tab => KeyCode::Tab,
479                crossterm_event::KeyCode::BackTab => KeyCode::BackTab,
480                crossterm_event::KeyCode::Esc => KeyCode::Esc,
481                crossterm_event::KeyCode::Up => KeyCode::Up,
482                crossterm_event::KeyCode::Down => KeyCode::Down,
483                crossterm_event::KeyCode::Left => KeyCode::Left,
484                crossterm_event::KeyCode::Right => KeyCode::Right,
485                crossterm_event::KeyCode::Home => KeyCode::Home,
486                crossterm_event::KeyCode::End => KeyCode::End,
487                crossterm_event::KeyCode::PageUp => KeyCode::PageUp,
488                crossterm_event::KeyCode::PageDown => KeyCode::PageDown,
489                crossterm_event::KeyCode::Delete => KeyCode::Delete,
490                crossterm_event::KeyCode::Insert => KeyCode::Insert,
491                crossterm_event::KeyCode::Null => KeyCode::Null,
492                crossterm_event::KeyCode::CapsLock => KeyCode::CapsLock,
493                crossterm_event::KeyCode::ScrollLock => KeyCode::ScrollLock,
494                crossterm_event::KeyCode::NumLock => KeyCode::NumLock,
495                crossterm_event::KeyCode::PrintScreen => KeyCode::PrintScreen,
496                crossterm_event::KeyCode::Pause => KeyCode::Pause,
497                crossterm_event::KeyCode::Menu => KeyCode::Menu,
498                crossterm_event::KeyCode::KeypadBegin => KeyCode::KeypadBegin,
499                crossterm_event::KeyCode::F(n) => KeyCode::F(n),
500                _ => return None,
501            };
502            let modifiers = convert_modifiers(k.modifiers);
503            let kind = match k.kind {
504                crossterm_event::KeyEventKind::Press => KeyEventKind::Press,
505                crossterm_event::KeyEventKind::Repeat => KeyEventKind::Repeat,
506                crossterm_event::KeyEventKind::Release => KeyEventKind::Release,
507            };
508            Some(Event::Key(KeyEvent {
509                code,
510                modifiers,
511                kind,
512            }))
513        }
514        crossterm_event::Event::Mouse(m) => {
515            let kind = match m.kind {
516                crossterm_event::MouseEventKind::Down(btn) => MouseKind::Down(convert_button(btn)),
517                crossterm_event::MouseEventKind::Up(btn) => MouseKind::Up(convert_button(btn)),
518                crossterm_event::MouseEventKind::Drag(btn) => MouseKind::Drag(convert_button(btn)),
519                crossterm_event::MouseEventKind::Moved => MouseKind::Moved,
520                crossterm_event::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
521                crossterm_event::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
522                crossterm_event::MouseEventKind::ScrollLeft => MouseKind::ScrollLeft,
523                crossterm_event::MouseEventKind::ScrollRight => MouseKind::ScrollRight,
524            };
525
526            Some(Event::Mouse(MouseEvent {
527                kind,
528                x: m.column as u32,
529                y: m.row as u32,
530                modifiers: convert_modifiers(m.modifiers),
531                pixel_x: None,
532                pixel_y: None,
533            }))
534        }
535        crossterm_event::Event::Resize(cols, rows) => Some(Event::Resize(cols as u32, rows as u32)),
536        crossterm_event::Event::Paste(mut s) => {
537            // Defense-in-depth: cap bracketed-paste payload to bound memory and
538            // prevent O(n²) handling in text widgets. 1 MiB is far more than any
539            // reasonable interactive paste; larger payloads are truncated on a
540            // char boundary with an ellipsis so the user sees that it was cut.
541            const MAX_PASTE_BYTES: usize = 1 << 20;
542            if s.len() > MAX_PASTE_BYTES {
543                let mut end = MAX_PASTE_BYTES;
544                while end > 0 && !s.is_char_boundary(end) {
545                    end -= 1;
546                }
547                s.truncate(end);
548                s.push('…');
549            }
550            Some(Event::Paste(s))
551        }
552        crossterm_event::Event::FocusGained => Some(Event::FocusGained),
553        crossterm_event::Event::FocusLost => Some(Event::FocusLost),
554    }
555}
556
557#[cfg(test)]
558mod event_constructor_tests {
559    use super::*;
560
561    #[test]
562    fn test_key_char() {
563        let e = Event::key_char('q');
564        if let Event::Key(k) = e {
565            assert!(matches!(k.code, KeyCode::Char('q')));
566            assert_eq!(k.modifiers, KeyModifiers::NONE);
567            assert!(matches!(k.kind, KeyEventKind::Press));
568        } else {
569            panic!("Expected Key event");
570        }
571    }
572
573    #[test]
574    fn test_key() {
575        let e = Event::key(KeyCode::Enter);
576        if let Event::Key(k) = e {
577            assert!(matches!(k.code, KeyCode::Enter));
578            assert_eq!(k.modifiers, KeyModifiers::NONE);
579            assert!(matches!(k.kind, KeyEventKind::Press));
580        } else {
581            panic!("Expected Key event");
582        }
583    }
584
585    #[test]
586    fn test_key_ctrl() {
587        let e = Event::key_ctrl('s');
588        if let Event::Key(k) = e {
589            assert!(matches!(k.code, KeyCode::Char('s')));
590            assert_eq!(k.modifiers, KeyModifiers::CONTROL);
591            assert!(matches!(k.kind, KeyEventKind::Press));
592        } else {
593            panic!("Expected Key event");
594        }
595    }
596
597    #[test]
598    fn test_key_mod() {
599        let modifiers = KeyModifiers(KeyModifiers::SHIFT.0 | KeyModifiers::ALT.0);
600        let e = Event::key_mod(KeyCode::Tab, modifiers);
601        if let Event::Key(k) = e {
602            assert!(matches!(k.code, KeyCode::Tab));
603            assert_eq!(k.modifiers, modifiers);
604            assert!(matches!(k.kind, KeyEventKind::Press));
605        } else {
606            panic!("Expected Key event");
607        }
608    }
609
610    #[test]
611    fn test_resize() {
612        let e = Event::resize(80, 24);
613        assert!(matches!(e, Event::Resize(80, 24)));
614    }
615
616    #[test]
617    fn test_mouse_click() {
618        let e = Event::mouse_click(10, 5);
619        if let Event::Mouse(m) = e {
620            assert!(matches!(m.kind, MouseKind::Down(MouseButton::Left)));
621            assert_eq!(m.x, 10);
622            assert_eq!(m.y, 5);
623            assert_eq!(m.modifiers, KeyModifiers::NONE);
624        } else {
625            panic!("Expected Mouse event");
626        }
627    }
628
629    #[test]
630    fn test_mouse_move() {
631        let e = Event::mouse_move(10, 5);
632        if let Event::Mouse(m) = e {
633            assert!(matches!(m.kind, MouseKind::Moved));
634            assert_eq!(m.x, 10);
635            assert_eq!(m.y, 5);
636            assert_eq!(m.modifiers, KeyModifiers::NONE);
637        } else {
638            panic!("Expected Mouse event");
639        }
640    }
641
642    #[test]
643    fn test_paste() {
644        let e = Event::paste("hello");
645        assert!(matches!(e, Event::Paste(s) if s == "hello"));
646    }
647}