Skip to main content

slt/
event.rs

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