feat: hide cursor on mount, show on unmount (matching Ink)
Write ESC[?25l to stdout after mount and ESC[?25h during teardown, matching Ink's cursor management. Only active in production mode (debug: false) to avoid interfering with test streams. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { createApp, Text } from "@vue-tui/runtime";
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
function makeTtyStream() {
|
||||
const stream = new PassThrough() as unknown as NodeJS.WriteStream & { chunks: string[] };
|
||||
Object.assign(stream, { isTTY: true, columns: 80, rows: 24 });
|
||||
stream.chunks = [];
|
||||
(stream as unknown as PassThrough).on("data", (chunk: Buffer) =>
|
||||
stream.chunks.push(chunk.toString()),
|
||||
);
|
||||
return stream;
|
||||
}
|
||||
|
||||
test("mount hides cursor, unmount shows cursor", async () => {
|
||||
const stdout = makeTtyStream();
|
||||
const stderr = makeTtyStream();
|
||||
const stdin = new PassThrough() as unknown as NodeJS.ReadStream;
|
||||
Object.assign(stdin, {
|
||||
isTTY: true,
|
||||
setRawMode() {
|
||||
return stdin;
|
||||
},
|
||||
});
|
||||
|
||||
const App = defineComponent(() => () => <Text>hello</Text>);
|
||||
|
||||
const app = createApp(App);
|
||||
app.mount({ stdout, stdin, stderr, debug: false, exitOnCtrlC: false });
|
||||
await nextTick();
|
||||
|
||||
// Cursor should be hidden after mount
|
||||
const afterMount = stdout.chunks.join("");
|
||||
expect(afterMount).toContain("\x1b[?25l");
|
||||
|
||||
app.unmount();
|
||||
await nextTick();
|
||||
|
||||
// Cursor should be shown after unmount
|
||||
const afterUnmount = stdout.chunks.join("");
|
||||
expect(afterUnmount).toContain("\x1b[?25h");
|
||||
});
|
||||
@@ -74,6 +74,10 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
// Vue's unmount may throw on double-unmount; swallow for idempotency.
|
||||
}
|
||||
if (mountedWriter && !mountedDebug) mountedWriter.done();
|
||||
// Show cursor on unmount (matching Ink).
|
||||
if (!mountedDebug && mountedAppContext) {
|
||||
mountedAppContext.stdout.write("\x1b[?25h");
|
||||
}
|
||||
if (mountedRoot) detachYoga(mountedRoot);
|
||||
if (mountedResizeHandler && mountedAppContext) {
|
||||
mountedAppContext.stdout.off("resize", mountedResizeHandler);
|
||||
@@ -204,6 +208,12 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
mountedRawMode = true;
|
||||
}
|
||||
|
||||
// Hide cursor on mount (matching Ink). Only in production mode — in
|
||||
// debug/test mode the stream may not be a real TTY.
|
||||
if (!debug) {
|
||||
stdout.write("\x1b[?25l");
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
|
||||
Reference in New Issue
Block a user