feat(runtime): enable raw mode by default on mount

TUI apps should suppress terminal echo to prevent keystroke pollution
in the rendered UI. Raw mode is now on by default (matching Bubbletea,
Textual, Ratatui), with exitOnCtrlC also defaulting to true.

Pass rawMode: false to mount() for pure-output CLI tools that don't
need input suppression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-25 17:42:13 +08:00
parent ca25ccd96e
commit 5b399e0844
2 changed files with 18 additions and 0 deletions
+6
View File
@@ -151,6 +151,12 @@ vp run -r build # build all packages
vue-tui dev # start an example with HMR
```
## Caveats
- vue-tui enables [raw mode](https://en.wikipedia.org/wiki/Terminal_mode) by default so the terminal won't echo keystrokes into your UI.
- Ctrl+C still exits — `exitOnCtrlC` defaults to `true`.
- For pure-output tools that don't need input suppression, pass `rawMode: false` to `mount()`.
## Credits
vue-tui started as a Vue port of [Ink](https://github.com/vadimdemedes/ink), the library that proved terminal UIs could be built with the same component patterns we use on the web. The component model, yoga-based layout, focus system, rendering pipeline — all of it originates in Ink's design, adapted to follow Vue's philosophy and conventions. Thank you to [Vadim Demedes](https://github.com/vadimdemedes), [Sindre Sorhus](https://github.com/sindresorhus), and the [Ink contributors](https://github.com/vadimdemedes/ink/graphs/contributors) for creating such a solid foundation.
+12
View File
@@ -26,6 +26,7 @@ export interface MountOptions {
stderr?: NodeJS.WriteStream;
debug?: boolean;
exitOnCtrlC?: boolean;
rawMode?: boolean;
}
export interface TuiApp extends Omit<VueApp<TuiNode>, "mount"> {
@@ -55,6 +56,7 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
let mountedExitListener: (() => void) | null = null;
let mountedFocusListener: (() => void) | null = null;
let mountedDebug = false;
let mountedRawMode = false;
// The renderer's onCommit closure is wired at createApp time but only does
// real work after mount swaps in scheduler.schedule. One renderer per app
@@ -85,6 +87,10 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
if (mountedFocusListener) {
mountedFocusListener();
}
if (mountedRawMode && mountedAppContext) {
mountedAppContext.setRawMode(false);
mountedRawMode = false;
}
if (mountedStdinController) {
mountedStdinController.dispose();
}
@@ -110,6 +116,7 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
const stderr = options.stderr ?? process.stderr;
const debug = options.debug ?? false;
const exitOnCtrlC = options.exitOnCtrlC ?? true;
const rawMode = options.rawMode ?? true;
mountedDebug = debug;
const appContext: AppContext = {
@@ -189,6 +196,11 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
appContext.exit(err instanceof Error ? err : new Error(String(err)));
};
if (rawMode && appContext.isRawModeSupported) {
appContext.setRawMode(true);
mountedRawMode = true;
}
// Built-in Tab / Shift+Tab / Escape focus navigation (matches Ink).
// Placed AFTER mount so a sync mount failure doesn't leak the listener.
const focusInputListener = (chunk: Buffer | string) => {