[Init] Initial commit - NetMesh terminal manager
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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,526 @@
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,
resolveDeferredKeyupRelease,
shouldDiscardStaleDeferredImeTextInput,
shouldFlushDeferredImeTextInputOnKeyUp,
shouldFlushStaleDeferredImeTextInput,
} 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", key: "," }),
true,
);
assert.equal(
shouldBlockKeyPressForImeTextInput(null, { type: "keypress", key: "," }),
false,
);
assert.equal(
shouldBlockKeyPressForImeTextInput(",", { type: "keydown", key: "," }),
false,
);
});
test("shouldBlockKeyPressForImeTextInput still blocks the deferred keystroke itself", () => {
assert.equal(
shouldBlockKeyPressForImeTextInput(
"/",
{ type: "keypress", key: "/", keyCode: 191 },
191,
),
true,
);
// Shift+/ reports "?" on keydown, so the deferral arms with "?".
assert.equal(
shouldBlockKeyPressForImeTextInput(
"?",
{ type: "keypress", key: "?", keyCode: 191 },
191,
),
true,
);
assert.equal(
shouldBlockKeyPressForImeTextInput(
"/",
{ type: "keypress", key: "/", isComposing: true },
191,
),
true,
);
});
test("a stale deferral must not swallow unrelated keypresses (#3103)", () => {
// Uppercase letters are routed through keypress by xterm, so a blanket
// keypress block turned one stale deferral into a terminal that ignored
// everything typed afterwards.
assert.equal(
shouldBlockKeyPressForImeTextInput(
"/",
{ type: "keypress", key: "X", keyCode: 88 },
191,
),
false,
);
assert.equal(
shouldBlockKeyPressForImeTextInput(
"/",
{ type: "keypress", key: "a", keyCode: 65 },
191,
),
false,
);
// A matching keyCode keeps the block when an IME rewrites the key label.
assert.equal(
shouldBlockKeyPressForImeTextInput(
"/",
{ type: "keypress", key: "1", keyCode: 191 },
191,
),
true,
);
});
test("a Windows IME release reporting Process must flush the deferred slash (#3103)", () => {
// Windows + CJK IME in vi: keydown still reports the physical key while the
// IME consumes it, then reports Process/229 on the release and never sends
// an insertText the runtime would treat as a remap.
let deferredKey: string | null = null;
const keydown = { type: "keydown", key: "/", keyCode: 191 };
if (shouldDeferKeyDownForImeTextInput(keydown)) deferredKey = keydown.key;
assert.equal(deferredKey, "/");
const release = { type: "keyup", key: "Process", keyCode: 229 };
if (shouldFlushDeferredImeTextInputOnKeyUp(deferredKey, release)) deferredKey = null;
assert.equal(deferredKey, null, "the deferral must not outlive its keystroke");
// Input keeps flowing afterwards.
assert.equal(
shouldBlockKeyPressForImeTextInput(deferredKey, { type: "keypress", key: "x", keyCode: 88 }),
false,
);
});
test("a held key released while the deferral is armed keeps its own keyup identity", () => {
// "/" is deferred (its own release was swallowed) and the user still holds
// "a" with the other hand, so the "a" keyup lands first. The stale deferral
// still ends, but that release is a real keyup, not an IME sentinel: it
// keeps its own identity, and the deferred press is released separately.
const deferredKey = "/";
const heldKeyUp = { type: "keyup", key: "a", keyCode: 65, code: "KeyA" };
assert.equal(
shouldFlushDeferredImeTextInputOnKeyUp(deferredKey, heldKeyUp),
true,
"the stale deferral still ends on a real release",
);
assert.equal(
resolveDeferredKeyupRelease(deferredKey, "Slash", heldKeyUp),
"unrelated",
"an unrelated release must not be rewritten to the deferred key",
);
});
test("only IME sentinel releases take over the deferred key's release identity", () => {
// Windows IMEs report Process/229 (occasionally Unidentified) as the release
// of a key they consumed — those are the only releases that stand in for the
// deferred punctuation key.
assert.equal(
resolveDeferredKeyupRelease("/", "Slash", { type: "keyup", key: "Process", keyCode: 229 }),
"deferred",
);
assert.equal(
resolveDeferredKeyupRelease("/", "Slash", { type: "keyup", key: "Unidentified" }),
"deferred",
);
assert.equal(
resolveDeferredKeyupRelease("/", "Slash", { type: "keyup", key: "/", keyCode: 191 }),
"own",
"a matched release already encodes from the real event",
);
assert.equal(
resolveDeferredKeyupRelease("/", "Slash", {
type: "keyup",
key: "1",
keyCode: 191,
code: "Slash",
}),
"own",
"a release of the same physical key already pairs the flushed press",
);
assert.equal(
resolveDeferredKeyupRelease("/", "Slash", { type: "keyup", key: "Shift", keyCode: 16 }),
"own",
);
assert.equal(
resolveDeferredKeyupRelease("/", "Slash", {
type: "keyup",
key: "a",
keyCode: 65,
code: "KeyA",
ctrlKey: true,
}),
"own",
);
assert.equal(
resolveDeferredKeyupRelease(null, null, { type: "keyup", key: "Process", keyCode: 229 }),
"own",
);
});
test("a deferral left without release or insertText recovers on the next keystroke (#3103)", () => {
let deferredKey: string | null = null;
const keydown = { type: "keydown", key: "/", keyCode: 191 };
if (shouldDeferKeyDownForImeTextInput(keydown)) deferredKey = keydown.key;
assert.equal(deferredKey, "/");
// No insertText and no keyup at all; the user then types a plain letter.
const nextKeyDown = { type: "keydown", key: "a", keyCode: 65 };
if (shouldFlushStaleDeferredImeTextInput(deferredKey, nextKeyDown)) deferredKey = null;
assert.equal(deferredKey, null, "the stale deferral must flush before the new keystroke");
assert.equal(
shouldBlockKeyPressForImeTextInput(deferredKey, { type: "keypress", key: "a", keyCode: 65 }),
false,
);
});
test("a modified keydown discards the stale deferral instead of flushing it", () => {
// The IME swallowed the "/" release and the user then interrupts with
// Ctrl+C. Flushing there would inject "/" in front of the interrupt, and
// keeping the deferral armed would inject it before the next character, so
// the lost keystroke is dropped instead.
assert.equal(
shouldFlushStaleDeferredImeTextInput("/", {
type: "keydown",
key: "c",
keyCode: 67,
ctrlKey: true,
}),
false,
);
assert.equal(
shouldDiscardStaleDeferredImeTextInput("/", {
type: "keydown",
key: "c",
keyCode: 67,
ctrlKey: true,
}),
true,
);
assert.equal(
shouldDiscardStaleDeferredImeTextInput("/", {
type: "keydown",
key: "Tab",
keyCode: 9,
altKey: true,
}),
true,
);
assert.equal(
shouldDiscardStaleDeferredImeTextInput("/", {
type: "keydown",
key: "d",
keyCode: 229,
ctrlKey: true,
isComposing: true,
}),
false,
"a composition still owns the keystroke and resolves via insertText",
);
assert.equal(
shouldDiscardStaleDeferredImeTextInput("/", { type: "keydown", key: "a", keyCode: 65 }),
false,
);
assert.equal(
shouldDiscardStaleDeferredImeTextInput(null, {
type: "keydown",
key: "c",
keyCode: 67,
ctrlKey: true,
}),
false,
);
});
test("auto-repeat and modifier keystrokes keep the deferral armed", () => {
// Same-key keydown is auto-repeat: re-arm instead of flushing, so a held key
// does not emit an extra character per repeat.
assert.equal(
shouldFlushStaleDeferredImeTextInput("/", { type: "keydown", key: "/", keyCode: 191 }),
false,
);
assert.equal(
shouldFlushStaleDeferredImeTextInput("/", { type: "keydown", key: "Shift", keyCode: 16 }),
false,
);
assert.equal(
shouldFlushStaleDeferredImeTextInput("/", {
type: "keydown",
key: "d",
keyCode: 229,
isComposing: true,
}),
false,
);
assert.equal(
shouldFlushDeferredImeTextInputOnKeyUp("/", { type: "keyup", key: "Shift", keyCode: 16 }),
false,
);
assert.equal(
shouldFlushDeferredImeTextInputOnKeyUp("/", {
type: "keyup",
key: "c",
keyCode: 67,
ctrlKey: true,
}),
false,
);
});
test("an active composition still resolves the deferred keystroke via insertText", () => {
// The IME absorbed the punctuation into a composition: the composing release
// must not flush the ASCII key ahead of the committed glyph.
assert.equal(
shouldFlushDeferredImeTextInputOnKeyUp("/", {
type: "keyup",
key: "/",
keyCode: 191,
isComposing: true,
}),
false,
);
assert.equal(
shouldCommitDeferredImeTextInput("/", { inputType: "insertText", data: "、" }),
true,
);
});
test("a matched keyup still flushes the English punctuation fallback (#2833)", () => {
assert.equal(
shouldFlushDeferredImeTextInputOnKeyUp("/", { type: "keyup", key: "/", keyCode: 191 }),
true,
);
assert.equal(
shouldFlushDeferredImeTextInputOnKeyUp(null, { type: "keyup", key: "/", keyCode: 191 }),
false,
);
assert.equal(
shouldFlushDeferredImeTextInputOnKeyUp("/", { type: "keydown", key: "/", keyCode: 191 }),
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\(\s*imeTextInputDeferredKey,\s*e,\s*imeTextInputDeferredKittyEvent\?\.keyCode \?\? null,\s*\)/,
);
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, sanitizedText)",
);
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, \{ perCharacterWrites: shouldSplitImeTextInputForWire\(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 + 560),
/if \(encoded\) \{[\s\S]*handleTerminalInputData\(encoded, \{ source: "kitty" \}\);[\s\S]*\} else \{[\s\S]*handleTerminalInputData\(sanitizedText, \{\s*perCharacterWrites: shouldSplitImeTextInputForWire\(sanitizedText\),?\s*\}\);/,
);
});
test("createXTermRuntime recovers a stuck IME punctuation deferral (#3103)", () => {
// Any real key release ends the deferral, not just an exact key match.
const keyupIdx = runtimeSource.indexOf('if (e.type === "keyup")');
assert.ok(keyupIdx >= 0);
const keyupSlice = runtimeSource.slice(keyupIdx, keyupIdx + 2800);
assert.match(
keyupSlice,
/shouldFlushDeferredImeTextInputOnKeyUp\(imeTextInputDeferredKey, e\)/,
);
assert.match(keyupSlice, /flushImeTextInputDeferral\(\);/);
// Only an IME sentinel release (Process/229/Unidentified) takes over the
// deferred key's release identity; an unrelated keyup keeps its own identity
// and the flushed press is released separately.
assert.match(
keyupSlice,
/resolveDeferredKeyupRelease\(\s*imeTextInputDeferredKey,\s*deferredKittyEvent\?\.code \?\? null,\s*e,?\s*\)/,
);
assert.match(keyupSlice, /releaseMode === "deferred"/);
assert.match(keyupSlice, /releaseMode === "unrelated"/);
assert.match(
keyupSlice,
/\.\.\.deferredKittyEvent,\s*type: "keyup",/,
);
const kittyReleaseIdx = keyupSlice.indexOf("toKittyKeyboardEvent(releaseEvent)");
assert.ok(kittyReleaseIdx > keyupSlice.indexOf("flushImeTextInputDeferral();"));
// The stale flush runs on keydown, after the broadcast guard and before the
// composition handling, so a wedged deferral cannot survive a new keystroke.
const staleIdx = runtimeSource.indexOf(
"shouldFlushStaleDeferredImeTextInput(imeTextInputDeferredKey, e)",
);
assert.ok(staleIdx > keyupIdx);
const keydownGuardIdx = runtimeSource.indexOf("if (handlingKittyBroadcast) return true;", keyupIdx);
const keyCode229Idx = runtimeSource.indexOf("if (e.keyCode === 229) {", keyupIdx);
assert.ok(
keydownGuardIdx > keyupIdx &&
staleIdx > keydownGuardIdx &&
keyCode229Idx > staleIdx,
);
assert.match(
runtimeSource.slice(staleIdx - 400, staleIdx + 200),
/flushImeTextInputDeferral\(\);/,
);
// The recovery runs because the deferred key's release will never arrive, so
// the press emitted by the flush must be paired with a synthesized release
// instead of staying forwarded until focus loss.
const recoverySlice = runtimeSource.slice(staleIdx, staleIdx + 500);
assert.match(recoverySlice, /deferredKittyEvent = imeTextInputDeferredKittyEvent/);
assert.match(recoverySlice, /releaseForwardedKittyPress\(\s*\{\s*\.\.\.deferredKittyEvent,\s*type: "keyup",?\s*\}\s*\)/);
// A modified keydown (Ctrl+C, Alt+…) drops the lost keystroke instead of
// injecting it in front of the interrupt or shortcut.
assert.match(
runtimeSource.slice(staleIdx, staleIdx + 700),
/shouldDiscardStaleDeferredImeTextInput\(imeTextInputDeferredKey, e\)/,
);
});