From 7a0459756e419dee035eed9f1efc98523688ce43 Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Sat, 25 Jul 2026 07:13:21 +0300 Subject: [PATCH 1/8] draw: try to fix splits --- src/draw/split.rs | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/draw/split.rs b/src/draw/split.rs index 1cee49c..2ee67b4 100644 --- a/src/draw/split.rs +++ b/src/draw/split.rs @@ -50,19 +50,36 @@ impl Split { thunk(move|to: &mut S|{ let (area_a, area_b) = to.xywh().split_half(self); let (origin_a, origin_b) = self.origins(); - let a = a.align(origin_a); - let b = b.align(origin_b); - match self { - Self::Below => { - to.show(area(area_b, b))?; - to.show(area(area_b, a))?; - }, - _ => { - to.show(area(area_a, a))?; - to.show(area(area_b, b))?; + let (a, b) = match self { + Self::Below => ( + to.show(area(area_b, b.align(origin_b)))?, + to.show(area(area_b, a.align(origin_a)))?, + ), + _ => ( + to.show(area(area_a, a.align(origin_a)))?, + to.show(area(area_b, b.align(origin_b)))?, + ) + }; + Ok(if let (Some(XYWH(xa, ya, wa, ha)), Some(XYWH(xb, yb, wb, hb))) = (a, b) { + match self { + Self::South => + Some(XYWH(xa.min(xb), ya, wa.max(wb), ha + hb)), + Self::East => + Some(XYWH(xa, ya.min(yb), wa + wb, ha.max(hb))), + Self::North => + Some(XYWH(xa.min(xb), yb, wa.max(wb), ha + hb)), + Self::West => + Some(XYWH(xb, ya.min(yb), wa + wb, ha.max(hb))), + Self::Above | Self::Below => + Some(XYWH(xa.min(xb), ya.min(yb), wa.max(wb), ha.max(hb))), } - } - Ok(Some(to.xywh())) // FIXME: compute and return actually used area + } else if let Some(a) = a { + Some(a) + } else if let Some(b) = b { + Some(b) + } else { + None + }) }) } @@ -89,8 +106,8 @@ impl Split { Self::East => (E, W), Self::North => (N, S), Self::West => (W, E), - Self::Above => (C, C), - Self::Below => (C, C), + Self::Above => (C, C), + Self::Below => (C, C), } } From be98e666f53eedf95618396239641ad846a772bd Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Sat, 25 Jul 2026 07:13:28 +0300 Subject: [PATCH 2/8] dizzle: use procmacro in examples --- Cargo.lock | 18 ++++++++++++++---- dizzle | 2 +- examples/tui_00.rs | 33 +++++++++++++++++++++------------ proc/src/lib.rs | 20 -------------------- shell.nix | 11 ++++++----- src/eval.rs | 25 +++++++------------------ src/term.rs | 5 ++++- 7 files changed, 53 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 877cb78..ac7fc49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,11 +524,21 @@ name = "dizzle" version = "0.1.0" dependencies = [ "const_panic", + "dizzle_proc", "itertools 0.14.0", "konst", "thiserror 2.0.18", ] +[[package]] +name = "dizzle_proc" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlib" version = "0.5.2" @@ -1573,9 +1583,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -2001,9 +2011,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", diff --git a/dizzle b/dizzle index 0050385..060c517 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 0050385e63ea5d3ab9be7c495dae5fc635396cf0 +Subproject commit 060c51757ccce141f751a79d8b9b2d5af3955f66 diff --git a/examples/tui_00.rs b/examples/tui_00.rs index 8aedd9f..3c5f3d9 100644 --- a/examples/tui_00.rs +++ b/examples/tui_00.rs @@ -5,7 +5,7 @@ use ::{ }; tui_main!(State { - cursor: 10, + cursor: 0, ..Default::default() }); @@ -20,11 +20,14 @@ tui_view!(|self: State| { let heading = format!("State {}/{} in {:?}", index, VIEWS.len(), &wh); let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_y(1)); let code = bg(Color::Rgb(10, 60, 10), format!("{}", src).align_n().push_y(2)); - //let content = ;//();//bg(Color::Rgb(10, 10, 60), View(self, CstIter::new(src))); - let widget = thunk(move|to: &mut Tui|self.interpret(to, &src).map(Some)); + let widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); self.size.of(south(title, north(code, widget))) }); +#[dizzle::namespace(bool)] +#[dizzle::namespace(u16)] +#[dizzle::namespace(Option)] +#[dizzle::namespace(Color)] #[derive(Debug, Default)] struct State { /** Command history (undo/redo). */ @@ -35,7 +38,16 @@ struct State { size: Sizer, } -impl Interpret> for State { +impl Interpret>> for State { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { + Ok(Some(if let Some(area) = eval_view(self, to, lang)? { + area + } else if let Some(area) = eval_view_tui(self, to, lang)? { + area + } else { + return Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) + })) + } } #[derive(Debug)] enum Action { @@ -45,11 +57,6 @@ impl Interpret> for State { Prev } -//impl_from!(Action: |input: &TuiIn| todo!()); - -fn draw_example (state: &State, to: &mut Tui) { -} - impl Action { const BINDS: &'static str = stringify! { @@ -155,9 +162,11 @@ const VIEWS: &'static [&'static str] = &[ ]; -//namespace!(State: bool {}); -//namespace!(State: u16 {}); -//namespace!(State: Color {}); //handle!(TuiIn: |self: State, input|Action::from(input).eval(self).map(|_|None)); //view!(State: Tui: [ evaluate_output_expression, evaluate_output_expression_tui ]); //draw!(State: Tui: [ draw_example ]); + +//impl_from!(Action: |input: &TuiIn| todo!()); + +//fn draw_example (state: &State, to: &mut Tui) { +//} diff --git a/proc/src/lib.rs b/proc/src/lib.rs index 7ae7699..1c31c78 100644 --- a/proc/src/lib.rs +++ b/proc/src/lib.rs @@ -50,26 +50,6 @@ pub fn view (meta: TokenStream, item: TokenStream) -> TokenStream { )) } -pub(crate) fn write (t: T) -> TokenStream { - let mut out = TokenStream2::new(); - t.to_tokens(&mut out); - out.into() -} - -pub(crate) fn write_quote (quote: TokenStream2) -> TokenStream2 { - let mut out = TokenStream2::new(); - for token in quote { - out.append(token); - } - out -} - -pub(crate) fn write_quote_to (out: &mut TokenStream2, quote: TokenStream2) { - for token in quote { - out.append(token); - } -} - #[cfg(test)] #[test] fn test_proc_view () { let x: crate::proc_view::ViewMeta = pq! { SomeOut }; let output: Ident = pq! { SomeOut }; diff --git a/shell.nix b/shell.nix index 6f44581..f3e43dc 100644 --- a/shell.nix +++ b/shell.nix @@ -1,13 +1,14 @@ #!/usr/bin/env nix-shell {pkgs?import{}}:let - name = "tengri"; - stdenv = pkgs.clang19Stdenv; - LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath []; + name = "tengri"; + stdenv = pkgs.clang19Stdenv; in pkgs.mkShell.override { inherit stdenv; } { - inherit name LIBCLANG_PATH LD_LIBRARY_PATH; + LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.pipewire.jack + ]; buildInputs = [ pkgs.libclang pkgs.jack2 diff --git a/src/eval.rs b/src/eval.rs index 67f565e..168a5ce 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -146,7 +146,9 @@ pub fn eval_view <'a, O: Screen + 'a, S> ( Some("align") => thunk(move|output: &mut O|{ state.interpret(output, &arg0) }).align(eval_enum!("align", output, state, frags.next(), arg0, Azimuth { - "c" => C, "n" => N, "s" => S, "e" => E, "w" => W, "x" => X, "y" => Y + "c" => C, "x" => X, "y" => Y, + "n" => N, "s" => S, "e" => E, "w" => W, + "nw" => NW, "sw" => SW, "ne" => NE, "se" => SE, })).draw(output), Some("exact") => eval_xy!( "exact" => expr, head, output, state, frags.next(), exact_wh, exact_w, exact_h, arg0, arg1, arg2, @@ -176,27 +178,14 @@ pub fn eval_view <'a, O: Screen + 'a, S> ( /// ``` /// use tengri::{*, lang::*, ratatui::prelude::Color}; /// -/// #[namespace(bool {})] -/// #[namespace(u8: try_to_u8)] -/// #[namespace(u16: try_to_u16)] -/// #[namespace(Color: try_to_color)] +/// #[namespace(bool)] +/// #[namespace(u8 try_to_u8)] +/// #[namespace(u16 try_to_u16)] +/// #[namespace(Color try_to_color)] /// #[interpret(Tui -> Option>: try_eval_tui)] /// struct State; /// tengri::lang::primitive!(u8: try_to_u8); /// tengri::lang::primitive!(u16: try_to_u16); -/// tengri::lang::namespace!(State: bool { -/// }); -/// tengri::lang::namespace!(State: u8 { -/// literal = |x|try_to_u8(x); -/// }); -/// tengri::lang::namespace!(State: u16 { -/// literal = |x|try_to_u16(x); -/// }); -/// tengri::lang::namespace!(State: Color { -/// expression = |_state| { -/// "g" (x: u8) => { ItemTheme::G[x as usize].base.term } -/// }; -/// }); /// tengri::lang::interpret!(|self: State, context: Tui, lang|->Option>{ /// expression = { /// "text" (...rest) => { todo!() } diff --git a/src/term.rs b/src/term.rs index a289e66..24364b7 100644 --- a/src/term.rs +++ b/src/term.rs @@ -103,7 +103,10 @@ impl Tui { /// 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 main () -> Usually<()> { + tui_setup_panic(); + tui_run_main(Arc::new(RwLock::new($state))) + } } } From ad1a514eddbf3104b7631dedef786389863988bf Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Sat, 25 Jul 2026 08:46:37 +0300 Subject: [PATCH 3/8] bump dizzle --- dizzle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dizzle b/dizzle index 060c517..e976853 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 060c51757ccce141f751a79d8b9b2d5af3955f66 +Subproject commit e9768535f35cd298a7b1035825e86cd13347792d From 4cfe8d087c3261644e45c7805f47fca3ee2fd0fa Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Sat, 25 Jul 2026 08:54:57 +0300 Subject: [PATCH 4/8] tui_00: runs again --- examples/tui_00.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/tui_00.rs b/examples/tui_00.rs index 3c5f3d9..eae8dd9 100644 --- a/examples/tui_00.rs +++ b/examples/tui_00.rs @@ -39,6 +39,14 @@ struct State { } impl Interpret>> for State { + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Symbol) -> Drawn { + match lang.src()? { + Some(":hello") => "hello".draw(to), + Some(":world") => "world".draw(to), + Some(":hello-world") => "Hello World!".draw(to), + _ => todo!() + } + } fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { Ok(Some(if let Some(area) = eval_view(self, to, lang)? { area From 66ac2bcbb6301bfb68ca0abfb6c392883a39c508 Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Sat, 25 Jul 2026 15:13:05 +0300 Subject: [PATCH 5/8] more entrypoint macros --- Cargo.lock | 28 +++ Justfile | 7 +- dizzle | 2 +- examples/mode_00.rs | 3 + examples/mode_01.rs | 128 ++++++++++ examples/{tui_01.rs => mode_02.rs} | 1 + examples/tui_00.rs | 180 -------------- src/draw.rs | 19 +- src/eval.rs | 1 + src/lib.rs | 1 - src/term.rs | 346 +++++++++++++++++++++------ src/term/button.rs | 29 +++ src/term/colors.rs | 80 +++++++ src/term/input.rs | 39 ---- src/term/output.rs | 364 ----------------------------- src/term/phat.rs | 24 ++ src/term/repeat.rs | 43 ++++ src/term/scroll.rs | 64 +++++ 18 files changed, 696 insertions(+), 663 deletions(-) create mode 100644 examples/mode_00.rs create mode 100644 examples/mode_01.rs rename examples/{tui_01.rs => mode_02.rs} (99%) delete mode 100644 examples/tui_00.rs create mode 100644 src/term/button.rs create mode 100644 src/term/colors.rs delete mode 100644 src/term/input.rs delete mode 100644 src/term/output.rs create mode 100644 src/term/phat.rs create mode 100644 src/term/repeat.rs create mode 100644 src/term/scroll.rs diff --git a/Cargo.lock b/Cargo.lock index ac7fc49..bd5f287 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -527,6 +527,7 @@ dependencies = [ "dizzle_proc", "itertools 0.14.0", "konst", + "peg", "thiserror 2.0.18", ] @@ -1390,6 +1391,33 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "peg" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" + [[package]] name = "percent-encoding" version = "2.3.2" diff --git a/Justfile b/Justfile index 8d7c790..b233be1 100644 --- a/Justfile +++ b/Justfile @@ -30,7 +30,10 @@ doc: cargo doc example-tui-00: - cargo run --example tui_00 + cargo run --example mode_0 example-tui-01: - cargo run --example tui_01 + cargo run --example mode_1 + +example-tui-02: + cargo run --example mode_2 diff --git a/dizzle b/dizzle index e976853..e4e01c0 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit e9768535f35cd298a7b1035825e86cd13347792d +Subproject commit e4e01c025befd4fd7ed217fa5cc492373f5d7e3b diff --git a/examples/mode_00.rs b/examples/mode_00.rs new file mode 100644 index 0000000..9ab858c --- /dev/null +++ b/examples/mode_00.rs @@ -0,0 +1,3 @@ +//! Mode 0: Direct draw +use ::{std::sync::{Arc, RwLock}, ratatui::style::Color, tengri::*}; +fn main () {} diff --git a/examples/mode_01.rs b/examples/mode_01.rs new file mode 100644 index 0000000..6f1173f --- /dev/null +++ b/examples/mode_01.rs @@ -0,0 +1,128 @@ +//! Mode 01 +use ::std::sync::{Arc, RwLock}; +use ::crossterm::event::{Event::*, KeyEvent, KeyCode::*}; +use ::ratatui::style::Color; +use ::tengri::{*, lang::*}; +tui_app!(State { + /** Command history (undo/redo). */ + history: Vec, + /** User-controllable value. */ + cursor: usize, + /** Rendered window size. */ + size: Sizer, +}); +tui_keys!(self: State, input { + Ok(if let Key(KeyEvent { code, .. }) = input.0 { + match code { + Up | Right => { self.next()?.map(|x|self.history.push(x)); }, + Down | Left => { self.prev()?.map(|x|self.history.push(x)); }, + _ => {} + } + }) +}); +tui_view!(self: State { + let index = self.cursor + 1; + let wh = (self.size.w(), self.size.h()); + let src = VIEWS.get(self.cursor).unwrap_or(&""); + let heading = format!("State {}/{} in {:?}", index, VIEWS.len(), &wh); + let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_y(1)); + let code = bg(Color::Rgb(10, 60, 10), format!("{}", src).align_n().push_y(2)); + let widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); + self.size.of(south(title, north(code, widget))) +}); +tui_ns!(self: State, to, src { + match src.src()? { + Some(":foo") => "foo".draw(to), + Some(":bar") => "bar".draw(to), + Some(":foobar") => "FOOBAR".draw(to), + _ => todo!() + } +}); +#[derive(Debug)] +enum Action { + /** Increment cursor */ Next, + /** Decrement cursor */ Prev, +} +impl Action { + fn eval (&self, state: &mut State) -> Perhaps { + use Action::*; + match self { Next => state.next(), Prev => state.prev(), } + } +} +impl State { + fn next (&mut self) -> Perhaps { + self.cursor = (self.cursor + 1) % VIEWS.len(); + Ok(Some(Action::Prev)) + } + fn prev (&mut self) -> Perhaps { + self.cursor = if self.cursor > 0 { self.cursor - 1 } else { VIEWS.len() - 1 }; + Ok(Some(Action::Next)) + } +} +const VIEWS: &'static [&'static str] = &[ + stringify! { :foobar }, + stringify! { (bg (g 8) :foobar) }, + stringify! { (fill/xy :foobar) }, + stringify! { (bsp/s :foo :bar) }, + stringify! { (fixed/xy 20 10 :foobar) }, + stringify! { (bsp/s (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, + stringify! { (bsp/e (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, + stringify! { (bsp/n (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, + stringify! { (bsp/w (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, + stringify! { (bsp/a (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, + stringify! { (bsp/b (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, + stringify! { + (bsp/s + (bsp/e (align/nw (fixed/xy 5 3 :foo)) + (bsp/e (align/n (fixed/xy 5 3 :foo)) + (align/ne (fixed/xy 5 3 :foo)))) + (bsp/s + (bsp/e (align/w (fixed/xy 5 3 :foo)) + (bsp/e (align/c (fixed/xy 5 3 :foo)) + (align/e (fixed/xy 5 3 :foo)))) + (bsp/e (align/sw (fixed/xy 5 3 :foo)) + (bsp/e (align/s (fixed/xy 5 3 :foo)) + (align/se (fixed/xy 5 3 :foo)))))) + }, + stringify! { + (bsp/s + (bsp/e (fixed/xy 8 5 (align/nw :foo)) + (bsp/e (fixed/xy 8 5 (align/n :foo)) + (fixed/xy 8 5 (align/ne :foo)))) + (bsp/s + (bsp/e (fixed/xy 8 5 (align/w :foo)) + (bsp/e (fixed/xy 8 5 (align/c :foo)) + (fixed/xy 8 5 (align/e :foo)))) + (bsp/e (fixed/xy 8 5 (align/sw :foo)) + (bsp/e (fixed/xy 8 5 (align/s :foo)) + (fixed/xy 8 5 (align/se :foo)))))) + }, + stringify! { + (bsp/s + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/nw :foo))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/n :foo))) + (grow/xy 1 1 (fixed/xy 8 5 (align/ne :foo))))) + (bsp/s + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/w :foo))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/c :foo))) + (grow/xy 1 1 (fixed/xy 8 5 (align/e :foo))))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/sw :foo))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/s :foo))) + (grow/xy 1 1 (fixed/xy 8 5 (align/se :foo))))))) + }, + stringify! { :map-e }, + stringify! { (align/c :map-e) }, + stringify! { :map-s }, + stringify! { (align/c :map-s) }, + stringify! { + (align/c (bg/behind :bg0 (margin/xy 1 1 (col + (bg/behind :bg1 (border/around :border1 (margin/xy 2 1 :label1))) + (bg/behind :bg2 (border/around :border2 (margin/xy 4 2 :label2))) + (bg/behind :bg3 (border/around :border3 (margin/xy 6 3 :label3))))))) + }, +]; +//handle!(TuiIn: |self: State, input|Action::from(input).eval(self).map(|_|None)); + //view!(State: Tui: [ evaluate_output_expression, evaluate_output_expression_tui ]); + //draw!(State: Tui: [ draw_example ]); +//impl_from!(Action: |input: &TuiIn| todo!()); +//fn draw_example (state: &State, to: &mut Tui) {} diff --git a/examples/tui_01.rs b/examples/mode_02.rs similarity index 99% rename from examples/tui_01.rs rename to examples/mode_02.rs index 9fa7a93..40fba02 100644 --- a/examples/tui_01.rs +++ b/examples/mode_02.rs @@ -1,3 +1,4 @@ +//! Mode 02 use ::{std::sync::{Arc, RwLock}, ratatui::style::Color, tengri::*}; fn main () {} diff --git a/examples/tui_00.rs b/examples/tui_00.rs deleted file mode 100644 index eae8dd9..0000000 --- a/examples/tui_00.rs +++ /dev/null @@ -1,180 +0,0 @@ -use ::{ - std::{io::stdout, sync::{Arc, RwLock}}, - ratatui::style::Color, - tengri::{*, lang::*}, -}; - -tui_main!(State { - cursor: 0, - ..Default::default() -}); - -tui_keys!(|self: State, input| { - Ok(()) -}); - -tui_view!(|self: State| { - let index = self.cursor + 1; - let wh = (self.size.w(), self.size.h()); - let src = VIEWS.get(self.cursor).unwrap_or(&""); - let heading = format!("State {}/{} in {:?}", index, VIEWS.len(), &wh); - let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_y(1)); - let code = bg(Color::Rgb(10, 60, 10), format!("{}", src).align_n().push_y(2)); - let widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); - self.size.of(south(title, north(code, widget))) -}); - -#[dizzle::namespace(bool)] -#[dizzle::namespace(u16)] -#[dizzle::namespace(Option)] -#[dizzle::namespace(Color)] -#[derive(Debug, Default)] -struct State { - /** Command history (undo/redo). */ - history: Vec, - /** User-controllable value. */ - cursor: usize, - /** Rendered window size. */ - size: Sizer, -} - -impl Interpret>> for State { - fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Symbol) -> Drawn { - match lang.src()? { - Some(":hello") => "hello".draw(to), - Some(":world") => "world".draw(to), - Some(":hello-world") => "Hello World!".draw(to), - _ => todo!() - } - } - fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { - Ok(Some(if let Some(area) = eval_view(self, to, lang)? { - area - } else if let Some(area) = eval_view_tui(self, to, lang)? { - area - } else { - return Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) - })) - } -} - -#[derive(Debug)] enum Action { - /** Increment cursor */ - Next, - /** Decrement cursor */ - Prev -} - -impl Action { - - const BINDS: &'static str = stringify! { - (@left prev) - (@right next) - }; - - fn eval (&self, state: &mut State) -> Perhaps { - use Action::*; - match self { Next => Self::next(state), Prev => Self::prev(state), } - } - - fn next (state: &mut State) -> Perhaps { - state.cursor = (state.cursor + 1) % VIEWS.len(); - Ok(Some(Self::Prev)) - } - - fn prev (state: &mut State) -> Perhaps { - state.cursor = if state.cursor > 0 { state.cursor - 1 } else { VIEWS.len() - 1 }; - Ok(Some(Self::Next)) - } - -} - -const VIEWS: &'static [&'static str] = &[ - - stringify! { :hello-world }, - - stringify! { (fill/xy :hello-world) }, - - stringify! { (bsp/s :hello :world) }, - - stringify! { (fixed/xy 20 10 :hello-world) }, - - stringify! { (bsp/s (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, - - stringify! { (bsp/e (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, - - stringify! { (bsp/n (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, - - stringify! { (bsp/w (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, - - stringify! { (bsp/a (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, - - stringify! { (bsp/b (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, - - stringify! { - (bsp/s - (bsp/e (align/nw (fixed/xy 5 3 :hello)) - (bsp/e (align/n (fixed/xy 5 3 :hello)) - (align/ne (fixed/xy 5 3 :hello)))) - (bsp/s - (bsp/e (align/w (fixed/xy 5 3 :hello)) - (bsp/e (align/c (fixed/xy 5 3 :hello)) - (align/e (fixed/xy 5 3 :hello)))) - (bsp/e (align/sw (fixed/xy 5 3 :hello)) - (bsp/e (align/s (fixed/xy 5 3 :hello)) - (align/se (fixed/xy 5 3 :hello)))))) - }, - - stringify! { - (bsp/s - (bsp/e (fixed/xy 8 5 (align/nw :hello)) - (bsp/e (fixed/xy 8 5 (align/n :hello)) - (fixed/xy 8 5 (align/ne :hello)))) - (bsp/s - (bsp/e (fixed/xy 8 5 (align/w :hello)) - (bsp/e (fixed/xy 8 5 (align/c :hello)) - (fixed/xy 8 5 (align/e :hello)))) - (bsp/e (fixed/xy 8 5 (align/sw :hello)) - (bsp/e (fixed/xy 8 5 (align/s :hello)) - (fixed/xy 8 5 (align/se :hello)))))) - }, - - stringify! { - (bsp/s - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/nw :hello))) - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/n :hello))) - (grow/xy 1 1 (fixed/xy 8 5 (align/ne :hello))))) - (bsp/s - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/w :hello))) - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/c :hello))) - (grow/xy 1 1 (fixed/xy 8 5 (align/e :hello))))) - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/sw :hello))) - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/s :hello))) - (grow/xy 1 1 (fixed/xy 8 5 (align/se :hello))))))) - }, - - stringify! { :map-e }, - - stringify! { (align/c :map-e) }, - - stringify! { :map-s }, - - stringify! { (align/c :map-s) }, - - stringify! { - (align/c (bg/behind :bg0 (margin/xy 1 1 (col - (bg/behind :bg1 (border/around :border1 (margin/xy 2 1 :label1))) - (bg/behind :bg2 (border/around :border2 (margin/xy 4 2 :label2))) - (bg/behind :bg3 (border/around :border3 (margin/xy 6 3 :label3))))))) - }, - -]; - - //handle!(TuiIn: |self: State, input|Action::from(input).eval(self).map(|_|None)); - //view!(State: Tui: [ evaluate_output_expression, evaluate_output_expression_tui ]); - //draw!(State: Tui: [ draw_example ]); - -//impl_from!(Action: |input: &TuiIn| todo!()); - -//fn draw_example (state: &State, to: &mut Tui) { -//} diff --git a/src/draw.rs b/src/draw.rs index a672576..38eaa78 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -7,13 +7,22 @@ use crate::*; /// struct TestOut { w: u16, h: u16 }; /// impl Wide for TestOut {} /// impl Tall for TestOut {} -/// impl Xy for TestOut { fn x (&self) -> u16 { 0 } fn y (&self) -> u16 { 0 } } +/// impl Xy for TestOut { +/// fn x (&self) -> u16 { 0 } +/// fn y (&self) -> u16 { 0 } +/// } /// impl Screen for TestOut { /// type Unit = u16; -/// fn show > (&mut self, _: D) -> Drawn { -/// println!("placed"); -/// Ok(None) -/// } +/// fn show (&mut self, _: impl Draw) -> Perhaps> +/// { println!("placed"); Ok(None) } +/// fn area (&self) -> XYWH +/// { Default::default() } +/// fn clip ( +/// &mut self, +/// area: impl Into>>, +/// draw: impl FnOnce(&mut Self)->T +/// ) -> T +/// { draw(self) } /// } /// /// impl_draw!(|self: String, to: TestOut|{ diff --git a/src/eval.rs b/src/eval.rs index 168a5ce..8cab4b0 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -217,6 +217,7 @@ pub fn eval_view_tui <'a, S> ( + for<'b>Namespace<'b, u16> + for<'b>Namespace<'b, Color> { + use crate::term::*; // See `tengri::eval_view` let head = expr.head()?; let mut frags = head.src()?.unwrap_or_default().split("/"); diff --git a/src/lib.rs b/src/lib.rs index f42030b..e3eec5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,6 @@ pub(crate) use ::{ std::ops::{Add, Sub, Mul, Div}, std::sync::{Arc, RwLock}, std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::*}, - std::error::Error, std::marker::PhantomData }; diff --git a/src/term.rs b/src/term.rs index 24364b7..420d0eb 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,11 +1,72 @@ -use crate::{*, lang::*}; +#[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_ns { + ($self:ident: $State:ident, $to:pat, $pat:ident $body:expr) => { + impl Interpret>> for $State { + fn interpret_word <'a> (&'a $self, $to: &mut Tui, $pat: &'a impl Symbol) -> Drawn { + $body + } + fn interpret_expr <'a> (&'a self, to: &mut Tui, src: &'a impl Expression) -> Drawn { + 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:block) => { + 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 input; pub use self::input::*; -mod output; pub use self::output::*; +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; @@ -32,14 +93,6 @@ pub(crate) use ::{ }, }; -/// Terminal output. -pub struct Tui( - /// Ratatui buffer; area is screen size - pub Buffer, - /// Current draw area - pub XYWH -); - 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 } } @@ -47,8 +100,86 @@ 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(); + match event { + + // Hardcoded exit. + Event::Key(KeyEvent { + modifiers: KeyModifiers::CONTROL, + code: KeyCode::Char('c'), + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) => { exited.store(true, Relaxed); }, + + // Handle all other events by the state: + event => { + 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)) } @@ -60,16 +191,17 @@ impl Tui { self.0.reset(); } } - pub fn redraw <'b, W: Write> (&'b mut self, back: &mut CrosstermBackend, mut next: &'b mut Self) { + 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(); } -} - -impl Tui { pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { for row in 0..self.h() { let y = self.y() + row; @@ -84,13 +216,6 @@ impl Tui { } 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, x: u16, y: u16, style: Option