FrankenTUI (ftui)

September 20, 2026 · View on GitHub

███████╗██████╗  █████╗ ███╗   ██╗██╗  ██╗███████╗███╗   ██╗████████╗██╗   ██╗██╗
██╔════╝██╔══██╗██╔══██╗████╗  ██║██║ ██╔╝██╔════╝████╗  ██║╚══██╔══╝██║   ██║██║
█████╗  ██████╔╝███████║██╔██╗ ██║█████╔╝ █████╗  ██╔██╗ ██║   ██║   ██║   ██║██║
██╔══╝  ██╔══██╗██╔══██║██║╚██╗██║██╔═██╗ ██╔══╝  ██║╚██╗██║   ██║   ██║   ██║██║
██║     ██║  ██║██║  ██║██║ ╚████║██║  ██╗███████╗██║ ╚████║   ██║   ╚██████╔╝██║
╚═╝     ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝╚══════╝╚═╝  ╚═══╝   ╚═╝    ╚═════╝ ╚═╝
FrankenTUI - Minimal, high-performance terminal UI kernel

High‑performance terminal UI kernel -- 1.1M+ lines of Rust across 20 crates, 80+ widget/stateful-widget implementations, 45 interactive demo screens, a Bayesian intelligence layer, resizable pane workspaces, and in-tree web/WASM backends -- focused on correctness, determinism, and clean architecture.

status rust license crates widgets screens

Try It in Your Browser

All 45 screens run as WASM at frankentui.com/web — no install, no toolchain. It renders through WebGPU where that exists and falls back to canvas2d, so Chrome, Edge, Safari and Firefox all work.

Phones too: swipe in from either edge to change screens, and the bar along the bottom carries a button for every key the current screen uses. Press and hold to drag; a divider drags straight away.

Quick Run (from source)

The primary way to see what the system can do is the demo showcase: cargo run -p ftui-demo-showcase (not the harness).

# Download source with curl (no installer yet)
curl -fsSL https://codeload.github.com/Dicklesworthstone/frankentui/tar.gz/main | tar -xz
cd frankentui-main

# Run the demo showcase (primary way to see what FrankenTUI can do)
cargo run -p ftui-demo-showcase

Or clone with git:

git clone https://github.com/Dicklesworthstone/frankentui.git
cd frankentui
cargo run -p ftui-demo-showcase

TL;DR

The Problem: Most TUI stacks make it easy to draw widgets, but hard to build correct, flicker‑free, inline UIs with strict terminal cleanup and deterministic rendering.

The Solution: FrankenTUI is a kernel‑level TUI foundation with a disciplined runtime, diff‑based renderer, and inline‑mode support that preserves scrollback while keeping UI chrome stable.

Why Use FrankenTUI?

FeatureWhat It DoesExample
Inline modeStable UI at top/bottom while logs scroll aboveScreenMode::Inline { ui_height: 10 } in the runtime
Deterministic renderingBuffer → Diff → Presenter → ANSI, no hidden I/OBufferDiff::compute(&prev, &next)
One‑writer ruleSerializes output for correctnessTerminalWriter owns all stdout writes
RAII cleanupTerminal state restored even on panicTerminalSession in ftui-core
Composable cratesLayout, text, style, runtime, widgetsAdd only what you need
80+ widgetsBlock, Paragraph, Table, Input, Tree, Modal, Command Palette, etc.Direct Widget / StatefulWidget implementations across ftui-widgets
Pane workspacesDrag‑to‑resize, docking, magnetic snap, inertial throw, undo/redoPaneTree + PaneDragResizeMachine in ftui-layout
Web/WASM backendSame Rust core renders in browser-oriented environmentsIn-tree ftui-web + ftui-showcase-wasm
Bayesian intelligenceStatistical diff strategy, resize coalescing, capability detectionBOCPD, VOI, conformal prediction, e‑processes
Shadow‑run validationProve rendering determinism across runtime migrationsShadowRun::compare() in ftui-harness
45 demo screensDashboard, visual effects, widget gallery, layout lab, and morecargo run -p ftui-demo-showcase

Getting Started (Library Consumers)

If you want to embed FrankenTUI in your own Rust app (not just run the demo), start here: docs/getting-started.md.

For web embedding into frankentui_website (Next.js + bun), see the Embedding In frankentui_website section in docs/getting-started.md. It includes:

  • in-tree build/check guidance for ftui-web and ftui-showcase-wasm,
  • a clear note that FrankenTermWeb is currently adjacent/out-of-tree from this checkout,
  • runtime initialization guidance for FrankenTermWeb once that bundle is available,
  • and explicit no-xterm.js guidance.

Quick Example

# Demo showcase (primary)
cargo run -p ftui-demo-showcase

# Pick a specific showcase screen
cargo run -p ftui-demo-showcase -- --screen=2   # Dashboard
FTUI_DEMO_SCREEN=18 cargo run -p ftui-demo-showcase  # Visual Effects

# Agent shell with a real child process
cargo run -p ftui --example agent_shell -- --exit-when-child-exits -- seq 1 10000

Run cargo run -p ftui-demo-showcase -- --help for screen numbers and options. Press 1–9/0 to select screens 1–10, or Tab/Shift+Tab to cycle through screens.


The demo showcase (cargo run -p ftui-demo-showcase) ships 45 interactive screens, each demonstrating a different subsystem:

CategoryScreensWhat They Show
Overviewdashboard, widget_gallery, advanced_featuresFull-system demos with animated panels, sparklines, markdown streaming
Layoutlayout_lab, layout_inspector, intrinsic_sizing, responsive_demoFlex/Grid solvers, pane workspaces, constraint visualization
Textshakespeare, markdown_rich_text, markdown_live_editor, advanced_text_editorRope editor, syntax highlighting, streaming markdown
Datatable_theme_gallery, data_viz, virtualized_search, log_searchThemed tables, charts, Fenwick-backed virtualization
Inputforms_input, form_validation, command_palette_lab, mouse_playgroundBayesian fuzzy search, gesture recognition, focus management
Visual FXvisual_effects, theme_studio, mermaid_showcase, mermaid_mega_showcaseGray-Scott reaction-diffusion, metaballs, Clifford attractors, fractal zooms
Systemterminal_capabilities, performance, performance_hud, determinism_labCapability probing, frame budgets, conformal risk gating
Diagnosticsexplainability_cockpit, voi_overlay, snapshot_player, accessibility_panelEvidence ledgers, VOI sampling visualization, a11y tree
Workflowfile_browser, kanban_board, async_tasks, notifications, drag_dropFile picker, task boards, notification toasts, drag handles
Advancedinline_mode_story, hyperlink_playground, i18n_demo, macro_recorder, quake_easter_eggInline scrollback, OSC 8 links, locale switching, event recording, a depth-buffered E1M1
Codecode_explorer, widget_builder, action_timelineAST browsing, widget composition, timeline aggregation
Tourguided_tourCinematic auto-play tour across key screens

Each screen is also a snapshot test target. BLESS=1 cargo test -p ftui-demo-showcase updates baselines.


Use Cases

  • Inline UI for CLI tools where logs must keep scrolling.
  • Full-screen dashboards that must never flicker.
  • Deterministic rendering harnesses for terminal regressions.
  • Libraries that want a strict “kernel” but their own widget layer.

Non-Goals

  • Not a full batteries‑included app framework (by design).
  • Not a drop‑in replacement for existing widget libraries.
  • Not a “best effort” renderer; correctness beats convenience.

Minimal API Example

This example targets FrankenTUI 0.9.0 with the default features enabled. See installation for version and local checkout dependencies.

use ftui::prelude::*;
use ftui::widgets::paragraph::Paragraph;

struct TickApp {
    ticks: u64,
}

#[derive(Debug, Clone)]
enum Msg {
    Tick,
    Quit,
}

impl From<Event> for Msg {
    fn from(e: Event) -> Self {
        match e {
            Event::Key(k) if k.is_char('q') => Msg::Quit,
            _ => Msg::Tick,
        }
    }
}

impl Model for TickApp {
    type Message = Msg;

    fn update(&mut self, msg: Msg) -> Cmd<Msg> {
        match msg {
            Msg::Tick => {
                self.ticks += 1;
                Cmd::none()
            }
            Msg::Quit => Cmd::quit(),
        }
    }

    fn view(&self, frame: &mut Frame) {
        let text = format!("Ticks: {}  (press 'q' to quit)", self.ticks);
        let area = Rect::new(0, 0, frame.width(), 1);
        Paragraph::new(text).render(area, frame);
    }
}

fn main() -> std::io::Result<()> {
    App::new(TickApp { ticks: 0 })
        .screen_mode(ScreenMode::Inline { ui_height: 1 })
        .run()
}

Design Philosophy

  1. Correctness over cleverness. Predictable terminal state is non-negotiable.
  2. Deterministic output. Buffer diffs and explicit presentation over ad-hoc writes.
  3. Inline first. Preserve scrollback while keeping chrome stable.
  4. Layered architecture. Core, render, runtime, widgets; no cyclic dependencies.
  5. Zero-surprise teardown. RAII cleanup, even when apps crash.

Workspace Overview (20 Crates)

Core Architecture

CratePurposeStatus
ftuiPublic facade + preludeImplemented
ftui-coreTerminal lifecycle, events, capabilities, animation, input parsing, gesturesImplemented
ftui-renderBuffer, diff, ANSI presenter, frame, grapheme pool, budget systemImplemented
ftui-styleStyle + theme system with CSS‑like cascadingImplemented
ftui-textSpans, segments, rope editor, cursor, BiDi, shaping, normalizationImplemented
ftui-layoutFlex + Grid solvers, pane workspace system (9K+ lines), e‑graph optimizerImplemented
ftui-runtimeElm/Bubbletea runtime, effect system, subscriptions, rollout policy, telemetry schema (13K+ line program.rs)Implemented
ftui-widgets80+ direct Widget / StatefulWidget implementations across the libraryImplemented
ftui-extrasFeature‑gated add‑ons, VFX rasterizer (opt‑level=3)Implemented

Backend & Platform

CratePurposeStatus
ftui-backendBackend abstraction layerImplemented
ftui-ttyTTY terminal backendImplemented
ftui-webWeb/WASM adapter with pointer/touch parityImplemented
ftui-showcase-wasmWASM build of the demo showcaseImplemented

Testing & Verification

CratePurposeStatus
ftui-harnessTest harness, shadow‑run comparison, benchmark gate, rollout scorecard, determinism fixturesImplemented
ftui-ptyPTY‑based test utilitiesImplemented
ftui-demo-showcase45 interactive demo screens + snapshot testsImplemented
doctor_frankentuiIntegrated TUI capture, seeding, suite reporting, diagnostics, and coverage gatingImplemented

Supporting

CratePurposeStatus
ftui-a11yAccessibility tree and node structuresImplemented
ftui-i18nInternationalization supportImplemented
ftui-simdPortable-SIMD ASCII detection and row compare (opt-in simd feature)Implemented

How FrankenTUI Compares

FeatureFrankenTUIRatatuitui-rs (legacy)Raw crossterm
Inline mode w/ scrollback✅ First‑class⚠️ App‑specific⚠️ App‑specific❌ Manual
Deterministic buffer diff✅ Kernel‑level
One‑writer rule✅ Enforced⚠️ App‑specific⚠️ App‑specific
RAII teardown✅ TerminalSession⚠️ App‑specific⚠️ App‑specific
Pane workspaces (drag/resize/dock)✅ Built‑in
Web/WASM backend✅ Shared Rust core
Bayesian diff strategy✅ Adaptive❌ Fixed❌ Fixed❌ N/A
Shadow‑run validation harness✅ Built‑in
Snapshot/time‑travel harness✅ Built‑in
Widget count80+ direct impls~20~120
Demo screens45~5~50

When to use FrankenTUI:

  • You want inline + scrollback without flicker.
  • You care about deterministic rendering and teardown guarantees.
  • You need resizable pane workspaces with drag, dock, and undo.
  • You want a single Rust codebase targeting both terminal and web.
  • You prefer a kernel you can build your own UI framework on top of.

When FrankenTUI might not be ideal:

  • You need a stable public API today (FrankenTUI is evolving fast).
  • You want a fully opinionated application framework rather than a kernel.

Installation

Quick Install (Source Tarball)

curl -fsSL https://codeload.github.com/Dicklesworthstone/frankentui/tar.gz/main | tar -xz
cd frankentui-main
cargo build --release

Git Clone

git clone https://github.com/Dicklesworthstone/frankentui.git
cd frankentui
cargo build --release

Use as a Workspace Dependency

# Cargo.toml
[dependencies]
ftui = { path = "../frankentui/crates/ftui" }

Crates.io Dependency

The workspace provides 17 library crates (the ftui facade plus ftui-core, ftui-render, ftui-style, ftui-text, ftui-layout, ftui-runtime, ftui-widgets, ftui-extras, ftui-backend, ftui-tty, ftui-web, ftui-harness, ftui-pty, ftui-a11y, ftui-i18n, ftui-simd):

[dependencies]
ftui = "=0.9.0"

These dependency examples target 0.9.0. To work from a local checkout, use the path dependency above instead.

The default features are runtime, extras, and backend. App::run() selects native ftui-tty on Unix and Crossterm elsewhere; ftui::DEFAULT_BACKEND reports the selected backend. Headless, WASM, or custom-backend consumers can disable the defaults:

[dependencies]
ftui = { version = "=0.9.0", default-features = false, features = ["runtime"] }

Without a compiled terminal backend, App::run() returns an Unsupported error naming the feature to enable. The doctor, demo, and WASM showcase targets are repository-only applications.


Quick Start

  1. Install Rust nightly (required by rust-toolchain.toml).
  2. Clone the repo and build:
    git clone https://github.com/Dicklesworthstone/frankentui.git
    cd frankentui
    cargo build
    
  3. Run the demo showcase (primary way to see the system):
    cargo run -p ftui-demo-showcase
    

Telemetry (Optional)

Telemetry is opt‑in. Enable the telemetry feature on ftui-runtime and set OTEL env vars (for example, OTEL_EXPORTER_OTLP_ENDPOINT) to export spans.

When the feature is off, telemetry code and dependencies are excluded. When the feature is on but env vars are unset, overhead is a single startup check.

See docs/telemetry.md for integration patterns and trace‑parent attachment.


Feature Flags

CrateFeatureWhat It Enables
ftuibidiBidirectional text reordering via unicode-bidi (default)
ftui-coretracingStructured spans for terminal lifecycle
ftui-coretracing-jsonJSON output via tracing-subscriber
ftui-rendertracingPerformance spans for diff/presenter
ftui-runtimetracingRuntime loop instrumentation
ftui-runtimetelemetryOpenTelemetry export (OTLP)
ftui-widgetsbidiBiDi reordering and RTL cursor support for widgets

Enable features per-crate in your Cargo.toml as needed.


Evidence Logs (JSONL Diagnostics)

FrankenTUI can emit structured, deterministic evidence logs for diff strategy decisions, resize coalescing, and budget alerts. The log sink is shared and configured at the runtime level.

use ftui::runtime::EvidenceSinkConfig;

App::new(model)
    .with_evidence_sink(
        EvidenceSinkConfig::enabled_file("evidence.jsonl").with_flush_on_write(true),
    )
    .run()

Example event line:

{"event":"diff_decision","run_id":"diff-4242","event_idx":12,"strategy":"DirtyRows","cost_full":1.230000,"cost_dirty":0.410000,"cost_redraw":0.000000,"posterior_mean":0.036000,"posterior_variance":0.000340,"alpha":3.500000,"beta":92.500000,"dirty_rows":4,"total_rows":40,"total_cells":3200,"span_count":2,"span_coverage_pct":6.250000,"max_span_len":12,"fallback_reason":"none","scan_cost_estimate":200,"bayesian_enabled":true,"dirty_rows_enabled":true}

Commands

Run the Demo Showcase (Primary)

cargo run -p ftui-demo-showcase

Run Harness Examples (tests and reference behavior)

minimal is a 45-line inline hello world with a tick counter; q, Escape or Ctrl-C quits. streaming generates high-volume logs and can also run a real child command (see the agent harness tutorial).

cargo run -p ftui-harness --example minimal
cargo run -p ftui-harness --example streaming

Tests

cargo test
BLESS=1 cargo test -p ftui-harness  # update snapshot baselines

Deterministic E2E Runs

Use deterministic fixtures for stable hashes and reproducible logs:

# Full E2E suite with deterministic seeds/time
E2E_DETERMINISTIC=1 E2E_SEED=0 E2E_TIME_STEP_MS=100 ./scripts/e2e_test.sh

# Demo showcase E2E with an explicit seed
E2E_DETERMINISTIC=1 E2E_SEED=42 ./scripts/demo_showcase_e2e.sh

Format + Lint

cargo fmt
cargo clippy --all-targets -- -D warnings

E2E Scripts

./scripts/e2e_test.sh
./scripts/widget_api_e2e.sh
./scripts/pane_e2e.sh --mode smoke
./scripts/pane_e2e.sh --mode full
./tests/e2e/check_pane_traceability.sh

doctor_frankentui Verification

Run the full doctor_frankentui verification stack locally:

Prerequisites:

  • cargo
  • python3
  • jq
  • rg (ripgrep)
  • cargo-llvm-cov (cargo install cargo-llvm-cov)
  • Python TOML parser support:
    • Python 3.11+ (built-in tomllib), or
    • python3 -m pip install tomli for Python <3.11
# Unit + integration tests
cargo test -p doctor_frankentui --all-targets -- --nocapture

# Workflow-level E2E
./scripts/doctor_frankentui_happy_e2e.sh /tmp/doctor_frankentui_ci/happy
./scripts/doctor_frankentui_failure_e2e.sh /tmp/doctor_frankentui_ci/failure

# Coverage gate
./scripts/doctor_frankentui_coverage.sh /tmp/doctor_frankentui_ci/coverage

