tengri/src/term.rs
2026-07-10 18:24:44 +03:00

155 lines
5.3 KiB
Rust

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 input; pub use self::input::*;
mod output; pub use self::output::*;
//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
},
};
/// Terminal output.
pub struct Tui(
/// Ratatui buffer; area is screen size
pub Buffer,
/// Current draw area
pub XYWH<u16>
);
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<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } }
impl Wide<u16> for Tui { fn w (&self) -> u16 { self.1.2 } }
impl Tall<u16> for Tui { fn h (&self) -> u16 { self.1.3 } }
impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } }
impl Xy<u16> for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } }
impl Tui {
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 <W: Write> (&mut self, back: &mut CrosstermBackend<W>, 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<W>, 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();
}
}
impl Tui {
pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH<u16> {
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 tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
for cell in self.0.content.iter_mut() {
cell.fg = fg;
cell.bg = bg;
cell.modifier = modifier;
}
}
pub fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
let text = text.as_ref();
let style = style.unwrap_or(Style::default());
if x < self.0.area.width && y < self.0.area.height {
self.0.set_string(x, y, text, style);
}
}
}
/// Implement standard [main] entrypoint for TUI apps.
#[macro_export] macro_rules! tui_main {
($state:expr) => {
pub fn main () -> Usually<()> { tui_run_main(Arc::new(RwLock::new($state))) }
}
}
pub fn tui_run_main <T> (state: Arc<RwLock<T>>) -> Usually<()> where
T: View<Tui> + Apply<TuiEvent, Usually<()>> + 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())?;
output.join();
tui_teardown(&mut stdout())
})
}
pub fn tui_teardown <W: Write> (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 tui_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);
}));
}
/// Spawn the TUI input and output threadsl.
pub fn tui_io <
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static,
W: Write + Send + Sync + 'static,
> (
exited: &Arc<AtomicBool>,
state: &Arc<RwLock<T>>,
poll: Duration,
sleep: Duration,
output: W,
) -> Result<(Task, Task), Box<dyn Error>> {
let keyboard = tui_input(exited, state, poll)?;
let terminal = tui_output(exited, state, sleep, output)?;
Ok((keyboard, terminal))
}