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/style.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import type { Terminal } from "@xterm/headless";
|
|
2
|
-
|
|
3
|
-
export type Color =
|
|
4
|
-
| { mode: "default" }
|
|
5
|
-
| { mode: "palette"; value: number }
|
|
6
|
-
| { mode: "rgb"; value: number };
|
|
7
|
-
|
|
8
|
-
export type CellStyle = {
|
|
9
|
-
fg: Color;
|
|
10
|
-
bg: Color;
|
|
11
|
-
bold: boolean;
|
|
12
|
-
dim: boolean;
|
|
13
|
-
italic: boolean;
|
|
14
|
-
underline: boolean;
|
|
15
|
-
inverse: boolean;
|
|
16
|
-
strikethrough: boolean;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const DEFAULT_STYLE: CellStyle = {
|
|
20
|
-
fg: { mode: "default" },
|
|
21
|
-
bg: { mode: "default" },
|
|
22
|
-
bold: false,
|
|
23
|
-
dim: false,
|
|
24
|
-
italic: false,
|
|
25
|
-
underline: false,
|
|
26
|
-
inverse: false,
|
|
27
|
-
strikethrough: false,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function extractStyle(
|
|
31
|
-
cell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
|
|
32
|
-
): CellStyle {
|
|
33
|
-
const fg = extractColor(
|
|
34
|
-
cell.isFgDefault(),
|
|
35
|
-
cell.isFgPalette(),
|
|
36
|
-
cell.isFgRGB(),
|
|
37
|
-
cell.getFgColor(),
|
|
38
|
-
);
|
|
39
|
-
const bg = extractColor(
|
|
40
|
-
cell.isBgDefault(),
|
|
41
|
-
cell.isBgPalette(),
|
|
42
|
-
cell.isBgRGB(),
|
|
43
|
-
cell.getBgColor(),
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const style: CellStyle = {
|
|
47
|
-
fg,
|
|
48
|
-
bg,
|
|
49
|
-
bold: cell.isBold() !== 0,
|
|
50
|
-
dim: cell.isDim() !== 0,
|
|
51
|
-
italic: cell.isItalic() !== 0,
|
|
52
|
-
underline: cell.isUnderline() !== 0,
|
|
53
|
-
inverse: cell.isInverse() !== 0,
|
|
54
|
-
strikethrough: cell.isStrikethrough() !== 0,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
return isDefaultStyle(style) ? DEFAULT_STYLE : style;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function isDefaultStyle(style: CellStyle): boolean {
|
|
61
|
-
return (
|
|
62
|
-
style.fg.mode === "default" &&
|
|
63
|
-
style.bg.mode === "default" &&
|
|
64
|
-
!style.bold &&
|
|
65
|
-
!style.dim &&
|
|
66
|
-
!style.italic &&
|
|
67
|
-
!style.underline &&
|
|
68
|
-
!style.inverse &&
|
|
69
|
-
!style.strikethrough
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function styleKey(style: CellStyle): string {
|
|
74
|
-
const fg = style.fg.mode === "default" ? "d" : `${style.fg.mode}:${style.fg.value}`;
|
|
75
|
-
const bg = style.bg.mode === "default" ? "d" : `${style.bg.mode}:${style.bg.value}`;
|
|
76
|
-
return [
|
|
77
|
-
fg,
|
|
78
|
-
bg,
|
|
79
|
-
style.bold ? "b" : "",
|
|
80
|
-
style.dim ? "d" : "",
|
|
81
|
-
style.italic ? "i" : "",
|
|
82
|
-
style.underline ? "u" : "",
|
|
83
|
-
style.inverse ? "r" : "",
|
|
84
|
-
style.strikethrough ? "s" : "",
|
|
85
|
-
].join("|");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function findMeaningfulEndCol(
|
|
89
|
-
line: ReturnType<Terminal["buffer"]["active"]["getLine"]>,
|
|
90
|
-
cols: number,
|
|
91
|
-
nullCell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
|
|
92
|
-
): number {
|
|
93
|
-
if (!line) return 0;
|
|
94
|
-
|
|
95
|
-
for (let x = cols - 1; x >= 0; x -= 1) {
|
|
96
|
-
const cell = line.getCell(x, nullCell);
|
|
97
|
-
if (!cell) continue;
|
|
98
|
-
if (cell.getWidth() === 0) continue;
|
|
99
|
-
|
|
100
|
-
const chars = cell.getChars();
|
|
101
|
-
const style = extractStyle(cell);
|
|
102
|
-
const meaningful = (chars !== "" && chars !== " ") || !isDefaultStyle(style);
|
|
103
|
-
if (meaningful) {
|
|
104
|
-
return x + 1;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return 0;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function extractColor(
|
|
112
|
-
isDefault: boolean,
|
|
113
|
-
isPalette: boolean,
|
|
114
|
-
isRgb: boolean,
|
|
115
|
-
value: number,
|
|
116
|
-
): Color {
|
|
117
|
-
if (isDefault) return { mode: "default" };
|
|
118
|
-
if (isRgb) return { mode: "rgb", value };
|
|
119
|
-
if (isPalette) return { mode: "palette", value };
|
|
120
|
-
return { mode: "default" };
|
|
121
|
-
}
|
package/src/terminal/view.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { SnapshotScope } from "./snapshot";
|
|
2
|
-
|
|
3
|
-
export type TerminalMeta = {
|
|
4
|
-
cols: number;
|
|
5
|
-
rows: number;
|
|
6
|
-
bufferType: "normal" | "alternate";
|
|
7
|
-
viewportY: number;
|
|
8
|
-
baseY: number;
|
|
9
|
-
length: number;
|
|
10
|
-
cursorX: number;
|
|
11
|
-
cursorY: number;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type SnapshotViewOptions = {
|
|
15
|
-
sessionId: string;
|
|
16
|
-
scope: SnapshotScope;
|
|
17
|
-
hash: string;
|
|
18
|
-
lines: string[];
|
|
19
|
-
meta: TerminalMeta;
|
|
20
|
-
lineNumbers?: boolean;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export function formatSnapshotView(options: SnapshotViewOptions): string {
|
|
24
|
-
const lineNumbers = options.lineNumbers ?? true;
|
|
25
|
-
|
|
26
|
-
const cursorAbsY = options.meta.baseY + options.meta.cursorY;
|
|
27
|
-
const cursorViewportRow = cursorAbsY - options.meta.viewportY;
|
|
28
|
-
const cursorViewportCol = options.meta.cursorX;
|
|
29
|
-
|
|
30
|
-
const header = [
|
|
31
|
-
`session=${options.sessionId}`,
|
|
32
|
-
`scope=${options.scope}`,
|
|
33
|
-
`size=${options.meta.cols}x${options.meta.rows}`,
|
|
34
|
-
`buffer=${options.meta.bufferType}`,
|
|
35
|
-
`cursor=${cursorViewportCol + 1},${cursorViewportRow + 1}`,
|
|
36
|
-
`hash=${options.hash}`,
|
|
37
|
-
].join(" ");
|
|
38
|
-
|
|
39
|
-
const digits = Math.max(2, String(options.lines.length).length);
|
|
40
|
-
const out: string[] = [header];
|
|
41
|
-
|
|
42
|
-
for (let i = 0; i < options.lines.length; i += 1) {
|
|
43
|
-
const n = i + 1;
|
|
44
|
-
const prefix = lineNumbers ? `${String(n).padStart(digits, "0")}│ ` : "";
|
|
45
|
-
out.push(`${prefix}${options.lines[i] ?? ""}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return out.join("\n");
|
|
49
|
-
}
|
package/src/trace/asciicast.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export type AsciicastHeader = {
|
|
2
|
-
version: 2;
|
|
3
|
-
width: number;
|
|
4
|
-
height: number;
|
|
5
|
-
timestamp?: number;
|
|
6
|
-
env?: Record<string, string>;
|
|
7
|
-
title?: string;
|
|
8
|
-
command?: string;
|
|
9
|
-
term?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type AsciicastEvent = [timeSeconds: number, type: "o" | "i" | "m" | "r", data: string];
|
|
13
|
-
|
|
14
|
-
export function encodeAsciicast(header: AsciicastHeader, events: AsciicastEvent[]): string {
|
|
15
|
-
const lines: string[] = [JSON.stringify(header)];
|
|
16
|
-
for (const event of events) {
|
|
17
|
-
lines.push(JSON.stringify(event));
|
|
18
|
-
}
|
|
19
|
-
return `${lines.join("\n")}\n`;
|
|
20
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
|
|
5
|
-
export type EnsureAsciinemaPlayerAssetsResult = {
|
|
6
|
-
ok: boolean;
|
|
7
|
-
copied: boolean;
|
|
8
|
-
cssPath: string;
|
|
9
|
-
jsPath: string;
|
|
10
|
-
error?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function ensureAsciinemaPlayerAssets(reportPath: string): EnsureAsciinemaPlayerAssetsResult {
|
|
14
|
-
const dir = dirname(reportPath);
|
|
15
|
-
const cssPath = join(dir, "asciinema-player.css");
|
|
16
|
-
const jsPath = join(dir, "asciinema-player.min.js");
|
|
17
|
-
|
|
18
|
-
const cssExists = existsSync(cssPath);
|
|
19
|
-
const jsExists = existsSync(jsPath);
|
|
20
|
-
if (cssExists && jsExists) {
|
|
21
|
-
return { ok: true, copied: false, cssPath, jsPath };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
mkdirSync(dir, { recursive: true });
|
|
26
|
-
|
|
27
|
-
const require = createRequire(import.meta.url);
|
|
28
|
-
const resolvedCss = require.resolve("asciinema-player/dist/bundle/asciinema-player.css");
|
|
29
|
-
const resolvedJs = require.resolve("asciinema-player/dist/bundle/asciinema-player.min.js");
|
|
30
|
-
|
|
31
|
-
if (!cssExists) copyFileSync(resolvedCss, cssPath);
|
|
32
|
-
if (!jsExists) copyFileSync(resolvedJs, jsPath);
|
|
33
|
-
|
|
34
|
-
return { ok: true, copied: true, cssPath, jsPath };
|
|
35
|
-
} catch (error) {
|
|
36
|
-
return {
|
|
37
|
-
ok: false,
|
|
38
|
-
copied: false,
|
|
39
|
-
cssPath,
|
|
40
|
-
jsPath,
|
|
41
|
-
error: (error as Error).message,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}
|
package/src/trace/cast_to_txt.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
function parseArgs(argv: string[]): { inPath: string; outPath?: string; stripAnsi: boolean } {
|
|
2
|
-
const out: Partial<{ inPath: string; outPath?: string; stripAnsi: boolean }> = {
|
|
3
|
-
stripAnsi: true,
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
7
|
-
const arg = argv[i];
|
|
8
|
-
const next = argv[i + 1];
|
|
9
|
-
|
|
10
|
-
if (!out.inPath && arg && !arg.startsWith("-")) {
|
|
11
|
-
out.inPath = arg;
|
|
12
|
-
continue;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (arg === "--in" && next) {
|
|
16
|
-
out.inPath = next;
|
|
17
|
-
i += 1;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (arg === "--out" && next) {
|
|
22
|
-
out.outPath = next;
|
|
23
|
-
i += 1;
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (arg === "--strip-ansi") {
|
|
28
|
-
out.stripAnsi = true;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (arg === "--keep-ansi") {
|
|
33
|
-
out.stripAnsi = false;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
throw new Error(`unknown arg: ${arg ?? ""}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!out.inPath) throw new Error("missing <castPath> (or --in <path>)");
|
|
41
|
-
return out as { inPath: string; outPath?: string; stripAnsi: boolean };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type AsciicastHeader = {
|
|
45
|
-
version?: number;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
type AsciicastEvent = [timeSeconds: number, type: "o" | "i" | "m" | "r", data: string];
|
|
49
|
-
|
|
50
|
-
function stripAnsi(text: string): string {
|
|
51
|
-
// CSI: ESC [ ... @-~ (includes SGR, cursor, erase, etc)
|
|
52
|
-
// eslint-disable-next-line no-control-regex
|
|
53
|
-
const csi = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
54
|
-
// OSC: ESC ] ... BEL or ST (ESC \)
|
|
55
|
-
// eslint-disable-next-line no-control-regex
|
|
56
|
-
const osc = /\u001b\][\s\S]*?(?:\u0007|\u001b\\)/g;
|
|
57
|
-
// Two-byte escapes: SS3 (ESC O) + final
|
|
58
|
-
// eslint-disable-next-line no-control-regex
|
|
59
|
-
const ss3 = /\u001bO[@-~]/g;
|
|
60
|
-
|
|
61
|
-
return text.replace(osc, "").replace(csi, "").replace(ss3, "");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function run(args: { inPath: string; outPath?: string; stripAnsi: boolean }): Promise<void> {
|
|
65
|
-
const input = await Bun.file(args.inPath).text();
|
|
66
|
-
const lines = input.split("\n").filter((l) => l.length > 0);
|
|
67
|
-
if (lines.length === 0) throw new Error("empty cast");
|
|
68
|
-
|
|
69
|
-
let header: AsciicastHeader | null = null;
|
|
70
|
-
try {
|
|
71
|
-
header = JSON.parse(lines[0] ?? "") as AsciicastHeader;
|
|
72
|
-
} catch {
|
|
73
|
-
throw new Error("invalid cast header JSON");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (header?.version !== 2) {
|
|
77
|
-
throw new Error(`unsupported asciicast version: ${header?.version ?? "unknown"}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let out = "";
|
|
81
|
-
for (const line of lines.slice(1)) {
|
|
82
|
-
let parsed: unknown;
|
|
83
|
-
try {
|
|
84
|
-
parsed = JSON.parse(line);
|
|
85
|
-
} catch {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!Array.isArray(parsed) || parsed.length < 3) continue;
|
|
90
|
-
const event = parsed as Partial<AsciicastEvent>;
|
|
91
|
-
if (event[1] !== "o") continue;
|
|
92
|
-
if (typeof event[2] !== "string") continue;
|
|
93
|
-
|
|
94
|
-
out += event[2];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const rendered = args.stripAnsi ? stripAnsi(out) : out;
|
|
98
|
-
|
|
99
|
-
if (args.outPath) {
|
|
100
|
-
await Bun.write(args.outPath, rendered);
|
|
101
|
-
} else {
|
|
102
|
-
process.stdout.write(rendered);
|
|
103
|
-
if (!rendered.endsWith("\n")) process.stdout.write("\n");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (import.meta.main) {
|
|
108
|
-
try {
|
|
109
|
-
const args = parseArgs(process.argv.slice(2));
|
|
110
|
-
await run(args);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
// eslint-disable-next-line no-console
|
|
113
|
-
console.error((error as Error).message);
|
|
114
|
-
process.exitCode = 1;
|
|
115
|
-
}
|
|
116
|
-
}
|
package/src/trace/recorder.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import type { AsciicastEvent, AsciicastHeader } from "./asciicast";
|
|
2
|
-
import { encodeAsciicast } from "./asciicast";
|
|
3
|
-
|
|
4
|
-
export type TraceRecorderOptions = {
|
|
5
|
-
maxEvents?: number;
|
|
6
|
-
maxDataChars?: number;
|
|
7
|
-
mergeOutput?: boolean;
|
|
8
|
-
timePrecisionMs?: number;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type TraceSnapshot = {
|
|
12
|
-
header: AsciicastHeader;
|
|
13
|
-
events: AsciicastEvent[];
|
|
14
|
-
cast: string;
|
|
15
|
-
droppedEvents: number;
|
|
16
|
-
droppedDataChars: number;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const DEFAULT_MAX_EVENTS = 50_000;
|
|
20
|
-
const DEFAULT_MAX_DATA_CHARS = 5_000_000;
|
|
21
|
-
const DEFAULT_TIME_PRECISION_MS = 1;
|
|
22
|
-
|
|
23
|
-
export class TraceRecorder {
|
|
24
|
-
private readonly header: AsciicastHeader;
|
|
25
|
-
private readonly startedAtMs: number;
|
|
26
|
-
private readonly maxEvents: number;
|
|
27
|
-
private readonly maxDataChars: number;
|
|
28
|
-
private readonly mergeOutput: boolean;
|
|
29
|
-
private readonly timePrecisionMs: number;
|
|
30
|
-
|
|
31
|
-
private readonly events: AsciicastEvent[] = [];
|
|
32
|
-
private dataChars = 0;
|
|
33
|
-
private droppedEvents = 0;
|
|
34
|
-
private droppedDataChars = 0;
|
|
35
|
-
|
|
36
|
-
constructor(header: AsciicastHeader, options?: TraceRecorderOptions) {
|
|
37
|
-
this.header = header;
|
|
38
|
-
this.startedAtMs = performance.now();
|
|
39
|
-
this.maxEvents = Math.max(1, Math.trunc(options?.maxEvents ?? DEFAULT_MAX_EVENTS));
|
|
40
|
-
this.maxDataChars = Math.max(1, Math.trunc(options?.maxDataChars ?? DEFAULT_MAX_DATA_CHARS));
|
|
41
|
-
this.mergeOutput = options?.mergeOutput ?? true;
|
|
42
|
-
this.timePrecisionMs = Math.max(
|
|
43
|
-
1,
|
|
44
|
-
Math.trunc(options?.timePrecisionMs ?? DEFAULT_TIME_PRECISION_MS),
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
recordOutput(data: string): void {
|
|
49
|
-
this.addEvent("o", data);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
recordInput(data: string): void {
|
|
53
|
-
this.addEvent("i", data);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
recordResize(cols: number, rows: number): void {
|
|
57
|
-
this.addEvent("r", `${cols}x${rows}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
mark(label?: string): void {
|
|
61
|
-
this.addEvent("m", label ?? "");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
snapshot(options?: { tailEvents?: number }): TraceSnapshot {
|
|
65
|
-
const tailEvents = options?.tailEvents;
|
|
66
|
-
const events = tailEvents
|
|
67
|
-
? this.events.slice(-Math.max(0, Math.trunc(tailEvents)))
|
|
68
|
-
: [...this.events];
|
|
69
|
-
return {
|
|
70
|
-
header: this.header,
|
|
71
|
-
events,
|
|
72
|
-
cast: encodeAsciicast(this.header, events),
|
|
73
|
-
droppedEvents: this.droppedEvents,
|
|
74
|
-
droppedDataChars: this.droppedDataChars,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private addEvent(type: AsciicastEvent[1], data: string): void {
|
|
79
|
-
const timeSeconds = this.nowSeconds();
|
|
80
|
-
|
|
81
|
-
const last = this.events.at(-1);
|
|
82
|
-
if (this.mergeOutput && type === "o" && last && last[1] === "o" && last[0] === timeSeconds) {
|
|
83
|
-
last[2] += data;
|
|
84
|
-
this.dataChars += data.length;
|
|
85
|
-
this.trim();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this.events.push([timeSeconds, type, data]);
|
|
90
|
-
this.dataChars += data.length;
|
|
91
|
-
this.trim();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private nowSeconds(): number {
|
|
95
|
-
const elapsedMs = performance.now() - this.startedAtMs;
|
|
96
|
-
const quantized = Math.round(elapsedMs / this.timePrecisionMs) * this.timePrecisionMs;
|
|
97
|
-
return quantized / 1000;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private trim(): void {
|
|
101
|
-
while (this.events.length > this.maxEvents || this.dataChars > this.maxDataChars) {
|
|
102
|
-
const removed = this.events.shift();
|
|
103
|
-
if (!removed) break;
|
|
104
|
-
const chars = removed[2].length;
|
|
105
|
-
this.dataChars -= chars;
|
|
106
|
-
this.droppedEvents += 1;
|
|
107
|
-
this.droppedDataChars += chars;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|