use crate::*; pub(crate) use ratatui::buffer::Cell; pub(crate) use crossterm::{ExecutableCommand}; pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers}; pub use ratatui::prelude::{Rect, Style, Color, Buffer}; pub use ratatui::style::{Stylize, Modifier}; use ratatui::backend::{Backend, CrosstermBackend}; use std::io::Stdout; use crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode }; pub struct Tui { exited: Arc, buffer: Buffer, backend: CrosstermBackend, 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, Ordering::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::PanicInfo|{ 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 { /// Run the main loop. pub fn run + Sized + 'static> ( state: Arc> ) -> Usually>> { let backend = CrosstermBackend::new(stdout()); let area = backend.size()?; let engine = Self { exited: Arc::new(AtomicBool::new(false)), buffer: Buffer::empty(area), area: area.xywh(), backend, }; let engine = Arc::new(RwLock::new(engine)); let _input_thread = Self::spawn_input_thread(&engine, &state, Duration::from_millis(100)); engine.write().unwrap().setup()?; let render_thread = Self::spawn_render_thread(&engine, &state, Duration::from_millis(10)); render_thread.join().expect("main thread failed"); engine.write().unwrap().teardown()?; Ok(state) } fn spawn_input_thread + Sized + 'static> ( engine: &Arc>, state: &Arc>, poll: Duration ) -> JoinHandle<()> { let exited = engine.read().unwrap().exited.clone(); let state = state.clone(); spawn(move || loop { if exited.fetch_and(true, Ordering::Relaxed) { break } if ::crossterm::event::poll(poll).is_ok() { let event = TuiEvent::Input(::crossterm::event::read().unwrap()); match event { key!(Ctrl-KeyCode::Char('c')) => { exited.store(true, Ordering::Relaxed); }, _ => { let exited = exited.clone(); if let Err(e) = state.write().unwrap().handle(&TuiInput { event, exited }) { panic!("{e}") } } } } }) } fn spawn_render_thread + Sized + 'static> ( engine: &Arc>, state: &Arc>, sleep: Duration ) -> JoinHandle<()> { let exited = engine.read().unwrap().exited.clone(); let engine = engine.clone(); let state = state.clone(); let mut buffer = Buffer::empty( engine.read().unwrap().backend.size().expect("get size failed") ); spawn(move || loop { if exited.fetch_and(true, Ordering::Relaxed) { break } let size = engine.read().unwrap().backend.size().expect("get size failed"); if let Ok(state) = state.try_read() { if buffer.area != size { buffer.resize(size); } let mut output = TuiOutput { buffer, area: size.xywh() }; state.render(&mut output).expect("render failed"); buffer = engine.write().unwrap().flip(output.buffer, size); } std::thread::sleep(sleep); }) } fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { if self.buffer.area != size { self.buffer.resize(size); } 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 struct TuiInput { event: TuiEvent, exited: Arc, } impl Input for TuiInput { type Event = TuiEvent; fn event (&self) -> &TuiEvent { &self.event } fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) } fn done (&self) { self.exited.store(true, Ordering::Relaxed); } } 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 render_in (&mut self, area: [u16;4], widget: &dyn Widget ) -> Usually<()> { let last = self.area(); *self.area_mut() = area; widget.render(self)?; *self.area_mut() = last; Ok(()) } } 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_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