fix(runtime): kitty key-release delivers input like Ink, not '' (parity) (#91)
useInput had an undocumented guard that blanked `input` to '' on any kitty key-RELEASE event, so a printable release (and a ctrl+letter release) delivered nothing. Ink has no release special-case -- it classifies a kitty event purely by isPrintable/ctrl+letter, so a printable release delivers `text ?? name` and a ctrl+letter release delivers the letter name (use-input.ts:204-217). Removes the guard to match. The kept "Ctrl+C exits under kitty" divergence is unaffected: the exit check in emitInput is already scoped to `eventType !== "release"`, so a Ctrl+C release flows through as input='c' without spuriously exiting (press still exits). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -378,3 +378,67 @@ test("exitOnCtrlC intercepts \\x03 in raw mode", async () => {
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
await expect(waitUntilExit()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
// --- Kitty release events deliver the key, matching Ink (no release special-case) ---
|
||||
// Ink (use-input.ts:204-217) classifies a kitty event purely by isPrintable /
|
||||
// ctrl+letter, regardless of press/repeat/release. A printable release ('a' up,
|
||||
// CSI 97;1:3 u) therefore delivers input "a", not "". vue-tui previously had an
|
||||
// undocumented `eventType === "release"` guard that blanked input to ""; these
|
||||
// tests lock the Ink-matching behavior.
|
||||
|
||||
test("useInput - kitty printable RELEASE delivers the key (input='a'), not ''", async () => {
|
||||
const calls: Array<{ input: string; key: Key }> = [];
|
||||
const App = defineComponent(() => {
|
||||
useInput((input, key) => calls.push({ input, key }));
|
||||
return () => <Text>listening</Text>;
|
||||
});
|
||||
|
||||
const { stdin } = await render(App);
|
||||
// 'a' release event (codepoint 97, modifier 1, eventType 3 = release)
|
||||
await stdin.write("\x1b[97;1:3u");
|
||||
expect(calls[0]?.input).toBe("a");
|
||||
expect(calls[0]?.key.eventType).toBe("release");
|
||||
});
|
||||
|
||||
test("useInput - kitty ctrl+letter RELEASE delivers the letter name (input='a'), not ''", async () => {
|
||||
const calls: Array<{ input: string; key: Key }> = [];
|
||||
const App = defineComponent(() => {
|
||||
useInput((input, key) => calls.push({ input, key }));
|
||||
return () => <Text>listening</Text>;
|
||||
});
|
||||
|
||||
const { stdin } = await render(App);
|
||||
// Ctrl+A via codepoint 1-26 form (codepoint 1), modifier 5 (ctrl),
|
||||
// eventType 3 (release). Not printable, but the ctrl+letter branch must still
|
||||
// flow the name "a" through on release — same as Ink.
|
||||
await stdin.write("\x1b[1;5:3u");
|
||||
expect(calls[0]?.input).toBe("a");
|
||||
expect(calls[0]?.key.ctrl).toBe(true);
|
||||
expect(calls[0]?.key.eventType).toBe("release");
|
||||
});
|
||||
|
||||
// VERIFY no spurious exit from a Ctrl+C RELEASE under exitOnCtrlC. The kitty
|
||||
// exit guard lives in emitInput (render.ts) and is scoped to `eventType !==
|
||||
// "release"`, so removing the useInput release guard must NOT make a release
|
||||
// exit. A Ctrl+C release should instead be delivered to the handler (input "c").
|
||||
test("useInput - kitty Ctrl+C RELEASE does not exit (delivered to handler); press still exits", async () => {
|
||||
const calls: Array<{ input: string; key: Key }> = [];
|
||||
const App = defineComponent(() => {
|
||||
useInput((input, key) => calls.push({ input, key }));
|
||||
return () => <Text>running</Text>;
|
||||
});
|
||||
const { stdin, waitUntilExit } = await render(App, { exitOnCtrlC: true });
|
||||
|
||||
// Ctrl+C RELEASE (codepoint 99 'c', modifier 5 = ctrl, eventType 3 = release):
|
||||
// must NOT exit; flows to the handler with input "c".
|
||||
await stdin.write("\x1b[99;5:3u");
|
||||
expect(calls[0]?.input).toBe("c");
|
||||
expect(calls[0]?.key.ctrl).toBe(true);
|
||||
expect(calls[0]?.key.eventType).toBe("release");
|
||||
|
||||
// Ctrl+C PRESS still exits (unchanged divergence behavior).
|
||||
await stdin.write("\x1b[99;5:1u");
|
||||
await expect(waitUntilExit()).resolves.toBeUndefined();
|
||||
// Press was intercepted in emitInput and never reached the handler.
|
||||
expect(calls.length).toBe(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user