Files
vue-tui/packages/runtime-tests/integration/pty/input-kitty.test.ts
T
Yunfei He 369442d4b2 fix(runtime): exit on Ctrl+C under kitty for raw-mode-only apps (#75)
Move the exitOnCtrlC guard into the always-on stdin controller (emitInput),
encoding-agnostic via parseKeypress, so Ctrl+C exits under both the legacy
\x03 byte and the kitty CSI-u form regardless of which composable holds raw
mode (useInput / useFocus / usePaste, or none). Single source of truth —
dropped from useInput. Excludes Ctrl+Shift+C; fast-paths \x03 and only parses
escape-prefixed sequences. Adds TDD PTY coverage and updates the divergence doc.

Also: stop tracking docs/superpowers/plans/2026-05-27-ink-test-parity.md —
docs/ must stay out of git (AGENTS.md); it was committed before .gitignore
covered it.
2026-05-31 00:17:21 +08:00

193 lines
6.1 KiB
TypeScript

import { test as it, expect } from "vite-plus/test";
import term from "./helpers/term.ts";
function kittyKey(codepoint: number, modifiers?: number, eventType?: number): string {
let seq = `\x1b[${codepoint}`;
if (modifiers !== undefined || eventType !== undefined) {
seq += `;${modifiers ?? 1}`;
}
if (eventType !== undefined) {
seq += `:${eventType}`;
}
seq += "u";
return seq;
}
// --- Kitty modifiers through useInput ---
it("useInput - handle kitty protocol super modifier", async () => {
const ps = term("use-input-kitty", ["super"]);
ps.write(kittyKey(115, 9));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - handle kitty protocol hyper modifier", async () => {
const ps = term("use-input-kitty", ["hyper"]);
ps.write(kittyKey(104, 17));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - handle kitty protocol capsLock", async () => {
const ps = term("use-input-kitty", ["capsLock"]);
ps.write(kittyKey(97, 65));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - handle kitty protocol numLock", async () => {
const ps = term("use-input-kitty", ["numLock"]);
ps.write(kittyKey(97, 129));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - handle kitty protocol super+ctrl", async () => {
const ps = term("use-input-kitty", ["superCtrl"]);
ps.write(kittyKey(115, 13));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
// --- Kitty event types through useInput ---
it("useInput - handle kitty protocol press event", async () => {
const ps = term("use-input-kitty", ["press"]);
ps.write(kittyKey(97, 1, 1));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - handle kitty protocol repeat event", async () => {
const ps = term("use-input-kitty", ["repeat"]);
ps.write(kittyKey(97, 1, 2));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - release event produces empty input", async () => {
const ps = term("use-input-kitty", ["releaseEmpty"]);
ps.write(kittyKey(97, 1, 3));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
// --- Special keys through useInput ---
it("useInput - handle kitty protocol escape key", async () => {
const ps = term("use-input-kitty", ["escape"]);
ps.write(kittyKey(27));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - handle kitty protocol backspace (codepoint 127)", async () => {
const ps = term("use-input-kitty", ["backspace"]);
ps.write(kittyKey(127));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - handle kitty protocol delete", async () => {
const ps = term("use-input-kitty", ["delete"]);
ps.write("\x1b[3;1:1~");
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
// --- Non-printable keys produce empty input ---
it("useInput - non-printable kitty key (capslock) produces empty input", async () => {
const ps = term("use-input-kitty", ["capslock-empty"]);
ps.write(kittyKey(57358));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - non-printable kitty key (f13) produces empty input", async () => {
const ps = term("use-input-kitty", ["f13-empty"]);
ps.write(kittyKey(57376));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - non-printable kitty key (printscreen) produces empty input", async () => {
const ps = term("use-input-kitty", ["printscreen-empty"]);
ps.write(kittyKey(57361));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
// --- Text input ---
it("useInput - kitty protocol space key produces space input", async () => {
const ps = term("use-input-kitty", ["space"]);
ps.write(kittyKey(32));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - kitty protocol return key produces carriage return input", async () => {
const ps = term("use-input-kitty", ["return"]);
ps.write(kittyKey(13));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("useInput - kitty protocol ctrl+letter via codepoint 1-26 produces input", async () => {
const ps = term("use-input-kitty", ["ctrlLetter"]);
ps.write(kittyKey(1, 5));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
// --- Kitty Ctrl+C with exitOnCtrlC ---
it("useInput - kitty Ctrl+C exits app when exitOnCtrlC is true", async () => {
const ps = term("use-input-kitty", ["kittyCtrlCExit"]);
ps.write(kittyKey(3, 5));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
// --- Ctrl+C exit without useInput (raw mode held by usePaste) ---
// exitOnCtrlC must exit even when no useInput is mounted: the exit lives in the
// always-on stdin controller, encoding-agnostically. The legacy case is the
// control (it already exits via the \x03 byte); the kitty case is the gap Ink
// has — it only checks \x03, so a CSI-u Ctrl+C never exits there.
it("usePaste-only app exits on legacy Ctrl+C (control)", async () => {
const ps = term("use-paste-ctrl-c");
ps.write("\x03");
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
it("usePaste-only app exits on kitty Ctrl+C", async () => {
const ps = term("use-paste-ctrl-c");
ps.write(kittyKey(99, 5));
await ps.waitForExit();
expect(ps.output).toContain("exited");
});
// Ctrl+Shift+C is a distinct combo (commonly "copy"), not Ctrl+C. The kitty
// protocol disambiguates it, so even with exitOnCtrlC on it must reach the
// handler rather than exit. The handler writes a marker to prove delivery.
it("useInput - kitty Ctrl+Shift+C is delivered, not treated as Ctrl+C exit", async () => {
const ps = term("use-input-kitty", ["ctrlShiftC"]);
ps.write(kittyKey(67, 6));
await ps.waitForExit();
expect(ps.output).toContain("__CTRL_SHIFT_C__");
});
// --- Query response suppression ---
it("useInput - query response is silently ignored, next real key works", async () => {
const ps = term("use-input-kitty", ["queryThenKey"]);
ps.write("\x1b[?1u");
ps.write("a");
await ps.waitForExit();
expect(ps.output).toContain("exited");
});