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 }; submod! { tui_border tui_buffer tui_colors tui_layout } pub struct Tui { exited: Arc, buffer: usize, buffers: [Buffer;2], backend: CrosstermBackend, event: RwLock>, area: [u16;4], } impl Engine for Tui { type Unit = u16; type Area = [Self::Unit;4]; type HandleInput = Self; type Handled = bool; type RenderInput = Self; type Rendered = Self::Area; 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(); 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) } // FIXME fn area (&self) -> Self::Area { self.area } #[inline] fn with_area (&mut self, x: u16, y: u16, w: u16, h: u16) -> &mut Self { self.with_rect([x, y, w, h]); self } } impl Tui { /// Run the main loop. pub fn run + Sized + 'static> ( state: Arc> ) -> Usually>> { let backend = CrosstermBackend::new(stdout()); let area = backend.size()?; let mut engine = Self { exited: Arc::new(AtomicBool::new(false)), event: RwLock::new(None), buffer: 0, buffers: [Buffer::empty(area), Buffer::empty(area)], backend, area: area.xywh(), }; engine.setup()?; let engine = Arc::new(RwLock::new(engine)); let _input_thread = { let engine = engine.clone(); let state = state.clone(); let poll = Duration::from_millis(100); spawn(move || loop { if ::crossterm::event::poll(poll).is_ok() { let event = TuiEvent::Input(::crossterm::event::read().unwrap()); match event { key!(Ctrl-KeyCode::Char('c')) => { engine.write().unwrap().exited.store(true, Ordering::Relaxed); }, _ => { *engine.write().unwrap().event.write().unwrap() = Some(event); if let Err(e) = state.write().unwrap().handle(&*engine.read().unwrap()) { panic!("{e}") } } } } if engine.read().unwrap().exited() { break } //engine.read().unwrap().handle(&mut *state.write().unwrap()).expect("handle failed"); }) }; let main_thread = { let engine = engine.clone(); let state = state.clone(); let sleep = Duration::from_millis(20); spawn(move || loop { if let (Ok(mut engine), Ok(state)) = (engine.write(), state.try_read()) { if engine.exited() { break } engine.area = engine.backend.size().expect("get size failed").xywh(); state.render(&mut engine).expect("render failed"); engine.flip(); } std::thread::sleep(sleep); }) }; main_thread.join().expect("main thread failed"); engine.write().unwrap().teardown()?; Ok(state) } pub fn event (&self) -> TuiEvent { self.event.read().unwrap().clone().unwrap() } pub fn buffer (&mut self) -> &mut Buffer { &mut self.buffers[self.buffer] } fn flip (&mut self) { let previous_buffer = &self.buffers[1 - self.buffer]; let current_buffer = &self.buffers[self.buffer]; let updates = previous_buffer.diff(current_buffer); self.backend.draw(updates.into_iter()).expect("failed to render"); self.buffers[1 - self.buffer].reset(); self.buffer = 1 - self.buffer; } pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) { buffer_update(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