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