[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
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:
221
components/terminal/runtime/win32InputMode.test.ts
Normal file
221
components/terminal/runtime/win32InputMode.test.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createRequire } from "node:module";
|
||||
import test from "node:test";
|
||||
|
||||
import { JSDOM } from "jsdom";
|
||||
import type { Terminal as XTermType } from "@xterm/xterm";
|
||||
|
||||
import { dispatchWin32InputModeEvent } from "./win32InputMode";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { Terminal: XTerm } = require("@xterm/xterm") as {
|
||||
Terminal: typeof XTermType;
|
||||
};
|
||||
|
||||
const write = (term: XTermType, data: string): Promise<void> => new Promise((resolve) => {
|
||||
term.write(data, resolve);
|
||||
});
|
||||
|
||||
test("xterm ignores ConPTY's Win32 input request unless the extension is enabled", async () => {
|
||||
const term = new XTerm({ allowProposedApi: true });
|
||||
|
||||
await write(term, "\u001b[?9001h");
|
||||
|
||||
assert.equal(term.modes.win32InputMode, false);
|
||||
term.dispose();
|
||||
});
|
||||
|
||||
test("xterm preserves modified Enter keys as Win32 input records for ConPTY", async () => {
|
||||
const dom = new JSDOM(
|
||||
"<!doctype html><html><body><div id=terminal></div><div id=source></div></body></html>",
|
||||
{
|
||||
pretendToBeVisual: true,
|
||||
},
|
||||
);
|
||||
const window = dom.window;
|
||||
window.matchMedia = () => ({
|
||||
matches: true,
|
||||
media: "",
|
||||
onchange: null,
|
||||
addListener() {},
|
||||
removeListener() {},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
dispatchEvent: () => true,
|
||||
});
|
||||
window.HTMLCanvasElement.prototype.getContext = (() => ({
|
||||
createLinearGradient: () => ({}),
|
||||
measureText: () => ({
|
||||
width: 10,
|
||||
actualBoundingBoxDescent: 2,
|
||||
actualBoundingBoxAscent: 8,
|
||||
}),
|
||||
fillRect() {},
|
||||
clearRect() {},
|
||||
getImageData: () => ({ data: new Uint8ClampedArray([0, 0, 0, 255]) }),
|
||||
})) as typeof window.HTMLCanvasElement.prototype.getContext;
|
||||
|
||||
const globals = [
|
||||
"window",
|
||||
"document",
|
||||
"navigator",
|
||||
"HTMLElement",
|
||||
"HTMLCanvasElement",
|
||||
"KeyboardEvent",
|
||||
"Event",
|
||||
"MouseEvent",
|
||||
"CompositionEvent",
|
||||
"InputEvent",
|
||||
] as const;
|
||||
const previous = new Map<string, PropertyDescriptor | undefined>();
|
||||
for (const name of globals) {
|
||||
previous.set(name, Object.getOwnPropertyDescriptor(globalThis, name));
|
||||
Object.defineProperty(globalThis, name, {
|
||||
value: window[name],
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
previous.set("devicePixelRatio", Object.getOwnPropertyDescriptor(globalThis, "devicePixelRatio"));
|
||||
Object.defineProperty(globalThis, "devicePixelRatio", { value: 1, configurable: true });
|
||||
previous.set("ResizeObserver", Object.getOwnPropertyDescriptor(globalThis, "ResizeObserver"));
|
||||
Object.defineProperty(globalThis, "ResizeObserver", {
|
||||
value: class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const term = new XTerm({
|
||||
allowProposedApi: true,
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
vtExtensions: { kittyKeyboard: true, win32InputMode: true },
|
||||
});
|
||||
try {
|
||||
term.open(window.document.getElementById("terminal") as HTMLElement);
|
||||
const sent: string[] = [];
|
||||
term.onData((data) => sent.push(data));
|
||||
|
||||
await write(term, "\u001b[?9001h");
|
||||
await write(term, "\u001b[>2u");
|
||||
assert.equal(term.modes.win32InputMode, true);
|
||||
|
||||
for (const modifiers of [
|
||||
{},
|
||||
{ shiftKey: true },
|
||||
{ ctrlKey: true },
|
||||
{ altKey: true },
|
||||
]) {
|
||||
term.textarea?.dispatchEvent(new window.KeyboardEvent("keydown", {
|
||||
key: "Enter",
|
||||
code: "Enter",
|
||||
keyCode: 13,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
...modifiers,
|
||||
}));
|
||||
}
|
||||
term.textarea?.dispatchEvent(new window.KeyboardEvent("keyup", {
|
||||
key: "Enter",
|
||||
code: "Enter",
|
||||
keyCode: 13,
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}));
|
||||
|
||||
assert.deepEqual(sent, [
|
||||
"\u001b[13;28;13;1;0;1_",
|
||||
"\u001b[13;28;13;1;16;1_",
|
||||
"\u001b[13;28;10;1;8;1_",
|
||||
"\u001b[13;28;13;1;2;1_",
|
||||
"\u001b[13;28;13;0;16;1_",
|
||||
]);
|
||||
|
||||
const sourceTerm = new XTerm({ allowProposedApi: true, cols: 80, rows: 24 });
|
||||
try {
|
||||
sourceTerm.open(window.document.getElementById("source") as HTMLElement);
|
||||
sourceTerm.focus();
|
||||
assert.equal(window.document.activeElement, sourceTerm.textarea);
|
||||
sent.length = 0;
|
||||
const shiftEnter = {
|
||||
key: "Enter",
|
||||
code: "Enter",
|
||||
keyCode: 13,
|
||||
shiftKey: true,
|
||||
};
|
||||
dispatchWin32InputModeEvent(term, { type: "keydown", ...shiftEnter });
|
||||
dispatchWin32InputModeEvent(term, { type: "keyup", ...shiftEnter });
|
||||
assert.equal(window.document.activeElement, sourceTerm.textarea);
|
||||
assert.deepEqual(sent, [
|
||||
"\u001b[13;28;13;1;16;1_",
|
||||
"\u001b[13;28;13;0;16;1_",
|
||||
]);
|
||||
} finally {
|
||||
sourceTerm.dispose();
|
||||
}
|
||||
|
||||
// Netcatty consumes shortcuts and local controls before xterm. Their
|
||||
// releases must be consumed too, otherwise ConPTY receives an orphaned
|
||||
// native key-up after a Ctrl+C interrupt or sudo/autocomplete Enter.
|
||||
sent.length = 0;
|
||||
const forwardedKeys = new Set<string>();
|
||||
let pendingKeyDownIdentity: string | null = null;
|
||||
term.onData((data) => {
|
||||
if (
|
||||
pendingKeyDownIdentity &&
|
||||
data.startsWith("\u001b[") &&
|
||||
data.endsWith("_")
|
||||
) {
|
||||
forwardedKeys.add(pendingKeyDownIdentity);
|
||||
pendingKeyDownIdentity = null;
|
||||
}
|
||||
});
|
||||
term.attachCustomKeyEventHandler((event) => {
|
||||
const identity = event.code || event.key;
|
||||
if (event.type === "keyup") {
|
||||
pendingKeyDownIdentity = null;
|
||||
return forwardedKeys.delete(identity);
|
||||
}
|
||||
if (event.type !== "keydown") return true;
|
||||
const consumed =
|
||||
(event.ctrlKey && event.key.toLowerCase() === "c") ||
|
||||
(event.key === "Enter" && !event.shiftKey);
|
||||
if (consumed) return false;
|
||||
pendingKeyDownIdentity = identity;
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const event of [
|
||||
new window.KeyboardEvent("keydown", {
|
||||
key: "c", code: "KeyC", keyCode: 67, ctrlKey: true, bubbles: true, cancelable: true,
|
||||
}),
|
||||
new window.KeyboardEvent("keyup", {
|
||||
key: "c", code: "KeyC", keyCode: 67, ctrlKey: true, bubbles: true, cancelable: true,
|
||||
}),
|
||||
new window.KeyboardEvent("keydown", {
|
||||
key: "Enter", code: "Enter", keyCode: 13, bubbles: true, cancelable: true,
|
||||
}),
|
||||
new window.KeyboardEvent("keyup", {
|
||||
key: "Enter", code: "Enter", keyCode: 13, bubbles: true, cancelable: true,
|
||||
}),
|
||||
new window.KeyboardEvent("keydown", {
|
||||
key: "Process", code: "KeyA", keyCode: 229, bubbles: true, cancelable: true,
|
||||
}),
|
||||
new window.KeyboardEvent("keyup", {
|
||||
key: "Process", code: "KeyA", keyCode: 229, bubbles: true, cancelable: true,
|
||||
}),
|
||||
]) {
|
||||
term.textarea?.dispatchEvent(event);
|
||||
}
|
||||
assert.deepEqual(sent, []);
|
||||
} finally {
|
||||
term.dispose();
|
||||
dom.window.close();
|
||||
for (const [name, descriptor] of previous) {
|
||||
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
|
||||
else Reflect.deleteProperty(globalThis, name);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user