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/paths.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
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.DEFAULT_CAPTURE_PREFIX = void 0;
|
|
7
|
+
exports.capturesDir = capturesDir;
|
|
8
|
+
exports.isExplicitPath = isExplicitPath;
|
|
9
|
+
exports.resolveOut = resolveOut;
|
|
10
|
+
exports.resolveInput = resolveInput;
|
|
11
|
+
exports.nextCaptureName = nextCaptureName;
|
|
12
|
+
const promises_1 = require("node:fs/promises");
|
|
13
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
/** Default base name for captures recorded without an explicit `-o`. */
|
|
16
|
+
exports.DEFAULT_CAPTURE_PREFIX = 'copilot-capture';
|
|
17
|
+
/**
|
|
18
|
+
* The folder where captures and exports live by default, so files land in one
|
|
19
|
+
* place regardless of which repo you run from: `$GHCP_CAPTURE_DIR`, or
|
|
20
|
+
* `~/captures` when that isn't set.
|
|
21
|
+
*/
|
|
22
|
+
function capturesDir() {
|
|
23
|
+
const fromEnv = process.env.GHCP_CAPTURE_DIR;
|
|
24
|
+
if (fromEnv && fromEnv.trim() !== '')
|
|
25
|
+
return fromEnv;
|
|
26
|
+
return node_path_1.default.join(node_os_1.default.homedir(), 'captures');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True when a name is an explicit location the user clearly meant to be
|
|
30
|
+
* relative to the current directory — an absolute path or one containing a path
|
|
31
|
+
* separator — rather than a bare filename destined for the captures folder.
|
|
32
|
+
*/
|
|
33
|
+
function isExplicitPath(name) {
|
|
34
|
+
return node_path_1.default.isAbsolute(name) || name.includes('/') || name.includes(node_path_1.default.sep);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve an output path so captures land in one place regardless of the repo
|
|
38
|
+
* you run from. A bare filename (or an omitted value) goes in the captures
|
|
39
|
+
* folder; a value containing a path separator (or an absolute path) is kept
|
|
40
|
+
* as-is, relative to the current working directory.
|
|
41
|
+
*/
|
|
42
|
+
function resolveOut(value, defaultName) {
|
|
43
|
+
const name = value ?? defaultName;
|
|
44
|
+
if (isExplicitPath(name))
|
|
45
|
+
return name;
|
|
46
|
+
return node_path_1.default.join(capturesDir(), name);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve an input path. An explicit path (absolute or containing a separator)
|
|
50
|
+
* is used as-is. A bare filename is looked up in the current directory first,
|
|
51
|
+
* then in the captures folder, so `render shot.ans` finds a capture recorded
|
|
52
|
+
* with `record -o shot.ans` regardless of where you run it.
|
|
53
|
+
*/
|
|
54
|
+
async function resolveInput(value) {
|
|
55
|
+
if (isExplicitPath(value))
|
|
56
|
+
return value;
|
|
57
|
+
try {
|
|
58
|
+
await (0, promises_1.stat)(value);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// not in the current directory
|
|
63
|
+
}
|
|
64
|
+
const inCaptures = node_path_1.default.join(capturesDir(), value);
|
|
65
|
+
try {
|
|
66
|
+
await (0, promises_1.stat)(inCaptures);
|
|
67
|
+
return inCaptures;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// fall through so readFile reports a clear error for the original name
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function escapeRegExp(input) {
|
|
75
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The next un-taken auto name in the captures folder — `copilot-capture_01.ans`,
|
|
79
|
+
* `copilot-capture_02.ans`, and so on. Used when `record` runs without an
|
|
80
|
+
* explicit `-o` so repeated captures don't clobber a single fixed filename.
|
|
81
|
+
* Scans the folder and picks the lowest free index (2-digit zero-padded).
|
|
82
|
+
*/
|
|
83
|
+
async function nextCaptureName(prefix = exports.DEFAULT_CAPTURE_PREFIX, ext = '.ans') {
|
|
84
|
+
const used = new Set();
|
|
85
|
+
const re = new RegExp(`^${escapeRegExp(prefix)}_(\\d+)${escapeRegExp(ext)}$`, 'i');
|
|
86
|
+
try {
|
|
87
|
+
for (const entry of await (0, promises_1.readdir)(capturesDir())) {
|
|
88
|
+
const match = entry.match(re);
|
|
89
|
+
if (match)
|
|
90
|
+
used.add(Number.parseInt(match[1], 10));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// captures folder doesn't exist yet → start at 1
|
|
95
|
+
}
|
|
96
|
+
let n = 1;
|
|
97
|
+
while (used.has(n))
|
|
98
|
+
n += 1;
|
|
99
|
+
return `${prefix}_${String(n).padStart(2, '0')}${ext}`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.recordSessionPty = recordSessionPty;
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const meta_1 = require("./meta");
|
|
39
|
+
const timing_1 = require("./timing");
|
|
40
|
+
const input_1 = require("./input");
|
|
41
|
+
/**
|
|
42
|
+
* Opt-in capture backend built on `node-pty`. Unlike the default `script`
|
|
43
|
+
* wrapper, node-pty lets us own the PTY directly: we set its size, stream raw
|
|
44
|
+
* bytes straight to the output file, and record the dimensions together with the
|
|
45
|
+
* bytes — no reliance on the system `script` arg quirks.
|
|
46
|
+
*
|
|
47
|
+
* `node-pty` is an OPTIONAL native dependency (it needs a compile step), so it's
|
|
48
|
+
* imported lazily through a variable specifier; tsc won't require it to be
|
|
49
|
+
* installed, and a missing install yields a clear, actionable error.
|
|
50
|
+
*/
|
|
51
|
+
async function recordSessionPty(command, outFile, opts = {}) {
|
|
52
|
+
if (command.length === 0) {
|
|
53
|
+
throw new Error('record: no command provided (use `-- <command...>`)');
|
|
54
|
+
}
|
|
55
|
+
// A variable specifier keeps tsc from resolving (and thus requiring) the
|
|
56
|
+
// optional native module at build time.
|
|
57
|
+
const moduleName = 'node-pty';
|
|
58
|
+
let pty;
|
|
59
|
+
try {
|
|
60
|
+
pty = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error('The pty backend needs the optional `node-pty` dependency.\n' +
|
|
64
|
+
' Install it with: npm install node-pty\n' +
|
|
65
|
+
' Then re-run with: --backend pty\n' +
|
|
66
|
+
'(node-pty is native; the default `script` backend needs no build step.)');
|
|
67
|
+
}
|
|
68
|
+
const { cols, rows } = (0, meta_1.terminalSize)();
|
|
69
|
+
const [file, ...args] = command;
|
|
70
|
+
const out = (0, node_fs_1.createWriteStream)(outFile);
|
|
71
|
+
// One shared clock for both sidecars so keystroke times line up with the
|
|
72
|
+
// frame appearance times derived from the timing checkpoints.
|
|
73
|
+
const startMs = Date.now();
|
|
74
|
+
const tracker = new timing_1.TimingTracker(startMs);
|
|
75
|
+
const captureInput = opts.input !== false && Boolean(process.stdin.isTTY);
|
|
76
|
+
const inputTracker = captureInput ? new input_1.InputTracker(startMs) : null;
|
|
77
|
+
const child = pty.spawn(file, args, {
|
|
78
|
+
name: process.env.TERM ?? 'xterm-256color',
|
|
79
|
+
cols,
|
|
80
|
+
rows,
|
|
81
|
+
cwd: process.cwd(),
|
|
82
|
+
env: process.env,
|
|
83
|
+
});
|
|
84
|
+
// Tee the child's output to both the file (raw, for replay) and our stdout
|
|
85
|
+
// (so the user sees the live session while it records), timestamping each
|
|
86
|
+
// chunk so the timing sidecar can map byte offsets back to wall-clock time.
|
|
87
|
+
child.onData((data) => {
|
|
88
|
+
tracker.mark(Buffer.byteLength(data, 'utf8'));
|
|
89
|
+
out.write(data);
|
|
90
|
+
process.stdout.write(data);
|
|
91
|
+
});
|
|
92
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
93
|
+
// Record a printable-count of each keystroke burst before forwarding it, so
|
|
94
|
+
// the parser can tell which output frames were produced by user typing.
|
|
95
|
+
const onStdin = (d) => {
|
|
96
|
+
inputTracker?.mark(d);
|
|
97
|
+
child.write(d.toString('utf8'));
|
|
98
|
+
};
|
|
99
|
+
if (isTTY) {
|
|
100
|
+
process.stdin.setRawMode?.(true);
|
|
101
|
+
process.stdin.resume();
|
|
102
|
+
process.stdin.on('data', onStdin);
|
|
103
|
+
}
|
|
104
|
+
const onResize = () => {
|
|
105
|
+
const s = (0, meta_1.terminalSize)();
|
|
106
|
+
try {
|
|
107
|
+
child.resize(s.cols, s.rows);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* child may have exited */
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
process.stdout.on('resize', onResize);
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
child.onExit(({ exitCode }) => {
|
|
116
|
+
if (isTTY) {
|
|
117
|
+
process.stdin.setRawMode?.(false);
|
|
118
|
+
process.stdin.pause();
|
|
119
|
+
process.stdin.off('data', onStdin);
|
|
120
|
+
}
|
|
121
|
+
process.stdout.off('resize', onResize);
|
|
122
|
+
const finish = () => resolve(exitCode ?? 0);
|
|
123
|
+
if (opts.meta === false) {
|
|
124
|
+
out.end(finish);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Flush the capture file before writing sidecars so they describe the
|
|
128
|
+
// complete stream and a reader sees the full `.ans`.
|
|
129
|
+
out.end(() => {
|
|
130
|
+
const capturedAt = new Date().toISOString();
|
|
131
|
+
const sidecars = [
|
|
132
|
+
(0, meta_1.writeMeta)(outFile, {
|
|
133
|
+
schemaVersion: meta_1.META_SCHEMA_VERSION,
|
|
134
|
+
cols,
|
|
135
|
+
rows,
|
|
136
|
+
capturedAt,
|
|
137
|
+
command: command.join(' '),
|
|
138
|
+
}),
|
|
139
|
+
(0, timing_1.writeTiming)(outFile, tracker.build()),
|
|
140
|
+
];
|
|
141
|
+
if (inputTracker && inputTracker.length > 0) {
|
|
142
|
+
sidecars.push((0, input_1.writeInput)(outFile, inputTracker.build()));
|
|
143
|
+
}
|
|
144
|
+
Promise.all(sidecars).then(finish, finish); // sidecars are best-effort
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
package/dist/record.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
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.recordSession = recordSession;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const meta_1 = require("./meta");
|
|
11
|
+
const timing_1 = require("./timing");
|
|
12
|
+
/**
|
|
13
|
+
* Record a live program's raw terminal output (escape codes intact) to a file
|
|
14
|
+
* using the system `script` utility, which allocates a real PTY so the target
|
|
15
|
+
* still behaves like it's attached to a terminal.
|
|
16
|
+
*
|
|
17
|
+
* Rather than let `script` write the capture file directly, we point its own
|
|
18
|
+
* output at `/dev/null` and **pipe its stdout** to ourselves: every chunk is
|
|
19
|
+
* timestamped (for the timing sidecar that powers animation), written to the
|
|
20
|
+
* `.ans` file, and echoed to our stdout so the live session is still visible.
|
|
21
|
+
* Capturing the bytes ourselves keeps the `.ans` offsets and the timing
|
|
22
|
+
* checkpoints perfectly aligned. The child still sees a genuine PTY from
|
|
23
|
+
* `script`, sized from the controlling terminal.
|
|
24
|
+
*
|
|
25
|
+
* The PTY dimensions are written to a `<out>.meta.json` sidecar (so `render`
|
|
26
|
+
* replays at the recorded size) and the chunk timing to `<out>.timing.json`.
|
|
27
|
+
*
|
|
28
|
+
* The resulting `.ans` file can be replayed with `parseAnsiToGrid`.
|
|
29
|
+
*/
|
|
30
|
+
function recordSession(command, outFile, opts = {}) {
|
|
31
|
+
if (command.length === 0) {
|
|
32
|
+
throw new Error('record: no command provided (use `-- <command...>`)');
|
|
33
|
+
}
|
|
34
|
+
// Snapshot the terminal size now: `script` sizes the child PTY to the current
|
|
35
|
+
// controlling terminal, so this matches the dimensions the program renders at.
|
|
36
|
+
const { cols, rows } = (0, meta_1.terminalSize)();
|
|
37
|
+
// Send `script`'s own typescript to the bit bucket — we capture the session
|
|
38
|
+
// from its stdout instead (see above). The platforms differ only in flag shape.
|
|
39
|
+
const sink = '/dev/null';
|
|
40
|
+
let bin;
|
|
41
|
+
let args;
|
|
42
|
+
if (node_os_1.default.platform() === 'darwin') {
|
|
43
|
+
// BSD/macOS: script [-q] file [command ...]
|
|
44
|
+
bin = 'script';
|
|
45
|
+
args = ['-q', sink, ...command];
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// util-linux: script -q -e -c "<cmd>" file
|
|
49
|
+
bin = 'script';
|
|
50
|
+
args = ['-q', '-e', '-c', command.join(' '), sink];
|
|
51
|
+
}
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const out = (0, node_fs_1.createWriteStream)(outFile);
|
|
54
|
+
const tracker = new timing_1.TimingTracker();
|
|
55
|
+
// Inherit stdin only when we're attached to a real TTY (interactive
|
|
56
|
+
// recording, where a human drives the program). For non-interactive
|
|
57
|
+
// captures (piped stdin, or commands like `copilot -p "..."` that exit on
|
|
58
|
+
// their own), inheriting a non-TTY pipe makes `script` abort before running
|
|
59
|
+
// the command, so fall back to /dev/null via 'ignore'.
|
|
60
|
+
//
|
|
61
|
+
// NB: `script` requires its stdin to be the controlling terminal — it calls
|
|
62
|
+
// tcgetattr on stdin and aborts ("Operation not supported on socket") if
|
|
63
|
+
// handed a pipe. So we never pipe its stdin, which means the `script`
|
|
64
|
+
// backend can't tee keystrokes. Keystroke capture (the `.input.json`
|
|
65
|
+
// sidecar that powers precise typing correlation) lives in the node-pty
|
|
66
|
+
// backend instead; the `script` backend still gets typing detection via the
|
|
67
|
+
// parser's content-growth fallback.
|
|
68
|
+
const stdin = process.stdin.isTTY ? 'inherit' : 'ignore';
|
|
69
|
+
const child = (0, node_child_process_1.spawn)(bin, args, { stdio: [stdin, 'pipe', 'inherit'] });
|
|
70
|
+
child.stdout.on('data', (chunk) => {
|
|
71
|
+
tracker.mark(chunk.length);
|
|
72
|
+
out.write(chunk);
|
|
73
|
+
process.stdout.write(chunk);
|
|
74
|
+
});
|
|
75
|
+
child.on('error', reject);
|
|
76
|
+
child.on('exit', (code) => {
|
|
77
|
+
// Flush the capture file before writing sidecars / resolving so callers
|
|
78
|
+
// (and tests) that read it immediately see the full stream.
|
|
79
|
+
out.end(() => {
|
|
80
|
+
const finish = () => resolve(code ?? 0);
|
|
81
|
+
if (opts.meta === false) {
|
|
82
|
+
finish();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const capturedAt = new Date().toISOString();
|
|
86
|
+
const sidecars = [
|
|
87
|
+
(0, meta_1.writeMeta)(outFile, {
|
|
88
|
+
schemaVersion: meta_1.META_SCHEMA_VERSION,
|
|
89
|
+
cols,
|
|
90
|
+
rows,
|
|
91
|
+
capturedAt,
|
|
92
|
+
command: command.join(' '),
|
|
93
|
+
}),
|
|
94
|
+
(0, timing_1.writeTiming)(outFile, tracker.build()),
|
|
95
|
+
];
|
|
96
|
+
Promise.all(sidecars).then(finish, finish); // sidecars are best-effort
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|