@jecruz/merc-tui 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/autocomplete.d.ts +1 -0
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +85 -24
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/box.d.ts +1 -0
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +6 -1
- package/dist/components/box.js.map +1 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +80 -115
- package/dist/components/editor.js.map +1 -1
- package/dist/components/image.d.ts +12 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +45 -6
- package/dist/components/image.js.map +1 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +7 -55
- package/dist/components/input.js.map +1 -1
- package/dist/components/markdown.d.ts +1 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +18 -6
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +4 -7
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/text.d.ts +1 -0
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +11 -5
- package/dist/components/text.js.map +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +23 -22
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +9 -1
- package/dist/keys.js.map +1 -1
- package/dist/native-modifier-digests.d.ts +6 -0
- package/dist/native-modifier-digests.d.ts.map +1 -0
- package/dist/native-modifier-digests.js +6 -0
- package/dist/native-modifier-digests.js.map +1 -0
- package/dist/native-modifiers.d.ts +11 -0
- package/dist/native-modifiers.d.ts.map +1 -0
- package/dist/native-modifiers.js +98 -0
- package/dist/native-modifiers.js.map +1 -0
- package/dist/stdin-buffer.d.ts +25 -2
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +176 -3
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal-image.d.ts +10 -0
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +102 -17
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +17 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +165 -56
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +28 -7
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +322 -125
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +16 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +115 -29
- package/dist/utils.js.map +1 -1
- package/dist/word-navigation.d.ts +25 -0
- package/dist/word-navigation.d.ts.map +1 -0
- package/dist/word-navigation.js +96 -0
- package/dist/word-navigation.js.map +1 -0
- package/native/darwin/README.md +13 -0
- package/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/native/darwin/prebuilds/manifest.json +24 -0
- package/package.json +4 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { NATIVE_MODIFIER_SHA256 } from "./native-modifier-digests.js";
|
|
8
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
9
|
+
let nativeModifiersHelper;
|
|
10
|
+
export function isNativeModifiersHelper(value) {
|
|
11
|
+
if (typeof value !== "object" || value === null)
|
|
12
|
+
return false;
|
|
13
|
+
const candidate = value.isModifierPressed;
|
|
14
|
+
return typeof candidate === "function";
|
|
15
|
+
}
|
|
16
|
+
/** Loads verified helper bytes from a private stage; this is not isolation from hostile same-UID processes. */
|
|
17
|
+
export function loadVerifiedNativeModifierCandidate(modulePath, manifestPath, arch, loadModule = (path) => cjsRequire(path)) {
|
|
18
|
+
try {
|
|
19
|
+
const manifestStats = lstatSync(manifestPath);
|
|
20
|
+
if (manifestStats.isSymbolicLink() || !manifestStats.isFile())
|
|
21
|
+
return undefined;
|
|
22
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
23
|
+
const expectedFile = `darwin-${arch}/darwin-modifiers.node`;
|
|
24
|
+
const entry = manifest.prebuilds?.find((candidate) => candidate.arch === arch && candidate.file === expectedFile);
|
|
25
|
+
if (typeof entry?.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha256))
|
|
26
|
+
return undefined;
|
|
27
|
+
if (entry.sha256 !== NATIVE_MODIFIER_SHA256[arch])
|
|
28
|
+
return undefined;
|
|
29
|
+
const stats = lstatSync(modulePath);
|
|
30
|
+
if (stats.isSymbolicLink() || !stats.isFile())
|
|
31
|
+
return undefined;
|
|
32
|
+
const bytes = readFileSync(modulePath);
|
|
33
|
+
if (createHash("sha256").update(bytes).digest("hex") !== entry.sha256)
|
|
34
|
+
return undefined;
|
|
35
|
+
const stagingDirectory = mkdtempSync(path.join(tmpdir(), "merc-native-modifiers-"));
|
|
36
|
+
try {
|
|
37
|
+
const stagedModulePath = path.join(stagingDirectory, "darwin-modifiers.node");
|
|
38
|
+
writeFileSync(stagedModulePath, bytes, { flag: "wx", mode: 0o400 });
|
|
39
|
+
return loadModule(stagedModulePath);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
rmSync(stagingDirectory, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function loadNativeModifiersHelper() {
|
|
50
|
+
if (nativeModifiersHelper !== undefined)
|
|
51
|
+
return nativeModifiersHelper ?? undefined;
|
|
52
|
+
nativeModifiersHelper = null;
|
|
53
|
+
if (process.platform !== "darwin")
|
|
54
|
+
return undefined;
|
|
55
|
+
const arch = process.arch;
|
|
56
|
+
if (arch !== "x64" && arch !== "arm64")
|
|
57
|
+
return undefined;
|
|
58
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
const nativePath = path.join("native", "darwin", "prebuilds", `darwin-${arch}`, "darwin-modifiers.node");
|
|
60
|
+
const packageRoot = path.join(moduleDir, "..");
|
|
61
|
+
const executableDir = path.dirname(process.execPath);
|
|
62
|
+
const candidates = [
|
|
63
|
+
{
|
|
64
|
+
modulePath: path.join(packageRoot, nativePath),
|
|
65
|
+
manifestPath: path.join(packageRoot, "native", "darwin", "prebuilds", "manifest.json"),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
modulePath: path.join(moduleDir, nativePath),
|
|
69
|
+
manifestPath: path.join(moduleDir, "native", "darwin", "prebuilds", "manifest.json"),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
modulePath: path.join(executableDir, nativePath),
|
|
73
|
+
manifestPath: path.join(executableDir, "native", "darwin", "prebuilds", "manifest.json"),
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
const helper = loadVerifiedNativeModifierCandidate(candidate.modulePath, candidate.manifestPath, arch);
|
|
78
|
+
if (isNativeModifiersHelper(helper)) {
|
|
79
|
+
nativeModifiersHelper = helper;
|
|
80
|
+
return helper;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
export function isNativeModifierPressed(key) {
|
|
86
|
+
if (process.env.PI_TUI_DISABLE_NATIVE_MODIFIERS === "1")
|
|
87
|
+
return false;
|
|
88
|
+
const helper = loadNativeModifiersHelper();
|
|
89
|
+
if (!helper)
|
|
90
|
+
return false;
|
|
91
|
+
try {
|
|
92
|
+
return helper.isModifierPressed(key) === true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=native-modifiers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-modifiers.js","sourceRoot":"","sources":["../src/native-modifiers.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { lstatSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { NATIVE_MODIFIER_SHA256 } from \"./native-modifier-digests.js\";\n\nconst cjsRequire = createRequire(import.meta.url);\n\nexport type ModifierKey = \"shift\" | \"command\" | \"control\" | \"option\";\n\ntype NativeModifiersHelper = {\n\tisModifierPressed: (name: ModifierKey) => boolean;\n};\n\ntype NativeModifierManifest = {\n\tprebuilds?: Array<{ arch?: unknown; file?: unknown; sha256?: unknown }>;\n};\n\nlet nativeModifiersHelper: NativeModifiersHelper | null | undefined;\n\nexport function isNativeModifiersHelper(value: unknown): value is NativeModifiersHelper {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst candidate = (value as { isModifierPressed?: unknown }).isModifierPressed;\n\treturn typeof candidate === \"function\";\n}\n\n/** Loads verified helper bytes from a private stage; this is not isolation from hostile same-UID processes. */\nexport function loadVerifiedNativeModifierCandidate(\n\tmodulePath: string,\n\tmanifestPath: string,\n\tarch: \"x64\" | \"arm64\",\n\tloadModule: (path: string) => unknown = (path) => cjsRequire(path),\n): unknown | undefined {\n\ttry {\n\t\tconst manifestStats = lstatSync(manifestPath);\n\t\tif (manifestStats.isSymbolicLink() || !manifestStats.isFile()) return undefined;\n\t\tconst manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as NativeModifierManifest;\n\t\tconst expectedFile = `darwin-${arch}/darwin-modifiers.node`;\n\t\tconst entry = manifest.prebuilds?.find((candidate) => candidate.arch === arch && candidate.file === expectedFile);\n\t\tif (typeof entry?.sha256 !== \"string\" || !/^[a-f0-9]{64}$/.test(entry.sha256)) return undefined;\n\t\tif (entry.sha256 !== NATIVE_MODIFIER_SHA256[arch]) return undefined;\n\t\tconst stats = lstatSync(modulePath);\n\t\tif (stats.isSymbolicLink() || !stats.isFile()) return undefined;\n\t\tconst bytes = readFileSync(modulePath);\n\t\tif (createHash(\"sha256\").update(bytes).digest(\"hex\") !== entry.sha256) return undefined;\n\t\tconst stagingDirectory = mkdtempSync(path.join(tmpdir(), \"merc-native-modifiers-\"));\n\t\ttry {\n\t\t\tconst stagedModulePath = path.join(stagingDirectory, \"darwin-modifiers.node\");\n\t\t\twriteFileSync(stagedModulePath, bytes, { flag: \"wx\", mode: 0o400 });\n\t\t\treturn loadModule(stagedModulePath);\n\t\t} finally {\n\t\t\trmSync(stagingDirectory, { recursive: true, force: true });\n\t\t}\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function loadNativeModifiersHelper(): NativeModifiersHelper | undefined {\n\tif (nativeModifiersHelper !== undefined) return nativeModifiersHelper ?? undefined;\n\tnativeModifiersHelper = null;\n\tif (process.platform !== \"darwin\") return undefined;\n\tconst arch = process.arch;\n\tif (arch !== \"x64\" && arch !== \"arm64\") return undefined;\n\n\tconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\tconst nativePath = path.join(\"native\", \"darwin\", \"prebuilds\", `darwin-${arch}`, \"darwin-modifiers.node\");\n\tconst packageRoot = path.join(moduleDir, \"..\");\n\tconst executableDir = path.dirname(process.execPath);\n\tconst candidates = [\n\t\t{\n\t\t\tmodulePath: path.join(packageRoot, nativePath),\n\t\t\tmanifestPath: path.join(packageRoot, \"native\", \"darwin\", \"prebuilds\", \"manifest.json\"),\n\t\t},\n\t\t{\n\t\t\tmodulePath: path.join(moduleDir, nativePath),\n\t\t\tmanifestPath: path.join(moduleDir, \"native\", \"darwin\", \"prebuilds\", \"manifest.json\"),\n\t\t},\n\t\t{\n\t\t\tmodulePath: path.join(executableDir, nativePath),\n\t\t\tmanifestPath: path.join(executableDir, \"native\", \"darwin\", \"prebuilds\", \"manifest.json\"),\n\t\t},\n\t];\n\n\tfor (const candidate of candidates) {\n\t\tconst helper = loadVerifiedNativeModifierCandidate(candidate.modulePath, candidate.manifestPath, arch);\n\t\tif (isNativeModifiersHelper(helper)) {\n\t\t\tnativeModifiersHelper = helper;\n\t\t\treturn helper;\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\nexport function isNativeModifierPressed(key: ModifierKey): boolean {\n\tif (process.env.PI_TUI_DISABLE_NATIVE_MODIFIERS === \"1\") return false;\n\tconst helper = loadNativeModifiersHelper();\n\tif (!helper) return false;\n\ttry {\n\t\treturn helper.isModifierPressed(key) === true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"]}
|
package/dist/stdin-buffer.d.ts
CHANGED
|
@@ -19,10 +19,23 @@
|
|
|
19
19
|
import { EventEmitter } from "events";
|
|
20
20
|
export type StdinBufferOptions = {
|
|
21
21
|
/**
|
|
22
|
-
* Maximum time to wait for
|
|
23
|
-
*
|
|
22
|
+
* Maximum time to wait for an incomplete keyboard or mouse sequence
|
|
23
|
+
* (default: 10ms). Strings with a recognized terminal reply header wait for
|
|
24
|
+
* their terminator instead, so delayed replies cannot become keypresses.
|
|
25
|
+
* Ambiguous legacy Alt-key input still uses this timeout.
|
|
24
26
|
*/
|
|
25
27
|
timeout?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Maximum time to wait after a lone ESC before treating it as Escape
|
|
30
|
+
* (default: 10ms). Increase for high-latency Alt+key input (SSH).
|
|
31
|
+
*/
|
|
32
|
+
escapeTimeout?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Maximum time input typed after an unterminated terminal reply waits for the reply's terminator
|
|
35
|
+
* (default: 1000ms). After it, the reply is discarded and the held input is delivered as keystrokes.
|
|
36
|
+
* A reply with no input after it keeps waiting, so delayed reply tails stay intact.
|
|
37
|
+
*/
|
|
38
|
+
replyResyncTimeout?: number;
|
|
26
39
|
};
|
|
27
40
|
export type StdinBufferEventMap = {
|
|
28
41
|
data: [string];
|
|
@@ -36,11 +49,21 @@ export declare class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
36
49
|
private buffer;
|
|
37
50
|
private timeout;
|
|
38
51
|
private readonly timeoutMs;
|
|
52
|
+
private readonly escapeTimeoutMs;
|
|
39
53
|
private pasteMode;
|
|
40
54
|
private pasteBuffer;
|
|
41
55
|
private pendingKittyPrintableCodepoint;
|
|
56
|
+
private readonly replyResyncTimeoutMs;
|
|
57
|
+
/** Buffer length at which a pending terminal reply ends; input after it may be keystrokes. */
|
|
58
|
+
private replyEnd;
|
|
59
|
+
private replyResyncTimer;
|
|
42
60
|
constructor(options?: StdinBufferOptions);
|
|
43
61
|
process(data: string | Buffer): void;
|
|
62
|
+
/** Holds input that arrived after an unterminated reply until the reply ends or the resync bound passes. */
|
|
63
|
+
private holdInputAfterReply;
|
|
64
|
+
/** Discards an unterminated reply and delivers the input that arrived after it as keystrokes. */
|
|
65
|
+
private resyncPendingReply;
|
|
66
|
+
private stopWaitingForReply;
|
|
44
67
|
private emitDataSequence;
|
|
45
68
|
flush(): string[];
|
|
46
69
|
clear(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stdin-buffer.d.ts","sourceRoot":"","sources":["../src/stdin-buffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AA6OtC,MAAM,MAAM,kBAAkB,GAAG;IAChC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IACjC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;IACf,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC;CAChB,CAAC;AAEF;;;GAGG;AACH,qBAAa,WAAY,SAAQ,YAAY,CAAC,mBAAmB,CAAC;IACjE,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAA8C;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,8BAA8B,CAAqB;IAE3D,YAAY,OAAO,GAAE,kBAAuB,EAG3C;IAEM,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoG1C;IAED,OAAO,CAAC,gBAAgB;IAWxB,KAAK,IAAI,MAAM,EAAE,CAchB;IAED,KAAK,IAAI,IAAI,CASZ;IAED,SAAS,IAAI,MAAM,CAElB;IAED,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["/**\n * StdinBuffer buffers input and emits complete sequences.\n *\n * This is necessary because stdin data events can arrive in partial chunks,\n * especially for escape sequences like mouse events. Without buffering,\n * partial sequences can be misinterpreted as regular keypresses.\n *\n * For example, the mouse SGR sequence `\\x1b[<35;20;5m` might arrive as:\n * - Event 1: `\\x1b`\n * - Event 2: `[<35`\n * - Event 3: `;20;5m`\n *\n * The buffer accumulates these until a complete sequence is detected.\n * Call the `process()` method to feed input data.\n *\n * Based on code from OpenTUI (https://github.com/anomalyco/opentui)\n * MIT License - Copyright (c) 2025 opentui\n */\n\nimport { EventEmitter } from \"events\";\n\nconst ESC = \"\\x1b\";\nconst BRACKETED_PASTE_START = \"\\x1b[200~\";\nconst BRACKETED_PASTE_END = \"\\x1b[201~\";\n\n/**\n * Check if a string is a complete escape sequence or needs more data\n */\nfunction isCompleteSequence(data: string): \"complete\" | \"incomplete\" | \"not-escape\" {\n\tif (!data.startsWith(ESC)) {\n\t\treturn \"not-escape\";\n\t}\n\n\tif (data.length === 1) {\n\t\treturn \"incomplete\";\n\t}\n\n\tconst afterEsc = data.slice(1);\n\n\t// CSI sequences: ESC [\n\tif (afterEsc.startsWith(\"[\")) {\n\t\t// Check for old-style mouse sequence: ESC[M + 3 bytes\n\t\tif (afterEsc.startsWith(\"[M\")) {\n\t\t\t// Old-style mouse needs ESC[M + 3 bytes = 6 total\n\t\t\treturn data.length >= 6 ? \"complete\" : \"incomplete\";\n\t\t}\n\t\treturn isCompleteCsiSequence(data);\n\t}\n\n\t// OSC sequences: ESC ]\n\tif (afterEsc.startsWith(\"]\")) {\n\t\treturn isCompleteOscSequence(data);\n\t}\n\n\t// DCS sequences: ESC P ... ESC \\ (includes XTVersion responses)\n\tif (afterEsc.startsWith(\"P\")) {\n\t\treturn isCompleteDcsSequence(data);\n\t}\n\n\t// APC sequences: ESC _ ... ESC \\ (includes Kitty graphics responses)\n\tif (afterEsc.startsWith(\"_\")) {\n\t\treturn isCompleteApcSequence(data);\n\t}\n\n\t// SS3 sequences: ESC O\n\tif (afterEsc.startsWith(\"O\")) {\n\t\t// ESC O followed by a single character\n\t\treturn afterEsc.length >= 2 ? \"complete\" : \"incomplete\";\n\t}\n\n\t// Meta key sequences: ESC followed by a single character\n\tif (afterEsc.length === 1) {\n\t\treturn \"complete\";\n\t}\n\n\t// Unknown escape sequence - treat as complete\n\treturn \"complete\";\n}\n\n/**\n * Check if CSI sequence is complete\n * CSI sequences: ESC [ ... followed by a final byte (0x40-0x7E)\n */\nfunction isCompleteCsiSequence(data: string): \"complete\" | \"incomplete\" {\n\tif (!data.startsWith(`${ESC}[`)) {\n\t\treturn \"complete\";\n\t}\n\n\t// Need at least ESC [ and one more character\n\tif (data.length < 3) {\n\t\treturn \"incomplete\";\n\t}\n\n\tconst payload = data.slice(2);\n\n\t// CSI sequences end with a byte in the range 0x40-0x7E (@-~)\n\t// This includes all letters and several special characters\n\tconst lastChar = payload[payload.length - 1];\n\tconst lastCharCode = lastChar.charCodeAt(0);\n\n\tif (lastCharCode >= 0x40 && lastCharCode <= 0x7e) {\n\t\t// Special handling for SGR mouse sequences\n\t\t// Format: ESC[<B;X;Ym or ESC[<B;X;YM\n\t\tif (payload.startsWith(\"<\")) {\n\t\t\t// Must have format: <digits;digits;digits[Mm]\n\t\t\tconst mouseMatch = /^<\\d+;\\d+;\\d+[Mm]$/.test(payload);\n\t\t\tif (mouseMatch) {\n\t\t\t\treturn \"complete\";\n\t\t\t}\n\t\t\t// If it ends with M or m but doesn't match the pattern, still incomplete\n\t\t\tif (lastChar === \"M\" || lastChar === \"m\") {\n\t\t\t\t// Check if we have the right structure\n\t\t\t\tconst parts = payload.slice(1, -1).split(\";\");\n\t\t\t\tif (parts.length === 3 && parts.every((p) => /^\\d+$/.test(p))) {\n\t\t\t\t\treturn \"complete\";\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn \"incomplete\";\n\t\t}\n\n\t\treturn \"complete\";\n\t}\n\n\treturn \"incomplete\";\n}\n\n/**\n * Check if OSC sequence is complete\n * OSC sequences: ESC ] ... ST (where ST is ESC \\ or BEL)\n */\nfunction isCompleteOscSequence(data: string): \"complete\" | \"incomplete\" {\n\tif (!data.startsWith(`${ESC}]`)) {\n\t\treturn \"complete\";\n\t}\n\n\t// OSC sequences end with ST (ESC \\) or BEL (\\x07)\n\tif (data.endsWith(`${ESC}\\\\`) || data.endsWith(\"\\x07\")) {\n\t\treturn \"complete\";\n\t}\n\n\treturn \"incomplete\";\n}\n\n/**\n * Check if DCS (Device Control String) sequence is complete\n * DCS sequences: ESC P ... ST (where ST is ESC \\)\n * Used for XTVersion responses like ESC P >| ... ESC \\\n */\nfunction isCompleteDcsSequence(data: string): \"complete\" | \"incomplete\" {\n\tif (!data.startsWith(`${ESC}P`)) {\n\t\treturn \"complete\";\n\t}\n\n\t// DCS sequences end with ST (ESC \\)\n\tif (data.endsWith(`${ESC}\\\\`)) {\n\t\treturn \"complete\";\n\t}\n\n\treturn \"incomplete\";\n}\n\n/**\n * Check if APC (Application Program Command) sequence is complete\n * APC sequences: ESC _ ... ST (where ST is ESC \\)\n * Used for Kitty graphics responses like ESC _ G ... ESC \\\n */\nfunction isCompleteApcSequence(data: string): \"complete\" | \"incomplete\" {\n\tif (!data.startsWith(`${ESC}_`)) {\n\t\treturn \"complete\";\n\t}\n\n\t// APC sequences end with ST (ESC \\)\n\tif (data.endsWith(`${ESC}\\\\`)) {\n\t\treturn \"complete\";\n\t}\n\n\treturn \"incomplete\";\n}\n\n/**\n * Split accumulated buffer into complete sequences\n */\nfunction parseUnmodifiedKittyPrintableCodepoint(sequence: string): number | undefined {\n\tconst match = sequence.match(/^\\x1b\\[(\\d+)(?::\\d*)?(?::\\d+)?u$/);\n\tif (!match) return undefined;\n\n\tconst codepoint = parseInt(match[1]!, 10);\n\treturn codepoint >= 32 ? codepoint : undefined;\n}\n\nfunction extractCompleteSequences(buffer: string): { sequences: string[]; remainder: string } {\n\tconst sequences: string[] = [];\n\tlet pos = 0;\n\n\twhile (pos < buffer.length) {\n\t\tconst remaining = buffer.slice(pos);\n\n\t\t// Try to extract a sequence starting at this position\n\t\tif (remaining.startsWith(ESC)) {\n\t\t\t// Find the end of this escape sequence\n\t\t\tlet seqEnd = 1;\n\t\t\twhile (seqEnd <= remaining.length) {\n\t\t\t\tconst candidate = remaining.slice(0, seqEnd);\n\t\t\t\tconst status = isCompleteSequence(candidate);\n\n\t\t\t\tif (status === \"complete\") {\n\t\t\t\t\t// WezTerm with enable_kitty_keyboard sends the Escape key press as a\n\t\t\t\t\t// raw '\\x1b' byte (simple text path in encode_kitty, ignoring\n\t\t\t\t\t// DISAMBIGUATE_ESCAPE_CODES) and the release as a full Kitty CSI-u\n\t\t\t\t\t// sequence. These arrive concatenated as '\\x1b\\x1b[27;...u'.\n\t\t\t\t\t// The buffer would normally treat '\\x1b\\x1b' as a complete meta-key\n\t\t\t\t\t// sequence (ESC + single char), leaving '[27;...u' to be typed as\n\t\t\t\t\t// plain text. If the character immediately following '\\x1b\\x1b'\n\t\t\t\t\t// would begin a new escape sequence, emit only the first ESC and\n\t\t\t\t\t// restart from the second.\n\t\t\t\t\tif (candidate === \"\\x1b\\x1b\") {\n\t\t\t\t\t\tconst nextChar = remaining[seqEnd];\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tnextChar === \"[\" || // CSI\n\t\t\t\t\t\t\tnextChar === \"]\" || // OSC\n\t\t\t\t\t\t\tnextChar === \"O\" || // SS3\n\t\t\t\t\t\t\tnextChar === \"P\" || // DCS\n\t\t\t\t\t\t\tnextChar === \"_\" // APC\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tsequences.push(ESC);\n\t\t\t\t\t\t\tpos += 1;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tsequences.push(candidate);\n\t\t\t\t\tpos += seqEnd;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (status === \"incomplete\") {\n\t\t\t\t\tseqEnd++;\n\t\t\t\t} else {\n\t\t\t\t\t// Should not happen when starting with ESC\n\t\t\t\t\tsequences.push(candidate);\n\t\t\t\t\tpos += seqEnd;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (seqEnd > remaining.length) {\n\t\t\t\treturn { sequences, remainder: remaining };\n\t\t\t}\n\t\t} else {\n\t\t\t// Not an escape sequence - take a single character\n\t\t\tsequences.push(remaining[0]!);\n\t\t\tpos++;\n\t\t}\n\t}\n\n\treturn { sequences, remainder: \"\" };\n}\n\nexport type StdinBufferOptions = {\n\t/**\n\t * Maximum time to wait for sequence completion (default: 10ms)\n\t * After this time, the buffer is flushed even if incomplete\n\t */\n\ttimeout?: number;\n};\n\nexport type StdinBufferEventMap = {\n\tdata: [string];\n\tpaste: [string];\n};\n\n/**\n * Buffers stdin input and emits complete sequences via the 'data' event.\n * Handles partial escape sequences that arrive across multiple chunks.\n */\nexport class StdinBuffer extends EventEmitter<StdinBufferEventMap> {\n\tprivate buffer: string = \"\";\n\tprivate timeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate readonly timeoutMs: number;\n\tprivate pasteMode: boolean = false;\n\tprivate pasteBuffer: string = \"\";\n\tprivate pendingKittyPrintableCodepoint: number | undefined;\n\n\tconstructor(options: StdinBufferOptions = {}) {\n\t\tsuper();\n\t\tthis.timeoutMs = options.timeout ?? 10;\n\t}\n\n\tpublic process(data: string | Buffer): void {\n\t\t// Clear any pending timeout\n\t\tif (this.timeout) {\n\t\t\tclearTimeout(this.timeout);\n\t\t\tthis.timeout = null;\n\t\t}\n\n\t\t// Handle high-byte conversion (for compatibility with parseKeypress)\n\t\t// If buffer has single byte > 127, convert to ESC + (byte - 128)\n\t\tlet str: string;\n\t\tif (Buffer.isBuffer(data)) {\n\t\t\tif (data.length === 1 && data[0]! > 127) {\n\t\t\t\tconst byte = data[0]! - 128;\n\t\t\t\tstr = `\\x1b${String.fromCharCode(byte)}`;\n\t\t\t} else {\n\t\t\t\tstr = data.toString();\n\t\t\t}\n\t\t} else {\n\t\t\tstr = data;\n\t\t}\n\n\t\tif (str.length === 0 && this.buffer.length === 0) {\n\t\t\tthis.emitDataSequence(\"\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.buffer += str;\n\n\t\tif (this.pasteMode) {\n\t\t\tthis.pasteBuffer += this.buffer;\n\t\t\tthis.buffer = \"\";\n\n\t\t\tconst endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);\n\t\t\tif (endIndex !== -1) {\n\t\t\t\tconst pastedContent = this.pasteBuffer.slice(0, endIndex);\n\t\t\t\tconst remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);\n\n\t\t\t\tthis.pasteMode = false;\n\t\t\t\tthis.pasteBuffer = \"\";\n\t\t\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\n\t\t\t\tthis.emit(\"paste\", pastedContent);\n\n\t\t\t\tif (remaining.length > 0) {\n\t\t\t\t\tthis.process(remaining);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst startIndex = this.buffer.indexOf(BRACKETED_PASTE_START);\n\t\tif (startIndex !== -1) {\n\t\t\tif (startIndex > 0) {\n\t\t\t\tconst beforePaste = this.buffer.slice(0, startIndex);\n\t\t\t\tconst result = extractCompleteSequences(beforePaste);\n\t\t\t\tfor (const sequence of result.sequences) {\n\t\t\t\t\tthis.emitDataSequence(sequence);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\t\t\tthis.buffer = this.buffer.slice(startIndex + BRACKETED_PASTE_START.length);\n\t\t\tthis.pasteMode = true;\n\t\t\tthis.pasteBuffer = this.buffer;\n\t\t\tthis.buffer = \"\";\n\n\t\t\tconst endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);\n\t\t\tif (endIndex !== -1) {\n\t\t\t\tconst pastedContent = this.pasteBuffer.slice(0, endIndex);\n\t\t\t\tconst remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);\n\n\t\t\t\tthis.pasteMode = false;\n\t\t\t\tthis.pasteBuffer = \"\";\n\t\t\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\n\t\t\t\tthis.emit(\"paste\", pastedContent);\n\n\t\t\t\tif (remaining.length > 0) {\n\t\t\t\t\tthis.process(remaining);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst result = extractCompleteSequences(this.buffer);\n\t\tthis.buffer = result.remainder;\n\n\t\tfor (const sequence of result.sequences) {\n\t\t\tthis.emitDataSequence(sequence);\n\t\t}\n\n\t\tif (this.buffer.length > 0) {\n\t\t\tthis.timeout = setTimeout(() => {\n\t\t\t\tconst flushed = this.flush();\n\n\t\t\t\tfor (const sequence of flushed) {\n\t\t\t\t\tthis.emitDataSequence(sequence);\n\t\t\t\t}\n\t\t\t}, this.timeoutMs);\n\t\t}\n\t}\n\n\tprivate emitDataSequence(sequence: string): void {\n\t\tconst rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined;\n\t\tif (rawCodepoint !== undefined && rawCodepoint === this.pendingKittyPrintableCodepoint) {\n\t\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.pendingKittyPrintableCodepoint = parseUnmodifiedKittyPrintableCodepoint(sequence);\n\t\tthis.emit(\"data\", sequence);\n\t}\n\n\tflush(): string[] {\n\t\tif (this.timeout) {\n\t\t\tclearTimeout(this.timeout);\n\t\t\tthis.timeout = null;\n\t\t}\n\n\t\tif (this.buffer.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst sequences = [this.buffer];\n\t\tthis.buffer = \"\";\n\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\t\treturn sequences;\n\t}\n\n\tclear(): void {\n\t\tif (this.timeout) {\n\t\t\tclearTimeout(this.timeout);\n\t\t\tthis.timeout = null;\n\t\t}\n\t\tthis.buffer = \"\";\n\t\tthis.pasteMode = false;\n\t\tthis.pasteBuffer = \"\";\n\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\t}\n\n\tgetBuffer(): string {\n\t\treturn this.buffer;\n\t}\n\n\tdestroy(): void {\n\t\tthis.clear();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stdin-buffer.d.ts","sourceRoot":"","sources":["../src/stdin-buffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAuUtC,MAAM,MAAM,kBAAkB,GAAG;IAChC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IACjC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;IACf,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC;CAChB,CAAC;AAEF;;;GAGG;AACH,qBAAa,WAAY,SAAQ,YAAY,CAAC,mBAAmB,CAAC;IACjE,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAA8C;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,8BAA8B,CAAqB;IAC3D,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,8FAA8F;IAC9F,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,gBAAgB,CAA8C;IAEtE,YAAY,OAAO,GAAE,kBAAuB,EAK3C;IAEM,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2I1C;IAED,4GAA4G;IAC5G,OAAO,CAAC,mBAAmB;IAa3B,iGAAiG;IACjG,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,gBAAgB;IAWxB,KAAK,IAAI,MAAM,EAAE,CAehB;IAED,KAAK,IAAI,IAAI,CAUZ;IAED,SAAS,IAAI,MAAM,CAElB;IAED,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["/**\n * StdinBuffer buffers input and emits complete sequences.\n *\n * This is necessary because stdin data events can arrive in partial chunks,\n * especially for escape sequences like mouse events. Without buffering,\n * partial sequences can be misinterpreted as regular keypresses.\n *\n * For example, the mouse SGR sequence `\\x1b[<35;20;5m` might arrive as:\n * - Event 1: `\\x1b`\n * - Event 2: `[<35`\n * - Event 3: `;20;5m`\n *\n * The buffer accumulates these until a complete sequence is detected.\n * Call the `process()` method to feed input data.\n *\n * Based on code from OpenTUI (https://github.com/anomalyco/opentui)\n * MIT License - Copyright (c) 2025 opentui\n */\n\nimport { EventEmitter } from \"events\";\n\nconst ESC = \"\\x1b\";\nconst DEFAULT_SEQUENCE_TIMEOUT_MS = 10;\nconst DEFAULT_ESCAPE_TIMEOUT_MS = 10;\n// How long input that arrived after an unterminated terminal reply waits for the reply's terminator before\n// the reply is discarded and that input is delivered as keystrokes (#3986).\nconst DEFAULT_REPLY_RESYNC_TIMEOUT_MS = 1000;\n// A pending reply plus held input larger than this is never a real reply; resynchronize at once.\nconst MAX_PENDING_REPLY_LENGTH = 4096;\nconst BRACKETED_PASTE_START = \"\\x1b[200~\";\nconst BRACKETED_PASTE_END = \"\\x1b[201~\";\n// Color and program-status OSC replies, XTVersion, and Kitty graphics replies.\n// Until their header is complete, the input may be batched legacy Alt keystrokes.\nconst TERMINAL_REPLY_HEADER_PATTERN = /^\\x1b(?:\\](?:1[01];|4;\\d{1,3};|7501;\\?)|P>\\||_G(?:[a-z]=\\d+,)*[a-z]=\\d+;)/;\n\n// Color replies (OSC 10, 11, 4;n) carry `rgb:R/G/B` with one to four hex digits per channel, all the same width.\nconst COLOR_REPLY_HEADER_PATTERN = /^\\x1b\\](?:1[01];|4;\\d{1,3};)/;\nconst COLOR_REPLY_PREFIX = \"rgb:\";\nconst MAX_COLOR_CHANNEL_DIGITS = 4;\nconst HEX_DIGIT_PATTERN = /[0-9a-fA-F]/;\n\n/**\n * Length of the longest prefix of `body` that can still belong to a color reply body. The first channel\n * fixes the width; anything that breaks the grammar is keyboard input that arrived in the same read.\n */\nfunction colorReplyBodyLength(body: string): number {\n\tlet index = 0;\n\twhile (index < COLOR_REPLY_PREFIX.length && body[index] === COLOR_REPLY_PREFIX[index]) index++;\n\tif (index < COLOR_REPLY_PREFIX.length) return index;\n\tlet width = MAX_COLOR_CHANNEL_DIGITS;\n\tfor (let channel = 0; channel < 3; channel++) {\n\t\tlet digits = 0;\n\t\twhile (digits < width && index < body.length && HEX_DIGIT_PATTERN.test(body[index]!)) {\n\t\t\tindex++;\n\t\t\tdigits++;\n\t\t}\n\t\tif (channel === 0) width = digits;\n\t\t// Stop at the end of the input, after the last channel, or where the next byte is not the separator.\n\t\tif (channel === 2 || digits === 0 || digits < width || body[index] !== \"/\") return index;\n\t\tindex++;\n\t}\n\treturn index;\n}\n\n/**\n * Length of the part of a pending reply that can belong to it. For color replies, bytes beyond a valid\n * color body are keyboard input that arrived in the same read; other replies have free-text bodies, so\n * everything buffered so far counts.\n */\nfunction pendingReplyLength(buffer: string): number {\n\tconst header = buffer.match(COLOR_REPLY_HEADER_PATTERN);\n\tif (!header) return buffer.length;\n\tconst length = header[0].length + colorReplyBodyLength(buffer.slice(header[0].length));\n\t// A final ESC right after the body may be the first byte of ST.\n\treturn length === buffer.length - 1 && buffer.endsWith(ESC) ? buffer.length : length;\n}\n\n/** True when input that arrived after a pending reply contains that reply's string terminator. */\nfunction containsReplyTerminator(reply: string, laterInput: string): boolean {\n\t// ST can be split between the reply's trailing ESC and the next chunk's backslash.\n\tconst joined = (reply.endsWith(ESC) ? ESC : \"\") + laterInput;\n\treturn joined.includes(`${ESC}\\\\`) || (reply.startsWith(`${ESC}]`) && laterInput.includes(\"\\x07\"));\n}\n\n/**\n * True when input that arrived after a pending reply can only be keyboard input: CAN or SUB, or an ESC that\n * starts a new sequence rather than ST. A trailing ESC stays ambiguous until the next byte arrives.\n */\nfunction abortsPendingReply(laterInput: string): boolean {\n\tif (laterInput.includes(\"\\x18\") || laterInput.includes(\"\\x1a\")) return true;\n\tfor (let index = laterInput.indexOf(ESC); index !== -1; index = laterInput.indexOf(ESC, index + 1)) {\n\t\tif (index < laterInput.length - 1 && laterInput[index + 1] !== \"\\\\\") return true;\n\t}\n\treturn false;\n}\n\nfunction isControlString(data: string): boolean {\n\treturn data.startsWith(`${ESC}]`) || data.startsWith(`${ESC}P`) || data.startsWith(`${ESC}_`);\n}\n\n/**\n * Check if a string is a complete escape sequence or needs more data\n */\nfunction isCompleteSequence(data: string): \"complete\" | \"incomplete\" | \"not-escape\" {\n\tif (!data.startsWith(ESC)) {\n\t\treturn \"not-escape\";\n\t}\n\n\tif (data.length === 1) {\n\t\treturn \"incomplete\";\n\t}\n\n\tconst afterEsc = data.slice(1);\n\n\t// CSI sequences: ESC [\n\tif (afterEsc.startsWith(\"[\")) {\n\t\t// Check for old-style mouse sequence: ESC[M + 3 bytes\n\t\tif (afterEsc.startsWith(\"[M\")) {\n\t\t\t// Old-style mouse needs ESC[M + 3 bytes = 6 total\n\t\t\treturn data.length >= 6 ? \"complete\" : \"incomplete\";\n\t\t}\n\t\treturn isCompleteCsiSequence(data);\n\t}\n\n\t// OSC sequences: ESC ]\n\tif (afterEsc.startsWith(\"]\")) {\n\t\treturn isCompleteOscSequence(data);\n\t}\n\n\t// DCS sequences: ESC P ... ESC \\ (includes XTVersion responses)\n\tif (afterEsc.startsWith(\"P\")) {\n\t\treturn isCompleteDcsSequence(data);\n\t}\n\n\t// APC sequences: ESC _ ... ESC \\ (includes Kitty graphics responses)\n\tif (afterEsc.startsWith(\"_\")) {\n\t\treturn isCompleteApcSequence(data);\n\t}\n\n\t// SS3 sequences: ESC O\n\tif (afterEsc.startsWith(\"O\")) {\n\t\t// ESC O followed by a single character\n\t\treturn afterEsc.length >= 2 ? \"complete\" : \"incomplete\";\n\t}\n\n\t// Meta key sequences: ESC followed by a single character\n\tif (afterEsc.length === 1) {\n\t\treturn \"complete\";\n\t}\n\n\t// Unknown escape sequence - treat as complete\n\treturn \"complete\";\n}\n\n/**\n * Check if CSI sequence is complete\n * CSI sequences: ESC [ ... followed by a final byte (0x40-0x7E)\n */\nfunction isCompleteCsiSequence(data: string): \"complete\" | \"incomplete\" {\n\tif (!data.startsWith(`${ESC}[`)) {\n\t\treturn \"complete\";\n\t}\n\n\t// Need at least ESC [ and one more character\n\tif (data.length < 3) {\n\t\treturn \"incomplete\";\n\t}\n\n\tconst payload = data.slice(2);\n\n\t// CSI sequences end with a byte in the range 0x40-0x7E (@-~)\n\t// This includes all letters and several special characters\n\tconst lastChar = payload[payload.length - 1];\n\tconst lastCharCode = lastChar.charCodeAt(0);\n\n\tif (lastCharCode >= 0x40 && lastCharCode <= 0x7e) {\n\t\t// Special handling for SGR mouse sequences\n\t\t// Format: ESC[<B;X;Ym or ESC[<B;X;YM\n\t\tif (payload.startsWith(\"<\")) {\n\t\t\t// Must have format: <digits;digits;digits[Mm]\n\t\t\tconst mouseMatch = /^<\\d+;\\d+;\\d+[Mm]$/.test(payload);\n\t\t\tif (mouseMatch) {\n\t\t\t\treturn \"complete\";\n\t\t\t}\n\t\t\t// If it ends with M or m but doesn't match the pattern, still incomplete\n\t\t\tif (lastChar === \"M\" || lastChar === \"m\") {\n\t\t\t\t// Check if we have the right structure\n\t\t\t\tconst parts = payload.slice(1, -1).split(\";\");\n\t\t\t\tif (parts.length === 3 && parts.every((p) => /^\\d+$/.test(p))) {\n\t\t\t\t\treturn \"complete\";\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn \"incomplete\";\n\t\t}\n\n\t\treturn \"complete\";\n\t}\n\n\treturn \"incomplete\";\n}\n\n/**\n * Check if OSC sequence is complete\n * OSC sequences: ESC ] ... ST (where ST is ESC \\ or BEL)\n */\nfunction isCompleteOscSequence(data: string): \"complete\" | \"incomplete\" {\n\tif (!data.startsWith(`${ESC}]`)) {\n\t\treturn \"complete\";\n\t}\n\n\t// OSC sequences end with ST (ESC \\) or BEL (\\x07)\n\tif (data.endsWith(`${ESC}\\\\`) || data.endsWith(\"\\x07\")) {\n\t\treturn \"complete\";\n\t}\n\n\treturn \"incomplete\";\n}\n\n/**\n * Check if DCS (Device Control String) sequence is complete\n * DCS sequences: ESC P ... ST (where ST is ESC \\)\n * Used for XTVersion responses like ESC P >| ... ESC \\\n */\nfunction isCompleteDcsSequence(data: string): \"complete\" | \"incomplete\" {\n\tif (!data.startsWith(`${ESC}P`)) {\n\t\treturn \"complete\";\n\t}\n\n\t// DCS sequences end with ST (ESC \\)\n\tif (data.endsWith(`${ESC}\\\\`)) {\n\t\treturn \"complete\";\n\t}\n\n\treturn \"incomplete\";\n}\n\n/**\n * Check if APC (Application Program Command) sequence is complete\n * APC sequences: ESC _ ... ST (where ST is ESC \\)\n * Used for Kitty graphics responses like ESC _ G ... ESC \\\n */\nfunction isCompleteApcSequence(data: string): \"complete\" | \"incomplete\" {\n\tif (!data.startsWith(`${ESC}_`)) {\n\t\treturn \"complete\";\n\t}\n\n\t// APC sequences end with ST (ESC \\)\n\tif (data.endsWith(`${ESC}\\\\`)) {\n\t\treturn \"complete\";\n\t}\n\n\treturn \"incomplete\";\n}\n\n/**\n * Split accumulated buffer into complete sequences\n */\nfunction parseUnmodifiedKittyPrintableCodepoint(sequence: string): number | undefined {\n\tconst match = sequence.match(/^\\x1b\\[(\\d+)(?::\\d*)?(?::\\d+)?u$/);\n\tif (!match) return undefined;\n\n\tconst codepoint = parseInt(match[1]!, 10);\n\treturn codepoint >= 32 ? codepoint : undefined;\n}\n\nfunction extractCompleteSequences(buffer: string): { sequences: string[]; remainder: string } {\n\tconst sequences: string[] = [];\n\tlet pos = 0;\n\n\twhile (pos < buffer.length) {\n\t\tconst remaining = buffer.slice(pos);\n\n\t\t// Try to extract a sequence starting at this position\n\t\tif (remaining.startsWith(ESC)) {\n\t\t\tconst terminalReply = isControlString(remaining) && TERMINAL_REPLY_HEADER_PATTERN.test(remaining);\n\t\t\t// Find the end of this escape sequence\n\t\t\tlet seqEnd = 1;\n\t\t\twhile (seqEnd <= remaining.length) {\n\t\t\t\tif (terminalReply && seqEnd > 2) {\n\t\t\t\t\tconst char = remaining[seqEnd - 1];\n\t\t\t\t\tif (char === \"\\x18\" || char === \"\\x1a\") {\n\t\t\t\t\t\t// CAN and SUB cancel the string, not subsequent keyboard input.\n\t\t\t\t\t\tpos += seqEnd;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif (char === ESC && seqEnd < remaining.length && remaining[seqEnd] !== \"\\\\\") {\n\t\t\t\t\t\t// A new escape sequence aborts an unterminated reply. Keep a\n\t\t\t\t\t\t// trailing ESC buffered until we can distinguish it from ST.\n\t\t\t\t\t\tpos += seqEnd - 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst candidate = remaining.slice(0, seqEnd);\n\t\t\t\tconst status = isCompleteSequence(candidate);\n\n\t\t\t\tif (status === \"complete\") {\n\t\t\t\t\t// WezTerm with enable_kitty_keyboard sends the Escape key press as a\n\t\t\t\t\t// raw '\\x1b' byte (simple text path in encode_kitty, ignoring\n\t\t\t\t\t// DISAMBIGUATE_ESCAPE_CODES) and the release as a full Kitty CSI-u\n\t\t\t\t\t// sequence. These arrive concatenated as '\\x1b\\x1b[27;...u'.\n\t\t\t\t\t// The buffer would normally treat '\\x1b\\x1b' as a complete meta-key\n\t\t\t\t\t// sequence (ESC + single char), leaving '[27;...u' to be typed as\n\t\t\t\t\t// plain text. If the character immediately following '\\x1b\\x1b'\n\t\t\t\t\t// would begin a new escape sequence, emit only the first ESC and\n\t\t\t\t\t// restart from the second.\n\t\t\t\t\tif (candidate === \"\\x1b\\x1b\") {\n\t\t\t\t\t\tconst nextChar = remaining[seqEnd];\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tnextChar === \"[\" || // CSI\n\t\t\t\t\t\t\tnextChar === \"]\" || // OSC\n\t\t\t\t\t\t\tnextChar === \"O\" || // SS3\n\t\t\t\t\t\t\tnextChar === \"P\" || // DCS\n\t\t\t\t\t\t\tnextChar === \"_\" // APC\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tsequences.push(ESC);\n\t\t\t\t\t\t\tpos += 1;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tsequences.push(candidate);\n\t\t\t\t\tpos += seqEnd;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (status === \"incomplete\") {\n\t\t\t\t\tseqEnd++;\n\t\t\t\t} else {\n\t\t\t\t\t// Should not happen when starting with ESC\n\t\t\t\t\tsequences.push(candidate);\n\t\t\t\t\tpos += seqEnd;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (seqEnd > remaining.length) {\n\t\t\t\treturn { sequences, remainder: remaining };\n\t\t\t}\n\t\t} else {\n\t\t\t// Not an escape sequence - take a single character\n\t\t\tsequences.push(remaining[0]!);\n\t\t\tpos++;\n\t\t}\n\t}\n\n\treturn { sequences, remainder: \"\" };\n}\n\nexport type StdinBufferOptions = {\n\t/**\n\t * Maximum time to wait for an incomplete keyboard or mouse sequence\n\t * (default: 10ms). Strings with a recognized terminal reply header wait for\n\t * their terminator instead, so delayed replies cannot become keypresses.\n\t * Ambiguous legacy Alt-key input still uses this timeout.\n\t */\n\ttimeout?: number;\n\t/**\n\t * Maximum time to wait after a lone ESC before treating it as Escape\n\t * (default: 10ms). Increase for high-latency Alt+key input (SSH).\n\t */\n\tescapeTimeout?: number;\n\t/**\n\t * Maximum time input typed after an unterminated terminal reply waits for the reply's terminator\n\t * (default: 1000ms). After it, the reply is discarded and the held input is delivered as keystrokes.\n\t * A reply with no input after it keeps waiting, so delayed reply tails stay intact.\n\t */\n\treplyResyncTimeout?: number;\n};\n\nexport type StdinBufferEventMap = {\n\tdata: [string];\n\tpaste: [string];\n};\n\n/**\n * Buffers stdin input and emits complete sequences via the 'data' event.\n * Handles partial escape sequences that arrive across multiple chunks.\n */\nexport class StdinBuffer extends EventEmitter<StdinBufferEventMap> {\n\tprivate buffer: string = \"\";\n\tprivate timeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate readonly timeoutMs: number;\n\tprivate readonly escapeTimeoutMs: number;\n\tprivate pasteMode: boolean = false;\n\tprivate pasteBuffer: string = \"\";\n\tprivate pendingKittyPrintableCodepoint: number | undefined;\n\tprivate readonly replyResyncTimeoutMs: number;\n\t/** Buffer length at which a pending terminal reply ends; input after it may be keystrokes. */\n\tprivate replyEnd: number | null = null;\n\tprivate replyResyncTimer: ReturnType<typeof setTimeout> | null = null;\n\n\tconstructor(options: StdinBufferOptions = {}) {\n\t\tsuper();\n\t\tthis.timeoutMs = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT_MS;\n\t\tthis.escapeTimeoutMs = options.escapeTimeout ?? DEFAULT_ESCAPE_TIMEOUT_MS;\n\t\tthis.replyResyncTimeoutMs = options.replyResyncTimeout ?? DEFAULT_REPLY_RESYNC_TIMEOUT_MS;\n\t}\n\n\tpublic process(data: string | Buffer): void {\n\t\t// Clear any pending timeout\n\t\tif (this.timeout) {\n\t\t\tclearTimeout(this.timeout);\n\t\t\tthis.timeout = null;\n\t\t}\n\n\t\t// Handle high-byte conversion (for compatibility with parseKeypress)\n\t\t// If buffer has single byte > 127, convert to ESC + (byte - 128)\n\t\tlet str: string;\n\t\tif (Buffer.isBuffer(data)) {\n\t\t\tif (data.length === 1 && data[0]! > 127) {\n\t\t\t\tconst byte = data[0]! - 128;\n\t\t\t\tstr = `\\x1b${String.fromCharCode(byte)}`;\n\t\t\t} else {\n\t\t\t\tstr = data.toString();\n\t\t\t}\n\t\t} else {\n\t\t\tstr = data;\n\t\t}\n\n\t\tif (str.length === 0 && this.buffer.length === 0) {\n\t\t\tthis.emitDataSequence(\"\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.buffer += str;\n\n\t\tif (this.pasteMode) {\n\t\t\tthis.pasteBuffer += this.buffer;\n\t\t\tthis.buffer = \"\";\n\n\t\t\tconst endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);\n\t\t\tif (endIndex !== -1) {\n\t\t\t\tconst pastedContent = this.pasteBuffer.slice(0, endIndex);\n\t\t\t\tconst remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);\n\n\t\t\t\tthis.pasteMode = false;\n\t\t\t\tthis.pasteBuffer = \"\";\n\t\t\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\n\t\t\t\tthis.emit(\"paste\", pastedContent);\n\n\t\t\t\tif (remaining.length > 0) {\n\t\t\t\t\tthis.process(remaining);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst startIndex = this.buffer.indexOf(BRACKETED_PASTE_START);\n\t\tif (startIndex !== -1) {\n\t\t\tif (startIndex > 0) {\n\t\t\t\tconst beforePaste = this.buffer.slice(0, startIndex);\n\t\t\t\tconst result = extractCompleteSequences(beforePaste);\n\t\t\t\tfor (const sequence of result.sequences) {\n\t\t\t\t\tthis.emitDataSequence(sequence);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\t\t\tthis.buffer = this.buffer.slice(startIndex + BRACKETED_PASTE_START.length);\n\t\t\tthis.pasteMode = true;\n\t\t\tthis.pasteBuffer = this.buffer;\n\t\t\tthis.buffer = \"\";\n\n\t\t\tconst endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);\n\t\t\tif (endIndex !== -1) {\n\t\t\t\tconst pastedContent = this.pasteBuffer.slice(0, endIndex);\n\t\t\t\tconst remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);\n\n\t\t\t\tthis.pasteMode = false;\n\t\t\t\tthis.pasteBuffer = \"\";\n\t\t\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\n\t\t\t\tthis.emit(\"paste\", pastedContent);\n\n\t\t\t\tif (remaining.length > 0) {\n\t\t\t\t\tthis.process(remaining);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.replyEnd !== null) {\n\t\t\tconst reply = this.buffer.slice(0, this.replyEnd);\n\t\t\tconst laterInput = this.buffer.slice(this.replyEnd);\n\t\t\tif (containsReplyTerminator(reply, laterInput)) {\n\t\t\t\t// A delayed tail: parse the completed reply normally.\n\t\t\t\tthis.stopWaitingForReply();\n\t\t\t} else if (abortsPendingReply(laterInput)) {\n\t\t\t\t// The terminal never finished the reply; the user is typing. Drop the reply, keep the keys.\n\t\t\t\tthis.stopWaitingForReply();\n\t\t\t\tthis.buffer = laterInput;\n\t\t\t} else {\n\t\t\t\tthis.holdInputAfterReply(laterInput);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tconst result = extractCompleteSequences(this.buffer);\n\t\tthis.buffer = result.remainder;\n\n\t\tfor (const sequence of result.sequences) {\n\t\t\tthis.emitDataSequence(sequence);\n\t\t}\n\n\t\tif (this.buffer.length > 0) {\n\t\t\t// Only a recognizable reply header establishes a delimited string.\n\t\t\t// Flushing it would turn a delayed tail into text and an OSC's BEL\n\t\t\t// terminator into Ctrl+G (the external editor shortcut). Other input,\n\t\t\t// including Alt+] followed by text, must retain bounded recovery.\n\t\t\tif (TERMINAL_REPLY_HEADER_PATTERN.test(this.buffer)) {\n\t\t\t\tif (this.buffer.length > MAX_PENDING_REPLY_LENGTH) {\n\t\t\t\t\t// No real reply is this long; drop it rather than hold it forever.\n\t\t\t\t\tthis.buffer = \"\";\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Remember where the reply ends so input typed after it is not swallowed (#3986).\n\t\t\t\tthis.replyEnd = pendingReplyLength(this.buffer);\n\t\t\t\tif (this.replyEnd < this.buffer.length) this.holdInputAfterReply(this.buffer.slice(this.replyEnd));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst timeoutMs = this.buffer === ESC ? this.escapeTimeoutMs : this.timeoutMs;\n\t\t\tthis.timeout = setTimeout(() => {\n\t\t\t\tconst flushed = this.flush();\n\n\t\t\t\tfor (const sequence of flushed) {\n\t\t\t\t\tif (sequence.length > 2 && isControlString(sequence)) {\n\t\t\t\t\t\t// Recover the Alt key and reparse its tail so batched text,\n\t\t\t\t\t\t// Enter, and control keys are not swallowed as one event.\n\t\t\t\t\t\tthis.emitDataSequence(sequence.slice(0, 2));\n\t\t\t\t\t\tthis.process(sequence.slice(2));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.emitDataSequence(sequence);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, timeoutMs);\n\t\t}\n\t}\n\n\t/** Holds input that arrived after an unterminated reply until the reply ends or the resync bound passes. */\n\tprivate holdInputAfterReply(laterInput: string): void {\n\t\tif (this.buffer.length > MAX_PENDING_REPLY_LENGTH) {\n\t\t\tthis.resyncPendingReply();\n\t\t} else if (laterInput.endsWith(ESC)) {\n\t\t\t// ST's two bytes are written together; an ESC still alone after the escape timeout is the\n\t\t\t// Escape key, which must not merge with the next key into Alt+key.\n\t\t\tif (this.replyResyncTimer) clearTimeout(this.replyResyncTimer);\n\t\t\tthis.replyResyncTimer = setTimeout(() => this.resyncPendingReply(), this.escapeTimeoutMs);\n\t\t} else if (!this.replyResyncTimer) {\n\t\t\tthis.replyResyncTimer = setTimeout(() => this.resyncPendingReply(), this.replyResyncTimeoutMs);\n\t\t}\n\t}\n\n\t/** Discards an unterminated reply and delivers the input that arrived after it as keystrokes. */\n\tprivate resyncPendingReply(): void {\n\t\tconst laterInput = this.replyEnd === null ? \"\" : this.buffer.slice(this.replyEnd);\n\t\tthis.stopWaitingForReply();\n\t\tthis.buffer = \"\";\n\t\tif (laterInput.length > 0) this.process(laterInput);\n\t}\n\n\tprivate stopWaitingForReply(): void {\n\t\tif (this.replyResyncTimer) {\n\t\t\tclearTimeout(this.replyResyncTimer);\n\t\t\tthis.replyResyncTimer = null;\n\t\t}\n\t\tthis.replyEnd = null;\n\t}\n\n\tprivate emitDataSequence(sequence: string): void {\n\t\tconst rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined;\n\t\tif (rawCodepoint !== undefined && rawCodepoint === this.pendingKittyPrintableCodepoint) {\n\t\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.pendingKittyPrintableCodepoint = parseUnmodifiedKittyPrintableCodepoint(sequence);\n\t\tthis.emit(\"data\", sequence);\n\t}\n\n\tflush(): string[] {\n\t\tif (this.timeout) {\n\t\t\tclearTimeout(this.timeout);\n\t\t\tthis.timeout = null;\n\t\t}\n\n\t\tif (this.buffer.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst sequences = [this.buffer];\n\t\tthis.buffer = \"\";\n\t\tthis.stopWaitingForReply();\n\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\t\treturn sequences;\n\t}\n\n\tclear(): void {\n\t\tif (this.timeout) {\n\t\t\tclearTimeout(this.timeout);\n\t\t\tthis.timeout = null;\n\t\t}\n\t\tthis.buffer = \"\";\n\t\tthis.stopWaitingForReply();\n\t\tthis.pasteMode = false;\n\t\tthis.pasteBuffer = \"\";\n\t\tthis.pendingKittyPrintableCodepoint = undefined;\n\t}\n\n\tgetBuffer(): string {\n\t\treturn this.buffer;\n\t}\n\n\tdestroy(): void {\n\t\tthis.clear();\n\t}\n}\n"]}
|
package/dist/stdin-buffer.js
CHANGED
|
@@ -18,8 +18,84 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { EventEmitter } from "events";
|
|
20
20
|
const ESC = "\x1b";
|
|
21
|
+
const DEFAULT_SEQUENCE_TIMEOUT_MS = 10;
|
|
22
|
+
const DEFAULT_ESCAPE_TIMEOUT_MS = 10;
|
|
23
|
+
// How long input that arrived after an unterminated terminal reply waits for the reply's terminator before
|
|
24
|
+
// the reply is discarded and that input is delivered as keystrokes (#3986).
|
|
25
|
+
const DEFAULT_REPLY_RESYNC_TIMEOUT_MS = 1000;
|
|
26
|
+
// A pending reply plus held input larger than this is never a real reply; resynchronize at once.
|
|
27
|
+
const MAX_PENDING_REPLY_LENGTH = 4096;
|
|
21
28
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
22
29
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
30
|
+
// Color and program-status OSC replies, XTVersion, and Kitty graphics replies.
|
|
31
|
+
// Until their header is complete, the input may be batched legacy Alt keystrokes.
|
|
32
|
+
const TERMINAL_REPLY_HEADER_PATTERN = /^\x1b(?:\](?:1[01];|4;\d{1,3};|7501;\?)|P>\||_G(?:[a-z]=\d+,)*[a-z]=\d+;)/;
|
|
33
|
+
// Color replies (OSC 10, 11, 4;n) carry `rgb:R/G/B` with one to four hex digits per channel, all the same width.
|
|
34
|
+
const COLOR_REPLY_HEADER_PATTERN = /^\x1b\](?:1[01];|4;\d{1,3};)/;
|
|
35
|
+
const COLOR_REPLY_PREFIX = "rgb:";
|
|
36
|
+
const MAX_COLOR_CHANNEL_DIGITS = 4;
|
|
37
|
+
const HEX_DIGIT_PATTERN = /[0-9a-fA-F]/;
|
|
38
|
+
/**
|
|
39
|
+
* Length of the longest prefix of `body` that can still belong to a color reply body. The first channel
|
|
40
|
+
* fixes the width; anything that breaks the grammar is keyboard input that arrived in the same read.
|
|
41
|
+
*/
|
|
42
|
+
function colorReplyBodyLength(body) {
|
|
43
|
+
let index = 0;
|
|
44
|
+
while (index < COLOR_REPLY_PREFIX.length && body[index] === COLOR_REPLY_PREFIX[index])
|
|
45
|
+
index++;
|
|
46
|
+
if (index < COLOR_REPLY_PREFIX.length)
|
|
47
|
+
return index;
|
|
48
|
+
let width = MAX_COLOR_CHANNEL_DIGITS;
|
|
49
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
50
|
+
let digits = 0;
|
|
51
|
+
while (digits < width && index < body.length && HEX_DIGIT_PATTERN.test(body[index])) {
|
|
52
|
+
index++;
|
|
53
|
+
digits++;
|
|
54
|
+
}
|
|
55
|
+
if (channel === 0)
|
|
56
|
+
width = digits;
|
|
57
|
+
// Stop at the end of the input, after the last channel, or where the next byte is not the separator.
|
|
58
|
+
if (channel === 2 || digits === 0 || digits < width || body[index] !== "/")
|
|
59
|
+
return index;
|
|
60
|
+
index++;
|
|
61
|
+
}
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Length of the part of a pending reply that can belong to it. For color replies, bytes beyond a valid
|
|
66
|
+
* color body are keyboard input that arrived in the same read; other replies have free-text bodies, so
|
|
67
|
+
* everything buffered so far counts.
|
|
68
|
+
*/
|
|
69
|
+
function pendingReplyLength(buffer) {
|
|
70
|
+
const header = buffer.match(COLOR_REPLY_HEADER_PATTERN);
|
|
71
|
+
if (!header)
|
|
72
|
+
return buffer.length;
|
|
73
|
+
const length = header[0].length + colorReplyBodyLength(buffer.slice(header[0].length));
|
|
74
|
+
// A final ESC right after the body may be the first byte of ST.
|
|
75
|
+
return length === buffer.length - 1 && buffer.endsWith(ESC) ? buffer.length : length;
|
|
76
|
+
}
|
|
77
|
+
/** True when input that arrived after a pending reply contains that reply's string terminator. */
|
|
78
|
+
function containsReplyTerminator(reply, laterInput) {
|
|
79
|
+
// ST can be split between the reply's trailing ESC and the next chunk's backslash.
|
|
80
|
+
const joined = (reply.endsWith(ESC) ? ESC : "") + laterInput;
|
|
81
|
+
return joined.includes(`${ESC}\\`) || (reply.startsWith(`${ESC}]`) && laterInput.includes("\x07"));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* True when input that arrived after a pending reply can only be keyboard input: CAN or SUB, or an ESC that
|
|
85
|
+
* starts a new sequence rather than ST. A trailing ESC stays ambiguous until the next byte arrives.
|
|
86
|
+
*/
|
|
87
|
+
function abortsPendingReply(laterInput) {
|
|
88
|
+
if (laterInput.includes("\x18") || laterInput.includes("\x1a"))
|
|
89
|
+
return true;
|
|
90
|
+
for (let index = laterInput.indexOf(ESC); index !== -1; index = laterInput.indexOf(ESC, index + 1)) {
|
|
91
|
+
if (index < laterInput.length - 1 && laterInput[index + 1] !== "\\")
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function isControlString(data) {
|
|
97
|
+
return data.startsWith(`${ESC}]`) || data.startsWith(`${ESC}P`) || data.startsWith(`${ESC}_`);
|
|
98
|
+
}
|
|
23
99
|
/**
|
|
24
100
|
* Check if a string is a complete escape sequence or needs more data
|
|
25
101
|
*/
|
|
@@ -165,9 +241,24 @@ function extractCompleteSequences(buffer) {
|
|
|
165
241
|
const remaining = buffer.slice(pos);
|
|
166
242
|
// Try to extract a sequence starting at this position
|
|
167
243
|
if (remaining.startsWith(ESC)) {
|
|
244
|
+
const terminalReply = isControlString(remaining) && TERMINAL_REPLY_HEADER_PATTERN.test(remaining);
|
|
168
245
|
// Find the end of this escape sequence
|
|
169
246
|
let seqEnd = 1;
|
|
170
247
|
while (seqEnd <= remaining.length) {
|
|
248
|
+
if (terminalReply && seqEnd > 2) {
|
|
249
|
+
const char = remaining[seqEnd - 1];
|
|
250
|
+
if (char === "\x18" || char === "\x1a") {
|
|
251
|
+
// CAN and SUB cancel the string, not subsequent keyboard input.
|
|
252
|
+
pos += seqEnd;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (char === ESC && seqEnd < remaining.length && remaining[seqEnd] !== "\\") {
|
|
256
|
+
// A new escape sequence aborts an unterminated reply. Keep a
|
|
257
|
+
// trailing ESC buffered until we can distinguish it from ST.
|
|
258
|
+
pos += seqEnd - 1;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
171
262
|
const candidate = remaining.slice(0, seqEnd);
|
|
172
263
|
const status = isCompleteSequence(candidate);
|
|
173
264
|
if (status === "complete") {
|
|
@@ -227,12 +318,19 @@ export class StdinBuffer extends EventEmitter {
|
|
|
227
318
|
buffer = "";
|
|
228
319
|
timeout = null;
|
|
229
320
|
timeoutMs;
|
|
321
|
+
escapeTimeoutMs;
|
|
230
322
|
pasteMode = false;
|
|
231
323
|
pasteBuffer = "";
|
|
232
324
|
pendingKittyPrintableCodepoint;
|
|
325
|
+
replyResyncTimeoutMs;
|
|
326
|
+
/** Buffer length at which a pending terminal reply ends; input after it may be keystrokes. */
|
|
327
|
+
replyEnd = null;
|
|
328
|
+
replyResyncTimer = null;
|
|
233
329
|
constructor(options = {}) {
|
|
234
330
|
super();
|
|
235
|
-
this.timeoutMs = options.timeout ??
|
|
331
|
+
this.timeoutMs = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT_MS;
|
|
332
|
+
this.escapeTimeoutMs = options.escapeTimeout ?? DEFAULT_ESCAPE_TIMEOUT_MS;
|
|
333
|
+
this.replyResyncTimeoutMs = options.replyResyncTimeout ?? DEFAULT_REPLY_RESYNC_TIMEOUT_MS;
|
|
236
334
|
}
|
|
237
335
|
process(data) {
|
|
238
336
|
// Clear any pending timeout
|
|
@@ -305,19 +403,92 @@ export class StdinBuffer extends EventEmitter {
|
|
|
305
403
|
}
|
|
306
404
|
return;
|
|
307
405
|
}
|
|
406
|
+
if (this.replyEnd !== null) {
|
|
407
|
+
const reply = this.buffer.slice(0, this.replyEnd);
|
|
408
|
+
const laterInput = this.buffer.slice(this.replyEnd);
|
|
409
|
+
if (containsReplyTerminator(reply, laterInput)) {
|
|
410
|
+
// A delayed tail: parse the completed reply normally.
|
|
411
|
+
this.stopWaitingForReply();
|
|
412
|
+
}
|
|
413
|
+
else if (abortsPendingReply(laterInput)) {
|
|
414
|
+
// The terminal never finished the reply; the user is typing. Drop the reply, keep the keys.
|
|
415
|
+
this.stopWaitingForReply();
|
|
416
|
+
this.buffer = laterInput;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
this.holdInputAfterReply(laterInput);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
308
423
|
const result = extractCompleteSequences(this.buffer);
|
|
309
424
|
this.buffer = result.remainder;
|
|
310
425
|
for (const sequence of result.sequences) {
|
|
311
426
|
this.emitDataSequence(sequence);
|
|
312
427
|
}
|
|
313
428
|
if (this.buffer.length > 0) {
|
|
429
|
+
// Only a recognizable reply header establishes a delimited string.
|
|
430
|
+
// Flushing it would turn a delayed tail into text and an OSC's BEL
|
|
431
|
+
// terminator into Ctrl+G (the external editor shortcut). Other input,
|
|
432
|
+
// including Alt+] followed by text, must retain bounded recovery.
|
|
433
|
+
if (TERMINAL_REPLY_HEADER_PATTERN.test(this.buffer)) {
|
|
434
|
+
if (this.buffer.length > MAX_PENDING_REPLY_LENGTH) {
|
|
435
|
+
// No real reply is this long; drop it rather than hold it forever.
|
|
436
|
+
this.buffer = "";
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// Remember where the reply ends so input typed after it is not swallowed (#3986).
|
|
440
|
+
this.replyEnd = pendingReplyLength(this.buffer);
|
|
441
|
+
if (this.replyEnd < this.buffer.length)
|
|
442
|
+
this.holdInputAfterReply(this.buffer.slice(this.replyEnd));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const timeoutMs = this.buffer === ESC ? this.escapeTimeoutMs : this.timeoutMs;
|
|
314
446
|
this.timeout = setTimeout(() => {
|
|
315
447
|
const flushed = this.flush();
|
|
316
448
|
for (const sequence of flushed) {
|
|
317
|
-
|
|
449
|
+
if (sequence.length > 2 && isControlString(sequence)) {
|
|
450
|
+
// Recover the Alt key and reparse its tail so batched text,
|
|
451
|
+
// Enter, and control keys are not swallowed as one event.
|
|
452
|
+
this.emitDataSequence(sequence.slice(0, 2));
|
|
453
|
+
this.process(sequence.slice(2));
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
this.emitDataSequence(sequence);
|
|
457
|
+
}
|
|
318
458
|
}
|
|
319
|
-
},
|
|
459
|
+
}, timeoutMs);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/** Holds input that arrived after an unterminated reply until the reply ends or the resync bound passes. */
|
|
463
|
+
holdInputAfterReply(laterInput) {
|
|
464
|
+
if (this.buffer.length > MAX_PENDING_REPLY_LENGTH) {
|
|
465
|
+
this.resyncPendingReply();
|
|
466
|
+
}
|
|
467
|
+
else if (laterInput.endsWith(ESC)) {
|
|
468
|
+
// ST's two bytes are written together; an ESC still alone after the escape timeout is the
|
|
469
|
+
// Escape key, which must not merge with the next key into Alt+key.
|
|
470
|
+
if (this.replyResyncTimer)
|
|
471
|
+
clearTimeout(this.replyResyncTimer);
|
|
472
|
+
this.replyResyncTimer = setTimeout(() => this.resyncPendingReply(), this.escapeTimeoutMs);
|
|
473
|
+
}
|
|
474
|
+
else if (!this.replyResyncTimer) {
|
|
475
|
+
this.replyResyncTimer = setTimeout(() => this.resyncPendingReply(), this.replyResyncTimeoutMs);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/** Discards an unterminated reply and delivers the input that arrived after it as keystrokes. */
|
|
479
|
+
resyncPendingReply() {
|
|
480
|
+
const laterInput = this.replyEnd === null ? "" : this.buffer.slice(this.replyEnd);
|
|
481
|
+
this.stopWaitingForReply();
|
|
482
|
+
this.buffer = "";
|
|
483
|
+
if (laterInput.length > 0)
|
|
484
|
+
this.process(laterInput);
|
|
485
|
+
}
|
|
486
|
+
stopWaitingForReply() {
|
|
487
|
+
if (this.replyResyncTimer) {
|
|
488
|
+
clearTimeout(this.replyResyncTimer);
|
|
489
|
+
this.replyResyncTimer = null;
|
|
320
490
|
}
|
|
491
|
+
this.replyEnd = null;
|
|
321
492
|
}
|
|
322
493
|
emitDataSequence(sequence) {
|
|
323
494
|
const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined;
|
|
@@ -338,6 +509,7 @@ export class StdinBuffer extends EventEmitter {
|
|
|
338
509
|
}
|
|
339
510
|
const sequences = [this.buffer];
|
|
340
511
|
this.buffer = "";
|
|
512
|
+
this.stopWaitingForReply();
|
|
341
513
|
this.pendingKittyPrintableCodepoint = undefined;
|
|
342
514
|
return sequences;
|
|
343
515
|
}
|
|
@@ -347,6 +519,7 @@ export class StdinBuffer extends EventEmitter {
|
|
|
347
519
|
this.timeout = null;
|
|
348
520
|
}
|
|
349
521
|
this.buffer = "";
|
|
522
|
+
this.stopWaitingForReply();
|
|
350
523
|
this.pasteMode = false;
|
|
351
524
|
this.pasteBuffer = "";
|
|
352
525
|
this.pendingKittyPrintableCodepoint = undefined;
|