Files
vue-tui/packages/runtime
Yunfei He a181462c37 refactor(vite): signal app-exit over the hot channel, not a process-global (#235)
Replace globalThis.__VUE_TUI_TEARDOWN__ with an in-process hot-channel event:
the runtime emits "vue-tui:exit" (notifyDevExit) when the app genuinely exits,
and the dev plugin closes the server via server.environments.ssr.hot.on(...).
Removes a cross-package process-global using public Vite APIs; the per-server
listener is GC'd with its server (no global slot for a sibling to clobber).

From the @vue-tui/vite "hack" adversarial review, this was the one item flagged
as a clean, avoidable improvement. The other reviewed mechanisms
(force-client-compile, the bindCLIShortcuts no-op, the entry-inject endsWith
match, and the bridge-hmr ws.send forward) are load-bearing and kept as-is. In
particular the bridge-hmr "file-changed -> hotUpdate" reduction was re-examined
adversarially and REJECTED: plugin-vue sends file-changed UNCONDITIONALLY as
metadata (rerender-vs-reload lives in the transformed module's _rerender_only); a
hand-rolled hotUpdate would have to re-derive normalizePath(filename) to match
the client's exact-string compare (any drift -> template edits silently fall back
to a state-resetting reload), and the error leg still needs the ws.send patch.
Net: more code + a platform-dependent regression risk for zero gain.

vp run ready green; the teardown change adversarially reviewed (ship-as-is).

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

@vue-tui/runtime

Public beta — the @vue-tui/runtime API is stabilizing toward 1.0; dev-mode HMR is still experimental. Bug reports welcome.

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() }
useWindowSize() 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() Position the terminal cursor — returns setCursorPosition(pos); pass undefined to hide it
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
useIsScreenReaderEnabled() Reactive boolean — whether screen-reader / accessibility mode is active

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 });

Dev (@vue-tui/vite) note: in a dev entry, prefer fire-and-forget mount(). The dev server already keeps the process alive, and a top-level await app.waitUntilExit() blocks the entry module's evaluation — which wedges Vite's HMR full-reload queue after the first reload. Reserve await app.waitUntilExit() for standalone/production entries (node dist/main.js).

Render to String

Render a component to a single output frame without driving a live terminal — useful for snapshots, logging, or non-interactive output:

import { renderToString } from "@vue-tui/runtime";

const frame = renderToString(App); // synchronous, returns a string

License

MIT