ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
User Interface

Terminal presentation, input routing, widgets, and interactive screens.

Terminal presentation, input routing, widgets, and interactive screens.

Keyboard input

Migrated from docs/keyboard-input.md into the rendered Doxygen topic set.

Keyboard input

Native keyboard input has four boundaries:

  • lib/platform/{posix,windows}/keyboard.c owns terminal/console access and restoration.
  • lib/platform/keyboard.c frames terminal escape sequences and replies, buffers fragmented text and delegates UTF-8 decoding, scalar validation, and encoding to utf8proc. Windows UTF-16 surrogate pairs become the same normalized keys; console repeats apply to complete characters.
  • lib/tui/input.c collects bracketed paste and queues decoded events in order. The media screen supplies the policy for recognizing dropped path/URL bursts. Paste bytes are validated before insertion and never execute shortcuts.
  • lib/ui/input.c edits caller-owned UTF-8 storage without terminal I/O or rendering. Prompts and log search share this implementation; callers decide when to submit, cancel, mask, or insert newlines. lib/tui/input.c retains screen ownership and modal routing, and shortcuts.c owns shortcut aliases/help.

Text and control keys use their Unicode code points directly. Special keys start at 0x110000, above the Unicode range, so no text tagging is needed. The browser renderer uses the same representation, including supplementary characters. Bracketed POSIX paste payloads remain bytes until whole-paste validation. Malformed and truncated typed sequences cannot mutate a field; new starters or special keys discard incomplete text. Buffer exhaustion rejects a whole inserted character or paste without truncating it.

Editing operates on Unicode code points. Combining marks and emoji joiners remain valid UTF-8 but are separate editing units; this is not grapheme-cluster editing.

Validation

The normal Criterion suites exercise the production library with the configured sanitizers: platform keyboard text/editing, UI text editing/event collection, terminal replies, log search, and UTF-8 utilities. The password PTY regression also types and deletes accented characters, CJK, and supplementary emoji.

On a configured Linux/macOS Clang build with Criterion and pkg-config, run:

uv run --frozen python tests/scripts/keyboard_text_coverage.py --build build

This runs the same exhaustive Criterion tests for every non-NUL Unicode scalar, malformed UTF-8, fragmented input, and UTF-16 recovery. It requires 100% line and branch coverage of the three keyboard_text_* Unicode functions in lib/platform/keyboard.c and writes its counters, gcov listing, and summary under the sibling build-keyboard-text-coverage/ directory. The focused measurement excludes sanitizer-generated branches and vendored utf8proc internals; the ordinary integrated tests retain sanitizers. This percentage does not describe all Unicode utilities or the whole application.

Keyboard Shortcuts

The help overlay is generated from lib/tui/shortcuts.c, so this page mirrors the application-level bindings without pretending that every modal screen accepts every key. Press ? for the same list at runtime.

Global TUI bindings

