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#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum AppEvent {
24 Key(AppKeyEvent),
26 Mouse(AppMouseEvent),
28 Resize {
30 width: u16,
32 height: u16,
34 },
35 FocusGained,
37 FocusLost,
39 Paste(String),
41 Unsupported,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct AppKeyEvent {
50 pub code: AppKeyCode,
52 pub modifiers: AppKeyModifiers,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum AppKeyCode {
59 Char(char),
61 F(u8),
63 Backspace,
65 Enter,
67 Left,
69 Right,
71 Up,
73 Down,
75 Tab,
77 BackTab,
79 Delete,
81 Home,
83 End,
85 PageUp,
87 PageDown,
89 Esc,
91 Null,
93}
94
95#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
97pub struct AppKeyModifiers {
98 pub control: bool,
100 pub alt: bool,
102 pub shift: bool,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct AppMouseEvent {
111 pub kind: AppMouseEventKind,
113 pub column: u16,
115 pub row: u16,
117 pub modifiers: AppKeyModifiers,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum AppMouseEventKind {
124 Down(AppMouseButton),
126 Up(AppMouseButton),
128 Drag(AppMouseButton),
130 Moved,
132 ScrollDown,
134 ScrollUp,
136 ScrollLeft,
138 ScrollRight,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub enum AppMouseButton {
145 Left,
147 Right,
149 Middle,
151}
152
153impl AppKeyEvent {
154 #[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
251pub trait Runtime {
259 type Backend: ratatui::backend::Backend;
261
262 fn init_terminal(&mut self) -> Result<Terminal<Self::Backend>, TuiError>;
269
270 fn restore_terminal(&mut self, terminal: &mut Terminal<Self::Backend>);
275
276 fn poll_event(&mut self, timeout: Duration) -> Result<bool, TuiError>;
282
283 fn read_event(&mut self) -> Result<AppEvent, TuiError>;
289
290 fn copy_to_clipboard(&mut self, text: &str) -> Result<(), String>;
296}
297
298#[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}