Skip to main content

clap_tui/
runtime.rs

1use std::io::{self, Stdout};
2use std::time::Duration;
3
4use arboard::Clipboard;
5use crossterm::event::{
6    self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
7    MouseEventKind,
8};
9use crossterm::execute;
10use crossterm::terminal::{
11    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
12};
13use ratatui::Terminal;
14use ratatui::backend::CrosstermBackend;
15
16use crate::error::TuiError;
17
18/// Input event emitted by a custom [`Runtime`] implementation.
19///
20/// Most applications can use [`CrosstermRuntime`]. Implement this surface only when you need to
21/// feed keyboard, mouse, or terminal events into [`crate::TuiApp`] from another runtime.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum AppEvent {
24    /// Keyboard input.
25    Key(AppKeyEvent),
26    /// Mouse input.
27    Mouse(AppMouseEvent),
28    /// Terminal resize event.
29    Resize {
30        /// New terminal width.
31        width: u16,
32        /// New terminal height.
33        height: u16,
34    },
35    /// Focus gained event.
36    FocusGained,
37    /// Focus lost event.
38    FocusLost,
39    /// Paste payload.
40    Paste(String),
41    /// Event variants the app does not currently model.
42    Unsupported,
43}
44
45/// Keyboard event used by custom [`Runtime`] implementations.
46///
47/// Custom runtimes can construct this type directly when translating backend-specific input.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct AppKeyEvent {
50    /// Pressed key.
51    pub code: AppKeyCode,
52    /// Active modifiers.
53    pub modifiers: AppKeyModifiers,
54}
55
56/// Key code used by [`AppKeyEvent`].
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum AppKeyCode {
59    /// Character input.
60    Char(char),
61    /// Function key.
62    F(u8),
63    /// Backspace key.
64    Backspace,
65    /// Enter key.
66    Enter,
67    /// Left arrow key.
68    Left,
69    /// Right arrow key.
70    Right,
71    /// Up arrow key.
72    Up,
73    /// Down arrow key.
74    Down,
75    /// Tab key.
76    Tab,
77    /// Reverse tab key.
78    BackTab,
79    /// Delete key.
80    Delete,
81    /// Home key.
82    Home,
83    /// End key.
84    End,
85    /// Page up key.
86    PageUp,
87    /// Page down key.
88    PageDown,
89    /// Escape key.
90    Esc,
91    /// Unsupported key code.
92    Null,
93}
94
95/// Key modifiers used by keyboard and mouse events.
96#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
97pub struct AppKeyModifiers {
98    /// Ctrl was pressed.
99    pub control: bool,
100    /// Alt was pressed.
101    pub alt: bool,
102    /// Shift was pressed.
103    pub shift: bool,
104}
105
106/// Mouse event used by custom [`Runtime`] implementations.
107///
108/// Custom runtimes can construct this type directly when translating backend-specific input.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct AppMouseEvent {
111    /// Mouse action.
112    pub kind: AppMouseEventKind,
113    /// Column in terminal coordinates.
114    pub column: u16,
115    /// Row in terminal coordinates.
116    pub row: u16,
117    /// Active modifiers.
118    pub modifiers: AppKeyModifiers,
119}
120
121/// Mouse event kind used by [`AppMouseEvent`].
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum AppMouseEventKind {
124    /// Button press.
125    Down(AppMouseButton),
126    /// Button release.
127    Up(AppMouseButton),
128    /// Button drag.
129    Drag(AppMouseButton),
130    /// Mouse move.
131    Moved,
132    /// Scroll down.
133    ScrollDown,
134    /// Scroll up.
135    ScrollUp,
136    /// Scroll left.
137    ScrollLeft,
138    /// Scroll right.
139    ScrollRight,
140}
141
142/// Mouse button used by [`AppMouseEventKind`].
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub enum AppMouseButton {
145    /// Left mouse button.
146    Left,
147    /// Right mouse button.
148    Right,
149    /// Middle mouse button.
150    Middle,
151}
152
153impl AppKeyEvent {
154    /// Construct a new key event.
155    #[must_use]
156    pub fn new(code: AppKeyCode, modifiers: AppKeyModifiers) -> Self {
157        Self { code, modifiers }
158    }
159}
160
161impl From<KeyModifiers> for AppKeyModifiers {
162    fn from(value: KeyModifiers) -> Self {
163        Self {
164            control: value.contains(KeyModifiers::CONTROL),
165            alt: value.contains(KeyModifiers::ALT),
166            shift: value.contains(KeyModifiers::SHIFT),
167        }
168    }
169}
170
171impl From<KeyCode> for AppKeyCode {
172    fn from(value: KeyCode) -> Self {
173        match value {
174            KeyCode::Char(c) => Self::Char(c),
175            KeyCode::F(value) => Self::F(value),
176            KeyCode::Backspace => Self::Backspace,
177            KeyCode::Enter => Self::Enter,
178            KeyCode::Left => Self::Left,
179            KeyCode::Right => Self::Right,
180            KeyCode::Up => Self::Up,
181            KeyCode::Down => Self::Down,
182            KeyCode::Tab => Self::Tab,
183            KeyCode::BackTab => Self::BackTab,
184            KeyCode::Delete => Self::Delete,
185            KeyCode::Home => Self::Home,
186            KeyCode::End => Self::End,
187            KeyCode::PageUp => Self::PageUp,
188            KeyCode::PageDown => Self::PageDown,
189            KeyCode::Esc => Self::Esc,
190            _ => Self::Null,
191        }
192    }
193}
194
195impl From<KeyEvent> for AppEvent {
196    fn from(value: KeyEvent) -> Self {
197        if value.kind == KeyEventKind::Release {
198            return Self::Unsupported;
199        }
200        Self::Key(AppKeyEvent::new(
201            AppKeyCode::from(value.code),
202            AppKeyModifiers::from(value.modifiers),
203        ))
204    }
205}
206
207impl From<MouseButton> for AppMouseButton {
208    fn from(value: MouseButton) -> Self {
209        match value {
210            MouseButton::Left => Self::Left,
211            MouseButton::Right => Self::Right,
212            MouseButton::Middle => Self::Middle,
213        }
214    }
215}
216
217impl From<MouseEvent> for AppEvent {
218    fn from(value: MouseEvent) -> Self {
219        let kind = match value.kind {
220            MouseEventKind::Down(button) => AppMouseEventKind::Down(button.into()),
221            MouseEventKind::Up(button) => AppMouseEventKind::Up(button.into()),
222            MouseEventKind::Drag(button) => AppMouseEventKind::Drag(button.into()),
223            MouseEventKind::Moved => AppMouseEventKind::Moved,
224            MouseEventKind::ScrollDown => AppMouseEventKind::ScrollDown,
225            MouseEventKind::ScrollUp => AppMouseEventKind::ScrollUp,
226            MouseEventKind::ScrollLeft => AppMouseEventKind::ScrollLeft,
227            MouseEventKind::ScrollRight => AppMouseEventKind::ScrollRight,
228        };
229        Self::Mouse(AppMouseEvent {
230            kind,
231            column: value.column,
232            row: value.row,
233            modifiers: AppKeyModifiers::from(value.modifiers),
234        })
235    }
236}
237
238impl From<Event> for AppEvent {
239    fn from(value: Event) -> Self {
240        match value {
241            Event::Key(event) => Self::from(event),
242            Event::Mouse(event) => Self::from(event),
243            Event::Resize(width, height) => Self::Resize { width, height },
244            Event::FocusGained => Self::FocusGained,
245            Event::FocusLost => Self::FocusLost,
246            Event::Paste(text) => Self::Paste(text),
247        }
248    }
249}
250
251/// Runtime services required by `TuiApp`.
252///
253/// Most applications should use [`CrosstermRuntime`]. Implement this trait only when you need
254/// to provide your own terminal, event, or clipboard integration.
255///
256/// The associated event types re-exported by the crate are part of this advanced integration
257/// path. Other internal modules remain implementation details.
258pub trait Runtime {
259    /// Terminal backend used by the runtime.
260    type Backend: ratatui::backend::Backend;
261
262    /// Enter interactive terminal mode and create a terminal instance.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error when the runtime cannot switch the terminal into
267    /// interactive mode.
268    fn init_terminal(&mut self) -> Result<Terminal<Self::Backend>, TuiError>;
269
270    /// Restore the terminal to its original state.
271    ///
272    /// Implementations should make a best effort to clean up even when prior
273    /// runtime operations failed.
274    fn restore_terminal(&mut self, terminal: &mut Terminal<Self::Backend>);
275
276    /// Poll for an input event.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error when event polling fails.
281    fn poll_event(&mut self, timeout: Duration) -> Result<bool, TuiError>;
282
283    /// Read the next input event.
284    ///
285    /// # Errors
286    ///
287    /// Returns an error when event reading fails.
288    fn read_event(&mut self) -> Result<AppEvent, TuiError>;
289
290    /// Copy text to the system clipboard.
291    ///
292    /// # Errors
293    ///
294    /// Returns an error string when the clipboard is unavailable.
295    fn copy_to_clipboard(&mut self, text: &str) -> Result<(), String>;
296}
297
298/// Default runtime backed by crossterm and arboard.
299///
300/// Most applications should use this runtime via [`crate::TuiApp`] without any extra setup.
301#[derive(Debug, Default, Clone, Copy)]
302pub struct CrosstermRuntime;
303
304impl Runtime for CrosstermRuntime {
305    type Backend = CrosstermBackend<Stdout>;
306
307    fn init_terminal(&mut self) -> Result<Terminal<Self::Backend>, TuiError> {
308        let mut stdout = io::stdout();
309        enable_raw_mode()?;
310
311        if let Err(err) = execute!(stdout, EnterAlternateScreen) {
312            let _ = disable_raw_mode();
313            return Err(err.into());
314        }
315        if let Err(err) = execute!(
316            stdout,
317            crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
318        ) {
319            let _ = execute!(stdout, LeaveAlternateScreen);
320            let _ = disable_raw_mode();
321            return Err(err.into());
322        }
323        #[cfg(feature = "mouse")]
324        if let Err(err) = execute!(stdout, crossterm::event::EnableMouseCapture) {
325            let _ = execute!(
326                stdout,
327                crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
328            );
329            let _ = execute!(stdout, LeaveAlternateScreen);
330            let _ = disable_raw_mode();
331            return Err(err.into());
332        }
333        if let Err(err) = execute!(stdout, crossterm::event::EnableBracketedPaste) {
334            #[cfg(feature = "mouse")]
335            {
336                let _ = execute!(stdout, crossterm::event::DisableMouseCapture);
337            }
338            let _ = execute!(
339                stdout,
340                crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
341            );
342            let _ = execute!(stdout, LeaveAlternateScreen);
343            let _ = disable_raw_mode();
344            return Err(err.into());
345        }
346
347        Terminal::new(CrosstermBackend::new(stdout)).map_err(TuiError::from)
348    }
349
350    fn restore_terminal(&mut self, terminal: &mut Terminal<Self::Backend>) {
351        let _ = disable_raw_mode();
352        #[cfg(feature = "mouse")]
353        {
354            let _ = execute!(
355                terminal.backend_mut(),
356                crossterm::event::DisableMouseCapture
357            );
358        }
359        let _ = execute!(
360            terminal.backend_mut(),
361            crossterm::event::DisableBracketedPaste
362        );
363        let _ = execute!(
364            terminal.backend_mut(),
365            crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
366        );
367        let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen);
368        let _ = terminal.show_cursor();
369    }
370
371    fn poll_event(&mut self, timeout: Duration) -> Result<bool, TuiError> {
372        event::poll(timeout).map_err(TuiError::from)
373    }
374
375    fn read_event(&mut self) -> Result<AppEvent, TuiError> {
376        event::read().map(AppEvent::from).map_err(TuiError::from)
377    }
378
379    fn copy_to_clipboard(&mut self, text: &str) -> Result<(), String> {
380        Clipboard::new()
381            .and_then(|mut clipboard| clipboard.set_text(text.to_string()))
382            .map_err(|err| err.to_string())
383    }
384}