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