Files
vue-tui/packages/runtime
Yunfei He 8a2efdfeef fix(runtime): make initHmrBridge idempotent (#176)
initHmrBridge registered three Vite HMR listeners (vite:error,
vite:beforeUpdate, vite:beforeFullReload) with no idempotency guard and is
called once per createApp() (dev block in render.ts). createApp() can run
multiple times in one dev process — two apps, an app that unmounts and is
re-created, a tool that restarts the UI, or a test run — and Vite's Node HMR
runtime APPENDS listeners with no dedup, so N calls leaked N copies of every
handler permanently. Every later HMR event then ran each handler N times.

Add a module-level boolean guard so the listeners register at most once for
the module's lifetime, regardless of how many times initHmrBridge is called.

Also parameterize the hot context (defaulting to import.meta.hot) so the body
is reachable under vitest, where import.meta.hot is undefined. HotContext is a
local structural type and import.meta.hot is read via a structural cast so the
module type-checks even when imported directly from runtime-tests, whose
tsconfig doesn't pick up env.d.ts's ambient ImportMeta.hot augmentation.

Out of scope: the setTimeout stale-timer in the vite:beforeUpdate handler is a
separate bug left exactly as-is.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:09:54 +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() }
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() 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