You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`perry/tui` (#358, closed) already ships a complete native TUI engine inside `perry-runtime/src/tui/` — taffy-based flexbox, double-buffered ANSI diff with cell-level dirty tracking, ~3K LOC, zero npm deps. Widget set: `Box`/`Text`/`Spacer`/`Input`/`TextArea`/`List`/`Select`/`Spinner`/`ProgressBar`/`Table`/`Tabs`. Reactivity ships as `state(initial)` with `.get()`/`.set()` plus a global STATE_DIRTY flag that re-renders on change.
This issue covers what's left to make `perry/tui` feel like ink to authors — same hooks, same prop names, JSX-friendly — so ink's mental model and AI tools' code-generation habits transfer with minimal friction. The engine is done. This is API polish + missing hooks.
This is the recommended first-party TUI path; ink-via-`compilePackages` (#348) becomes a compiler smoke test only.
Goal
An ink program of the shape below compiles via `perry compile` with a one-line import change (`from 'ink'` → `from 'perry/tui'`) and runs natively:
Run after mount + on dep change; cleanup on unmount/dep-change
`useApp() → { exit, ... }`
❌
Imperative `exit()` from inside components
`useStdout() → { write, columns, rows }`
❌
Surface terminal dims, raw write escape hatch
`useFocus() → { isFocused }` + ``
❌
Tab navigation between focusable widgets
`useMemo(fn, deps)` / `useRef(initial)`
❌
Standard hook surface
JSX as authoring surface
⚠️ partial
``/`` lower to `Box(...)`/`Text(...)` — audit
Text styling props (`color`, `bgColor`, `bold`, `italic`, `underline`, `dimColor`, `inverse`)
partial
parity with ink
Source-compat test suite
❌
5–10 small real-world ink programs as regression tests
Phases
Phase 1 — Hook API parity (highest leverage)
`useState(initial)` — alias to existing `state()` builder, but match React signature: returns `[value, setter]` tuple, not `{ get, set }` object. Internal slot allocation reuses `js_perry_tui_state_alloc`.
`useEffect(fn, deps?)` — runs after first render; if `deps` change between renders, re-runs (after running prior cleanup); cleanup returned from `fn` runs on unmount. Wire to render loop's frame boundary.
`useApp()` — returns `{ exit(code?), waitUntilExit() }`. `exit` flips a global flag the run loop checks at frame top.
`useStdout()` — returns `{ write(str), columns, rows }`. `write` for escape-hatch raw output; columns/rows from current terminal dims.
`useMemo(fn, deps)` / `useRef(initial)` — small additions, same slot machinery as `useState`.
Phase 2 — JSX audit + ergonomics
Verify `<Box flexDirection="row" gap={1}>...` lowers correctly through SWC's JSX transform to `Box({ flexDirection: "row", gap: 1, children: [...] })`.
Confirm children are passed through as an array, not spread positionally.
Document the JSX pragma / tsconfig setup for `perry/tui` in CLAUDE.md.
Phase 3 — Focus + keyboard navigation
`useFocus({ autoFocus?, isActive? })` — register the calling widget with a focus manager.
Take 5–10 small ink programs from the wild (counter, todo list, file picker, multi-step prompt, log viewer). Patch `from 'ink'` → `from 'perry/tui'`. Each compiles + runs + matches expected output. Lives in `test-files/test_perry_tui_inkcompat_*.ts`.
This is the acceptance gate — "ink-shape" is real when these compile.
Phase 5 — Text styling parity
Audit `<Text color="red" bold italic>`, ``, ``, `` against ink behavior. Should already mostly work via existing style.rs; this is a parity audit + filling in gaps.
Background
`perry/tui` (#358, closed) already ships a complete native TUI engine inside `perry-runtime/src/tui/` — taffy-based flexbox, double-buffered ANSI diff with cell-level dirty tracking, ~3K LOC, zero npm deps. Widget set: `Box`/`Text`/`Spacer`/`Input`/`TextArea`/`List`/`Select`/`Spinner`/`ProgressBar`/`Table`/`Tabs`. Reactivity ships as `state(initial)` with `.get()`/`.set()` plus a global STATE_DIRTY flag that re-renders on change.
This issue covers what's left to make `perry/tui` feel like ink to authors — same hooks, same prop names, JSX-friendly — so ink's mental model and AI tools' code-generation habits transfer with minimal friction. The engine is done. This is API polish + missing hooks.
This is the recommended first-party TUI path; ink-via-`compilePackages` (#348) becomes a compiler smoke test only.
Goal
An ink program of the shape below compiles via `perry compile` with a one-line import change (`from 'ink'` → `from 'perry/tui'`) and runs natively:
```tsx
import { Box, Text, useState, useInput, useApp, render } from "perry/tui";
function App() {
const { exit } = useApp();
const [n, setN] = useState(0);
useInput((ch) => {
if (ch === "+") setN(n + 1);
if (ch === "-") setN(n - 1);
if (ch === "q") exit();
});
return count: {n};
}
render();
```
What ships today vs what's missing
Phases
Phase 1 — Hook API parity (highest leverage)
Phase 2 — JSX audit + ergonomics
Phase 3 — Focus + keyboard navigation
Phase 4 — Source-compat test suite
Phase 5 — Text styling parity
Out of scope (deliberately)
Acceptance
Related
ink(React-based TUI framework) end-to-end viaperry.compilePackages#348 — ink-via-compilePackages (open, deprioritized to smoke-test-only — sibling track, not blocked-by)