Artifact contract (CI and local):

  • .../happy/meta/summary.json: happy-path pass/fail and per-step timing.
  • .../happy/meta/artifact_manifest.json: checksums, sizes, and mtimes for expected outputs.
  • .../failure/meta/summary.json: failure-matrix pass/fail counts.
  • .../failure/meta/case_results.json: per-case expected vs actual exits and key artifacts.
  • .../coverage/coverage_gate_report.json: machine-readable threshold decision.
  • .../coverage/coverage_gate_report.txt: human-readable coverage gate details.

Troubleshooting map:

  • doctor/capture/suite/report chain failures: inspect .../happy/logs/*.stderr.log and .../happy/meta/command_manifest.txt.
  • failure-case assertion mismatches: inspect .../failure/meta/case_results.json and .../failure/cases/<case_id>/logs/.
  • JSON contract regressions: inspect json_* case stdout logs under .../failure/cases/.
  • coverage regressions: inspect .../coverage/coverage_gate_report.json for failing group + threshold delta.

Release Publishing (Maintainers)

Releases use DSR on native build hosts with the channel in rust-toolchain.toml. GitHub Actions stays disabled. Run the registry helper from a clean candidate checkout inside the native DSR job:

bash scripts/release_publish_crates.sh VERSION NEW_OUTPUT_DIR --dry-run
bash scripts/release_publish_crates.sh VERSION NEW_OUTPUT_DIR --publish

Each invocation requires a fresh output directory outside the checkout. The dry run verifies all 17 packages without a registry token once their sibling dependency versions are available in the registry; unpublished siblings can block a prepublication dry run. Publication follows dependency order, verifies registry checksums and Git provenance, and retains archives for safe retries. Repeating publication with a new output directory verifies and skips already-published versions. Keep the structured release_log.jsonl, Cargo logs, registry responses, and retained archives.


Configuration

The following variables configure the reference harness binary, cargo run -p ftui-harness. Export them in your shell or prefix the command with assignments; the binary does not load a .env file automatically.

export FTUI_HARNESS_SCREEN_MODE=inline   # inline | alt
export FTUI_HARNESS_UI_HEIGHT=12         # rows reserved for UI
export FTUI_HARNESS_VIEW=layout-grid     # harness fixture selector
export FTUI_HARNESS_ENABLE_MOUSE=true    # enable mouse events
export FTUI_HARNESS_ENABLE_FOCUS=true    # enable focus events
export FTUI_HARNESS_LOG_LINES=25         # generate this many startup log lines
export FTUI_HARNESS_LOG_MARKUP=true      # parse markup in loaded log lines
export FTUI_HARNESS_LOG_FILE=/path/to/log.txt  # read lines into the log viewer
export FTUI_HARNESS_EXIT_AFTER_MS=0      # 0 disables auto-exit

# Optional controls (uncomment only those you need):
# export FTUI_HARNESS_AUTO_UI_HEIGHT=true      # grow inline UI above its minimum height
# export FTUI_HARNESS_ENABLE_KITTY_KEYBOARD=true  # request enhanced keyboard reporting
# export FTUI_HARNESS_LOCALE=en-US            # runtime locale context
# export FTUI_HARNESS_SUPPRESS_WELCOME=true   # omit startup welcome lines
# export FTUI_HARNESS_LOG_KEYS=true          # log received key events
# export FTUI_HARNESS_DIFF_BAYESIAN=true      # Bayesian diff strategy selection
# export FTUI_HARNESS_BOCPD=true             # change-point resize coalescing
# export FTUI_HARNESS_CONFORMAL=true         # conformal frame-budget prediction
# export FTUI_HARNESS_FRAME_BUDGET_US=16667   # total frame budget, microseconds
# export FTUI_HARNESS_RENDER_BUDGET_US=8000   # render budget; may raise total budget
# export FTUI_HARNESS_EVIDENCE_JSONL=/path/to/harness.jsonl  # rendering evidence output

Harness view names include default, layout-flex-row, layout-flex-col, layout-grid, layout-nested, widget-block, widget-paragraph, widget-table, widget-list, widget-input, widget-inspector, widget-budget, tile-skip, span-diff, selector-storm, and locale-context. Unknown names select the default view. Harness examples have their own options: of these harness settings, minimal uses only auto-exit, rounded up to its 250 ms tick interval.

The showcase, cargo run -p ftui-demo-showcase, has separate controls. Command-line flags take precedence over environment defaults.

export FTUI_DEMO_SCREEN=2                # --screen=N; 1-indexed
export FTUI_DEMO_SCREEN_MODE=alt         # --screen-mode=alt|inline|inline-auto
export FTUI_DEMO_MOUSE=auto              # --mouse=on|off|auto
export FTUI_DEMO_EXIT_AFTER_MS=0         # 0 disables auto-exit
export FTUI_DEMO_DETERMINISTIC=1         # deterministic fixtures and time
export FTUI_DEMO_SEED=0                  # fixture seed
export FTUI_DEMO_EVIDENCE_JSONL=/path/to/evidence.jsonl  # write rendering evidence

# Optional controls:
# export FTUI_DEMO_UI_HEIGHT=20         # --ui-height; inline rows
# export FTUI_DEMO_UI_MIN_HEIGHT=12     # --ui-min-height; inline-auto minimum
# export FTUI_DEMO_UI_MAX_HEIGHT=40     # --ui-max-height; inline-auto maximum
# export FTUI_DEMO_TOUR=true            # --tour; guided tour on launch
# export FTUI_DEMO_TOUR_SPEED=1.0       # --tour-speed; speed multiplier
# export FTUI_DEMO_TOUR_START_STEP=1    # --tour-start-step; 1-indexed
# export FTUI_DEMO_PANE_WORKSPACE=/path/to/panes.json  # --pane-workspace; persisted layout

For the full showcase CLI and environment list, including tour and VFX controls, run cargo run -p ftui-demo-showcase -- --help.

Fixture and diagnostic controls are listed with their purposes in docs/env-internal-allowlist.txt. python3 -B scripts/check_env_docs.py checks binary attribution and the environment-variable inventory in the harness main and showcase CLI entry points.

Terminal capability detection uses standard environment variables (TERM, COLORTERM, NO_COLOR, TMUX, ZELLIJ, KITTY_WINDOW_ID).


Architecture

┌──────────────────────────────────────────────────────────────────────────────┐
│                                 INPUT LAYER                                  │
├──────────────────────────────────────────────────────────────────────────────┤
│ TerminalSession (crossterm)                                                  │
│   └─ raw terminal events  →  Event (ftui-core)                               │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│                                RUNTIME LOOP                                  │
├──────────────────────────────────────────────────────────────────────────────┤
│ Program / Model (ftui-runtime)                                               │
│   update(Event) → (Model', Cmd)                                              │
│   Cmd → Effects                                                              │
│   Subscriptions → Event stream (tick / io / resize / ...)                    │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│                               RENDER KERNEL                                  │
├──────────────────────────────────────────────────────────────────────────────┤
│ view(Model) → Frame → Buffer → BufferDiff → Presenter → ANSI                 │
│                 (cell grid)    (minimal)       (encode bytes)                │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│                                OUTPUT LAYER                                  │
├──────────────────────────────────────────────────────────────────────────────┤
│ TerminalWriter                                                               │
│   inline mode (scrollback-friendly)  |  alt-screen mode (classic)            │
└──────────────────────────────────────────────────────────────────────────────┘

Frame Pipeline (Step-by-Step)

  1. InputTerminalSession reads Event.
  2. Modelupdate() returns Cmd for side effects.
  3. Viewview() renders into Frame.
  4. BufferFrame writes cells into a 2D Buffer.
  5. DiffBufferDiff computes minimal changes.
  6. Presenter → emits ANSI with state tracking.
  7. Writer → enforces one‑writer rule, flushes output.

This is the core loop that ensures determinism and flicker‑free output.


Pane Workspace System

FrankenTUI includes a full pane workspace system (9,000+ lines in ftui-layout/src/pane.rs) that goes far beyond simple split layouts:

  • Drag‑to‑resize via splitter handles with cell‑level hit‑testing
  • Drag‑to‑move with magnetic docking fields and live ghost preview targets
  • Inertial throw: release mid-drag and panes coast with momentum via PaneInertialThrow
  • Pressure-sensitive snap: snap strength derived from drag speed and direction changes via PanePressureSnapProfile
  • Multi‑pane selection via Shift+Click with PaneSelectionState
  • Intelligence modes: Focus, Compare, Monitor, Compact layout presets via PaneLayoutIntelligenceMode
  • Persistent interaction timeline with full undo/redo/replay via PaneInteractionTimeline
  • Right‑click mode cycling through all four intelligence modes
  • Scroll-wheel magnetic field tuning: adjust snap strength without leaving the pane
  • Terminal + Web parity: same pane interactions work in both backends via PaneTerminalAdapter and PanePointerCaptureAdapter

The pane system is integrated into three of the 45 demo screens (Dashboard, Widget Gallery, Layout Lab) and has dedicated E2E tests (scripts/pane_e2e.sh).

Pane Architecture

PaneTree                         ← Spatial layout tree (HSplit / VSplit / Leaf)
  └── PaneOperation              ← Atomic layout mutation (resize, swap, split, close)
       └── PaneInteractionTimeline  ← Undo/redo/replay history of operations
            └── PaneDragResizeMachine   ← State machine for pointer gesture lifecycle
                 └── PaneSemanticInputEvent  ← High‑level input abstraction

Runtime Migration & Rollout Infrastructure

FrankenTUI is migrating its execution substrate through three lanes:

LaneDescriptionStatus
LegacyThread‑based subscriptions with manual stop coordinationAvailable
StructuredCancellationToken‑backed subscriptions (current default)Active
AsupersyncAsupersync blocking task pool with CancellationToken-backed subscriptionsAvailable with ftui-runtime/asupersync-executor

Selecting asupersync without that feature emits a warning and resolves to Structured. An explicit task backend in EffectQueueConfig takes precedence over the lane default. Subscriptions retain their structured cancellation; the lane selects how Cmd::Task closures execute.

Rollout Policy

Runtime lane transitions are managed through a shadow‑run comparison system to prevent regressions:

// Operator workflow: Off → Shadow → Evaluate → Enable → Monitor → Rollback
let config = ProgramConfig::default()
    .with_lane(RuntimeLane::Structured) // Current execution backend
    .with_rollout_policy(RolloutPolicy::Shadow) // Shadow‑compare before enabling
    .with_env_overrides(); // FTUI_RUNTIME_LANE, FTUI_ROLLOUT_POLICY
Environment VariableValuesDefaultPurpose
FTUI_RUNTIME_LANElegacy, structured, asupersyncstructuredSelect execution backend
FTUI_ROLLOUT_POLICYoff, shadow, enabledoffControl rollout behavior

Shadow‑Run Validation

Prove rendering determinism across runtime migrations by running the same model through two independent execution paths and comparing frame checksums:

use ftui_harness::{ShadowRun, ShadowRunConfig, ShadowVerdict};

let config = ShadowRunConfig::new("migration_test", "tick_counter", 42).viewport(80, 24);
let result = ShadowRun::compare(config, MyModel::new, |session| {
    session.init();
    session.tick();
    session.capture_frame();
});
assert_eq!(result.verdict, ShadowVerdict::Match);

Rollout Scorecard & Evidence Bundle

Combine shadow evidence + benchmark results into a single go/no‑go release decision:

use ftui_harness::{
    RolloutEvidenceBundle, RolloutScorecard, RolloutScorecardConfig, RolloutVerdict,
};

let mut scorecard =
    RolloutScorecard::new(RolloutScorecardConfig::default().min_shadow_scenarios(3));
for shadow_result in shadow_results {
    scorecard.add_shadow_result(shadow_result);
}
assert_eq!(scorecard.evaluate(), RolloutVerdict::Go);

// Machine-readable JSON evidence for CI gates
let bundle = RolloutEvidenceBundle {
    scorecard: scorecard.summary(),
    queue_telemetry: Some(ftui_runtime::effect_system::queue_telemetry()),
    requested_lane: "structured".to_string(),
    resolved_lane: "structured".to_string(),
    rollout_policy: "shadow".to_string(),
};
println!("{}", bundle.to_json()); // Self-contained release decision artifact

Effect Queue Telemetry & Backpressure

The effect executor tracks queue health with monotonic counters and enforces backpressure:

// Configure backpressure bounds
let config = ProgramConfig::default().with_effect_queue(
    EffectQueueConfig::default()
        .with_enabled(true)
        .with_max_queue_depth(64), // Drop tasks beyond this depth
);

// Monitor queue health at runtime
let snap = ftui::runtime::effect_system::queue_telemetry();
// snap.enqueued, snap.processed, snap.dropped, snap.high_water, snap.in_flight

Unified Telemetry Schema

All runtime telemetry uses canonical targets and event names defined in ftui_runtime::telemetry_schema:

TargetPurpose
ftui.runtimeStartup, shutdown, lane resolution
ftui.effectCommand/subscription execution, queue drops
ftui.processProcess subscription lifecycle
ftui.decision.resizeResize coalescer decisions
ftui.voiValue‑of‑information sampling
ftui.bocpdChange‑point detection
ftui.eprocessE‑process throttle decisions
ftui.guardrailsFrame guardrails: soft memory trims and emergency frame drops
ftui.a11yAccessibility tree diffs and screen‑reader announcements (only with ProgramConfig::with_accessibility)

Web/WASM Backend

FrankenTUI targets both native terminals and web browsers from a single Rust codebase:

CratePurpose
ftui-webWeb adapter with pointer/touch parity, DPR/zoom handling
ftui-showcase-wasmWASM build target for the demo showcase

The web adapter translates browser pointer events into the same PaneSemanticInputEvent stream used by the terminal backend, ensuring interaction parity across platforms.

This checkout does not currently vendor local crates/frankenterm-core or crates/frankenterm-web workspace members. Those names still appear in specs and adjacent integration docs, but the in-tree browser surface today is ftui-web plus the ftui-showcase-wasm target. ftui-pty integrates with an external frankenterm-core dependency rather than a local workspace crate.


FrankenTerm Integration

FrankenTUI shares contracts and backend primitives with the broader FrankenTerm effort, but this repository currently contains the ftui-side integration pieces rather than a vendored local browser-terminal crate.

What is in-tree right now:

  • ftui-web backend primitives, patch formats, and deterministic host-driven runtime support
  • ftui-showcase-wasm as the WASM demo/showcase target
  • ftui-pty integration against the external frankenterm-core crate

What remains adjacent/out-of-tree from this checkout:

  • a browser-facing FrankenTermWeb bundle/package
  • the local crates/frankenterm-core / crates/frankenterm-web layout still referenced by some specs

The adjacent terminal-emulator design target still includes:

  • CSI/OSC/DCS sequences for cursor control, colors, and window operations
  • Alternate screen buffer switching (like vim/less)
  • Mouse reporting protocols (X10, SGR, URXVT)
  • Scrollback with configurable history depth
  • Selection/copy with Unicode-aware grapheme boundaries

"Alien Artifact" Quality Algorithms

FrankenTUI employs mathematically rigorous algorithms that go far beyond typical TUI implementations. We call this "alien artifact" quality engineering.

Bayesian Fuzzy Scoring (Command Palette)

The command palette uses a Bayesian evidence ledger for match scoring, not simple string distance:

Score = P(relevant | evidence) computed via posterior odds:

P(relevant | evidence) / P(not_relevant | evidence)
    = [P(relevant) / P(not_relevant)] × Π_i BF_i

where BF_i = Bayes Factor for evidence type i
          = P(evidence_i | relevant) / P(evidence_i | not_relevant)

Prior odds by match type:

Match TypePrior OddsP(relevant)Intuition
Exact99:199%Almost always what user wants
Prefix9:190%Very likely relevant
Word-start4:180%Probably relevant
Substring2:167%Possibly relevant
Fuzzy1:325%Needs additional evidence

Evidence factors that update posterior:

  • Word boundary bonus (BF ≈ 2.0): Match at start of word
  • Position bonus (BF ∝ 1/position): Earlier matches stronger
  • Gap penalty (BF < 1.0): Gaps between matched chars reduce confidence
  • Tag match bonus (BF ≈ 3.0): Query matches command tags
  • Length factor (BF ∝ 1/length): Shorter, more specific titles preferred

Result: Every search result includes an explainable evidence ledger showing exactly why it ranked where it did.

Bayesian Hint Ranking (Keybinding Hints)

Keybinding hints are ranked by expected utility minus display cost, with a VOI exploration bonus and hysteresis for stability:

Utility posterior:
    U_i ~ Beta(α_i, β_i)
    E[U_i] = α_i / (α_i + β_i)
    VOI_i = sqrt(Var(U_i))

Net value:
    V_i = E[U_i] + w_voi × VOI_i - λ × C_i

Hysteresis:
    swap only if V_i - V_j > ε

Result: the UI surfaces the most valuable shortcuts without flicker, while still exploring uncertain hints.

Bayesian Diff Strategy Selection

The renderer adaptively chooses between diff strategies using a Beta posterior over change rates:

Change-rate model:
    p ~ Beta(α, β)

Prior: α₀ = 1, β₀ = 19  →  E[p] = 5% (expect sparse changes)

Per-frame update:
    α ← α × decay + N_changed
    β ← β × decay + (N_scanned - N_changed)

where decay = 0.95 (exponential forgetting for non-stationary workloads)

Strategy cost model:

Cost = c_scan × cells_scanned + c_emit × cells_emitted

Full Diff:     Cost = c_row × H + c_scan × D × W + c_emit × p × N
Dirty-Row:     Cost = c_scan × D × W + c_emit × p × N
Full Redraw:   Cost = c_emit × N

Decision: argmin { E[Cost_full], E[Cost_dirty], E[Cost_redraw] }

Conservative mode: Uses 95th percentile of p (not mean) when posterior variance is high, because the system knows when it's uncertain.

Bayesian Capability Detection (Terminal Caps Probe)

Runtime capability upgrades are decided by a log-Bayes-factor ledger (ftui_core::caps_probe::ledger_for): every signal contributes a fixed log-odds weight and an upgrade is applied when the posterior clears 0.8 (DECISION_THRESHOLD):

log-odds = Σ log BF_i
    environment/identity   +3.0 present / −0.4 absent
    XTGETTCAP RGB reply     ±4.6      (truecolor)
    DECRPM ?2026 reply      ±4.6      (synchronized output)
    probe timeout           −0.7
    multiplexer detected    −0.5
    FTUI_* operator switch  ±9.2      (saturates: no heuristic outvotes it)

P = 1 / (1 + exp(−log-odds))   →   upgrade when P ≥ 0.8

In code: ftui_core::caps_probe::CapabilityLedger with weights, threshold P0.8P \ge 0.8, upgrade-only refinement (refine_from_ledgers), evidence event capability_decision per capability at startup, feature caps-probe (enabled by the showcase; consumers enable it on ftui-core), and the probe preconditions (currently: native backend, stdin is a TTY, color depth Ansi256 for the truecolor probe; G05 extends to DECRPM 2026 on all non-mux terminals).

ConstantValueDescription
ENV_POSITIVE+3.0Environment variable explicitly indicates support (e.g. COLORTERM=truecolor)
ENV_ABSENT-0.4Environment variable absent but not definitive
DA2_KNOWN_TERMINAL+1.8DA2 terminal type matches known-good terminal
DA1_CONFIRMED+3.5DA1 attribute code confirms feature (e.g. Sixel)
DECRPM_CONFIRMED+4.6DECRPM confirms mode is recognized (status 1–4)
DECRPM_DENIED-4.6DECRPM denies mode (status 0)
TIMEOUT-0.7Probe timed out — weak negative evidence
MUX_PENALTY-0.5Multiplexer detected — slight negative for passthrough features

The ledger is upgrade-only: a capability the environment or the allowlist already granted is never removed by a negative or absent reply (terminfo under-reports truecolor over ssh; the allowlist knows terminals that ignore DECRPM). The startup probe sends only the two bounded queries above (300 ms, fail-open, FTUI_CAPS_PROBE=0 disables it); DA1/DA2 weights exist for the asynchronous CapabilityProber used by the showcase's capability screen. Every session writes one capability_decision evidence row per capability (truecolor, synchronized output, scroll region) carrying exactly the rows that decided.

Dirty-Span Interval Union (Sparse Diff Scans)

For sparse updates, each row tracks dirty spans and the diff scans only the union of those spans:

Row y spans:
    S_y = union_k [x0_k, x1_k)

Scan cost:
    sum_y |S_y|

Result: scan work scales with the actual changed area, not full row width.

Summed-Area Table (Tile-Skip Diff)

To skip empty tiles on large screens, a summed-area table (2D prefix sum) allows O(1) tile density checks:

SAT(x,y) = A(x,y)
         + SAT(x-1,y) + SAT(x,y-1) - SAT(x-1,y-1)

The SAT is queried per tile row (one subtraction) to skip whole rows of clean tiles before per-tile checks; tiles engage at 12,000\ge 12,000 cells (200×60 by default); measured: diff/sparse_5pct_rows/200x60 p50 28 µs → 21 µs (~25% speedup retiring 5 of 8 tile rows, docs/perf/sat_tile_skip_2026-09.md).

Fenwick Tree (Prefix Sums for Virtualized Lists)

Variable-height virtualized lists use a Fenwick tree (Binary Indexed Tree) for fast prefix sums:

sum(i) = sum_{k=1..i} a_k
update(i, Δ): for (j=i; j<=n; j+=j&-j) tree[j]+=Δ
query(i):     for (j=i; j>0; j-=j&-j)  sum+=tree[j]

Result: O(log n) height lookup and scroll positioning without scanning all rows.

Bayesian Height Prediction + Conformal Bounds (Virtualized Lists)

Virtualized lists predict unseen row heights to avoid scroll jumps, using a Normal-Normal conjugate update plus conformal bounds:

Prior:     μ ~ N(μ₀, σ₀²/κ₀)
Posterior: μ_n = (κ₀·μ₀ + n·x̄) / (κ₀ + n)

Conformal interval:
    [μ_n - q_{1-α}, μ_n + q_{1-α}]

Variance is tracked online with Welford’s algorithm, and q is the empirical quantile of |residuals|.

Where it runs: Virtualized::with_variable_heights(default) selects the Fenwick tracker as the variable-height strategy (with_variable_heights_fenwick takes an explicit capacity; ItemHeight::Variable(HeightCache) is the legacy linear-scan mode), and the tracker follows push, trim_front, clear and set_external_len. with_height_prediction(PredictorConfig) attaches the model to that tracker: unmeasured rows hold the posterior mean instead of a constant default, each observe_height(idx, h) trains it, and when the rounded prediction moves the unmeasured rows are refilled so total height and offsets track what has been measured. The VirtualizedList widget renders variable heights with .variable_heights(): each row is as tall as RenderItem::height says, rows are measured into the state's Fenwick tracker as they render (VirtualizedListState::measure for off‑screen changes), and the selection adjustment, bottom clamp and scrollbar work in rows with O(log n) offset queries. Scrolling is item‑aligned. The widget's state tracker has no predictor yet; predicted layouts use the Virtualized primitive.

Nothing in this repository uses variable heights. Virtualized defaults to ItemHeight::Fixed(1), and its only production consumer is LogViewer, which takes that default (log_viewer.rs:221). with_variable_heights, with_variable_heights_fenwick and .variable_heights() are called from tests and benches only, so everything above describes an API that works when you call it, not a path the demo or the log viewer exercises. That is worth knowing before relying on it: two reachability bugs lived here undetected (79f87122, 794b0482), because the tests set a viewport, read a range and assert, and never performed the sequence that breaks it — render at one offset, then scroll to the bottom. Its own tests are all the assurance this mode has (bd-wleec).

BOCPD: Online Change-Point Detection

Resize coalescing uses Bayesian Online Change-Point Detection to detect regime transitions:

Observation model (inter-arrival times):
    Steady: x_t ~ Exponential(λ_steady)  where μ_steady ≈ 200ms
    Burst:  x_t ~ Exponential(λ_burst)   where μ_burst ≈ 20ms

Run-length posterior (recursive update):
    P(r_t = 0 | x_1:t) ∝ Σᵣ P(r_{t-1} = r) × H(r) × P(x_t | r)
    P(r_t = r+1 | x_1:t) ∝ P(r_{t-1} = r) × (1 - H(r)) × P(x_t | r)

Hazard function (geometric prior):
    H(r) = 1/λ_hazard  where λ_hazard = 50

Complexity: O(K) per update with K=100 run-length truncation

Regime posterior:

P(burst | observations) = Σᵣ P(burst | r, x_1:t) × P(r | x_1:t)

Decision thresholds:
    p_burst > 0.7  →  Burst regime (aggressive coalescing)
    p_burst < 0.3  →  Steady regime (responsive)
    otherwise      →  Transitional (interpolate delay)

The delays those regimes select, and the two numbers most easily confused with them:

ValueWhere
Steady coalescing delay16 msCoalescerConfig::steady_delay_ms
Burst coalescing delay40 msCoalescerConfig::burst_delay_ms
Hard deadline100 msCoalescerConfig::hard_deadline_ms
Steady observation mean μ_steady200 msBocpdConfig::mu_steady_ms
Burst observation mean μ_burst20 msBocpdConfig::mu_burst_ms

The first three are how long a resize is held; the last two are how far apart resizes are expected to arrive in each regime, and are what the exponential observation model above is written in terms of. An earlier version of this section reported 200 ms and 20 ms as the coalescing delays, which is the one confusion this table exists to prevent. The hard deadline outranks both: a pending resize is applied at 100 ms whatever the posterior thinks, so a drag cannot be coalesced indefinitely.

default_config_matches_readme_constants (bocpd.rs) and steady_regime_delay_is_16ms_and_burst_is_40ms (resize_coalescer.rs) fail if any number above stops matching the code.

Does it beat the rate heuristic it replaced? Measured 2026-09-19: no. Both detectors were driven over identical resize-storm schedules on a virtual clock (docs/perf/resize_differential_2026-09.md):

Patternrepaints during drag (heuristic → BOCPD)final apply ms
burst_506 → 733 → 49
burst_20026 → 2724 → 0
sweep_80x24_to_200x6010 → 1240 → 24
oscillate_104 → 752 → 52
mixed_10025 → 3345 → 45
pathological_5020 → 2251 → 19

BOCPD draws more repaints during the drag on every pattern, and both detectors land outside the 40 ms settling budget on three of six, so it is worse on one criterion and no better on the other. The likely cause is that recommended_delay interpolates between 16 ms and 40 ms while p_burst is in the transitional band, and 5–50 ms inter-arrivals keep it there rather than committing to the burst delay.

The default is still enable_bocpd = true while that is decided (bd-h8l3d). Use CoalescerConfig::default().without_bocpd() for the rate heuristic alone; on these patterns it coalesces more aggressively.

Where it runs: on by default. CoalescerConfig::default() carries enable_bocpd = true and heuristic_fallback = true; ResizeCoalescer feeds every resize event's inter‑arrival to the posterior and reads the regime and the recommended delay from it. The 10/5 events‑per‑second rate heuristic decides only when the posterior is undefined: the first event of a session, an inter‑arrival outside [min_observation_ms, max_observation_ms] (1 ms to 10 s by default, which the posterior would otherwise see clamped), or a non‑finite posterior. While the heuristic decides it also owns the Burst exit (cooldown on ticks, rate check on the immediate path). The posterior itself only moves on events, so in BOCPD mode a Burst regime returns to Steady on a frame tick once the silence reaches mu_steady_ms (200 ms; reason code bocpd_idle_exit, confidence 1 − exp(−idle/μ_burst)). Every decision and regime_transition evidence row carries detector (bocpd or heuristic) and p_burst, the config row carries both flags, budget_decision carries resize_detector, and CoalescerStats::detector_decisions counts decisions per detector. CoalescerConfig::without_bocpd() returns to the heuristic alone; with_heuristic_fallback(false) hands even the first event to the posterior.

Bayes-Factor Evidence Ledger (Resize Coalescer)

Resize coalescing decisions are explained with a log10 Bayes factor ledger:

LBF = log10(P(evidence | apply_now) / P(evidence | coalesce))

Interpretation:
    LBF > 0  → apply now
    LBF < 0  → coalesce
    |LBF| > 1 strong, |LBF| > 2 decisive

Result: coalescing is transparent and audit‑friendly, not heuristic black magic.

Value-of-Information (VOI) Sampling

Inline-auto height remeasurement uses VOI analysis to decide when to take another height measurement:

``$ \text{Beta} \text{posterior} \text{over} \text{violation} \text{probability}: \text{p} ~ \text{Beta}(α, β)

\text{VOI} \text{computation}: \text{n} = α + β \text{p_hat} = α / \text{n} \text{variance_before} = αβ / (\text{n}² \times (\text{n}+1)) \text{variance_if_violation} = (α+1)β / ((\text{n}+1)² \times (\text{n}+2)) \text{variance_if_no_violation} = α(β+1) / ((\text{n}+1)² \times (\text{n}+2)) \text{expected_after} = \text{p_hat} \times \text{variance_if_violation} + (1-\text{p_hat}) \times \text{variance_if_no_violation} \text{VOI} = \text{variance_before} - \text{expected_after} = αβ / (\text{n}² \times (\text{n}+1)²)

\text{Decision}: \text{score} = \text{VOI} \times \text{value_scale} \times (1 + \text{boundary_weight} \times \text{boundary_score}) \text{forced} = \text{an} \text{enabled} \text{maximum} \text{time}/\text{event} \text{interval} \text{has} \text{been} \text{reached} \text{blocked} = \text{after} \text{the} \text{first} \text{sample}, \text{an} \text{enabled} \text{minimum} \text{interval} \text{is} \text{unmet} \text{sample} \text{iff} \text{forced} \text{OR} (\text{NOT} \text{blocked} \text{AND} \text{score} ≥ \text{sample_cost}) $``

boundary_score measures proximity to the sampler's e-value threshold. Initial height measurement runs whenever the runtime has no height estimate, independently of this sampling decision.

Inline-auto defaults:

  • prior_alpha=1.0, prior_beta=9.0 (expect 10% violation rate)
  • max_interval_ms=1000 (force sampling at the next decision after one second)
  • min_interval_ms=100 (prevent over-sampling)
  • sample_cost=0.08, value_scale=1.0, boundary_weight=1.0

These defaults rely on forced intervals: since n ≥ 10 and boundary_score ≤ 1, the VOI score is at most 1/242 ≈ 0.00413, below sample_cost. The cost comparison therefore requests no extra samples with this configuration. Other VoiConfig settings can enable sampling between forced intervals.

E-Process: Anytime-Valid Testing

Several statistical thresholds use e-processes (wealth-based sequential tests):

E-process (exponential / sub-Gaussian form, as implemented):
    E_t = E_{t-1} × exp(λ·r_t − λ²/2)      where r_t is a standardized residual

Key guarantee:
    P(∃t: E_t ≥ 1/α) ≤ α   under null hypothesis

This holds at ANY stopping time, with no peeking penalty.

λ is fixed on the render path. ftui_render::budget's EProcessState reads λ straight from its config, so the production e-process is a fixed-betting test, not an adaptive one. GRAPA — adapting λ by λ ← clamp(λ + η · z/(1 + λ·z)), with grapa_eta defaulting to 0.1 — is implemented only in the experimental conformal_alert. An earlier version of this block wrote the multiplicative-wealth form W_t = W_{t-1}(1 + λ_t(X_t − μ₀)) and attributed λ_t to GRAPA, which described neither implementation.

Applications in FrankenTUI:

  • Budget degradation decisions (ftui_render::budget's EProcessState, on the render path)
  • Flake detection in tests (experimental)
  • Allocation budget alerts (experimental)
  • Conformal alert threshold calibration (experimental conformal_alert)

Not the Mondrian conformal frame-time gate described below, which is the conformal path that is on by default: it calibrates from residual quantiles and carries no e-process.

Conformal Alerting

Status: experimental (see Experimental modules)

Where it runs: nowhere in production. This describes ftui-runtime's conformal_alert, whose only consumers are other experimental modules (timeline_aggregator, alpha_investing, resize_sla) and a proptest — a quarantined cluster wired to itself, not to the runtime. The conformal path that is on by default is the Mondrian frame-time gate above, which has no e-process layer.

Budget and performance alerts in that module use distribution-free conformal prediction:

Nonconformity score:
    R_t = |observed_t - predicted_t|

Threshold (finite-sample guarantee):
    q = quantile_{(1-α)(n+1)/n}(R_1, ..., R_n)

Coverage guarantee:
    P(R_{n+1} ≤ q) ≥ 1 - α   for any distribution!

E-process layer (anytime-valid):
    e_t = exp(λ × (z_t - μ₀) - λ²σ²/2)

Why conformal? No distributional assumptions required. Works for any data pattern.

Mondrian Conformal Frame-Time Risk Gating

Frame-time risk gating uses bucketed (Mondrian) conformal prediction keyed by screen mode, diff strategy, and size:

Residuals: r_t = y_t - ŷ_t
Upper bound: ŷ_t^+ = ŷ_t + q_{1-α}(|r|)

Risk if: ŷ_t^+ > budget

Buckets fall back from (mode, diff, size) → (mode, diff) → (mode) → global default, preserving coverage even when data is sparse.

Where it runs: on by default (ProgramConfig::default() carries ConformalConfig::default(): α = 0.05, min_samples = 20, window 256, q_default = 10 ms). Every frame the runtime predicts ŷ_t^+ for its bucket before rendering and, when the bound exceeds the frame budget, steps the render degradation level down; the frame's measured time is fed back as the next residual. Warm-up rule: a verdict is only acted on once the level it rests on has at least min_samples residuals, so the q_default prior can never degrade a fresh program (it is still written to the budget_decision evidence row as risk with n_b < 20). ProgramConfig::without_conformal() turns the gate off; with_conformal_config(..) tunes it.

CUSUM Control Charts

Allocation budget tracking uses CUSUM (Cumulative Sum) for fast change detection:

One-sided CUSUM:
    S_t = max(0, S_{t-1} + (X_t - μ₀) - k)

Alert when:
    S_t > h (threshold)

Parameters:
    k = allowance (typically σ/2)
    h = threshold (controls sensitivity vs false alarms)

Dual detection:
    Alert iff (CUSUM detects AND e-process confirms)
           OR (e-process alone exceeds 1/α)

Why dual? CUSUM is fast but can false-alarm; e-process is slower but anytime-valid. Intersection gives speed with guarantees.

CUSUM Hover Stabilizer (Mouse Jitter)

Hover target flicker is suppressed with a CUSUM change‑point detector on boundary‑crossing distance:

S_t = max(0, S_{t-1} + d_t - k)
switch if S_t > h

where d_t is signed distance to the current target boundary, k is drift allowance, and h is the switch threshold.

Result: single‑cell jitter doesn’t cause hover flicker, but intentional crossings still switch within a couple frames.

Where it runs: ListState::default().with_hover_stabilizer(HoverStabilizerConfig::default()) routes the list's mouse-move hover through the stabilizer (default: 1-cell hysteresis band, drift allowance 0.5, threshold 2.0, 500 ms hold); without it hovered follows the raw hit test. The showcase's mouse playground uses the same type directly. Table has no hover state, so nothing to stabilize there yet.

Gesture Recognition State Machine

The GestureRecognizer in ftui-core (2,100+ lines) transforms raw terminal events into semantic events via a multi-phase state machine:

Raw Events                    Semantic Events
─────────                    ───────────────
MouseDown(x,y)  ─┬─ idle ──→ Click
MouseDown(x,y)   │          DoubleClick
MouseDown(x,y)  ─┤          TripleClick (select word / line)
MouseMove(x,y)  ─┤─ armed ─→ DragStart
MouseMove(x,y)  ─┤          DragMove
MouseUp(x,y)    ─┤─ drag ──→ DragEnd

Key(a)          ─┤          KeyChord (multi-key sequences)
Key(Ctrl+x)     ─┘          ModifiedKey

Dead zone: drag is only recognized after the pointer moves beyond a configurable threshold (GestureConfig::drag_threshold, default: 3 cells of Manhattan distance), preventing accidental drags from jittery mice.

Multi-click timing: double/triple clicks use a configurable interval window (multi_click_timeout, default: 300 ms; click_tolerance 1 cell) with a click_count counter that resets on timeout or position change. A stationary press becomes a LongPress after long_press_threshold (default: 500 ms).

Chord recognition: multi-key sequences like g g (vim-style) use a KeySequence buffer with configurable timeout, enabling complex keybinding schemes without blocking single-key shortcuts.

Where it runs: opt in with ProgramConfig::default().with_gestures(GestureConfig::default()) and implement Model::on_gesture(&mut self, SemanticEvent) -> Cmd<Msg>. The runtime feeds every input event through the recognizer after the ordinary update() call (raw events are never swallowed), delivers each recognized gesture to on_gesture, and polls for long presses once per tick. Mouse gestures need mouse capture enabled.

Input Parser (3,200+ Lines)

The InputParser in ftui-core handles the full complexity of terminal input encoding:

  • ANSI escape sequences: CSI, SS3, DCS, OSC, APC parsing with timeout-based disambiguation
  • Kitty keyboard protocol: repeat/release events, modifier encoding, functional key disambiguation
  • Bracketed paste: captures pasted text as a single Paste event, preventing paste injection attacks
  • Mouse protocols: X10, SGR, URXVT, SGR-Pixels with automatic protocol detection
  • UTF-8 streaming: multi-byte character assembly across partial reads
  • Ambiguous prefix handling: ESC alone vs ESC [ (Alt+key vs CSI) resolved by timing

Keybinding System (3,700+ Lines)

The Keybinding module supports:

  • Declarative binding maps with priority levels (global, mode, widget)
  • Chord sequences (g g, Ctrl+x Ctrl+s) with configurable timeout
  • Context-sensitive activation: bindings active only in specific modes/focus states
  • Conflict detection: warns when bindings shadow each other
  • Serialization: load/save binding maps for user customization (ftui-core feature serde, ftui feature serde): KeyMap<A> serializes as a file with chords as text, contexts by name and chord_timeout_ms, so toml::from_str::<KeyMap<Msg>>(&text) reads a hand-edited keymap and a bad chord is reported with its binding index

Where it runs: ftui_core::keybinding::{KeyCombo, Chord, KeyMap, KeyDispatcher} (also in ftui::prelude). KeyMap::bind(Chord::parse("g g")?, Msg::GoTop) and bind_in(chord, action, Priority::Widget, Some(ctx)) declare bindings; KeyMap::conflicts() returns the shadow/prefix/duplicate report with one warning line per item; KeyDispatcher::feed(&key_event, now) and tick(now) return Dispatch::{Action, Pending, Expired, Unbound, Esc}. Resolution order: an active-context binding beats a context-free one, then higher priority, then the most recent binding. A key that is both bound alone and a chord prefix (g with g g) waits for the chord timeout (1 s default, 200 ms to 5 s) or a non-extending key, then fires; single-key shortcuts are never swallowed. Esc and Esc Esc go through the same 250 ms SequenceDetector window the rest of the runtime uses. The pane keyboard controller (ftui_runtime::pane_keymap) is routed through a KeyMap: pane_keymap() builds it from one binding table, PaneKeyboardController::handle_key resolves keys against it (with_keymap swaps in a host map), and pane_keyboard_hints() is generated from the same table, so hints and behaviour cannot drift. The showcase's global keys still match keys directly.

Damped Spring Dynamics (Animation System)

Animation transitions use a damped harmonic oscillator for natural motion:

F = -k(x - x*) - c v
⇒ x'' + c x' + k(x - x*) = 0

Critical damping (fastest convergence without overshoot) is:

c_crit = 2√k

We integrate with semi‑implicit Euler and clamp large dt by subdividing into small steps for stability. The result is deterministic, smooth motion without frame‑rate sensitivity.

Easing Curves + Stagger Distributions

Base animations use analytic easing curves:

ease_in(t)  = t²
ease_out(t) = 1 - (1 - t)²
ease_in_out(t) =
    2t²                (t < 0.5)
    1 - (-2t + 2)²/2   (t ≥ 0.5)

Staggered lists distribute start offsets by applying easing to normalized indices:

offset_i = D · ease(i / (n - 1))

Optional deterministic jitter is added with a xorshift PRNG and clamped, so cascades feel organic but remain reproducible in tests.

Sine Pulse Sequences (Attention Cues)

Attention pulses are a single half‑cycle sine:

p(t) = sin(πt),  t ∈ [0, 1]

This produces a smooth 0→1→0 emphasis without sharp edges or flicker.

Perceived Luminance (Terminal Background Probe)

Background probing converts RGB to perceived luminance:

Y = 0.299R + 0.587G + 0.114B

That classification feeds capability detection for dark/light defaults.

Jain's Fairness Index (Input Guard)

Input fairness monitoring uses Jain's Fairness Index:

F(x₁, ..., xₙ) = (Σxᵢ)² / (n × Σxᵢ²)

Properties:
    F = 1.0  →  Perfect fairness (all equal)
    F = 1/n  →  Complete unfairness (one dominates)

Intervention:
    if input_latency > threshold OR F < 0.8:
        force_coalescer_yield()

Why Jain's? Scale-independent, bounded [1/n, 1], interpretable.


Troubleshooting

"terminal is corrupted after crash"

FrankenTUI uses RAII cleanup via TerminalSession. If you see a broken terminal, make sure you are not force‑killing the process.

# Reset terminal state
reset

“error: the option -Z is only accepted on the nightly compiler”

FrankenTUI requires nightly. Install and use nightly or let rust-toolchain.toml select it.

rustup toolchain install nightly

“raw mode not restored”

Ensure your app exits normally (or panics) and does not call process::exit() before TerminalSession drops.

“no mouse events”

Mouse must be enabled in the session and supported by your terminal.

FTUI_HARNESS_ENABLE_MOUSE=true cargo run -p ftui-harness
FTUI_DEMO_MOUSE=on cargo run -p ftui-demo-showcase

For tmux, enable mouse forwarding with set -g mouse on in your tmux configuration.

“output flickers”

Inline mode uses synchronized output where supported: allowlisted terminals get it immediately and any terminal that answers the DECRPM 2026 probe gets it at startup. Inside tmux/screen/zellij and under WezTerm identities it is off by policy. If you know your terminal handles CSI ? 2026 h/l correctly, set FTUI_SYNC_OUTPUT=1 (and FTUI_SCROLL_REGION=1 for the inline scroll-region strategy); set them to 0 to force the conservative path.


Limitations

What FrankenTUI Doesn’t Do (Yet)

  • Stable public API: APIs are evolving quickly.
  • Full widget ecosystem: Core widgets exist, but the ecosystem is still growing.
  • Guaranteed behavior on every terminal: Capability detection is conservative; older terminals may degrade.

Known Limitations

CapabilityCurrent StatePlanned
Stable API❌ Not yetYes (post‑v1)
Widget ecosystem✅ 80+ direct widget implementationsExpanding
Formal compatibility matrix⚠️ In progressYes
Asupersync execution lane✅ Blocking task pool with ftui-runtime/asupersync-executor; otherwise falls back to StructuredStructured subscription cancellation retained
crates.io publishing✅ 17 of 20 crates (all libraries)Showcase targets stay workspace-local

FAQ

Why “FrankenTUI”?

A modular kernel assembled from focused, composable parts. A deliberate, engineered “monster.”

Is this a full framework?

It’s a kernel plus a large widget library plus a demo showcase with 45 screens plus a full pane workspace system. You can build a framework on top, but expect APIs to evolve.

Does it work on Windows?

Windows support is tracked in docs/WINDOWS.md and the deferred native-backend strategy is documented in docs/spec/frankenterm-architecture.md (Section 13.5).

Can I embed it in an existing CLI tool?

Yes. Inline mode is designed for CLI + UI coexistence.

Can it run in a browser?

Yes. ftui-web provides a WASM adapter that renders through the same Rust core. ftui-showcase-wasm is the WASM build target for the demo showcase.

How do I update snapshot tests?

BLESS=1 cargo test -p ftui-demo-showcase

How many lines of code is it?

1,110,000+ lines of Rust across 20 crates, with 80+ direct widget implementations, 45 demo screens, and a broad PTY/scripted E2E surface.

What's the performance like?

The 16‑byte cell design puts 4 cells per cache line. Bayesian diff strategy selection avoids scanning unchanged regions. The presenter uses cost‑optimal cursor positioning. Frame‑time budgets are enforced via conformal prediction with automatic degradation (Full → SimpleBorders → NoStyling → EssentialOnly → Skeleton → SkipFrame).

How does the rollout system work?

The runtime supports three execution lanes (Legacy, Structured, Asupersync) with a shadow‑run comparison system that proves determinism before enabling a new lane. The RolloutScorecard combines shadow evidence with benchmark results into a machine‑readable go/no‑go verdict. See the "Runtime Migration & Rollout Infrastructure" section above.


Key Docs

  • docs/ACCESSIBILITY.md — accessibility tree / proxy notes (relocated from repo root)
  • docs/planning/UPGRADE_LOG.md — upgrade history (relocated from repo root)
  • docs/operational-playbook.md
  • docs/risk-register.md
  • docs/glossary.md
  • docs/adr/README.md
  • docs/concepts/screen-modes.md
  • docs/spec/state-machines.md
  • docs/spec/frankenterm-correctness.md
  • docs/telemetry.md
  • docs/spec/telemetry.md
  • docs/spec/telemetry-events.md
  • docs/testing/coverage-matrix.md
  • docs/testing/coverage-playbook.md
  • docs/one-writer-rule.md
  • docs/ansi-reference.md
  • docs/WINDOWS.md
  • docs/testing/e2e-playbook.md

Pane workspace:

  • docs/guides/pane-101.md — getting started with panes
  • docs/guides/pane-showcase-scenarios.md — demo scenarios + regression-test map
  • docs/cookbook/panes.md — task-oriented pane recipes
  • docs/api/pane-stability-contract.md — supported surface, schema versions, deprecation policy
  • docs/migration/flex-to-pane-and-versioning.md — Flex/Grid → panes + persisted-workspace versioning
  • docs/spec/pane-parity-contract-and-program.md — terminal/web parity guarantee
  • docs/pane-release-gate-policy.md — go/no-go release gate clauses + staged rollout
  • docs/pane-operational-runbook.md — incident response + emergency rollback

E-Graph Layout Optimizer

The layout engine includes an equality saturation optimizer (1,700+ lines in ftui-layout/src/egraph.rs) that finds optimal constraint solutions through algebraic rewriting:

Expression Language:
  Expr ::= Num(u16)           -- concrete pixel value
         | Var(NodeId)         -- widget reference
         | Add(Expr, Expr)     -- constraint arithmetic
         | Sub(Expr, Expr)
         | Max(Expr, Expr)     -- competing constraints
         | Min(Expr, Expr)     -- bounded constraints

Rewrite Rules (equality saturation):
  Add(a, Num(0)) → a                    -- identity
  Add(Num(x), Num(y)) → Num(x + y)     -- constant folding
  Max(a, a) → a                          -- idempotence
  Add(a, b) = Add(b, a)                  -- commutativity
  ...plus ~20 more domain-specific rules

How it works: rather than applying rewrites greedily (which can miss global optima), the e-graph compactly represents all equivalent forms simultaneously. After saturation, the cheapest expression is extracted using a cost model that penalizes deep nesting and prefers constant propagation.

Where it runs: nowhere on the layout path, by measurement. ftui_layout::egraph::solve_layout is a complete, tested alternative solver, but Flex/Grid do not call it and should not: benchmarked against Flex::split over the same constraint sets it is 4x to 19x slower — 233 ns vs 1.99 µs for a typical three-way split, and 5.2 µs vs 99.8 µs for a pathological 200-constraint layout (cargo bench -p ftui-layout --bench layout_bench -- layout/egraph, numbers in docs/perf/egraph_vs_flex_2026-09-18.md).

Equality saturation buys a globally optimal expression, and for this problem that optimum is not worth its price: the constraint counts a terminal layout produces are small enough that the direct solver wins outright. The module stays because the saturation engine is a sound piece of work and the comparison is worth keeping honest, not because it is on a path to being switched on.


Text Engine

The ftui-text crate provides a full text processing stack:

Rope-Backed Storage

Large text buffers (e.g., the advanced text editor demo) use a rope data structure for efficient editing:

Rope (balanced tree of chunks):
  ┌──────┐
  │ Node │  ← weight = total chars in left subtree
  ├──┬───┤
  │  │   │
 ┌┴┐ ┌┴┐
 │A│ │B│   ← leaf chunks (typically 512–2048 chars)
 └─┘ └─┘

Insert at position i:  O(log n) — split + rebalance
Delete range [i,j):    O(log n) — split + drop + rebalance
Index by position:     O(log n) — walk tree using weights

Why rope? For a 100K-line log viewer, inserting at the cursor is O(log n) vs O(n) for a flat String. The rope also enables efficient line-index lookups and range extraction.

Text Editor Core

The ftui_text::Editor provides:

  • Cursor model with visual position (column) vs byte offset tracking
  • Selection with anchor/head semantics (Shift+Arrow, Shift+Click)
  • Word movement through move_word_left() / move_word_right(), with Unicode word-boundary detection; paragraph movement through move_paragraph_up() / move_paragraph_down(), landing on separating blank lines.
  • Grouped undo/redo joins adjacent typing runs. A new word after whitespace, a change in deletion direction, navigation, paste, or break_undo_group() starts a separate group. set_coalesce_idle() configures the idle interval (500 ms by default); callers supply monotonic milliseconds through Editor::tick(), so the editor does not read a wall clock itself.
  • Native clipboard commands through Cmd::set_clipboard(text) / Cmd::get_clipboard(): the runtime writes OSC 52 through TerminalWriter, and terminal replies arrive as Event::Clipboard. The encoded payload limit is 74,994 base64 bytes (at most 56,244 input bytes).

Clipboard output follows terminal capability policy. Multiplexer sessions default to disabled; FTUI_OSC52_CLIPBOARD=1 explicitly enables output while preserving tmux/screen wrapping, and FTUI_OSC52_CLIPBOARD=0 disables it. The terminal and multiplexer must also permit clipboard access and passthrough. A query does not guarantee a reply. Browser clipboard access requires host JavaScript integration; native OSC 52 commands do not grant browser clipboard permissions.

BiDi & Shaping

  • BiDi (bidi.rs, 1,146 lines): Unicode Bidirectional Algorithm for mixed LTR/RTL text (feature bidi, used by Paragraph and the editors)
  • Shaping (shaping.rs, 1,500+ lines): script/run segmentation for cluster-aware rendering
  • Normalization (normalization.rs): NFC/NFD Unicode normalization for consistent comparison

Width Calculation

Every width in the render path goes through ftui_core::text_width. ASCII is answered inline; every other grapheme cluster is looked up in a per-thread S3-FIFO cache in front of the Unicode width tables (unicode-width, VS16 stripping, zero-width scans):

grapheme_width(cluster)
  ASCII?            → byte length, no cache
  cache hit         → width (S3-FIFO, 4096 entries, keyed by a 64-bit hash)
  miss              → Unicode tables, then insert

Width Embedding:
  GraphemeId packs the display width (4 bits) next to the pool slot, so cells
  never consult the pool for width

S3-FIFO is scan-resistant: a stream of one-off emoji in a log viewer cannot evict the hot CJK labels of the table above it. FTUI_WIDTH_CACHE=0 disables the cache; text_width::width_cache_stats() reports hits and occupancy. The W-TinyLFU and LRU caches in ftui-text are benchmark subjects only and are not on the render path. Measured numbers (cached vs uncached, cold vs warm) are in docs/perf/text_width_cache_2026-09-02.md.


Degradation Cascade

When frame rendering exceeds its time budget, FrankenTUI executes a principled degradation cascade that preserves correctness while shedding visual fidelity:

Conformal Frame Guard
  │ "frame will likely exceed budget"

Budget Controller (PID)
  │ computes control signal from frame-time error

Degradation Level Selection
  │ Full → SimpleBorders → NoStyling → EssentialOnly → Skeleton → SkipFrame

Widget Priority Filtering
  │ high-priority widgets rendered first

Evidence Emission
    structured JSONL documenting every decision

Key properties:

  • Recoverable: when load drops, the cascade automatically restores visual fidelity
  • Observable: every degradation event is logged with conformal prediction context
  • Widget-aware: critical widgets (input fields, status bars) degrade last
  • Deterministic: same input sequence always produces the same degradation path

Formal Cost Models

Status: experimental (see Experimental modules)

The cost_model module (1,800 lines) provides closed-form cost models for three subsystems:

Where it runs: nowhere. Nothing in any crate's src/ imports ftui_runtime::cost_model; the formulas below are a library you can call, not a model the runtime consults when sizing caches or scheduling. (ftui-render/src/presenter.rs has a private mod cost_model for ANSI cursor-move costs — same name, unrelated code.)

Cache Cost Model

Loss function:
  L(h, m) = c_miss × (1 - h) + c_memory × m

Optimal cache size (LRU under Zipf workload):
  m* = argmin_m { c_miss × (1 - h(m)) + c_memory × m }

where h(m) is the hit-rate function derived from the characteristic time approximation.

Pipeline Scheduling Model

``$ \text{M}/\text{G}/1 \text{queue} \text{model}: ρ = λ \times \text{E}[\text{S}] -- \text{utilization} \text{W} = (λ \times \text{E}[\text{S}²]) / (2 \times (1 - ρ)) -- \text{Pollaczek}-\text{Khinchine} \text{waiting} \text{time} \text{T} = \text{W} + \text{E}[\text{S}] -- \text{mean} \text{response} \text{time}

\text{Applies} \text{to}: \text{effect} \text{queue}, \text{render} \text{pipeline}, \text{subscription} \text{dispatch} $``

Batching Cost Model

Batch-and-process cost:
  C(b) = c_setup / b + c_per_item × b    -- amortized setup vs holding cost

Optimal batch size:
  b* = √(c_setup / c_per_item)           -- square root law

Applies to: ANSI emission, change run coalescing, event drain bursts

Flake Detection & Sequential FDR Control

Anytime-Valid Flake Detector

Status: experimental (see Experimental modules)

The design is for E2E timing tests to use an e-process to detect flaky regressions without inflating false positives across the hundreds of frames tested:

Sub-Gaussian e-value:
  e_t = exp(λ × r_t − λ²σ²/2)

Cumulative evidence:
  E_t = ∏ᵢ eᵢ

Reject H₀ when E_t ≥ 1/α, valid at ANY stopping time.

Why this matters: traditional significance tests become unreliable when you check p-values after every frame (the "peeking problem"). E-processes eliminate this entirely.

Where it runs: nowhere. No E2E test uses flake_detector. Nothing in any crate's src/ imports it, and its only exercisers are a proptest over the module itself and the quarantine compile check. The detector is implemented and tested; nothing feeds it E2E timings.

Alpha-Investing (Sequential FDR Control)

Status: experimental (see Experimental modules)

When many monitors fire simultaneously (budget alerts, degradation triggers, capability decisions), testing each at a fixed alpha inflates false discoveries. Alpha-Investing treats significance as a spendable resource:

Wealth process:
  W₀ = initial_wealth          (e.g. 0.5)

Per-test:
  αᵢ = min(W, α_max)           -- spend from wealth
  W ← W - αᵢ                    -- deduct cost
  if test i rejects:
    W ← W + reward              -- earn back on discovery

FDR guarantee:
  E[FDP] ≤ initial_wealth / (initial_wealth + reward_total)

Result: the wealth process lets dozens of simultaneous statistical monitors run without false-alarm inflation.

Where it runs: nowhere. Nothing in any crate's src/ imports ftui_runtime::alpha_investing, so the monitors that are on by default — BOCPD and the Mondrian conformal frame-time gate — do not spend from a shared alpha budget. They are two monitors, not dozens, which is why this has not bitten; the accounting exists for a future where it would.


Rough-Path Signatures

Status: experimental (see Experimental modules)

The rough_path module implements rough-path signatures for sequential trace feature extraction, a technique from stochastic analysis:

Given a d-dimensional path X: [0,T] → ℝᵈ, the signature is:

S(X)^{i₁,...,iₖ} = ∫₀<t₁<...<tₖ<T dX^{i₁}_{t₁} ⊗ ... ⊗ dX^{iₖ}_{tₖ}

Truncated at depth K:
  S_K(X) = (1, S¹(X), S²(X), ..., Sᴷ(X))

Properties:

  • Parameterization invariance: S(X) is the same regardless of time warping
  • Universality: signatures separate paths; different paths always have different signatures
  • Efficient computation: Chen's identity enables O(nK²d²) incremental updates

Intended applications in FrankenTUI — none of these are wired; no crate imports ftui_runtime::rough_path:

  • Workload characterization: frame time series → signature → anomaly detection
  • Trace comparison: compare two execution traces without aligning timestamps
  • Regression detection: signature distance between baseline and candidate runs

Where it runs: nowhere. The signature computation is implemented and unit-tested; nothing feeds it a frame time series.


Core Algorithms & Data Structures

FrankenTUI is built on carefully chosen algorithms and data structures optimized for terminal rendering constraints.

Math-Driven Performance

FrankenTUI deliberately uses “heavy” math where it buys real-world speed or determinism. The core idea is: spend a little compute on principled decisions that prevent expensive work later.

Bayesian Match Scoring (Command Palette)

Instead of raw string distance, the palette asks “how likely is this the right command?” Each clue (word start, tags, position) is a multiplier on confidence.

P(RE)P(¬RE)=P(R)P(¬R)iBFi,BFi=P(EiR)P(Ei¬R)\frac{P(R\mid E)}{P(\neg R\mid E)} = \frac{P(R)}{P(\neg R)} \prod_i BF_i, \quad BF_i = \frac{P(E_i\mid R)}{P(E_i\mid \neg R)}

Intuition: add a few strong clues and the right command jumps to the top without expensive rescoring passes.

Evidence Ledger (Explainable Bayes)

Every probabilistic decision records its “why” as a ledger of factors. Internally this is just log‑odds arithmetic:

logP(RE)P(¬RE)=logP(R)P(¬R)+ilogBFi\log \frac{P(R\mid E)}{P(\neg R\mid E)} = \log \frac{P(R)}{P(\neg R)} + \sum_i \log BF_i

Intuition: you can read a human‑friendly list of reasons instead of debugging a black‑box score.

Bayesian Cost Models (Diff Strategy)

The renderer learns the change rate instead of guessing. It keeps a Beta posterior and chooses the cheapest strategy (full diff vs dirty rows vs redraw).

pBeta(α,β),ααγ+k,ββγ+(nk)p \sim \mathrm{Beta}(\alpha,\beta), \quad \alpha \leftarrow \alpha\cdot\gamma + k, \quad \beta \leftarrow \beta\cdot\gamma + (n-k) E[cost]=cscanNscan+cemitNemitE[\text{cost}] = c_{scan}\,N_{scan} + c_{emit}\,N_{emit}

Intuition: when the screen is stable we avoid scanning; when it’s noisy we switch to the cheapest path.

Presenter Cost Modeling (Cursor/Byte Economy)

Even after diffing, there are multiple ways to emit ANSI. We compute a cheap byte‑level cost for cursor moves vs merged runs.

cost=cscanNscan+cemitNemit\text{cost} = c_{scan}\,N_{scan} + c_{emit}\,N_{emit}

Intuition: fewer cursor moves and shorter sequences means less output and lower latency.

BOCPD for Resize Regimes

Resize storms are handled by Bayesian Online Change‑Point Detection. It detects when the stream changes from steady to burst, and only then coalesces aggressively.

H(r)=1λ,P(rt=0x1:t)rP(rt1=r)H(r)P(xtr)H(r)=\frac{1}{\lambda}, \quad P(r_t=0\mid x_{1:t}) \propto \sum_r P(r_{t-1}=r)\,H(r)\,P(x_t\mid r)

Intuition: no brittle thresholds; the model smoothly adapts to drag vs pause behavior.

Run‑Length Posterior + Hazard Function (BOCPD Core)

BOCPD’s main state is the run‑length posterior, which tracks how long the current regime has lasted.

P(rt=rx1:t)P(rt1=r1)(1H(r1))P(xtr)P(r_t=r\mid x_{1:t}) \propto P(r_{t-1}=r-1)\,(1-H(r-1))\,P(x_t\mid r)

Intuition: long steady streaks increase confidence; a sudden timing change collapses the posterior and triggers coalescing.

Conformal Prediction (Risk Bounds)

Alerts are not hard‑coded. The threshold is learned from recent residuals so false‑alarm rates stay stable under distribution shifts.

q=Quantile(1α)(n+1)(R1,,Rn)q = \text{Quantile}_{\lceil(1-\alpha)(n+1)\rceil}(R_1,\dots,R_n)

Intuition: the system learns what “normal” looks like and updates the bar automatically.

E‑Processes + GRAPA (Anytime‑Valid Monitoring)

We can check alerts continuously without “peeking penalties” using a test‑martingale (e‑process). GRAPA tunes the betting fraction.

Wt=Wt1(1+λt(Xtμ0))W_t = W_{t-1}\bigl(1 + \lambda_t (X_t-\mu_0)\bigr)

Intuition: we can look after every frame, and the false‑alarm guarantees still hold.

GRAPA (Adaptive Betting Fraction)

GRAPA adjusts the betting fraction to keep the e‑process sensitive but stable.

λt+1=λt+ηλlogWt\lambda_{t+1} = \lambda_t + \eta\,\nabla_{\lambda}\,\log W_t

Intuition: it auto‑tunes how aggressively we test, instead of locking a single sensitivity.

CUSUM (Fast Drift Detection)

CUSUM accumulates small deviations until they add up, catching sustained drift quickly.

St=max(0,St1+(Xtμ0)k)S_t = \max\bigl(0,\,S_{t-1} + (X_t-\mu_0) - k\bigr)

Intuition: small problems that persist trigger quickly, while isolated noise is ignored.

Value‑of‑Information (VOI) Sampling

The sampler weighs expected variance reduction against measurement cost, with minimum and maximum sampling intervals.

Var(p)=αβ(α+β)2(α+β+1),VOI=Var(p)E[Var(p1 sample)]\mathrm{Var}(p)=\frac{\alpha\beta}{(\alpha+\beta)^2(\alpha+\beta+1)},\quad \mathrm{VOI}=\mathrm{Var}(p)-\mathbb{E}[\mathrm{Var}(p\mid 1\ \text{sample})]

Intuition: sample when uncertainty reduction is worth the configured cost, or when the maximum interval forces a measurement.

Jain’s Fairness Index (Input Guarding)

We watch whether rendering is starving input processing.

F=(xi)2nxi2F=\frac{(\sum x_i)^2}{n\sum x_i^2}

Intuition: a single metric tells us when to yield so the UI feels responsive.

PID / PI Control (Frame Pacing)

Frame‑time control is classic feedback control.

ut=Kpet+Kiet+KdΔetu_t = K_p e_t + K_i \sum e_t + K_d \Delta e_t

Intuition: if we’re too slow, dial down; if we’re too fast, allow more detail. PI is the default because it’s robust and cheap.

MPC (Model Predictive Control) Evaluation

We test MPC vs PI to prove we’re not leaving performance on the table.

minut:t+Hk=0Hyt+ky2+ρut+k2\min_{u_{t:t+H}} \sum_{k=0}^H \|y_{t+k}-y^*\|^2 + \rho\,\|u_{t+k}\|^2

Intuition: MPC looks ahead but costs more; the tests show PI is already good enough for TUI pacing.

Count‑Min Sketch (Approximate Counts)

We track hot items with a probabilistic sketch, then tighten error bounds with PAC‑Bayes.

f^(x)=minjCj,hj(x),\hat f(x)=\min_j C_{j,h_j(x)},\quad

Intuition: a tiny data structure gives you “close enough” frequencies at huge scale.

PAC‑Bayes Calibration (Error Tightening)

We tighten sketch error bounds using PAC‑Bayes.

E[err]eˉ+KL(qp)2n\mathbb{E}[\text{err}] \le \bar e + \sqrt{\frac{\mathrm{KL}(q\|\|p)}{2n}}

Intuition: the bound shrinks as we observe more data, without assuming a specific distribution.

Scheduling Math (Smith’s Rule + Aging)

Background work is ordered by “importance per remaining time,” with aging to prevent starvation.

priority=wr+await\text{priority}=\frac{w}{r}+a\cdot\text{wait}

Intuition: short, important jobs finish quickly, but long‑waiting jobs still rise.

Every one of these is directly tied to throughput, latency, and determinism under real terminal workloads.

Visual FX Math At a Glance

The visual effects screen is deterministic math, not “random shader noise.” Each effect is a concrete dynamical system or PDE with explicit time‑stepping.

EffectCore Equation (MathJax)What It Produces
MetaballsF(x,y)=iri2(xxi)2+(yyi)2F(x,y)=\sum_i \frac{r_i^2}{(x-x_i)^2+(y-y_i)^2}, render iso‑surface FτF\ge \tauSmooth, organic blob fields
Plasmav=16k=16sin(ϕk(x,y,t))v=\frac{1}{6}\sum_{k=1}^6 \sin(\phi_k(x,y,t)) (wave interference in 2D)Psychedelic interference bands
Gray‑Scotttu=Du2uuv2+F(1u)\partial_t u = D_u\nabla^2u - uv^2 + F(1-u); tv=Dv2v+uv2(F+k)v\partial_t v = D_v\nabla^2v + uv^2 - (F+k)vReaction‑diffusion morphogenesis
Clifford Attractorxt+1=sin(ayt)+ccos(axt)x_{t+1}=\sin(a y_t)+c\cos(a x_t); yt+1=sin(bxt)+dcos(byt)y_{t+1}=\sin(b x_t)+d\cos(b y_t)Chaotic strange‑attractor filaments
Mandelbrot / Juliazn+1=zn2+cz_{n+1}=z_n^2+c (escape‑time coloring)Fractal boundaries + deep zooms
Lissajous / Harmonographx=Asin(at+δ)x=A\sin(a t+\delta), y=Bsin(bt+ϕ)y=B\sin(b t+\phi) (optionally eγte^{-\gamma t} damping)Elegant phase‑locked curves
Flow Fieldv(x,y)=(cos2πN, sin2πN)\vec v(x,y)=(\cos 2\pi N,\ \sin 2\pi N); pt+1=pt+vΔtp_{t+1}=p_t+\vec v\,\Delta tParticle ribbons through a vector field
Wave InterferenceI(x,t)=isin(kixsiωit)I(x,t)=\sum_i \sin(k_i\|x-s_i\|-\omega_i t)Multi‑source ripple patterns
Spiral Galaxyr=aebθr=a e^{b\theta} with θ(t)=θ0+ωt\theta(t)=\theta_0+\omega tLogarithmic spiral starfields
Spin Lattice (LLG)dSdt=S×HαS×(S×H)\frac{d\vec S}{dt}=-\vec S\times \vec H-\alpha\,\vec S\times(\vec S\times\vec H)Magnetic domain dynamics

Math At a Glance

TechniqueWhere It’s UsedCore Formula / Idea (MathJax)Performance Impact
Bayes FactorsCommand palette scoringP(RE)P(¬RE)=P(R)P(¬R)iBFi\frac{P(R\mid E)}{P(\neg R\mid E)}=\frac{P(R)}{P(\neg R)}\prod_i BF_iBetter ranking with fewer re‑sorts
Evidence LedgerExplanations for probabilistic decisionslogP(RE)P(¬RE)=logP(R)P(¬R)+ilogBFi\log\frac{P(R\mid E)}{P(\neg R\mid E)}=\log\frac{P(R)}{P(\neg R)}+\sum_i\log BF_iDebuggable, auditable scoring
Log‑BF Capability ProbeTerminal caps detectionlogBF=logP(dataH)P(data¬H)\log BF=\log \frac{P(data\mid H)}{P(data\mid \neg H)}Robust detection from noisy probes
Log10‑BF CoalescerResize scheduler evidence ledgerLBF=log10P(Eapply)P(Ecoalesce)LBF=\log_{10}\frac{P(E\mid apply)}{P(E\mid coalesce)}Explainable, stable resize decisions
Bayesian Hint RankingKeybinding hint orderingVi=E[Ui]+wvoiVar(Ui)λCiV_i=E[U_i]+w_{voi}\sqrt{Var(U_i)}-\lambda C_iStable, utility‑aware hints
Conformal Rank ConfidenceCommand palette stabilitypi=1nj1[gjgi]p_i=\frac{1}{n}\sum_j \mathbf{1}[g_j\le g_i] (gap‑based p‑value)Deterministic tie‑breaks + stable top‑k
Beta-BinomialDiff strategy selectionpBeta(α,β)p\sim\mathrm{Beta}(\alpha,\beta) with binomial updatesAvoids slow strategies as workload shifts
Interval UnionDirty-span diff scanSy=k[x0k,x1k)S_y=\bigcup_k [x_{0k},x_{1k})Scan proportional to changed segments
Summed-Area TableTile-skip diffSAT(x,y)=A(x,y)+SAT(x1,y)+SAT(x,y1)SAT(x1,y1)SAT(x,y)=A(x,y)+SAT(x-1,y)+SAT(x,y-1)-SAT(x-1,y-1)Row-level clean-tile skip on large screens (200x60: 28→21 µs)
Fenwick TreeVirtualized listsPrefix sums with i±(i&i)i\pm (i\&-i)O(log n) scroll + height queries
Bayesian Height PredictorVirtualized list preallocationμn=κ0μ0+nxˉκ0+n\mu_n=\frac{\kappa_0\mu_0+n\bar{x}}{\kappa_0+n} + conformal q1αq_{1-\alpha}Fewer scroll jumps
BOCPDResize coalescingRun‑length posterior + hazard H(r)H(r)Fewer redundant renders during drags
Run‑Length PosteriorBOCPD coreP(rt=rx1:t)P(r_t=r\mid x_{1:t}) recursionFast regime switches without thresholds
E‑ProcessBudget alerts, throttleWt=Wt1(1+λt(Xtμ0))W_t=W_{t-1}(1+\lambda_t(X_t-\mu_0))Safe early exits under continuous monitoring
GRAPAAdaptive e‑processλt+1=λt+ηλlogWt\lambda_{t+1}=\lambda_t+\eta\nabla_{\lambda}\log W_tSelf‑tuning sensitivity
Conformal PredictionRisk boundsq=Quantile(1α)(n+1)(R)q=\text{Quantile}_{\lceil(1-\alpha)(n+1)\rceil}(R)Stable thresholds without tuning
Mondrian ConformalFrame‑time risk gating$\hat y^+=\hat y+q_{1-\alpha}(r
CUSUMBudget change detectionSt=max(0,St1+Xtμ0k)S_t=\max(0,S_{t-1}+X_t-\mu_0-k)Fast drift detection
CUSUM Hover StabilizerMouse hover jitterSt=max(0,St1+dtk)S_t=\max(0,S_{t-1}+d_t-k)Stable hover targets without lag
Damped SpringAnimation transitionsx+cx+k(xx)=0x''+c x' + k(x-x^*)=0Natural motion without frame‑rate artifacts
Easing CurvesFade/slide timingt2t^2, $1-(1-t)^2$, cubic variantsPredictable velocity shaping
Staggered CascadesList animationsoffseti=Dease(i/(n1))offset_i=D\cdot ease(i/(n-1))Coordinated, non‑uniform entrances
Sine PulseAttention pulsesp(t)=sin(πt)p(t)=\sin(\pi t)Smooth 0→1→0 emphasis
Perceived LuminanceDark/light probeY=0.299R+0.587G+0.114BY=0.299R+0.587G+0.114BReliable theme defaults
PID / PIDegradation controlut=Kpet+Kiet+KdΔetu_t=K_pe_t+K_i\sum e_t+K_d\Delta e_tSmooth frame‑time stabilization
MPCControl evaluationminut:t+Hyt+ky2+ρut+k2\min_{u_{t:t+H}}\sum\|y_{t+k}-y^*\|^2+\rho\|u_{t+k}\|^2Confirms PI is sufficient
VOI SamplingExpensive measurementsVOI=VarE[Varsample]\mathrm{VOI}=\mathrm{Var}-\mathbb{E}[\mathrm{Var}\mid\text{sample}]Lower overhead in steady state
Jain’s FairnessInput guardF=(xi)2/(nxi2)F=(\sum x_i)^2/(n\sum x_i^2)Prevents UI render from starving input
Count‑Min SketchWidth cache + timeline aggregationf^(x)=minjCj,hj(x)\hat f(x)=\min_j C_{j,h_j(x)}Fast approximate counts
W‑TinyLFU AdmissionWidth cache admissionadmit if f^(x)f^(y)\hat f(x)\ge \hat f(y) (Doorkeeper → CMS)Higher cache hit‑rate, fewer width recomputes
PAC‑BayesSketch calibrationeˉ+KL(qp)/(2n)\bar e+\sqrt{\mathrm{KL}(q\|\|p)/(2n)}Tighter error bounds
Smith’s Rule + AgingQueueing schedulerpriority=wr+awaitpriority=\frac{w}{r}+a\cdot\text{wait}Fair throughput under load
Cost ModelingPresenter decisionscost=cscanNscan+cemitNemitcost=c_{scan}N_{scan}+c_{emit}N_{emit}Minimizes cursor bytes

The Cell: A 16-Byte Cache-Optimized Unit

Every terminal cell is exactly 16 bytes, fitting 4 cells per 64-byte cache line:

┌──────────────┬──────────────┬──────────────┬───────────────────────┐
│              │              │              │  CellAttrs (4 bytes)  │
│  CellContent │      fg      │      bg      ├───────────┬───────────┤
│   (4 bytes)  │  PackedRgba  │  PackedRgba  │StyleFlags │  link_id  │
│   char/gid   │   (4 bytes)  │   (4 bytes)  │ bits 31-24│ bits 23-0 │
│              │              │              │  (8 bits) │ (24 bits) │
└──────────────┴──────────────┴──────────────┴───────────┴───────────┘
                              Cell (16 bytes)
4 cells per 64-byte cache line. SIMD-friendly 128-bit equality via bits_eq().

Four fields, not five: the hyperlink id has no field of its own, it lives in the low 24 bits of CellAttrs alongside 8 bits of StyleFlags. That is why the API is cell.attrs = cell.attrs.with_link(id) rather than cell.link_id = id, and why a link id is capped at LINK_ID_MAX = 0x00FF_FFFF with 0 reserved as the "no hyperlink" sentinel. const _: () = assert!(size_of::<Cell>() == 16); in cell.rs makes the 16 bytes a compile error to break.

Why 16 bytes?

  • Cache efficiency: 4 cells per cache line means sequential row scans hit L1 cache optimally
  • SIMD comparison: Single 128-bit comparison via bits_eq() for cell equality
  • No heap allocation: 99% of cells store their character inline; only complex graphemes (emoji, ZWJ sequences) use the grapheme pool

That 128-bit comparison is the compiler's, not a hand-written kernel's, and measurement says to leave it that way: explicit std::simd row compares in ftui-simd$ \text{run} 3–4 \times *\text{slower}*, \text{because} \text{safe} \text{code} \text{cannot} \text{view} $&[Cell] as lanes and has to build each vector with shifts and masks, while bits_eq() already lowers to one 128-bit compare. The simd feature therefore accelerates only the printable-ASCII width fast path, where the same kernels win by 5–44×. Numbers in docs/perf/simd_kernels_2026-09-18.md.

Block-Based Diff Algorithm

The diff engine processes cells in 4-cell blocks (64 bytes) for autovectorization:

``$ \text{for} \text{each} \text{row}: \text{if} \text{rows_equal}(\text{old}[\text{y}], \text{new}[\text{y}]): ← \text{Fast} \text{path}: \text{skip} \text{unchanged} \text{rows} \text{continue}

\text{for} \text{each} 4-\text{cell} \text{block}: \text{compare} 4 \times 128-\text{bit} \text{cells} ← \text{SIMD}-\text{friendly} \text{if} \text{any} \text{changed}: \text{coalesce} \text{into} \text{ChangeRun} ← \text{Minimize} \text{cursor} \text{positioning} $``

Key optimizations:

  • Row-skip fast path: Unchanged rows detected with single comparison, no cell iteration
  • Dirty row tracking: Mathematical invariant ensures only mutated rows are checked
  • Change coalescing: Adjacent changed cells become single ChangeRun (one cursor move vs many)

Presenter Cost Model

The ANSI presenter dynamically chooses the cheapest cursor positioning strategy:

// CUP (Cursor Position): CSI {row+1};{col+1}H
fn cup_cost(row, col) → 4 + digits(row+1) + digits(col+1)   // e.g., "\x1b[12;45H" = 8 bytes

// CHA (Column Absolute): CSI {col+1}G
fn cha_cost(col) → 3 + digits(col+1)                        // e.g., "\x1b[45G" = 5 bytes

// Per-row decision: sparse runs vs merged write-through
strategy = argmin(sparse_cost, merged_cost)

cheapest_move_cost(from, to) returns 0 when the cursor is already in place, and on the same row prefers CHA over CUP because CUP always pays the extra row field. The costs are byte counts, so the choice is exact rather than estimated: cup_cost(0, 0) == 6 is pinned against the six bytes of \x1b[1;1H.


Bayesian Intelligence Layer

FrankenTUI uses principled statistical methods for runtime decisions, replacing ad-hoc heuristics with Bayesian inference.

BOCPD: Bayesian Online Change-Point Detection

The resize coalescer uses BOCPD to detect regime changes (steady typing vs burst resizing):

Observation Model:
  inter-arrival times ~ Exponential(λ_steady) or Exponential(λ_burst)

Run-Length Posterior:
  P(r_t | x_1:t) with truncation at K=100 for O(K) complexity

Regime Decision:
  P(burst | observations) → coalescing delay selection

The defaults, the 16/40/100 ms delays, the fallback rule and the evidence fields are stated once, in BOCPD: Online Change-Point Detection. This section is the motivation; that one is the contract. Restating the numbers here is how they drifted apart before.

Why Bayesian?

  • No magic thresholds: Prior beliefs updated with evidence
  • Smooth transitions: Probability-weighted decisions, not binary switches
  • Principled uncertainty: Knows when it doesn't know

E-Process: Anytime-Valid Statistical Testing

Budget decisions and alert thresholds use e-processes (betting-based sequential tests):

Wealth Process:
  W_t = W_{t-1} × (1 + λ_t(X_t - μ₀))

Guarantee:
  P(∃t: W_t ≥ 1/α) ≤ α under null hypothesis

Key Property:
  Valid at ANY stopping time (not just fixed sample sizes)

Practical benefit: You can check the e-process after every frame without inflating false positive rates.

VOI Sampling: Value of Information

The runtime's inline-auto height policy uses the VOI sampling rule above. For a uniform Beta(1,1) prior, one Bernoulli observation gives this exact check:

$ \text{variance\_before} = 1/12 \text{variance\_after\_violation} = \text{Var}[\text{Beta}(2{,}1)] = 1/18 \text{variance\_after\_no\_violation} = \text{Var}[\text{Beta}(1{,}2)] = 1/18 \text{expected\_after} = (1/2) \times (1/18) + (1/2) \times (1/18) = 1/18 \text{VOI} = 1/12 - 1/18 = 1/36 $

Both possible observations contribute to the expectation. The sampler compares the scaled gain with cost, subject to its minimum and maximum sampling intervals.


Experimental modules

These modules compile only with --features experimental on the crate that owns them. APIs may change or be removed without notice.

Read experimental here as "quarantined", not "unstable but working". These are research code with unit tests and no production consumer: nothing in the render path, the runtime loop, or the widget library constructs any of them. The workspace's own compile check is named all_quarantined_modules_are_nameable (ftui-runtime/tests/experimental_feature_matrix.rs) and does exactly what the name says — it binds each type to None to prove the module still compiles. Enabling the feature makes the code available to you; it does not switch anything on inside FrankenTUI.

Sections below that describe an experimental module each carry a Where it runs line, so you do not have to cross-reference this table to find out whether a feature is connected. Checked 2026-09-19 by resolving use ftui_runtime::<module> across every crate's src/. (One trap if you check this yourself: ftui-render/src/presenter.rs declares its own private mod cost_model for ANSI cursor-move costs, which is unrelated to ftui_runtime::cost_model and makes a bare grep for cost_model:: look like a hit.)

ftui-runtime = { version = "0.9", features = ["experimental"] }
CrateModuleWhat it isStatus
ftui-renderroaring_bitmapMinimal Roaring Bitmap for cell-level dirty region trackingexperimental
ftui-runtimeallocation_budgetSequential allocation leak detection using CUSUM and e-processesmerge pending (G13)
ftui-runtimealpha_investingSequential FDR control for multiple simultaneous statistical monitorsexperimental
ftui-runtimeconformal_alertConformal alert threshold calibration with anytime-valid e-process controlexperimental
ftui-runtimeconformal_frame_guardConformal frame guard for frame timing with explicit unavailable boundsexperimental
ftui-runtimeconformal_stagesMulti-stage Mondrian conformal prediction for render pipeline timingexperimental
ftui-runtimecost_modelFormal mathematical cost models for caches, scheduling, and batchingexperimental
ftui-runtimecountmin_sketchCount-Min Sketch with PAC-Bayes error budgeting for timeline aggregationexperimental
ftui-runtimedegradation_cascadeCascade from conformal frame guard risk detection through budget controller to widget prioritymerge pending (G13)
ftui-runtimediff_evidenceBayesian diff strategy evidence ledger in a fixed-capacity ring buffermerge pending (G13)
ftui-runtimeeprocess_throttleAnytime-valid adaptive recompute throttle using GRAPA test martingalesmerge pending (G13)
ftui-runtimeevidence_bridgesConvert domain-specific decision types into unified evidence ledger recordsexperimental
ftui-runtimeflake_detectorAnytime-valid test martingale detector for flaky timing regressions in E2E testsexperimental
ftui-runtimeflat_combineCaller-driven flat combining for batched operation dispatchexperimental
ftui-runtimeivmIncremental View Maintenance (IVM) delta-propagation DAG for derived render stateexperimental
ftui-runtimelensBidirectional algebraic lenses for state-widget bindingexperimental
ftui-runtimepolicy_configPolicy-as-data configuration capturing tunable parameters across the decision stackexperimental
ftui-runtimepolicy_registryThread-safe registry of named policy configurations with lock-free reads and atomic hot-swapexperimental
ftui-runtimeresize_slaResize SLA monitoring with conformal alertingexperimental
ftui-runtimereversibleReversible computing primitives where mutations know their own inverses for undoexperimental
ftui-runtimerough_pathRough-path signatures for sequential trace feature extractionexperimental
ftui-runtimeschedule_traceDeterministic golden trace infrastructure for async task manager testingexperimental
ftui-runtimesloMachine-readable SLO definitions, breach detection, and safe-mode enforcementexperimental
ftui-runtimesos_barrierSum-of-squares (SOS) polynomial barrier certificate evaluator for frame-budget admissibilityexperimental
ftui-runtimetimeline_aggregatorBounded-memory action timeline event aggregation and change-point alertingexperimental
ftui-runtimevalidation_pipelineExpected-cost validation ordering with Bayesian online learningexperimental
ftui-runtimewasm_runnerStep-based synchronous program runner driving a Model without background threadsexperimental

Performance Engineering

Dirty Row Tracking

Every buffer mutation marks its row dirty in O(1):

// ftui-render/src/buffer.rs
#[inline]
fn mark_dirty_row(&mut self, y: u16) {
    if let Some(slot) = self.dirty_rows.get_mut(y as usize) {
        *slot = true;
    }
}

Buffer::set calls this after bounds, scissor, opacity and wide-grapheme handling — it is not a bare index-and-assign, and an out-of-bounds or fully clipped write marks nothing.

Invariant: If is_row_dirty(y) == false, row y is guaranteed unchanged since last clear.

Three levels, not one. dirty_rows is a Vec<bool> (one byte per row) for row-granularity skipping; dirty_spans is a per-row SmallVec<[DirtySpan; 4]> that keeps sparse edits to a few ranges before overflowing to whole-row; and dirty_bits is a per-cell Vec<u8> — one byte per cell, not one bit — backing tile-based skipping.

Cost: dirty_bits$ \text{dominates} \text{at} \text{width} \times \text{height} \text{bytes}, \text{so} \text{dirty} \text{tracking} \text{adds} \text{roughly} 6% \text{to} \text{the} \text{memory} \text{of} \text{a} 16-\text{byte}-\text{per}-$Cell buffer; dirty_rows and dirty_spans are O(height). In exchange the diff skips clean rows entirely and clean spans within dirty rows.

Grapheme Pooling

Complex graphemes (emoji, ZWJ sequences) are reference-counted in a pool:

GraphemeId (4 bytes):
┌─────────────────────────────────────────────────────────────┐
│ [30-27: width] [26-16: generation] [15-0: pool slot index] │
└─────────────────────────────────────────────────────────────┘

Capacity: 65,536 slots, display widths 0-15; the generation counter
          detects stale ids after a slot is recycled
Lookup:   O(1) via hash-map deduplication

Why pooling?

  • Most cells are ASCII (stored inline, no pool lookup)
  • Complex graphemes deduplicated (same emoji = same GraphemeId)
  • Width embedded in ID (no pool lookup for width queries)

Synchronized Output

Frames are wrapped in DEC 2026 sync brackets for atomic display:

CSI ? 2026 h    ←Begin synchronized update
[all frame output]
CSI ? 2026 l    ← End synchronized update (terminal displays atomically)

Guarantee: while sync output is enabled, no partial frame is ever visible, eliminating flicker even on slow terminals. It is enabled when either:

  • the terminal identity is on the allowlist (kitty, Ghostty, Alacritty via TERM_PROGRAM or TERM=alacritty, Contour), or
  • the terminal answers the DECRPM probe (CSI ? 2026 $ p) at startup saying it recognizes mode 2026. This is how plain xterm-256color, iTerm2, VS Code, and any terminal reached over ssh get flicker-free frames; the probe is bounded (300 ms), fail-open, and upgrade-only.

It is deliberately off inside tmux, screen, and zellij, and for WezTerm identities (WezTerm multiplexer sessions were observed to misbehave around ?2026 h/l, and the markers that would distinguish a mux session from a local window do not survive every launch path). Operators can override policy without rebuilding: FTUI_SYNC_OUTPUT=1|0 and FTUI_SCROLL_REGION=1|0 force the setting (an explicit 1 also lifts the WezTerm gate, never real multiplexer evidence), and FTUI_CAPS_PROBE=0 disables startup probing. The matrix in scripts/pty_identity_matrix.py runs the showcase under a real PTY per identity and asserts exactly which of these outcomes occur; the measured results are in docs/compat-matrix.md.


The Elm Architecture in Rust

FrankenTUI implements the Elm/Bubbletea architecture with Rust's type system:

The Model Trait

pub trait Model: Sized {
    type Message: From<Event> + Send + 'static;

    // Required.
    fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message>;
    fn view(&self, frame: &mut Frame);

    // Defaulted — override only what you need.
    fn init(&mut self) -> Cmd<Self::Message> { Cmd::none() }
    fn subscriptions(&self) -> Vec<Box<dyn Subscription<Self::Message>>> { vec![] }
    fn on_gesture(&mut self, gesture: SemanticEvent) -> Cmd<Self::Message> { Cmd::none() }
}

Update/View Loop

┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
│  Event  │───▶│ Message │───▶│ Update  │───▶│  View   │
│ (input) │    │ (enum)  │    │ (model) │    │ (frame) │
└─────────┘    └─────────┘    └─────────┘    └─────────┘
                                   │              │
                                   ▼              ▼
                              ┌─────────┐    ┌─────────┐
                              │   Cmd   │    │ Render  │
                              │ (async) │    │ (diff)  │
                              └─────────┘    └─────────┘

Commands & Side Effects

Cmd::none()                          // No side effect
Cmd::task(|| Msg::Loaded(load()))    // Background work on a worker → Message
Cmd::tick(Duration::from_millis(50)) // One Tick message after the delay
Cmd::msg(Msg::Refresh)               // Deliver a message on the next turn
Cmd::quit()                          // Exit program
Cmd::batch(vec![...])                // Multiple commands, order irrelevant
Cmd::sequence(vec![...])             // Multiple commands, in order

Cmd::SetClipboard(String) requests a clipboard write; construct it with Cmd::set_clipboard(text). Cmd::GetClipboard, constructed with Cmd::get_clipboard(), requests a read whose reply is delivered as Event::Clipboard. Native execution uses the terminal capability policy and OSC 52 limits described above.

Subscriptions

Declarative, long-running event sources:

fn subscriptions(&self) -> Vec<Box<dyn Subscription<Msg>>> {
    vec![
        tick_every(Duration::from_millis(16), || Msg::Tick), // 60fps timer
        file_watcher("config.toml", Msg::ConfigChanged), // FileEvent::{Created, Modified, Removed}
    ]
}

Subscriptions are automatically started/stopped based on what subscriptions() returns each frame; a subscription's id (interval for tick_every, path for file_watcher) is what keeps it running across frames. file_watcher polls metadata (mtime and size, 250 ms by default; FileWatcher::new(..).with_interval(..) to change it), so it works on every platform without a native watcher dependency.


Safety & Correctness Guarantees

Zero Unsafe Code Policy

// ftui-render/src/lib.rs
#![forbid(unsafe_code)]

// ftui-runtime/src/lib.rs
#![forbid(unsafe_code)]

// ftui-layout/src/lib.rs
#![forbid(unsafe_code)]

The entire render pipeline, runtime, and layout engine contain zero unsafe blocks.

Integer Overflow Protection

Coordinate arithmetic uses saturating or checked operations — 3,216 saturating_* and 68 checked_* call sites across the workspace:

// Cursor positioning (saturating)
let next_x = current_x.saturating_add(width as u16);

// Bounds checking (checked)
let Some(target_x) = x.checked_add(offset) else { continue };

// Intentional wrapping (PRNG only)
seed.wrapping_mul(6364136223846793005).wrapping_add(1)

This is a convention, not a compiler guarantee: no clippy::arithmetic_side_effects lint is enabled, so a bare + on a coordinate would compile. The unsafe ban above is enforced — #![forbid(unsafe_code)] is present in all 20 crates.

Flicker-Free Proof Sketch

The codebase includes formal proof sketches in no_flicker_proof.rs:

Theorem 1 (Sync Bracket Completeness): Every byte emitted by Presenter is wrapped in DEC 2026 sync brackets.

Theorem 2 (Diff Completeness): BufferDiff::compute(old, new) produces exactly {(x,y) | old[x,y] ≠ new[x,y]}.

Theorem 3 (Dirty Tracking Soundness): If any cell in row y was mutated, is_row_dirty(y) == true.

Theorem 4 (Diff-Dirty Equivalence): compute() and compute_dirty() produce identical output when dirty invariants hold.


Test Infrastructure

Property-Based Testing

crates/ftui-render/tests/proptest_diff_invariants.rs generates random buffers and change sets and checks the diff against them from both directions:

proptest! {
    // Soundness: nothing the diff reports is actually unchanged.
    #[test]
    fn no_false_positive_changes((w, h) in dims(), changes in change_set(80, 40)) { /* .. */ }

    // Completeness: nothing that changed is missing from the diff.
    #[test]
    fn no_false_negative_changes((w, h) in dims(), changes in change_set(80, 40)) { /* .. */ }
}

