Files
vue-tui/packages/runtime
Yunfei He 7bf033b009 fix(runtime): freeze useAnimation frame on batched pause+interval change (Ink parity) (#143)
Pausing (isActive→false) in the SAME synchronous batch as an interval change froze
the frame at 0 instead of the last live frame. vue-tui split Ink's single render-time
`shouldReset` into TWO `flush:"sync"` watchers; sync fires once-per-mutation, so
`interval.value = X; isActive.value = false` ran the interval watcher first (while
still active) → erroneous start() zeroed the frame, before the isActive watcher
stop()'d.

Replace them with ONE `flush:"post"` watcher on `[isActive, interval]` that coalesces
the batch and fires once with the final values, mirroring Ink's
`shouldReset = isActive && (intervalChanged || becameActive)` (use-animation.ts:77-96):
paused → stop() (freeze, no reset); active + (becameActive || intervalChanged) →
start(). `immediate:true` keeps the initial mount synchronous (one subscribe, no
double-subscribe).

flush:"post" was verified to fire in the BLESSED standalone (no-component) fallback:
Vue's post-flush queue flushes on any reactive mutation's microtask, independent of
component updates.

Adds tests: batched pause+interval (both orders) freezes; resume zeros then advances
at the new interval; same-interval rerender does not reset; and two standalone
(no-render-tree) cases.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:51:57 +08:00
..

@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.

npm version npm downloads

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)
useApp() App lifecycle — { exit(error?), waitUntilRenderFlush() }
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 });

License

MIT