Skip to Content
ftui-coreTerminal session (RAII)

TerminalSession — RAII Lifecycle

TerminalSession is a one-call entry point that flips every terminal mode your application needs (raw mode, alternate screen, mouse capture, bracketed paste, focus events, kitty keyboard) and guarantees — via Drop — that every one of them is turned off again when the program exits, panics, or is killed by a signal it has registered for.

The contract is deliberately blunt: acquire one, use it, drop it. There is no open/close/reopen state machine to reason about. The Drop impl undoes the modes in reverse order, restores the cursor, and takes the scrollback back to where it was.

This page documents the knobs on SessionOptions, the invariants the guard upholds, and what happens when things go wrong. For the reasoning that led to this shape, see ADR-003.

Motivation

Terminals are global, shared, stateful hardware abstractions — they do not bounce back after a crash. A panic inside a widget that left CSI ?1049h applied will dump the user back at a shell with no echo, no cooked newlines, and a mysterious cursor on row 47. The historical answer — “remember to call cleanup()” — does not survive contact with panics, signals, or early returns.

RAII flips that responsibility: the guard is the cleanup. As long as the guard’s Drop runs (which Rust guarantees outside panic = "abort" and std::process::exit), the terminal is restored. All the rest of ftui-core assumes this and does not redundantly track mode state.

Mental model

SessionOptions ─┐ TerminalSession::new() │ enter raw mode │ push panic hook │ enable alt-screen ────┐ │ enable mouse SGR ────┤ tracked with │ enable paste ─────┤ _enabled flags │ enable focus ──────┤ │ enable kitty-kb ──────┘ ... application runs ... Drop: disable kitty-kb (if set) disable focus (if set) disable paste (if set) disable mouse (if set) disable alt-screen (if set) leave raw mode remove panic hook

The guard records a boolean *_enabled flag for each optional mode after the enabling escape sequence has actually been written, so partial failures (e.g. mouse enable rejected by the terminal) still leave the cleanup sound.

SessionOptions

src/bin/app.rs
use ftui_core::terminal_session::{SessionOptions, TerminalSession}; let session = TerminalSession::new(SessionOptions { alternate_screen: true, // CSI ? 1049 h — preserves scrollback mouse_capture: true, // CSI ? 1000;1002;1006 h — SGR mouse bracketed_paste: true, // CSI ? 2004 h — wrap pastes in markers focus_events: true, // CSI ? 1004 h — FocusIn / FocusOut kitty_keyboard: false, // CSI > 15 u — release + repeat (opt-in) intercept_signals: true, // install SIGINT/SIGTERM/SIGHUP handler })?;
FieldANSIWhen to enable
alternate_screenCSI ?1049hFull-screen apps that want scrollback restored on exit. Leave false for inline mode.
mouse_captureCSI ?1000;1002;1006h (plus mux-safe variant)Any app that wants clicks, drags, or wheel events. SGR supports coordinates > 223.
bracketed_pasteCSI ?2004hApps that need to distinguish typed from pasted text (e.g. a REPL).
focus_eventsCSI ?1004hApps that want to pause animations when the terminal loses focus.
kitty_keyboardCSI >15uApps that need key-release and key-repeat semantics (code editors). Opt-in, capability-gated.
intercept_signalsInstalls a signal handler that cooperates with shutdown_signal(). Default true.

All requested modes are sanitized against the detected TerminalCapabilities before any escape sequence is written — asking for kitty-keyboard inside tmux silently downgrades to “off” rather than emitting a sequence the mux will mangle.

What gets restored

On Drop, TerminalSession writes the disabling counterpart of every sequence it successfully emitted, in reverse order. Concretely, a session that enabled all five optional modes performs:

Exit kitty keyboard

CSI < u (pop flags).

Disable focus events

CSI ?1004l.

Disable bracketed paste

CSI ?2004l.

Disable mouse capture

CSI ?1006;1002;1000l (reverse order of enable).

Leave alt-screen

CSI ?1049l — this restores the original scrollback.

Leave raw mode

Cooked input, echo, and line buffering come back. Cursor is made visible.

Drop signal guard

The process signal mask returns to what it was before new().

The effect is idempotent: calling drop(session) explicitly or letting the guard fall out of scope both produce the same terminal state.

Invariants

  1. RAII cleanup runs on panic (unless the binary uses panic = "abort"). Drop is a library-level guarantee.

  2. Exclusive ownership. A process-wide SessionLock prevents two TerminalSession::new() calls from racing. The second call returns io::Error. (new_for_tests intentionally bypasses the lock so headless tests can run in parallel.)

  3. Sanitize before apply. Requested modes are AND-ed with TerminalCapabilities before emission (crates/ftui-core/src/terminal_session.rs:L360). Unsafe combos (kitty-keyboard inside tmux, focus events inside a mux) are silently cleared.

  4. No reference cycles into the terminal. The guard holds only bool flags and an optional SignalGuard. No thread ever observes a partially-initialized session.

Never construct a second session while one is alive. The exclusivity lock will return an error, but if you manage to defeat it (e.g. by leaking the first guard), cleanup order becomes undefined and the terminal may end up in raw mode with alt-screen active. Drop the first session before creating another.

Inline mode

If alternate_screen is left false, the session preserves the scrollback region; your UI must cooperate by rendering through InlineRenderer and picking an InlineStrategy that matches the terminal’s scroll-region and sync-output support.

use ftui_core::terminal_session::{SessionOptions, TerminalSession}; // Inline — no alt-screen, still want mouse + paste let _session = TerminalSession::new(SessionOptions { alternate_screen: false, mouse_capture: true, bracketed_paste: true, ..Default::default() })?;

Signals and panic interaction

When intercept_signals is true (the default), the session registers for SIGINT, SIGTERM, and SIGHUP on Unix. Any delivered signal sets the process-global shutdown_signal slot (first-pending wins) and arranges for an orderly drop of the session. The runtime event loop checks this slot every tick and breaks cleanly.

Panic hooks are installed for the same purpose: if a widget or the runtime panics, the hook ensures the guard’s Drop still fires before the default panic message is printed. Without this, the stack unwind might race the terminal restore and print the backtrace into raw-mode glyphs.

Testing hook

TerminalSession::new_for_tests(options) produces a headless guard that does not write escape sequences, does not acquire the exclusivity lock, and does not install a panic hook. Use it in tests that need a plausible session shape without touching a real TTY — or, better, use the ftui-harness program simulator which bypasses the terminal entirely.

Cross-references

Where next