Alongside them: identical_buffers_produce_empty_diff, diff_is_deterministic, compute_and_compute_into_equivalent, runs_cover_all_changes and changes_sorted_row_major.

Snapshot Testing

# Run tests, auto-update baselines
BLESS=1 cargo test -p ftui-demo-showcase

# 599 plain-text .snap files, one per case, reviewed as an ordinary diff
crates/ftui-demo-showcase/tests/snapshots/   # 432
├── a11y_accessibility_panel_all_modes_120x40.snap
├── a11y_accessibility_panel_high_contrast_80x24.snap
└── widget_gallery_verification_120x40.snap
crates/ftui-extras/tests/snapshots/          # 101
crates/ftui-harness/tests/snapshots/         #  66

A .snap file is the rendered frame as text, so a failing snapshot diff shows the actual character grid that changed rather than an opaque blob.

Formal Verification Patterns

Theorem 3 (dirty-tracking soundness) is pinned by set_marks_row_dirty in ftui-render/src/buffer.rs — if it fails, the theorem is false:

#[test]
fn set_marks_row_dirty() {
    let mut buf = Buffer::new(10, 5);
    buf.clear_dirty(); // Reset initial dirty state
    buf.set(3, 2, Cell::from_char('X'));
    assert!(buf.is_row_dirty(2));
    assert!(!buf.is_row_dirty(0));
    assert!(!buf.is_row_dirty(1));
    assert!(!buf.is_row_dirty(3));
    assert!(!buf.is_row_dirty(4));
}

