Commit Graph

181 Commits

Author SHA1 Message Date
Yunfei He 2f7babd258 fix: configure PTY tests with pool: forks + maxWorkers: 1 for node-pty compat
node-pty requires child_process.fork(), not worker_threads.
Explicit pool: forks + maxWorkers: 1 + isolate: false ensures
single forked process execution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:40:35 +08:00
Yunfei He 9e7449b1ee chore: exclude PTY fixtures from main tsconfig + lint
Fixture files use jsx: react-jsx (via their own tsconfig) and have
expected children type differences. Exclude from package-level checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:14:41 +08:00
Yunfei He 54b5394e88 test: add issue-450 rerender + animation exit PTY fixtures + 7 tests
- issue-450-fixture-helpers.tsx with rerender/initial helpers
- 5 issue-450 fixture files (fullscreen, overflow, grow, shrink)
- 2 useAnimation exit fixtures (interactive false, non-interactive)
- 7 tests with exact clearTerminal/eraseLine count assertions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:12:00 +08:00
Yunfei He 34031f35b6 test: add erase + fullscreen PTY fixtures + 8 tests
- erase.tsx, erase-with-static.tsx, erase-with-state-change.tsx
- fullscreen-no-extra-newline.tsx, issue-442-full-height.tsx
- render.test.ts with clearTerminal/fullscreen assertions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:06:23 +08:00
Yunfei He 4c2ebe8501 test: add CI rendering PTY fixtures + 4 tests
- ci.tsx: Static items + counter (non-interactive/CI behavior)
- ci-debug.tsx: debug mode rendering
- ci-debug-after-exit.tsx: debug mode exit + DONE output

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:01:50 +08:00
Yunfei He d8aeb2bc02 feat: add clearTerminal strategy, fullscreen suppression, log-update integration
- shouldClearTerminalForFrame() for overflow/fullscreen transitions
- renderInteractiveFrame with fullscreen newline suppression
- writeToStdout/Stderr restore via log-update clear + re-render
- incrementalRendering MountOption
- Static output capture inlined in commit paths

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:57:59 +08:00
Yunfei He 7403522c96 feat: port Ink's log-update and replace npm log-update dependency
Port Ink's self-built log-update with standard and incremental rendering
modes, cursor positioning, and sync support. Rewrite frame-writer to use
the new internal log-update instead of the npm package, adding sync,
setCursorPosition, and willRender methods while preserving existing
write/done/clear behavior. Remove the npm log-update dependency (-6
packages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:51:20 +08:00
Yunfei He 4a9d401b0f feat: port cursor-helpers from Ink for cursor positioning escape sequences
Add ansi-escapes to pnpm catalog and runtime dependencies. Port all
cursor helper functions (cursorPositionChanged, buildCursorSuffix,
buildReturnToBottom, buildReturnToBottomPrefix, buildCursorOnlySequence)
with 10 colocated unit tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:48:03 +08:00
Yunfei He 3995e84285 test: add PTY console and useStdout fixture scripts + tests
Port console.log and useStdout.write fixtures from Ink to verify
patchConsole and useStdout().write() work correctly in a real terminal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:45:17 +08:00
Yunfei He 15aca4502d test: add PTY input/paste fixture scripts and test file (47 tests)
Port input handling fixtures from Ink to vue-tui:
- use-input.tsx: multi-mode fixture (30+ input modes via process.argv[2])
- use-input-ctrl-c.tsx: Ctrl+C with exitOnCtrlC=false
- use-input-multiple.tsx: active/inactive useInput hooks
- use-input-many.tsx: 11 useInput hooks without MaxListenersExceededWarning
- use-input-discrete-priority.tsx: rapid input with deferred state sync
- use-paste.tsx: bracketed paste with multiple modes
- tsconfig.json: add "types": ["node"] to fix TS2591 in fixtures

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:41:38 +08:00
Yunfei He 8dea7bea10 test: add PTY exit test file (14 tests)
Tests use run()/term() helpers with real PTY subprocesses.
Note: requires unsandboxed environment for node-pty to spawn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:31:32 +08:00
Yunfei He ba9a7d3545 feat: add 14 exit fixture scripts for PTY testing
Translate Ink's exit-*.tsx fixtures to vue-tui's createApp/mount pattern.
Covers: normal exit, exit(), unmount(), error, result, raw mode, static.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:27:05 +08:00
Yunfei He 2e01fc50a4 chore: remove 8 exit todo stubs (covered by PTY fixtures)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:23:39 +08:00
Yunfei He 3422e6edc1 feat: stdin.ref/unref, raw mode default false, conditional listener, maxListeners
- stdin.ref() on raw mode enable, stdin.unref() on disable
- setEncoding("utf8") on raw mode enable
- Raw mode default changed from true to false (hooks acquire it)
- useStdin().setRawMode routed through ref-counted controller
- Stdin listeners attached only when raw mode is active
- EventEmitter maxListeners set to Infinity

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:22:58 +08:00
Yunfei He 05c4dd5076 feat: add beforeExit natural unmount + stdout write barrier
Register process.once('beforeExit') in waitUntilExit() so TUI apps
that finish naturally (no explicit exit/unmount) clean up before the
event loop drains. Add stdout write barrier in resolveExit() to ensure
all output is flushed before settling the exit promise.

Update testing render helper to await the additional async tick
introduced by the write barrier so early-error detection still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:18:56 +08:00
Yunfei He 28d18bedda chore: add PTY test vitest config and ready integration
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:11:21 +08:00
Yunfei He 8a3b60635d feat: port run() and term() PTY test helpers from Ink
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:07:39 +08:00
Yunfei He bc4bea93bd chore: add node-pty and tsx deps, PTY test directory structure
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:05:33 +08:00
Yunfei He a87cc08d0d Revert "chore: remove 8 subprocess-fixture todo stubs (behaviors already covered)"
This reverts commit a5b0530c04.
2026-05-26 17:35:16 +08:00
Yunfei He a5b0530c04 chore: remove 8 subprocess-fixture todo stubs (behaviors already covered)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:25:06 +08:00
Yunfei He 222b520678 feat: add useWindowSize alias, terminal-size fallback, update public API + README
- useWindowSize alias for useTerminalSize (Ink API naming parity)
- terminal-size fallback when stdout.columns/rows are 0/missing
- Public API test covers all new exports
- README documents all MountOptions and composables

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:28:46 +08:00
Yunfei He 11c655c76e feat: add accessibility support (aria props, screen reader output)
- Box: aria-label, aria-hidden, aria-role (typed union), aria-state
- Text: aria-label, aria-hidden
- Transform: accessibilityLabel prop
- internal_accessibility on TuiBox node (role + state only)
- renderScreenReaderOutput() alternate render path
- Component-level aria-hidden hides, aria-label replaces content
- 14 new accessibility tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:26:10 +08:00
Yunfei He 83d9dd6ce8 feat: add useIsScreenReaderEnabled + MountOptions.isScreenReaderEnabled
- Default: true when INK_SCREEN_READER=true env var set
- Screen reader mode bypasses render throttle
- Composable exposes flag via provide/inject

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:16:31 +08:00
Yunfei He 2c186951ed feat: add useAnimation composable for frame-based animations
Drives animations with a frame counter, elapsed time, delta, and reset
function. Supports configurable interval and reactive isActive toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:13:43 +08:00
Yunfei He edf51bef3c feat: add useCursor composable for terminal cursor control
- setCursorPosition propagates to app context for frame restoration
- Cursor cleared on component unmount via onScopeDispose

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:10:51 +08:00
Yunfei He ca8ba65300 feat: add useBoxMetrics composable and measureElement function
- useBoxMetrics returns reactive { width, height, left, top, hasMeasured }
- measureElement returns imperative { width, height } snapshot
- Handles Vue component instance refs via $el resolution

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:08:30 +08:00
Yunfei He 95ba2f5796 feat: add renderToString for synchronous string rendering
Standalone renderer path with no terminal bindings. Captures Static
output, provides no-op contexts for hooks, proper WASM cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:59:17 +08:00
Yunfei He 3ee01dc278 feat: add patchConsole, waitUntilRenderFlush, onRender, and maxFps
Add four behavioral surfaces to the runtime:

- patchConsole (default: true): intercepts console.* methods to route
  output through writeToStdout/writeToStderr, preventing frame corruption.
  Disabled in debug mode. Restored before Vue cleanup on teardown.
- waitUntilRenderFlush(): flushes pending throttled renders and waits
  for the stdout write barrier, exposed on TuiApp and testing RenderResult.
- onRender callback: fires after each commit with { renderTime } in ms.
- maxFps option: overrides the scheduler's default 32ms throttle interval
  with 1000/maxFps. The scheduler now accepts a throttleMs option and
  exposes hasPending() for flush coordination.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:54:18 +08:00
Yunfei He dee060bb76 feat: Static style prop, identity tracking, and rerender semantics
- Add style prop to Static (default: position absolute, flexDirection column)
- Apply style props to isolated static paint (padding, flexDirection, etc.)
- Track staticNode/previousStaticNode for identity changes
- Reset fullStaticOutput on Static unmount/remount
- Convert all 6 Static todo tests to active passing tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:47:43 +08:00
Yunfei He a280b4f684 feat: add interactive mode detection and output stream coordination
- Auto-detect interactive mode via is-in-ci + stdout.isTTY
- writeToStdout/writeToStderr with frame clear/restore coordination
- Non-interactive mode: static immediate, dynamic at unmount
- Cursor position tracking for Phase 5 useCursor integration
- useStdout/useStderr route through coordinated context methods

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:34:41 +08:00
Yunfei He 1a888a8eb1 feat: add usePaste composable with bracketed paste mode
- Ref-counted bracketed paste mode (\x1b[?2004h/l)
- usePaste acquires raw mode + paste mode, listens on paste channel
- Paste falls through to useInput when no paste listeners exist
- isActive option for conditional paste listening

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:25:07 +08:00
Yunfei He f51079a5ea feat: rewrite stdin pipeline with input-parser, exitOnCtrlC via \x03
- Replace data event with readable + input-parser state machine
- Intercept \x03 in input pipeline (not process SIGINT)
- useInput now uses parse-keypress for semantic key mapping
- 20ms pending escape flush timer matching Ink
- Esc focus reset in input pipeline
- Delete old key-parser.ts (replaced by input-parser + parse-keypress)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:04:39 +08:00
Yunfei He 283444e87b feat: port parse-keypress (semantic mapper) from Ink
Port Ink's parse-keypress.ts to map raw input event strings to semantic
key objects with name, ctrl, shift, meta, and kitty protocol support.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:55:30 +08:00
Yunfei He ab2444fcb0 feat: port input-parser (chunk splitter) from Ink
Port Ink's stateful input parser that splits raw stdin chunks into
discrete input events. Supports CSI/SS3 sequence parsing, double-ESC
prefix, bracketed paste, backspace byte splitting, and pending state
for incomplete escape sequences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:52:01 +08:00
Yunfei He 1106be867b feat: add error boundary with onErrorCaptured to catch descendant errors
Wrap user root component in an InternalErrorBoundary that catches all
descendant errors (setup, render, lifecycle) via onErrorCaptured and
routes them through exit(error). This prevents yoga WASM state corruption
that occurred when errors propagated uncaught during Vue's render phase.

- Add ErrorOverview component for rendering error frames
- Replace try-catch around mount with onErrorCaptured boundary
- Update testing render() to detect early exit errors
- Convert 3 todo tests to active tests (nested setup, unhandledRejection, post-mount)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:48:23 +08:00
Yunfei He 272f1f0c32 refactor!: tree validation throws on invalid nesting instead of warn+skip
Change Box-in-Text validation from dev-only warn+skip to unconditional
throw, matching Ink's reconciler behavior. Add text-outside-Text
validation that throws when non-empty text-leaf nodes are inserted into
box/root/static containers outside a Text context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:37:39 +08:00
Yunfei He 57887eefe1 feat: port ANSI sanitizer and integrate into paint pipeline
Port Ink's sanitize-ansi.ts to strip non-SGR/non-OSC ANSI escape
sequences (cursor movement, screen clearing) from text nodes.
Integrated as the final step of renderTextWithInlineStyles() so all
squashed text is sanitized before layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:29:33 +08:00
Yunfei He f6bd888444 feat: port ANSI tokenizer from Ink
Port Ink's ansi-tokenizer.ts (509 lines) as a standalone ANSI escape
sequence parser that produces typed tokens. Exports tokenizeAnsi() and
hasAnsiControlCharacters(). Supports CSI, ESC, OSC, DCS, PM, APC, SOS,
C1 control characters, string terminator, and invalid/malformed tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:27:29 +08:00
Yunfei He d0df28850c fix: prepend transform in paintNode to match Ink inner-first nesting order
Transforms were appended (`[...transformers, node.transform]`) causing outer
transforms to execute before inner ones. Ink prepends so inner transforms run
first and outer transforms wrap the result. Adds a nesting-order regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:23:03 +08:00
Yunfei He 957d30f43a feat: rewrite Output class with StyledChar[][] grid for correct emoji/wide char handling
Port Ink's output character model using @alcalzone/ansi-tokenize. Each cell is now a
StyledChar with proper grapheme clustering. Fixes emoji alignment, wide char boundaries,
and changes clipping to top-only (matching Ink). Upgraded ansi-tokenize to ^0.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:19:50 +08:00
Yunfei He 8b834c2b7f chore: add @alcalzone/ansi-tokenize dependency
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:10:22 +08:00
Yunfei He 37c4905bbc fix: warn and skip insertion when Box is nested inside Text
Detect invalid <Box> inside <Text> nesting at the DOM insert level and
emit a dev warning instead of crashing the WASM yoga engine. The box
insertion is skipped to prevent layout corruption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:04:16 +08:00
Yunfei He 45b9ed3d9d fix: use EDGE_START/EDGE_END for marginLeft/marginRight matching Ink
Ink uses Yoga.EDGE_START for marginLeft and Yoga.EDGE_END for
marginRight, which is correct for RTL-aware layout. Changed from
EDGE_LEFT/EDGE_RIGHT to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:02:49 +08:00
Yunfei He d86abe0fb1 fix: Newline renders as yoga carrier (text node) when standalone
Match Ink's behavior where Newline renders as ink-text (yoga carrier).
When inside a Text parent, Newline still renders as virtual-text for
inline behavior. When standalone, it renders as a text node so it
participates in yoga layout and occupies vertical space.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:02:02 +08:00
Yunfei He e79c3660f5 test: verify error boundary routes post-mount errors through exit()
Vue's errorHandler (installed after mount) already catches render-time
errors from nested components and routes them through exit(err),
providing error boundary behavior matching Ink's design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:00:56 +08:00
Yunfei He 42c69d72f2 feat: hide cursor on mount, show on unmount (matching Ink)
Write ESC[?25l to stdout after mount and ESC[?25h during teardown,
matching Ink's cursor management. Only active in production mode
(debug: false) to avoid interfering with test streams.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:00:17 +08:00
Yunfei He 8585598d14 fix: make Transform a yoga carrier matching Ink's ink-text behavior
Transform nodes now participate in yoga layout with flexShrink=1,
flexDirection='row', matching Ink's Transform which renders as ink-text.
This fixes multi-line text under Transform not getting proper layout
height. Transform nodes inside Text parents remain inline (excluded
from yoga tree) to preserve renderTextWithInlineStyles behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:59:03 +08:00
Yunfei He 8565f41827 feat: add time-based render throttle (~30fps) matching Ink
Add leading+trailing throttle to the commit scheduler in production mode.
In debug/test mode (immediate: true), commits fire without delay to
preserve test determinism. Adjusted the non-debug static test to account
for the throttle timer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:55:02 +08:00
Yunfei He b8778c1847 fix: Text node defaults to flexDirection='row' matching Ink's ink-text
Set flexDirection=row, flexShrink=1, flexGrow=0 on text yoga nodes at
creation time, matching Ink's <ink-text> style defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:52:37 +08:00
Yunfei He 2e1493b785 test: verify text wraps correctly within border/padding content area
Yoga's measure function already receives the content-area width (minus
parent border/padding), so text wrapping is correct without additional
adjustment in the paint pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:51:27 +08:00