ptywright 0.1.0 → 0.2.0
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 +459 -116
- package/dist/agent.mjs +2 -0
- package/dist/bin/ptywright.mjs +6 -0
- package/dist/cli-DIUx2w6X.mjs +3587 -0
- package/dist/cli.mjs +2 -0
- package/{src/index.ts → dist/index.mjs} +7 -9
- package/dist/mcp.mjs +2 -0
- package/dist/pty-cassette.mjs +24 -0
- package/dist/pty_like-Cpkh_O9B.mjs +404 -0
- package/dist/runner-DzZlFrt1.mjs +1897 -0
- package/dist/runner-zApMYWZx.mjs +3257 -0
- package/dist/script.mjs +2 -0
- package/dist/server-VHuEWWj_.mjs +3068 -0
- package/dist/session.mjs +2 -0
- package/dist/terminal_session-DopC7Xg6.mjs +893 -0
- package/package.json +28 -21
- package/schemas/ptywright-agent-cassette.schema.json +57 -0
- package/schemas/ptywright-agent-check.schema.json +122 -0
- package/schemas/ptywright-agent-manifest.schema.json +107 -0
- package/schemas/ptywright-agent-promote.schema.json +146 -0
- package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
- package/schemas/ptywright-agent-run.schema.json +126 -0
- package/schemas/ptywright-agent.schema.json +182 -0
- package/schemas/ptywright-pty-cassette.schema.json +86 -0
- package/schemas/ptywright-script-manifest.schema.json +75 -0
- package/schemas/ptywright-script-run-summary.schema.json +114 -0
- package/schemas/ptywright-script.schema.json +55 -3
- package/skills/ptywright-testing/SKILL.md +53 -33
- package/bin/ptywright +0 -4
- package/src/cli.ts +0 -414
- package/src/generator/doc_parser.ts +0 -341
- package/src/generator/generate.ts +0 -161
- package/src/generator/index.ts +0 -10
- package/src/generator/script_generator.ts +0 -209
- package/src/generator/step_extractor.ts +0 -397
- package/src/mcp/http_server.ts +0 -174
- package/src/mcp/script_recording.ts +0 -238
- package/src/mcp/server.ts +0 -1348
- package/src/pty/bun_pty_adapter.ts +0 -34
- package/src/pty/bun_terminal_adapter.ts +0 -149
- package/src/pty/pty_adapter.ts +0 -31
- package/src/script/dsl.ts +0 -188
- package/src/script/module.ts +0 -43
- package/src/script/path.ts +0 -151
- package/src/script/run.ts +0 -108
- package/src/script/run_all.ts +0 -229
- package/src/script/runner.ts +0 -983
- package/src/script/schema.ts +0 -237
- package/src/script/steps/assert_snapshot_equals.ts +0 -21
- package/src/script/steps/index.ts +0 -2
- package/src/script/suite_report.ts +0 -626
- package/src/session/session_manager.ts +0 -145
- package/src/session/terminal_session.ts +0 -473
- package/src/terminal/ansi.ts +0 -142
- package/src/terminal/keys.ts +0 -180
- package/src/terminal/mask.ts +0 -70
- package/src/terminal/mouse.ts +0 -75
- package/src/terminal/snapshot.ts +0 -196
- package/src/terminal/style.ts +0 -121
- package/src/terminal/view.ts +0 -49
- package/src/trace/asciicast.ts +0 -20
- package/src/trace/asciinema_player_assets.ts +0 -44
- package/src/trace/cast_to_txt.ts +0 -116
- package/src/trace/recorder.ts +0 -110
- package/src/trace/report.ts +0 -2092
- package/src/types.ts +0 -86
- package/src/util/hash.ts +0 -8
- package/src/util/sleep.ts +0 -5
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import { Terminal } from "@xterm/headless";
|
|
2
|
+
//#region src/terminal/keys.ts
|
|
3
|
+
const CSI = "\x1B[";
|
|
4
|
+
const SS3 = "\x1BO";
|
|
5
|
+
function ctrlChar(letter) {
|
|
6
|
+
const code = letter.toUpperCase().charCodeAt(0);
|
|
7
|
+
if (code < 65 || code > 90) throw new Error(`Unsupported ctrl key: ${letter}`);
|
|
8
|
+
return String.fromCharCode(code - 64);
|
|
9
|
+
}
|
|
10
|
+
function encodeKey(key) {
|
|
11
|
+
const normalized = key.trim();
|
|
12
|
+
if (normalized.length === 1) return normalized;
|
|
13
|
+
const ctrlMatch = /^c-(.)$/i.exec(normalized);
|
|
14
|
+
if (ctrlMatch) return ctrlChar(ctrlMatch[1] ?? "");
|
|
15
|
+
const parsed = parseKeySpec(normalized);
|
|
16
|
+
const mod = modifierParam(parsed.modifiers);
|
|
17
|
+
const keyName = parsed.key.toLowerCase();
|
|
18
|
+
if (keyName === "enter" || keyName === "return") return "\r";
|
|
19
|
+
if (keyName === "esc" || keyName === "escape") return "\x1B";
|
|
20
|
+
if (keyName === "backspace") return "";
|
|
21
|
+
if (keyName === "space") return " ";
|
|
22
|
+
if (keyName === "tab") return parsed.modifiers.shift ? `${CSI}Z` : " ";
|
|
23
|
+
if (keyName === "backtab" || keyName === "btab") return `${CSI}Z`;
|
|
24
|
+
const arrowFinal = arrowKeyFinal(keyName);
|
|
25
|
+
if (arrowFinal) {
|
|
26
|
+
if (mod === null) return `${CSI}${arrowFinal}`;
|
|
27
|
+
return `${CSI}1;${mod}${arrowFinal}`;
|
|
28
|
+
}
|
|
29
|
+
const homeEndFinal = homeEndFinalChar(keyName);
|
|
30
|
+
if (homeEndFinal) {
|
|
31
|
+
if (mod === null) return `${CSI}${homeEndFinal}`;
|
|
32
|
+
return `${CSI}1;${mod}${homeEndFinal}`;
|
|
33
|
+
}
|
|
34
|
+
const tildeCode = tildeKeyCode(keyName);
|
|
35
|
+
if (tildeCode) {
|
|
36
|
+
if (mod === null) return `${CSI}${tildeCode}~`;
|
|
37
|
+
return `${CSI}${tildeCode};${mod}~`;
|
|
38
|
+
}
|
|
39
|
+
const functionKey = functionKeySpec(keyName);
|
|
40
|
+
if (functionKey) {
|
|
41
|
+
if (mod === null) return functionKey.base;
|
|
42
|
+
return functionKey.modified(mod);
|
|
43
|
+
}
|
|
44
|
+
if (parsed.key.length === 1) {
|
|
45
|
+
let outChar = parsed.key;
|
|
46
|
+
if (parsed.modifiers.shift && /^[a-z]$/i.test(outChar)) outChar = outChar.toUpperCase();
|
|
47
|
+
let encoded = outChar;
|
|
48
|
+
if (parsed.modifiers.ctrl && /^[a-z]$/i.test(outChar)) encoded = ctrlChar(outChar);
|
|
49
|
+
if (parsed.modifiers.alt) encoded = `\x1b${encoded}`;
|
|
50
|
+
return encoded;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Unsupported key: ${key}`);
|
|
53
|
+
}
|
|
54
|
+
function parseKeySpec(spec) {
|
|
55
|
+
const tokens = spec.trim().split(/[+-]/g).map((t) => t.trim()).filter(Boolean);
|
|
56
|
+
const modifiers = {
|
|
57
|
+
shift: false,
|
|
58
|
+
alt: false,
|
|
59
|
+
ctrl: false
|
|
60
|
+
};
|
|
61
|
+
const keys = [];
|
|
62
|
+
for (const token of tokens) {
|
|
63
|
+
const lower = token.toLowerCase();
|
|
64
|
+
if (lower === "shift") {
|
|
65
|
+
modifiers.shift = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (lower === "alt" || lower === "meta") {
|
|
69
|
+
modifiers.alt = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (lower === "ctrl" || lower === "control") {
|
|
73
|
+
modifiers.ctrl = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
keys.push(token);
|
|
77
|
+
}
|
|
78
|
+
const key = keys.join("+").trim();
|
|
79
|
+
if (!key) throw new Error(`Unsupported key: ${spec}`);
|
|
80
|
+
return {
|
|
81
|
+
modifiers,
|
|
82
|
+
key
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function modifierParam(mods) {
|
|
86
|
+
if (!mods.shift && !mods.alt && !mods.ctrl) return null;
|
|
87
|
+
return 1 + (mods.shift ? 1 : 0) + (mods.alt ? 2 : 0) + (mods.ctrl ? 4 : 0);
|
|
88
|
+
}
|
|
89
|
+
function arrowKeyFinal(key) {
|
|
90
|
+
if (key === "up") return "A";
|
|
91
|
+
if (key === "down") return "B";
|
|
92
|
+
if (key === "right") return "C";
|
|
93
|
+
if (key === "left") return "D";
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function homeEndFinalChar(key) {
|
|
97
|
+
if (key === "home") return "H";
|
|
98
|
+
if (key === "end") return "F";
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function tildeKeyCode(key) {
|
|
102
|
+
if (key === "pageup" || key === "pgup") return "5";
|
|
103
|
+
if (key === "pagedown" || key === "pgdn") return "6";
|
|
104
|
+
if (key === "insert" || key === "ins") return "2";
|
|
105
|
+
if (key === "delete" || key === "del") return "3";
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function functionKeySpec(key) {
|
|
109
|
+
const match = /^f(\d{1,2})$/.exec(key);
|
|
110
|
+
if (!match) return null;
|
|
111
|
+
const n = Number(match[1]);
|
|
112
|
+
if (!Number.isFinite(n) || n < 1 || n > 12) return null;
|
|
113
|
+
if (n === 1) return {
|
|
114
|
+
base: `${SS3}P`,
|
|
115
|
+
modified: (m) => `${CSI}1;${m}P`
|
|
116
|
+
};
|
|
117
|
+
if (n === 2) return {
|
|
118
|
+
base: `${SS3}Q`,
|
|
119
|
+
modified: (m) => `${CSI}1;${m}Q`
|
|
120
|
+
};
|
|
121
|
+
if (n === 3) return {
|
|
122
|
+
base: `${SS3}R`,
|
|
123
|
+
modified: (m) => `${CSI}1;${m}R`
|
|
124
|
+
};
|
|
125
|
+
if (n === 4) return {
|
|
126
|
+
base: `${SS3}S`,
|
|
127
|
+
modified: (m) => `${CSI}1;${m}S`
|
|
128
|
+
};
|
|
129
|
+
const code = {
|
|
130
|
+
5: "15",
|
|
131
|
+
6: "17",
|
|
132
|
+
7: "18",
|
|
133
|
+
8: "19",
|
|
134
|
+
9: "20",
|
|
135
|
+
10: "21",
|
|
136
|
+
11: "23",
|
|
137
|
+
12: "24"
|
|
138
|
+
}[n];
|
|
139
|
+
if (!code) return null;
|
|
140
|
+
return {
|
|
141
|
+
base: `${CSI}${code}~`,
|
|
142
|
+
modified: (m) => `${CSI}${code};${m}~`
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/terminal/style.ts
|
|
147
|
+
const DEFAULT_STYLE = {
|
|
148
|
+
fg: { mode: "default" },
|
|
149
|
+
bg: { mode: "default" },
|
|
150
|
+
bold: false,
|
|
151
|
+
dim: false,
|
|
152
|
+
italic: false,
|
|
153
|
+
underline: false,
|
|
154
|
+
inverse: false,
|
|
155
|
+
strikethrough: false
|
|
156
|
+
};
|
|
157
|
+
function extractStyle(cell) {
|
|
158
|
+
const style = {
|
|
159
|
+
fg: extractColor(cell.isFgDefault(), cell.isFgPalette(), cell.isFgRGB(), cell.getFgColor()),
|
|
160
|
+
bg: extractColor(cell.isBgDefault(), cell.isBgPalette(), cell.isBgRGB(), cell.getBgColor()),
|
|
161
|
+
bold: cell.isBold() !== 0,
|
|
162
|
+
dim: cell.isDim() !== 0,
|
|
163
|
+
italic: cell.isItalic() !== 0,
|
|
164
|
+
underline: cell.isUnderline() !== 0,
|
|
165
|
+
inverse: cell.isInverse() !== 0,
|
|
166
|
+
strikethrough: cell.isStrikethrough() !== 0
|
|
167
|
+
};
|
|
168
|
+
return isDefaultStyle(style) ? DEFAULT_STYLE : style;
|
|
169
|
+
}
|
|
170
|
+
function isDefaultStyle(style) {
|
|
171
|
+
return style.fg.mode === "default" && style.bg.mode === "default" && !style.bold && !style.dim && !style.italic && !style.underline && !style.inverse && !style.strikethrough;
|
|
172
|
+
}
|
|
173
|
+
function styleKey(style) {
|
|
174
|
+
return [
|
|
175
|
+
style.fg.mode === "default" ? "d" : `${style.fg.mode}:${style.fg.value}`,
|
|
176
|
+
style.bg.mode === "default" ? "d" : `${style.bg.mode}:${style.bg.value}`,
|
|
177
|
+
style.bold ? "b" : "",
|
|
178
|
+
style.dim ? "d" : "",
|
|
179
|
+
style.italic ? "i" : "",
|
|
180
|
+
style.underline ? "u" : "",
|
|
181
|
+
style.inverse ? "r" : "",
|
|
182
|
+
style.strikethrough ? "s" : ""
|
|
183
|
+
].join("|");
|
|
184
|
+
}
|
|
185
|
+
function findMeaningfulEndCol(line, cols, nullCell) {
|
|
186
|
+
if (!line) return 0;
|
|
187
|
+
for (let x = cols - 1; x >= 0; x -= 1) {
|
|
188
|
+
const cell = line.getCell(x, nullCell);
|
|
189
|
+
if (!cell) continue;
|
|
190
|
+
if (cell.getWidth() === 0) continue;
|
|
191
|
+
const chars = cell.getChars();
|
|
192
|
+
const style = extractStyle(cell);
|
|
193
|
+
if (chars !== "" && chars !== " " || !isDefaultStyle(style)) return x + 1;
|
|
194
|
+
}
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
function extractColor(isDefault, isPalette, isRgb, value) {
|
|
198
|
+
if (isDefault) return { mode: "default" };
|
|
199
|
+
if (isRgb) return {
|
|
200
|
+
mode: "rgb",
|
|
201
|
+
value
|
|
202
|
+
};
|
|
203
|
+
if (isPalette) return {
|
|
204
|
+
mode: "palette",
|
|
205
|
+
value
|
|
206
|
+
};
|
|
207
|
+
return { mode: "default" };
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/terminal/ansi.ts
|
|
211
|
+
const ESC$2 = "\x1B";
|
|
212
|
+
const SGR_RESET = `${ESC$2}[0m`;
|
|
213
|
+
function renderAnsiLines(terminal, options) {
|
|
214
|
+
const scope = options?.scope ?? "visible";
|
|
215
|
+
const trimRight = options?.trimRight ?? false;
|
|
216
|
+
const buffer = terminal.buffer.active;
|
|
217
|
+
const nullCell = buffer.getNullCell();
|
|
218
|
+
let startY = 0;
|
|
219
|
+
let count = buffer.length;
|
|
220
|
+
if (scope === "visible") {
|
|
221
|
+
startY = buffer.viewportY;
|
|
222
|
+
count = terminal.rows;
|
|
223
|
+
}
|
|
224
|
+
const out = [];
|
|
225
|
+
for (let i = 0; i < count; i += 1) {
|
|
226
|
+
const y = startY + i;
|
|
227
|
+
const line = buffer.getLine(y);
|
|
228
|
+
out.push(renderLine(line, terminal.cols, nullCell, trimRight));
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function renderLine(line, cols, nullCell, trimRight) {
|
|
233
|
+
let endCol = cols;
|
|
234
|
+
if (trimRight) endCol = findMeaningfulEndCol(line, cols, nullCell);
|
|
235
|
+
let ansi = "";
|
|
236
|
+
let plain = "";
|
|
237
|
+
let hasStyle = false;
|
|
238
|
+
let usedSgr = false;
|
|
239
|
+
const defaultKey = styleKey(DEFAULT_STYLE);
|
|
240
|
+
let currentKey = defaultKey;
|
|
241
|
+
for (let x = 0; x < endCol; x += 1) {
|
|
242
|
+
const cell = line?.getCell(x, nullCell);
|
|
243
|
+
if (!cell) {
|
|
244
|
+
plain += " ";
|
|
245
|
+
ansi += " ";
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (cell.getWidth() === 0) continue;
|
|
249
|
+
const chars = cell.getChars() || " ";
|
|
250
|
+
const style = extractStyle(cell);
|
|
251
|
+
const isDefault = isDefaultStyle(style);
|
|
252
|
+
if (!isDefault) hasStyle = true;
|
|
253
|
+
const key = styleKey(style);
|
|
254
|
+
if (key !== currentKey) {
|
|
255
|
+
ansi += isDefault ? SGR_RESET : toSgr(style);
|
|
256
|
+
usedSgr = true;
|
|
257
|
+
currentKey = key;
|
|
258
|
+
}
|
|
259
|
+
ansi += chars;
|
|
260
|
+
plain += chars;
|
|
261
|
+
}
|
|
262
|
+
if (usedSgr && currentKey !== defaultKey) ansi += SGR_RESET;
|
|
263
|
+
return {
|
|
264
|
+
ansi,
|
|
265
|
+
plain,
|
|
266
|
+
hasStyle
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function toSgr(style) {
|
|
270
|
+
const codes = ["0"];
|
|
271
|
+
if (style.bold) codes.push("1");
|
|
272
|
+
if (style.dim) codes.push("2");
|
|
273
|
+
if (style.italic) codes.push("3");
|
|
274
|
+
if (style.underline) codes.push("4");
|
|
275
|
+
if (style.inverse) codes.push("7");
|
|
276
|
+
if (style.strikethrough) codes.push("9");
|
|
277
|
+
if (style.fg.mode === "palette") codes.push(`38;5;${style.fg.value}`);
|
|
278
|
+
else if (style.fg.mode === "rgb") {
|
|
279
|
+
const [r, g, b] = rgb(style.fg.value);
|
|
280
|
+
codes.push(`38;2;${r};${g};${b}`);
|
|
281
|
+
}
|
|
282
|
+
if (style.bg.mode === "palette") codes.push(`48;5;${style.bg.value}`);
|
|
283
|
+
else if (style.bg.mode === "rgb") {
|
|
284
|
+
const [r, g, b] = rgb(style.bg.value);
|
|
285
|
+
codes.push(`48;2;${r};${g};${b}`);
|
|
286
|
+
}
|
|
287
|
+
return `${ESC$2}[${codes.join(";")}m`;
|
|
288
|
+
}
|
|
289
|
+
function rgb(value) {
|
|
290
|
+
return [
|
|
291
|
+
value >> 16 & 255,
|
|
292
|
+
value >> 8 & 255,
|
|
293
|
+
value & 255
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/terminal/mouse.ts
|
|
298
|
+
const ESC$1 = "\x1B";
|
|
299
|
+
function encodeSgrMouse(event) {
|
|
300
|
+
const x = clampInt(event.x, 1, 500);
|
|
301
|
+
const y = clampInt(event.y, 1, 300);
|
|
302
|
+
if (event.action === "click") return `${encodeSingleSgrMouse({
|
|
303
|
+
...event,
|
|
304
|
+
action: "down"
|
|
305
|
+
}, x, y)}${encodeSingleSgrMouse({
|
|
306
|
+
...event,
|
|
307
|
+
action: "up"
|
|
308
|
+
}, x, y)}`;
|
|
309
|
+
return encodeSingleSgrMouse(event, x, y);
|
|
310
|
+
}
|
|
311
|
+
function encodeSingleSgrMouse(event, x, y) {
|
|
312
|
+
const modifiers = event.modifiers;
|
|
313
|
+
const modifierBits = (modifiers?.shift ? 4 : 0) + (modifiers?.alt ? 8 : 0) + (modifiers?.ctrl ? 16 : 0);
|
|
314
|
+
const buttonCode = buttonToCode(event.button ?? "left");
|
|
315
|
+
if (event.action === "down") return `${ESC$1}[<${buttonCode + modifierBits};${x};${y}M`;
|
|
316
|
+
if (event.action === "up") return `${ESC$1}[<${buttonCode + modifierBits};${x};${y}m`;
|
|
317
|
+
if (event.action === "scroll_up") return `${ESC$1}[<${64 + modifierBits};${x};${y}M`;
|
|
318
|
+
if (event.action === "scroll_down") return `${ESC$1}[<${65 + modifierBits};${x};${y}M`;
|
|
319
|
+
return `${ESC$1}[<${32 + (event.button ? buttonCode : 3) + modifierBits};${x};${y}M`;
|
|
320
|
+
}
|
|
321
|
+
function buttonToCode(button) {
|
|
322
|
+
if (button === "left") return 0;
|
|
323
|
+
if (button === "middle") return 1;
|
|
324
|
+
return 2;
|
|
325
|
+
}
|
|
326
|
+
function clampInt(value, min, max) {
|
|
327
|
+
if (!Number.isFinite(value)) return min;
|
|
328
|
+
const int = Math.trunc(value);
|
|
329
|
+
if (int < min) return min;
|
|
330
|
+
if (int > max) return max;
|
|
331
|
+
return int;
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/terminal/snapshot.ts
|
|
335
|
+
function snapshotVisibleLines(terminal, trimRight) {
|
|
336
|
+
const buffer = terminal.buffer.active;
|
|
337
|
+
const nullCell = buffer.getNullCell();
|
|
338
|
+
const startY = buffer.viewportY;
|
|
339
|
+
const lines = [];
|
|
340
|
+
for (let row = 0; row < terminal.rows; row += 1) {
|
|
341
|
+
const line = buffer.getLine(startY + row);
|
|
342
|
+
lines.push(snapshotPlainLine(line, terminal.cols, nullCell, trimRight));
|
|
343
|
+
}
|
|
344
|
+
return lines;
|
|
345
|
+
}
|
|
346
|
+
function snapshotVisibleStyleRuns(terminal, trimRight) {
|
|
347
|
+
const buffer = terminal.buffer.active;
|
|
348
|
+
const nullCell = buffer.getNullCell();
|
|
349
|
+
const startY = buffer.viewportY;
|
|
350
|
+
const out = [];
|
|
351
|
+
for (let row = 0; row < terminal.rows; row += 1) {
|
|
352
|
+
const line = buffer.getLine(startY + row);
|
|
353
|
+
out.push(snapshotStyleRuns(line, terminal.cols, nullCell, trimRight));
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
function snapshotBufferLines(terminal, trimRight) {
|
|
358
|
+
const buffer = terminal.buffer.active;
|
|
359
|
+
const lines = [];
|
|
360
|
+
for (let y = 0; y < buffer.length; y += 1) {
|
|
361
|
+
const line = buffer.getLine(y);
|
|
362
|
+
lines.push(line ? line.translateToString(trimRight, 0, terminal.cols) : "");
|
|
363
|
+
}
|
|
364
|
+
return lines;
|
|
365
|
+
}
|
|
366
|
+
function snapshotLines(terminal, options) {
|
|
367
|
+
const trimRight = options?.trimRight ?? true;
|
|
368
|
+
return (options?.scope ?? "visible") === "buffer" ? snapshotBufferLines(terminal, trimRight) : snapshotVisibleLines(terminal, trimRight);
|
|
369
|
+
}
|
|
370
|
+
function snapshotGrid(terminal, options) {
|
|
371
|
+
const trimRight = options?.trimRight ?? true;
|
|
372
|
+
const includeStyles = options?.includeStyles ?? false;
|
|
373
|
+
const buffer = terminal.buffer.active;
|
|
374
|
+
const lines = snapshotVisibleLines(terminal, trimRight);
|
|
375
|
+
return {
|
|
376
|
+
cols: terminal.cols,
|
|
377
|
+
rows: terminal.rows,
|
|
378
|
+
bufferType: buffer.type,
|
|
379
|
+
cursorX: buffer.cursorX,
|
|
380
|
+
cursorY: buffer.cursorY,
|
|
381
|
+
viewportY: buffer.viewportY,
|
|
382
|
+
lines,
|
|
383
|
+
styleRuns: includeStyles ? snapshotVisibleStyleRuns(terminal, trimRight) : void 0
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function snapshotPlainLine(line, cols, nullCell, trimRight) {
|
|
387
|
+
let endCol = cols;
|
|
388
|
+
if (trimRight) endCol = findMeaningfulEndCol(line, cols, nullCell);
|
|
389
|
+
let out = "";
|
|
390
|
+
for (let x = 0; x < endCol; x += 1) {
|
|
391
|
+
const cell = line?.getCell(x, nullCell);
|
|
392
|
+
if (!cell) {
|
|
393
|
+
out += " ";
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (cell.getWidth() === 0) continue;
|
|
397
|
+
out += cell.getChars() || " ";
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
function snapshotStyleRuns(line, cols, nullCell, trimRight) {
|
|
402
|
+
let endCol = cols;
|
|
403
|
+
if (trimRight) endCol = findMeaningfulEndCol(line, cols, nullCell);
|
|
404
|
+
const out = [];
|
|
405
|
+
let currentKey = null;
|
|
406
|
+
let currentRun = null;
|
|
407
|
+
for (let x = 0; x < endCol; x += 1) {
|
|
408
|
+
const cell = line?.getCell(x, nullCell);
|
|
409
|
+
if (!cell) {
|
|
410
|
+
if (currentRun) {
|
|
411
|
+
out.push(currentRun);
|
|
412
|
+
currentRun = null;
|
|
413
|
+
currentKey = null;
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const width = cell.getWidth();
|
|
418
|
+
if (width === 0) continue;
|
|
419
|
+
const style = extractStyle(cell);
|
|
420
|
+
if (isDefaultStyle(style)) {
|
|
421
|
+
if (currentRun) {
|
|
422
|
+
out.push(currentRun);
|
|
423
|
+
currentRun = null;
|
|
424
|
+
currentKey = null;
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const key = styleKey(style);
|
|
429
|
+
if (currentRun && key === currentKey) {
|
|
430
|
+
currentRun.endCol = x + width;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (currentRun) out.push(currentRun);
|
|
434
|
+
currentKey = key;
|
|
435
|
+
currentRun = {
|
|
436
|
+
startCol: x,
|
|
437
|
+
endCol: x + width,
|
|
438
|
+
style
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (currentRun) out.push(currentRun);
|
|
442
|
+
return out;
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/terminal/mask.ts
|
|
446
|
+
function applyTextMaskRules(lines, rules) {
|
|
447
|
+
if (!rules || rules.length === 0) return lines;
|
|
448
|
+
const compiled = compileMaskRules(rules);
|
|
449
|
+
if (compiled.length === 0) return lines;
|
|
450
|
+
return lines.map((line) => applyCompiledMaskRules(line, compiled));
|
|
451
|
+
}
|
|
452
|
+
function compileMaskRules(rules) {
|
|
453
|
+
const compiled = [];
|
|
454
|
+
for (const rule of rules) {
|
|
455
|
+
if (!rule.regex.trim()) continue;
|
|
456
|
+
const preserveLength = rule.preserveLength ?? false;
|
|
457
|
+
const replacement = preserveLength ? firstChar(rule.replacement) : rule.replacement ?? "<masked>";
|
|
458
|
+
const flags = normalizeFlags(rule.flags);
|
|
459
|
+
let regex;
|
|
460
|
+
try {
|
|
461
|
+
regex = new RegExp(rule.regex, flags);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
throw new Error(`invalid mask rule regex=${JSON.stringify(rule.regex)} flags=${JSON.stringify(rule.flags ?? "")}: ${error.message}`);
|
|
464
|
+
}
|
|
465
|
+
compiled.push({
|
|
466
|
+
regex,
|
|
467
|
+
replacement,
|
|
468
|
+
preserveLength
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return compiled;
|
|
472
|
+
}
|
|
473
|
+
function normalizeFlags(flags) {
|
|
474
|
+
const value = flags?.trim() ? flags.trim() : "g";
|
|
475
|
+
const set = new Set(value.split(""));
|
|
476
|
+
set.add("g");
|
|
477
|
+
return [...set].join("");
|
|
478
|
+
}
|
|
479
|
+
function firstChar(value) {
|
|
480
|
+
const trimmed = value?.trim();
|
|
481
|
+
return trimmed && trimmed.length > 0 ? trimmed[0] : "█";
|
|
482
|
+
}
|
|
483
|
+
function applyCompiledMaskRules(line, rules) {
|
|
484
|
+
let out = line;
|
|
485
|
+
for (const rule of rules) out = out.replace(rule.regex, (match) => rule.preserveLength ? rule.replacement.repeat(match.length) : rule.replacement);
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/util/hash.ts
|
|
490
|
+
function fnv1a32(text) {
|
|
491
|
+
let hash = 2166136261;
|
|
492
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
493
|
+
hash ^= text.charCodeAt(i);
|
|
494
|
+
hash = Math.imul(hash, 16777619);
|
|
495
|
+
}
|
|
496
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/util/sleep.ts
|
|
500
|
+
async function sleep(ms) {
|
|
501
|
+
await new Promise((resolve) => {
|
|
502
|
+
setTimeout(resolve, ms);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/trace/asciicast.ts
|
|
507
|
+
function encodeAsciicast(header, events) {
|
|
508
|
+
const lines = [JSON.stringify(header)];
|
|
509
|
+
for (const event of events) lines.push(JSON.stringify(event));
|
|
510
|
+
return `${lines.join("\n")}\n`;
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/trace/recorder.ts
|
|
514
|
+
const DEFAULT_MAX_EVENTS = 5e4;
|
|
515
|
+
const DEFAULT_MAX_DATA_CHARS = 5e6;
|
|
516
|
+
const DEFAULT_TIME_PRECISION_MS = 1;
|
|
517
|
+
var TraceRecorder = class {
|
|
518
|
+
header;
|
|
519
|
+
startedAtMs;
|
|
520
|
+
maxEvents;
|
|
521
|
+
maxDataChars;
|
|
522
|
+
mergeOutput;
|
|
523
|
+
timePrecisionMs;
|
|
524
|
+
events = [];
|
|
525
|
+
dataChars = 0;
|
|
526
|
+
droppedEvents = 0;
|
|
527
|
+
droppedDataChars = 0;
|
|
528
|
+
constructor(header, options) {
|
|
529
|
+
this.header = header;
|
|
530
|
+
this.startedAtMs = performance.now();
|
|
531
|
+
this.maxEvents = Math.max(1, Math.trunc(options?.maxEvents ?? DEFAULT_MAX_EVENTS));
|
|
532
|
+
this.maxDataChars = Math.max(1, Math.trunc(options?.maxDataChars ?? DEFAULT_MAX_DATA_CHARS));
|
|
533
|
+
this.mergeOutput = options?.mergeOutput ?? true;
|
|
534
|
+
this.timePrecisionMs = Math.max(1, Math.trunc(options?.timePrecisionMs ?? DEFAULT_TIME_PRECISION_MS));
|
|
535
|
+
}
|
|
536
|
+
recordOutput(data) {
|
|
537
|
+
this.addEvent("o", data);
|
|
538
|
+
}
|
|
539
|
+
recordInput(data) {
|
|
540
|
+
this.addEvent("i", data);
|
|
541
|
+
}
|
|
542
|
+
recordResize(cols, rows) {
|
|
543
|
+
this.addEvent("r", `${cols}x${rows}`);
|
|
544
|
+
}
|
|
545
|
+
mark(label) {
|
|
546
|
+
this.addEvent("m", label ?? "");
|
|
547
|
+
}
|
|
548
|
+
snapshot(options) {
|
|
549
|
+
const tailEvents = options?.tailEvents;
|
|
550
|
+
const events = tailEvents ? this.events.slice(-Math.max(0, Math.trunc(tailEvents))) : [...this.events];
|
|
551
|
+
return {
|
|
552
|
+
header: this.header,
|
|
553
|
+
events,
|
|
554
|
+
cast: encodeAsciicast(this.header, events),
|
|
555
|
+
droppedEvents: this.droppedEvents,
|
|
556
|
+
droppedDataChars: this.droppedDataChars
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
addEvent(type, data) {
|
|
560
|
+
const timeSeconds = this.nowSeconds();
|
|
561
|
+
const last = this.events.at(-1);
|
|
562
|
+
if (this.mergeOutput && type === "o" && last && last[1] === "o" && last[0] === timeSeconds) {
|
|
563
|
+
last[2] += data;
|
|
564
|
+
this.dataChars += data.length;
|
|
565
|
+
this.trim();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this.events.push([
|
|
569
|
+
timeSeconds,
|
|
570
|
+
type,
|
|
571
|
+
data
|
|
572
|
+
]);
|
|
573
|
+
this.dataChars += data.length;
|
|
574
|
+
this.trim();
|
|
575
|
+
}
|
|
576
|
+
nowSeconds() {
|
|
577
|
+
const elapsedMs = performance.now() - this.startedAtMs;
|
|
578
|
+
return Math.round(elapsedMs / this.timePrecisionMs) * this.timePrecisionMs / 1e3;
|
|
579
|
+
}
|
|
580
|
+
trim() {
|
|
581
|
+
while (this.events.length > this.maxEvents || this.dataChars > this.maxDataChars) {
|
|
582
|
+
const removed = this.events.shift();
|
|
583
|
+
if (!removed) break;
|
|
584
|
+
const chars = removed[2].length;
|
|
585
|
+
this.dataChars -= chars;
|
|
586
|
+
this.droppedEvents += 1;
|
|
587
|
+
this.droppedDataChars += chars;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/session/terminal_session.ts
|
|
593
|
+
const ESC = "\x1B";
|
|
594
|
+
var TerminalSession = class {
|
|
595
|
+
id;
|
|
596
|
+
pty;
|
|
597
|
+
terminal;
|
|
598
|
+
snapshotRingSize;
|
|
599
|
+
trace;
|
|
600
|
+
writeChain = Promise.resolve();
|
|
601
|
+
disposables = [];
|
|
602
|
+
rawOutputRing = [];
|
|
603
|
+
snapshotRing = [];
|
|
604
|
+
closed = null;
|
|
605
|
+
constructor(options) {
|
|
606
|
+
this.id = options.id;
|
|
607
|
+
this.pty = options.pty;
|
|
608
|
+
this.snapshotRingSize = options.snapshotRingSize;
|
|
609
|
+
this.terminal = new Terminal({
|
|
610
|
+
cols: options.cols,
|
|
611
|
+
rows: options.rows,
|
|
612
|
+
allowProposedApi: true,
|
|
613
|
+
scrollback: 2e3,
|
|
614
|
+
convertEol: true
|
|
615
|
+
});
|
|
616
|
+
this.trace = new TraceRecorder({
|
|
617
|
+
version: 2,
|
|
618
|
+
width: options.cols,
|
|
619
|
+
height: options.rows,
|
|
620
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
621
|
+
env: options.trace?.env,
|
|
622
|
+
title: options.trace?.title ?? options.id,
|
|
623
|
+
command: options.trace?.command,
|
|
624
|
+
term: options.trace?.env?.TERM
|
|
625
|
+
});
|
|
626
|
+
this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "n" }, (params) => this.handleCsiDsr(params)));
|
|
627
|
+
this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "c" }, (params) => this.handleCsiDa(params)));
|
|
628
|
+
this.disposables.push(this.pty.onData((data) => {
|
|
629
|
+
this.appendRawOutput(data);
|
|
630
|
+
this.trace.recordOutput(data);
|
|
631
|
+
this.enqueueWrite(data);
|
|
632
|
+
}));
|
|
633
|
+
this.disposables.push(this.pty.onExit((event) => {
|
|
634
|
+
this.closed = {
|
|
635
|
+
type: "process_exit",
|
|
636
|
+
exitCode: event.exitCode,
|
|
637
|
+
signal: event.signal
|
|
638
|
+
};
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
get cols() {
|
|
642
|
+
return this.terminal.cols;
|
|
643
|
+
}
|
|
644
|
+
get rows() {
|
|
645
|
+
return this.terminal.rows;
|
|
646
|
+
}
|
|
647
|
+
isClosed() {
|
|
648
|
+
return this.closed !== null;
|
|
649
|
+
}
|
|
650
|
+
getCloseReason() {
|
|
651
|
+
return this.closed;
|
|
652
|
+
}
|
|
653
|
+
resize(cols, rows) {
|
|
654
|
+
this.trace.recordResize(cols, rows);
|
|
655
|
+
this.pty.resize(cols, rows);
|
|
656
|
+
this.terminal.resize(cols, rows);
|
|
657
|
+
}
|
|
658
|
+
sendText(text, options) {
|
|
659
|
+
const payload = options?.enter ?? false ? `${text}\r` : text;
|
|
660
|
+
this.trace.recordInput(payload);
|
|
661
|
+
this.pty.write(payload);
|
|
662
|
+
}
|
|
663
|
+
pressKey(key) {
|
|
664
|
+
const encoded = encodeKey(key);
|
|
665
|
+
this.trace.recordInput(encoded);
|
|
666
|
+
this.pty.write(encoded);
|
|
667
|
+
}
|
|
668
|
+
sendMouse(event) {
|
|
669
|
+
const encoded = encodeSgrMouse(event);
|
|
670
|
+
this.trace.recordInput(encoded);
|
|
671
|
+
this.pty.write(encoded);
|
|
672
|
+
}
|
|
673
|
+
async flush() {
|
|
674
|
+
await this.writeChain;
|
|
675
|
+
}
|
|
676
|
+
getMeta() {
|
|
677
|
+
const buffer = this.terminal.buffer.active;
|
|
678
|
+
return {
|
|
679
|
+
cols: this.terminal.cols,
|
|
680
|
+
rows: this.terminal.rows,
|
|
681
|
+
bufferType: buffer.type,
|
|
682
|
+
viewportY: buffer.viewportY,
|
|
683
|
+
baseY: buffer.baseY,
|
|
684
|
+
length: buffer.length,
|
|
685
|
+
cursorX: buffer.cursorX,
|
|
686
|
+
cursorY: buffer.cursorY
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
async snapshotText(options) {
|
|
690
|
+
await this.flush();
|
|
691
|
+
if (options?.maxLines !== void 0 && options.tailLines !== void 0) throw new Error("snapshotText: maxLines and tailLines are mutually exclusive");
|
|
692
|
+
let lines = snapshotLines(this.terminal, {
|
|
693
|
+
scope: options?.scope,
|
|
694
|
+
trimRight: options?.trimRight
|
|
695
|
+
});
|
|
696
|
+
if (options?.trimBottom ?? true) lines = trimBottomEmptyLines(lines);
|
|
697
|
+
if (options?.maxLines !== void 0) {
|
|
698
|
+
const max = Math.max(0, Math.trunc(options.maxLines));
|
|
699
|
+
lines = lines.slice(0, max);
|
|
700
|
+
}
|
|
701
|
+
if (options?.tailLines !== void 0) {
|
|
702
|
+
const tail = Math.max(0, Math.trunc(options.tailLines));
|
|
703
|
+
lines = lines.slice(Math.max(0, lines.length - tail));
|
|
704
|
+
}
|
|
705
|
+
lines = applyTextMaskRules(lines, options?.mask);
|
|
706
|
+
const text = lines.join("\n");
|
|
707
|
+
const hash = fnv1a32(text);
|
|
708
|
+
if (options?.captureFrame ?? true) this.captureFrame(text, hash);
|
|
709
|
+
return {
|
|
710
|
+
text,
|
|
711
|
+
hash
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
async snapshotAnsi(options) {
|
|
715
|
+
await this.flush();
|
|
716
|
+
if (options?.maxLines !== void 0 && options.tailLines !== void 0) throw new Error("snapshotAnsi: maxLines and tailLines are mutually exclusive");
|
|
717
|
+
let lines = renderAnsiLines(this.terminal, {
|
|
718
|
+
scope: options?.scope,
|
|
719
|
+
trimRight: options?.trimRight
|
|
720
|
+
});
|
|
721
|
+
if (options?.trimBottom ?? true) lines = trimBottomEmptyAnsiLines(lines);
|
|
722
|
+
if (options?.maxLines !== void 0) {
|
|
723
|
+
const max = Math.max(0, Math.trunc(options.maxLines));
|
|
724
|
+
lines = lines.slice(0, max);
|
|
725
|
+
}
|
|
726
|
+
if (options?.tailLines !== void 0) {
|
|
727
|
+
const tail = Math.max(0, Math.trunc(options.tailLines));
|
|
728
|
+
lines = lines.slice(Math.max(0, lines.length - tail));
|
|
729
|
+
}
|
|
730
|
+
if (options?.mask && options.mask.length > 0) {
|
|
731
|
+
const maskedPlain = applyTextMaskRules(lines.map((line) => line.plain), options.mask);
|
|
732
|
+
const maskedAnsi = applyTextMaskRules(lines.map((line) => line.ansi), options.mask);
|
|
733
|
+
lines = lines.map((line, idx) => ({
|
|
734
|
+
...line,
|
|
735
|
+
plain: maskedPlain[idx] ?? "",
|
|
736
|
+
ansi: maskedAnsi[idx] ?? ""
|
|
737
|
+
}));
|
|
738
|
+
}
|
|
739
|
+
const ansi = lines.map((l) => l.ansi).join("\n");
|
|
740
|
+
return {
|
|
741
|
+
ansi,
|
|
742
|
+
plain: lines.map((l) => l.plain).join("\n"),
|
|
743
|
+
hash: fnv1a32(ansi),
|
|
744
|
+
lines
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
async snapshotGrid(options) {
|
|
748
|
+
await this.flush();
|
|
749
|
+
const grid = snapshotGrid(this.terminal, {
|
|
750
|
+
trimRight: options?.trimRight,
|
|
751
|
+
includeStyles: options?.includeStyles
|
|
752
|
+
});
|
|
753
|
+
const hash = fnv1a32(JSON.stringify(grid));
|
|
754
|
+
if (options?.captureFrame ?? true) this.captureFrame(grid.lines.join("\n"), hash);
|
|
755
|
+
return {
|
|
756
|
+
grid,
|
|
757
|
+
hash
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
async snapshotCast(options) {
|
|
761
|
+
await this.flush();
|
|
762
|
+
return this.trace.snapshot({ tailEvents: options?.tailEvents });
|
|
763
|
+
}
|
|
764
|
+
mark(label) {
|
|
765
|
+
this.trace.mark(label);
|
|
766
|
+
}
|
|
767
|
+
getSnapshotFrames() {
|
|
768
|
+
return [...this.snapshotRing];
|
|
769
|
+
}
|
|
770
|
+
getRawOutputChunks() {
|
|
771
|
+
return [...this.rawOutputRing];
|
|
772
|
+
}
|
|
773
|
+
async waitForText(args) {
|
|
774
|
+
const startedAt = Date.now();
|
|
775
|
+
let closedSince = null;
|
|
776
|
+
while (Date.now() - startedAt <= args.timeoutMs) {
|
|
777
|
+
const snapshot = await this.snapshotText({
|
|
778
|
+
captureFrame: true,
|
|
779
|
+
scope: args.scope
|
|
780
|
+
});
|
|
781
|
+
if (args.text && snapshot.text.includes(args.text)) return {
|
|
782
|
+
found: true,
|
|
783
|
+
...snapshot
|
|
784
|
+
};
|
|
785
|
+
if (args.regex && args.regex.test(snapshot.text)) return {
|
|
786
|
+
found: true,
|
|
787
|
+
...snapshot
|
|
788
|
+
};
|
|
789
|
+
if (this.isClosed()) {
|
|
790
|
+
closedSince ??= Date.now();
|
|
791
|
+
const drainMs = Math.max(500, args.intervalMs * 4);
|
|
792
|
+
if (Date.now() - closedSince >= drainMs) break;
|
|
793
|
+
}
|
|
794
|
+
await sleep(args.intervalMs);
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
found: false,
|
|
798
|
+
...await this.snapshotText({
|
|
799
|
+
captureFrame: true,
|
|
800
|
+
scope: args.scope
|
|
801
|
+
})
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
async waitForStableScreen(args) {
|
|
805
|
+
const startedAt = Date.now();
|
|
806
|
+
let stableSince = null;
|
|
807
|
+
let lastHash = null;
|
|
808
|
+
while (Date.now() - startedAt <= args.timeoutMs) {
|
|
809
|
+
const snapshot = await this.snapshotText({ captureFrame: true });
|
|
810
|
+
if (snapshot.hash === lastHash) stableSince ??= Date.now();
|
|
811
|
+
else {
|
|
812
|
+
stableSince = null;
|
|
813
|
+
lastHash = snapshot.hash;
|
|
814
|
+
}
|
|
815
|
+
if (stableSince !== null && Date.now() - stableSince >= args.quietMs) return {
|
|
816
|
+
stable: true,
|
|
817
|
+
...snapshot
|
|
818
|
+
};
|
|
819
|
+
await sleep(args.intervalMs);
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
stable: false,
|
|
823
|
+
...await this.snapshotText({ captureFrame: true })
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
close() {
|
|
827
|
+
if (this.closed === null) this.closed = { type: "closed_by_user" };
|
|
828
|
+
this.pty.kill();
|
|
829
|
+
for (const d of this.disposables) d.dispose();
|
|
830
|
+
this.terminal.dispose();
|
|
831
|
+
}
|
|
832
|
+
enqueueWrite(data) {
|
|
833
|
+
this.writeChain = this.writeChain.then(() => new Promise((resolve) => {
|
|
834
|
+
this.terminal.write(data, resolve);
|
|
835
|
+
}));
|
|
836
|
+
}
|
|
837
|
+
appendRawOutput(data) {
|
|
838
|
+
this.rawOutputRing.push(data);
|
|
839
|
+
if (this.rawOutputRing.length > 2e3) this.rawOutputRing.splice(0, this.rawOutputRing.length - 2e3);
|
|
840
|
+
}
|
|
841
|
+
captureFrame(text, hash) {
|
|
842
|
+
this.snapshotRing.push({
|
|
843
|
+
atMs: Date.now(),
|
|
844
|
+
hash,
|
|
845
|
+
text
|
|
846
|
+
});
|
|
847
|
+
if (this.snapshotRing.length > this.snapshotRingSize) this.snapshotRing.splice(0, this.snapshotRing.length - this.snapshotRingSize);
|
|
848
|
+
}
|
|
849
|
+
writePtySafely(data) {
|
|
850
|
+
if (this.isClosed()) return;
|
|
851
|
+
try {
|
|
852
|
+
this.pty.write(data);
|
|
853
|
+
} catch {}
|
|
854
|
+
}
|
|
855
|
+
handleCsiDsr(params) {
|
|
856
|
+
if (params.length !== 1) return false;
|
|
857
|
+
const raw = params[0];
|
|
858
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
859
|
+
if (value === 5) {
|
|
860
|
+
this.writePtySafely(`${ESC}[0n`);
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
if (value !== 6) return false;
|
|
864
|
+
const meta = this.getMeta();
|
|
865
|
+
const row = meta.baseY + meta.cursorY - meta.viewportY + 1;
|
|
866
|
+
const col = meta.cursorX + 1;
|
|
867
|
+
this.writePtySafely(`${ESC}[${row};${col}R`);
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
handleCsiDa(params) {
|
|
871
|
+
if (params.length > 1) return false;
|
|
872
|
+
const raw = params[0];
|
|
873
|
+
if ((raw === void 0 ? 0 : Array.isArray(raw) ? raw[0] : raw) !== 0) return false;
|
|
874
|
+
this.writePtySafely(`${ESC}[?1;2c`);
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
function trimBottomEmptyLines(lines) {
|
|
879
|
+
let end = lines.length;
|
|
880
|
+
while (end > 0 && lines[end - 1] === "") end -= 1;
|
|
881
|
+
return end === lines.length ? lines : lines.slice(0, end);
|
|
882
|
+
}
|
|
883
|
+
function trimBottomEmptyAnsiLines(lines) {
|
|
884
|
+
let end = lines.length;
|
|
885
|
+
while (end > 0) {
|
|
886
|
+
const line = lines[end - 1];
|
|
887
|
+
if (!(!line?.hasStyle && (line?.plain ?? "").trim() === "")) break;
|
|
888
|
+
end -= 1;
|
|
889
|
+
}
|
|
890
|
+
return end === lines.length ? lines : lines.slice(0, end);
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
893
|
+
export { applyTextMaskRules as a, encodeSgrMouse as c, isDefaultStyle as d, styleKey as f, fnv1a32 as i, extractStyle as l, TraceRecorder as n, snapshotGrid as o, encodeKey as p, sleep as r, snapshotLines as s, TerminalSession as t, findMeaningfulEndCol as u };
|