Note it clears the initial dirty state first: a fresh Buffer starts with every row dirty (dirty_rows_start_dirty), so a test that skipped clear_dirty() would pass without proving anything. Its siblings cover the other mutation paths — set_raw_marks_row_dirty, get_mut_marks_row_dirty, fill_marks_affected_rows_dirty, clear_marks_all_dirty.

Benchmark Suite

cargo bench -p ftui-render

# Output:
# diff/identical_100x50    time: [1.2 µs]   throughput: [4.2 Mcells/s]
# diff/sparse_5pct_100x50  time: [8.3 µs]   throughput: [602 Kcells/s]
# diff/dense_100x50        time: [45 µs]    throughput: [111 Kcells/s]

Runtime Systems

Resize Coalescing

Rapid resize events (e.g., window drag) are coalesced to prevent render thrashing:

Event Stream:    R1 ─ R2 ─ R3 ─ R4 ─ R5 ─ [gap] ─ R6
                 └───────────────────┘           │
                        coalesced               applied
                      (only R5 rendered)

Regimes:
  Steady (200ms delay)  ← Responsive to deliberate resizes
  Burst  (20ms delay)   ← Aggressive coalescing during drag

Budget-Based Degradation

Frame time is regulated with a PID controller:

Error:        e_t = (actual_ms - target_ms) / target_ms   // normalized; POSITIVE = over budget
Control:      u_t = Kp·e_t + Ki·Σe + Kd·Δe
Integral:     Σe clamped to ±5.0 (anti-windup)
Degradation:  Full → SimpleBorders → NoStyling → EssentialOnly → Skeleton → SkipFrame

