Architecture - D0n9X1n/SonicTerm GitHub Wiki
Start here for the system map. Follow one input in From Keypress to Pixel, then use Runtime Lifecycle for state changes, Architecture Internals for correctness rules, and Crate Reference for exact dependencies.
SonicTerm separates terminal behavior from native windows and presentation. A pseudo-terminal (PTY) is the operating-system channel between a pane and its child process. PTY work runs away from the winit event-loop thread.
flowchart TD
platform["sonicterm-mac / sonicterm-windows / sonicterm-linux"]
shell["MacShell / WindowsShell / LinuxShell"]
app["sonicterm-app<br/>live windows, tabs, panes, event routing"]
core["sonicterm-app-core<br/>pure intents, state, effects"]
io["sonicterm-io<br/>PTY and process I/O"]
vt["sonicterm-vt<br/>ANSI/VT parser"]
grid["sonicterm-grid<br/>cells, history, dirty rows"]
model["sonicterm-render-model<br/>renderer-facing frame data"]
font["sonicterm-font / engine / text<br/>discovery, shape, raster, caches"]
gpu["sonicterm-gpu<br/>frame assembly and presentation"]
screen(["native window surface"])
resource["sonicterm-resource<br/>owner tree and ledger"]
platform --> shell --> app
app --> core
app --> io --> vt --> grid --> model --> gpu --> screen
font --> gpu
app --> resource
The arrows show runtime flow, not every Cargo edge. sonicterm-app owns the
live topology. sonicterm-app-core owns a separate backend-free state machine.
sonicterm-gpu receives terminal and UI types through the render-model boundary.
| Boundary | Owns | Excludes |
|---|---|---|
sonicterm-types |
small values and backend-free trait contracts | winit, wgpu, native PTYs |
sonicterm-resource |
resource owners, ledger entries, reservations, close checks | retained payloads and seam-specific reclamation |
sonicterm-app-core |
AppState, AppIntent, AppEffect, reducers, effect ordering |
native handles, blocking I/O, winit, wgpu |
sonicterm-io |
local PTY/process work | ANSI interpretation and UI state |
sonicterm-vt / sonicterm-grid
|
terminal parsing, cells, scrollback, cursor state, dirty rows | native windows and GPU resources |
sonicterm-cfg / sonicterm-ui
|
configuration, themes, keymaps, tabs, panes, search, selection, IME | native presentation calls |
sonicterm-render-model |
pane geometry and renderer-facing data types | wgpu policy and window ownership |
sonicterm-font-config and font wrapper crates |
font configuration and generated FFI boundaries | app and renderer topology |
sonicterm-font / sonicterm-engine / sonicterm-text
|
discovery, shaping, rasterization, atlas data, row glyph caches | window lifecycle and PTY ownership |
sonicterm-gpu |
damage, row/background caches, frame assembly, wgpu, Windows CPU composition | app topology and child processes |
sonicterm-app |
winit handler, live topology, PTY wiring, redraw scheduling, config application | platform-only AppKit, Win32, X11, or Wayland setup |
| platform crates | executable startup, native menus, drag/drop, backdrop hooks, package metadata | reusable terminal behavior |
The renderer has one declared terminal/UI type seam. sonicterm-gpu depends on
sonicterm-render-model, not directly on sonicterm-grid, sonicterm-cfg, or
sonicterm-ui. It imports their unchanged type identities through
sonicterm_render_model::boundary::{grid,cfg,ui}.
App owns HashMap<WindowId, WindowState> and main_window_id. The main window
and torn-out windows use the same WindowState representation.
Keyboard, IME, focus and modifier events resolve their source identity before main/child presentation paths diverge. Shared input ownership does not replace per-pane terminal protocol negotiation or native platform metadata. Native drop ownership is also selected before window creation, using the installed backend's capability rather than a second window registry.
Each WindowState owns:
- its optional
Arc<Window>andGpuRenderer; -
TabBarandVec<TabState>; -
HashMap<PaneId, PaneState>; - selection, copy mode, IME, drag, notification, hover, and redraw state.
Each TabState owns a PaneTree, its active pane id, search state, and command
status. Each PaneState owns its parser, optional PtyHandle, redraw target,
terminal-mode atomics, inline images, and resource charges.
AppStateMachine holds backend-free compatibility observations, not live GUI
topology. App::observe_intent updates them and discards their effects;
App::dispatch_intent handles supported explicit-target work separately.
Missing, removed, or zero window keys never select main or frontmost. Native
input goes to its source window's active pane through the bounded PTY queue.
See Runtime Lifecycle for the state/intent/effect inventory
and the distinction between record-only effects and native completion.
The backend-free reducer keeps stable effect ordering. GUI observations do not execute its batch; explicit-target operations cross the live app separately. The exact order, cascade bound, and dormant queue are recorded in Runtime Lifecycle.
A PtyHandle owns the child process boundary, input/output channels, and native
reader and writer threads. A pane VT worker owns parser advancement. It holds the
pane parser lock while applying a batch, then releases it before sending
UserEvent::PaneOutput, at most one outstanding per pane.
Worker threads do not resolve WindowId or call native window APIs. The winit
thread finds the pane's current window and requests a frame only when that window
shows new output or changed command chrome.
Both window roles use the app-local VisibleFrameSources collector. It validates
the active layout before cloning visible parser/image handles and viewport metadata.
It acquires all visible parser guards with try_lock, then briefly locks and copies
only those panes' image lists. Inactive-tab and zoom-hidden image stores cannot defer
the visible frame and are not cloned. The owned handles outlive the guards through
ordinary borrows, without a lifetime cast. The shared builder creates real
PaneRender grid borrows and moves each image snapshot once, inside the frame source
(HeldFrameSource) that owns the parser guards. GpuRenderer::render_releasing borrows the
panes from that source exactly once, to assemble the frame. The guards drop as soon as
assembly ends, before the surface is acquired, the retained frame blitted, or the frame
submitted and presented, so the VT worker can parse during presentation. A presented frame
returns metadata receipts. The window applies them at its next successful collection, under
the guards that collection holds. Any visible lock miss discards the whole collection before
entering the existing contention retry.
GpuRenderer::render_releasing receives a frame source that lends visible PaneRender records,
plus explicit UI arguments. A metadata-only FramePlan selects identity, mode, damage, clips,
viewport slots, and expected revisions; it is not a copied-grid or threaded
renderer boundary. Production uses PaneRender and WeztermPipeline, not the
public compatibility RenderInputs/Painter seams. See
Rendering and Fonts for frame assembly and
Architecture Internals for guard and revision rules.
sonicterm-engine::FontStack adapts sonicterm-font to the renderer. HarfBuzz
shapes text. CoreText discovers fonts on macOS, GDI on Windows, and Fontconfig on
Linux. DirectWrite is the default Windows rasterizer. FreeType is the default on
macOS and Linux and the Windows fallback.
Generated FreeType and HarfBuzz bindings and hand-written Fontconfig declarations stay inside their wrapper crates. The renderer receives safe shape results, glyph metrics, and raster pixels rather than raw FFI handles.
The three shipping binaries share ShellRunner through MacShell,
WindowsShell, and LinuxShell.
- macOS owns AppKit menu setup, native tab suppression, pasteboard handoff, and AppKit window hooks.
- Windows owns per-monitor-v2 DPI setup,
mudamenus, DWM backdrop work, OLE drag/drop, and GDI software presentation hooks. - Linux owns X11/Wayland application identity, package assets, and font preflight. All platform binaries expose the shared native runtime smoke; Linux package layouts additionally run it on X11 and Wayland. Native menus, desktop notifications, material backdrops, and cross-process tab drag are absent there.
All reusable keyboard, terminal, pane, and renderer behavior stays in shared crates. The UI text editor uses AppKit's string-only word-boundary API on macOS for native Option deletion; it creates no native views or presentation objects.
App owns a process-local ResourceGovernor. The live GUI owner tree is
Process â Window â AppPane. Seam code owns and enforces the primary memory
caps. The pane owner has a derived governor backstop. The process and window
owners track totals without aggregate limits.
The governor records only charged classes. Renderer surfaces, glyph atlases, and software frames are measured separately and are not in the governor total. See Memory and Runtime Lifecycle for the accounting and release rules.
- Only the winit event-loop thread creates, resolves, or presents native windows.
- PTY reader, writer, VT, path-probe, and cleanup workers stay outside the event loop.
- Rendering never blocks on a parser. One unavailable required lock defers the whole frame.
- A tab transfer moves each live
PaneStateandPtyHandle. It changes the shared redrawWindowId; it does not clone or restart the shell. - Dropping
PtyHandlestarts bounded process and I/O teardown. Existing-window transfers validate destination readiness and retain detached custody until attachment commits. Direct drag-merge uses this same boundary. Destination setup, topology, or charge-admission refusal restores source order, focus, tree/zoom, live PTYs, sizes, and charges. New-window tear-out prepares hidden native artifacts and transfers accounting before revealing the destination; source hiding or reaping happens only after commitment. - Terminal mutations mark damage in the same frame. Cache invalidation follows font, scale, theme, surface, atlas, and topology changes.
The exact safety conditions are in Architecture Internals.
| Group | Direction |
|---|---|
| contracts | sonicterm-types |
| accounting |
sonicterm-resource feeds sonicterm-app; logging tests also use it, and it owns no terminal or frame payload |
| terminal |
sonicterm-io supplies bytes; sonicterm-vt interprets them; sonicterm-grid stores the result |
| UI model |
sonicterm-cfg and sonicterm-grid feed sonicterm-ui; all three feed sonicterm-render-model
|
| fonts |
sonicterm-font-config and native wrappers feed sonicterm-font; sonicterm-font and sonicterm-text independently feed sonicterm-engine
|
| rendering | render model, engine, text, types, and block glyphs feed sonicterm-gpu
|
| app | app core, terminal, UI, rendering, logging, and resource crates feed sonicterm-app
|
| platform |
sonicterm-app feeds sonicterm-mac, sonicterm-windows, and sonicterm-linux
|
| Topic | Primary paths |
|---|---|
| App state and topology | crates/sonicterm-app/src/app/{mod,window_state,window_registry}.rs |
| Intents, effects, and reducer | crates/sonicterm-app-core/src/{intent,effect,reducer,state_machine,app_state}.rs |
| Shell boundary | crates/sonicterm-app/src/shell.rs |
| PTY and process boundary | crates/sonicterm-io/src/pty.rs |
| VT and grid |
crates/sonicterm-vt/src/vt.rs, crates/sonicterm-grid/src/grid.rs
|
| Render model | crates/sonicterm-render-model/src/{pane_render,inputs,painter,lib}.rs |
| Renderer | crates/sonicterm-gpu/src/core.rs |
| Font adapter | crates/sonicterm-engine/src/fontstack.rs |
| Resource governor and app charging |
crates/sonicterm-resource/src/, crates/sonicterm-app/src/app/retention.rs
|
| Platform entry points | crates/sonicterm-{mac,windows,linux}/src/main.rs |