termcut 0.6.4 → 0.7.1
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 +10 -2
- package/package.json +1 -1
- package/scripts/build-themes.ts +10 -6
- package/src/browser.ts +57 -13
- package/src/cast.ts +2 -0
- package/src/cli.ts +135 -27
- package/src/config.ts +35 -2
- package/src/diff.ts +3 -13
- package/src/duration.ts +4 -8
- package/src/edit.ts +188 -0
- package/src/export/frames.ts +18 -1
- package/src/export/html.ts +6 -2
- package/src/export/svg.ts +31 -3
- package/src/index.ts +6 -2
- package/src/keylabels.ts +28 -27
- package/src/keys.ts +18 -18
- package/src/live.ts +18 -7
- package/src/loop.ts +14 -9
- package/src/presets.ts +2 -2
- package/src/publish.ts +19 -21
- package/src/recorder.ts +45 -6
- package/src/render.ts +69 -2
- package/src/renderer/encoder.ts +49 -11
- package/src/renderer/generated/page.js +1 -1
- package/src/renderer/page-entry.ts +18 -6
- package/src/renderer/page.ts +99 -3
- package/src/renderer/png.ts +189 -0
- package/src/renderer/view.ts +41 -0
- package/src/renderer/webview.ts +38 -15
- package/src/screen.ts +9 -3
- package/src/scriptgen.ts +31 -30
- package/src/themes.ts +9 -11
- package/src/timeline.ts +21 -5
- package/src/types.ts +63 -2
- package/src/video.ts +15 -12
package/src/types.ts
CHANGED
|
@@ -109,6 +109,39 @@ export interface BrowserSession {
|
|
|
109
109
|
readonly url: string;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/** Drop shadow under the window(s). Drawn by the compositor, so it is in every output that has pixels — and in SVG. */
|
|
113
|
+
export interface ShadowConfig {
|
|
114
|
+
/** Horizontal offset, px. Default 0. */
|
|
115
|
+
x?: number;
|
|
116
|
+
/** Vertical offset, px. Default 18. */
|
|
117
|
+
y?: number;
|
|
118
|
+
/** Blur radius, px. Default 50. */
|
|
119
|
+
blur?: number;
|
|
120
|
+
/** Shadow colour. Default "#000000". */
|
|
121
|
+
color?: string;
|
|
122
|
+
/** 0–1. Default 0.45. */
|
|
123
|
+
opacity?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type WatermarkPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right" | "center";
|
|
127
|
+
|
|
128
|
+
/** A watermark drawn over the whole picture (outside the terminal grid): a line of text or an image file. */
|
|
129
|
+
export interface WatermarkConfig {
|
|
130
|
+
text?: string;
|
|
131
|
+
/** PNG/JPEG/SVG/WebP file. */
|
|
132
|
+
image?: string;
|
|
133
|
+
/** Default "bottom-right". */
|
|
134
|
+
position?: WatermarkPosition;
|
|
135
|
+
/** 0–1. Default 0.6. */
|
|
136
|
+
opacity?: number;
|
|
137
|
+
/** Text size in px (default 14) or image height in px (default 28). */
|
|
138
|
+
size?: number;
|
|
139
|
+
/** Text colour. Default: the theme foreground. */
|
|
140
|
+
color?: string;
|
|
141
|
+
/** Distance from the picture's edge, px. Default 16. */
|
|
142
|
+
margin?: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
112
145
|
export interface CursorConfig {
|
|
113
146
|
/** Default true. Blink is driven by the render clock, so it is deterministic. */
|
|
114
147
|
blink?: boolean;
|
|
@@ -182,8 +215,15 @@ export interface VideoConfig {
|
|
|
182
215
|
padding?: number;
|
|
183
216
|
/** Space around the window, px. Default 0. */
|
|
184
217
|
margin?: number;
|
|
185
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Colour behind the window (visible when margin > 0). Default: theme background. `"transparent"` gives real
|
|
220
|
+
* alpha in PNG, WebP, GIF, WebM, SVG and HTML output (MP4 and JPEG fall back to the theme background).
|
|
221
|
+
*/
|
|
186
222
|
marginFill?: string;
|
|
223
|
+
/** Drop shadow under the window; `true` uses soft defaults. Needs margin — `margin` defaults to 40 when unset. */
|
|
224
|
+
shadow?: boolean | ShadowConfig;
|
|
225
|
+
/** Watermark over the picture: a string is text in the bottom-right corner; an object picks image/position/size. */
|
|
226
|
+
watermark?: string | WatermarkConfig;
|
|
187
227
|
/** Rounded corner radius of the window, px. Default 0 (12 is nice with a margin). */
|
|
188
228
|
borderRadius?: number;
|
|
189
229
|
windowBar?: WindowBar;
|
|
@@ -224,6 +264,8 @@ export interface ResolvedConfig {
|
|
|
224
264
|
padding: number;
|
|
225
265
|
margin: number;
|
|
226
266
|
marginFill: string;
|
|
267
|
+
shadow?: Required<ShadowConfig>;
|
|
268
|
+
watermark?: Required<Pick<WatermarkConfig, "position" | "opacity" | "size" | "color" | "margin">> & Pick<WatermarkConfig, "text" | "image">;
|
|
227
269
|
borderRadius: number;
|
|
228
270
|
windowBar: WindowBar;
|
|
229
271
|
title: string;
|
|
@@ -326,8 +368,13 @@ export interface TerminalSession {
|
|
|
326
368
|
title(text: string, opts?: { pause?: Duration }): Promise<void>;
|
|
327
369
|
/** Magnify a region of the terminal (animated at render time); `zoom(null)` resets. */
|
|
328
370
|
zoom(region: ZoomRegion | null): Promise<void>;
|
|
329
|
-
/** Named chapter: becomes mp4 chapter metadata
|
|
371
|
+
/** Named chapter: becomes mp4 chapter metadata, shows up in `--json` output, and is a cut point for `--chapters` / `--split-chapters`. */
|
|
330
372
|
chapter(name: string): Promise<void>;
|
|
373
|
+
/**
|
|
374
|
+
* Everything inside `fn` plays back `speed`× faster (default 8). Unlike `maxPause`, which only squeezes silence,
|
|
375
|
+
* this squeezes active output too — installs, builds, test runs.
|
|
376
|
+
*/
|
|
377
|
+
timelapse<T>(fn: () => Promise<T>, opts?: { speed?: number }): Promise<T>;
|
|
331
378
|
/** The recorded browser window; throws if `browser` is not configured. */
|
|
332
379
|
readonly browser: BrowserSession;
|
|
333
380
|
/** Overlay layout: bring the terminal or the browser window to the front (recorded as a marker). */
|
|
@@ -381,9 +428,23 @@ export interface RenderProgress {
|
|
|
381
428
|
total: number;
|
|
382
429
|
}
|
|
383
430
|
|
|
431
|
+
/** Which part of the visible timeline to render. */
|
|
432
|
+
export interface ClipSelection {
|
|
433
|
+
/** Start, seconds. */
|
|
434
|
+
from?: number;
|
|
435
|
+
/** End, seconds. */
|
|
436
|
+
to?: number;
|
|
437
|
+
/** Keep only these chapters (titles or 1-based numbers), joined in the order given. */
|
|
438
|
+
chapters?: string[];
|
|
439
|
+
/** Render every chapter to its own file: `demo.mp4` → `demo-01-install.mp4`, … */
|
|
440
|
+
splitChapters?: boolean;
|
|
441
|
+
}
|
|
442
|
+
|
|
384
443
|
export interface RenderOptions {
|
|
385
444
|
/** Override resolved config values (theme, font, outputs, …) without re-recording. */
|
|
386
445
|
overrides?: Partial<VideoConfig>;
|
|
446
|
+
/** Render only part of the recording (by time or by chapter). */
|
|
447
|
+
clip?: ClipSelection;
|
|
387
448
|
onProgress?: (p: RenderProgress) => void;
|
|
388
449
|
}
|
|
389
450
|
|
package/src/video.ts
CHANGED
|
@@ -3,8 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { readCast, writeCast } from "./cast";
|
|
4
4
|
import { applyOverrides, resolveConfig } from "./config";
|
|
5
5
|
import { record } from "./recorder";
|
|
6
|
-
import {
|
|
7
|
-
import type { CastEvent, RecordOptions, Recording, RenderOptions, ResolvedConfig, Script, VideoConfig } from "./types";
|
|
6
|
+
import { renderSelection, type RenderResult } from "./render";
|
|
7
|
+
import type { CastEvent, ClipSelection, RecordOptions, Recording, RenderOptions, ResolvedConfig, Script, VideoConfig } from "./types";
|
|
8
8
|
|
|
9
9
|
export interface VideoRecordOptions extends RecordOptions {
|
|
10
10
|
/** Re-record even if a cached cast matches. */
|
|
@@ -69,8 +69,7 @@ export class Video {
|
|
|
69
69
|
if (!(await file.exists())) return undefined;
|
|
70
70
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
71
71
|
hasher.update(await file.arrayBuffer());
|
|
72
|
-
const subset
|
|
73
|
-
for (const key of RECORD_KEYS) subset[key] = this.config[key];
|
|
72
|
+
const subset = Object.fromEntries(RECORD_KEYS.map((key) => [key, this.config[key]]));
|
|
74
73
|
hasher.update(JSON.stringify(subset));
|
|
75
74
|
return hasher.digest("hex");
|
|
76
75
|
}
|
|
@@ -113,7 +112,7 @@ export class Video {
|
|
|
113
112
|
async render(recording?: Recording, opts: RenderOptions = {}): Promise<RenderResult> {
|
|
114
113
|
const rec = recording ?? (await readCast(this.config.cast));
|
|
115
114
|
const config = applyOverrides(this.config, opts.overrides);
|
|
116
|
-
return
|
|
115
|
+
return renderSelection(rec, config, opts.clip, opts.onProgress);
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
async run(opts: RunOptions = {}): Promise<RunResult> {
|
|
@@ -132,8 +131,9 @@ export function defineVideo(config: VideoConfig, script: Script): Video {
|
|
|
132
131
|
return new Video(config, script);
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
/** True for a `defineVideo()` result — checked by brand, so a Video built by another copy of tcut still counts. */
|
|
135
|
+
export function isVideo<T>(value: T): value is T & Video {
|
|
136
|
+
return value instanceof Object && "__bunVideo" in value && value.__bunVideo === true;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** Render an existing .cast file (from tcut or asciinema) with the given settings. */
|
|
@@ -141,11 +141,14 @@ export async function renderCast(
|
|
|
141
141
|
castFile: string,
|
|
142
142
|
overrides: Partial<VideoConfig> & { output?: string | string[] },
|
|
143
143
|
onProgress?: RenderOptions["onProgress"],
|
|
144
|
+
clip?: ClipSelection,
|
|
144
145
|
): Promise<RenderResult> {
|
|
145
146
|
const rec = await readCast(castFile);
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
const config = applyOverrides(castConfig(rec, castFile, overrides.output), overrides);
|
|
148
|
+
return renderSelection(rec, config, clip, onProgress);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The config a cast should be rendered with: the one recorded in its header, or sensible defaults for foreign casts. */
|
|
152
|
+
export function castConfig(rec: Recording, castFile: string, output?: string | string[]): ResolvedConfig {
|
|
153
|
+
return rec.header.bunVideo ?? resolveConfig({ output: output ?? castFile.replace(/\.cast$/, "") + ".mp4", cols: rec.header.width, rows: rec.header.height });
|
|
151
154
|
}
|