Gains: Kp=0.5, Ki=0.05, Kd=0.2 (tuned for 16ms / 60fps)

The error is a fraction of the budget, not milliseconds, and positive means the frame ran long — so a rising u_t degrades. The gains above are tuned for that normalized form; feeding them raw milliseconds would scale the response by the budget. An earlier version of this block had the error as target_ms - actual_ms, which is both un-normalized and the opposite sign, and would have had the controller degrading on fast frames.

When frames exceed budget, the renderer automatically degrades visual fidelity to maintain responsiveness.

Input Fairness Guard

Prevents render work from starving input processing:

Fairness Index: F = (Σx_i)² / (n × Σx_i²)   ← Jain's Fairness Index

Intervention: if input_latency > threshold OR F < 0.8:
  force_resize_coalescer_yield()

Widget System (80+ Direct Implementations)

FrankenTUI ships 80+ direct Widget and StatefulWidget implementations across ftui-widgets.

Core Widgets

WidgetDescriptionKey Feature
BlockContainer with borders/title7 named border styles, title alignment
ParagraphText with wrappingWord/char wrap, scroll
ListSelectable itemsVirtualized, custom highlight
TableColumnar dataColumn constraints, row selection, themed
InputText inputCursor, selection, history
TextareaMulti-line inputLine numbers, syntax hooks
TabsTab barCloseable, reorderable
ProgressProgress barsDeterminate/indeterminate
SparklineInline chartsMin/max markers
TreeHierarchical dataExpand/collapse, lazy loading
CommandPaletteFuzzy searchBayesian scoring with evidence ledger
ModalDialog/overlay systemStack‑based, focus capture
JsonViewJSON tree viewerCollapse/expand nodes
FilePickerFile browserDirectory navigation
VirtualizedListLarge listsFixed or variable row heights (variable_heights(), Fenwick-tracked, O(log n) scroll); the Virtualized primitive adds Bayesian height prediction
ToastNotificationsTimed, dismissable
SpinnerActivity indicatorMultiple styles
ScrollbarScroll positionProportional thumb

