#[macro_export] macro_rules! tui_app { ($Struct:ident { $($fields:tt)* }) => { #[dizzle::namespace(bool)] #[dizzle::namespace(u16)] #[dizzle::namespace(Option)] #[dizzle::namespace(Color)] #[derive(Debug, Default)] pub struct $Struct { $($fields)* } tui_main!($Struct { ..Default::default() }); } } /// Implement standard [main] entrypoint for TUI apps. #[macro_export] macro_rules! tui_main { ($state:expr) => { pub fn main () -> Usually<()> { Tui::setup_panic(); Tui::run_main(Arc::new(RwLock::new($state))) } } } /// Enable TUI output for state struct. #[macro_export] macro_rules! tui_view { ($self:ident: $State:ty $body:block) => { impl View for $State { fn view (&$self) -> impl Draw $body } } } #[macro_export] macro_rules! tui_interpret { ($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => { impl Interpret for $State { fn interpret_word <'a> (&'a $self, $to: &mut Tui, $pat: &'a impl Symbol) -> Usually<$Result> { $($body)+ } fn interpret_expr <'a> (&'a self, to: &mut Tui, src: &'a impl Expression) -> Usually<$Result> { Ok(Some(if let Some(area) = eval_view(self, to, src)? { area } else if let Some(area) = eval_view_tui(self, to, src)? { area } else { return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) })) } } }; } /// Enable TUI keyboard input for main state struct. #[macro_export] macro_rules! tui_keys { ($self:ident:$State:ty,$input:ident $($body:tt)+) => { impl Apply> for $State { fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $($body)+ } }; } use crate::{*, lang::*}; mod border; pub use self::border::*; mod event; pub use self::event::*; mod keys; pub use self::keys::*; mod buffer; pub use self::buffer::*; mod repeat; pub use self::repeat::*; mod scroll; pub use self::scroll::*; mod colors; pub use self::colors::*; mod phat; pub use self::phat::*; mod button; pub use self::button::*; //use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; //use rand::distributions::uniform::UniformSampler; pub(crate) use ::{ std::{ io::{stdout, Write}, time::Duration, ops::{Deref, DerefMut}, }, ratatui::{ prelude::{Style, Position, Backend, Color}, style::{Modifier, Color::*}, backend::{CrosstermBackend, ClearType}, layout::{Size, Rect}, buffer::{Buffer, Cell}, crossterm::{ ExecutableCommand, terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, //event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, } }, crossterm::event::{ read, Event, KeyEvent, KeyModifiers, KeyCode, KeyEventKind, KeyEventState }, }; impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } impl AsMut for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } } impl Wide for Tui { fn w (&self) -> u16 { self.1.2 } } impl Tall for Tui { fn h (&self) -> u16 { self.1.3 } } impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } impl Xy for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } } /// Terminal output. pub struct Tui( /// Ratatui buffer; area is screen size pub Buffer, /// Current draw area pub XYWH ); impl Tui { pub fn setup_panic () { use ::std::panic::{set_hook, PanicHookInfo}; use ::better_panic::{Settings, Verbosity}; let panic = Settings::auto() .verbosity(Verbosity::Full) .create_panic_handler(); set_hook(Box::new(move |info: &PanicHookInfo|{ let _ = Tui::teardown(&mut stdout()); panic(info); })); } pub fn run_main (state: Arc>) -> Usually<()> where T: View + Apply> + Send + Sync + 'static { Exit::run(|exit|{ let scan = Duration::from_millis(100); let frame = Duration::from_millis(10); let (_input, output) = Tui::io(exit.as_ref(), &state, scan, frame, std::io::stdout())?; let _ = output.join(); Tui::teardown(&mut stdout()) }) } /// Spawn the TUI input and output threadsl. pub fn io < T: View + Apply> + Send + Sync + 'static, W: Write + Send + Sync + 'static, > ( exited: &Arc, state: &Arc>, poll: Duration, sleep: Duration, output: W, ) -> Result<(Task, Task), Box> { Ok(( Tui::input(exited, state, poll)?, Tui::output(exited, state, sleep, output)?, )) } /// Spawn the TUI input thread which reads keys from the terminal. pub fn input > + Send + Sync + 'static> ( exited: &Arc, state: &Arc>, poll: Duration ) -> Result { let exited = exited.clone(); let state = state.clone(); Task::new_poll(exited.clone(), poll, move |_| { let event = read().unwrap(); if Exit::is(&event) { exited.store(true, Relaxed); } else if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) { panic!("{e}") } }) } pub fn teardown (backend: &mut W) -> Usually<()> { use ::ratatui::backend::Backend; stdout().execute(LeaveAlternateScreen)?; CrosstermBackend::new(backend).show_cursor()?; disable_raw_mode().map_err(Into::into) } pub fn new (width: u16, height: u16) -> Self { Self(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height)) } pub fn resize (&mut self, back: &mut CrosstermBackend, width: u16, height: u16) { let size = Rect { x: 0, y: 0, width, height }; if self.0.area != size { back.clear_region(ClearType::All).unwrap(); self.0.resize(size); self.0.reset(); } } pub fn redraw <'b, W: Write> ( &'b mut self, back: &mut CrosstermBackend, mut next: &'b mut Self ) { let updates = self.0.diff(&next.0); back.draw(updates.into_iter()).expect("failed to render"); Backend::flush(back).expect("failed to flush output new"); std::mem::swap(self, &mut next); next.0.reset(); } pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { for row in 0..self.h() { let y = self.y() + row; for col in 0..self.w() { let x = self.x() + col; if x < self.0.area.width && y < self.0.area.height { if let Some(cell) = self.0.cell_mut(Position { x, y }) { callback(cell, col, row); } } } } self.xywh() } pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option