10 releases (5 breaking)

0.7.0 Aug 3, 2023
0.6.4 Jun 30, 2023
0.6.3 Feb 5, 2023
0.5.0 Dec 29, 2022
0.0.1 Feb 12, 2019

#114 in Command-line interface


Used in w3t

MIT license

295KB
7K SLoC

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)
}

asciicast

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:

asciicast

Why?

Back to basics, less line drawing, more content and elegance. Async?

Usage

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.

What's in the box?

On the system side:

  • Asynchronous input parsing into a neat input model stolen from crossterm.
  • Synchronous output with escaping and style.
  • TTY terminal ioctl / Windows console WinRT set up and configuration.
  • Plug in your own IO and control.
  • Explicit use of IOs = there is no single system-wide tty/console or global settings.
  • Internal screen buffer trying hard to reduce the amount of IO.
  • Separation of concerns, there is the UI, there is the model and there is the app.

On the UI side:

  • A composable widget ecosystem encouraging you to make your own.
  • Focus on making a fancy UI, forget the terminal and IO and OS.
  • Colors! Do not worry about which color suits which terminal, we're compatible. Express it in RGB if you will and we take care that it shows on 8 color terminals, too, well in 8 colors approximated.
  • Styles! With a convenient API to make things Pretty, literaly.
  • Styles on windows

On the app side:

  • Termit helps you to juggle app loop, input, printing...
  • It is opinionated, but you can do some things your way, like handling input, printing.
  • Async first - except for output which complicates things a lot (how do you run an async terminal cleanup from drop()?).
  • Usable without async.
  • Reduce dependencies with disabling default features, you will still be able to render a UI with --no-default-features while dropping async-std, windows-sys, rustix... We try hard to degrade gracefully.
  • Integration with other TUI libs should be possible through TerminalEmulator. Let them print their ANSI-fu to temu and then show it like a termit widget.
  • Reusable components - take just the input parser, screen buffer or terminal initialization...

On the curiosity side:

  • The screen buffer lives in an anonymous memory mapped area avoiding frequent allocation. MmapVec is as big as it's contents.
  • We've got some Windows Console API calls neatly wrapped in rust style. It is calling new windows-sys crate and WinRT.
  • We use rustix for all *nix ioctl things. You may be able to compile without libc, not tested.
  • Some support for terminal emulation - we can run other terminal apps and display them in a window.
  • Constant time refresh for FPS sensitive apps - games.
  • Virtual terminal emulation is still incomplete.

Development

  • Docs and examples
  • Test coverage
  • Automated building and testing

And the real world applications:

  • A terminal delta.chat client - dech - still in the making, but already chatting...

Non-goals:

  • Full coverage of tty ioctl / windows console features. We'll implement as much as is necessary to render UI and handle input.
  • Big library of ready made widgets - please make your own crate and ping me for linking.

Concepts

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

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.

Widgets maintain their own state

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 update the app model directly

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.

Simple widgets, composition and decoration

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());
# }

Company

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.

  • tui - probably closest to termit
  • rltk a.k.a. bracket lib geared towards games
  • ncurses - legacy ncurses wrapper
  • cursive - abstraction over multiple backends

Dependencies

~4–16MB
~189K SLoC