9227ddf696
* test: add text ANSI sanitization parity tests from Ink (+15) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add use-animation parity tests from Ink (+43) Port 43 new tests from Ink's use-animation test suite covering: - Multiple animations in sync, different rates, sibling unmount - Timer cleanup/recreation on unmount and remount - Inactive animations, timer leak prevention - Edge intervals (NaN, Infinity, -Infinity, oversized, zero, negative) - isActive toggle resets, pause/resume cycles - Frame catch-up, time/delta tracking, reset() behavior - Newly mounted/activated animations don't inherit elapsed time - Wall clock monotonicity, getter function isActive support Uses selective fake timers (setInterval + performance only) so that render()'s internal setImmediate still works on real clocks. Fake timer tests read refs directly to avoid Vue scheduler flush timing issues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add use-box-metrics/measure parity tests from Ink (+16) Port 16 missing tests from Ink's use-box-metrics, measure-element, and measure-text test suites. Fix useBoxMetrics to reset metrics to zeros when the tracked ref detaches (element unmounts or ref switches to null). 3 tests are skipped because vue-tui's useBoxMetrics uses watchPostEffect (re-runs only when ref.value changes) rather than Ink's layout-commit listener pattern, so sibling-content and resize-driven re-measurement is not yet supported. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add screen-reader parity tests from Ink (+11) Add 11 screen-reader integration tests covering aria-label substitution on Text/Box, ANSI styling omission, multiple/nested components, null component, aria-state variants (busy, disabled, expanded), multi-line roles, and multiselectable listbox. Also fix component prop bug: Vue normalizes kebab-case prop names to camelCase at runtime, so props["aria-label"] was always undefined. Switch Box/Text prop declarations and access to camelCase (ariaLabel, ariaHidden, ariaRole, ariaState). Add isScreenReaderEnabled option to renderToString() so tests can exercise screen-reader output through the component pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add render-to-string parity tests from Ink (+18) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add cursor composable parity tests from Ink (+7) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add 6 missing screen-reader Ink parity tests Add tests for aria-hidden, select input (list with roles/states/labels), aria-state.multiline, aria-state.readonly, aria-state.required, and nested multi-line text rendering in screen-reader mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: fix render-to-string missing Ink parity tests (+10) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: fix cursor composable missing Ink parity tests (+6) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: fix use-box-metrics missing Ink parity tests (+4) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add layout listener so useBoxMetrics updates on resize and sibling changes Adds a layout listener mechanism to TuiRoot matching Ink's architecture: - TuiRoot.layoutListeners Set with addLayoutListener/emitLayoutListeners - emitLayoutListeners called after every yoga.calculateLayout in commit() - useBoxMetrics subscribes to layout listeners, diffs values before updating Enables 4 previously-skipped tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vue-tui/runtime
Early stage — under active development. Bug reports welcome, but not recommended for production use yet.
Vue 3 terminal renderer with Yoga flexbox layout — build rich TUI apps with the same component model you use on the web.
Why
- Vue SFC & JSX —
<template>, TSX, or render functions — your choice - Yoga flexbox — the same layout engine behind React Native, not a CSS-subset hack
- Built-in input system — keyboard handling, focus management, Tab navigation
- Terminal-native — renders directly to stdout, purpose-built for CLI tools and AI agent interfaces
@vue-tui/runtime is a terminal platform renderer parallel to @vue/runtime-dom, comparable to React Ink but adapted for Vue's reactivity model.
Install
npm install @vue-tui/runtime vue
Quick Start
// src/main.ts
import { createApp } from "@vue-tui/runtime";
import App from "./App.vue";
createApp(App).mount();
<!-- src/App.vue -->
<script setup lang="ts">
import { shallowRef } from "vue";
import { Box, Text, useInput } from "@vue-tui/runtime";
const count = shallowRef(0);
useInput((input) => {
if (input === "+") count.value++;
if (input === "-") count.value--;
});
</script>
<template>
<Box>
<Text>Count: </Text>
<Text bold color="green">{{ count }}</Text>
<Text dimColor> (+/- to change)</Text>
</Box>
</template>
Components
| Component | Description |
|---|---|
<Box> |
Flexbox container — direction, wrap, align, justify, gap, padding, margin, borders, background |
<Text> |
Styled text — color, bold, italic, underline, strikethrough, dimColor, wrap/truncate modes |
<Spacer> |
Expands to fill available space (flex-grow: 1) |
<Newline> |
Inserts line breaks (configurable count) |
<Static> |
Renders a list of items once, above the redrawn region |
<Transform> |
Applies a string transform function to each rendered line |
Composables
| Composable | Description |
|---|---|
useInput(handler, opts?) |
Keyboard input — (input, key) with modifier and arrow key detection |
useFocus(opts?) |
Component-level focus — returns { isFocused, focus } |
useFocusManager() |
App-level focus — focusNext(), focusPrevious(), focus(id) |
useExit() |
Programmatic exit — returns exit(error?) |
useTerminalSize() |
Reactive terminal dimensions — { columns, rows } |
useAnimation(opts?) |
Frame-based animation loop — returns { frame, time, delta, reset } |
useBoxMetrics(ref) |
Reactive layout metrics — { width, height, left, top, hasMeasured } |
measureElement(node) |
Imperative read of computed { width, height } from a yoga node |
useCursor() |
Control terminal cursor visibility |
usePaste(handler, opts?) |
Handle clipboard paste events |
useStdin() |
Access stdin stream and raw mode control |
useStdout() |
Write directly to stdout |
useStderr() |
Write directly to stderr |
App Lifecycle
import { createApp } from "@vue-tui/runtime";
// Fire and forget (most common):
createApp(App).mount();
// Wait for the app to exit:
const app = createApp(App);
app.mount();
await app.waitUntilExit();
// Custom streams (for testing):
createApp(App).mount({ stdout, stdin, stderr });
Links
- vue-tui — monorepo root
@vue-tui/cli— dev server with HMR@vue-tui/testing— test harness for terminal components
License
MIT