use crate::*; use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}; use std::io::{stdout, Stdout}; use std::time::Duration; use std::thread::{spawn, JoinHandle}; pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity}; pub use ::crossterm; pub(crate) use crossterm::{ ExecutableCommand, terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState}, }; pub use ::ratatui; pub(crate) use ratatui::{ prelude::{Color, Style, Buffer}, style::Modifier, backend::{Backend, CrosstermBackend, ClearType}, layout::{Size, Rect}, buffer::Cell }; impl Coordinate for u16 {} pub struct Tui { pub exited: Arc, pub buffer: Buffer, pub backend: CrosstermBackend, pub area: [u16;4], // FIXME auto resize } impl Engine for Tui { type Unit = u16; type Size = [Self::Unit;2]; type Area = [Self::Unit;4]; type Input = TuiInput; type Handled = bool; type Output = TuiOutput; fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) } fn setup (&mut self) -> Usually<()> { let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler(); std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{ stdout().execute(LeaveAlternateScreen).unwrap(); CrosstermBackend::new(stdout()).show_cursor().unwrap(); disable_raw_mode().unwrap(); better_panic_handler(info); })); stdout().execute(EnterAlternateScreen)?; self.backend.hide_cursor()?; enable_raw_mode().map_err(Into::into) } fn teardown (&mut self) -> Usually<()> { stdout().execute(LeaveAlternateScreen)?; self.backend.show_cursor()?; disable_raw_mode().map_err(Into::into) } } impl Tui { /// Construct a new TUI engine and wrap it for shared ownership. pub fn new () -> Usually>> { let backend = CrosstermBackend::new(stdout()); let Size { width, height } = backend.size()?; Ok(Arc::new(RwLock::new(Self { exited: Arc::new(AtomicBool::new(false)), buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }), area: [0, 0, width, height], backend, }))) } /// Update the display buffer. fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { if self.buffer.area != size { self.backend.clear_region(ClearType::All).unwrap(); self.buffer.resize(size); self.buffer.reset(); } let updates = self.buffer.diff(&buffer); self.backend.draw(updates.into_iter()).expect("failed to render"); self.backend.flush().expect("failed to flush output buffer"); std::mem::swap(&mut self.buffer, &mut buffer); buffer.reset(); buffer } } pub trait TuiRun + Handle + Sized + 'static> { /// Run an app in the main loop. fn run (&self, state: &Arc>) -> Usually<()>; /// Spawn the input thread. fn run_input (&self, state: &Arc>, poll: Duration) -> JoinHandle<()>; /// Spawn the output thread. fn run_output (&self, state: &Arc>, sleep: Duration) -> JoinHandle<()>; } impl + Handle + Sized + 'static> TuiRun for Arc> { fn run (&self, state: &Arc>) -> Usually<()> { let _input_thread = self.run_input(state, Duration::from_millis(100)); self.write().unwrap().setup()?; let render_thread = self.run_output(state, Duration::from_millis(10)); render_thread.join().expect("main thread failed"); self.write().unwrap().teardown()?; Ok(()) } fn run_input (&self, state: &Arc>, poll: Duration) -> JoinHandle<()> { let exited = self.read().unwrap().exited.clone(); let state = state.clone(); spawn(move || loop { if exited.fetch_and(true, Relaxed) { break } if ::crossterm::event::poll(poll).is_ok() { let event = ::crossterm::event::read().unwrap(); match event { key_pat!(Ctrl-KeyCode::Char('c')) => { exited.store(true, Relaxed); }, _ => { let exited = exited.clone(); if let Err(e) = state.write().unwrap().handle(&TuiInput { event, exited }) { panic!("{e}") } } } } }) } fn run_output (&self, state: &Arc>, sleep: Duration) -> JoinHandle<()> { let exited = self.read().unwrap().exited.clone(); let engine = self.clone(); let state = state.clone(); let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed"); let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height }); spawn(move || loop { if exited.fetch_and(true, Relaxed) { break } let Size { width, height } = engine.read().unwrap().backend.size() .expect("get size failed"); if let Ok(state) = state.try_read() { let size = Rect { x: 0, y: 0, width, height }; if buffer.area != size { engine.write().unwrap().backend.clear_region(ClearType::All) .expect("clear failed"); buffer.resize(size); buffer.reset(); } let mut output = TuiOutput { buffer, area: [0, 0, width, height] }; state.render(&mut output); buffer = engine.write().unwrap().flip(output.buffer, size); } std::thread::sleep(sleep); }) } } #[derive(Debug, Clone)] pub struct TuiInput { pub(crate) exited: Arc, pub(crate) event: crossterm::event::Event, } impl Input for TuiInput { type Event = crossterm::event::Event; fn event (&self) -> &crossterm::event::Event { &self.event } fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) } fn done (&self) { self.exited.store(true, Relaxed); } } #[macro_export] macro_rules! key_pat { (Ctrl-Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) }; (Ctrl-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL) }; (Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::ALT) }; (Shift-$code:pat) => { key_event_pat!($code, KeyModifiers::SHIFT) }; ($code:pat) => { crossterm::event::Event::Key(KeyEvent { code: $code, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE }) }; } #[macro_export] macro_rules! key_event_pat { ($code:pat) => { crossterm::event::Event::Key(KeyEvent { code: $code, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE }) }; ($code:pat, $modifiers: pat) => { crossterm::event::Event::Key(KeyEvent { code: $code, modifiers: $modifiers, kind: KeyEventKind::Press, state: KeyEventState::NONE }) }; } #[macro_export] macro_rules! kexp { (Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::from_bits(0b0000_0110).unwrap()) }; (Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) }; (Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) }; (Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) }; ($code:ident) => { key_event_expr!($code) }; ($code:expr) => { key_event_expr!($code) }; } #[macro_export] macro_rules! key_expr { (Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) }; (Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) }; (Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) }; (Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) }; ($code:ident) => { key_event_expr!($code) }; } #[macro_export] macro_rules! key_event_expr { ($code:expr, $modifiers: expr) => { crossterm::event::Event::Key(KeyEvent { code: $code, modifiers: $modifiers, kind: KeyEventKind::Press, state: KeyEventState::NONE }) }; ($code:expr) => { crossterm::event::Event::Key(KeyEvent { code: $code, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE }) }; } pub struct TuiOutput { pub buffer: Buffer, pub area: [u16;4] } impl Output for TuiOutput { #[inline] fn area (&self) -> [u16;4] { self.area } #[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area } #[inline] fn place (&mut self, area: [u16;4], content: &impl Content) { let last = self.area().xywh().clone(); //panic!("a {last:?} {area:?} {:?}", self.area); *self.area_mut() = area.xywh().clone(); //panic!("b {last:?} {area:?} {:?}", self.area); content.render(self); //panic!("c {last:?} {area:?} {:?}", self.area); *self.area_mut() = last; //panic!("placed"); } } impl TuiOutput { pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) { buffer_update(&mut self.buffer, area, callback); } pub fn fill_bold (&mut self, area: [u16;4], on: bool) { if on { self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD)) } else { self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD)) } } pub fn fill_bg (&mut self, area: [u16;4], color: Color) { self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);}) } pub fn fill_fg (&mut self, area: [u16;4], color: Color) { self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);}) } pub fn fill_ul (&mut self, area: [u16;4], color: Color) { self.buffer_update(area, &|cell,_,_|{ cell.modifier = ratatui::prelude::Modifier::UNDERLINED; cell.underline_color = color; }) } pub fn fill_char (&mut self, area: [u16;4], c: char) { self.buffer_update(area, &|cell,_,_|{cell.set_char(c);}) } pub fn make_dim (&mut self) { for cell in self.buffer.content.iter_mut() { cell.bg = ratatui::style::Color::Rgb(30,30,30); cell.fg = ratatui::style::Color::Rgb(100,100,100); cell.modifier = ratatui::style::Modifier::DIM; } } pub fn blit ( &mut self, text: &impl AsRef, x: u16, y: u16, style: Option