Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
rattery
Ship a ratatui app as a thin binary that runs it sandboxed and talks to your server.
Write the app with ratatui. Declare its backend calls as #[rattery_app::server]
functions, Leptos / Dioxus fullstack style. The app compiles to a WASI component; a
few lines of build.rs build it and a few lines of main.rs embed it, so cargo build of your CLI produces one binary with the app inside, pointed at your API:
// build.rs
new.build;
// main.rs
let report = from_bytes
.origin
.run_blocking?;
exit;
The host inside that binary is to the app what a browser is to a web page: it runs the component in a wasmtime sandbox, hands it the terminal through a small WIT interface, keeps its cookies, and lets it make HTTP requests to its own origin only unless you or the other server say otherwise. The same host can just as well fetch the component from a URL at startup, so an app can be deployed by replacing one file on the server. A rattery is an enclosure for rats. This one keeps a ratatui app where it can't touch your filesystem, your network, or your other terminals.
Why
- One fullstack dev model.
#[rattery_app::server]isserver_fn's#[server]with the client filled in. The same shared crate compiles into the app (calls become HTTP) and into the server (bodies run). It is the crate Leptos and Dioxus use, so request/response, streaming responses, websockets, multipart uploads, and cookie sessions all work as they do there. - A real sandbox. The component gets the terminal, a clock, randomness, and HTTP to its origin. Nothing else is linked in. Embedding someone else's TUI, or loading one from a URL, is as safe as opening a web page.
- Embeddable first.
rattery::Appis the product: a builder your CLI calls. There is no daemon and no required command;examples/rattery-clishows a general-purpose runner in 150 lines if you want one. - Deploy by URL, optionally.
App::from_urlfetches the component like a browser would; with.watch(true)a running app reloads when the server publishes a new one. - Thick client. UI state stays local, the server only answers RPC. Compare with SSH-app frameworks, which run the whole UI server-side and stream frames.
How it works
┌────────── your terminal ──────────┐
│ your CLI (rattery::App inside) │ HTTP ┌────────────────────┐
│ crossterm ⇄ terminal (WIT) ⇄ app │ ───────────────▶ │ axum server │
│ wasi:http ── origin policy ──────┼────────────────▶ │ /api/* server fns │
│ cookies │ POST /api/... │ (/app.wasm, opt.) │
└───────────────────────────────────┘ └────────────────────┘
crates/rattery-app/wit/rattery.witis the entire contract. It mirrors ratatui'sBackendtrait (draw a list of changed cells, cursor, size, flush) plus a crossterm-shaped event stream and awasi:iopollable so an app canawaitkey presses and server responses at the same time. A second small interface provides websockets, which WASI 0.2 lacks; the host applies the same origin policy and cookie jar to them.crates/ratteryis the host library: wasmtime +wasmtime-wasi+wasmtime-wasi-http, crossterm behind the terminal interface, a loader, the origin policy, the cookie jar, and a headless mode.rattery::Appis the entry point.crates/rattery-buildbuilds an app to a component frombuild.rsso a shim can embed it withrattery::embed!().crates/rattery-appis what apps depend on: a ratatuiBackendover the WIT interface, the event API, background tasks, timers, websockets, and aserver_fnclient that speakswasi:http@0.3. On native targets it provides only what the server build of a shared crate needs.crates/rattery-macrosprovides#[rattery_app::server].examples/rattery-cliis a general-purpose runner built on the library, used by the dev loop and the benchmark, and the reference for a shim that takes everything as flags. It is not published.
Diffing happens inside ratatui's Terminal in the guest, so a frame is one draw call
carrying only the cells that changed, and one flush.
The app is a real async program. It exports one async func run and the host
drives it with the component model's async ABI, so waiting for a key press, a server
response, a websocket message, or a timer is a plain .await and other tasks in the
app keep running meanwhile. Input is an async func on the terminal interface; HTTP
and timers use WASI 0.3, while the standard library keeps using WASI 0.2 for stdio.
All of this builds on stable Rust for wasm32-wasip2: the async ABI does not need
the wasm32-wasip3 target, which has no prebuilt standard library yet.
Quick start
Everything is in the nix dev shell (direnv allow or nix develop): stable Rust
with the wasm32-wasip2 target, wasm-tools, and the wasmtime CLI.
# the dev loop: build the app and the server, serve, rebuild on change
# in another terminal, run the app like a browser would; --watch reloads it
# in place every time the component is rebuilt
Or by hand:
Writing an app
A shared crate holds the server functions and any types they exchange:
// counter-shared/src/lib.rs
use ;
use ;
pub async
/// A streaming response: the server pushes lines for as long as the app reads.
pub async
/// A websocket: a stream in, a stream out, for as long as the connection lives.
pub async
/// A file upload. The app builds a `rattery_app::multipart::FormData`; the server
/// gets the parsed parts as a `multer` stream.
pub async
[]
= ["rattery/ssr"]
= ["ssr", "rattery/axum"]
The app is an ordinary binary crate built for wasm32-wasip2; rattery_app::app!
exports the component's entry point (and supplies the placeholder main a binary
needs; the host never calls it, because a synchronous main could not await). Server
calls run as background tasks so the UI never blocks; a finished task surfaces as
Event::Wake:
use *;
use ;
app!;
async
The server depends on the shared crate with the axum feature and mounts two routes:
new
.route
.route
examples/counter is the complete version: background calls with a spinner, a
streaming live feed, a websocket echo, a multipart upload, cookie sessions with
per-session state, and ETags for --watch. rattery_app::websocket::WebSocket is also usable
directly, outside server functions.
rattery_app::location() returns the URL the app was loaded from, query string included,
so rattery https://host/app.wasm?team=infra passes parameters the way a web page
gets them (the example reads ?title=). rattery_app::origin() is where server calls go.
Event types mirror crossterm's (KeyCode::Char('q'), KeyModifiers::CONTROL, ...)
so existing ratatui code ports by changing an import. event::next_timeout and
rattery_app::time::sleep drive animations; task::wake lets a long-running task ask for
a redraw, which is how the example renders a streaming response line by line.
The host
Everything below is a method on rattery::App; examples/rattery-cli exposes each as
a flag, shown here because it reads well:
rattery <URL or path>
[--origin URL] [--allow-origin URL]... [--allow-all-origins]
[--incognito | --no-cookies | --cookie-jar FILE]
[--watch] [--location URL] [--env KEY=VALUE]... [--no-mouse] [--no-cache]
[--headless COLSxROWS [--script FILE] [--timeout SECS]]
Origin policy. An app may reach its own origin: where it was loaded from (after
same-origin redirects; cross-origin redirects are refused), or --origin for an app
loaded from a file. --allow-origin adds more and --allow-all-origins disables the
check. Every request carries an Origin header. Everything else is refused before a
connection is opened. There is no CORS mode: cross-origin access is allow-list only
until proper preflight and credential semantics exist. A RequestPolicy on the
builder sees every allowed request and can refuse or edit it.
Cookies. The host keeps a jar the way a browser does: the app never sees Cookie
or Set-Cookie, so ordinary cookie sessions on the server work unchanged and
HttpOnly means what it says. The jar persists under the user's local data directory;
--incognito keeps it in memory, --no-cookies drops everything, --cookie-jar picks
a file.
Reload. --watch polls the URL with If-None-Match and restarts the app in place
when the server publishes a new component.
Safety. Everything the app sends toward the terminal is validated: control
characters and malformed symbols never reach the screen or the title, cells outside
the screen are dropped, and guest output is rendered with escapes shown rather than
interpreted. Resource use is bounded by Limits (memory, CPU time on a continuous
10 ms epoch tick, queues, message and body sizes, concurrency). Ctrl-C three times
within 1.5 seconds interrupts an unresponsive app. Raw mode and the alternate screen
are always restored, including on panic, and every background task is stopped before
the terminal is handed back. See docs/security.md.
Stats. Report::timings and Report::stats (the --stats flag prints them) carry
phase timings (load, compile, instantiate, first frame) and terminal counters. cargo xtask bench runs a rendering and request latency benchmark; see docs/perf.md.
Headless. --headless 80x24 --script keys.txt runs the app on an in-memory screen,
feeds it a script (key k, key ctrl-c, type hello, paste, resize, sleep,
snapshot; see --help-script), and prints the snapshots. This is how the repository's
end-to-end tests work, and it is a ready-made test harness for your own app.
Embedding the host
use ;
// One specific app against one specific backend, embedded in the binary
// (rattery-build compiled it in build.rs).
let report = from_bytes
.origin
.cookies
.run_blocking?;
exit;
// A subcommand of an existing async CLI that opens a remote TUI.
let report = from_url?
.allow_origin
.run
.await?;
rattery_build::App takes the app crate's path (and optionally a package name,
features, or the dev profile), compiles it for wasm32-wasip2 into a target directory
under OUT_DIR, and exports the component's path as RATTERY_APP_WASM; changes under
the app's src rebuild it. The nested build needs the target installed
(rustup target add wasm32-wasip2).
Production controls on the builder: limits (see docs/security.md), on_phase
(loaded, compiled, ready, denied requests, reload, exit), request_policy (route
authorisation, credential injection), extension and state (extra WIT imports
backed by your own state), from_resolver (the embedder retrieves and validates the
bytes). rattery::inspect checks a component against rattery::ABI before it runs.
App::headless returns the snapshots in the Report, so an app's integration tests
can be a few lines:
let report = from_url?
.headless
.run
.await?;
assert!;
Status
Working: rendering, keyboard, mouse, paste, focus and resize events; request/response, streaming, websocket, and multipart server functions; background tasks on the component model's async ABI with HTTP over WASI 0.3; the origin policy with allow lists; a persistent cookie jar; hot reload; the library API; headless mode; a kill switch and timeouts; end-to-end tests of all of it. Note that wasmtime's WASI 0.3 support is marked experimental upstream; rattery pins wasmtime and tracks it.
License
MIT