ptywright 0.1.1 → 0.3.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 +318 -1
- package/dist/agent.mjs +2 -0
- package/dist/bin/ptywright.mjs +6 -0
- package/dist/cli-CfvlbRoZ.mjs +3585 -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-zApMYWZx.mjs +3257 -0
- package/dist/runner-zi0nItvB.mjs +1874 -0
- package/dist/script.mjs +2 -0
- package/dist/server-BC3yo-dq.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 +166 -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/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
package/src/terminal/ansi.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { Terminal } from "@xterm/headless";
|
|
2
|
-
|
|
3
|
-
import type { SnapshotScope } from "./snapshot";
|
|
4
|
-
import type { CellStyle } from "./style";
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_STYLE,
|
|
7
|
-
extractStyle,
|
|
8
|
-
findMeaningfulEndCol,
|
|
9
|
-
isDefaultStyle,
|
|
10
|
-
styleKey,
|
|
11
|
-
} from "./style";
|
|
12
|
-
|
|
13
|
-
export type AnsiRenderedLine = {
|
|
14
|
-
ansi: string;
|
|
15
|
-
plain: string;
|
|
16
|
-
hasStyle: boolean;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type SnapshotAnsiOptions = {
|
|
20
|
-
scope?: SnapshotScope;
|
|
21
|
-
trimRight?: boolean;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const ESC = "\x1b";
|
|
25
|
-
const SGR_RESET = `${ESC}[0m`;
|
|
26
|
-
|
|
27
|
-
export function renderAnsiLines(
|
|
28
|
-
terminal: Terminal,
|
|
29
|
-
options?: SnapshotAnsiOptions,
|
|
30
|
-
): AnsiRenderedLine[] {
|
|
31
|
-
const scope = options?.scope ?? "visible";
|
|
32
|
-
const trimRight = options?.trimRight ?? false;
|
|
33
|
-
|
|
34
|
-
const buffer = terminal.buffer.active;
|
|
35
|
-
const nullCell = buffer.getNullCell();
|
|
36
|
-
|
|
37
|
-
let startY = 0;
|
|
38
|
-
let count = buffer.length;
|
|
39
|
-
if (scope === "visible") {
|
|
40
|
-
startY = buffer.viewportY;
|
|
41
|
-
count = terminal.rows;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const out: AnsiRenderedLine[] = [];
|
|
45
|
-
for (let i = 0; i < count; i += 1) {
|
|
46
|
-
const y = startY + i;
|
|
47
|
-
const line = buffer.getLine(y);
|
|
48
|
-
out.push(renderLine(line, terminal.cols, nullCell, trimRight));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function renderLine(
|
|
55
|
-
line: ReturnType<Terminal["buffer"]["active"]["getLine"]>,
|
|
56
|
-
cols: number,
|
|
57
|
-
nullCell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
|
|
58
|
-
trimRight: boolean,
|
|
59
|
-
): AnsiRenderedLine {
|
|
60
|
-
let endCol = cols;
|
|
61
|
-
if (trimRight) {
|
|
62
|
-
endCol = findMeaningfulEndCol(line, cols, nullCell);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
let ansi = "";
|
|
66
|
-
let plain = "";
|
|
67
|
-
let hasStyle = false;
|
|
68
|
-
let usedSgr = false;
|
|
69
|
-
|
|
70
|
-
const defaultKey = styleKey(DEFAULT_STYLE);
|
|
71
|
-
let currentKey = defaultKey;
|
|
72
|
-
for (let x = 0; x < endCol; x += 1) {
|
|
73
|
-
const cell = line?.getCell(x, nullCell);
|
|
74
|
-
if (!cell) {
|
|
75
|
-
plain += " ";
|
|
76
|
-
ansi += " ";
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const width = cell.getWidth();
|
|
81
|
-
if (width === 0) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const chars = cell.getChars() || " ";
|
|
86
|
-
const style = extractStyle(cell);
|
|
87
|
-
const isDefault = isDefaultStyle(style);
|
|
88
|
-
if (!isDefault) {
|
|
89
|
-
hasStyle = true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const key = styleKey(style);
|
|
93
|
-
if (key !== currentKey) {
|
|
94
|
-
ansi += isDefault ? SGR_RESET : toSgr(style);
|
|
95
|
-
usedSgr = true;
|
|
96
|
-
currentKey = key;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
ansi += chars;
|
|
100
|
-
plain += chars;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (usedSgr && currentKey !== defaultKey) {
|
|
104
|
-
ansi += SGR_RESET;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return { ansi, plain, hasStyle };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function toSgr(style: CellStyle): string {
|
|
111
|
-
const codes: string[] = ["0"];
|
|
112
|
-
|
|
113
|
-
if (style.bold) codes.push("1");
|
|
114
|
-
if (style.dim) codes.push("2");
|
|
115
|
-
if (style.italic) codes.push("3");
|
|
116
|
-
if (style.underline) codes.push("4");
|
|
117
|
-
if (style.inverse) codes.push("7");
|
|
118
|
-
if (style.strikethrough) codes.push("9");
|
|
119
|
-
|
|
120
|
-
if (style.fg.mode === "palette") {
|
|
121
|
-
codes.push(`38;5;${style.fg.value}`);
|
|
122
|
-
} else if (style.fg.mode === "rgb") {
|
|
123
|
-
const [r, g, b] = rgb(style.fg.value);
|
|
124
|
-
codes.push(`38;2;${r};${g};${b}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (style.bg.mode === "palette") {
|
|
128
|
-
codes.push(`48;5;${style.bg.value}`);
|
|
129
|
-
} else if (style.bg.mode === "rgb") {
|
|
130
|
-
const [r, g, b] = rgb(style.bg.value);
|
|
131
|
-
codes.push(`48;2;${r};${g};${b}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return `${ESC}[${codes.join(";")}m`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function rgb(value: number): [number, number, number] {
|
|
138
|
-
const r = (value >> 16) & 0xff;
|
|
139
|
-
const g = (value >> 8) & 0xff;
|
|
140
|
-
const b = value & 0xff;
|
|
141
|
-
return [r, g, b];
|
|
142
|
-
}
|
package/src/terminal/keys.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
const CSI = "\x1b[";
|
|
2
|
-
const SS3 = "\x1bO";
|
|
3
|
-
|
|
4
|
-
function ctrlChar(letter: string): string {
|
|
5
|
-
const upper = letter.toUpperCase();
|
|
6
|
-
const code = upper.charCodeAt(0);
|
|
7
|
-
if (code < 65 || code > 90) {
|
|
8
|
-
throw new Error(`Unsupported ctrl key: ${letter}`);
|
|
9
|
-
}
|
|
10
|
-
return String.fromCharCode(code - 64);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function encodeKey(key: string): string {
|
|
14
|
-
const normalized = key.trim();
|
|
15
|
-
|
|
16
|
-
if (normalized.length === 1) {
|
|
17
|
-
return normalized;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Back-compat: "c-x" style.
|
|
21
|
-
const ctrlMatch = /^c-(.)$/i.exec(normalized);
|
|
22
|
-
if (ctrlMatch) {
|
|
23
|
-
return ctrlChar(ctrlMatch[1] ?? "");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const parsed = parseKeySpec(normalized);
|
|
27
|
-
const mod = modifierParam(parsed.modifiers);
|
|
28
|
-
|
|
29
|
-
const keyName = parsed.key.toLowerCase();
|
|
30
|
-
|
|
31
|
-
if (keyName === "enter" || keyName === "return") return "\r";
|
|
32
|
-
if (keyName === "esc" || keyName === "escape") return "\x1b";
|
|
33
|
-
if (keyName === "backspace") return "\x7f";
|
|
34
|
-
|
|
35
|
-
if (keyName === "space") return " ";
|
|
36
|
-
|
|
37
|
-
if (keyName === "tab") {
|
|
38
|
-
return parsed.modifiers.shift ? `${CSI}Z` : "\t";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (keyName === "backtab" || keyName === "btab") {
|
|
42
|
-
return `${CSI}Z`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const arrowFinal = arrowKeyFinal(keyName);
|
|
46
|
-
if (arrowFinal) {
|
|
47
|
-
if (mod === null) return `${CSI}${arrowFinal}`;
|
|
48
|
-
return `${CSI}1;${mod}${arrowFinal}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const homeEndFinal = homeEndFinalChar(keyName);
|
|
52
|
-
if (homeEndFinal) {
|
|
53
|
-
if (mod === null) return `${CSI}${homeEndFinal}`;
|
|
54
|
-
return `${CSI}1;${mod}${homeEndFinal}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const tildeCode = tildeKeyCode(keyName);
|
|
58
|
-
if (tildeCode) {
|
|
59
|
-
if (mod === null) return `${CSI}${tildeCode}~`;
|
|
60
|
-
return `${CSI}${tildeCode};${mod}~`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const functionKey = functionKeySpec(keyName);
|
|
64
|
-
if (functionKey) {
|
|
65
|
-
if (mod === null) return functionKey.base;
|
|
66
|
-
return functionKey.modified(mod);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Ctrl/Alt modifiers for single characters.
|
|
70
|
-
if (parsed.key.length === 1) {
|
|
71
|
-
let outChar = parsed.key;
|
|
72
|
-
if (parsed.modifiers.shift && /^[a-z]$/i.test(outChar)) {
|
|
73
|
-
outChar = outChar.toUpperCase();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let encoded = outChar;
|
|
77
|
-
if (parsed.modifiers.ctrl && /^[a-z]$/i.test(outChar)) {
|
|
78
|
-
encoded = ctrlChar(outChar);
|
|
79
|
-
}
|
|
80
|
-
if (parsed.modifiers.alt) {
|
|
81
|
-
encoded = `\x1b${encoded}`;
|
|
82
|
-
}
|
|
83
|
-
return encoded;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
throw new Error(`Unsupported key: ${key}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
type KeyModifiers = { shift: boolean; alt: boolean; ctrl: boolean };
|
|
90
|
-
|
|
91
|
-
function parseKeySpec(spec: string): { modifiers: KeyModifiers; key: string } {
|
|
92
|
-
const tokens = spec
|
|
93
|
-
.trim()
|
|
94
|
-
.split(/[+-]/g)
|
|
95
|
-
.map((t) => t.trim())
|
|
96
|
-
.filter(Boolean);
|
|
97
|
-
|
|
98
|
-
const modifiers: KeyModifiers = { shift: false, alt: false, ctrl: false };
|
|
99
|
-
const keys: string[] = [];
|
|
100
|
-
|
|
101
|
-
for (const token of tokens) {
|
|
102
|
-
const lower = token.toLowerCase();
|
|
103
|
-
if (lower === "shift") {
|
|
104
|
-
modifiers.shift = true;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (lower === "alt" || lower === "meta") {
|
|
108
|
-
modifiers.alt = true;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (lower === "ctrl" || lower === "control") {
|
|
112
|
-
modifiers.ctrl = true;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
keys.push(token);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const key = keys.join("+").trim();
|
|
119
|
-
if (!key) throw new Error(`Unsupported key: ${spec}`);
|
|
120
|
-
return { modifiers, key };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function modifierParam(mods: KeyModifiers): number | null {
|
|
124
|
-
if (!mods.shift && !mods.alt && !mods.ctrl) return null;
|
|
125
|
-
return 1 + (mods.shift ? 1 : 0) + (mods.alt ? 2 : 0) + (mods.ctrl ? 4 : 0);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function arrowKeyFinal(key: string): "A" | "B" | "C" | "D" | null {
|
|
129
|
-
if (key === "up") return "A";
|
|
130
|
-
if (key === "down") return "B";
|
|
131
|
-
if (key === "right") return "C";
|
|
132
|
-
if (key === "left") return "D";
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function homeEndFinalChar(key: string): "H" | "F" | null {
|
|
137
|
-
if (key === "home") return "H";
|
|
138
|
-
if (key === "end") return "F";
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function tildeKeyCode(key: string): "5" | "6" | "2" | "3" | null {
|
|
143
|
-
if (key === "pageup" || key === "pgup") return "5";
|
|
144
|
-
if (key === "pagedown" || key === "pgdn") return "6";
|
|
145
|
-
if (key === "insert" || key === "ins") return "2";
|
|
146
|
-
if (key === "delete" || key === "del") return "3";
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function functionKeySpec(key: string): { base: string; modified: (mod: number) => string } | null {
|
|
151
|
-
const match = /^f(\d{1,2})$/.exec(key);
|
|
152
|
-
if (!match) return null;
|
|
153
|
-
|
|
154
|
-
const n = Number(match[1]);
|
|
155
|
-
if (!Number.isFinite(n) || n < 1 || n > 12) return null;
|
|
156
|
-
|
|
157
|
-
// F1..F4
|
|
158
|
-
if (n === 1) return { base: `${SS3}P`, modified: (m) => `${CSI}1;${m}P` };
|
|
159
|
-
if (n === 2) return { base: `${SS3}Q`, modified: (m) => `${CSI}1;${m}Q` };
|
|
160
|
-
if (n === 3) return { base: `${SS3}R`, modified: (m) => `${CSI}1;${m}R` };
|
|
161
|
-
if (n === 4) return { base: `${SS3}S`, modified: (m) => `${CSI}1;${m}S` };
|
|
162
|
-
|
|
163
|
-
const tildeCodes: Record<number, string> = {
|
|
164
|
-
5: "15",
|
|
165
|
-
6: "17",
|
|
166
|
-
7: "18",
|
|
167
|
-
8: "19",
|
|
168
|
-
9: "20",
|
|
169
|
-
10: "21",
|
|
170
|
-
11: "23",
|
|
171
|
-
12: "24",
|
|
172
|
-
};
|
|
173
|
-
const code = tildeCodes[n];
|
|
174
|
-
if (!code) return null;
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
base: `${CSI}${code}~`,
|
|
178
|
-
modified: (m) => `${CSI}${code};${m}~`,
|
|
179
|
-
};
|
|
180
|
-
}
|
package/src/terminal/mask.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
export type TextMaskRule = {
|
|
2
|
-
regex: string;
|
|
3
|
-
flags?: string;
|
|
4
|
-
replacement?: string;
|
|
5
|
-
preserveLength?: boolean;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type CompiledMaskRule = {
|
|
9
|
-
regex: RegExp;
|
|
10
|
-
replacement: string;
|
|
11
|
-
preserveLength: boolean;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function applyTextMaskRules(lines: string[], rules?: TextMaskRule[]): string[] {
|
|
15
|
-
if (!rules || rules.length === 0) return lines;
|
|
16
|
-
|
|
17
|
-
const compiled = compileMaskRules(rules);
|
|
18
|
-
if (compiled.length === 0) return lines;
|
|
19
|
-
|
|
20
|
-
return lines.map((line) => applyCompiledMaskRules(line, compiled));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function compileMaskRules(rules: TextMaskRule[]): CompiledMaskRule[] {
|
|
24
|
-
const compiled: CompiledMaskRule[] = [];
|
|
25
|
-
|
|
26
|
-
for (const rule of rules) {
|
|
27
|
-
if (!rule.regex.trim()) continue;
|
|
28
|
-
|
|
29
|
-
const preserveLength = rule.preserveLength ?? false;
|
|
30
|
-
const replacement = preserveLength
|
|
31
|
-
? firstChar(rule.replacement)
|
|
32
|
-
: (rule.replacement ?? "<masked>");
|
|
33
|
-
|
|
34
|
-
const flags = normalizeFlags(rule.flags);
|
|
35
|
-
let regex: RegExp;
|
|
36
|
-
try {
|
|
37
|
-
regex = new RegExp(rule.regex, flags);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`invalid mask rule regex=${JSON.stringify(rule.regex)} flags=${JSON.stringify(rule.flags ?? "")}: ${(error as Error).message}`,
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
compiled.push({ regex, replacement, preserveLength });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return compiled;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function normalizeFlags(flags?: string): string {
|
|
51
|
-
const value = flags?.trim() ? flags.trim() : "g";
|
|
52
|
-
const set = new Set(value.split(""));
|
|
53
|
-
set.add("g");
|
|
54
|
-
return [...set].join("");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function firstChar(value?: string): string {
|
|
58
|
-
const trimmed = value?.trim();
|
|
59
|
-
return trimmed && trimmed.length > 0 ? trimmed[0] : "█";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function applyCompiledMaskRules(line: string, rules: CompiledMaskRule[]): string {
|
|
63
|
-
let out = line;
|
|
64
|
-
for (const rule of rules) {
|
|
65
|
-
out = out.replace(rule.regex, (match) =>
|
|
66
|
-
rule.preserveLength ? rule.replacement.repeat(match.length) : rule.replacement,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|
package/src/terminal/mouse.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
const ESC = "\x1b";
|
|
2
|
-
|
|
3
|
-
export type MouseButton = "left" | "middle" | "right";
|
|
4
|
-
|
|
5
|
-
export type MouseAction = "down" | "up" | "move" | "click" | "scroll_up" | "scroll_down";
|
|
6
|
-
|
|
7
|
-
export type MouseModifiers = {
|
|
8
|
-
shift?: boolean;
|
|
9
|
-
alt?: boolean;
|
|
10
|
-
ctrl?: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type MouseEvent = {
|
|
14
|
-
action: MouseAction;
|
|
15
|
-
x: number;
|
|
16
|
-
y: number;
|
|
17
|
-
button?: MouseButton;
|
|
18
|
-
modifiers?: MouseModifiers;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function encodeSgrMouse(event: MouseEvent): string {
|
|
22
|
-
const x = clampInt(event.x, 1, 500);
|
|
23
|
-
const y = clampInt(event.y, 1, 300);
|
|
24
|
-
|
|
25
|
-
if (event.action === "click") {
|
|
26
|
-
const press = encodeSingleSgrMouse({ ...event, action: "down" }, x, y);
|
|
27
|
-
const release = encodeSingleSgrMouse({ ...event, action: "up" }, x, y);
|
|
28
|
-
return `${press}${release}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return encodeSingleSgrMouse(event, x, y);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function encodeSingleSgrMouse(event: MouseEvent, x: number, y: number): string {
|
|
35
|
-
const modifiers = event.modifiers;
|
|
36
|
-
const modifierBits =
|
|
37
|
-
(modifiers?.shift ? 4 : 0) + (modifiers?.alt ? 8 : 0) + (modifiers?.ctrl ? 16 : 0);
|
|
38
|
-
|
|
39
|
-
const buttonCode = buttonToCode(event.button ?? "left");
|
|
40
|
-
|
|
41
|
-
if (event.action === "down") {
|
|
42
|
-
return `${ESC}[<${buttonCode + modifierBits};${x};${y}M`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (event.action === "up") {
|
|
46
|
-
return `${ESC}[<${buttonCode + modifierBits};${x};${y}m`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (event.action === "scroll_up") {
|
|
50
|
-
return `${ESC}[<${64 + modifierBits};${x};${y}M`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (event.action === "scroll_down") {
|
|
54
|
-
return `${ESC}[<${65 + modifierBits};${x};${y}M`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// move
|
|
58
|
-
// 32 indicates motion. When no button is pressed, use "3".
|
|
59
|
-
const motionButton = event.button ? buttonCode : 3;
|
|
60
|
-
return `${ESC}[<${32 + motionButton + modifierBits};${x};${y}M`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function buttonToCode(button: MouseButton): number {
|
|
64
|
-
if (button === "left") return 0;
|
|
65
|
-
if (button === "middle") return 1;
|
|
66
|
-
return 2;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function clampInt(value: number, min: number, max: number): number {
|
|
70
|
-
if (!Number.isFinite(value)) return min;
|
|
71
|
-
const int = Math.trunc(value);
|
|
72
|
-
if (int < min) return min;
|
|
73
|
-
if (int > max) return max;
|
|
74
|
-
return int;
|
|
75
|
-
}
|
package/src/terminal/snapshot.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import type { Terminal } from "@xterm/headless";
|
|
2
|
-
|
|
3
|
-
import type { CellStyle } from "./style";
|
|
4
|
-
import { extractStyle, findMeaningfulEndCol, isDefaultStyle, styleKey } from "./style";
|
|
5
|
-
|
|
6
|
-
export type SnapshotScope = "visible" | "buffer";
|
|
7
|
-
|
|
8
|
-
export type TerminalSnapshotStyleRun = {
|
|
9
|
-
startCol: number;
|
|
10
|
-
endCol: number;
|
|
11
|
-
style: CellStyle;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type TerminalSnapshotGrid = {
|
|
15
|
-
cols: number;
|
|
16
|
-
rows: number;
|
|
17
|
-
bufferType: "normal" | "alternate";
|
|
18
|
-
cursorX: number;
|
|
19
|
-
cursorY: number;
|
|
20
|
-
viewportY: number;
|
|
21
|
-
lines: string[];
|
|
22
|
-
styleRuns?: TerminalSnapshotStyleRun[][];
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function snapshotVisibleLines(terminal: Terminal, trimRight: boolean): string[] {
|
|
26
|
-
const buffer = terminal.buffer.active;
|
|
27
|
-
const nullCell = buffer.getNullCell();
|
|
28
|
-
|
|
29
|
-
const startY = buffer.viewportY;
|
|
30
|
-
const lines: string[] = [];
|
|
31
|
-
|
|
32
|
-
for (let row = 0; row < terminal.rows; row += 1) {
|
|
33
|
-
const line = buffer.getLine(startY + row);
|
|
34
|
-
lines.push(snapshotPlainLine(line, terminal.cols, nullCell, trimRight));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return lines;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function snapshotVisibleStyleRuns(
|
|
41
|
-
terminal: Terminal,
|
|
42
|
-
trimRight: boolean,
|
|
43
|
-
): TerminalSnapshotStyleRun[][] {
|
|
44
|
-
const buffer = terminal.buffer.active;
|
|
45
|
-
const nullCell = buffer.getNullCell();
|
|
46
|
-
|
|
47
|
-
const startY = buffer.viewportY;
|
|
48
|
-
const out: TerminalSnapshotStyleRun[][] = [];
|
|
49
|
-
|
|
50
|
-
for (let row = 0; row < terminal.rows; row += 1) {
|
|
51
|
-
const line = buffer.getLine(startY + row);
|
|
52
|
-
out.push(snapshotStyleRuns(line, terminal.cols, nullCell, trimRight));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function snapshotBufferLines(terminal: Terminal, trimRight: boolean): string[] {
|
|
59
|
-
const buffer = terminal.buffer.active;
|
|
60
|
-
const lines: string[] = [];
|
|
61
|
-
for (let y = 0; y < buffer.length; y += 1) {
|
|
62
|
-
const line = buffer.getLine(y);
|
|
63
|
-
lines.push(line ? line.translateToString(trimRight, 0, terminal.cols) : "");
|
|
64
|
-
}
|
|
65
|
-
return lines;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function snapshotLines(
|
|
69
|
-
terminal: Terminal,
|
|
70
|
-
options?: { scope?: SnapshotScope; trimRight?: boolean },
|
|
71
|
-
): string[] {
|
|
72
|
-
const trimRight = options?.trimRight ?? true;
|
|
73
|
-
const scope = options?.scope ?? "visible";
|
|
74
|
-
return scope === "buffer"
|
|
75
|
-
? snapshotBufferLines(terminal, trimRight)
|
|
76
|
-
: snapshotVisibleLines(terminal, trimRight);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function snapshotGrid(
|
|
80
|
-
terminal: Terminal,
|
|
81
|
-
options?: { trimRight?: boolean; includeStyles?: boolean },
|
|
82
|
-
): TerminalSnapshotGrid {
|
|
83
|
-
const trimRight = options?.trimRight ?? true;
|
|
84
|
-
const includeStyles = options?.includeStyles ?? false;
|
|
85
|
-
|
|
86
|
-
const buffer = terminal.buffer.active;
|
|
87
|
-
const lines = snapshotVisibleLines(terminal, trimRight);
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
cols: terminal.cols,
|
|
91
|
-
rows: terminal.rows,
|
|
92
|
-
bufferType: buffer.type,
|
|
93
|
-
cursorX: buffer.cursorX,
|
|
94
|
-
cursorY: buffer.cursorY,
|
|
95
|
-
viewportY: buffer.viewportY,
|
|
96
|
-
lines,
|
|
97
|
-
styleRuns: includeStyles ? snapshotVisibleStyleRuns(terminal, trimRight) : undefined,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function snapshotPlainLine(
|
|
102
|
-
line: ReturnType<Terminal["buffer"]["active"]["getLine"]>,
|
|
103
|
-
cols: number,
|
|
104
|
-
nullCell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
|
|
105
|
-
trimRight: boolean,
|
|
106
|
-
): string {
|
|
107
|
-
let endCol = cols;
|
|
108
|
-
if (trimRight) {
|
|
109
|
-
endCol = findMeaningfulEndCol(line, cols, nullCell);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let out = "";
|
|
113
|
-
for (let x = 0; x < endCol; x += 1) {
|
|
114
|
-
const cell = line?.getCell(x, nullCell);
|
|
115
|
-
if (!cell) {
|
|
116
|
-
out += " ";
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const width = cell.getWidth();
|
|
121
|
-
if (width === 0) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
out += cell.getChars() || " ";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function snapshotStyleRuns(
|
|
132
|
-
line: ReturnType<Terminal["buffer"]["active"]["getLine"]>,
|
|
133
|
-
cols: number,
|
|
134
|
-
nullCell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
|
|
135
|
-
trimRight: boolean,
|
|
136
|
-
): TerminalSnapshotStyleRun[] {
|
|
137
|
-
let endCol = cols;
|
|
138
|
-
if (trimRight) {
|
|
139
|
-
endCol = findMeaningfulEndCol(line, cols, nullCell);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const out: TerminalSnapshotStyleRun[] = [];
|
|
143
|
-
|
|
144
|
-
let currentKey: string | null = null;
|
|
145
|
-
let currentRun: { startCol: number; endCol: number; style: CellStyle } | null = null;
|
|
146
|
-
|
|
147
|
-
for (let x = 0; x < endCol; x += 1) {
|
|
148
|
-
const cell = line?.getCell(x, nullCell);
|
|
149
|
-
if (!cell) {
|
|
150
|
-
if (currentRun) {
|
|
151
|
-
out.push(currentRun);
|
|
152
|
-
currentRun = null;
|
|
153
|
-
currentKey = null;
|
|
154
|
-
}
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const width = cell.getWidth();
|
|
159
|
-
if (width === 0) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const style = extractStyle(cell);
|
|
164
|
-
if (isDefaultStyle(style)) {
|
|
165
|
-
if (currentRun) {
|
|
166
|
-
out.push(currentRun);
|
|
167
|
-
currentRun = null;
|
|
168
|
-
currentKey = null;
|
|
169
|
-
}
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const key = styleKey(style);
|
|
174
|
-
if (currentRun && key === currentKey) {
|
|
175
|
-
currentRun.endCol = x + width;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (currentRun) {
|
|
180
|
-
out.push(currentRun);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
currentKey = key;
|
|
184
|
-
currentRun = {
|
|
185
|
-
startCol: x,
|
|
186
|
-
endCol: x + width,
|
|
187
|
-
style,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (currentRun) {
|
|
192
|
-
out.push(currentRun);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return out;
|
|
196
|
-
}
|