Shared Rust engine¶
roost-engine is Roost's toolkit-neutral authoritative Rust application
engine. It is the core of the Linux UI — roost-iced is an adapter over it,
not a second implementation of it — and it is the seam a future convergence
would build on, whichever way
that question resolves: either an iced
shell on both platforms sits on this engine, or a Swift shell adopts it
incrementally without exposing Rust layouts across an ABI.
That the engine has exactly one consumer today is a fact about the present, not a licence to fold it back into the UI. Its whole value is that the authoritative state machine is separable from the toolkit.
Ownership¶
The engine owns workspace transitions and ordered events, agent-state
derivation, persistence/restoration, PTY supervision, terminal session
lifecycle, streaming OSC routing/application, profile-scoped instance locking,
full-state reconciliation, and target-neutral IPC dispatch. It reuses the
focused roost-ipc and roost-osc contracts.
roost-ui-model owns toolkit-neutral configuration, terminal theme/color
resolution, keybinds, command palettes, providers, custom commands, agent and
notification projections, project rollups, shell escaping, and word
selection.
roost-vt::TerminalScroll owns the synchronous terminal-wheel policy. Every
live terminal has an independent accumulator and snap-to-bottom state. The
adapter normalizes native units into signed rows and retains pointer geometry,
modifiers, encoders, and PTY writes; the shared model selects mouse-report,
alternate-screen key, or local viewport behavior with mouse tracking taking
precedence. Local movement reconciles against libghostty's authoritative
scrollbar rather than guessing whether a downward request reached the live
bottom. The outcome enum is deliberately a plain value type — a realistic
adoption seam for a second adapter without exposing Rust layouts through an
ABI.
The UI adapter retains native widgets and layout, terminal drawing and libghostty-vt event-loop access, platform input translation, clipboard and notifications, URL launching, screenshot capture, and event-loop marshalling.
Boundary¶
The crate boundary in production use today is the concrete API: Workspace,
LocalClient, PtySupervisor, and roost_engine::ipc's exhaustively-matched
UiRequest port. The exhaustive match is the mechanism, not a style
preference: a new capability cannot reach the UI without the compiler naming
every adapter that has to answer it. Never add a wildcard arm.
events::subscribe bridges Workspace's broadcast channel into an unbounded
mpsc the adapter drains on its own main-loop task — for iced, the EngineFeed
wake subscription — folding lag or the startup gap into a full-state Resync
the adapter reconciles against.
The facade module (Engine/EngineCommand/EngineSnapshot/
EngineEventStream) is the experimental Swift-facing boundary. It has no
production consumer, is feature-gated (roost-engine/facade, compiled and
tested in CI but off by default), and stays unproven until a real adapter
adopts it — tracked as
#286, held at "don't invest,
don't delete" while the Mac-shell question is open (see
Direction and the
iced migration's M5). Its design:
EngineCommand reuses serializable roost-ipc request DTOs.
Engine::execute returns an owned CommandResult or a typed EngineError
with a stable status key. EngineSnapshot is a versioned, owned, ordered
replacement projection. Every committed workspace transition has a monotonic
in-process revision; compound events share that revision and retain their
established order.
EngineEventStream subscribes before its initial snapshot, discards any stale
buffered deltas already represented by that snapshot, and turns broadcast lag
or a revision gap into EngineEvent::Resync. Event publication is synchronous
and non-blocking while the state lock establishes order. No UI callback runs
under that lock, and persistence I/O happens after it is released.
Workspace also owns the last selected live tab for each project. Its
preferred_tab query falls back to display order and repairs preferences when
tabs or projects close. Adapters therefore do not need a competing
per-project selection map, and project shortcuts can resolve against a fresh
authoritative snapshot. Only the globally active project/tab position is
persisted today; inactive-project preferences are intentionally runtime-only
and rebuild during restoration, preserving the established state.json
schema and semantics.
Each live Rust terminal owns a roost_engine::osc::OscRouter. The caller feeds
PTY bytes plus an owned renderer-derived RGB/palette snapshot and receives an
ordered list of workspace, PTY-input, clipboard, and pointer actions. This
keeps split-sequence scan state and reply ordering inside the engine while the
adapter retains libghostty-vt access and executes native clipboard/pointer
ports. No UI callback or renderer type crosses the router boundary.
Dependency checks¶
make check-iced enforces these with cargo tree greps; CI runs its
own equivalent in iced-build-e2e's toolkit-boundary step:
# make check-iced (excerpt)
cargo tree -p roost-iced | grep -E 'gtk4|libadwaita|pango|cairo-rs' # must not match
cargo tree -p roost-engine | grep -E 'gtk4|libadwaita|iced|notify-rust|zbus|arboard' # must not match
Neither shared crate may gain Iced, AppKit, or any other renderer, windowing,
or desktop-integration dependency — the Make target names iced,
notify-rust, zbus, and arboard explicitly for roost-engine; the CI
step covers roost-engine (gtk4/libadwaita/iced) and additionally
roost-ui-model (those plus pango/cairo-rs/wgpu). roost-ui-model uses the
renderer-independent roost_vt::ColorRgb; enabling libghostty-vt FFI remains a
UI-adapter choice.
roost-iced depends on roost-engine; neither shared crate depends back on
the adapter.