Key Action Availability
? Toggle the current help screen Interactive screens
] Open the live log viewer/searcher Interactive screens
= Open runtime statistics Native builds
0 Open sync-primitives diagnostics Debug builds
` Open errno-stack diagnostics Diagnostic UI builds
F12 Open the version update checker Native builds
o, O, Ctrl-O Open media from a path or URL Client/media screens
q / Esc Quit ascii-chat normally Interactive screens
Shift-q / Shift-Esc Panic exit without cleanup Interactive screens

Media controls

Key Action
Space Pause or resume media
Left / Right Seek backward or forward when supported
Up / Down Increase or decrease speaker volume
m Toggle mute
c Cycle color mode
r Cycle render mode
f Cycle color filter
x / y Toggle horizontal / vertical flip
1 Toggle Matrix rain
2 Toggle audio waveform
3 Toggle audio frequency display
- Toggle FPS counter
w Toggle face follow
z Toggle Zalgo intensity

Audio visualizations require audio to be enabled. Pressing 2 or 3 selects that visualization and disables the other visualization and Matrix rain.

Prompt and navigation keys

Text prompts support printable Unicode input, Backspace, Delete, Left/Right, Home/End, Enter to accept, and Escape or Ctrl-C to cancel. Shift-Enter inserts a newline in multiline prompts. Bracketed paste is inserted as one validated value and cannot trigger application shortcuts.

Log, sync, notice, and selection screens add local navigation such as Up/Down, Page Up/Page Down, Home/End, and Escape. Their live help overlay is authoritative when a local binding differs from the media screen.

Build-dependent keys

The sync inspector is omitted from release builds, errno-stack diagnostics require ASCIICHAT_DIAGNOSTIC_UI, and native-only controls are filtered from browser help. This is why runtime help is generated rather than copied into a static table.

See also
topic_tui
topic_keyboard
md_docs_2keyboard-input.html

Keymask public-key art

Migrated from docs/keymask.md into the rendered Doxygen topic set.

Keymask public-key art

Keymask is ascii-chat's deterministic, monochrome public-key emblem. It supplements the full SHA-256 fingerprint and existing authentication checks. A similar-looking picture is not proof that two keys match.

Art appears automatically on interactive stderr terminals outside snapshot mode, when the terminal has room. Redirected output keeps the textual fingerprint. Art uses the terminal's default foreground, adapting to light and dark themes; warning text retains its severity color. Quiet, grep, and JSON output retain their existing notice policies.

Stable v2 mapping

  1. Compute SHA-256 of the 32 public-key bytes. This remains the full fingerprint.
  2. For each feature, compute SHA-256 of the UTF-8 domain ascii-chat/keymask/v2, a NUL byte, the 32-byte fingerprint, and the ASCII feature name: outline, crown, eyes, center, mouth, or markings (no trailing NUL).
  3. Select one of eight silhouette profiles, eight crowns, eight eye pairs, six noses, and eight mouths using the first byte of the corresponding feature hash modulo the template count. Outline byte 1 selects single/double edges.
  4. Stamp features into a 32 by 16 canvas. Cheek details use markings bytes 7-10; bytes 0-1 select the position and side of an asymmetric stripe. Template tables and placement rules in keymask.c define the versioned mapping.
  5. Render ASCII literally; Unicode replaces only # with ░. Surround the canvas with a 34-column, 18-row border and display Keymask v2 / SHA-256 followed by the unchanged complete fingerprint.

V2 prioritizes recognizable shapes and empty space over random texture. It does not preserve all 256 digest bits in its output. Exact visual collisions and perceptual lookalikes are possible, including through deliberate key searches; compare the full fingerprint for exact identity verification. V1 artwork changes under v2, but saved trust records and fingerprints do not change.

SSH, raw X25519, and GPG Ed25519 public-key objects use the same mapping. Identical public bytes produce identical art regardless of file, agent, or network source. The algorithm label distinguishes Ed25519 from X25519; GPG metadata is not part of the hash. The fingerprint convention is ascii-chat's existing SHA-256 of raw key bytes, not OpenSSH's hash of its serialized SSH key blob.

Display behavior

  • Servers announce their configured public identities at startup; discovery-service announces its public identity too.
  • Clients show the presented server identity after the existing key checks. The label explicitly says unverified if signature verification was not enabled.
  • Servers show a client's public identity after handshake completion, labeled with its connection ID and authenticated/unverified status. Anonymous clients have no persistent key art.
  • Unknown-host prompts include art when it fits. Short terminals (fewer than 38 rows) keep just the fingerprint so the question and input remain accessible.
  • Changed-host warnings show received and stored identities. When several stored keys exist, the first nonzero candidate is labeled as such. Missing stored identities are described as unavailable, never visualized as a zero key.
  • If the available notice content width is below 34 columns, only the fingerprint is shown. The existing UI controller handles later terminal resizes and prompt minimum dimensions.

Trust decisions, known-host file format, and network packets are unchanged.

Implementation and verification

lib/crypto/key_identity/keymask.c contains the allocation-free mask renderer and shared public-key digest function. display.c formats identity cards and applies terminal/notice policy. Public headers mirror that directory under include/ascii-chat/crypto/key_identity/.

Build using the default CMake preset, then run against the compiled shared library:

uv run --frozen python tests/integration/keymask.py --library build/bin/asciichat.dll --terminal --binary build/bin/ascii-chat.exe

Use the corresponding .so/.dylib path on other platforms. The core checks use only Python's standard library. --terminal additionally requires pyte and pywinpty on Windows, or pexpect on POSIX. --contact-sheet output.png uses Pillow to render 36 deterministic examples from the native renderer.

The tests cover a fixed v2 golden vector, all 256 single-bit input mutations, ASCII/UTF-8 equivalence, buffer boundaries, SHA-256 parity, public-key imports, narrow layouts, stored-key comparisons, noninteractive rejection, and real PTY/ConPTY notices and trust prompts. --binary additionally exercises a real loopback server/client authenticated snapshot with temporary SSH keys (requires ssh-keygen), checking both identity cards and that snapshot stdout stays clean. They use temporary fixtures and isolate the user config directory.

Example contact sheet

These 36 synthetic digest fixtures are rendered by the compiled C implementation.

36 Keymask v2 examples

Shared session TUI

Migrated from docs/session-tui.md into the rendered Doxygen topic set.

Shared session TUI

The native terminal and browser use the C views and key handling in include/ascii-chat/tui/session.h. The browser does not implement its own splash, help layout, status/log feed, or settings-key switch in React.

session_splash.c renders the existing rainbow logo and connection header into a frame buffer. session_help.c renders the keyboard/settings screen without a native display context. session_input.c changes the common C options and returns host actions for pause, seek, close, and help. Native session/keyboard_handler.c executes those actions against native capture/audio resources; the browser adapter executes them against HTML media and Web Audio. Keyboard settings are read back into the React controls and URL state.

Both hosts present through ui_session_render(). It owns screen transitions, resize recovery, minimum-size coverage, redraw decisions, refresh deadlines, and the splash animation clock. Both hosts select screens using the common priority order through ui_session_select_screen().

ui/controller.c is the native adapter: it retains immutable snapshots and terminal descriptors, synchronizes producers with the presentation thread, and supplies a borrowed view to the shared runtime. Existing screen callbacks write into the runtime's frame buffer; the adapter then writes the completed frame to the terminal. This includes native status, help, media, progress, notices, and prompts. Native terminal FPS/write instrumentation and the independent deadlock-debug mailbox stay in the adapter. Batch output keeps its synchronous semantics.

The native splash monitor handles startup completion, connection metadata, and keyboard/log-search input. It no longer advances a separate animation or publishes every animation frame: the shared runtime supplies the current animation sample when rendering its header.

The browser owns a ui_session_t per canvas. ui_session_tick() advances the same C animation engine and returns a complete ANSI frame without sleeping or writing to a terminal. JavaScript supplies monotonic timestamps, dimensions, media frames, and focused canvas key events. Help covers splash, and splash covers media. Incoming media is retained while covered, so closing help restores the latest frame. Frames go through the existing libvterm/FreeType canvas renderer. Resize and renderer recreation invalidate presentation without resetting options.

The browser's ui_session_tick() assembles its retained media/splash/help view and calls that same compositor. Rendering a native view is serialized by its host; its snapshot and callback context must remain alive through the call. Neither host holds producer locks while running screen callbacks.

The C options registry and logger are scoped to a WASM module, not to each canvas. The logger owns the bounded history from initialization through shutdown. Status, and client overlays borrow it, so closing a screen does not stop capture or erase earlier messages. Native-only debug/statistics and blocking prompt workflows are not exposed as browser controls.

The WASM build includes the real session log buffer rather than its former no-op implementation. Its provenance hash includes src/common/session as well as the shared library, headers, and web adapter. Rebuild and publish both WASM modules after changing these sources.

web/web/tests/e2e/session-ui.spec.ts exercises the actual compiled C runtime and canvas: animated splash pixels, keyboard/settings synchronization, overlay ownership over video, resize recovery, latest-frame restoration, bounds checking, and repeated teardown. Run it with the web app's Playwright configuration and fresh WASM artifacts.

Server status UI in client modes

] toggles the existing server status UI over client, discovery-client, and mirror media. Client, discovery, and mirror startup show the animated rainbow splash without logs; only server CLI modes show logs by default. Logs remain captured while hidden and appear only after ]. ? switches to keyboard help; ] switches back. Escape cancels a search first, then closes the overlay. Covered media continues and its newest frame is restored on close. The keyboard help lists the shortcut. The live session accepts q or Escape for the normal coordinated shutdown path (equivalent to Ctrl+C). Shift-Q or Shift-Escape performs immediate process termination without cleanup (equivalent to a second Ctrl+C during shutdown) and should be reserved for a hung or otherwise non-responsive shutdown. Panic-exit keys are handled before modal input dispatch, so they remain available while help, logs, prompts, notices, or diagnostic TUIs are open.

session_status.c contains the server's four-line bordered header and the shared terminal_screen_compose() implementation, extracted from ui/status.c and ui/terminal_screen.c. Both server modes, native client overlays, and WASM call these same functions. Client headers show mode/uptime and controls in place of server listener metadata. Colors, ANSI-aware wrapping, latest-log selection, and /pattern/flags filtering/highlighting all use the existing C implementations. The native adapter supplies terminal I/O; the web adapter feeds the resulting ANSI frame to its existing canvas renderer.

All emitted C application log routes retain history, including console-disabled, file-only, JSON, and mmap output. Configured severity still determines which messages are emitted. The history holds the latest 8,192 lines; the view displays the newest lines that fit. Emergency signal-handler output remains direct because it cannot safely acquire a history mutex. Search uses PCRE2's portable matching entry point, including when JIT is unavailable in WASM. Decoded search-edit keys use the same C line editor on Windows, POSIX, and in the browser.

tests/python/tests/integration/terminal_logs/test_logs.py exercises mirror and TCP client modes through real terminals and captures their cell grids. tests/python/tests/native/test_isolated_shared.py checks retained C log routes with console output disabled. The browser suite also checks real C logs, search, overlay exclusivity, resize, and connected-client logs; set SESSION_UI_SERVER_URL to a local server's WebSocket URL for its network case.

The browser FPS counter counts successful canvas paints over the last second for all views: splash, help, logs, and media. Only actual canvas paints contribute; a view settles to zero when it needs no repaint. The displayed target is the configured video FPS, not a forced redraw rate for static overlays.

Shared ASCII terminal emission

Migrated from docs/simd-emission.md into the rendered Doxygen topic set.

Shared ASCII terminal emission

Each ISA owns its image and half-block processing loops in its color.c and halfblock.c. Monochrome uses that ISA's same pixel pipeline. Shared output allocation, terminal state, and ANSI emission live in lib/video/ascii/render.c. The normal ISA entry points do not call a shared scalar renderer. SSE2, SSSE3, AVX2, NEON, and SVE each supply vector stages for luminance, RGB packing, palette lookup, exact indexed-color quantization, run comparison, and half-block classification. Runtime selection keeps the CPU/OS safety checks; incomplete fixed-width vector tails use the scalar stages. ISA types do not cross the shared output boundary in simd/cells.h.

The original SSE2/SSSE3 two-half arithmetic, AVX2 32-pixel helpers, NEON 16-pixel structure loads and widening arithmetic, and SVE channel/widening processing are retained in each ISA's luminance.c and used by its row loop. The SVE active count is bounded by halfword lanes before widening. NEON retains vector UTF-8 compaction with st1, st2, st3, and st4 stores for uniform glyph lengths; mixed lengths compact the gathered byte streams in order. Duplicate per-ISA ANSI decimal/escape builders are replaced by the shared encoders.

The architecture mono.c, color.c, and halfblock.c files also instantiate their vector implementations from simd/lookup.c, simd/quantize.c, and simd/pack.c, with per-file ISA flags and no unity/PCH mixing. The shared math expresses one color contract using vector operations; it does not call the scalar renderer for complete SIMD blocks. The retained NEON table lookup uses tbl, NEON half-block input uses interleaved ld3, x86 uses byte shuffles/gathers where available, and SVE uses predicated scalable loads and comparisons.

SSE2 has no byte shuffle or gather: palette lookup uses parallel comparisons against ranges of equal UTF-8 keys. SSSE3 uses byte-shuffle tables, AVX2 uses 32-bit gathers, NEON uses four 64-entry byte tables, and SVE uses predicated gather loads. Color quantization computes exact palette distances in parallel, including lower-index tie breaking. Floyd-Steinberg error diffusion remains sequential because each pixel changes subsequent pixels; packing, palette lookup, and run comparison still use the selected SIMD backend in that mode.

Cell and row contract

  • One source pixel produces one palette glyph. Palette lookup uses rounded BT.601 luminance, the 6-bit bucket luminance >> 2, and cache64. A glyph is copied as a complete UTF-8 sequence; repeated glyphs compare their bytes, not their luminance bucket or their first byte.
  • Monochrome, ANSI 16-color, xterm 256-color, and truecolor share terminal state and row semantics. NEON monochrome retains vector UTF-8 compaction; other paths use the common cell/run writer. Dithered 16-color quantization still processes every pixel in source order, even within a run.
  • Foreground mode colors the glyph. Background mode colors the background and selects contrasting black or white text. Indexed modes use the shared terminal palette quantizers; architecture-specific approximate palettes cannot change the displayed colors.
  • Runs compare the glyph and both colors. REP is used only when shorter than literal UTF-8 output. Runs stop at row boundaries. Colored rows reset SGR before a newline, with no trailing newline after the final row. Monochrome output emits no SGR. No erase-to-EOL sequence inherits an active background.
  • Dynamic buffer growth checks arithmetic and handles even a one-byte initial capacity. The shared renderer reserves a checked worst-case capacity including UTF-8, both colors, row endings, and the terminator.
  • Every backend has truecolor, 256-color, 16-color, and monochrome half-block entry points. The row driver invokes that backend's vector RGB packing, indexed-color quantization, transparency/shade classification, and run comparison stages. The capability-aware application path selects them at runtime for every color mode. Scalar-named half-block functions explicitly select the scalar reference, rather than hiding a runtime SIMD selection. Odd heights duplicate the final source row. All-black colored padding resets colors and emits spaces; monochrome keeps the existing dark-pixel threshold.
  • Invalid images, empty palettes, and missing kernels return NULL with error context. Returned buffers belong to the caller and use SAFE_FREE.

lib/video/ascii/sgr.c owns truecolor and 256-color sequence encoding. append_truecolor_* and append_256color_* remain compatibility entry points into those encoders. Indexed SGR caches are thread-local, so concurrent first use does not publish partially initialized sequences.

Verification

The Criterion fixture tests/unit/video/ascii_emission_test.c also runs through the isolated Linux target. Only process diagnostics and the debug name registry are replaced in that target; rendering, palette construction, quantization, allocation, lifecycle, and locks use production sources.

cmake -S tests/simd -B build-emission -G Ninja -DCMAKE_C_COMPILER=clang \
-DCMAKE_BUILD_TYPE=Release -DTEST_EMISSION=ON
cmake --build build-emission --parallel 4
ctest --test-dir build-emission --output-on-failure

For memory safety, configure another build with -DCMAKE_C_FLAGS=-fsanitize=address,undefined. The fixture independently decodes SGR, REP, UTF-8, and row boundaries and checks the resulting cells. It explicitly calls every CPU-supported backend, including lower-priority x86 variants. Cases cover widths 1 through 257 around vector boundaries, three-row images, noisy colors, long runs, alternating colors, ASCII and Unicode palettes, foreground and background modes, 16-color dithering, every half-block color mode, odd heights, padded strides, encoder size boundaries, and buffer growth from one byte. Stage tests independently compare every backend's packing, lookup, quantization, and run callbacks against scalar results for every tail length through 257 and a Cartesian set of palette boundaries.

Local validation on 2026-10-18:

  • Linux x86-64: dispatch, forced-scalar, invalid-override, and emission tests pass; the emission fixture also passes AddressSanitizer and UndefinedBehaviorSanitizer.
  • Windows x64: all three standalone dispatch checks pass with Clang; the changed shared and x86 renderer sources pass native Clang syntax checking.
  • ARM64 under QEMU 6.2: NEON and SVE cell equivalence passes at SVE vector lengths 16, 32, 64, 128, and 256 bytes; a cortex-a53 run checks NEON without SVE.
  • Instruction audits find vector instructions in packing, lookup, quantization, run comparison, and half-block classification for all five backends. Repeat with uv run --frozen python tests/simd/audit_cells.py build-emission/emission-test avx2; for ARM pass --objdump aarch64-linux-gnu-objdump and backend neon or sve. Sample instructions for every stage are recorded in instructions-x86.txt and instructions-arm.txt alongside the CSV reports.
  • Physical ARM hardware performance is not measured here; emulator throughput is not a hardware benchmark.

The full application configure/build attempts were stopped while rebuilding unrelated FFmpeg (Windows) and OpenSSL (Linux) dependencies; no complete application build or media snapshot run is claimed. The isolated targets compile and link all production code involved in this refactor.

Before/after measurements

Raw CSVs are in tests/simd/results/issue503/. The baseline is c0b985af819da5b8238d16e733629727d68902a5. Measurements use Clang 14 -O3, 161x61 RGB images, 120 frames, ASCII palettes, truecolor and 256-color foreground, and two patterns (40-pixel runs and changing colors). The host is an AMD Ryzen 7 5800X through WSL2. ARM measurements use QEMU 6.2 -cpu max with its default SVE vector length, 64 bytes. Emulation and host scheduling make these single-run timings approximate; they are not stable regression thresholds. Allocation counts include malloc/calloc/realloc calls after warming caches. SIMD is not uniformly faster for these small frames: indexed-color paths benefit from vector quantization, while some small/truecolor paths pay extra preparation and dispatch costs. The raw results include those slower cases. The halfblock-x86.csv and halfblock-arm.csv reports compare all four half-block modes across every supported backend using the same workload. Build and run halfblock-benchmark to repeat those measurements. The prior half-block renderer had only a NEON-specific truecolor entry point; the other ISA/color-mode combinations are new.

The new renderer uses two allocations per frame (output and vector row scratch) for every backend. The scratch allocation holds packed pixels and prepared cell columns for the vector stages. The former SIMD implementations sometimes used one allocation, while others grew their output buffers. The shared path uses a checked output capacity and one reusable row scratch allocation.

The baseline cannot supply valid performance comparisons for every backend:

  • SSE2 and SSSE3 advance 16 source pixels after emitting eight. Their changing- color frames contain roughly half the required output. Their reported speed therefore measures incomplete frames.
  • AVX2 aborts with heap corruption in this workload before producing timing rows.
  • NEON's old color implementation does not compile when included: its helper declaration disagrees with its call, and the normal build omitted the NEON mono/color files. The baseline ARM benchmark disables NEON to measure SVE.
  • SVE truecolor reports only 9,881 bytes even for the changing-color image, versus 184,057 bytes from the complete shared renderer. These outputs also cannot support a valid speed comparison.

The restored ISA-owned pipelines pass the new cell tests for all of those variants. The raw baseline numbers are retained as evidence of the old workloads, not as equivalent-rendering speedups. Scalar timings are comparable as rendering workloads, while Unicode lookup and row-reset semantics are now normalized across all paths.

To reproduce a baseline without changing the checkout:

uv run --frozen python tests/simd/export_emission_baseline.py c0b985af819da5b8238d16e733629727d68902a5 /tmp/emission-before
cmake -S tests/simd -B build-emission-before -G Ninja -DCMAKE_C_COMPILER=clang \
-DCMAKE_BUILD_TYPE=Release -DTEST_EMISSION=ON \
-DEMISSION_BENCHMARK_BASELINE=/tmp/emission-before
cmake --build build-emission-before --target emission-benchmark
ASCII_CHAT_SIMD=scalar build-emission-before/emission-benchmark
ASCII_CHAT_SIMD=scalar build-emission/emission-benchmark

The benchmark accepts an optional backend name to isolate a failing baseline. For ARM cross-compilation, configure with -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=aarch64 -DCMAKE_C_COMPILER_TARGET=aarch64-linux-gnu -DCMAKE_C_FLAGS=--gcc-toolchain=/usr. The old NEON benchmark also requires -DASCIICHAT_CAN_COMPILE_NEON=OFF because of the baseline compile failure. Run emulated tests at an explicit SVE length with qemu-aarch64 -L /usr/aarch64-linux-gnu -cpu max build-emission-arm/emission-test 16.

Important terminal notices

Migrated from docs/terminal-notices.md into the rendered Doxygen topic set.

Important terminal notices

Issue #255 uses one layout for important printed messages and interactive prompts.

Severity Terminal color Use
UI_NOTICE_INFO White Session strings and join instructions
UI_NOTICE_WARNING Yellow Missing credentials, unavailable features, overwrite confirmations, actionable configuration errors
UI_NOTICE_DANGER Red Identity changes, failed authentication, disabled encryption, privacy exposure
UI_NOTICE_FATAL Purple (ANSI magenta) Fatal errors and their immediate context

NOTICE(DANGER, "SERVER IDENTITY CHANGED", "s", details) records an ordinary log message and presents the complete title and body in a box. Do not include borders or ANSI escapes in the supplied text. Use a literal format string when including remote messages. The renderer makes terminal control characters visible instead of executing them. Fingerprints, paths, and commands wrap without ellipses.

ui_notice_confirm(severity, text, timeout_seconds) uses the existing yes/no prompt policy with a No default. This is presentation only: changing a notice must not change host trust, authentication, timeout, or persistence decisions. Ordinary text/password prompts also use the shared layout; only their existing display-safe, masked text reaches the renderer.

The layout uses physical terminal columns, reserves the last column against autowrap, and caps boxes at 80 columns. It falls back to ASCII borders when UTF-8 is unavailable and to plain wrapping in exceptionally narrow terminals. Color follows terminal capabilities, --color, --color-mode, and --strip-ansi. Redirected output has no cursor movement. Terminal boxes do not decorate file or JSON message bodies; existing log header formatting is retained. Log levels, quiet mode, and grep filtering still apply to logged notices. Grep matches the uncolored log template, including severity, source file, function, and the notice body; box decorations are not part of the filter input. Session invitations use NOTICE_ANNOUNCE to remain visible at every log level; quiet mode, grep filtering, and JSON output still apply to these announcements.

During live presentation, notices queue above ordinary screens and below input prompts. Long notices advance a page every ten seconds; resizing restarts the reading interval. Enter/Escape dismiss a notice through the normal input loop; quit remains available. Prompt input is never interpreted as notice dismissal. The display queue holds up to 64 notices; overflow stays in the log and produces a rate-limited diagnostic. Pending notices are printed in full on controller teardown. Fatal notices retire the active screens and print synchronously. No new confirmation is required for ordinary notices. Security prompts retain their original deadlines.

Coverage checklist

Family Covered entry points
Unknown host prompt_unknown_host: fingerprint prompt, noninteractive refusal, explicit/debug verification bypass
Changed host key display_mitm_warning: received/expected fingerprints, known-hosts path, recovery guidance, rejection
Missing/removed identity prompt_unknown_host_no_identity, check_known_host_no_identity, handshake client/server: unverified peer and identity downgrade
Discovery lookup Expected host-key mismatch rejects the lookup result; insecure lookup without a pinned host key warns about MITM risk
ACDS trust discovery_keys_verify_change: old/new fingerprints, operator verification, default-No confirmation and noninteractive refusal
Verification failure Handshake client: invalid server signature, browser-pinned key mismatch, configured server-key mismatch, invalid HMAC
Authentication rejection Handshake reason flags and client protocol: incorrect/missing password, missing/rejected client key, invalid signature
Credentials and persistence Client identity loading, private-key file permissions, known-hosts write/flush failures, discovery identity fallback
Encryption Client/server --no-encrypt, encryption mode mismatch, server encryption unavailable, received encryption-policy violation
Privacy Public IP disclosure confirmation/result; discovery disabled when security was not configured
TLS downgrade WebSocket certificate/key configuration falling back to plain WS
Session information LAN-only and global session strings and join commands
Operational failures Invalid startup options, no discovered servers, connection failure, camera/media failure, audio unavailable, discovery/mirror failure
Destructive local actions Config, completions, and man-page overwrite prompts use yellow warnings with their existing No default
Fatal failures FATAL context summary and explicit fatal notices use purple boxes; ordinary log_fatal records do not retire screens

Routine packet diagnostics, per-frame errors, backtraces, and debug reports remain logs. Existing specialized help/update screens retain their own layouts. WASM uses textual console logging and its existing prompt implementation.

Verification

Build with the default CMake preset, then run:

uv run --frozen python tests/integration/terminal_notices.py --library build/bin/asciichat.dll
uv run --frozen python tests/integration/terminal_prompt.py --library build/bin/asciichat.dll
uv run --frozen python tests/integration/prompt_timeouts.py --library build/bin/asciichat.dll

The notice suite also accepts a Linux shared-library path. It checks Unicode and ASCII layouts, seven widths, all four color mappings, complete long fingerprints, control-character escaping, multiline log retention, grep/JSON behavior, changed-key rejection, real unknown-host/ACDS prompts, default-No behavior after resizing, queued live notices, and fatal screen teardown. Regression cases cover non-terminating fatal-level logging, invitations at WARN/ERROR/FATAL levels, announcement quiet/grep/JSON behavior, restored terminal output, and flushing a queued warning before forced exit. The PTY assertions inspect the rendered terminal cells, not just log output. The existing prompt suites cover masked input, discarded covered input, absolute timeouts, noninteractive refusal, and automated responses.

Terminal presentation

Migrated from docs/terminal-ui.md into the rendered Doxygen topic set.

Terminal presentation

lib/tui/controller.c owns terminal rendering only. It never reads keys, queues input, edits answers, or interprets commands. Capture, encoding, networking, and status producers publish screen snapshots; they do not decide when to show the terminal-size warning.

Each screen supplies an output descriptor, its minimum rows/columns, a renderer, and a snapshot. ui_controller_submit() copies the snapshot. Renderers receive physical dimensions from the output descriptor, independent of media dimension overrides. Screens with pointer members must remove their slot before destroying the pointed-to objects. Removal waits for any in-progress rendering to finish.

The controller selects the highest-priority registered screen: authentication/text prompt, update prompt, mDNS selection, help, splash, media, then status. It retains covered screens and the latest media frame. A 16 ms tick detects resizing even when no producer has a new frame. Below the active screen's minimum it presents tui/too_small.h; recovery restores the underlying screen. Interrupts still terminate the process while the warning is visible.

Media uses the same 20-column and 10-row minimum as the option registry. Help's height comes from the rows built by its layout, including conditional controls. Splash and status provide their header geometry. Paused media retains its last frame; if that frame is larger than the resized terminal, presentation clips it without resuming capture.

lib/tui/input.c owns keyboard polling, covered-input suppression, and prompt editing. It reads the controller's presentation state, then lets the appropriate consumer handle the key. It has no presentation thread or rendering callbacks. The media consumer also handles help keys. Prompt editing publishes only visible text; passwords are masked before publication and raw key bytes are not logged. Authentication warnings and fingerprints are part of the prompt snapshot so they survive resize and cannot be covered by the splash or media screen.

All session and UI writes (including ASCII writer compatibility calls, grep, logging, prompt text, and final upgrade instructions) use the controller output sink. A callback may write its selected screen; unrelated terminal text cannot paint over an active screen. ui_controller_finish() retires the live screens and writes final text synchronously. The obsolete standalone FPS writer is gone; FPS is part of the media snapshot. CLI document generation, files, and emergency crash output are not live screen rendering.

Screen callbacks run outside the controller mutex and may submit or remove screens. Producers must not hold locks needed by a callback while removing its screen. The stop, blocked, live-screen, and lifecycle states use custom atomics and named registry entries. Other controller state is mutex-protected.

Non-terminal output and finite snapshots use synchronous output, without the interactive warning. Recording happens before terminal presentation and is not suppressed by the warning. Shutdown joins the presentation thread before keyboard, log, option, and named-registry teardown.

Interactive presentation takes ownership of the terminal's alternate screen when the controller starts. The alternate screen begins at the home position with the cursor hidden and has no application scrollback, so live TUI and ASCII frames cannot be scrolled independently of the screen. Controller shutdown restores the normal screen, cursor, and terminal attributes; redirected output and snapshot mode do not enter the alternate screen.

Audio visualizations

Keyboard help places Current Settings and Animations to the right of Navigation & Control. Its width is measured from both columns and its height from the longer column; undersized terminals show the shared size warning.

The keyboard help menu's Animations section lists 2 (audio waveform) and 3 (audio frequencies/FFT), with live on/off indicators. Either key selects its visualization and disables the other and Matrix rain. Matrix (1) disables both audio visualizations. Pressing the active animation key again restores video. Both audio visualizations follow --audio-source and require audio to be enabled. Mirror mode initializes microphone capture even without a media audio track or recording, so keyboard activation has an audio source available.

tests/integration/audio_visualization_keys.py --binary build/bin/ascii-chat generates a changing audio signal and checks real terminal output after keyboard activation, help state, mutual exclusion, both off toggles, and video restoration. It uses the same terminal dependencies as terminal_ui.py, plus ffmpeg and an audio output device. Playback volume is zero during the test.

Sync diagnostics (Debug builds)

Press 0 to replace media or help with the sync screen; 0 or Escape restores what was underneath. ? switches to help. Matrix rain uses 1. Left/Right pages through every registered mutex, rwlock, condition variable, wrapped atomic, and atomic pointer. Up/Down selects a row for its address, owner/waiter, source location, and last-operation age. Source locations start at lib/, src/, or include/ and use forward slashes; media paths are omitted from the details. Home/End selects the first/last page. Higher-priority prompts still cover diagnostics. The list repaginates on live resize. The Name column fits the longest registered primitive name across all pages. The minimum width is measured from the rendered header, columns, details, and footer; the minimum height fits those lines plus one entry. The shared "Terminal too small" screen appears below that size; resizing back restores the list.

The table shows lock/unlock rates (wait/signal rates for conditions), atomic values, and actual value changes per second. Repeated stores of the same value, failed compare/exchanges, and zero-delta fetches do not count as changes. Rates are red at 60 or more, yellow at 10 or more, green when nonzero, and blue at zero.

lib/tui/sync.c collects protected copies on its own thread using try-locks. The presentation thread renders a separate owned mailbox; terminal output never holds a mailbox lock. Input continues polling independently of the main loop. Registry contention retains the last complete sample and displays its growing age. The registry snapshot grows dynamically without a 256-entry limit. Mutex wait cycles use the existing stack graph detector; its bounded thread, stack, and cycle capacities are reported when reached. An idle condition wait is shown as a wait, not proof of a deadlock. These are sampled diagnostics, not an atomic snapshot of the entire process.

Verification

tests/python/tests/integration/terminal_ui/test_ui.py drives the real executable in a resizable PTY (ConPTY on Windows), interprets its output using pyte, and checks startup-small, resize recovery, a live WebSocket handshake while covered, ignored keyboard input, help restoration, paused playback, explicit dimensions, recording while covered, interrupt shutdown, and redirected snapshots. It requires pyte, pywinpty on Windows or pexpect on POSIX, and ffmpeg.

uv run --frozen python -m pytest tests/python/tests/integration/terminal_ui --ascii-chat-binary build/bin/ascii-chat --debug-sync
cmake --build build --target test-sync-regressions
uv run --frozen python -m pytest tests/python/tests/integration/sync --sync-probe build/bin/test-sync-regressions

The Criterion ui_too_small suite checks exact boundaries and the 1x1/unknown-size formatting cases. Criterion runs on supported POSIX hosts; the PTY suite also runs on Windows.

tests/integration/terminal_prompt.py --library build/bin/asciichat.dll drives native Windows prompt APIs over a live media screen and checks text editing, password masking, covered input, resize restoration, visible host fingerprints, and absence of plaintext passwords in terminal output and logs.

Docker and tmux matrix

The tmux_*.py drivers run the native executable in detached tmux PTYs and save plain/ANSI pane captures, exit status, application logs, and sanitizer reports. They require Python 3, tmux, ffmpeg/ffprobe, and ssh-keygen inside the Linux test container. Use a private tmux socket; these tests use ascii390.

tmux -L ascii390 new-session -d -s setup -x 80 -y 40
tmux -L ascii390 set-option -g remain-on-exit on
mkdir -p /tmp/render-evidence
ffmpeg -y -v error -f lavfi -i testsrc2=size=160x120:rate=10 \
-f lavfi -i sine=frequency=440:sample_rate=48000 -t 10 \
-c:v libx264 -pix_fmt yuv420p -c:a aac /tmp/render-evidence/media.mp4
uv run --frozen python tests/integration/tmux_rendering.py --binary build/bin/ascii-chat \
--artifacts /tmp/render-evidence
uv run --frozen python tests/integration/tmux_flows.py --binary build/bin/ascii-chat \
--artifacts /tmp/render-evidence
uv run --frozen python tests/integration/tmux_auth.py --binary build/bin/ascii-chat \
--artifacts /tmp/render-evidence
uv run --frozen python tests/integration/tmux_exports.py --binary build/bin/ascii-chat \
--artifacts /tmp/render-evidence

Build the native prompt fixture with the same compiler, library and sanitizer settings as the application:

cmake --build build --target ui-native-probe
uv run --frozen python tests/integration/tmux_prompts.py --probe build/bin/ui-native-probe \
--artifacts /tmp/render-evidence

Coverage is explicit rather than every possible Cartesian product:

  • 84 live cases: all three render modes, six color spellings, render aliases, six palettes, all 13 filter spellings across render modes, Matrix, FPS, waveform/FFT with all four audio selections, flips/stretch, and UTF-8 off. Each checks startup-small, recovery, blocked keys, help, and shutdown.
  • 13 flows: server status/grep with a live WebSocket handshake while covered, ACDS startup, accept/decline for three overwrite prompts, IP disclosure refusal, paused/help recovery with explicit dimensions, and recording while covered with all three render themes.
  • Four loopback authentication flows: encrypted SSH key, server password, missing client identity, and host-key acceptance.
  • 16 native prompt/TUI fixtures: text editing, password variants, yes/no defaults, cancellation, host/ACDS fingerprints, update choices, mDNS selection/cancellation, and splash. Four additional PCM fixtures exercise microphone/remote waveform and FFT with a nonzero 440 Hz signal.
  • Seven advertised file extensions and three redirected plain snapshots.

mDNS, update availability, GPG passphrase entry, and changed ACDS identities use deterministic native API fixtures; they do not prove external discovery/update services or GPG decryption. The Docker mirror's microphone/call sources are silent; injected PCM fixtures cover their nonzero rendering. ACDS currently has no status callback, so its startup output is tested without claiming a status TUI. Hardware cameras, physical audio devices, browser/WASM rendering, and every combination of independent options are outside this matrix.

Expected refusal exit codes are checked explicitly. Interactive client shutdown currently returns 1 for an interrupted connection. Sanitizers remain enabled; the harness treats any sanitizer report as a failure and retains previous-run logs when retrying a case.

Stdio capture regression

Live screens retain a duplicate of their output descriptor until removal. Media libraries temporarily redirect process stdout/stderr through LOG_IO; presentation uses the retained terminal for geometry and writes, and keeps accepting snapshots as live output during capture. This prevents a one-time size warning from being captured as a diagnostic instead of displayed. Keyboard initialization also retries when stdout is temporarily redirected, rather than committing an unconfigured keyboard.

The additional deterministic fixture holds LOG_IO active for five seconds and requires both the small-terminal warning and restored frame before capture ends:

uv run --frozen python tests/integration/tmux_capture.py --probe build/bin/ui-native-probe \
--artifacts /tmp/render-evidence
uv run --frozen python tests/integration/tmux_startup.py --binary build/bin/ascii-chat \
--artifacts /tmp/render-evidence --count 40 --color-mode 16

Startup stress defaults to warning-level logging so debug logging does not mask initialization races. These checks supplement the 131-case matrix above.

Half-block geometry and playback regression

tmux_animation.py samples 36 ANSI frames over more than 14 seconds from the 10-second testsrc2 fixture above. It requires changing frames before resize, after restoration, and after the loop boundary. Run it with the ordinary fixture and again with an -an -c:v copy video-only copy to cover both decoder layouts.

uv run --frozen python tests/integration/tmux_animation.py --binary build/bin/ascii-chat \
--artifacts /tmp/render-evidence

The halfblock_cover_uses_cell_geometry Criterion test verifies that half-block sampling does not double the viewport height used for aspect-ratio cropping. These longer animation and geometry checks supplement the 131-case interaction matrix; that matrix alone does not establish continuous playback across EOF.

Procedural animation controller

video/anim/controller.h provides typed dispatch for splash rainbow, digital rain, rainbow filtering, and the generated test pattern. Instances are independent; there are no groups or background animation threads in this layer. Existing producers own and serialize their instances, registered by name for diagnostics. No new shared atomics are needed. Immutable samples can be copied to the presentation thread.

animation_update() accepts monotonic timestamps in a caller-selected domain, advances the local clock, and returns frame-change and next-deadline information. Pause, visibility policy, and speed are per instance. Update at a control-change boundary before changing controls; continue ticking suspended instances so hidden or paused time is discarded. Call animation_reset() before seeking backwards. animation_sample_at() samples an already-scaled elapsed time without allocating an instance; the rainbow adapter uses the renderer's existing timestamps. Both shared test patterns use elapsed timestamps from their native or browser producer; pattern 0 is the gradient/square and pattern 1 the rainbow/circle.

animation_apply() switches on effect type and validates the target: splash writes RGB colors, rainbow writes RGB or allocated ANSI, digital rain writes allocated ANSI, and test patterns render into their reusable pattern context. ANSI outputs belong to the caller and must be released with SAFE_FREE. Digital rain's target includes its producer-owned column state. Application is stateful for rain (brightness smoothing); sample values are immutable, but a rain target must not be shared across concurrent renders.

Native and WASM renderers retain their existing public effect entry points, which now delegate to this controller. Existing effect ordering, terminal presentation, and video/GIF decoding, playback clocks, and audio synchronization are unchanged. The presentation scheduler still controls terminal refreshes; deadline metadata is available for producers without adding a global animation registry or scheduler.

Validation: tests/unit/video/animation_test.c covers clocks, independent instances, visibility, reset, typed targets, both shared patterns, and rain/rainbow composition. tests/integration/tmux_animations.py records all four animations and combined Matrix/rainbow from real tmux panes before and after resize. Stationary video input isolates effect motion from source motion. Captures/reports are generated outside Git, not checked into the repository.

Shared components and screens

Reusable terminal widgets live in lib/ui/, with public headers under include/ascii-chat/ui/. Complete screens and their input/presentation runtime live in lib/tui/ and include/ascii-chat/tui/. The size warning is a screen. The generic UI output sink keeps widgets independent of the TUI controller.

ui/popup.h provides overdrawn dialog frames and rules. ui/help_menu.h builds help popups from separate binding and description fields: binding tokens are yellow, separators retain the foreground, and numeric values use the theme's number color. Help retains the underlying screen, including animated media. Server and discovery-service help contain their own mode controls and settings.

ui/table.h formats ANSI-aware cells and renders rectangular tables with fixed headers. Callers own sorting and data; a cell callback supplies each visible row. ui/viewport.h keeps selection, page size, and the scroll offset within a page independent. A zero page size means one scrollable dataset. ui/scrollbar.h provides proportional thumb geometry and rendering, also usable by non-table screens such as logs and LAN selection. Tables reserve their rightmost column for this scrollbar. Streaming tables can use the allocation-free row formatter.

Other components include buttons, a UTF-8 line editor, fixed-region layout, text clipping/styling, progress bars, and spinners. Caller-owned storage has explicit capacities; invalid arguments and capacity errors set error context with SET_ERRNO. terminal_should_use_utf8() resolves the explicit UTF-8 option before capability detection. Widget colors go through colored_string(); ui_text_color_prefix() supports incremental formatting with the same policy.

tests/python/tests/integration/tui_modes/test_shortcuts.py covers help, logs, sync, stats and error stacks in mirror and both server modes, with status screens enabled and disabled. tests/python/tests/integration/native_ui/test_screens.py covers prompts, identity warnings, update buttons, LAN selection, notices, recording progress and splash, including size recovery. Both can export terminal-cell screenshots and original ANSI streams.

Terminal UI (TUI)

Runtime model

The TUI is a snapshot-driven presentation layer. Session, server, media, discovery, and diagnostic workers publish state; lib/tui/controller.c chooses and renders the active screen. No producer should call printf() or write ANSI sequences directly while a live screen owns the terminal.

Main screens

  • Media: live ASCII video, audio visualizations, FPS, and playback controls.
  • Splash/status: startup, connection, discovery, and server state.
  • Help: generated from the same shortcut definitions used by dispatch.
  • Logs: level-filtered output with interactive search.
  • Statistics: runtime counters and rates.
  • Sync diagnostics: debug-only lock, condition, and atomic activity.
  • Prompts, notices, mDNS selection, update, and error screens: modal overlays.

Controls

Press ? from an interactive screen for context-sensitive help. The complete shortcut reference, including build-dependent controls, is in Keyboard Shortcuts. Prompts use the shared UTF-8 line editor; Escape cancels and Enter accepts unless a screen documents a different action.

Resize and output

Screens calculate their own minimum dimensions. A resize can occur without a new media frame, so the controller checks geometry on its presentation tick. Paused media retains its last frame and clips it after a smaller resize rather than resuming capture. Redirected and non-TTY output renders synchronously.

Diagnostics

Debug builds can expose sync and errno-stack inspectors. These are sampled diagnostic views, not atomic snapshots of every application lock. Use --sync-state, --log-file, and --grep when investigating a hang; keep symbolization and logging out of realtime media loops.

Testing

The PTY integration suite exercises startup-small, resize recovery, covered input, help restoration, prompts, recording, redirected snapshots, and shutdown:

uv run --frozen python -m pytest \
tests/python/tests/integration/terminal_ui --ascii-chat-binary build/bin/ascii-chat
See also
topic_ui
topic_keyboard_shortcuts
md_docs_2session-tui.html
md_docs_2terminal-ui.html

User Interface Architecture

The UI is split into reusable widgets, the TUI screen/controller layer, and the platform keyboard decoder. Producers publish snapshots; they do not write directly to the terminal or own keyboard policy.

Boundaries

  • lib/ui/ and include/ascii-chat/ui/ contain reusable widgets.
  • lib/tui/ and include/ascii-chat/tui/ contain complete screens, snapshots, input routing, and the controller.
  • lib/platform/keyboard.c normalizes POSIX escape sequences, Windows console events, UTF-8, and UTF-16 into keyboard_key_t.
  • lib/platform/terminal.c owns terminal geometry, capabilities, raw mode, and restoration.

Controller and screens

ui_controller_submit() copies a screen snapshot and selects the highest priority live screen. Covered screens remain registered, so dismissing a prompt or help overlay restores the previous owner. Rendering callbacks run without the controller mutex; producers must not hold a lock required by a callback while removing its screen.

The controller owns the output sink. Live screen output goes through ui_controller_write() or ui_controller_present() so logs, prompts, and media cannot paint over one another. ui_controller_finish() retires live screens before synchronous final output.

Screen model

The runtime includes status, media, splash, progress, logs, error stacks, help, statistics, sync diagnostics, mDNS selection, update, notice, prompt, and error screens. Screens declare a minimum terminal size. When the terminal is too small, tui/too_small.h is presented and the underlying screen is restored after resize.

Input routing

lib/tui/input.c reads decoded events and routes them according to the active screen. Modal prompts consume editing keys; covered screens do not receive shortcuts. Bracketed paste is validated as a complete event and is never interpreted as a sequence of application shortcuts. See Keyboard Shortcuts for the current bindings.

Threading and shutdown

Producers may publish snapshots from worker threads. The controller copies owned snapshot data and callbacks execute on the presentation thread. Shutdown stops producers, retires screens, joins presentation, restores the terminal, then tears down keyboard and logging state.

See also
topic_tui
topic_keyboard_shortcuts
md_docs_2terminal-ui.html

Zalgo terminal effect

Migrated from docs/zalgo.md into the rendered Doxygen topic set.

Zalgo terminal effect

--zalgo decorates the displayed video with Unicode combining marks. It is available in mirror, client, and discovery modes. Omit the option to disable it.

ascii-chat mirror --test-pattern --zalgo
ascii-chat mirror --zalgo=1
ascii-chat mirror --webcam-index 1 --zalgo=10

Bare --zalgo uses intensity 7. Both --zalgo=7 and --zalgo 7 select an explicit intensity. Values must be integers from 1 to 10. A following session name is not consumed as the optional number. ASCII_CHAT_ZALGO accepts 1-10.

Intensity Coverage of eligible characters Marks per affected character
1 3% 1
2 6% 1
3 10% 1
4 18% 1
5 28% 1-2
6 40% 1-3
7 55% 2-4
8 70% 3-5
9 85% 4-7
10 100% 6-10

Positions and mark choices remain stable between frames. Increasing intensity adds marks without reshuffling the pattern. Only printable ASCII characters other than spaces are decorated. Existing Unicode, spaces, colors, and control sequences are preserved. REP compression is expanded before adding marks.

Use a UTF-8 terminal and a font with combining-mark support. On Windows, run chcp 65001 in the terminal before starting ascii-chat. Stacking and clipping vary by terminal and font; high levels deliberately obscure the image.

--utf8=true overrides automatic detection. --utf8=false is incompatible with Zalgo, including piped snapshots. If decoration would exceed the presenter's 16 MiB frame limit (including cursor controls), the original frame is displayed and a rate-limited warning is logged.

The effect applies locally to terminal output and text snapshots, including --strip-ansi output. It does not modify network frames, logs, status screens, or --render-file video exports. Half-block and other Unicode palettes retain their original glyphs, so use --render-mode foreground for ASCII decoration.

Browser rendering

The web app offers Zalgo: Off / Level 1-10 in Settings for Mirror, Client, and Discovery. The level is saved in the URL (?zalgo=7). The website's display effects demo starts at 7 and offers all ten levels with the demo video or webcam.

Browser canvases reuse the native distribution and mark selection. A separate combining font is composited after all cell backgrounds, so marks can cross rows and work alongside Matrix without being erased. The precise glyph stacking can differ from a terminal's font shaping. This is a local display effect; network frames are unchanged.

Windows Terminal captures

These are actual terminal screenshots, using a 90 by 26 foreground grid and UTF-8 output. The webcam captures use device 1 (2K Webcam).

Level Test pattern Webcam
1 Test pattern at level 1 Webcam at level 1
2 Test pattern at level 2 Webcam at level 2
3 Test pattern at level 3 Webcam at level 3
4 Test pattern at level 4 Webcam at level 4
5 Test pattern at level 5 Webcam at level 5
6 Test pattern at level 6 Webcam at level 6
7 Test pattern at level 7 Webcam at level 7
8 Test pattern at level 8 Webcam at level 8
9 Test pattern at level 9 Webcam at level 9
10 Test pattern at level 10 Webcam at level 10