Files
vue-tui/packages/runtime
Yunfei He 04707e82d9 fix: intersect all clip rects in stack for nested overflow:hidden
Previously Output.get() only used the last clip rect (clips.at(-1)),
which meant an inner overflow:hidden box wider than its outer ancestor
could effectively loosen the outer clip. Now all active clips are
intersected via reduce, and writes are skipped entirely when the
intersection is empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:53:41 +08:00
..
2026-05-24 22:58:29 +08:00
2026-05-24 22:58:29 +08:00

@vue-tui/runtime

Vue-idiomatic terminal renderer in the spirit of React Ink. Platform-specific runtime parallel to @vue/runtime-dom.

Install

pnpm add @vue-tui/runtime vue

Quickstart

import { defineComponent, h, ref } from "vue";
import { createApp, Box, Text, useInput } from "@vue-tui/runtime";

const Counter = defineComponent({
  setup() {
    const count = ref(0);
    useInput((input) => {
      if (input === "+") count.value++;
      if (input === "-") count.value--;
    });
    return () => h(Box, null, h(Text, null, `Count: ${count.value}`));
  },
});

createApp(Counter).mount();

API

Single entry point — createApp(root, rootProps?). Returns a TuiApp (Omit<VueApp<TuiNode>, "mount"> plus four TUI methods).

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

const app = createApp(App, { initialState }).use(myPlugin).provide(themeKey, dark);

app.mount(); // all defaults (process.*)
app.mount({ debug: true }); // just flags
app.mount({ stdout: customWritable }); // partial stream override
app.mount({ stdout, stdin, stderr }); // explicit streams (testing)

await app.waitUntilExit();
app.unmount(); // optional — see "Cleanup" below

Cleanup

mount() registers a process.on("exit") listener that runs the same teardown as unmount(). So you only need app.unmount() if you want to tear down the app before the process is ready to exit (mid-program, in tests, or when one process hosts multiple UIs sequentially). For a normal CLI that exits when the user quits or the script ends, cleanup happens automatically.

TuiApp interface

interface TuiApp extends VueApp<TuiNode> {
  mount(options?: MountOptions): ComponentPublicInstance;
  unmount(): void;
  waitUntilExit(): Promise<void>;
}

interface MountOptions {
  stdout?: NodeJS.WriteStream; // default: process.stdout
  stdin?: NodeJS.ReadStream; // default: process.stdin
  stderr?: NodeJS.WriteStream; // default: process.stderr
  debug?: boolean; // default: false
  exitOnCtrlC?: boolean; // default: true
}

All fields optional with per-field fallback. Matches Ink's RenderOptions shape.

Components

Box, Text, Newline, Spacer, Static, Transform.

Composables

useExit, useInput, useFocus, useFocusManager, useStdin, useStdout, useStderr, useTerminalSize.

Tab / Shift+Tab / Escape are handled automatically when any component uses useFocus. useFocus returns { isFocused, focus } and manages raw mode. useFocusManager exposes activeId in addition to focusNext / focusPrevious / focus / enableFocus / disableFocus.

Waiting on exit / handling errors

// Fire-and-forget (most common):
createApp(App).mount();

// Wait for the app to exit:
const app = createApp(App);
app.mount();
await app.waitUntilExit();

// Catch errors thrown from setup / render / useExit(err):
const app = createApp(App);
app.mount();
app.waitUntilExit().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

See docs/superpowers/specs/2026-05-18-vue-tui-core-design.md for the full design.