Files
Yunfei He 3e44c9a266
CI / Check, build & test (push) Has been cancelled
feat(runtime): add mouse input API (#245)
* docs: record mouse input API design

* feat(runtime): add mouse input API

* fix(runtime): align mouse edge cases

* fix(runtime): suppress click after drag

* refactor(runtime): use template refs for dragging

* docs: record mouse API follow-ups

* fix(runtime): align useDraggable semantics

* fix(runtime): align mouse composable APIs

* fix(runtime): keep mouse input handler refs explicit

* fix(runtime): clean up mouse mode state

* fix(examples): use templates for mouse demo
2026-07-06 01:20:27 +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
useMouseInput(handler, opts?) Terminal mouse input — currently SGR wheel events with ref-counted mouse-mode ownership
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