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/timing.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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.TimingTracker = exports.TIMING_SCHEMA_VERSION = void 0;
|
|
7
|
+
exports.timingPathFor = timingPathFor;
|
|
8
|
+
exports.writeTiming = writeTiming;
|
|
9
|
+
exports.readTiming = readTiming;
|
|
10
|
+
exports.timeAtOffset = timeAtOffset;
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
exports.TIMING_SCHEMA_VERSION = 1;
|
|
14
|
+
/** Sibling sidecar path for a capture file: `foo.ans` -> `foo.timing.json`. */
|
|
15
|
+
function timingPathFor(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}.timing.json`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Accumulates timing checkpoints while a capture streams in. Call {@link mark}
|
|
22
|
+
* with the byte length of each chunk *as it's written to the `.ans`* (so offsets
|
|
23
|
+
* line up with the file the parser later reads), then {@link build} to get the
|
|
24
|
+
* serializable sidecar.
|
|
25
|
+
*/
|
|
26
|
+
class TimingTracker {
|
|
27
|
+
events = [];
|
|
28
|
+
startMs;
|
|
29
|
+
offset = 0;
|
|
30
|
+
constructor(startMs = Date.now()) {
|
|
31
|
+
this.startMs = startMs;
|
|
32
|
+
}
|
|
33
|
+
/** Record that `byteLength` more bytes were written at time `now`. */
|
|
34
|
+
mark(byteLength, now = Date.now()) {
|
|
35
|
+
if (byteLength <= 0)
|
|
36
|
+
return;
|
|
37
|
+
this.offset += byteLength;
|
|
38
|
+
this.events.push([Math.max(0, now - this.startMs), this.offset]);
|
|
39
|
+
}
|
|
40
|
+
/** Total bytes seen so far (the final stream length). */
|
|
41
|
+
get byteLength() {
|
|
42
|
+
return this.offset;
|
|
43
|
+
}
|
|
44
|
+
build() {
|
|
45
|
+
return {
|
|
46
|
+
schemaVersion: exports.TIMING_SCHEMA_VERSION,
|
|
47
|
+
startedAt: new Date(this.startMs).toISOString(),
|
|
48
|
+
events: this.events,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.TimingTracker = TimingTracker;
|
|
53
|
+
/** Write the timing sidecar next to `captureFile`. Returns the sidecar path. */
|
|
54
|
+
async function writeTiming(captureFile, timing) {
|
|
55
|
+
const out = timingPathFor(captureFile);
|
|
56
|
+
await (0, promises_1.writeFile)(out, `${JSON.stringify(timing)}\n`, 'utf8');
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read a capture's timing sidecar, or null if it's missing or unusable. Never
|
|
61
|
+
* throws — a missing/garbled sidecar simply means "no timing; fall back to a
|
|
62
|
+
* uniform default duration".
|
|
63
|
+
*/
|
|
64
|
+
async function readTiming(captureFile) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await (0, promises_1.readFile)(timingPathFor(captureFile), 'utf8');
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (!Array.isArray(parsed.events))
|
|
69
|
+
return null;
|
|
70
|
+
const events = parsed.events.filter((e) => Array.isArray(e) &&
|
|
71
|
+
e.length === 2 &&
|
|
72
|
+
Number.isFinite(e[0]) &&
|
|
73
|
+
Number.isFinite(e[1]));
|
|
74
|
+
if (events.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
return {
|
|
77
|
+
schemaVersion: parsed.schemaVersion ?? exports.TIMING_SCHEMA_VERSION,
|
|
78
|
+
startedAt: parsed.startedAt ?? '',
|
|
79
|
+
events,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Map a byte offset in the captured stream to the moment (ms since start) it
|
|
88
|
+
* appeared on screen. A chunk ending at offset `o` arrived at time `t`, so every
|
|
89
|
+
* byte in `(prevOffset, o]` is attributed to `t`; we return the time of the
|
|
90
|
+
* first checkpoint whose end offset reaches `offset`. Offsets past the last
|
|
91
|
+
* checkpoint resolve to the final timestamp; an empty list yields 0.
|
|
92
|
+
*/
|
|
93
|
+
function timeAtOffset(events, offset) {
|
|
94
|
+
if (events.length === 0)
|
|
95
|
+
return 0;
|
|
96
|
+
if (offset <= 0)
|
|
97
|
+
return events[0][0];
|
|
98
|
+
const lastIdx = events.length - 1;
|
|
99
|
+
if (offset >= events[lastIdx][1])
|
|
100
|
+
return events[lastIdx][0];
|
|
101
|
+
// Binary search for the first checkpoint whose offset >= `offset`.
|
|
102
|
+
let lo = 0;
|
|
103
|
+
let hi = lastIdx;
|
|
104
|
+
while (lo < hi) {
|
|
105
|
+
const mid = (lo + hi) >> 1;
|
|
106
|
+
if (events[mid][1] >= offset)
|
|
107
|
+
hi = mid;
|
|
108
|
+
else
|
|
109
|
+
lo = mid + 1;
|
|
110
|
+
}
|
|
111
|
+
return events[lo][0];
|
|
112
|
+
}
|
package/dist/types.js
ADDED
package/dist/version.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
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.PACKAGE_NAME = void 0;
|
|
7
|
+
exports.currentVersion = currentVersion;
|
|
8
|
+
exports.parseVersion = parseVersion;
|
|
9
|
+
exports.compareVersions = compareVersions;
|
|
10
|
+
exports.isNewer = isNewer;
|
|
11
|
+
exports.installKind = installKind;
|
|
12
|
+
exports.updateCommand = updateCommand;
|
|
13
|
+
exports.fetchLatestVersion = fetchLatestVersion;
|
|
14
|
+
exports.checkForUpdate = checkForUpdate;
|
|
15
|
+
exports.runSelfUpdate = runSelfUpdate;
|
|
16
|
+
const node_child_process_1 = require("node:child_process");
|
|
17
|
+
const node_fs_1 = require("node:fs");
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
/** The published npm package name, shared by the registry lookup and update command. */
|
|
20
|
+
exports.PACKAGE_NAME = 'tui-cap';
|
|
21
|
+
/** npm registry endpoint for the latest published manifest (small payload). */
|
|
22
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${exports.PACKAGE_NAME}/latest`;
|
|
23
|
+
let cachedVersion;
|
|
24
|
+
/**
|
|
25
|
+
* The `version` field of the package this module ships in, read once and cached.
|
|
26
|
+
* `package.json` sits one level above this module in both dev (`src/`) and the
|
|
27
|
+
* published build (`dist/`), and npm always includes it in the tarball.
|
|
28
|
+
*/
|
|
29
|
+
function currentVersion() {
|
|
30
|
+
if (cachedVersion !== undefined)
|
|
31
|
+
return cachedVersion;
|
|
32
|
+
try {
|
|
33
|
+
const pkgPath = node_path_1.default.join(__dirname, '..', 'package.json');
|
|
34
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, 'utf8'));
|
|
35
|
+
cachedVersion = typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
cachedVersion = '0.0.0';
|
|
39
|
+
}
|
|
40
|
+
return cachedVersion;
|
|
41
|
+
}
|
|
42
|
+
/** Parse a semver-ish string (`1.2.3`, `v1.2.3`, `1.2.3-beta.1`), or null. */
|
|
43
|
+
function parseVersion(value) {
|
|
44
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(value.trim());
|
|
45
|
+
if (!m)
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
major: Number(m[1]),
|
|
49
|
+
minor: Number(m[2]),
|
|
50
|
+
patch: Number(m[3]),
|
|
51
|
+
pre: m[4] ? m[4].split('.') : [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Compare two pre-release identifier lists per semver precedence rules. */
|
|
55
|
+
function comparePre(a, b) {
|
|
56
|
+
// A normal release outranks any pre-release of the same x.y.z.
|
|
57
|
+
if (a.length === 0 && b.length === 0)
|
|
58
|
+
return 0;
|
|
59
|
+
if (a.length === 0)
|
|
60
|
+
return 1;
|
|
61
|
+
if (b.length === 0)
|
|
62
|
+
return -1;
|
|
63
|
+
const n = Math.max(a.length, b.length);
|
|
64
|
+
for (let i = 0; i < n; i++) {
|
|
65
|
+
const ai = a[i];
|
|
66
|
+
const bi = b[i];
|
|
67
|
+
if (ai === undefined)
|
|
68
|
+
return -1; // shorter set is lower
|
|
69
|
+
if (bi === undefined)
|
|
70
|
+
return 1;
|
|
71
|
+
const aNum = /^\d+$/.test(ai);
|
|
72
|
+
const bNum = /^\d+$/.test(bi);
|
|
73
|
+
if (aNum && bNum) {
|
|
74
|
+
const d = Number(ai) - Number(bi);
|
|
75
|
+
if (d !== 0)
|
|
76
|
+
return d < 0 ? -1 : 1;
|
|
77
|
+
}
|
|
78
|
+
else if (aNum !== bNum) {
|
|
79
|
+
return aNum ? -1 : 1; // numeric identifiers are lower than alphanumeric
|
|
80
|
+
}
|
|
81
|
+
else if (ai !== bi) {
|
|
82
|
+
return ai < bi ? -1 : 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Semver precedence compare: -1 if `a` < `b`, 0 if equal, 1 if `a` > `b`.
|
|
89
|
+
* Unparseable input compares as equal (0) so a garbled value never triggers a
|
|
90
|
+
* spurious update prompt.
|
|
91
|
+
*/
|
|
92
|
+
function compareVersions(a, b) {
|
|
93
|
+
const pa = parseVersion(a);
|
|
94
|
+
const pb = parseVersion(b);
|
|
95
|
+
if (!pa || !pb)
|
|
96
|
+
return 0;
|
|
97
|
+
if (pa.major !== pb.major)
|
|
98
|
+
return pa.major < pb.major ? -1 : 1;
|
|
99
|
+
if (pa.minor !== pb.minor)
|
|
100
|
+
return pa.minor < pb.minor ? -1 : 1;
|
|
101
|
+
if (pa.patch !== pb.patch)
|
|
102
|
+
return pa.patch < pb.patch ? -1 : 1;
|
|
103
|
+
return comparePre(pa.pre, pb.pre);
|
|
104
|
+
}
|
|
105
|
+
/** True when `candidate` is a strictly newer version than `base`. */
|
|
106
|
+
function isNewer(candidate, base) {
|
|
107
|
+
return compareVersions(candidate, base) > 0;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Best-effort detection of how this CLI was installed. Uses the real (symlink-
|
|
111
|
+
* resolved) module directory: npm's npx cache lives under `…/_npx/…`, and a dev
|
|
112
|
+
* checkout sits under the current working directory; anything else is treated as
|
|
113
|
+
* a global install (the common case for `npm install -g`).
|
|
114
|
+
*/
|
|
115
|
+
function installKind() {
|
|
116
|
+
let dir = __dirname;
|
|
117
|
+
try {
|
|
118
|
+
dir = (0, node_fs_1.realpathSync)(__dirname);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// fall back to the un-resolved path
|
|
122
|
+
}
|
|
123
|
+
const normalized = dir.split(node_path_1.default.sep).join('/');
|
|
124
|
+
if (normalized.includes('/_npx/'))
|
|
125
|
+
return 'npx';
|
|
126
|
+
let cwd = process.cwd();
|
|
127
|
+
try {
|
|
128
|
+
cwd = (0, node_fs_1.realpathSync)(cwd);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// use cwd as-is
|
|
132
|
+
}
|
|
133
|
+
if (dir === cwd || dir.startsWith(cwd + node_path_1.default.sep))
|
|
134
|
+
return 'local';
|
|
135
|
+
if (normalized.includes('/node_modules/'))
|
|
136
|
+
return 'global';
|
|
137
|
+
return 'unknown';
|
|
138
|
+
}
|
|
139
|
+
/** The shell command that upgrades a global install to the latest release. */
|
|
140
|
+
function updateCommand() {
|
|
141
|
+
return `npm install -g ${exports.PACKAGE_NAME}@latest`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Fetch the latest published version from the npm registry, or null if the
|
|
145
|
+
* request fails or times out. Never throws — a missing answer simply means "no
|
|
146
|
+
* update prompt", so an offline machine degrades quietly.
|
|
147
|
+
*/
|
|
148
|
+
async function fetchLatestVersion(timeoutMs = 3500) {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
151
|
+
try {
|
|
152
|
+
const res = await fetch(REGISTRY_URL, {
|
|
153
|
+
signal: controller.signal,
|
|
154
|
+
headers: {
|
|
155
|
+
// The abbreviated metadata document is smaller and cache-friendly.
|
|
156
|
+
accept: 'application/vnd.npm.install-v1+json, application/json',
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok)
|
|
160
|
+
return null;
|
|
161
|
+
const body = (await res.json());
|
|
162
|
+
return typeof body.version === 'string' ? body.version : null;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Gather everything the GUI/CLI needs to decide whether to prompt for an update. */
|
|
172
|
+
async function checkForUpdate(timeoutMs) {
|
|
173
|
+
const current = currentVersion();
|
|
174
|
+
const latest = await fetchLatestVersion(timeoutMs);
|
|
175
|
+
return {
|
|
176
|
+
name: exports.PACKAGE_NAME,
|
|
177
|
+
current,
|
|
178
|
+
latest,
|
|
179
|
+
updateAvailable: latest !== null && isNewer(latest, current),
|
|
180
|
+
installKind: installKind(),
|
|
181
|
+
updateCommand: updateCommand(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Run `npm install -g tui-cap@latest` and resolve with the outcome. Captures a
|
|
186
|
+
* bounded tail of the command output so a failure (e.g. EACCES on a root-owned
|
|
187
|
+
* global prefix) can be shown without flooding. Never rejects.
|
|
188
|
+
*/
|
|
189
|
+
function runSelfUpdate(timeoutMs = 120_000) {
|
|
190
|
+
const command = updateCommand();
|
|
191
|
+
const fromVersion = currentVersion();
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
let output = '';
|
|
194
|
+
const capture = (buf) => {
|
|
195
|
+
output += buf.toString();
|
|
196
|
+
if (output.length > 20_000)
|
|
197
|
+
output = output.slice(-20_000);
|
|
198
|
+
};
|
|
199
|
+
let child;
|
|
200
|
+
try {
|
|
201
|
+
child = (0, node_child_process_1.spawn)('npm', ['install', '-g', `${exports.PACKAGE_NAME}@latest`], {
|
|
202
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
203
|
+
// npm is a `.cmd` shim on Windows; a shell lets it resolve on PATH.
|
|
204
|
+
shell: process.platform === 'win32',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
resolve({
|
|
209
|
+
ok: false,
|
|
210
|
+
fromVersion,
|
|
211
|
+
command,
|
|
212
|
+
output: err instanceof Error ? err.message : String(err),
|
|
213
|
+
code: null,
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const timer = setTimeout(() => {
|
|
218
|
+
output += '\nUpdate timed out.';
|
|
219
|
+
child.kill('SIGKILL');
|
|
220
|
+
}, timeoutMs);
|
|
221
|
+
child.stdout?.on('data', capture);
|
|
222
|
+
child.stderr?.on('data', capture);
|
|
223
|
+
child.on('error', (err) => {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
resolve({ ok: false, fromVersion, command, output: `${output}\n${err.message}`.trim(), code: null });
|
|
226
|
+
});
|
|
227
|
+
child.on('close', (code) => {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
resolve({ ok: code === 0, fromVersion, command, output: output.trim(), code });
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|