mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-09-18 05:16:44 +02:00
349 lines
12 KiB
Rust
349 lines
12 KiB
Rust
#[macro_export] macro_rules! tui_app {
|
|
($Struct:ident { $($fields:tt)* }) => {
|
|
#[dizzle::namespace(bool)]
|
|
#[dizzle::namespace(u16)]
|
|
#[dizzle::namespace(Option<u16>)]
|
|
#[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<Tui> for $State {
|
|
fn view (&$self) -> impl Draw<Tui> $body
|
|
}
|
|
}
|
|
}
|
|
|
|
#[macro_export] macro_rules! tui_interpret {
|
|
($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => {
|
|
impl Interpret<Tui, $Result> 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<TuiEvent, Usually<()>> 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<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 } }
|
|
/// Terminal output.
|
|
pub struct Tui(
|
|
/// Ratatui buffer; area is screen size
|
|
pub Buffer,
|
|
/// Current draw area
|
|
pub XYWH<u16>
|
|
);
|
|
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 <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())?;
|
|
let _ = output.join();
|
|
Tui::teardown(&mut stdout())
|
|
})
|
|
}
|
|
/// Spawn the TUI input and output threadsl.
|
|
pub fn 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 std::error::Error>> {
|
|
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 <T: Apply<TuiEvent, Usually<()>> + Send + Sync + 'static> (
|
|
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
|
|
) -> Result<Task, std::io::Error> {
|
|
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 <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 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();
|
|
}
|
|
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 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);
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
/// Spawn the TUI output thread which writes colored characters to the terminal.
|
|
///
|
|
/// ```
|
|
/// let state = std::sync::Arc::new(std::sync::RwLock::new(()));
|
|
/// let _ = tengri::Exit::run(|exit|{
|
|
/// tengri::Tui::output(
|
|
/// exit.as_ref(),
|
|
/// &state,
|
|
/// std::time::Duration::from_millis(10),
|
|
/// std::io::stdout()
|
|
/// )
|
|
/// });
|
|
/// ```
|
|
pub fn output <
|
|
W: Write + Send + Sync + 'static, T: View<Tui> + Send + Sync + 'static
|
|
> (
|
|
exited: &Arc<AtomicBool>,
|
|
state: &Arc<RwLock<T>>,
|
|
sleep: Duration,
|
|
output: W,
|
|
) -> Usually<Task> {
|
|
let state = state.clone();
|
|
stdout().execute(EnterAlternateScreen)?;
|
|
CrosstermBackend::new(stdout()).hide_cursor()?;
|
|
enable_raw_mode()?;
|
|
let mut backend = CrosstermBackend::new(output);
|
|
let Size { width, height } = backend.size().expect("get size failed");
|
|
let mut prev = Tui::new(width, height);
|
|
let mut next = Tui::new(width, height);
|
|
Ok(Task::new_sleep(exited.clone(), sleep, move |perf| {
|
|
let Size { width, height } = backend.size().expect("get size failed");
|
|
if let Ok(state) = state.try_read() {
|
|
prev.resize(&mut backend, width, height);
|
|
state.view().draw(&mut next).expect("draw failed"); // TODO draw error
|
|
prev.redraw(&mut backend, &mut next);
|
|
}
|
|
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
|
|
prev.set_string(0, 0, &timer, Style::default());
|
|
})?)
|
|
}
|
|
/// Draw TUI content or its error message.
|
|
///
|
|
/// ```
|
|
/// for variant in [
|
|
/// Ok(Some("hello")),
|
|
/// Ok(None),
|
|
/// Err("fail".into()),
|
|
/// ] {
|
|
/// let _ = tengri::Tui::catcher(variant);
|
|
/// }
|
|
/// ```
|
|
pub fn catcher <T: Draw<Tui>> (result: Usually<T>) -> impl Draw<Tui> {
|
|
thunk(move|to: &mut Tui|match result {
|
|
Ok(content) => content.draw(to),
|
|
Err(e) => {
|
|
let err_fg = Color::Rgb(255,224,244);
|
|
let err_bg = Color::Rgb(96, 24, 24);
|
|
let title = east(bold(true, "upsi daisy. "), "rendering failed.");
|
|
let error = east("\"why?\" ", bold(true, format!("{e}")));
|
|
fg(err_fg, bg(err_bg, south(title, error))).draw(to)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
impl Screen for Tui {
|
|
type Unit = u16;
|
|
/// Render drawable in subarea specified by `area`
|
|
fn show (&mut self, content: impl Draw<Self>) -> Perhaps<XYWH<u16>> {
|
|
let previous_area = self.1;
|
|
Ok(if let Some(area) = content.layout(self.1)? {
|
|
self.1 = area;
|
|
if let Some(result_area) = content.draw(self)? {
|
|
self.1 = previous_area;
|
|
Some(result_area)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
})
|
|
}
|
|
|
|
/// Get current clipping area
|
|
fn area (&self) -> XYWH<Self::Unit> {
|
|
self.1
|
|
}
|
|
|
|
fn clip <T> (
|
|
&mut self,
|
|
area: impl Into<Option<XYWH<u16>>>,
|
|
draw: impl FnOnce(&mut Self)->T
|
|
) -> T {
|
|
let prev = self.1;
|
|
if let Some(area) = area.into() {
|
|
self.1 = area.into();
|
|
}
|
|
let result = draw(self);
|
|
self.1 = prev;
|
|
result
|
|
}
|
|
}
|
|
|
|
pub const fn fill_char (c: char) -> impl Draw<Tui> {
|
|
thunk(move|to: &mut Tui|Ok(Some(to.update(&|cell,_,_|{
|
|
cell.set_char(c);
|
|
}))))
|
|
}
|
|
|
|
/// Draw contents with modifier applied.
|
|
pub const fn modify (on: bool, modifier: Modifier, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
|
thunk(move|to: &mut Tui|{
|
|
fill_mod(on, modifier).draw(to)?;
|
|
draw.draw(to)
|
|
})
|
|
}
|
|
|
|
pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw<Tui> {
|
|
thunk(move|to: &mut Tui|Ok(Some({
|
|
if on {
|
|
to.update(&|cell,_,_|cell.modifier.insert(modifier))
|
|
} else {
|
|
to.update(&|cell,_,_|cell.modifier.remove(modifier))
|
|
}
|
|
})))
|
|
}
|
|
|
|
/// Draw contents with bold modifier applied.
|
|
pub const fn bold (on: bool, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
|
modify(on, Modifier::BOLD, draw)
|
|
}
|