use std::io::{self, Stdout};
use std::time::Duration;
use arboard::Clipboard;
use crossterm::event::{
self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
MouseEventKind,
};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use crate::error::TuiError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppEvent {
Key(AppKeyEvent),
Mouse(AppMouseEvent),
Resize {
width: u16,
height: u16,
},
FocusGained,
FocusLost,
Paste(String),
Unsupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AppKeyEvent {
pub code: AppKeyCode,
pub modifiers: AppKeyModifiers,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AppKeyCode {
Char(char),
F(u8),
Backspace,
Enter,
Left,
Right,
Up,
Down,
Tab,
BackTab,
Delete,
Home,
End,
PageUp,
PageDown,
Esc,
Null,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AppKeyModifiers {
pub control: bool,
pub alt: bool,
pub shift: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AppMouseEvent {
pub kind: AppMouseEventKind,
pub column: u16,
pub row: u16,
pub modifiers: AppKeyModifiers,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AppMouseEventKind {
Down(AppMouseButton),
Up(AppMouseButton),
Drag(AppMouseButton),
Moved,
ScrollDown,
ScrollUp,
ScrollLeft,
ScrollRight,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AppMouseButton {
Left,
Right,
Middle,
}
impl AppKeyEvent {
#[must_use]
pub fn new(code: AppKeyCode, modifiers: AppKeyModifiers) -> Self {
Self { code, modifiers }
}
}
impl From<KeyModifiers> for AppKeyModifiers {
fn from(value: KeyModifiers) -> Self {
Self {
control: value.contains(KeyModifiers::CONTROL),
alt: value.contains(KeyModifiers::ALT),
shift: value.contains(KeyModifiers::SHIFT),
}
}
}
impl From<KeyCode> for AppKeyCode {
fn from(value: KeyCode) -> Self {
match value {
KeyCode::Char(c) => Self::Char(c),
KeyCode::F(value) => Self::F(value),
KeyCode::Backspace => Self::Backspace,
KeyCode::Enter => Self::Enter,
KeyCode::Left => Self::Left,
KeyCode::Right => Self::Right,
KeyCode::Up => Self::Up,
KeyCode::Down => Self::Down,
KeyCode::Tab => Self::Tab,
KeyCode::BackTab => Self::BackTab,
KeyCode::Delete => Self::Delete,
KeyCode::Home => Self::Home,
KeyCode::End => Self::End,
KeyCode::PageUp => Self::PageUp,
KeyCode::PageDown => Self::PageDown,
KeyCode::Esc => Self::Esc,
_ => Self::Null,
}
}
}
impl From<KeyEvent> for AppEvent {
fn from(value: KeyEvent) -> Self {
if value.kind == KeyEventKind::Release {
return Self::Unsupported;
}
Self::Key(AppKeyEvent::new(
AppKeyCode::from(value.code),
AppKeyModifiers::from(value.modifiers),
))
}
}
impl From<MouseButton> for AppMouseButton {
fn from(value: MouseButton) -> Self {
match value {
MouseButton::Left => Self::Left,
MouseButton::Right => Self::Right,
MouseButton::Middle => Self::Middle,
}
}
}
impl From<MouseEvent> for AppEvent {
fn from(value: MouseEvent) -> Self {
let kind = match value.kind {
MouseEventKind::Down(button) => AppMouseEventKind::Down(button.into()),
MouseEventKind::Up(button) => AppMouseEventKind::Up(button.into()),
MouseEventKind::Drag(button) => AppMouseEventKind::Drag(button.into()),
MouseEventKind::Moved => AppMouseEventKind::Moved,
MouseEventKind::ScrollDown => AppMouseEventKind::ScrollDown,
MouseEventKind::ScrollUp => AppMouseEventKind::ScrollUp,
MouseEventKind::ScrollLeft => AppMouseEventKind::ScrollLeft,
MouseEventKind::ScrollRight => AppMouseEventKind::ScrollRight,
};
Self::Mouse(AppMouseEvent {
kind,
column: value.column,
row: value.row,
modifiers: AppKeyModifiers::from(value.modifiers),
})
}
}
impl From<Event> for AppEvent {
fn from(value: Event) -> Self {
match value {
Event::Key(event) => Self::from(event),
Event::Mouse(event) => Self::from(event),
Event::Resize(width, height) => Self::Resize { width, height },
Event::FocusGained => Self::FocusGained,
Event::FocusLost => Self::FocusLost,
Event::Paste(text) => Self::Paste(text),
}
}
}
pub trait Runtime {
type Backend: ratatui::backend::Backend;
fn init_terminal(&mut self) -> Result<Terminal<Self::Backend>, TuiError>;
fn restore_terminal(&mut self, terminal: &mut Terminal<Self::Backend>);
fn poll_event(&mut self, timeout: Duration) -> Result<bool, TuiError>;
fn read_event(&mut self) -> Result<AppEvent, TuiError>;
fn copy_to_clipboard(&mut self, text: &str) -> Result<(), String>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CrosstermRuntime;
impl Runtime for CrosstermRuntime {
type Backend = CrosstermBackend<Stdout>;
fn init_terminal(&mut self) -> Result<Terminal<Self::Backend>, TuiError> {
let mut stdout = io::stdout();
enable_raw_mode()?;
if let Err(err) = execute!(stdout, EnterAlternateScreen) {
let _ = disable_raw_mode();
return Err(err.into());
}
if let Err(err) = execute!(
stdout,
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
) {
let _ = execute!(stdout, LeaveAlternateScreen);
let _ = disable_raw_mode();
return Err(err.into());
}
#[cfg(feature = "mouse")]
if let Err(err) = execute!(stdout, crossterm::event::EnableMouseCapture) {
let _ = execute!(
stdout,
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
);
let _ = execute!(stdout, LeaveAlternateScreen);
let _ = disable_raw_mode();
return Err(err.into());
}
if let Err(err) = execute!(stdout, crossterm::event::EnableBracketedPaste) {
#[cfg(feature = "mouse")]
{
let _ = execute!(stdout, crossterm::event::DisableMouseCapture);
}
let _ = execute!(
stdout,
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
);
let _ = execute!(stdout, LeaveAlternateScreen);
let _ = disable_raw_mode();
return Err(err.into());
}
Terminal::new(CrosstermBackend::new(stdout)).map_err(TuiError::from)
}
fn restore_terminal(&mut self, terminal: &mut Terminal<Self::Backend>) {
let _ = disable_raw_mode();
#[cfg(feature = "mouse")]
{
let _ = execute!(
terminal.backend_mut(),
crossterm::event::DisableMouseCapture
);
}
let _ = execute!(
terminal.backend_mut(),
crossterm::event::DisableBracketedPaste
);
let _ = execute!(
terminal.backend_mut(),
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
);
let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen);
let _ = terminal.show_cursor();
}
fn poll_event(&mut self, timeout: Duration) -> Result<bool, TuiError> {
event::poll(timeout).map_err(TuiError::from)
}
fn read_event(&mut self) -> Result<AppEvent, TuiError> {
event::read().map(AppEvent::from).map_err(TuiError::from)
}
fn copy_to_clipboard(&mut self, text: &str) -> Result<(), String> {
Clipboard::new()
.and_then(|mut clipboard| clipboard.set_text(text.to_string()))
.map_err(|err| err.to_string())
}
}