Plus: Align, Badge, CachedWidget, Columns, ConstraintOverlay, DebugOverlay, DecisionCard, Draggable, Emoji, ErrorBoundary, Group, Help, HistoryPanel, InspectorOverlay, LogViewer, NotificationQueue, Padding, Paginator, Panel, Pretty, Rule, StatusLine, Stopwatch, Timer, ValidationErrorDisplay, VoiDebugOverlay, DriftVisualization, and more.

Table Theming System

The table widget has a dedicated theme engine (3,500+ lines in ftui-style/src/table_theme.rs) that goes far beyond simple row striping:

Theme FeatureWhat It Controls
Row stripingAlternating background colors with configurable period
Column emphasisPer-column foreground/background overrides
Header stylingSeparate style for header row with bottom border
Selection highlightActive row/cell highlight with blend modes
Hover stateMouse-over styling with CUSUM-stabilized transitions
Border variants7 built-in border styles + Custom per table edge
Cell paddingPer-cell horizontal/vertical padding
TruncationEllipsis, clip, or wrap per column
AlignmentLeft/center/right per column with Unicode-aware width

Themes are composable; a base theme can be overlaid with per-instance overrides:

// Six built-in presets (Aurora, Graphite, Neon, Slate, Solar, Orchard), each a
// complete set of header / stripe / selection effect rules
let theme = TableTheme::preset(TablePresetId::Aurora);
let resolver = theme.effect_resolver();

