mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 22:40:09 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ff48cee66 |
No files matched your search
@@ -122,6 +122,7 @@ import {
|
||||
shouldFlushDeferredImeTextInputOnKeyUp,
|
||||
shouldFlushStaleDeferredImeTextInput,
|
||||
} from "./terminalImeTextInput";
|
||||
import { keepImeCommittedTextThroughModifierKeyDowns } from "./imeModifierKeyDownSeenGuard";
|
||||
import { formatSerialLocalEcho } from "./serialLocalEcho";
|
||||
import { getLastChar, removeLastChar, isPrintableInput } from "../../../domain/serialCharMetrics";
|
||||
import { mapTerminalBackspaceInput } from "./terminalBackspaceInput";
|
||||
@@ -631,6 +632,10 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
},
|
||||
});
|
||||
installSearchDecorationTracker(term);
|
||||
// Sogou on Windows commits pending preedit text when Shift toggles
|
||||
// Chinese/English mode; a pure modifier keydown must not arm xterm's
|
||||
// insertText dedupe guard or the committed text is dropped (#3441).
|
||||
keepImeCommittedTextThroughModifierKeyDowns(term);
|
||||
|
||||
type MaybeRenderer = {
|
||||
constructor?: { name?: string };
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
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 {
|
||||
isModifierOnlyKeyDownEvent,
|
||||
keepImeCommittedTextThroughModifierKeyDowns,
|
||||
} from "./imeModifierKeyDownSeenGuard";
|
||||
|
||||
const runtimeSource = readFileSync(
|
||||
join(dirname(fileURLToPath(import.meta.url)), "createXTermRuntime.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("isModifierOnlyKeyDownEvent accepts both DOM key and classic keyCode shapes", () => {
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "Shift", keyCode: 16 }), true);
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "Shift" }), true);
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "Control", keyCode: 17 }), true);
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "Alt", keyCode: 18 }), true);
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "Meta", keyCode: 91 }), true);
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "CapsLock" }), true);
|
||||
});
|
||||
|
||||
test("isModifierOnlyKeyDownEvent rejects real keys and IME process events", () => {
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "a", keyCode: 65 }), false);
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "Process", keyCode: 229 }), false);
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "Enter", keyCode: 13 }), false);
|
||||
assert.equal(isModifierOnlyKeyDownEvent({ key: "Dead", keyCode: 221 }), false);
|
||||
});
|
||||
|
||||
type FakeCore = {
|
||||
_keyDown: (event: Pick<KeyboardEvent, "key" | "keyCode">) => boolean;
|
||||
_keyDownSeen: boolean;
|
||||
_keyUp: () => void;
|
||||
};
|
||||
|
||||
function createFakeCore(initialSeen = false): {
|
||||
core: FakeCore;
|
||||
term: { _core: FakeCore };
|
||||
} {
|
||||
const core: FakeCore = {
|
||||
_keyDownSeen: initialSeen,
|
||||
_keyDown(_event) {
|
||||
// Mirrors xterm: every keydown arms the insertText dedupe guard.
|
||||
this._keyDownSeen = true;
|
||||
return true;
|
||||
},
|
||||
_keyUp() {
|
||||
this._keyDownSeen = false;
|
||||
},
|
||||
};
|
||||
return { core, term: { _core: core } };
|
||||
}
|
||||
|
||||
test("keepImeCommittedTextThroughModifierKeyDowns preserves seen state across Shift keydown", () => {
|
||||
// Sogou Shift commit: a previous real keydown armed the guard, and the
|
||||
// Shift keydown must not re-arm (or clear) it before the composed input.
|
||||
const { core, term } = createFakeCore(false);
|
||||
keepImeCommittedTextThroughModifierKeyDowns(term as never);
|
||||
core._keyDown({ key: "Shift", keyCode: 16 } as KeyboardEvent);
|
||||
assert.equal(core._keyDownSeen, false);
|
||||
});
|
||||
|
||||
test("keepImeCommittedTextThroughModifierKeyDowns keeps guard armed for real keydowns", () => {
|
||||
const { core, term } = createFakeCore(false);
|
||||
keepImeCommittedTextThroughModifierKeyDowns(term as never);
|
||||
core._keyDown({ key: "a", keyCode: 65 } as KeyboardEvent);
|
||||
assert.equal(core._keyDownSeen, true);
|
||||
});
|
||||
|
||||
test("keepImeCommittedTextThroughModifierKeyDowns preserves an already-armed guard", () => {
|
||||
const { core, term } = createFakeCore(true);
|
||||
keepImeCommittedTextThroughModifierKeyDowns(term as never);
|
||||
core._keyDown({ key: "Shift" } as KeyboardEvent);
|
||||
assert.equal(core._keyDownSeen, true);
|
||||
});
|
||||
|
||||
test("keepImeCommittedTextThroughModifierKeyDowns leaves non-modifier behavior intact", () => {
|
||||
const { core, term } = createFakeCore(false);
|
||||
keepImeCommittedTextThroughModifierKeyDowns(term as never);
|
||||
core._keyDown({ key: "Process", keyCode: 229 } as KeyboardEvent);
|
||||
assert.equal(core._keyDownSeen, true);
|
||||
core._keyDown({ key: "Control", keyCode: 17 } as KeyboardEvent);
|
||||
assert.equal(core._keyDownSeen, true, "Control still restored to true (was true)");
|
||||
});
|
||||
|
||||
test("guard tolerates a terminal without an introspectable core", () => {
|
||||
assert.doesNotThrow(() => {
|
||||
keepImeCommittedTextThroughModifierKeyDowns({} as never);
|
||||
});
|
||||
});
|
||||
|
||||
test("runtime installs the guard when the terminal is created", () => {
|
||||
assert.match(
|
||||
runtimeSource,
|
||||
/keepImeCommittedTextThroughModifierKeyDowns\(term\)/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* xterm.js arms its `insertText` dedupe guard (`_keyDownSeen`) on every
|
||||
* keydown and only clears it on keyup. A pure modifier keydown never emits
|
||||
* text itself, but on Windows Sogou Pinyin commits the pending preedit text
|
||||
* when Shift toggles Chinese/English mode — and the composed `input` event
|
||||
* that carries the commit is delivered right after the Shift keydown. The
|
||||
* armed guard then suppresses that event, so the committed text never
|
||||
* reaches `onData` and the preedit text vanishes (#3441).
|
||||
*
|
||||
* Same root cause as upstream xtermjs/xterm.js#6054: a modifier keydown
|
||||
* never produces text itself, so it must not arm the "a real keydown was
|
||||
* already handled" heuristic. Preserve the pre-keydown guard state for
|
||||
* modifier-only keydowns (both `key === "Shift"` and legacy keyCode 16/17/
|
||||
* 18 shapes) so composed IME commits still arrive through `_inputEvent`.
|
||||
*/
|
||||
|
||||
import type { Terminal as XTerm } from "@xterm/xterm";
|
||||
import { isModifierOnlyKey } from "./terminalImeTextInput";
|
||||
|
||||
/** Classic keyCodes that stand for a modifier-only keydown. */
|
||||
const MODIFIER_KEY_CODES = new Set([16, 17, 18, 91, 92, 93, 224]);
|
||||
|
||||
export function isModifierOnlyKeyDownEvent(event: {
|
||||
key: string;
|
||||
keyCode?: number;
|
||||
}): boolean {
|
||||
return (
|
||||
isModifierOnlyKey(event.key)
|
||||
|| (event.keyCode !== undefined && MODIFIER_KEY_CODES.has(event.keyCode))
|
||||
);
|
||||
}
|
||||
|
||||
type XTermInputCore = {
|
||||
_keyDown?: (event: KeyboardEvent) => boolean | undefined;
|
||||
_keyDownSeen?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap xterm's keydown entry so a pure modifier keydown cannot arm the
|
||||
* `insertText` dedupe guard. Installed per-terminal instance; the prototype
|
||||
* method stays untouched.
|
||||
*/
|
||||
export function keepImeCommittedTextThroughModifierKeyDowns(term: XTerm): void {
|
||||
const core = (term as XTerm & { _core?: XTermInputCore })._core;
|
||||
if (!core || typeof core._keyDown !== "function") return;
|
||||
const originalKeyDown = core._keyDown.bind(core);
|
||||
core._keyDown = (event: KeyboardEvent) => {
|
||||
if (!isModifierOnlyKeyDownEvent(event)) return originalKeyDown(event);
|
||||
const seenBefore = core._keyDownSeen;
|
||||
try {
|
||||
return originalKeyDown(event);
|
||||
} finally {
|
||||
core._keyDownSeen = seenBefore;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user