Skip to content

perry/tui: native TUI engine (zero-flicker, double-buffered, cell-level dirty tracking) #358

Closed

Description

@proggeramlug

Tracking issue for a Perry-native TUI engine, exposed as the built-in perry/tui module. Surfaced in discussion #333 (TUI/CLI support) after pushback that ink "was pretty bad" — and the complaints turn out to be architectural, not personal taste.

Why a native option, not just ink (#348)

The flicker problem in ink isn't a config knob — it's the renderer's design. Stock ink does full component-tree traversal + complete buffer regeneration on every state change, with no cell-level dirty tracking and no double buffering. log-update.ts issues \e[2K\e[1A per line and rewrites everything; on a 60-line UI that's 60+ erase sequences per update at 32ms throttle. Long views flicker badly (ink#359, open since 2020). It got bad enough that Anthropic rewrote the renderer from scratch for Claude Code v2.0.10 (Oct 2025) — kept React + Yoga + ANSI as the shell, replaced the critical path with packed typed arrays, pool-based string interning, and double buffering. Their writeup: "started with Ink, then forked it beyond recognition."

So Perry's TUI story should be two tracks in parallel, not one:

  • Track A (Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348) — wire ink as a compilePackages target so existing ink apps Just Work. Perry stays the runtime, ink stays the library.
  • Track B (this issue) — first-party perry/tui: same architectural pattern as perry/ui does for native GUI, but for the terminal. Cell-level dirty tracking + double buffering from day one, designed not to have ink's flicker problem. For users who want zero-flicker out of the box and don't care which JS lib is under the hood, this becomes the recommended path.

Architecture (matches perry/ui shape)

TS (declarative) → HIR → Codegen → calls into perry-tui (Rust) → terminal
                                       ├─ layout engine (Taffy)
                                       ├─ cell grid + dirty tracking
                                       ├─ double-buffered renderer
                                       └─ ANSI emitter (minimal escape sequences)
  • Runtime crate — new crates/perry-tui (Rust). Cell grid as packed Vec<Cell> (no per-cell allocation per frame). Double buffer: render to back buffer, diff against front, emit only the minimal ANSI to reconcile changed cells. SIGWINCH handler + graceful resize.
  • Layout — Taffy (Rust-native flexbox/grid, used by Bevy / Dioxus). Avoids the Yoga FFI/cross-compile mess Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348 has to solve. Same flexbox mental model ink users are used to.
  • TS surface — declarative widget set exposed via import { Box, Text, render, useInput, useEffect } from "perry/tui". JSX-friendly; perry compile lowers to native widget calls the same way perry/ui does (HIR NativeMethodCall dispatch).
  • State / hooks — small built-in hook surface (useState, useEffect, useInput, useStdout) — no React, no react-reconciler dep. The whole point is to skip the "JS object per cell per frame" tax. If users want React semantics, they use ink (Track A).
  • Widget set (v1) — Box, Text, Spacer, Input (single-line), TextArea, List, Select, Spinner, ProgressBar. Table and Tabs v1.5.

Depends on

Acceptance

  • A 30-line perry/tui counter program (increment on +, decrement on -, quit on q) compiles via perry compile, produces a single-file native binary, runs interactively.
  • A streaming-log viewer that appends 1000 lines/sec to a scrollable region shows no visible flicker on Terminal.app / iTerm2 / Windows Terminal / GNOME Terminal. (This is the explicit anti-goal vs ink — if perry/tui flickers on this workload, the design is wrong.)
  • Resize (SIGWINCH) reflows correctly without artifacts.
  • Layout matches Taffy's flexbox semantics (verified against a small parity suite — display: flex, flex-direction, justify-content, align-items, flex-grow).
  • perry/tui adds zero npm dependencies — it's a built-in module like perry/ui / perry/thread.

Out of scope (separate follow-ups)

  • React compatibility shim — if there's demand for "use React with perry/tui's renderer", that's a v2 conversation. v1 ships with Perry's own minimal hook API.
  • Mouse input — niche enough to defer.
  • Image / sixel / kitty graphics protocol — defer.
  • ink-style component library compat (ink-table, ink-spinner, etc.) — those users should use ink via Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348.

Why both tracks, not one

ink ships ~3.1M weekly downloads vs react-blessed's ~640 — it's the dominant JS TUI library and a lot of existing apps need it. But the framework wars in this space aren't settled (ink's flicker, no React 19 yet, heavy dep tree; Anthropic's rewrite; OpenTUI; Ratatui-bindings experiments). Perry shouldn't try to settle them. #347 makes Perry the runtime that any of them can target; this issue gives Perry users a first-party option that doesn't have ink's known architectural papercuts.

Discussion: #333. Primitives: #347. Sibling track: #348.

Activity

  1. added 4 commits that reference this issue on May 1, 2026
  2. added 2 commits that reference this issue on May 1, 2026
  3. added 4 commits that reference this issue on May 10, 2026
  4. MehmetAliPolat1515 commented on Aug 22, 2026

    @MehmetAliPolat1515

    As someone who uses the TUI feature, I think it would be good to develop and make improvements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions