F
FrankenTUI
Battle Reports

War
Stories.

Building a TUI kernel in 5 days meant fighting synchronized output deadlocks, battling character collisions, and optimizing allocations into oblivion.

Critical Defects

Battle Reports

The most dangerous defects encountered and neutralized during development.

Bug Report

Terminal Sync Freeze Safety

Crash During Render Frozen Terminal

If an application panicked mid-render while the terminal was in DEC 2026 synchronized output mode, the terminal would remain frozen — requiring a manual `reset` command.

Root Cause Analysis

TerminalSession::cleanup (the RAII Drop impl) did not emit SYNC_END. The panic hook had this safety, but the destructor did not. Fixed by adding stdout.write_all(SYNC_END) to cleanup, guaranteeing unfreeze on every exit path.

Resolution ImpactTerminal never left frozen regardless of crash timing.
Bug Report

The SOH Collision

U+0001 vs Cell::CONTINUATION

A collision between the SOH control character (U+0001) and the internal `Cell::CONTINUATION` marker (which was 1) in `ftui-render/src/cell.rs`.

Root Cause Analysis

The `CONTINUATION` constant was defined as `1`, which collided with the valid ASCII Start of Heading (SOH) character. This meant SOH characters were being interpreted as wide-char placeholders. Fixed by changing `CONTINUATION` to `0x7FFF_FFFF`.

Resolution ImpactEnsured correct rendering of binary-like or control-heavy output.
Bug Report

Recursive Tree Flattening

O(N) Allocation to Zero-Alloc

Optimized Tree widget rendering by replacing the O(N) `flatten` implementation with a zero-allocation recursive visitor `render_node`.

Root Cause Analysis

The initial implementation allocated a vector of all visible nodes every frame. The fix replaced this with a recursive walker that renders directly to the buffer, respecting scissor rects and avoiding all intermediate allocations.

Resolution ImpactSignificant performance boost for large trees (file explorers).
Bug Report

GraphemePool Optimization

Memory Leak in Text Interning

Optimized `GraphemePool` garbage collection and intern logic to prevent unbounded growth.

Root Cause Analysis

The GraphemePool interns strings to `GraphemeId`s. Without proper GC or reference counting, long-running sessions would accumulate unused graphemes. The fix added a generational GC strategy to prune unused IDs.

Resolution ImpactStable memory usage for long-running dashboards.
Technical Debt

Deep Fixes

Subtle, multi-layered bugs that required understanding terminal internals and Unicode edge cases.

Bug Report

The Inline Ghosting Trilogy

Three bugs, one symptom: ghost UI

Inline mode kept leaving 'ghost' frames on screen. Fix #63 removed an unconditional clear that blanked unchanged rows. Fix #68 changed Buffer::new to initialize dirty_rows=true so fresh buffers trigger full diffs. Fix #70 invalidated prev_buffer after log writes so the renderer knew the screen had moved.

Root Cause Analysis

These three bugs interacted: #63 caused flicker by clearing too aggressively, #68 caused ghosting by not clearing enough, and #70 caused stale rendering when logs scrolled the screen. Each fix was correct individually but the full picture required all three working in concert.

Resolution ImpactEliminated all ghosting and flicker in inline mode across terminals.
Bug Report

Zero-Width Char Desync

Combining marks broke the cursor

Standalone combining marks (zero-width characters) caused the Presenter's cursor position to desynchronize from the terminal's actual cursor. Characters after a zero-width mark rendered at the wrong position.

Root Cause Analysis

emit_cell wrote bytes for zero-width chars but CellContent::width() returned 0, so the internal cursor_x didn't advance while the terminal cursor did. Fixed by replacing zero-width content with U+FFFD (replacement character, width 1) to maintain grid alignment.

Resolution ImpactCorrect rendering of Unicode combining marks and edge-case graphemes.
Bug Report

The Infinite Wrap Loop

CJK characters wider than viewport

When a single CJK character (width 2) was wider than the available wrap width (1 column), the word-wrap algorithm entered an infinite loop — no progress was ever made.

Root Cause Analysis

Both wrap_line_words and wrap_line_chars had the same vulnerability: they checked if the next grapheme fit, found it didn't, but had no fallback to force progress. Fixed by adding a forced-progress path that consumes the character even when it overflows.

Resolution ImpactPrevented hangs on narrow terminals and edge-case CJK input.
Bug Report

Input Parser DoS Protection

Malformed escape sequences swallowed input

The CSI/OSC ignore states used for DoS protection were too sticky — they continued ignoring bytes until a valid terminator appeared. A malformed sequence like `ESC [ ... 1GB of zeros` would swallow all subsequent valid input.

Root Cause Analysis

Updated process_csi_ignore, process_osc_content, and process_osc_ignore to abort on invalid control characters (bytes < 0x20). Now if a malicious sequence hits a control char like newline, the parser resets to ground state immediately.

