hottyHTML over the TTY

☰ Contents · Reference › ratatui

hotty in a ratatui program

hotty-ratatui puts hotty's surfaces in a ratatui program. A Surface is a widget: on a host the host draws its HTML above the cells it renders into; anywhere else it draws its fallback, any ratatui widget. So one program has both renditions, and the user's terminal picks.

Install

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

It is for ratatui 0.30 on crossterm, on Unix. A program that enables ratatui's scrolling-regions feature enables hotty-ratatui's too, since it adds methods to the backend.

A program

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

const CARD: &str = "<p id=n>0</p><button id=ok>Click</button>";

fn main() -> std::io::Result<()> {
    let (mut terminal, session) = hotty_ratatui::init("hello")?;
    let card = session.surface("card");
    let mut clicks = 0;
    loop {
        session.draw(&mut terminal, |frame, surfaces| {
            let cells = format!("{clicks} clicks · q quits");
            frame.render_stateful_widget(
                Surface::new(&card, || CARD.into())
                    .fallback(Paragraph::new(cells)),
                frame.area(),
                surfaces,
            );
        })?;
        match session.read()? {
            Event::Input(Input::Key(k)) => match k.code {
                KeyCode::Char('q') => break,
                // A field keeps the keyboard until the program takes it.
                KeyCode::Esc => session.send(hotty::blur(&card)),
                _ => {}
            },
            Event::Hotty(e) if e.kind == "click" => {
                clicks += 1;
                // The surface changes in place, with the next frame.
                session.send(hotty::set_text(&card, "n", clicks));
            }
            _ => {} // Ready, Relayout, a resize: draw again
        }
    }
    hotty_ratatui::restore(&mut terminal, &session)
}
  • init opens the terminal as ratatui::init does (raw mode, the alternate screen, a panic hook that restores it), starts a session, and waits until it knows whether the terminal is a host, so the first frame draws the right rendition. restore deletes the surfaces and gives the terminal back.
  • session.draw is terminal.draw with the frame's surfaces: the Surface widgets it renders are the surfaces on screen. A new one gets its document, a moved one is placed again, one the frame no longer renders is deleted, or hidden if it is keep(true). Their commands go out with the frame's cells.
  • The document is asked for only when it must be sent: the first frame that shows the surface, and after the host lost it. Change it afterwards with deltas: session.send(hotty::delta(…)).

Events

session.read() and session.poll(timeout) stand where crossterm's event::read() and event::poll() stood:

Event what it is
Input(ev) crossterm's event, as crossterm would read it: keys, the mouse, pastes, focus, resizes. Hand it to the handler the program has
Ready(mode) the session knows the terminal: Mode::Native (a host) or Mode::Text. Draw
Hotty(ev) something the user did in a surface: click, submit, change, input, … with its target and detail
Relayout { lost } the screen was erased or scrolled under the surfaces, or the host lost a document: draw, which places them again. With lost, the host may have lost the resources (hotty::res) its documents refer to as well: session.send them again before drawing, so that they reach it ahead of the documents
Error(reply), Ack(reply) a command the host refused, or the ok of one the program numbered
Pong(took) the answer to session.ping(): how long the terminal took to take a frame

The session reads the terminal itself. A host answers on the terminal's input, among the keys, and crossterm's reader would take its answers for keys. So the session cuts hotty's sequences out of the input and parses the rest into crossterm's events. While a session is open, nothing else may read the terminal: not crossterm's event::read, and not cursor::position. The terminal init makes asks for the cursor's position through the session, which ratatui's Terminal::clear and inline viewports need.

The keyboard

A surface with a field takes the keyboard when the user clicks into it: the keys go to the field, and the ones it doesn't use (Escape, a key its data-keys gives the program) come back as Input. The program takes the keyboard back with a blur, as the program above does on Escape: session.send(hotty::blur(&card)).

A field edits the same in cells as on a host when the program gives both the same keymap: hotty::TERMINAL_KEYS in the surface's data-keys, and a hotty::Field with hotty::resolve(…, TERMINAL_KEYS) in the cells.

The widget

Surface what it does
new(name, doc) the surface name (session.surface("card") makes one of this program's own), its document from doc
.fallback(widget) what to draw in the cells where the terminal is not a host, or not known to be one yet
.whole(rect) the whole surface, where it reaches past the area it renders into: a surface scrolled partly out of view
.keep(true) hide it, rather than delete it, when a frame doesn't render it
.press(true), .fit(true), .hover(true) ask for press, fit and hover events
.scroll(axes) let the host scroll the document itself: hotty::SCROLL_VERTICAL, SCROLL_HORIZONTAL
.z(n) above or below the surfaces it overlaps

Example

board is a full-screen program with a surface among its cells: buttons and a form on a host, a paragraph in cells, and a log of what it heard.

cargo run -p hotty-ratatui --example board

Loading hotty-demo…