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/// A terminal input event.
11///
12/// Produced each frame by the run loop and passed to your UI closure via
13/// [`crate::Context`]. Use the helper methods on `Context` (e.g., `key()`,
14/// `key_code()`) rather than matching on this type directly.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum Event {
17    /// A keyboard event.
18    Key(KeyEvent),
19    /// A mouse event (requires `mouse: true` in [`crate::RunConfig`]).
20    Mouse(MouseEvent),
21    /// The terminal was resized to the given `(columns, rows)`.
22    Resize(u32, u32),
23    /// Pasted text (bracketed paste). May contain newlines.
24    Paste(String),
25    /// The terminal window gained focus.
26    FocusGained,
27    /// The terminal window lost focus. Used to clear hover state.
28    FocusLost,
29}
30
31/// A keyboard event with key code and modifiers.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct KeyEvent {
34    /// The key that was pressed.
35    pub code: KeyCode,
36    /// Modifier keys held at the time of the press.
37    pub modifiers: KeyModifiers,
38}
39
40/// Key identifier.
41///
42/// Covers printable characters, control keys, arrow keys, function keys,
43/// and navigation keys. Unrecognized keys are silently dropped by the
44/// crossterm conversion layer.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum KeyCode {
47    /// A printable character (letter, digit, symbol, space, etc.).
48    Char(char),
49    /// Enter / Return key.
50    Enter,
51    /// Backspace key.
52    Backspace,
53    /// Tab key (forward tab).
54    Tab,
55    /// Shift+Tab (back tab).
56    BackTab,
57    /// Escape key.
58    Esc,
59    /// Up arrow key.
60    Up,
61    /// Down arrow key.
62    Down,
63    /// Left arrow key.
64    Left,
65    /// Right arrow key.
66    Right,
67    /// Home key.
68    Home,
69    /// End key.
70    End,
71    /// Page Up key.
72    PageUp,
73    /// Page Down key.
74    PageDown,
75    /// Delete (forward delete) key.
76    Delete,
77    /// Function key `F1`..`F12` (and beyond). The inner `u8` is the number.
78    F(u8),
79}
80
81/// Modifier keys held during a key press.
82///
83/// Stored as bitflags in a `u8`. Check individual modifiers with
84/// [`KeyModifiers::contains`].
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub struct KeyModifiers(pub u8);
87
88impl KeyModifiers {
89    /// No modifier keys held.
90    pub const NONE: Self = Self(0);
91    /// Shift key held.
92    pub const SHIFT: Self = Self(1 << 0);
93    /// Control key held.
94    pub const CONTROL: Self = Self(1 << 1);
95    /// Alt / Option key held.
96    pub const ALT: Self = Self(1 << 2);
97
98    /// Returns `true` if all bits in `other` are set in `self`.
99    #[inline]
100    pub fn contains(self, other: Self) -> bool {
101        (self.0 & other.0) == other.0
102    }
103}
104
105/// A mouse event with position and kind.
106///
107/// Coordinates are zero-based terminal columns (`x`) and rows (`y`).
108/// Mouse events are only produced when `mouse: true` is set in
109/// [`crate::RunConfig`].
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct MouseEvent {
112    /// The type of mouse action that occurred.
113    pub kind: MouseKind,
114    /// Column (horizontal position), zero-based.
115    pub x: u32,
116    /// Row (vertical position), zero-based.
117    pub y: u32,
118    /// Modifier keys held at the time of the event.
119    pub modifiers: KeyModifiers,
120}
121
122/// The type of mouse event.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum MouseKind {
125    /// A mouse button was pressed.
126    Down(MouseButton),
127    /// A mouse button was released.
128    Up(MouseButton),
129    /// The mouse was moved while a button was held.
130    Drag(MouseButton),
131    /// The scroll wheel was rotated upward.
132    ScrollUp,
133    /// The scroll wheel was rotated downward.
134    ScrollDown,
135    /// The mouse was moved without any button held.
136    Moved,
137}
138
139/// Mouse button identifier.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum MouseButton {
142    /// Primary (left) mouse button.
143    Left,
144    /// Secondary (right) mouse button.
145    Right,
146    /// Middle mouse button (scroll wheel click).
147    Middle,
148}
149
150fn convert_modifiers(modifiers: crossterm::event::KeyModifiers) -> KeyModifiers {
151    let mut out = KeyModifiers::NONE;
152    if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
153        out.0 |= KeyModifiers::SHIFT.0;
154    }
155    if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
156        out.0 |= KeyModifiers::CONTROL.0;
157    }
158    if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
159        out.0 |= KeyModifiers::ALT.0;
160    }
161    out
162}
163
164fn convert_button(button: crossterm::event::MouseButton) -> MouseButton {
165    match button {
166        crossterm::event::MouseButton::Left => MouseButton::Left,
167        crossterm::event::MouseButton::Right => MouseButton::Right,
168        crossterm::event::MouseButton::Middle => MouseButton::Middle,
169    }
170}
171
172// ── crossterm conversions ────────────────────────────────────────────
173
174/// Convert a raw crossterm event into our lightweight [`Event`].
175/// Returns `None` for event kinds we don't handle.
176pub(crate) fn from_crossterm(raw: crossterm::event::Event) -> Option<Event> {
177    match raw {
178        crossterm::event::Event::Key(k) => {
179            // Only handle key-press (not repeat/release) to avoid double-fire.
180            if k.kind != crossterm::event::KeyEventKind::Press {
181                return None;
182            }
183            let code = match k.code {
184                crossterm::event::KeyCode::Char(c) => KeyCode::Char(c),
185                crossterm::event::KeyCode::Enter => KeyCode::Enter,
186                crossterm::event::KeyCode::Backspace => KeyCode::Backspace,
187                crossterm::event::KeyCode::Tab => KeyCode::Tab,
188                crossterm::event::KeyCode::BackTab => KeyCode::BackTab,
189                crossterm::event::KeyCode::Esc => KeyCode::Esc,
190                crossterm::event::KeyCode::Up => KeyCode::Up,
191                crossterm::event::KeyCode::Down => KeyCode::Down,
192                crossterm::event::KeyCode::Left => KeyCode::Left,
193                crossterm::event::KeyCode::Right => KeyCode::Right,
194                crossterm::event::KeyCode::Home => KeyCode::Home,
195                crossterm::event::KeyCode::End => KeyCode::End,
196                crossterm::event::KeyCode::PageUp => KeyCode::PageUp,
197                crossterm::event::KeyCode::PageDown => KeyCode::PageDown,
198                crossterm::event::KeyCode::Delete => KeyCode::Delete,
199                crossterm::event::KeyCode::F(n) => KeyCode::F(n),
200                _ => return None,
201            };
202            let modifiers = convert_modifiers(k.modifiers);
203            Some(Event::Key(KeyEvent { code, modifiers }))
204        }
205        crossterm::event::Event::Mouse(m) => {
206            let kind = match m.kind {
207                crossterm::event::MouseEventKind::Down(btn) => MouseKind::Down(convert_button(btn)),
208                crossterm::event::MouseEventKind::Up(btn) => MouseKind::Up(convert_button(btn)),
209                crossterm::event::MouseEventKind::Drag(btn) => MouseKind::Drag(convert_button(btn)),
210                crossterm::event::MouseEventKind::Moved => MouseKind::Moved,
211                crossterm::event::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
212                crossterm::event::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
213                _ => return None,
214            };
215
216            Some(Event::Mouse(MouseEvent {
217                kind,
218                x: m.column as u32,
219                y: m.row as u32,
220                modifiers: convert_modifiers(m.modifiers),
221            }))
222        }
223        crossterm::event::Event::Resize(cols, rows) => {
224            Some(Event::Resize(cols as u32, rows as u32))
225        }
226        crossterm::event::Event::Paste(s) => Some(Event::Paste(s)),
227        crossterm::event::Event::FocusGained => Some(Event::FocusGained),
228        crossterm::event::Event::FocusLost => Some(Event::FocusLost),
229    }
230}