tui-cap 0.1.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/LICENSE +21 -0
- package/README.md +213 -0
- package/dist/anim.js +104 -0
- package/dist/animation.js +96 -0
- package/dist/asciinema.js +125 -0
- package/dist/cli.js +648 -0
- package/dist/edits.js +527 -0
- package/dist/fonts.js +87 -0
- package/dist/input.js +136 -0
- package/dist/meta.js +58 -0
- package/dist/palette.js +111 -0
- package/dist/parse.js +659 -0
- package/dist/paths.js +100 -0
- package/dist/record-pty.js +148 -0
- package/dist/record.js +100 -0
- package/dist/server.js +978 -0
- package/dist/svg.js +767 -0
- package/dist/timing.js +112 -0
- package/dist/types.js +2 -0
- package/dist/version.js +232 -0
- package/dist/web/app.js +3312 -0
- package/dist/web/fonts/MonaSansMonoVF-wght.woff2 +0 -0
- package/dist/web/fonts/MonaSansVF-wdth-wght-opsz.woff2 +0 -0
- package/dist/web/fonts/README.md +13 -0
- package/dist/web/index.html +382 -0
- package/dist/web/logo.svg +11 -0
- package/dist/web/styles.css +925 -0
- package/dist/web/timing-model.js +115 -0
- package/dist/web/vendor/mp4-muxer.LICENSE +21 -0
- package/dist/web/vendor/mp4-muxer.js +1885 -0
- package/package.json +61 -0
package/dist/input.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.InputTracker = exports.INPUT_SCHEMA_VERSION = void 0;
|
|
7
|
+
exports.inputPathFor = inputPathFor;
|
|
8
|
+
exports.countPrintable = countPrintable;
|
|
9
|
+
exports.writeInput = writeInput;
|
|
10
|
+
exports.readInput = readInput;
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
exports.INPUT_SCHEMA_VERSION = 1;
|
|
14
|
+
/** Sibling sidecar path for a capture file: `foo.ans` -> `foo.input.json`. */
|
|
15
|
+
function inputPathFor(captureFile) {
|
|
16
|
+
const dir = node_path_1.default.dirname(captureFile);
|
|
17
|
+
const base = node_path_1.default.basename(captureFile, node_path_1.default.extname(captureFile));
|
|
18
|
+
return node_path_1.default.join(dir, `${base}.input.json`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Count the printable characters in an input burst, ignoring escape sequences
|
|
22
|
+
* and control bytes so only real text insertions are tallied.
|
|
23
|
+
*
|
|
24
|
+
* Terminal stdin carries far more than glyphs: arrow keys and function keys
|
|
25
|
+
* arrive as CSI/SS3 escape sequences (`ESC [ … final`, `ESC O x`), Enter is a
|
|
26
|
+
* carriage return, Backspace/Delete are control bytes, and a paste is wrapped in
|
|
27
|
+
* bracketed-paste markers (`ESC [ 200 ~` … `ESC [ 201 ~`). We skip every escape
|
|
28
|
+
* sequence and C0 control (and DEL), counting only Unicode scalar values at or
|
|
29
|
+
* above U+0020 — which includes the space bar and the printable contents of a
|
|
30
|
+
* paste, so a paste registers as typing too.
|
|
31
|
+
*/
|
|
32
|
+
function countPrintable(input) {
|
|
33
|
+
const s = typeof input === 'string' ? input : input.toString('utf8');
|
|
34
|
+
let n = 0;
|
|
35
|
+
let i = 0;
|
|
36
|
+
while (i < s.length) {
|
|
37
|
+
const code = s.codePointAt(i);
|
|
38
|
+
const len = code > 0xffff ? 2 : 1; // surrogate pair vs BMP
|
|
39
|
+
if (code === 0x1b) {
|
|
40
|
+
// Escape sequence — consume it whole so its payload isn't miscounted.
|
|
41
|
+
i++;
|
|
42
|
+
if (i < s.length && s[i] === '[') {
|
|
43
|
+
// CSI: run until a final byte in 0x40–0x7e (covers arrows, Home/End,
|
|
44
|
+
// and the 200~/201~ bracketed-paste markers).
|
|
45
|
+
i++;
|
|
46
|
+
while (i < s.length) {
|
|
47
|
+
const c = s.charCodeAt(i);
|
|
48
|
+
i++;
|
|
49
|
+
if (c >= 0x40 && c <= 0x7e)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (i < s.length && s[i] === 'O') {
|
|
54
|
+
i += 2; // SS3: ESC O x (e.g. F1–F4)
|
|
55
|
+
}
|
|
56
|
+
else if (i < s.length) {
|
|
57
|
+
i++; // ESC + one byte (Alt-modified key)
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (code < 0x20 || code === 0x7f) {
|
|
62
|
+
i += len; // C0 control (Tab, Enter, Backspace…) or DEL — not a glyph
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
n++;
|
|
66
|
+
i += len;
|
|
67
|
+
}
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Accumulates keystroke bursts while a capture streams in. Construct it with the
|
|
72
|
+
* SAME start time as the recording's {@link TimingTracker} so event times and
|
|
73
|
+
* frame appearance times share a clock. Call {@link mark} with each chunk of
|
|
74
|
+
* user input as it's forwarded to the child; bursts with no printable text are
|
|
75
|
+
* ignored. {@link build} returns the serializable sidecar.
|
|
76
|
+
*/
|
|
77
|
+
class InputTracker {
|
|
78
|
+
events = [];
|
|
79
|
+
startMs;
|
|
80
|
+
constructor(startMs = Date.now()) {
|
|
81
|
+
this.startMs = startMs;
|
|
82
|
+
}
|
|
83
|
+
/** Record an input burst sent at time `now`; no-op if it's non-printable. */
|
|
84
|
+
mark(input, now = Date.now()) {
|
|
85
|
+
const count = countPrintable(input);
|
|
86
|
+
if (count <= 0)
|
|
87
|
+
return;
|
|
88
|
+
this.events.push([Math.max(0, now - this.startMs), count]);
|
|
89
|
+
}
|
|
90
|
+
/** Number of printable-burst events recorded so far. */
|
|
91
|
+
get length() {
|
|
92
|
+
return this.events.length;
|
|
93
|
+
}
|
|
94
|
+
build() {
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: exports.INPUT_SCHEMA_VERSION,
|
|
97
|
+
startedAt: new Date(this.startMs).toISOString(),
|
|
98
|
+
events: this.events,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.InputTracker = InputTracker;
|
|
103
|
+
/** Write the input sidecar next to `captureFile`. Returns the sidecar path. */
|
|
104
|
+
async function writeInput(captureFile, input) {
|
|
105
|
+
const out = inputPathFor(captureFile);
|
|
106
|
+
await (0, promises_1.writeFile)(out, `${JSON.stringify(input)}\n`, 'utf8');
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Read a capture's input sidecar, or null if it's missing or unusable. Never
|
|
111
|
+
* throws — a missing/garbled sidecar simply means "no keystroke data", so the
|
|
112
|
+
* typing features quietly no-op for that recording.
|
|
113
|
+
*/
|
|
114
|
+
async function readInput(captureFile) {
|
|
115
|
+
try {
|
|
116
|
+
const raw = await (0, promises_1.readFile)(inputPathFor(captureFile), 'utf8');
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
if (!Array.isArray(parsed.events))
|
|
119
|
+
return null;
|
|
120
|
+
const events = parsed.events.filter((e) => Array.isArray(e) &&
|
|
121
|
+
e.length === 2 &&
|
|
122
|
+
Number.isFinite(e[0]) &&
|
|
123
|
+
Number.isFinite(e[1]) &&
|
|
124
|
+
e[1] > 0);
|
|
125
|
+
if (events.length === 0)
|
|
126
|
+
return null;
|
|
127
|
+
return {
|
|
128
|
+
schemaVersion: parsed.schemaVersion ?? exports.INPUT_SCHEMA_VERSION,
|
|
129
|
+
startedAt: parsed.startedAt ?? '',
|
|
130
|
+
events,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dist/meta.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.META_SCHEMA_VERSION = void 0;
|
|
7
|
+
exports.metaPathFor = metaPathFor;
|
|
8
|
+
exports.writeMeta = writeMeta;
|
|
9
|
+
exports.readMeta = readMeta;
|
|
10
|
+
exports.terminalSize = terminalSize;
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
exports.META_SCHEMA_VERSION = 1;
|
|
14
|
+
/** Sibling sidecar path for a capture file: `foo.ans` -> `foo.meta.json`. */
|
|
15
|
+
function metaPathFor(captureFile) {
|
|
16
|
+
const dir = node_path_1.default.dirname(captureFile);
|
|
17
|
+
const base = node_path_1.default.basename(captureFile, node_path_1.default.extname(captureFile));
|
|
18
|
+
return node_path_1.default.join(dir, `${base}.meta.json`);
|
|
19
|
+
}
|
|
20
|
+
/** Write the sidecar next to `captureFile`. Returns the sidecar path. */
|
|
21
|
+
async function writeMeta(captureFile, meta) {
|
|
22
|
+
const out = metaPathFor(captureFile);
|
|
23
|
+
await (0, promises_1.writeFile)(out, `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read a capture's sidecar, or null if it's missing or unusable. Never throws —
|
|
28
|
+
* a missing/garbled sidecar simply means "fall back to defaults".
|
|
29
|
+
*/
|
|
30
|
+
async function readMeta(captureFile) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await (0, promises_1.readFile)(metaPathFor(captureFile), 'utf8');
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (typeof parsed.cols === 'number' &&
|
|
35
|
+
Number.isFinite(parsed.cols) &&
|
|
36
|
+
typeof parsed.rows === 'number' &&
|
|
37
|
+
Number.isFinite(parsed.rows)) {
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: parsed.schemaVersion ?? exports.META_SCHEMA_VERSION,
|
|
40
|
+
cols: parsed.cols,
|
|
41
|
+
rows: parsed.rows,
|
|
42
|
+
capturedAt: parsed.capturedAt ?? '',
|
|
43
|
+
command: parsed.command,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Current terminal size, with a sensible fallback when stdout isn't a TTY. */
|
|
53
|
+
function terminalSize(fallbackCols = 120, fallbackRows = 50) {
|
|
54
|
+
return {
|
|
55
|
+
cols: process.stdout.columns ?? fallbackCols,
|
|
56
|
+
rows: process.stdout.rows ?? fallbackRows,
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/palette.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.THEMES = exports.ANSI_16 = exports.ANSI_PALETTES = void 0;
|
|
4
|
+
exports.packedRgbToHex = packedRgbToHex;
|
|
5
|
+
exports.paletteToHex = paletteToHex;
|
|
6
|
+
/**
|
|
7
|
+
* The 16 base ANSI colors per theme, mapping ANSI palette indices 0–15 to
|
|
8
|
+
* GitHub's brand terminal hues so captured output reproduces brand-correct.
|
|
9
|
+
*
|
|
10
|
+
* Source of truth: @primer/primitives v11.9.0, the `--ansi-*` functional tokens
|
|
11
|
+
* (`dist/css/functional/themes/{dark,light}.css`). Standard ANSI-16 order:
|
|
12
|
+
* black, red, green, yellow, blue, magenta, cyan, white, then the 8 bright
|
|
13
|
+
* variants. Values are copied verbatim from those tokens — do not eyeball.
|
|
14
|
+
*
|
|
15
|
+
* Provenance note (verified against github/copilot-agent-runtime): the Copilot
|
|
16
|
+
* CLI itself does NOT hardcode GitHub-brand ANSI hexes. It queries the live
|
|
17
|
+
* terminal for its palette (OSC 4/10/11) and emits 24-bit truecolor built from
|
|
18
|
+
* that, falling back to the Windows-Terminal "Campbell" scheme only when the
|
|
19
|
+
* query fails. Real Copilot captures are therefore 100% truecolor and round-trip
|
|
20
|
+
* exactly through `packedRgbToHex` — this ANSI-16 table is the brand-correct
|
|
21
|
+
* fallback for streams that DO emit ANSI-16/256 (other CLIs, no-truecolor
|
|
22
|
+
* terminals). We deliberately choose the Primer brand palette here, not Campbell,
|
|
23
|
+
* because this is a GitHub brand tool.
|
|
24
|
+
*/
|
|
25
|
+
exports.ANSI_PALETTES = {
|
|
26
|
+
dark: [
|
|
27
|
+
'#2f3742', // 0 black
|
|
28
|
+
'#ff7b72', // 1 red
|
|
29
|
+
'#3fb950', // 2 green
|
|
30
|
+
'#d29922', // 3 yellow
|
|
31
|
+
'#58a6ff', // 4 blue
|
|
32
|
+
'#be8fff', // 5 magenta
|
|
33
|
+
'#39c5cf', // 6 cyan
|
|
34
|
+
'#f0f6fc', // 7 white
|
|
35
|
+
'#656c76', // 8 bright black
|
|
36
|
+
'#ffa198', // 9 bright red
|
|
37
|
+
'#56d364', // 10 bright green
|
|
38
|
+
'#e3b341', // 11 bright yellow
|
|
39
|
+
'#79c0ff', // 12 bright blue
|
|
40
|
+
'#d2a8ff', // 13 bright magenta
|
|
41
|
+
'#56d4dd', // 14 bright cyan
|
|
42
|
+
'#ffffff', // 15 bright white
|
|
43
|
+
],
|
|
44
|
+
light: [
|
|
45
|
+
'#1f2328', // 0 black
|
|
46
|
+
'#cf222e', // 1 red
|
|
47
|
+
'#116329', // 2 green
|
|
48
|
+
'#4d2d00', // 3 yellow
|
|
49
|
+
'#0969da', // 4 blue
|
|
50
|
+
'#8250df', // 5 magenta
|
|
51
|
+
'#1b7c83', // 6 cyan
|
|
52
|
+
'#59636e', // 7 white
|
|
53
|
+
'#393f46', // 8 bright black
|
|
54
|
+
'#a40e26', // 9 bright red
|
|
55
|
+
'#1a7f37', // 10 bright green
|
|
56
|
+
'#633c01', // 11 bright yellow
|
|
57
|
+
'#218bff', // 12 bright blue
|
|
58
|
+
'#a475f9', // 13 bright magenta
|
|
59
|
+
'#3192aa', // 14 bright cyan
|
|
60
|
+
'#818b98', // 15 bright white
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
/** Back-compat alias for the dark ANSI-16 set (the original single export). */
|
|
64
|
+
exports.ANSI_16 = exports.ANSI_PALETTES.dark;
|
|
65
|
+
/**
|
|
66
|
+
* Window/canvas colors per theme. Sourced from @primer/primitives v11.9.0
|
|
67
|
+
* functional tokens: background = `--bgColor-default`, foreground =
|
|
68
|
+
* `--fgColor-default`, chrome (title bar) = `--bgColor-muted`, border =
|
|
69
|
+
* `--borderColor-default`.
|
|
70
|
+
*/
|
|
71
|
+
exports.THEMES = {
|
|
72
|
+
dark: {
|
|
73
|
+
background: '#0d1117',
|
|
74
|
+
foreground: '#f0f6fc',
|
|
75
|
+
chrome: '#151b23',
|
|
76
|
+
border: '#3d444d',
|
|
77
|
+
},
|
|
78
|
+
light: {
|
|
79
|
+
background: '#ffffff',
|
|
80
|
+
foreground: '#1f2328',
|
|
81
|
+
chrome: '#f6f8fa',
|
|
82
|
+
border: '#d1d9e0',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const CUBE_LEVELS = [0, 95, 135, 175, 215, 255];
|
|
86
|
+
function toHex(r, g, b) {
|
|
87
|
+
const h = (n) => n.toString(16).padStart(2, '0');
|
|
88
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
89
|
+
}
|
|
90
|
+
/** Convert an xterm packed RGB integer (0xRRGGBB) to a #rrggbb string. */
|
|
91
|
+
function packedRgbToHex(value) {
|
|
92
|
+
return toHex((value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Convert a 0–255 xterm palette index to a #rrggbb string.
|
|
96
|
+
*
|
|
97
|
+
* Indices 0–15 are theme-dependent (ANSI-16, brand-themed). The 6×6×6 color
|
|
98
|
+
* cube (16–231) and the grayscale ramp (232–255) are fixed by the xterm-256
|
|
99
|
+
* spec and identical across themes. Defaults to the dark palette for
|
|
100
|
+
* back-compat with callers that don't pass a theme.
|
|
101
|
+
*/
|
|
102
|
+
function paletteToHex(index, theme = 'dark') {
|
|
103
|
+
if (index < 16)
|
|
104
|
+
return exports.ANSI_PALETTES[theme][index];
|
|
105
|
+
if (index < 232) {
|
|
106
|
+
const i = index - 16;
|
|
107
|
+
return toHex(CUBE_LEVELS[Math.floor(i / 36) % 6], CUBE_LEVELS[Math.floor(i / 6) % 6], CUBE_LEVELS[i % 6]);
|
|
108
|
+
}
|
|
109
|
+
const gray = 8 + (index - 232) * 10;
|
|
110
|
+
return toHex(gray, gray, gray);
|
|
111
|
+
}
|