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/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
+ }
@@ -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
+ }