Files
vue-tui/packages/runtime-tests/integration/pty/fixtures/erase-with-static.tsx
T
Yunfei He 2372b6b03b feat(runtime): own raw mode for the interactive lifetime by default (rawMode option) (#120)
Add a `rawMode?: 'always' | 'auto'` mount option (replacing the dead, unwired
`rawMode?: boolean`), defaulting to 'always'.

- 'always' (default): the App takes a lifetime raw-mode hold at mount (gated on
  interactive + a TTY stdin), so raw mode is held for the whole run regardless of
  which input composables are mounted. Keystrokes never echo into the rendered
  frame on a no-input/streaming screen, and Ctrl+C is handled consistently on
  every screen (e.g. it reaches an agent's "interrupt generation" handler instead
  of becoming a kernel SIGINT). Because owning raw mode ref()s stdin, the app
  stays alive until an explicit unmount()/exit() — it does NOT auto-exit when idle.
- 'auto': Ink's original lazy model — raw mode is acquired only while a useInput /
  useFocus / usePaste is mounted, so a no-input screen returns to cooked mode and a
  no-input app auto-exits. The opt-out for inline / render-and-exit tools.

This is a deliberate divergence from Ink (the cross-framework norm — Bubble Tea,
Textual, Ratatui, prompt_toolkit all own the terminal for the program lifetime;
Ink's hook-driven model is the outlier). Documented in
.agents/docs/ink-divergences.md.

Implementation: the App holds a `lifetimeFloor` ref via holdRawModeForLifetime();
input composables stack above it. The per-consumer clearInputState is re-based to
the floor so a buffered partial escape (e.g. a lone ESC at a screen transition)
can't bleed into the next consumer — cleared both when the last consumer releases
and when the first consumer re-acquires above the floor (covers same-tick swaps
AND a delayed idle→input transition). The data listener and raw toggle stay on
until teardown, where dispose() releases the floor ref (raw disabled + stdin
unref'd exactly once).

Tests: rawMode-lifecycle ('always' holds raw with no input; 'auto' stays cooked;
no mid-session oscillation; no partial-escape bleed across a swap or an idle gap);
PTY exit-rawmode-always (a no-input 'always' app stays alive and exits on Ctrl+C).
The 6 auto-exit PTY fixtures are pinned to 'auto' (they model render-and-exit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:00:09 +08:00

24 lines
754 B
TypeScript

import process from "node:process";
import { Box, Static, Text, createApp } from "@vue-tui/runtime";
import { Fragment, defineComponent, h } from "vue";
const EraseWithStatic = defineComponent(() => {
return () =>
h(Fragment, [
h(
Static,
{ items: ["A", "B", "C"] },
{ default: ({ item }: { item: string }) => h(Text, { key: item }, () => item) },
),
h(Box, { flexDirection: "column" }, () => [
h(Text, null, () => "D"),
h(Text, null, () => "E"),
h(Text, null, () => "F"),
]),
]);
});
process.stdout.rows = Number(process.argv[2]);
const app = createApp(EraseWithStatic);
app.mount({ rawMode: "auto" }); // relies on auto-exit (default "always" holds raw & never exits)