hottyHTML over the TTY

☰ Contents · Reference › hotty-rs

hotty-rs

hotty for Rust programs, ratatui's among them, and for hosts

hotty-rs is the Rust SDK for hotty, HTML over the TTY. With it, a terminal program shows small HTML documents, called surfaces, on rectangles of cells, and hears what the user does in them. It follows the hotty repository's SDK.md: the same two layers, names and defaults as the other SDKs, and every SDK section of the conformance vectors.

It speaks hotty 0.6: a host's replies and events carry msgpack bodies (SPEC §3.3), its query lists the version it speaks (a=q:n=1:v=0.6, SPEC §4), and keys are named as myelin key names 0.1 names them (SPEC §10.4). A terminal of another hotty version is no host to it, and the program draws in cells.

crate what it is SDK.md
hotty the wire layer: builders, encode, Decoder, Scanner, Detector, replies, events and capabilities, keys and keymaps; a Field edited as a host edits one; Link, Session and Terminal; and host, what a host or a relay reads and writes §3, §4.1, §4.3, §4.6
hotty-ratatui hotty for a ratatui program: a Surface widget, a session that reads the terminal and hands back crossterm's events, a backend §4.3

0.6.x speaks hotty 0.6; the patch number is this repository's own.

Install

hotty-rs isn't on crates.io yet. Depend on it from GitHub, and pin a commit for builds you can reproduce:

[dependencies]
hotty = { git = "https://github.com/neuroplastio/hotty-rs" }
hotty-ratatui = { git = "https://github.com/neuroplastio/hotty-rs" }  # with ratatui 0.30

It needs Rust 1.88 or later; its terminal is Unix's.

Guides

A program that prints

use hotty::{Placement, Terminal};

fn main() -> std::io::Result<()> {
    let Ok(t) = Terminal::open("hello") else {
        println!("Hello."); // no terminal: a pipe, a cron job
        return Ok(());
    };
    if t.detect() {
        t.line_start(); // surfaces are placed at the cursor
        t.print("hello", "<p>Hello, <b>HTML</b> in the terminal.</p>", &Placement::new(40, 1))?;
        t.fence().ok(); // the host's replies, before the shell reads the terminal
    } else {
        println!("Hello."); // a terminal that is not a host, or not of this version
    }
    Ok(())
}

Terminal opens /dev/tty, whatever the standard streams are, reads it on a thread of its own, and puts it in raw mode while a call waits for an answer. It is Unix only, behind the default feature term.

A ratatui program

use hotty_ratatui::{Event, Surface};
use ratatui::crossterm::event::{Event as Input, KeyCode};
use ratatui::widgets::Paragraph;

let (mut terminal, session) = hotty_ratatui::init("hello")?;
let card = session.surface("card");
loop {
    session.draw(&mut terminal, |frame, surfaces| {
        frame.render_stateful_widget(
            Surface::new(&card, || "<p>Hello, <b>HTML</b>. <button id=ok>OK</button></p>".into())
                .fallback(Paragraph::new("Hello, cells. Press q.")),
            frame.area(),
            surfaces,
        );
    })?;
    match session.read()? {
        Event::Input(Input::Key(k)) if k.code == KeyCode::Char('q') => break,
        Event::Hotty(e) if e.kind == "click" => break,
        _ => {} // Ready, Relayout, a resize: draw again
    }
}
hotty_ratatui::restore(&mut terminal, &session)?;

A Surface renders its document on a host, where the cells under it stay blank and the host draws above them, and its fallback widget anywhere else. session.draw lays the frame's surfaces out: a new one gets its document and a placement, a moved one a placement, one the frame no longer renders is hidden or deleted. Their commands go out with the frame's cells. Deltas (session.send(hotty::set_text(…))) go with the next frame.

The session reads the terminal. The host answers on the terminal's input, among the keys, and crossterm's reader would take its answers for keys. So the session reads the terminal itself, cuts hotty's sequences out, and hands the rest back as crossterm's events, Event::Input: a program reads session.read() and session.poll() where it read crossterm's event::read() and event::poll(), and keeps its handlers. It must not read the terminal any other way while the session is open, and that includes crossterm's cursor::position(). hotty_ratatui::Backend asks for the cursor's position through the session, which ratatui's Terminal::clear and inline viewports need; init makes a terminal on it.

