From 62867d26bddbc415d11673a053ec0350f2fd9905 Mon Sep 17 00:00:00 2001 From: Erik Kundt Date: Mon, 4 Jul 2022 12:44:06 +0200 Subject: [PATCH] tui: Add basic framework Signed-off-by: Erik Kundt --- Cargo.lock | 141 ++++++++++++++++++++++++++ Cargo.toml | 1 + radicle-tui/Cargo.toml | 21 ++++ radicle-tui/src/lib.rs | 88 ++++++++++++++++ radicle-tui/src/main.rs | 58 +++++++++++ radicle-tui/src/ui.rs | 46 +++++++++ radicle-tui/src/ui/components.rs | 166 +++++++++++++++++++++++++++++++ radicle-tui/src/ui/layout.rs | 49 +++++++++ radicle-tui/src/ui/theme.rs | 49 +++++++++ radicle-tui/src/ui/widget.rs | 80 +++++++++++++++ 10 files changed, 699 insertions(+) create mode 100644 radicle-tui/Cargo.toml create mode 100644 radicle-tui/src/lib.rs create mode 100644 radicle-tui/src/main.rs create mode 100644 radicle-tui/src/ui.rs create mode 100644 radicle-tui/src/ui/components.rs create mode 100644 radicle-tui/src/ui/layout.rs create mode 100644 radicle-tui/src/ui/theme.rs create mode 100644 radicle-tui/src/ui/widget.rs diff --git a/Cargo.lock b/Cargo.lock index 18b4aaf3..13f9e465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,26 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "amplify" version = "4.0.0-beta.17" @@ -299,6 +319,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cc" version = "1.0.78" @@ -987,6 +1013,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "headers" @@ -1261,6 +1290,29 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "lazy-regex" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a505da2f89befd87ab425d252795f0f285e100b43e7d22d29528df3d9a576793" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2075,6 +2127,17 @@ dependencies = [ "radicle-cli", ] +[[package]] +name = "radicle-tui" +version = "0.1.0" +dependencies = [ + "anyhow", + "lexopt", + "radicle-term", + "tui-realm-stdlib", + "tuirealm", +] + [[package]] name = "rand" version = "0.7.3" @@ -2170,6 +2233,8 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -2458,6 +2523,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "snapbox" version = "0.4.4" @@ -2668,6 +2739,17 @@ dependencies = [ "redox_termios", ] +[[package]] +name = "textwrap" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.38" @@ -2928,6 +3010,55 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags", + "cassowary", + "termion 1.5.6", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "tui-realm-stdlib" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66f252bf8b07c6fd708ddd6349b5f044ae5b488b26929c745728d9c7e2cebfa6" +dependencies = [ + "textwrap", + "tuirealm", + "unicode-width", +] + +[[package]] +name = "tuirealm" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265411b5606f400459af94fbc5aae6a7bc0e98094d08cb5868390c932be88e26" +dependencies = [ + "bitflags", + "lazy-regex", + "termion 1.5.6", + "thiserror", + "tui", + "tuirealm_derive", +] + +[[package]] +name = "tuirealm_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0adcdaf59881626555558eae08f8a53003c8a1961723b4d7a10c51599abbc81" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.16.0" @@ -2946,6 +3077,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown", + "regex", +] + [[package]] name = "unicode-normalization" version = "0.1.22" diff --git a/Cargo.toml b/Cargo.toml index 6814b72b..1fe5c8cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "radicle-remote-helper", "radicle-ssh", "radicle-tools", + "radicle-tui", ] default-members = [ "radicle", diff --git a/radicle-tui/Cargo.toml b/radicle-tui/Cargo.toml new file mode 100644 index 00000000..194fca3b --- /dev/null +++ b/radicle-tui/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "radicle-tui" +license = "MIT OR Apache-2.0" +version = "0.1.0" +authors = ["Erik Kundt "] +edition = "2021" +build = "../build.rs" + +[[bin]] +name = "radicle-tui" +path = "src/main.rs" + +[dependencies] +anyhow = { version = "1" } +lexopt = { version = "0.2" } +tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] } +tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] } + +[dependencies.radicle-term] +version = "0" +path = "../radicle-term" diff --git a/radicle-tui/src/lib.rs b/radicle-tui/src/lib.rs new file mode 100644 index 00000000..36ea0ff0 --- /dev/null +++ b/radicle-tui/src/lib.rs @@ -0,0 +1,88 @@ +use std::hash::Hash; +use std::time::Duration; + +use anyhow::Result; + +use tuirealm::terminal::TerminalBridge; +use tuirealm::Frame; +use tuirealm::{Application, EventListenerCfg, NoUserEvent}; + +mod ui; + +/// Trait that must be implemented by client applications in order to be run +/// as tui-application using tui-realm. Implementors act as models to the +/// tui-realm application that can be polled for new messages, updated +/// accordingly and rendered with new state. +/// +/// Please see `examples/` for further information on how to use it. +pub trait Tui +where + Id: Eq + PartialEq + Clone + Hash, + Message: Eq, +{ + /// Should initialize an application by mounting and activating components. + fn init(&mut self, app: &mut Application) -> Result<()>; + + /// Should update the current state by handling a message from the view. + fn update(&mut self, app: &mut Application, interval: u64); + + /// Should draw the application to a frame. + fn view(&mut self, app: &mut Application, frame: &mut Frame); + + /// Should return true if the application is requested to quit. + fn quit(&self) -> bool; +} + +/// A tui-window using the cross-platform Terminal helper provided +/// by tui-realm. +pub struct Window { + /// Helper around `Terminal` to quickly setup and perform on terminal. + pub terminal: TerminalBridge, +} + +impl Default for Window { + fn default() -> Self { + Self::new() + } +} + +/// Provides a way to create and run a new tui-application. +impl Window { + /// Creates a tui-window using the default cross-platform Terminal + /// helper and panics if its creation fails. + pub fn new() -> Self { + Self { + terminal: TerminalBridge::new().expect("Cannot create terminal bridge"), + } + } + + /// Runs this tui-window with the tui-application given and performs the + /// following steps: + /// 1. Enter alternative terminal screen + /// 2. Run main loop until application should quit and with each iteration + /// - poll new events (tick or user event) + /// - update application state + /// - redraw view + /// 3. Leave alternative terminal screen + pub fn run(&mut self, tui: &mut T, interval: u64) -> Result<()> + where + T: Tui, + Id: Eq + PartialEq + Clone + Hash, + Message: Eq, + { + let mut app = Application::init( + EventListenerCfg::default().default_input_listener(Duration::from_millis(interval)), + ); + tui.init(&mut app)?; + + while !tui.quit() { + tui.update(&mut app, interval); + + self.terminal.raw_mut().draw(|frame| { + tui.view(&mut app, frame); + })?; + } + + Ok(()) + } +} diff --git a/radicle-tui/src/main.rs b/radicle-tui/src/main.rs new file mode 100644 index 00000000..4e548250 --- /dev/null +++ b/radicle-tui/src/main.rs @@ -0,0 +1,58 @@ +use std::process; + +use radicle_term as term; + +pub const NAME: &str = "radicle-tui"; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const GIT_HEAD: &str = env!("GIT_HEAD"); +pub const FPS: u64 = 60; + +pub const HELP: &str = r#" +Usage + + radicle-tui [