Crates.io | termit |
lib.rs | termit |
version | 0.7.0 |
source | src |
created_at | 2019-02-12 18:28:56.848357 |
updated_at | 2023-08-03 19:00:08.592437 |
description | Terminal UI over crossterm |
homepage | https://gitlab.com/BrightOpen/BackYard/termit |
repository | https://gitlab.com/BrightOpen/BackYard/termit |
max_upload_size | |
id | 114348 |
size | 381,948 |
Welcome to the mound! Termit implements TUI - yet another terminal UI.
Tl;DR:
//! This is a basic hello world termit example.
//!
//! Just add a loop and some input handling...
use std::io;
use termit::prelude::*;
#[async_std::main]
async fn main() -> io::Result<()> {
// The Termit facilitator:
// * Use the Terminal to set up your terminal experience.
// * Initialize Termit.
let mut termit = Terminal::try_system_default()?.into_termit::<NoAppEvent>();
// The TUI tree:
// * Canvas sets the default style for the contained widget and fills space.
// * "Hello World!" - &str/String - is a widget.
// * We just place the text in the middle with some placement constraints.
let mut ui = Canvas::new(
"Hello World!"
.width(6)
.height(2)
).back(Color::blue(false));
// Render and update and print... rinse and repeat?
termit.step(&mut (), &mut ui).await?;
// \ |
// the mutable | |
// application | |
// model goes here \- The UI to show off
Ok(())
}
You can also use termit in bare form, just to render some once of content
//! This is a minimal hello world termit example.
//!
//! There is no input processing, no async, no looping. Just print some nice screen.
use std::io;
use termit::prelude::*;
fn main() -> io::Result<()> {
// The Termit facilitator:
// * Use the Terminal to set up your terminal experience.
// * Initialize Termit.
let mut termit = Terminal::try_system_default()?.into_termit::<NoAppEvent>();
// The TUI tree:
// * Canvas sets the default style for the contained widget and fills space.
// * "Hello World!" - &str/String - is a widget.
// * We just place the text in the middle with some placement constraints.
let mut ui = Canvas::new(
"Hello World!"
.width(6)
.height(2)
).back(Color::blue(false));
// render and update:
termit.update(&mut (), Event::Refresh(0), &mut ui);
// | | |
// the mutable | | \- The UI to show off
// application | |
// model goes here -/ |
// \- this is an event,
// here a refresh
//
// print ,- the reduced scope (rectangular window)
// | None => all of the screen
termit.print(None)
}
For a little more sophisticated example with input event loop and custom widgets, cargo run --example color
. A simplistic Delta.Chat client inspired by the dreamer project is called dech. There are more examples.
Here's a replay of the editor example:
Back to basics, less line drawing, more content and elegance. Async?
In your Cargo.toml
:
[dependencies]
termit = "*"
Please note that the API is still evolving. Feedback is most welcome. Ergonomic static UI initialization and dynamic UI updates are somewhat at odds with each other.
On the system side:
On the UI side:
Pretty
, literaly.On the app side:
Termit
helps you to juggle app loop, input, printing...drop()
?).--no-default-features
while dropping async-std, windows-sys, rustix...
We try hard to degrade gracefully.TerminalEmulator
. Let them print
their ANSI-fu to temu and then show it like a termit widget.On the curiosity side:
MmapVec
is as big as it's contents.windows-sys
crate and WinRT.rustix
for all *nix ioctl things. You may be able to compile without libc
, not tested.Development
And the real world applications:
dech
- still in the making, but already chatting...Non-goals:
Termit is heavily inspired by crossterm and tui. It hopefully delivers a dramatic improvement of control (ttys) and API ergonomy over crossterm. For the number of design flaws in crossterm, we've moved to our own cross platform implementations. Crossterm was absolutely instrumental in getting this crate of the ground.
Termit takes care of the most common tasks around running a terminal app and building a TUI - a text based user interface. It could be your one stop shop for all things 'terminal' if you're not looking for a comprehensive coverage of all Terminal / Console features.
Async to blend in with the rest of the efficient async app. Termit is useful without async as well. For optimal fit, you'd design your backend as an event stream that consumes commands. Simple apps can be designed with a simple static state and in-loop processing, though.
Unlike in some other TUI libs, widgets are not ephemeral unless you want to. You can implement a stateful Widget
easily:
use termit::prelude::*;
struct UpdateCounter {
state: usize
}
impl<M, A: AppEvent> Widget<M, A> for UpdateCounter {
fn update(
&mut self,
_model: &mut M,
_input: &Event<A>,
screen: &mut Screen,
painter: &Painter,
) -> Window {
self.state +=1;
painter.paint(&format!("{}", self.state), screen, 0, false)
}
}
This is usefult if your widget tracks multiple internal state values which are irrelevant to the application as a whole. For instance, a text box editor doesn't need to polute application state with cursor position.
You may also recreate the whole or part of the UI tree in each update cycle if you want to. Then your widgets must keep their state in the model.
Widgets can manipulate the application state model directly.
However, these changes should be near instant. More intensive work (io, network) should be sent to the application as a command and processed independently and ideally asynchronously. This is done in the dech example.
use termit::prelude::*;
struct AppState {
state: usize
}
struct UpdateCounter;
impl Widget<AppState, ()> for UpdateCounter {
fn update(
&mut self,
model: &mut AppState,
_input: &Event<()>,
screen: &mut Screen,
painter: &Painter,
) -> Window {
model.state +=1;
painter.paint(&format!("{}", model.state), screen, 0, false)
}
}
The widget generally accesses the whole app state mutably. It should be possible, though, to create a widget adapter that will focus on a subset of the model or another model alltogether.
Avoid creating complex widgets. Instead, add features by wrapping other widgets in decorators or by composing a complex subtree from other widgets.
Look at the [crate::widget::WidgetBinder
] for instance which allows us to load and save any widget, any property of the widget or even replcace it altogether on update. It is a decorator implemented for all impl Widget
's with .bind_with()
:
# use termit::prelude::*;
# fn sample () {
struct AppState {
banner: String
}
let mut bound_widget = Canvas::new(String::new()).back(Color::blue(false))
.bind_with(
|w,m:&AppState| *w.content_mut() = m.banner.clone(),
|w,m:&mut AppState| m.banner = w.content().clone()
);
# bound_widget.update(&mut AppState{banner:"I".to_string()}, &Event::<()>::Refresh(0), &mut Screen::default(), &Painter::default());
# }
The same with [crate::widget::WidgetPropertyBinder
]:
# use termit::prelude::*;
# fn sample () {
struct AppState {
banner: String
}
let mut bound_widget = Canvas::new(String::new()).back(Color::blue(false))
.bind_property(|w| w.content_mut(), |m: &mut AppState| &mut m.banner);
# bound_widget.update(&mut AppState{banner:"I".to_string()}, &Event::<()>::Refresh(0), &mut Screen::default(), &Painter::default());
# }
There are some other popular TUI libs. Termit is Rust native (no awkward ffi to C* TUI libs). It aims to be quick to get started while giving you advanced control where needed. And it is compact, trying to avoid bloat.