Resolution ImpactTerminal remains responsive even when processing corrupted or adversarial input.
Bug Report

Terminal Sync Freeze Safety

Crash during render froze the terminal

If an application panicked mid-render while the terminal was in DEC 2026 synchronized output mode, the terminal would remain frozen — requiring a manual `reset` command.

Root Cause Analysis

TerminalSession::cleanup (the RAII Drop impl) did not emit SYNC_END. The panic hook had this safety, but the destructor did not. Fixed by adding stdout.write_all(SYNC_END) to cleanup, guaranteeing unfreeze on every exit path.

Resolution ImpactTerminal never left frozen regardless of crash timing.
Bug Report

Shakespeare Search: 100K Allocations

O(N) allocs per keystroke

The Shakespeare text search allocated a new String (via to_ascii_lowercase) for every line in a 100K+ line document on every keystroke, causing severe input lag during search.

Root Cause Analysis

Replaced with line_contains_ignore_case, an allocation-free helper that performs case-insensitive substring checks by comparing char-by-char. The query is lowercased once; each line is scanned without allocation. Same fix applied to Code Explorer and LogViewer.

Resolution ImpactSearch went from multi-second lag to instant (<5ms) on large documents.
Bug Report

Presenter Cost Model Overflow

Wrong cursor moves on 4K displays

The digit_count function capped at 3 for any input >= 100, causing incorrect cost estimation for terminals >= 1000 columns wide. This led to suboptimal cursor movement strategies on large displays.

Root Cause Analysis

Extended digit_count to handle 4 and 5 digit numbers (up to u16::MAX = 65535). Without this, the presenter would choose 'move cursor to column' over 'relative move' even when the relative move was cheaper on wide terminals.

Resolution ImpactOptimal ANSI byte output on 4K and ultrawide displays.
Bug Report

Ratio Constraint Identity Crisis

Ratio behaved like flex-grow

Constraint::Ratio(n, d) was implemented as a flexible weight (like CSS flex-grow) instead of a fixed fractional allocation. This made it impossible to create fixed proportional layouts like a 1/4 width sidebar.

Root Cause Analysis

Moved Ratio handling from the flexible allocation pass to the fixed allocation pass of the layout solver. It now allocates available_size * n / d, aligning behavior with Percentage and standard grid expectations.

Resolution ImpactPredictable proportional layouts that match developer intent.
Bug Report

TimeTravel Eviction Corruption

Delta frames without a base

When the TimeTravel recorder reached capacity and evicted the oldest frame, the new oldest frame could be a delta-encoded snapshot with no base frame to reconstruct against.

Root Cause Analysis

Updated record() to perform eviction before computing the new snapshot, and to force a Full snapshot if the history is empty after eviction. This guarantees get(0) always returns a self-contained reconstructable frame.

Resolution ImpactTime-travel debugging works correctly even at buffer capacity.
Bug Report

SGR Delta Cost Miscalculation

Reset-to-default cost overestimated 4x

The presenter estimated resetting a color to default (transparent) at 19 bytes (full RGB sequence cost), but the actual sequence is only 5 bytes. This caused unnecessary full style resets instead of cheaper delta updates.

Root Cause Analysis

Updated delta_est to check if the new color is transparent (alpha=0) and use 5 bytes for transparent transitions, 19 for opaque. This ensures the SGR delta engine correctly identifies when a delta update is cheaper than a full reset.

Resolution ImpactUp to 40% reduction in ANSI output bytes for typical workloads.
Runtime Invariants

Quality Guards

Hard performance targets enforced in CI. Violations are test failures, not warnings.

MetricTargetHard CapNotes
Resize → first stable present≤ 120ms (p95)≤ 250ms (p99)Drop intermediate sizes if over budget
Action resolution latency< 16ms< 16msAll keybinding actions complete within one frame
Conformal prediction alpha0.05Coverage: P(y_t ≤ U_t) ≥ 95% within each bucket
Dirty-span overhead (dense)< 2%< 5%Overhead of dirty-span tracking vs full scan
Dirty-span improvement (sparse)> 50%Scan cost reduction for ≤ 5% edit density
Hardware Alignment

Efficiency Gains

Key architectural decisions that ensure FrankenTUI runs at 60 FPS even on legacy hardware.

0 allocs/frame

Zero-Alloc Diffing

The diff algorithm compares buffers without allocating new vectors, reusing `ChangeRun` structs.

1 cycle/cell

SIMD Cell Comparison

Cells are exactly 16 bytes, allowing single 128-bit SIMD comparison for equality checks.

O(1) layout

Cached Text Measurement

WidthCache memoizes text measurements, skipping expensive grapheme segmentation on repeated frames.

Sub-ms diffs

Dirty Row Tracking

Widgets mark specific rows as dirty, allowing the renderer to skip diffing static regions.

Neutron-Grade Code.

Explore the repository to see the fixes and optimizations in their full technical context. Built for correctness from the ground up.