Files
vue-tui/packages/runtime
Yunfei He c76a0f09b2 fix(runtime): raw-mode teardown matches Ink's clearInputState + disableRawMode (#101)
Two related fixes to the raw-mode controller, mirroring Ink's split
(App.tsx:212-224,357):

- Sync input-state clear (P6): on the last useInput release (refs→0), reset the
  input parser, clear the pending escape-flush timer, and detach the stdin
  listeners SYNCHRONOUSLY — only the terminal raw-mode toggle stays deferred.
  Previously everything was deferred in one microtask, so a same-tick useInput
  SWAP (old unmounts → refs 0 → queued; new mounts → refs 0→1; the queued reset
  then short-circuits on refs>0) left the parser un-reset and a partial escape
  buffered before the swap leaked into the replacement handler. Ink's
  clearInputState runs synchronously and unconditionally so this can't happen.

- Force raw-off (P7): the final disable now unconditionally setRawMode(false),
  matching Ink's disableRawMode. The previous prevRaw-restore re-captured
  stdin.isRaw at acquire while raw was still active on a sync false→true→false
  swap, snapshotting `true` and leaving the terminal in RAW mode after exit. No
  test locked the prevRaw-restore (an undocumented vue invention), so the field
  is removed entirely — eliminating the corruption and aligning with Ink.

The swap-keeps-raw-on behavior (deferred toggle short-circuits on refs>0) is
preserved. Tests lock the partial-escape no-leak on swap and the terminal being
restored (final setRawMode is false) via an isRaw-tracking stdin.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 22:25:42 +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