TTY backend (ftui-tty)
ftui-tty is the native Unix implementation of the
ftui-backend traits. It owns raw-mode
lifecycle, reads bytes from the tty file descriptor, drives the
ftui-core InputParser, and emits escape sequences through a
TtyPresenter. It is what Program::with_native_backend uses when
the native-backend cargo feature is on.
File: crates/ftui-tty/src/lib.rs.
What it is (and isn’t)
ftui-tty replaces Crossterm as FrankenTUI’s I/O layer on Unix-like
systems (Linux, macOS, BSDs). It is:
- The only crate that calls
tcsetattr, writes to stdout, or installs SIGWINCH handlers. - A three-trait implementation:
BackendClock,BackendEventSource,BackendPresenter. - A hard RAII boundary — drop always restores the terminal, even
on panic, unless you compile with
panic = "abort".
It is not:
- A cross-platform shim. Windows is currently deferred; the Crossterm
compat layer (
features = ["crossterm-compat"]) remains available. - A source of events for widgets. The runtime consumes
Events produced here and dispatches them intoModel::update.
Structure
pub struct TtyBackend {
clock: TtyClock,
events: TtyEventSource,
presenter: TtyPresenter,
alt_screen_active: bool,
raw_mode: Option<RawModeGuard>, // Some == live, None == headless
}
impl TtyBackend {
pub fn new(width: u16, height: u16) -> Self; // headless
pub fn with_capabilities(w: u16, h: u16, caps: TerminalCapabilities) -> Self;
#[cfg(unix)]
pub fn open(w: u16, h: u16, options: TtySessionOptions) -> io::Result<Self>;
pub fn is_live(&self) -> bool;
}TtySessionOptions controls session startup:
pub struct TtySessionOptions {
pub alternate_screen: bool,
pub features: BackendFeatures, // mouse, bracketed paste, focus, kitty
pub intercept_signals: bool, // SIGINT / SIGTERM / SIGHUP cleanup
}Drop order
The TtyBackend drop sequence is deliberate: disable features, show
the cursor, exit the alt screen, flush, then restore termios. Any
other order leaves the terminal in a half-configured state when a
panic occurs.
Usage
use ftui::prelude::*;
use ftui_runtime::TtyBackend;
use ftui_backend::BackendFeatures;
use ftui_tty::TtySessionOptions;
fn main() -> std::io::Result<()> {
let backend = TtyBackend::open(
80, 24,
TtySessionOptions {
alternate_screen: true,
features: BackendFeatures {
mouse_capture: true,
bracketed_paste: true,
focus_events: true,
kitty_keyboard: false,
},
intercept_signals: true,
},
)?;
let cfg = ProgramConfig::fullscreen();
Program::with_native_backend(MyModel::new(), cfg)?.run()
}In most applications the constructor you reach for is
Program::with_native_backend(model, config), which opens a
TtyBackend internally using the features implied by your
ProgramConfig.
Escape sequence reference
Every capability is a DEC or xterm sequence; the exact bytes are
compiled into ftui-tty so the backend is self-contained. The table
below mirrors the one at the top of crates/ftui-tty/src/lib.rs.
| Feature | Enable | Disable |
|---|---|---|
| Alternate screen | CSI ? 1049 h | CSI ? 1049 l |
| Mouse (SGR) | CSI ? 1000;1002;1006 h (with split-form compatibility) | CSI ? 1000;1002;1006 l (+ legacy reset hygiene) |
| Bracketed paste | CSI ? 2004 h | CSI ? 2004 l |
| Focus events | CSI ? 1004 h | CSI ? 1004 l |
| Kitty keyboard | CSI > 15 u | CSI < u |
| Cursor show / hide | CSI ? 25 h | CSI ? 25 l |
| Synchronized output | CSI ? 2026 h | CSI ? 2026 l |
Notes on mouse hygiene (see MOUSE_ENABLE / MOUSE_DISABLE in
lib.rs:56–61):
- SGR format (1006) is set before the mode bits so terminals that key off last-mode-set order negotiate correctly.
- Legacy encodings (1001, 1003, 1005, 1015, 1016) are explicitly disabled on entry so they cannot linger from a previous session.
- DECSET 1003 (any-motion mouse) is not enabled. The high-rate motion stream destabilises some multiplexers.
- A
MOUSE_ENABLE_MUX_SAFEvariant is used when a multiplexer is detected.
Capabilities and feature union
BackendFeatures is a plain struct of bools:
pub struct BackendFeatures {
pub mouse_capture: bool,
pub bracketed_paste: bool,
pub focus_events: bool,
pub kitty_keyboard: bool,
}TtyEventSource keeps track of the current feature set and emits
only the delta when set_features is called (see
write_feature_delta at lib.rs:1126). Requests are sanitised
against the detected TerminalCapabilities:
- Requesting
kitty_keyboardon a terminal that doesn’t advertise it is quietly dropped. - Requesting features inside a multiplexer falls back to the multiplexer-safe escape variants.
SIGWINCH handling
On Unix, a dedicated signal thread listens for SIGWINCH and writes
a byte to a Unix socket pair. The event loop’s poll(2) blocks on
both the tty fd and the wake socket, so resize events have
bounded-latency delivery without polling winsize each frame. See
ResizeSignalGuard at lib.rs:468.
SIGINT, SIGTERM, SIGHUP, and SIGQUIT are handled by an
optional signal-interception guard (when
TtySessionOptions.intercept_signals == true) that triggers the
runtime’s shutdown path so Drop cleanup runs.
Layout within the workspace
- src/lib.rs escape sequences, backend, signal guard
- Cargo.toml unix-only by default
The runtime’s native-backend feature re-exports TtyBackend so
application code can write use ftui::prelude::*; and get the whole
stack. Without the feature the runtime still compiles — it just
won’t expose Program::with_native_backend.
Pitfalls
Never println! inside a live session. Raw mode and
synchronized output make a stray write a corrupted frame. All
diagnostic output must go through Cmd::Log
(inline-safe) or tracing (stderr) — not through stdout directly.
Don’t compile with panic = "abort" if you value your
terminal. Abort panics skip Drop, so the session never restores
termios and the user’s shell ends up in raw mode. Stick to
panic = "unwind" (the default) or wrap your fn main in a panic
hook that resets the terminal.
Headless mode is not the same as a file backend.
TtyBackend::new(w, h) creates a headless instance — no I/O, no
escape sequences, useful for tests. If you want to render into a
file or pty, use the ftui-pty crate and the appropriate
presenter.