Stylesheet System

Named styles are defined once in a StyleSheet (thread-safe, ftui_style::stylesheet) for consistent theming across widgets:

use ftui::render::cell::PackedRgba;
use ftui::style::stylesheet::StyleSheet;

let sheet = StyleSheet::new();
let heading = Style::new().bold().fg(PackedRgba::rgb(80, 160, 255));
let error = Style::new().bold().fg(PackedRgba::rgb(220, 60, 60));
sheet.define("heading", heading);
sheet.define("error", error);
sheet.define("muted", Style::new().fg(PackedRgba::rgb(128, 128, 128)));

// Resolve by name anywhere in the widget tree; compose layers left to right
let resolved = sheet.get_or_default("heading");
let loud_error = sheet.compose(&["heading", "error"]);

Widget Composition

// Widgets compose via Frame's render helpers (FrameExt, in the prelude)
use ftui::layout::{Constraint, Flex};

fn view(&self, frame: &mut Frame) {
    let chunks = Flex::horizontal()
        .constraints([Constraint::Percentage(30.0), Constraint::Percentage(70.0)])
        .split(frame.area());

    frame.render_widget(&self.sidebar, chunks[0]);
    frame.render_widget(&self.main_content, chunks[1]);
}

Stateful Widgets

// State lives in your Model; `view(&self)` borrows it mutably through a RefCell
struct MyModel {
    items: Vec<String>,
    list_state: RefCell<ListState>,
}

fn view(&self, frame: &mut Frame) {
    let list = List::new(self.items.iter().map(String::as_str));
    frame.render_stateful_widget(&list, frame.area(), &mut self.list_state.borrow_mut());
}

Advanced Features

let link_id = frame.register_link("https://example.com");
let mut cell = Cell::from_char('x');
cell.attrs = cell.attrs.with_link(link_id);
// Emits OSC 8 hyperlink sequences for supporting terminals

Focus Management

// Declarative focus graph: FocusManager owns a FocusGraph of nodes and nav edges
let graph = focus.graph_mut();
let input1 = graph.insert(FocusNode::new(1, input1_area));
let input2 = graph.insert(FocusNode::new(2, input2_area));
graph.connect(input1, NavDirection::Next, input2); // Tab order

// Navigation
focus.focus_next(); // Tab
focus.focus_prev(); // Shift+Tab
let id = modal_stack.push(Box::new(confirm_dialog)); // any `StackModal`
// Modals capture input, render above main content
// Escape or button press pops the stack; `pop()` returns the ModalResult

Time-Travel Debugging

// Record frames for debugging (ftui-harness; delta-compressed ring of 256 frames)
let mut history = TimeTravel::new(256);
history.record(&frame.buffer, FrameMetadata::new(frame_number, render_time));

// Replay
let historical_frame = history.get(frame_index);

Accessibility

The ftui-a11y crate provides an accessibility tree that mirrors the widget render tree:

  • Semantic nodes for every interactive widget (button, input, list item)
  • Role/state/label properties following WAI-ARIA semantics
  • Focus graph with keyboard navigation order (Tab/Shift+Tab)
  • Live regions for announcing dynamic content changes to screen readers
  • Contrast checking using WCAG 2.1 luminance ratios (ftui-style/src/color.rs)

Runtime wiring. Widgets that implement ftui_a11y::Accessible (Block, Input, List, Table, Tabs, Paragraph, Progress, Scrollbar, Spinner) push their nodes into the Frame as they render. Enable collection per program:

use ftui::{ProgramConfig, ScreenReaderPolicy};

let config = ProgramConfig::default().with_accessibility(ScreenReaderPolicy::default());

With that set, every rendered frame builds an A11yTree, diffs it against the previous frame and derives bounded screen‑reader announcements (focus changes, live‑region additions and changes). Read them through Program::accessibility_tree / accessibility_announcements / accessibility_dump, or react from the model via Model::on_accessibility, which runs after each frame whose tree changed:

fn on_accessibility(&mut self, a11y: AccessibilityFrame<'_>) -> Cmd<Msg> {
    // Runs after each frame whose tree changed. Forward the bounded
    // announcements to a host bridge, a log, or an on-screen live region.
    self.announcements
        .extend(a11y.announcements.iter().map(|a| a.text.clone()));
    Cmd::none()
}

The evidence sink gets a11y_tree and a11y_announcement rows (schema in docs/spec/telemetry-events.md) and the ftui.a11y tracing target carries the same data. Without the config no tree is built and rendering is unchanged.

Not done: there is no operating‑system bridge (AT‑SPI, UIA, NSAccessibility). Announcements are evidence rows, tracing events and hook calls that a host or an app forwards; nothing reaches a screen reader on its own. Containers other than Block do not scope their children yet, and Form has no accessibility nodes. Details and proofs: docs/ACCESSIBILITY.md.

The accessibility_panel demo screen mirrors the live tree (size, leading dump lines) and the latest announcements as you navigate the UI.


Internationalization

The ftui-i18n crate provides locale-aware rendering:

  • Locale context propagated through the runtime (ProgramConfig::with_locale("fr"), LocaleContext::direction())

  • String catalog with fallback chains and CLDR-style plural rules (StringCatalog, PluralRule)

  • Text direction from locale with per-line UAX#9 reordering in Paragraph/TextInput/TextArea (feature bidi, on by default in ftui)

  • Number & date formatting backed by pinned Unicode CLDR v45.0 data for 7 declared locales (NumberFormatter, DateTimeFormatter, format_number, format_date)

The i18n_demo screen switches live between English, Spanish, French, German, Russian, Arabic and Japanese; Arabic renders right-to-left.


Queueing-Theoretic Scheduler (Deep Dive)

The effect queue scheduler (2,800+ lines in queueing_scheduler.rs) implements multiple scheduling disciplines from queueing theory:

SRPT (Shortest Remaining Processing Time)

Optimal for minimizing mean response time in M/G/1 queues:
  E[T_SRPT] ≤ E[T_FCFS]  for any service-time distribution

Selection rule:
  next_job = argmin { remaining_time(j) : j ∈ ready_queue }

Problem: can starve long jobs indefinitely.

Smith's Rule (Weighted SRPT)

Maximizes weighted throughput:
  priority(j) = weight(j) / remaining_time(j)

next_job = argmax { priority(j) : j ∈ ready_queue }

Aging for Starvation Prevention

Effective priority:
  priority_eff(j) = weight(j) / remaining_time(j) + aging_factor × wait_time(j)

Wait time grows linearly, so even low-priority jobs eventually rise above high-priority ones.
Guaranteed service: every job completes within O(N × max_weight / aging_factor) time.

Queue Telemetry

let snap = ftui_runtime::effect_system::queue_telemetry();
// QueueTelemetry {
//   enqueued: 1042,          -- total tasks submitted
//   processed: 1038,         -- total tasks completed
//   dropped: 2,              -- tasks dropped (backpressure/shutdown)
//   high_water: 12,          -- peak queue depth observed
//   in_flight: 2,            -- currently executing
// }

Backpressure kicks in when in_flight ≥ max_queue_depth, preventing unbounded memory growth under burst load.


Inline Mode: How Scrollback Preservation Works

Most TUI frameworks take over the alternate screen, destroying the user's scrollback history. FrankenTUI's inline mode keeps the UI stable while letting log output scroll naturally above it. Three strategies are implemented and selected automatically based on terminal capabilities:

Strategy A: Scroll Region (DECSTBM)

Terminal viewport (24 rows):
  ┌──────────────────────────┐
  │ log line 47              │ ← scrollable region (rows 1-20)
  │ log line 48              │    DECSTBM constrains scrolling here
  │ log line 49              │
  │ ...                      │
  │ log line 66              │
  ├──────────────────────────┤
  │ ▌Status: 3 tasks  FPS:60│ ← fixed UI region (rows 21-24)
  │ ▌[Tab] switch  [q] quit │    cursor never enters this region
  └──────────────────────────┘

