mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-03 12:32:49 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b076b4199 | ||
|
|
7ae41fdd5c | ||
|
|
51e510f029 | ||
|
|
48fde96f42 | ||
|
|
867452d860 | ||
|
|
f770a59847 |
No files matched your search
@@ -95,6 +95,12 @@ import {
|
||||
resolveShiftEnterText,
|
||||
shouldSendShiftEnterText,
|
||||
} from "./shiftEnterText";
|
||||
import {
|
||||
isUnchangedDeferredImeTextInput,
|
||||
shouldBlockKeyPressForImeTextInput,
|
||||
shouldCommitDeferredImeTextInput,
|
||||
shouldDeferKeyDownForImeTextInput,
|
||||
} from "./terminalImeTextInput";
|
||||
import { formatSerialLocalEcho } from "./serialLocalEcho";
|
||||
import { mapTerminalBackspaceInput } from "./terminalBackspaceInput";
|
||||
import { formatTelnetLocalEcho } from "./telnetLocalEcho";
|
||||
@@ -1142,6 +1148,8 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
|
||||
let kittyCompositionPending = false;
|
||||
let kittyCompositionClearTimer: number | undefined;
|
||||
let imeTextInputDeferredKey: string | null = null;
|
||||
let imeTextInputDeferredKittyEvent: KittyKeyboardEvent | null = null;
|
||||
const kittyForwardedKeys = new Map<string, KittyKeyboardForwardedPress>();
|
||||
const broadcastForwardedKeys = new Map<string, KittyKeyboardForwardedPress>();
|
||||
const broadcastEncodedKeys = new Set<string>();
|
||||
@@ -1151,6 +1159,10 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
let suppressNextTerminalDataBroadcast = false;
|
||||
let broadcastLegacyDataPending: string | null = null;
|
||||
let broadcastLegacyDataClearTimer: number | undefined;
|
||||
const clearImeTextInputDeferral = () => {
|
||||
imeTextInputDeferredKey = null;
|
||||
imeTextInputDeferredKittyEvent = null;
|
||||
};
|
||||
const clearBroadcastLegacyDataPending = () => {
|
||||
broadcastLegacyDataPending = null;
|
||||
suppressNextTerminalDataBroadcast = false;
|
||||
@@ -1177,6 +1189,137 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
getDispatcher: () => ctx.onBroadcastInputRef.current,
|
||||
});
|
||||
|
||||
const commitImeTextInput = (text: string) => {
|
||||
const deferredKey = imeTextInputDeferredKey;
|
||||
const deferredKittyEvent = imeTextInputDeferredKittyEvent;
|
||||
clearImeTextInputDeferral();
|
||||
clearBroadcastLegacyDataPending();
|
||||
|
||||
// Deferred punctuation reaches the PTY via this manual commit instead of
|
||||
// xterm's keydown → onUserInput pipeline, so SelectionService never clears.
|
||||
// Match ordinary typed characters unless preserveSelectionOnInput is on.
|
||||
if (
|
||||
!ctx.terminalSettingsRef.current?.preserveSelectionOnInput &&
|
||||
term.hasSelection()
|
||||
) {
|
||||
term.clearSelection();
|
||||
}
|
||||
|
||||
// Unchanged ASCII must keep the Kitty press/release path. Composition
|
||||
// encoding under report-all emits unidentified CSI 0 u (or associated
|
||||
// text without a physical key), so ordinary punctuation breaks in TUIs.
|
||||
if (
|
||||
isUnchangedDeferredImeTextInput(deferredKey, text) &&
|
||||
deferredKittyEvent &&
|
||||
kittyKeyboardProtocolEnabled
|
||||
) {
|
||||
const pressEvent: KittyKeyboardEvent = {
|
||||
...deferredKittyEvent,
|
||||
type: "keydown",
|
||||
};
|
||||
const sequence = encodeKittyKeyEvent(kittyKeyboardMode, pressEvent);
|
||||
if (sequence) {
|
||||
const identity = pressEvent.code || pressEvent.key;
|
||||
upsertKittyKeyboardForwardedPress(
|
||||
kittyForwardedKeys,
|
||||
identity,
|
||||
pressEvent,
|
||||
[],
|
||||
);
|
||||
handleTerminalInputData(sequence, { source: "kitty" });
|
||||
const forwarded = broadcastKittyInput({
|
||||
kind: "key",
|
||||
event: pressEvent,
|
||||
fallbackToLegacy: true,
|
||||
});
|
||||
if (forwarded) {
|
||||
upsertKittyKeyboardForwardedPress(
|
||||
broadcastForwardedKeys,
|
||||
identity,
|
||||
pressEvent,
|
||||
forwarded.targetSessionIds,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUnchangedDeferredImeTextInput(deferredKey, text)) {
|
||||
// Source may write the literal glyph (Kitty off / no sequence), but
|
||||
// broadcast peers still need the physical key + paired release so
|
||||
// report-all/event-type targets do not receive composition text.
|
||||
if (ctx.isBroadcastEnabledRef.current && ctx.onBroadcastInputRef.current) {
|
||||
suppressNextTerminalDataBroadcast = true;
|
||||
}
|
||||
handleTerminalInputData(text);
|
||||
if (deferredKittyEvent) {
|
||||
const pressEvent: KittyKeyboardEvent = {
|
||||
...deferredKittyEvent,
|
||||
type: "keydown",
|
||||
};
|
||||
const identity = pressEvent.code || pressEvent.key;
|
||||
// Event-type mode (flag 2) without report-all leaves printable presses
|
||||
// on the text path, but still emits a paired :3 release on keyup.
|
||||
// Mirror the ordinary keydown handler so that release is not dropped.
|
||||
if (
|
||||
kittyKeyboardProtocolEnabled &&
|
||||
shouldTrackKittyKeyRelease(kittyKeyboardMode, pressEvent)
|
||||
) {
|
||||
upsertKittyKeyboardForwardedPress(
|
||||
kittyForwardedKeys,
|
||||
identity,
|
||||
pressEvent,
|
||||
[],
|
||||
);
|
||||
}
|
||||
const forwarded = broadcastKittyInput({
|
||||
kind: "key",
|
||||
event: pressEvent,
|
||||
fallbackToLegacy: true,
|
||||
});
|
||||
if (forwarded) {
|
||||
upsertKittyKeyboardForwardedPress(
|
||||
broadcastForwardedKeys,
|
||||
identity,
|
||||
pressEvent,
|
||||
forwarded.targetSessionIds,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
broadcastKittyInput({ kind: "text", text });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Actual IME remap — Kitty associated-text composition when negotiated;
|
||||
// otherwise send the committed glyph (report-all alone cannot carry it).
|
||||
const encoded = encodeKittyCompositionText(kittyKeyboardMode, text);
|
||||
if (encoded) {
|
||||
handleTerminalInputData(encoded, { source: "kitty" });
|
||||
} else {
|
||||
if (ctx.isBroadcastEnabledRef.current && ctx.onBroadcastInputRef.current) {
|
||||
suppressNextTerminalDataBroadcast = true;
|
||||
}
|
||||
handleTerminalInputData(text);
|
||||
}
|
||||
broadcastKittyInput({ kind: "text", text });
|
||||
};
|
||||
const flushImeTextInputDeferral = () => {
|
||||
const fallback = imeTextInputDeferredKey;
|
||||
if (!fallback) return;
|
||||
commitImeTextInput(fallback);
|
||||
};
|
||||
const armImeTextInputDeferral = (event: KeyboardEvent) => {
|
||||
clearImeTextInputDeferral();
|
||||
clearBroadcastLegacyDataPending();
|
||||
// Wait for insertText (full-width) before keyup; keyup/blur flushes ASCII
|
||||
// when the IME did not remap the key (English punctuation mode).
|
||||
imeTextInputDeferredKey = event.key;
|
||||
// Keep the physical key even when the source is not in Kitty mode so
|
||||
// broadcast targets can still receive paired key events.
|
||||
imeTextInputDeferredKittyEvent = toKittyKeyboardEvent(event);
|
||||
};
|
||||
|
||||
term.attachCustomKeyEventHandler((e: KeyboardEvent) => {
|
||||
// Preserve mouse selection across keystrokes when enabled. xterm.js
|
||||
// unconditionally clears the selection on user input
|
||||
@@ -1221,6 +1364,11 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
}
|
||||
|
||||
if (e.type === "keyup") {
|
||||
// insertText for this keystroke has already run when present; flush the
|
||||
// deferred ASCII key only when the IME did not remap it.
|
||||
if (imeTextInputDeferredKey !== null && e.key === imeTextInputDeferredKey) {
|
||||
flushImeTextInputDeferral();
|
||||
}
|
||||
const identity = kittyKeyIdentity(e);
|
||||
if (broadcastLegacyDataPending === identity) clearBroadcastLegacyDataPending();
|
||||
const forwardedPress = broadcastForwardedKeys.get(identity);
|
||||
@@ -1246,6 +1394,12 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
return true;
|
||||
}
|
||||
|
||||
// Block keypress so xterm cannot re-emit the half-width ASCII char after we
|
||||
// deferred the matching keydown for IME insertText (#2833).
|
||||
if (shouldBlockKeyPressForImeTextInput(imeTextInputDeferredKey, e)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.type !== "keydown") {
|
||||
return true;
|
||||
}
|
||||
@@ -1557,6 +1711,15 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
}
|
||||
}
|
||||
|
||||
// Sogou/macOS CJK punctuation: keydown still reports ASCII "," while the
|
||||
// full-width "," arrives on insertText. Returning false (without
|
||||
// preventDefault) stops xterm/Kitty from sending the half-width key so the
|
||||
// input listener can commit the real glyph (#2833).
|
||||
if (shouldDeferKeyDownForImeTextInput(e)) {
|
||||
armImeTextInputDeferral(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (kittySequenceForKeyDown) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -1723,6 +1886,9 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
}, 0);
|
||||
};
|
||||
const clearKittyConnectionInputState = () => {
|
||||
// Drop deferred IME punctuation on session reset; do not write into the
|
||||
// next connection. Focus loss uses clearKittyTransientInputState instead.
|
||||
clearImeTextInputDeferral();
|
||||
kittyCompositionPending = false;
|
||||
kittyForwardedKeys.clear();
|
||||
clearKittyKeyboardBroadcastPairingState(
|
||||
@@ -1736,6 +1902,7 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
}
|
||||
};
|
||||
const clearKittyTransientInputState = () => {
|
||||
flushImeTextInputDeferral();
|
||||
flushKittyKeyboardBroadcastReleases(
|
||||
kittyForwardedKeys,
|
||||
(input) => {
|
||||
@@ -1755,6 +1922,17 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
const textarea = term.textarea;
|
||||
const startKittyComposition = () => markKittyCompositionPending();
|
||||
const markKittyTextInput = (event: InputEvent) => {
|
||||
if (shouldCommitDeferredImeTextInput(imeTextInputDeferredKey, event)) {
|
||||
commitImeTextInput(event.data);
|
||||
// Keep the helper textarea empty so the next composition does not treat
|
||||
// the deferred punctuation as pre-existing suffix text.
|
||||
try {
|
||||
if (textarea) textarea.value = "";
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (shouldMarkKittyTextInputEvent(event)) markKittyCompositionPending(true);
|
||||
};
|
||||
textarea?.addEventListener("compositionstart", startKittyComposition);
|
||||
|
||||
@@ -456,8 +456,8 @@ test("broadcast composition text follows each target's negotiated mode", () => {
|
||||
urgentInterrupt: false,
|
||||
});
|
||||
assert.deepEqual(resolve(reportAll), {
|
||||
data: "\u001b[0u",
|
||||
kittyEncoded: true,
|
||||
data: "你a",
|
||||
kittyEncoded: false,
|
||||
urgentInterrupt: false,
|
||||
});
|
||||
assert.deepEqual(resolve(associatedText), {
|
||||
|
||||
@@ -688,8 +688,9 @@ test("associated text supports multiple code points and pure composition text",
|
||||
assert.equal(encodeKittyCompositionText(state, "\n\u0085"), null);
|
||||
});
|
||||
|
||||
test("report-all without associated text emits an unidentified CSI-u composition event", () => {
|
||||
assert.equal(encodeKittyCompositionText(withFlags(8), "你"), "\u001b[0u");
|
||||
test("report-all without associated text leaves composition text for literal fallback", () => {
|
||||
assert.equal(encodeKittyCompositionText(withFlags(8), "你"), null);
|
||||
assert.equal(encodeKittyCompositionText(withFlags(8), ","), null);
|
||||
});
|
||||
|
||||
test("encodes the complete functional, keypad, media, and modifier ranges", () => {
|
||||
|
||||
@@ -766,7 +766,9 @@ export const encodeKittyCompositionText = (
|
||||
): string | null => {
|
||||
const flags = getKittyKeyboardModeFlags(state);
|
||||
if (!(flags & KITTY_KEYBOARD_REPORT_ALL_KEYS_AS_ESC_CODES)) return null;
|
||||
if (!(flags & KITTY_KEYBOARD_REPORT_ASSOCIATED_TEXT)) return "\u001b[0u";
|
||||
// Without associated text, CSI 0 u is unidentified and drops the glyph.
|
||||
// Callers fall back to the literal committed text instead.
|
||||
if (!(flags & KITTY_KEYBOARD_REPORT_ASSOCIATED_TEXT)) return null;
|
||||
const points = textCodePoints(text);
|
||||
return points.length > 0 ? `\u001b[0;;${points.join(":")}u` : null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
isAsciiPunctuationKey,
|
||||
isUnchangedDeferredImeTextInput,
|
||||
shouldBlockKeyPressForImeTextInput,
|
||||
shouldCommitDeferredImeTextInput,
|
||||
shouldDeferKeyDownForImeTextInput,
|
||||
} from "./terminalImeTextInput";
|
||||
|
||||
const runtimeSource = readFileSync(
|
||||
join(dirname(fileURLToPath(import.meta.url)), "createXTermRuntime.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("isAsciiPunctuationKey accepts common remappable punctuation", () => {
|
||||
for (const key of [",", ".", "/", ";", "'", "[", "]", "\\", "-", "=", "`", "?", "!", ":", '"', "<", ">", "{", "}", "|", "_", "+", "~", "@", "#", "$", "%", "^", "&", "*", "(", ")"]) {
|
||||
assert.equal(isAsciiPunctuationKey(key), true, key);
|
||||
}
|
||||
});
|
||||
|
||||
test("isAsciiPunctuationKey rejects letters, digits, space, and CJK", () => {
|
||||
for (const key of ["a", "Z", "0", " ", ",", "、", "?", "Enter", "ArrowLeft"]) {
|
||||
assert.equal(isAsciiPunctuationKey(key), false, key);
|
||||
}
|
||||
});
|
||||
|
||||
test("shouldDeferKeyDownForImeTextInput defers bare ASCII punctuation keydowns", () => {
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: ",", keyCode: 188 }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: "?", keyCode: 191 }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldDeferKeyDownForImeTextInput leaves composition and modified keys alone", () => {
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: ",", keyCode: 229 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: ",", isComposing: true }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: ",", ctrlKey: true }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: "a", keyCode: 65 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keypress", key: "," }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldBlockKeyPressForImeTextInput only while a deferral is armed", () => {
|
||||
assert.equal(shouldBlockKeyPressForImeTextInput(",", { type: "keypress" }), true);
|
||||
assert.equal(shouldBlockKeyPressForImeTextInput(null, { type: "keypress" }), false);
|
||||
assert.equal(shouldBlockKeyPressForImeTextInput(",", { type: "keydown" }), false);
|
||||
});
|
||||
|
||||
test("shouldCommitDeferredImeTextInput accepts insertText payloads while deferred", () => {
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(",", { inputType: "insertText", data: "," }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(",", { inputType: "insertText", data: "," }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(null, { inputType: "insertText", data: "," }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(",", { inputType: "insertFromPaste", data: "," }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(",", { inputType: "insertText", data: null }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("isUnchangedDeferredImeTextInput detects English punctuation flush", () => {
|
||||
assert.equal(isUnchangedDeferredImeTextInput(",", ","), true);
|
||||
assert.equal(isUnchangedDeferredImeTextInput(",", ","), false);
|
||||
assert.equal(isUnchangedDeferredImeTextInput(null, ","), false);
|
||||
});
|
||||
|
||||
test("createXTermRuntime defers ASCII punctuation keydowns to insertText", () => {
|
||||
assert.match(runtimeSource, /shouldDeferKeyDownForImeTextInput\(e\)/);
|
||||
assert.match(runtimeSource, /armImeTextInputDeferral\(e\)/);
|
||||
assert.match(runtimeSource, /shouldBlockKeyPressForImeTextInput\(imeTextInputDeferredKey, e\)/);
|
||||
assert.match(
|
||||
runtimeSource,
|
||||
/shouldCommitDeferredImeTextInput\(imeTextInputDeferredKey, event\)/,
|
||||
);
|
||||
assert.match(runtimeSource, /commitImeTextInput\(event\.data\)/);
|
||||
assert.match(runtimeSource, /isUnchangedDeferredImeTextInput\(deferredKey, text\)/);
|
||||
assert.match(runtimeSource, /imeTextInputDeferredKittyEvent/);
|
||||
// Manual commit bypasses xterm onUserInput; clear selection unless preserved.
|
||||
assert.match(
|
||||
runtimeSource,
|
||||
/!ctx\.terminalSettingsRef\.current\?\.preserveSelectionOnInput/,
|
||||
);
|
||||
assert.match(runtimeSource, /term\.clearSelection\(\)/);
|
||||
const clearSelIdx = runtimeSource.indexOf("term.clearSelection()");
|
||||
const commitIdx = runtimeSource.indexOf("const commitImeTextInput = (text: string)");
|
||||
const firstHandleIdx = runtimeSource.indexOf(
|
||||
"handleTerminalInputData",
|
||||
commitIdx,
|
||||
);
|
||||
assert.ok(
|
||||
commitIdx >= 0 &&
|
||||
clearSelIdx > commitIdx &&
|
||||
firstHandleIdx > clearSelIdx,
|
||||
);
|
||||
// Must run before Kitty/xterm send the half-width key from keydown.
|
||||
const deferIdx = runtimeSource.indexOf("shouldDeferKeyDownForImeTextInput(e)");
|
||||
const kittySendIdx = runtimeSource.indexOf("if (kittySequenceForKeyDown)");
|
||||
assert.ok(deferIdx >= 0 && kittySendIdx > deferIdx);
|
||||
// Unchanged ASCII must encode via Kitty key events, not composition text.
|
||||
const unchangedIdx = runtimeSource.indexOf("isUnchangedDeferredImeTextInput(deferredKey, text)");
|
||||
const compositionIdx = runtimeSource.indexOf(
|
||||
"encodeKittyCompositionText(kittyKeyboardMode, text)",
|
||||
);
|
||||
assert.ok(unchangedIdx >= 0 && compositionIdx > unchangedIdx);
|
||||
// Even when the source writes the literal glyph, broadcast peers still get
|
||||
// the deferred physical key (with legacy fallback), not composition text.
|
||||
const armIdx = runtimeSource.indexOf("const armImeTextInputDeferral");
|
||||
assert.ok(armIdx >= 0);
|
||||
const armSlice = runtimeSource.slice(armIdx, armIdx + 700);
|
||||
assert.match(
|
||||
armSlice,
|
||||
/imeTextInputDeferredKittyEvent = toKittyKeyboardEvent\(event\)/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
armSlice,
|
||||
/imeTextInputDeferredKittyEvent = kittyKeyboardProtocolEnabled/,
|
||||
);
|
||||
const unchangedFallbackIdx = runtimeSource.indexOf(
|
||||
"isUnchangedDeferredImeTextInput(deferredKey, text))",
|
||||
unchangedIdx + 1,
|
||||
);
|
||||
assert.ok(unchangedFallbackIdx > unchangedIdx);
|
||||
assert.match(
|
||||
runtimeSource.slice(unchangedFallbackIdx, unchangedFallbackIdx + 1800),
|
||||
/handleTerminalInputData\(text\);[\s\S]*shouldTrackKittyKeyRelease\(kittyKeyboardMode, pressEvent\)[\s\S]*upsertKittyKeyboardForwardedPress\(\s*kittyForwardedKeys,[\s\S]*broadcastKittyInput\(\{[\s\S]*kind: "key",[\s\S]*fallbackToLegacy: true,/,
|
||||
);
|
||||
// Remap path must fall back to literal text when composition encoding is null
|
||||
// (report-all without associated text).
|
||||
assert.match(
|
||||
runtimeSource.slice(compositionIdx, compositionIdx + 420),
|
||||
/if \(encoded\) \{[\s\S]*handleTerminalInputData\(encoded, \{ source: "kitty" \}\);[\s\S]*\} else \{[\s\S]*handleTerminalInputData\(text\);/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* CJK IMEs (notably Sogou on macOS) often emit a keydown whose `event.key` is
|
||||
* still the half-width ASCII punctuation, then commit the full-width glyph via
|
||||
* an `input`/`insertText` event. xterm.js sends the keydown character and then
|
||||
* drops the input event because `_keyDownSeen` is set — so the PTY receives
|
||||
* "," instead of ",".
|
||||
*
|
||||
* Defer those keydowns to the following insertText. If no remap arrives before
|
||||
* keyup/blur, the original ASCII key is flushed. Composition (keyCode 229 /
|
||||
* isComposing) stays on xterm's CompositionHelper path.
|
||||
*/
|
||||
|
||||
export type ImeTextInputKeyEvent = {
|
||||
type?: string;
|
||||
key: string;
|
||||
keyCode?: number;
|
||||
altKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
isComposing?: boolean;
|
||||
};
|
||||
|
||||
export type ImeTextInputEvent = Pick<InputEvent, "data" | "inputType">;
|
||||
|
||||
/** Printable ASCII punctuation IMEs commonly remap to full-width forms. */
|
||||
const ASCII_PUNCTUATION_RE = /^[\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]$/;
|
||||
|
||||
export function isAsciiPunctuationKey(key: string): boolean {
|
||||
return ASCII_PUNCTUATION_RE.test(key);
|
||||
}
|
||||
|
||||
export function shouldDeferKeyDownForImeTextInput(
|
||||
event: ImeTextInputKeyEvent,
|
||||
): boolean {
|
||||
if (event.type !== undefined && event.type !== "keydown") return false;
|
||||
if (event.isComposing === true || event.keyCode === 229) return false;
|
||||
if (event.altKey || event.ctrlKey || event.metaKey) return false;
|
||||
return isAsciiPunctuationKey(event.key);
|
||||
}
|
||||
|
||||
export function shouldBlockKeyPressForImeTextInput(
|
||||
deferredKey: string | null | undefined,
|
||||
event: Pick<ImeTextInputKeyEvent, "type">,
|
||||
): boolean {
|
||||
return Boolean(deferredKey) && event.type === "keypress";
|
||||
}
|
||||
|
||||
export function shouldCommitDeferredImeTextInput(
|
||||
deferredKey: string | null | undefined,
|
||||
event: ImeTextInputEvent,
|
||||
): event is ImeTextInputEvent & { data: string } {
|
||||
return (
|
||||
Boolean(deferredKey) &&
|
||||
event.inputType === "insertText" &&
|
||||
typeof event.data === "string" &&
|
||||
event.data.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when insertText/flush kept the deferred ASCII key (no CJK remap).
|
||||
* Those commits must not use Kitty composition encoding — under report-all
|
||||
* that emits unidentified CSI 0 u and drops press/release.
|
||||
*/
|
||||
export function isUnchangedDeferredImeTextInput(
|
||||
deferredKey: string | null | undefined,
|
||||
text: string,
|
||||
): boolean {
|
||||
return deferredKey != null && text === deferredKey;
|
||||
}
|
||||
Reference in new issue
Block a user