Files
NetMesh/components/terminal/runtime/terminalImeTextInput.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

191 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.
*
* The deferral must never outlive the keystroke it was armed for: Windows IMEs
* report `Process` / keyCode 229 as the keyup of a key they consumed (or drop
* the keyup), so a deferral flushed only on an exact key match stayed armed and
* blocked typed input from then on (#3103). Any real key release, and any later
* unrelated keydown, now ends the deferral.
*/
export type ImeTextInputKeyEvent = {
type?: string;
key: string;
code?: 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);
}
/** Key releases that carry no keystroke of their own. */
const MODIFIER_ONLY_KEY_RE =
/^(Shift|Control|Alt|Meta|CapsLock|NumLock|ScrollLock|Hyper|Super|Fn|FnLock|Symbol|SymbolLock)$/;
export function isModifierOnlyKey(key: string): boolean {
return MODIFIER_ONLY_KEY_RE.test(key);
}
/**
* DOM keys that stand in for a key the IME consumed, mirroring the non-text
* DOM keys the Kitty encoder already refuses to send as text.
*/
const IME_SENTINEL_KEYS = new Set(["Dead", "Process", "Unidentified", "Compose"]);
export function isImeSentinelKeyUp(event: ImeTextInputKeyEvent): boolean {
if (event.type !== undefined && event.type !== "keyup") return false;
return event.keyCode === 229 || IME_SENTINEL_KEYS.has(event.key);
}
/**
* A deferred punctuation keystroke is over once any real key release arrives.
* The IME remap (insertText) is dispatched before keyup, so a release means the
* IME did not remap the key and the ASCII character must be flushed.
*
* The release key cannot be matched exactly: Windows IMEs report `Process` /
* keyCode 229 as the release of a key they consumed, and some drop the release
* entirely. Requiring an exact key match left the deferral armed, and the
* armed deferral then blocked input (#3103). Composing releases are excluded —
* an active composition still owns the keystroke and resolves it via
* insertText.
*/
export function shouldFlushDeferredImeTextInputOnKeyUp(
deferredKey: string | null | undefined,
event: ImeTextInputKeyEvent,
): boolean {
if (!deferredKey) return false;
if (event.type !== undefined && event.type !== "keyup") return false;
if (event.isComposing === true) return false;
if (event.altKey || event.ctrlKey || event.metaKey) return false;
return !isModifierOnlyKey(event.key);
}
/**
* A deferral that outlived its own keystroke is stale — the IME swallowed the
* release. Flush it when a new, unmodified, non-composing keydown for a
* different key arrives so the pending ASCII character still reaches the PTY.
* A same-key keydown is auto-repeat (or a second IME attempt) and keeps
* re-arming instead.
*/
export function shouldFlushStaleDeferredImeTextInput(
deferredKey: string | null | undefined,
event: ImeTextInputKeyEvent,
): boolean {
if (!deferredKey) return false;
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;
if (isModifierOnlyKey(event.key)) return false;
return event.key !== deferredKey;
}
/**
* A modified keydown is a command (Ctrl+C, Alt+…), not the continuation of the
* lost punctuation keystroke: flushing there would inject the ASCII character
* in front of the interrupt or shortcut. Drop the stale deferral instead — the
* keystroke was already lost to the IME.
*/
export function shouldDiscardStaleDeferredImeTextInput(
deferredKey: string | null | undefined,
event: ImeTextInputKeyEvent,
): boolean {
if (!deferredKey) return false;
if (event.type !== undefined && event.type !== "keydown") return false;
if (event.isComposing === true || event.keyCode === 229) return false;
return Boolean(event.altKey || event.ctrlKey || event.metaKey);
}
/**
* How the keyup that ends a deferral relates to the deferred key.
* - `deferred`: an IME sentinel stood in for the deferred key, so the paired
* release must be encoded from the deferred physical key.
* - `own`: the release belongs to the deferred key itself (matched key or
* physical code), so it already pairs the press the flush emitted.
* - `unrelated`: another held key was released; it keeps its own identity and
* the deferred press needs a separate synthesized release.
*/
export type DeferredKeyupReleaseMode = "deferred" | "own" | "unrelated";
export function resolveDeferredKeyupRelease(
deferredKey: string | null | undefined,
deferredCode: string | null | undefined,
event: ImeTextInputKeyEvent,
): DeferredKeyupReleaseMode {
if (!deferredKey) return "own";
if (event.type !== undefined && event.type !== "keyup") return "own";
if (event.altKey || event.ctrlKey || event.metaKey) return "own";
if (isModifierOnlyKey(event.key)) return "own";
if (event.key === deferredKey) return "own";
if (event.code && deferredCode && event.code === deferredCode) return "own";
if (isImeSentinelKeyUp(event)) return "deferred";
return "unrelated";
}
export function shouldBlockKeyPressForImeTextInput(
deferredKey: string | null | undefined,
event: ImeTextInputKeyEvent,
deferredKeyCode?: number | null,
): boolean {
if (!deferredKey || event.type !== "keypress") return false;
// Composition keypresses carry no committable character on this path.
if (event.isComposing === true || event.keyCode === 229) return true;
// Only the deferred keystroke itself is suppressed. Blocking every keypress
// while armed turned one stale deferral into a terminal that ignored all
// typed input (#3103).
return (
event.key === deferredKey ||
(deferredKeyCode != null && event.keyCode === deferredKeyCode)
);
}
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;
}