CSI sequence: ESC [ top ; bottom r   (set scrolling region)

When new log output arrives, the terminal scrolls only within the designated region. The UI rows below are untouched.

Strategy B: Overlay Redraw

For terminals that don't support scroll regions reliably (some multiplexers, older emulators), FrankenTUI saves the cursor, clears the UI area, writes new log lines, redraws the UI, and restores the cursor. Wrapped in DEC 2026 sync brackets, this appears atomic to the user.

Strategy C: Hybrid

Scroll-region without synchronized output: the fast path is the same DECSTBM region as Strategy A, but without sync brackets around the redraw. Before the first frame the runtime runs a DECSTBM self‑test on the live terminal (ftui_core::caps_probe::probe_scroll_region): save cursor, set the region, turn origin mode on, move the cursor far below, ask for a cursor‑position report, restore. A terminal that honours the region reports the region's last row; one that ignores it reports the screen bottom (or nothing within 150 ms). On a bad verdict the writer switches to Strategy B before anything is drawn and writes an inline_strategy_fallback evidence row (reason cpr_mismatch or cpr_timeout, observed and expected rows); the inline_strategy row carries scroll_region_verified. The probe runs at construction time, before the input reader starts, so it cannot race the event loop; it is not repeated on resize. FTUI_SCROLL_REGION=1 skips the test and trusts the region, FTUI_SCROLL_REGION=0 selects overlay redraw outright. scripts/inline_scroll_region_selftest_e2e.py runs the showcase under a PTY that answers the cursor query as a honouring, an ignoring and a silent terminal and checks all three outcomes. Under this repository's own WezTerm session the WezTerm identity counts as a multiplexer, so inline runs there use overlay redraw and emit no DECSTBM at all; that is the mux rule, not a writer bug.

Which strategy you actually get

InlineStrategy::select (in ftui_core::inline_mode) decides in this order:

  1. inside tmux, screen, zellij or a WezTerm mux session: Overlay Redraw
  2. scroll-region capability and synchronized output both usable: Scroll Region
  3. scroll-region usable, no synchronized output: Hybrid
  4. otherwise: Overlay Redraw

"Usable" is the policy answer (use_scroll_region() / use_sync_output()) after identity detection, the live DECRPM probe and the FTUI_SCROLL_REGION / FTUI_SYNC_OUTPUT operator switches. Strategies 2 and 3 are then confirmed by the DECSTBM self‑test described under Strategy C; a terminal that fails it gets Overlay Redraw. The decision is written to the evidence file as capability_decision rows plus an inline_strategy row (with scroll_region_verified) and, on a failed self‑test, an inline_strategy_fallback row when an evidence sink is configured.

Key Invariants

  • Scrollback history is never destroyed
  • UI region never flickers (sync brackets guarantee atomicity)
  • Cursor position is restored exactly after each render cycle
  • Log output above the UI region is genuine terminal scrollback (you can scroll up to see it)

Incremental View Maintenance (IVM)

Status: experimental (see Experimental modules)

The idea is to stop recomputing layouts, styled text, and visibility flags from scratch every frame, and instead propagate deltas through a DAG of view operators — the technique materialized-view databases use (Materialize, Noria), adapted for frame-rate rendering.

Where it runs: nowhere. There is no propagation engine. ftui_runtime::ivm has the pieces such an engine would need and not the engine itself, verified 2026-09-19:

  • Nothing walks the DAG feeding one view's output into the next. The module's only use statements are std::fmt and std::hash, so no delta can reach a Buffer or the presenter.
  • An earlier version of this section showed a diagram with StyleMap, TextWrap, FlexSolve and RenderPlan operators, and said theme-change deltas flow to RenderPlan without re-running wrapping or constraint solving. None of those four types exist, and neither do the LayoutView/RenderView that the module's own docs named — the two descriptions did not even agree with each other.
  • FRANKENTUI_FULL_RECOMPUTE=1 is read by IvmConfig::from_env, but nothing reads the resulting flag, so it changes no behaviour.

What does exist, and is tested: the signed-tuple delta algebra ((key, weight, logical_time) with cancellation), a DAG with topological ordering and cycle detection, a fallback-policy predicate, and two IncrementalView implementations (StyleResolutionView, FilteredListView). Useful groundwork; not something you can turn on.

Before anyone builds the engine, note that the render cost surface measured on 2026-09-19 found buffer_diff to be the cheapest pipeline stage at 4.5–9.1% of the frame, while cell_mutation and presenter_emit dominate (docs/perf/cost_surface_stage_dominance_2026-09-19.md). An incremental layer that avoids recomputation has to beat simply doing the work — which is the measurement that stopped the e-graph. Tracked in bd-lksq7.


SOS Barrier Certificates

Status: experimental (see Experimental modules)

The module offers a frame-budget admissibility test built on a polynomial barrier certificate in the sum-of-squares style. Nothing calls it — see Where it runs below — so the present tense here describes the function, not the render path:

State space:
  x₁ = budget_remaining ∈ [0, 1]    (fraction of frame budget left)
  x₂ = change_rate ∈ [0, 1]         (estimated render cost)

Barrier certificate B(x₁, x₂):
  B(x₁, x₂) = Σ cᵢⱼ x₁ⁱ x₂ʲ     (polynomial, degree 4, 15 terms)

Safety:
  B(x) > 0   ⟹  state is admissible (safe to render at full fidelity)
  B(x) ≤ 0   ⟹  state is at or beyond the unsafe boundary

Provenance: the coefficients are hand-chosen, not SDP-solved. sos_barrier_coeffs.rs says so itself — they were written by hand to satisfy eight verification points, and no scripts/solve_sos_barrier.py exists in this repository or its history. An earlier version of this section claimed an SOS/SDP relaxation produced them and named that script; the 2026-09-01 reality check found that claim false, the source header was corrected, and this section was not. If a solver is ever added, regenerate the coefficients from it and record the solver, its inputs and the run date in that file.

So what the evaluator gives you is a polynomial admissibility test whose shape was chosen by hand, not a certificate carrying an SOS proof of the Lyapunov-like decrease condition.

Where it runs: nowhere. Nothing in any crate's src/ imports ftui_runtime::sos_barrier, so no frame's budget is actually checked against the barrier. An earlier correction of this section said the evaluator "runs in constant time per frame with no allocations" — true of the function's complexity, misleading about its use, since nothing calls it per frame or at all. sos_barrier.rs is 257 lines, allocation-free and constant-time when called, which is the part worth relying on if you call it yourself.

Why SOS instead of a simple threshold? A polynomial barrier can encode nonlinear safe/unsafe boundaries that accurately reflect the interaction between budget remaining and workload estimate. A flat threshold either triggers too early (wasting visual quality) or too late (missing the deadline).


S3-FIFO Cache

Grapheme width lookups (ftui_core::text_width) use an S3-FIFO eviction policy, which was shown to match or outperform W-TinyLFU and ARC on most workloads while being simpler to implement (terminal capability detection is not cached; it runs once per session):

Three queues:
  Small  (10% capacity) ← new entries land here
  Main   (90% capacity) ← promoted entries (accessed ≥ 1 time in Small)
  Ghost  (keys only)    ← recently evicted keys for frequency tracking

Eviction from Small:
  if accessed ≥ 1 time → promote to Main
  else → evict (key goes to Ghost)

Eviction from Main (FIFO + frequency):
  if freq > 0 → decrement freq, re-insert at tail
  else → evict permanently

The key insight: S3-FIFO is scan-resistant without the overhead of an LRU doubly-linked list. Sequential access patterns (like scanning a large buffer) don't flush the cache.


Flat Combining

Status: experimental (see Experimental modules)

When multiple event sources (timers, background tasks, input) post operations concurrently, flat combining batches them into a single pass. One thread becomes the "combiner" and executes ALL pending operations while holding the state lock:

Thread 1: post(op_a) → wait
Thread 2: post(op_b) → wait           combiner (Thread 3):
Thread 3: post(op_c) → becomes          lock state
           combiner                      execute op_a
                                         execute op_b
                                         execute op_c
                                         unlock state
                                         wake threads 1, 2

Benefits over a bare Mutex:

  • Data stays hot in L1 cache (one thread touches everything)
  • Lock acquisition happens once per batch, not once per operation
  • Natural coalescing: redundant operations (multiple redraws) collapse

Where it runs: nowhere. Nothing in any crate's src/ imports ftui_runtime::flat_combine, and the runtime's event sources do not post through a combiner. The diagram above describes what the module implements, not how FrankenTUI dispatches operations today.


Bidirectional Lenses

Status: experimental (see Experimental modules)

The lens module provides algebraic lenses for binding widgets to model subfields:

use ftui::runtime::lens::{Lens, field_lens};

struct Config {
    volume: u8,
    brightness: u8,
}

// A lens focuses on one part of a larger structure
let volume = field_lens(|c: &Config| c.volume, |c: &mut Config, v| c.volume = v);

// Laws: GetPut (setting what you just read is a no-op),
//       PutGet (you read back exactly what you set)
let mut config = Config {
    volume: 75,
    brightness: 50,
};
assert_eq!(volume.view(&config), 75);
volume.set(&mut config, 100);
assert_eq!(config.volume, 100);
assert_eq!(config.brightness, 50); // other fields untouched

Lenses compose, so compose(config_lens, volume_lens) creates a lens from AppState directly to volume through an intermediate Config struct.

Where it runs: nowhere. The module is titled "state-widget binding", but no widget binds through a lens: nothing in any crate's src/ imports ftui_runtime::lens. The laws hold and the example above is compiled by readme_snippets; what is missing is anything on the widget side that consumes one.


Input Macro Recording & Playback

The InputMacro system records terminal events with timing for deterministic replay:

// Record
let mut recorder = MacroRecorder::new("login_flow");
recorder.record_event(key_event_username);
recorder.record_event_with_delay(key_event_tab, Duration::from_millis(200));
recorder.record_event(key_event_password);
recorder.record_event(key_event_enter);
let macro_data = recorder.finish();

// Replay through ProgramSimulator (replay_with_timing honors the recorded delays)
MacroPlayer::new(&macro_data).replay_all(&mut simulator);

Uses: regression testing, demo recording, user workflow capture. The macro_recorder demo screen shows this in action.


State Persistence

Widget state survives across sessions via the StateRegistry:

┌───────────────────────────────────────────────────────────────┐
│                       StateRegistry                           │
│   In-memory cache of widget states (HashMap<WidgetId, State>) │
│   Delegates to StorageBackend for persistence                 │
└───────────────────────────────┬───────────────────────────────┘

                ┌───────────────┼───────────────┐
                ▼               ▼               ▼
          FileStorage     MemoryStorage   impl StorageBackend
          (JSON on disk)  (tests only)    (user-provided)

Configuration:

use ftui::runtime::{PersistenceConfig, StateRegistry};
use std::sync::Arc;

// In-memory registry needs no feature; `StateRegistry::with_file(path)` (JSON on
// disk, atomic writes) needs ftui-runtime's `state-persistence` feature.
let registry = Arc::new(StateRegistry::in_memory());
let config = ProgramConfig::default().with_persistence(
    PersistenceConfig::with_registry(registry)
        .auto_load(true)
        .auto_save(true)
        .checkpoint_every(Duration::from_secs(30)),
);

Widgets opt in by implementing the Stateful trait. On program start, the registry loads saved state; on exit (or periodic checkpoints), it flushes back to the backend.


SLO Schema & Breach Detection

Status: experimental (see Experimental modules)

FrankenTUI supports machine-readable Service Level Objectives for runtime behavior:

# slo.yaml - the schema parse_slo_yaml actually accepts
regression_threshold: 0.10
noise_tolerance: 0.05
safe_mode_breach_count: 2
safe_mode_error_rate: 0.05

metrics:
  render_p99:
    metric_type: latency
    max_value: 16000.0
    max_ratio: 1.25
    safe_mode_trigger: true
  shutdown_us:
    metric_type: latency
    max_value: 5000.0
    safe_mode_trigger: false

metrics is a map keyed by metric name, not a list. regression_threshold and noise_tolerance are global fractions in [0.0, 1.0]; max_value is an absolute ceiling and max_ratio a multiple of the baseline; safe_mode_trigger makes one breach trip safe mode on its own, while safe_mode_breach_count covers simultaneous breaches. metric_type accepts exactly latency, memory and error_rate — anything else is an UnknownMetricType error.

Note the parser skips whole-line # comments but does not strip trailing ones, so max_value: 16000.0 # ceiling fails to parse. Keep annotations on their own lines.

The project's own slo.yaml is the worked example, and slo_yaml_parses_without_errors keeps it parsing.

An earlier version of this block documented an objectives: list with metric / budget_us / window_seconds / error_budget_pct keys. No such schema exists — not in the parser, and not in the repository's own slo.yaml, which has used the fields above all along. Because an empty document parses to defaults, copying the old example produced a schema with zero metrics rather than failing loudly. readme_slo_yaml_example_parses now pins the block above byte-for-byte against the parser so it cannot drift again.

The SLO engine checks observations against budgets and tracks error-budget consumption:

slo.yaml  ──parse──▶  SloSchema

        observations ──▶ check_breach() ──▶ BreachResult

                                   ┌──────────┴──────────┐
                                   ▼                     ▼
                              No breach             Breach detected
                              (continue)            (enter safe mode)

When an SLO is breached, safe mode (reduced rendering, aggressive coalescing) is meant to hold until the error budget recovers.

Where it runs: nowhere. Nothing in any crate's src/ imports ftui_runtime::slo, so nothing feeds it observations and nothing acts on a BreachResult — the runtime does not enter safe mode from this path, because no code path reaches it. The parser, the breach check and the error-budget accounting are implemented and covered by tests in ftui-harness.


Multi-Stage Conformal Monitoring

Status: experimental (see Experimental modules)

The design gives each render pipeline stage an independent conformal monitor:

view() → [Layout] → Buffer → [Diff] → Changes → [Present] → ANSI
           ↑               ↑                ↑
      stage monitor   stage monitor   stage monitor
      (calibration)   (calibration)   (calibration)

Each stage would maintain its own Mondrian-bucketed residual set, so a regression in layout computation is detected independently from diff or presenter regressions. Buckets are keyed by (screen mode, diff strategy, terminal size) and fall back to coarser groupings when data is sparse. The point of the granularity is to identify which pipeline stage is responsible for a slowdown rather than just flagging "frame was slow."

Where it runs: nowhere. StagedConformalPredictor is never constructed outside its own unit tests — nothing in any crate's src/ imports ftui_runtime::conformal_stages, and no stage timing is fed to it, so the runtime cannot currently attribute a slowdown to a stage. The bucketing, calibration and alerting logic are implemented and tested.

Not to be confused with conformal_predictor, which is on by default in the runtime and monitors whole-frame timing. That one is real; this one is the per-stage version that was never connected.


Headless Simulator

The ProgramSimulator (1,700+ lines) runs a Model without a real terminal, enabling deterministic testing:

use ftui::runtime::ProgramSimulator;

let mut sim = ProgramSimulator::new(MyModel::default());
sim.init();
sim.send(Msg::LoadData);
sim.tick();

// Capture rendered output without a terminal
let frame = sim.capture_frame(80, 24);
assert_eq!(frame.width(), 80);
assert_eq!(sim.model().items.len(), 42);
assert!(sim.is_running());

The simulator is the backbone of the shadow-run comparison system and the rollout scorecard. It powers every harness test without needing a PTY or terminal emulator.


Frame Arena Allocator

The render hot path uses a bump allocator reset at frame boundaries, eliminating per-frame allocator churn:

let mut arena = FrameArena::new(256 * 1024); // 256 KB initial

// During frame rendering: bump-allocate transient strings
let label: &str = arena.alloc_str(&format!("{done}/{total}"));
let cell_text: &str = arena.alloc_fmt(format_args!("{value:>8}"));

// At frame boundary:
arena.reset(); // O(1), no individual deallocations

Why bump allocation? The render path produces many small, short-lived allocations (styled text spans, layout rectangles, change runs). A bump allocator satisfies these in O(1) with zero fragmentation, and reset() reclaims everything in a single pointer write.


Color System

The ftui-style color module supports multiple color profiles with automatic downgrade:

ProfileColorsWhen Used
TrueColor16M (24-bit RGB)Modern terminals with COLORTERM=truecolor
Ansi256256Terminals with 256-color support
Ansi1616Basic terminals
Mono2NO_COLOR set, or dumb terminals

Color downgrade is automatic based on terminal capability detection:

TrueColor RGB(128, 0, 255)
  → Ansi256: round channels to the 6×6×6 cube; exact grays choose the closer cube/ramp candidate by weighted RGB distance
  → Ansi16: map to closest basic color
  → Mono: drop color entirely, keep bold/underline for emphasis

The module includes WCAG 2.1 contrast ratio utilities for accessibility checking:

let ratio = contrast_ratio(Rgb::new(220, 220, 220), Rgb::new(30, 30, 30));
// WCAG AA: ratio ≥ 4.5 for normal text, ≥ 3.0 for large text
// WCAG AAA: ratio ≥ 7.0 for normal text, ≥ 4.5 for large text

Perceived luminance uses the standard formula: Y = 0.2126R + 0.7152G + 0.0722B (linearized sRGB).


Evidence Sink Architecture

Every probabilistic decision in FrankenTUI is logged to a shared evidence sink, a structured JSONL stream that captures the reasoning behind runtime behavior:

let config = ProgramConfig::default()
    .with_evidence_sink(EvidenceSinkConfig::enabled_file("evidence.jsonl"));

Evidence events actually written by the runtime (grep for the "event" value):

  • diff_decision: which diff strategy was chosen and why (Beta posterior, cost estimates, fallback reason)
  • decision, decision_evidence, regime_transition, summary: the resize coalescer's apply-vs-coalesce choices with their log10 Bayes-factor ledger and steady/burst regime changes
  • budget_decision: the conformal frame-time gate (bucket, quantile, upper bound, risk) together with the degradation tier chosen before and after the frame
  • guardrail_snapshot: memory and effect-queue guardrail state whenever a guardrail fires (queue depth is the live effect backlog)
  • voi_decision: the inline-auto height sampler's value-of-information decision (posterior, gain, cost, e-value)
  • capability_decision: one row per session for truecolor, synchronized output and the scroll-region policy: environment detection, probe outcome, multiplexer veto, operator switch, and the log-odds ledger behind the final value
  • inline_strategy: which inline strategy the writer selected from those capabilities (scroll_region, hybrid, overlay_redraw)
  • fairness_config / fairness_decision: the input-fairness guard's Jain-index decisions
  • effect_queue_select: scheduler job selection when the EffectQueue executor backend is enabled
  • widget_refresh, certificate_decision, task_executor_*: widget refresh plans, presenter certificates, and executor lifecycle

Why evidence? When a frame is slow, operators can grep the JSONL for that frame index and see exactly which decisions were made and what statistical state drove them. No black boxes.


About Contributions

About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.


License

MIT License (with OpenAI/Anthropic Rider) © 2026 Jeffrey Emanuel. See LICENSE.