Termstead
A native terminal product shell built around daemon-owned sessions and a client-owned workspace.
- Role
- Systems software engineer
- Sources
- 1 grouped repositories
Problem
Build a native terminal product without mixing GUI layout policy with PTY, process, and terminal-session authority.
Constraints
- The daemon owns PTYs, child processes, terminal state, revisions, modes, and session lifetime.
- The GUI owns windows, workspaces, tabs, pane layout, focus, commands, and client-side workspace policy.
- Terminal state reaches the renderer through validated full snapshots and revision-contiguous deltas.
- The optimized terminal canvas must remain separate from ordinary product chrome and per-cell widgets.
Architecture
Daemon and session authority
Own PTYs, child processes, authoritative terminal state, terminal modes, revisions, and daemon-session lifetime.
GUI shell and workspace
Own windows, tabs, recursive pane layout, focus, overlays, commands, and client-side workspace policy.
IPC and rendering
Carry validated snapshots/deltas across the IPC boundary and paint the terminal grid through one optimized canvas.
Evidence record
Mixed classificationsHow the system is split, synchronized, and tested
Termstead is presented here as an evidence-backed systems project: authority stays explicit across the daemon, GUI shell, IPC/SessionHub, and renderer, while acceptance and measurement caveats remain visible.
Ownership boundaries
The system is easier to reason about when authority, policy, processing, and presentation state have explicit owners.
Daemon and session authority
IMPLEMENTEDterminal-daemon and terminal-engine
Own PTYs, child processes, authoritative terminal state, terminal modes, revisions, and daemon-session lifetime.
GUI shell and workspace
IMPLEMENTEDterminal-app
Own windows, tabs, recursive pane layout, focus, commands, and view attachment lifecycle; layout policy stays on the client side.
IPC and renderer
IMPLEMENTEDterminal-ipc, SessionHub, and terminal-render
Move validated snapshots and revision-contiguous deltas across the Unix-socket boundary and paint the terminal grid through one optimized canvas.
Each boundary is shown with its owner and classification. The arrows describe the evidence-oriented relationship, not a claim that every message moves in one direction.
Transport and boundary contracts
Each boundary describes how data, identity, state, or work crosses between owned parts of the system.
Transport and identity
IMPLEMENTEDThe protocol uses one Unix connection with request IDs, session/attachment identity, bounded pending work, and a fixed reader path rather than one socket per pane.
Authoritative state
IMPLEMENTEDFullSnapshot and revision-contiguous RenderDelta are state authority; RenderReady is only a bounded/coalescible wake hint.
Backpressure and recovery
ACCEPTEDPending requests, ordered events, RenderReady entries, UI acknowledgements, and bootstrap waits are bounded; journal gaps recover with a snapshot and disconnects reconnect through a fresh identity.
Event mode
ACCEPTEDNegotiated RenderReady event mode has no compatibility polling deadline; legacy fallback is one process-wide compatibility path rather than a timer per tab or pane.
Architecture decisions
Each decision records the boundary it protects and the alternative that was deliberately not chosen.
Keep workspace layout client-owned
ACCEPTED- Decision
- The GUI owns windows, tabs, splits, focus, commands, and layout policy; the daemon owns terminal sessions.
- Why
- View composition can evolve without putting presentation policy into terminald or confusing a pane with a process.
- Alternative considered
- Daemon-owned workspace/layout was rejected because it would couple terminal authority to GUI presentation state.
Treat a pane as a view of a session
ACCEPTED- Decision
- Pane, tab, close, and detach operations release the view binding; explicit TerminateSession owns process termination.
- Why
- Session lifetime must survive ordinary view changes and remain available for reconnect while termination stays explicit.
- Alternative considered
- Equating one tab with one process was rejected because tab/view lifetime and process lifetime have different semantics.
Use event-driven synchronization with snapshots and deltas
ACCEPTED- Decision
- RenderReady wakes visible bindings; snapshots and revision-contiguous deltas remain authoritative.
- Why
- The split reduces unnecessary polling while preserving recovery when a journal gap or reconnect invalidates incremental state.
- Alternative considered
- Per-pane polling and treating RenderReady as a state payload were rejected by the SessionHub design.
Keep the current engine dispatch model for now
ACCEPTED- Decision
- Retain one event-dispatch thread per daemon session while recording a future reactor revisit.
- Why
- The current baseline supports the implemented lifecycle and resource behavior, while the scaling cost remains visible instead of being hidden.
- Alternative considered
- A reactor/event-loop replacement is deferred until broader evidence justifies that architectural change.
Render through one terminal canvas
ACCEPTED- Decision
- Keep terminal-grid rendering in terminal-render and use ordinary GPUI elements for product chrome rather than per-cell widgets.
- Why
- The renderer boundary can validate terminal state and localize row work without turning every cell into a UI element.
- Alternative considered
- Per-cell GPUI elements were rejected as an unsuitable rendering boundary for the terminal grid.
Primary lifecycle
The primary sequence follows input, ownership, processing, result, and cleanup transitions without collapsing synchronous and queued work into one claim.
Open or reuse
IMPLEMENTEDOpen or reuse
GUI and SessionHub
Create or list/reuse a session, attach, register the pane/session/attachment/generation identity, acquire control, and request the initial snapshot.
Result: The binding becomes ready from authoritative daemon state.
Synchronize
ACCEPTEDSynchronize
Daemon, transport, SessionHub, and renderer
Commit a revision, emit a coalesced wake hint, fetch deltas when contiguous, or request a snapshot after a journal gap; acknowledge the UI update before the next one.
Result: The renderer applies validated state without treating the wake hint as state authority.
Detach a pane, tab, or window
IMPLEMENTEDDetach a pane, tab, or window
GUI shell
Hide or blur the view, release control, cancel pending binding work, and detach the attachment.
Result: The view ends while the daemon session and its terminal process continue.
Reconnect
ACCEPTEDReconnect
SessionHub
Reconnect the transport, re-establish attachment identity, bootstrap with a bounded snapshot, and discard stale generations.
Result: The client recovers from current authoritative state when the daemon remains available.
Explicit termination
IMPLEMENTEDExplicit termination
User through the GUI
Confirm TerminateSession as a separate command; the daemon then shuts down the selected engine/session.
Result: The named session transitions toward exited state; closing a view alone does not perform this operation.
Application quit semantics
DESIGNED / PLANNEDApplication quit semantics
Architecture boundary
The architecture record specifies detach-all without terminating sessions, but the inspected m6 source has no explicit AppCommand::Quit to independently verify as a named command.
Result: Treat this as a designed contract, not as a separately accepted runtime behavior.
Daemon crash, reboot, or power loss
DEFERREDDaemon crash, reboot, or power loss
Out of verified scope
Persistence across daemon failure or machine restart was not part of the inspected acceptance boundary.
Result: Deferred; no portfolio claim is made about this behavior.
Focused code excerpts
Short, source-pinned excerpts make one engineering boundary concrete without reproducing a source file.
Pass one cell advance to GPUI
rustfn forced_cell_width(text: &str, columns: u16, cell_width: gpui::Pixels) -> Option<gpui::Pixels> {
// GPUI's `force_width` is the advance assigned to each glyph, not the
// total width of the shaped run.
should_force_cell_width(text, columns).then_some(cell_width)
}The renderer correction passes one cell advance per glyph instead of a shaped run's total width, making the root cause concrete without claiming pixel-perfect output.
Limit: The correction was covered by focused geometry/shaping evidence, not a pixel-golden or cross-device rendering proof.
View exact sourcecommit 0776a19f39539c396df76038c55a14bb55948a53
Evidence, measurements, and limits
Values retain their context, sample, method, and limits so an implementation detail is not mistaken for universal capacity or outcome evidence.
| Evidence | Classification | Result and context | Method / environment / sample | Limitations |
|---|---|---|---|---|
| Daemon resource scaling | MEASURED | Detached sessions scale from 3,220 KiB daemon RSS at zero sessions to 29,808 KiB at 20 sessions; the 20-session row has 41 daemon threads and 124 file descriptors. Resource table for 0, 1, 5, 10, and 20 detached /bin/sh sessions. | Method: Release build followed by termstead-perf with three samples; daemon RSS/PSS excludes child-shell memory. Environment: Linux 7.0.0-31-generic x86_64; Intel i5-8500; 6 logical CPUs; 15,511,612 kB host memory; rustc/cargo 1.98.0. Sample: 3 samples; baseline recorded 2026-09-14 UTC from daemon commit 310bcd5f7b44a32c2fcaa8687fedffbbe713fceb. |
|
| Idle daemon CPU | MEASURED | Raw CPU ticks were zero at 0, 1, 10, and 20 sessions during the one-second idle samples; the documented interpretation is below 1.000% at CLK_TCK=100, not exactly zero CPU. Detached idle daemon/resource probe. | Method: Fixed idle interval using raw process ticks and the documented CLK_TCK resolution bound. Environment: Same Linux/Intel baseline host and release build as the performance baseline. Sample: One-second samples at four session counts; recorded 2026-09-14 UTC. |
|
| Startup and session creation | MEASURED | Handshake p50/p95 was 10.626/122.880 ms and session creation p50/p95 was 0.899/0.927 ms in the recorded sample. Daemon benchmark path, not a browser or native-window startup measurement. | Method: Release benchmark with three samples and p50/p95 summaries. Environment: Same Linux/Intel baseline host; daemon-side protocol/session operations. Sample: 3 samples; baseline recorded 2026-09-14 UTC. |
|
| Churn and short-soak boundedness | MEASURED | The recorded short soak completed 207 iterations with RSS moving from 4,036 to 40,196 KiB, a 43,208 KiB maximum, and final 10 file descriptors/4 threads; session and attachment churn returned thread/file-descriptor counts to baseline. Stress probes for session churn, attachment churn, malformed clients, resize, and detached output. | Method: Release stress harness with process resource samples and clean-up checks. Environment: Same Linux/Intel baseline host; workload-dependent shell/output paths. Sample: 100 session cycles, 100 attachment cycles, 207 short-soak iterations, plus bounded malformed-client/resize probes. |
|
| SessionHub boundedness and event mode | ACCEPTED | The accepted boundary records one multiplexed Unix connection, one reader thread, pending requests capped at 64, coalesced RenderReady entries capped at 64 sessions, ordered events capped at 64, and no compatibility polling in negotiated event mode. M2 SessionHub acceptance; later m6 source adds the product-shell bindings and lifecycle work. | Method: Focused protocol/transport/SessionHub tests plus a real Wayland acceptance run. Environment: Ubuntu/GNOME Wayland on Intel UHD 630/Mesa for the live acceptance portion. Sample: Acceptance record status PASS WITH FOLLOW-UPS; five resource runs for the retained M2 resource confirmation. |
|
| Daemon-side input-to-authoritative-state marker | MEASURED | The baseline reports p50/p95 0.600/0.664 ms without flood and 0.638/0.750 ms under another-session flood. Input through PTY/parser/state to an authoritative snapshot marker. | Method: Daemon performance harness marker; includes input, PTY/parsing, state, snapshot request, and serialization. Environment: Same Linux/Intel baseline host and release build. Sample: Recorded 2026-09-14 UTC; benchmark sample size is documented as 10 for microbenchmarks, while this marker is not a display probe. |
|
| Renderer geometry and shaping smoke | ACCEPTED | The real Wayland smoke recorded a 14 px font, 8.429 logical-cell width, 16.100 cell width, 16.198 baseline, an 80x24 to 113x29 grid change, 69 shaped runs, 10 fallback runs, and 4 emoji glyph flags. Renderer correction and native-window geometry/shaping evidence. | Method: Focused terminal-render tests plus one real-host Wayland smoke after the force_width correction. Environment: Ubuntu 26.04.1; GNOME 50.1 Wayland; Linux 7.0.0-31; Intel UHD 630/Mesa; scale 1. Sample: Focused test suite and one recorded real-window smoke on 2026-09-22. |
|
| Physical input and IME acceptance | NOT TESTED | Physical keyboard, mouse, and IME acceptance was not run in the inspected Termstead acceptance records. A deliberate boundary on the native interaction evidence. | Method: No physical-device test was available in the recorded acceptance environment. Environment: Recorded Wayland host evidence covered native-window geometry/shaping, not physical input or IME behavior. Sample: No physical-device sample. |
|
| Pixel-golden and compositor coverage | ENVIRONMENT-LIMITED | The recorded renderer proof is environment-limited to geometry/shaping counters; pixel-golden capture and broad compositor/font/device coverage were unavailable. Visual evidence boundary for the force_width correction. | Method: Real-host Wayland smoke plus focused renderer tests; no pixel comparison harness was run. Environment: Ubuntu 26.04.1, GNOME 50.1 Wayland, Intel UHD 630/Mesa, scale 1. Sample: One real-window smoke and focused tests on 2026-09-22. |
|
| Bounded journals and scrollback | IMPLEMENTED | The design and source bound the delta journal at 256 entries/8 MiB and use a default 10,000-line scrollback; transport queues and UI acknowledgement slots are bounded separately. A source/design guardrail, not a claim that every workload has a fixed byte cost. | Method: Source and protocol-spec inspection with focused bounded-queue tests. Environment: Repository source at the verified remote default-branch commit. Sample: Deterministic implementation/test evidence; no production workload sample. |
|
Failure and recovery stories
Each story connects a symptom to an owned boundary and keeps verification limits visible.
Correcting per-glyph terminal width at the GPUI boundary
ACCEPTED- Symptom
- Terminal text showed scattered glyph spacing because a shaped run could receive an unexpectedly large forced width.
- Root cause
- The renderer passed total run width (columns multiplied by cell width) to GPUI TextRun.force_width, although GPUI applies that value as the advance for each glyph.
- Correction
- Pass one cell width only for compatible runs whose scalar count matches terminal columns; exclude wide and combining content from that shortcut.
- Verification
- Focused renderer tests cover one-cell-per-glyph, combining/wide exclusions, fractional edges, and resize metrics; the real Wayland smoke confirmed geometry/shaping counters after the correction.
Limitations
- The correction was not validated with a pixel-golden screenshot.
- Visual acceptance remained environment-limited because physical/compositor capture was unavailable.
Limitations and deferred evidence
- The primary evidence target is the verified remote default branch product-shell-m6-layout-persistence at 0776a19f39539c396df76038c55a14bb55948a53; a separate local m8 checkout was dirty and was not used as the portfolio evidence basis.
- The evidence record does not claim persistence across daemon crash, logout, reboot, or power loss; that scope is deferred.
- The inspected source has no explicit AppCommand::Quit matching the architecture document's detach-all quit semantics, so that behavior remains designed rather than independently accepted.
- Physical keyboard, mouse, IME, broad compositor/font/device coverage, and pixel-golden capture were not tested or were environment-limited.
- Below-resolution CPU samples are reported as bounds, not zero-CPU claims; daemon-side markers are not input-to-display measurements.
- The resource baseline is single-host evidence. Output-memory budgeting remains an investigation item, and the thread-per-session model is retained with a future reactor revisit.
- The M2 SessionHub acceptance is PASS WITH FOLLOW-UPS and should not be read as unconditional production proof for every later product-shell feature.
Source evidence
Links point to the commits inspected for this page. The classification attached to each claim is more important than the existence of a link alone.
- Product-shell architecture
Ownership, lifecycle semantics, IPC authority, renderer boundary, alternatives, and explicit non-goals.
commit f0889ba7ebf8d5585a20aedc22439dcc12b98472 · 2026-09-22
- SessionHub specification
Approved bounded transport, identity, event, backpressure, and recovery contract.
commit 8ca581048f33a474f27f74b0a2c3e47c1b5364c6 · 2026-09-22
- SessionHub acceptance
PASS WITH FOLLOW-UPS acceptance record with live Wayland and resource limitations.
commit 8ca581048f33a474f27f74b0a2c3e47c1b5364c6 · 2026-09-22
- Performance baseline
Resource scaling, latency markers, throughput, stress, boundedness, and methodology.
commit 0bd57c0c3e82aefaee91392e5d78e3de3f11a5bc · 2026-09-14
- Performance methodology
Definitions and caveats separating daemon-side measurements from display/frame claims.
commit 0bd57c0c3e82aefaee91392e5d78e3de3f11a5bc · 2026-09-14
- GPUI final acceptance
Renderer correction, real-host geometry/shaping smoke, and environment-limited visual acceptance.
commit 6391d27b29524ea07d6a0afeb3dcdf4edb48000a · 2026-09-22
- Terminal core wire model
Session, attachment, revision, snapshot, delta, and lifecycle types.
commit 0776a19f39539c396df76038c55a14bb55948a53
- Terminal engine
PTY, terminal state, parser/event loop, resize, and engine shutdown ownership.
commit 0776a19f39539c396df76038c55a14bb55948a53
- Terminal daemon
Session manager, attachments, control, journals, lifecycle, and event dispatch.
commit 0776a19f39539c396df76038c55a14bb55948a53
- IPC protocol
Protocol versions, capabilities, RenderReady, and lifecycle message kinds.
commit 0776a19f39539c396df76038c55a14bb55948a53
- Multiplexed transport
Bounded requests/events, request identity, reader/writer ownership, and coalescing.
commit 0776a19f39539c396df76038c55a14bb55948a53
- SessionHub implementation
Process-wide hub, binding identity, synchronization, visibility, recovery, and cleanup.
commit 0776a19f39539c396df76038c55a14bb55948a53
- Product shell lifecycle commands
Pane/tab close, detach, explicit termination, and workspace-level view cleanup.
commit 0776a19f39539c396df76038c55a14bb55948a53
- Application command definitions
Named application commands inspected for lifecycle coverage, including the absence of an explicit Quit command.
commit 0776a19f39539c396df76038c55a14bb55948a53
- Semantic input boundary
Protocol-independent key, paste, focus, mouse, and input-mode encoding boundary; source inspection is not physical-device acceptance.
commit 0776a19f39539c396df76038c55a14bb55948a53
- Renderer state model
Atomic snapshot validation, contiguous delta application, identity, and grid checks.
commit 0776a19f39539c396df76038c55a14bb55948a53
- Terminal renderer
Run shaping, cell-width correction, row caching, and one-canvas paint boundary.
commit 0776a19f39539c396df76038c55a14bb55948a53
Repository enrichment
Mutable public facts are optional; the project identity above is portfolio-owned.
termstead
Native terminal product, daemon, IPC, session lifecycle, and renderer workspace
- Updated
- 23 Sept 2026
- Language
- Rust
- Stars
- 0
- Forks
- 0
Related Engineering Notes
Related Engineering Decisions
See the selected constraints, alternatives, and evidence behind this project's key boundaries.
Explore Termstead decisionsContent notes
- No quantitative benchmark is promoted into the summary model; future evidence content should include methodology and caveats.