Commit Graph

35 Commits

Author SHA1 Message Date
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 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 c4161c99a7 fix: Box defaults to flexDirection='row', flexShrink=1, flexWrap='nowrap', flexGrow=0
Match Ink's Box defaults by setting yoga properties at node creation time.
This ensures proper row-based layout regardless of Vue's prop patching order.
Updated affected test snapshots and added explicit flexDirection="column"
where tests relied on the old column default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:49:27 +08:00
Yunfei He 9d0c59c641 feat: expand key parser with home, end, ctrl+arrow, meta+arrow
Add parsing for Home (ESC[H, ESC[1~, ESC[7~), End (ESC[F, ESC[4~,
ESC[8~), Ctrl+arrow (CSI 1;5 A/B/C/D), and Meta+arrow (double-ESC
prefix) key sequences. Add home/end fields to the Key interface.
Implement 12 new useInput tests for these key combinations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:12:05 +08:00
Yunfei He bcfdaddec7 feat: exit() accepts result value, waitUntilExit resolves with it
Change exit(error?: Error) to exit(errorOrResult?: unknown). When an
Error is passed, waitUntilExit rejects; otherwise it resolves with the
value. This matches Ink's behavior where exit("hello") resolves the
promise with "hello".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:10:52 +08:00
Yunfei He 97ee43ea14 feat: support Transform inside Text for inline text transformation
Update renderTextWithInlineStyles and flattenLeaves to handle transform
nodes nested inside text elements, matching Ink's squashTextNodes
behavior. Skip transform application on empty text to avoid wrapping
empty strings. Skip writing empty text nodes to avoid applying line
transformers to empty content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:08:44 +08:00
Yunfei He bbaa77bce1 feat: support per-axis overflowX/overflowY clipping
Add independent horizontal/vertical clip support in the paint pass,
matching Ink's per-axis overflow behavior. Add overflowX/overflowY to
STYLE_PROPS so they are stored in el.props for the paint pass.
Implement 26 new overflow tests translated from Ink.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:04:57 +08:00
Yunfei He fa60d12f5a fix: handle null/undefined Text children without crashing yoga
Guard flattenLeaves and renderTextWithInlineStyles to skip comment
nodes (produced by Vue for null/undefined children) and return empty
strings for childless text nodes. Also guard bindTextMeasure to return
zero dimensions for empty text. Expand TuiInlineNode to include
TuiComment since Vue inserts comments into text containers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:01:32 +08:00
Yunfei He 9e0bdfcb97 fix: use Object.hasOwn in isYogaProp to avoid Object.prototype false positives
"constructor" in {} returns true because it hits Object.prototype.
Using Object.hasOwn ensures only actual yoga prop keys are recognized.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:58:04 +08:00
Yunfei He 07cfe5d6fb fix: align wrap mode with Ink (trim: false)
Ink's wrap mode uses { trim: false } to preserve leading/trailing
whitespace on wrapped lines. We incorrectly used trim: true which
stripped whitespace. Updates unit test and inline snapshots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:14:17 +08:00
Yunfei He 664b568536 fix: align overflow and type behavior with Ink
- Remove yoga setOverflow call — Ink only clips visually in paint,
  not at the yoga layout level. Calling setOverflow(HIDDEN) prevented
  nodes from expanding, diverging from Ink's behavior.
- Fix AlignContent type to exclude baseline (not valid for align-content
  in CSS/Yoga, and Ink doesn't allow it).
- Keep border background fallback to backgroundColor — Ink achieves
  the same visual result via background fill underneath borders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:05:14 +08:00
Yunfei He 771b8ef180 fix: emit empty frames so components rendering null produce output
The frame writer initialized lastFrame to "" and also reset it to ""
on clear(). Since commit() calls clear() then write(), any component
that painted to an empty string (e.g. Transform with no children, or
a component returning null after unmounting children) would be
deduplicated against the cleared state and never emitted.

Use null as the sentinel for "no previous frame" so the first write
after a clear always goes through, even when the frame is empty.

Fixes: Transform with null/undefined children tests
Fixes: Focus unmount crash tests (stale frame persisted)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:55:21 +08:00
Yunfei He 04707e82d9 fix: intersect all clip rects in stack for nested overflow:hidden
Previously Output.get() only used the last clip rect (clips.at(-1)),
which meant an inner overflow:hidden box wider than its outer ancestor
could effectively loosen the outer clip. Now all active clips are
intersected via reduce, and writes are skipped entirely when the
intersection is empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:53:41 +08:00
Yunfei He da7c843d44 feat: implement overflow clipping in paint pipeline
Port Ink's clip/unclip approach to vue-tui's paint output. When a box
has overflow="hidden", content painted by its children that falls
outside the box's content area (inside borders) is clipped.

Implementation:
- Add ClipRect, clip(), and unclip() to the Output class
- Maintain a clip stack; active clip is applied to each write op
- Horizontal clipping uses slice-ansi for correct ANSI handling
- Vertical clipping slices the lines array
- Store "overflow" in STYLE_PROPS so it's available in node.props for
  the paint pass

Also adds explicit flexDirection="row" to two overflow tests where
multiple child boxes should lay out side-by-side (Ink defaults to row,
vue-tui defaults to yoga's column).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:47:10 +08:00
Yunfei He 07a851ce2a fix: add flexShrink=1 default to Text and sub-1px measure guard
Ink's <Text> sets flexShrink: 1 by default so text nodes shrink when
they overflow their container in no-wrap flex rows. Without this,
vue-tui's text nodes had flexShrink=0 (yoga default) and would never
shrink, causing overflow tests to fail.

Also ports Ink's measureTextNode guard: when yoga asks if text can fit
in a sub-1px space during shrink calculations, return the natural
dimensions to prevent text from wrapping to infinite height.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:44:26 +08:00
Yunfei He 5a1f2eba2d fix: trim trailing whitespace per line in paint output to match Ink behavior
Ink's Output.get() calls .trimEnd() on each line of the rendered grid.
Without this, vue-tui's paint grid always fills the full root width with
spaces, causing raw frame output to contain 100-char-wide padding. This
broke tests using { raw: true } and made meaningful trailing-space
assertions impossible with { trimLines: true }.

Also corrects test assertions for 'set width in percent' and 'relative
position' that expected trailing spaces which Ink never produces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:36:48 +08:00
Yunfei He 562b07b60e test: port Ink component tests (38 tests)
Distributes across text, static, transform, newline-spacer,
error-handling, and prop-reset test files.

Tests requiring features not yet in vue-tui are marked test.todo:
- <Transform> inside <Text> (paint pass limitation)
- multi-line transform (transform nodes not yoga carriers)
- text/box nesting validation (no runtime validation yet)
- complex Static rerender patterns (Ink-specific API)

Also fixes wrap="hard" to use wordWrap:false so text breaks at exact
character boundaries rather than word boundaries, matching Ink behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:01:10 +08:00
Yunfei He 22ecbe0f80 test: port Ink layout tests (157 passing, 21 known failures)
Translates flex, direction, wrap, align, justify, gap, padding,
margin, width-height, position, display, and overflow tests from
Ink. Adds trimLines mode and trailing-newline strip to test helper.
Fixes Box type for alignContent (space-between/around/evenly) and
alignSelf (stretch/baseline).

21 tests fail due to behavioral differences (overflow clipping,
percentage dimensions, relative position offsets, align-content
space-* variants). These represent real gaps to investigate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:24:31 +08:00
Yunfei He ffb5d69b0b feat(runtime): add per-side border dim and background color props
Adds borderTopDimColor/borderBottomDimColor/borderLeftDimColor/
borderRightDimColor and borderBackgroundColor with per-side variants.
Refactors drawBorder to use per-edge colorizers that respect
edge-specific color, dim, and background overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:50:22 +08:00
Yunfei He 1503c18a66 feat(runtime): add wrap='hard' mode to Text component
Hard wrap breaks text at exact width boundaries without respecting
word boundaries. Uses wrap-ansi with { hard: true, trim: false }.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:45:21 +08:00
Yunfei He f4ef12f484 feat(runtime): add maxWidth, maxHeight, aspectRatio, alignContent, position props
Adds 9 new Box props backed by yoga-layout: maxWidth, maxHeight,
aspectRatio, alignContent, position, top, right, bottom, left.
Also adds baseline alignment support. Includes reset semantics
for props that Ink tests exercise clearing on rerender.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:42:49 +08:00
Yunfei He b78d65789d feat(paint): support ansi256() color format
Ink uses 'ansi256(N)' while vue-tui only parsed 'ansi(N)'.
Both now work. Needed for Ink test migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:40:03 +08:00
Yunfei He 53206292ce refactor(paint): preserve trailing spaces in Output.get()
Removes trimEnd() from the paint grid output. The test helper's
trimFrame() still handles cosmetic trimming for lastFrame(), so
existing tests are unaffected. This enables layout tests to assert
exact widths via lastFrame({ raw: true }).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:33:17 +08:00
Yunfei He 5b399e0844 feat(runtime): enable raw mode by default on mount
TUI apps should suppress terminal echo to prevent keystroke pollution
in the rendered UI. Raw mode is now on by default (matching Bubbletea,
Textual, Ratatui), with exitOnCtrlC also defaulting to true.

Pass rawMode: false to mount() for pure-output CLI tools that don't
need input suppression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:43:21 +08:00
Yunfei He ca25ccd96e fix(paint): continuous backgroundColor across wrapped text lines
- Wrap each text line with applyChalk(bgProps) after wrapText so
  background color is reapplied on every wrapped line (wrap-ansi
  doesn't carry ANSI state across line breaks)
- Add wrapped-text backgroundColor regression test
- Fix typo: color=" cyan" → color="cyan" in example
- Add FORCE_COLOR=3 debugging tip to AGENTS.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:31:00 +08:00
Yunfei He e62c802829 fix(paint): continuous backgroundColor with borders
- Fill background inside border area only (not under border chars)
  so border placeLine doesn't overwrite background ANSI codes
- Pad text lines to full layout width when inheritedBg is set
  so background extends past text content to fill the row

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:24:36 +08:00
Yunfei He 8e6c4c5c2b fix(paint): backgroundColor now visible with borders and text
Root causes:
- drawBorder wrote plain characters, overwriting fillBackground's ANSI codes
- renderTextWithInlineStyles spread node.props over inherited bg, but
  undefined props from Vue clobbered the inherited backgroundColor
- Child text nodes did not inherit backgroundColor from parent Box

Fixes:
- drawBorder applies backgroundColor (and borderColor) to border chars
- Filter out undefined props before merging in renderTextWithInlineStyles
- Pass inheritedBg through the paint tree to text nodes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:11:54 +08:00
Yunfei He ddac248d26 fix(runtime): set Static yoga node to display:none in dynamic layout
Static content is rendered via paintIsolated (separate channel), but its
yoga node was still participating in the dynamic frame layout calculation,
adding blank lines equal to the Static content height between the last
Static item and the dynamic content below.

Setting display:none on the Static yoga node removes it from the dynamic
layout. paintIsolated is unaffected — it reparents children into a
temporary yoga tree.
2026-05-24 23:16:58 +08:00
Yunfei He b834a87529 fix(runtime): clear dynamic frame before flushing static content
flushStatic() writes directly to stdout, but log-update tracks cursor
position for in-place re-rendering. Without clearing log-update first,
the static content gets overwritten when the dynamic frame re-renders.

Adds a regression test that verifies the clear sequence appears before
static content in non-debug mode.
2026-05-24 23:00:05 +08:00
Yunfei He ae33947c37 chore: prepare v0.0.1 release
- Bump all packages to 0.0.1
- Add publishConfig.access: public to @vue-tui/cli
- Disable tsgo for dts generation (missing native-preview dep)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:58:29 +08:00
Yunfei He 81a640f3ba feat(runtime): add HMR event bridge, DevOverlay, and dev mode wiring
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:01:07 +08:00
Yunfei He ff82f6e064 feat(runtime): Vue terminal renderer with Ink-aligned API
Custom Vue 3 renderer for terminal UIs, built on yoga-layout for
flexbox and chalk for styling.

Components: Box, Text, Newline, Spacer, Static, Transform
Composables: useExit, useInput, useFocus, useFocusManager,
  useStdin, useStdout, useStderr, useTerminalSize
Entry: createApp(root) → app.mount(options) → app.waitUntilExit()
Focus: Ink-aligned Tab/Shift+Tab/Escape with Focusable[] ring
Internal subpath: @vue-tui/runtime/internal (yogaNodeTracker)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:13:33 +08:00