#![feature(type_changing_struct_update, trait_alias)] use std::{time::Duration, thread::{spawn, JoinHandle}}; use unicode_width::*; pub use ::{ dizzle, tengri_input, tengri_output, ratatui, crossterm, palette, better_panic, }; pub(crate) use ::{ dizzle::*, tengri_input::*, tengri_output::*, atomic_float::AtomicF64, std::{io::{stdout, Stdout}, sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}}, better_panic::{Settings, Verbosity}, palette::{*, convert::*, okhsl::*}, ratatui::{ prelude::{Color, Style, Buffer, Position}, style::{Stylize, Modifier, Color::*}, backend::{Backend, CrosstermBackend, ClearType}, layout::{Size, Rect}, buffer::Cell }, crossterm::{ ExecutableCommand, terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, } }; #[macro_export] macro_rules! tui_main { ($expr:expr) => { fn main () -> Usually<()> { let state = Arc::new(RwLock::new($expr)); tengri_tui::Tui::new().unwrap().run(&state)?; Ok(()) } }; } #[macro_export] macro_rules! has_color { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? { fn color (&$self) -> ItemColor { $cb } } } } macro_rules! border { ($($T:ident { $nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal $($x:tt)* }),+) => {$( impl BorderStyle for $T { const NW: &'static str = $nw; const N: &'static str = $n; const NE: &'static str = $ne; const W: &'static str = $w; const E: &'static str = $e; const SW: &'static str = $sw; const S: &'static str = $s; const SE: &'static str = $se; $($x)* fn enabled (&self) -> bool { self.0 } } #[derive(Copy, Clone)] pub struct $T(pub bool, pub Style); impl Layout for $T {} impl Draw for $T { fn draw (&self, to: &mut TuiOut) { if self.enabled() { let _ = BorderStyle::draw(self, to); } } } )+} } mod tui_structs; pub use self::tui_structs::*; mod tui_traits; pub use self::tui_traits::*; mod tui_impls; pub use self::tui_impls::*; /// Run an app in the main loop. pub fn tui_run (state: &Arc>) -> Usually>> { let backend = CrosstermBackend::new(stdout()); let Size { width, height } = backend.size()?; let tui = Arc::new(RwLock::new(Tui { exited: Arc::new(AtomicBool::new(false)), buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }), area: [0, 0, width, height], perf: Default::default(), backend, })); let _input_thread = tui_input(tui, state, Duration::from_millis(100)); tui.write().unwrap().setup()?; let render_thread = tui_output(tui, state, Duration::from_millis(10))?; match render_thread.join() { Ok(result) => { tui.write().unwrap().teardown()?; println!("\n\rRan successfully: {result:?}\n\r"); }, Err(error) => { tui.write().unwrap().teardown()?; panic!("\n\rDraw thread failed: error={error:?}.\n\r") }, } Ok(tui) } pub fn tui_setup (backend: &mut CrosstermBackend) -> 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)?; backend.hide_cursor()?; enable_raw_mode().map_err(Into::into) } pub fn tui_teardown (backend: &mut CrosstermBackend) -> Usually<()> { stdout().execute(LeaveAlternateScreen)?; backend.show_cursor()?; disable_raw_mode().map_err(Into::into) } pub fn tui_resized ( backend: &mut CrosstermBackend, buffer: &mut Buffer, size: ratatui::prelude::Rect ) { if buffer.area != size { backend.clear_region(ClearType::All).unwrap(); buffer.resize(size); buffer.reset(); } } pub fn tui_redrawn ( backend: &mut CrosstermBackend, buffer: &mut Buffer, new_buffer: &mut Buffer ) { let updates = buffer.diff(&new_buffer); backend.draw(updates.into_iter()).expect("failed to render"); backend.flush().expect("failed to flush output new_buffer"); std::mem::swap(&mut buffer, &mut new_buffer); new_buffer.reset(); } pub fn tui_update (buf: &mut Buffer, area: XY, callback: &impl Fn(&mut Cell, u16, u16)) { for row in 0..area.h() { let y = area.y() + row; for col in 0..area.w() { let x = area.x() + col; if x < buf.area.width && y < buf.area.height { if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) { callback(cell, col, row); } } } } } /// Spawn the output thread. pub fn tui_output + Send + Sync + 'static> ( engine: &Arc>, state: &Arc>, timer: Duration ) -> Result, std::io::Error> { let exited = engine.read().unwrap().exited.clone(); let engine = engine.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 }); std::thread::Builder::new() .name("tui output thread".into()) .spawn(move || loop { if exited.fetch_and(true, Relaxed) { break } let t0 = engine.read().unwrap().perf.get_t0(); 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 = TuiOut { buffer, area: XYWH(0, 0, width, height) }; state.draw(&mut output); buffer = engine.write().unwrap().flip(output.buffer, size); } let t1 = (*engine.read().unwrap()).perf.get_t1(t0).unwrap(); buffer.set_string(0, 0, &format!("{:>3}.{:>3}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default()); std::thread::sleep(timer); }) } /// Spawn the input thread. pub fn tui_input + Send + Sync + 'static> ( engine: &Arc>, state: &Arc>, timer: Duration ) -> JoinHandle<()> { let exited = engine.read().unwrap().exited.clone(); let state = state.clone(); spawn(move || loop { if exited.fetch_and(true, Relaxed) { break } if poll(timer).is_ok() { let event = read().unwrap(); match event { Event::Key(KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE }) => { exited.store(true, Relaxed); }, _ => { let exited = exited.clone(); let event = TuiEvent::from_crossterm(event); if let Err(e) = state.write().unwrap().handle(&TuiIn { exited, event }) { panic!("{e}") } } } } }) } #[cfg(feature = "dsl")] pub fn evaluate_output_expression_tui <'a, S> ( state: &S, output: &mut TuiOut, expr: impl Expression + 'a ) -> Usually where S: View + for<'b>Namespace<'b, bool> + for<'b>Namespace<'b, u16> + for<'b>Namespace<'b, Color> { // See `tengri_output::evaluate_output_expression` let head = expr.head()?; let mut frags = head.src()?.unwrap_or_default().split("/"); let args = expr.tail(); let arg0 = args.head(); let tail0 = args.tail(); let arg1 = tail0.head(); let tail1 = tail0.tail(); let _arg2 = tail1.head(); match frags.next() { Some("text") => if let Some(src) = args?.src()? { output.place(&src) }, Some("fg") => { let arg0 = arg0?.expect("fg: expected arg 0 (color)"); output.place(&Tui::fg( Namespace::::resolve(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")), Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()), )) }, Some("bg") => { let arg0 = arg0?.expect("bg: expected arg 0 (color)"); output.place(&Tui::bg( Namespace::::resolve(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")), Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()), )) }, _ => return Ok(false) }; Ok(true) } pub fn named_key (token: &str) -> Option { use KeyCode::*; Some(match token { "up" => Up, "down" => Down, "left" => Left, "right" => Right, "esc" | "escape" => Esc, "enter" | "return" => Enter, "delete" | "del" => Delete, "backspace" => Backspace, "tab" => Tab, "space" => Char(' '), "comma" => Char(','), "period" => Char('.'), "plus" => Char('+'), "minus" | "dash" => Char('-'), "equal" | "equals" => Char('='), "underscore" => Char('_'), "backtick" => Char('`'), "lt" => Char('<'), "gt" => Char('>'), "cbopen" | "openbrace" => Char('{'), "cbclose" | "closebrace" => Char('}'), "bropen" | "openbracket" => Char('['), "brclose" | "closebracket" => Char(']'), "pgup" | "pageup" => PageUp, "pgdn" | "pagedown" => PageDown, "f1" => F(1), "f2" => F(2), "f3" => F(3), "f4" => F(4), "f5" => F(5), "f6" => F(6), "f7" => F(7), "f8" => F(8), "f9" => F(9), "f10" => F(10), "f11" => F(11), "f12" => F(12), _ => return None, }) } pub fn button_2 <'a> (key: impl Content, label: impl Content, editing: bool) -> impl Content { Tui::bold(true, Bsp::e( Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(Tui::g(96), &"▐")))), When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)))) } pub fn button_3 <'a> ( key: impl Content, label: impl Content, value: impl Content, editing: bool, ) -> impl Content { Tui::bold(true, Bsp::e( Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "▐")))), Bsp::e( When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &"▐"),)), Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &"▌"), )))) } border! { Square { "┌" "─" "┐" "│" "│" "└" "─" "┘" fn style (&self) -> Option