A program that enables ratatui's scrolling-regions feature enables hotty-ratatui's too, since it adds methods to the backend trait.

Text fields in cells

A program that draws its fields in cells edits them with the keymap it gives its surfaces' fields, so a field edits the same on a host and off one:

use hotty::{Field, INSERT, TERMINAL_KEYS, decode_keys, key_text, platform_of, resolve};

let keys = resolve(&platform_of(&caps), false, [TERMINAL_KEYS]);
let mut f = Field::default();
for key in decode_keys(&read).into_iter().flatten() {
    match keys.lookup(&key) {
        Some(INSERT) => { f.type_text(&key_text(&key).unwrap()); }
        Some("submit") => submit(&f.value),
        Some(a) if keys.selects(&key) => { f.extend(a); }
        Some(a) => { f.do_action(a); }
        None => on_key(&key), // not the field's: the program's own keymap has it
    }
}

A host or a relay

hotty::host is the other side of the wire, for a host or for a relay that stands between programs and a host (SDK.md §4.5). hotty-blitz is built on it.

use hotty::host::{Output, Splitter, delta_of, reply_err};

let mut split = Splitter::new();
split.feed(&program_output, &mut |o| match o {
    // To the cells, as they are.
    Output::Bytes(b) => terminal.write(b),
    // The alternate screen, a reset, …: the host acts, then passes on.
    Output::Seq(seq, b) => {
        on_mode(&seq);
        terminal.write(b)
    }
    Output::Command(cmd) => match cmd.action() {
        // Checked whole before any op applies (SPEC §6.1).
        "delta" => match delta_of(&cmd) {
            Ok(ops) => apply(ops),
            // EINVAL, and the index of the op at fault.
            Err(e) => reply(reply_err(&cmd, &e)),
        },
        _ => {}
    },
    Output::Invalid(why) => log(why),
});
  • Splitter cuts a program's output into the bytes to pass on, the control sequences a host acts on (private modes, erase in display, a full reset) with their bytes, and hotty's commands, decoded.
  • delta_of, field_of, menu_of, menu_show_of, touch_of and placement_of read a command as SPEC has a host read it, and refuse one with the EINVAL to answer; control_kind and field_fits say whether an a=field fits its target; quiet_of and speaks say what to answer. A placement with b=1 is a backdrop (SPEC §5.2): a host without the feature backdrop ignores it, and a relay passes it on, as it does z.
  • reply_ok, reply_err, reply_query and encode_event write what a host sends, each body typed as SPEC §3.3 types it. A relay that re-names an event's surface passes its detail on as it came.
  • hotty::keys has what a host that sees the key itself needs: KeyParts, Keymap::press, Platform::edit_press.

Examples

example what it shows
hello open, detect, line_start, print, fence: the smallest program
progress a bar moved by one delta of var and text ops; three renditions: surface, cells, pipe
board a full-screen ratatui program: a surface among its cells, clicks and a form, deltas, keys for the cells
cargo run -p hotty --example hello
cargo run -p hotty-ratatui --example board

Names

The canonical names of SDK.md, in Rust's case (its Appendix A). Where a name is a keyword or clashes:

  • the capabilities' and a Platform's mod is modifier;
  • a Field's Do and Type are do_action and type_text;
  • the builders return a Cmd (or a Doc, a Query), since Command is a command event's detail; options are its builder methods: .n(4), .q(Quiet::NoReply), doc(…).detached().scroll(SCROLL_VERTICAL), query(1).late().

Dependencies

hotty takes flate2 (feature zlib, to compress what it sends and inflate what a relay meets), rustix (feature term, for /dev/tty) and unicode-segmentation (grapheme clusters, SDK.md §4.6); base64 and msgpack are its own. hotty-ratatui adds ratatui-core, ratatui-crossterm and signal-hook.

Development

make check is the gate: rustfmt, the build, the tests (the conformance vectors among them), clippy, the docs, and the wire layer without default features. make vectors refreshes the vectors from a hotty checkout next to this one (or HOTTY_DIR).

Licence

Apache-2.0.

Loading hotty-demo…