termcut 0.6.0 → 0.6.2
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/package.json +1 -1
- package/src/browser.ts +17 -4
- package/src/config.ts +5 -0
- package/src/keylabels.ts +29 -15
- package/src/renderer/page.ts +9 -4
- package/src/renderer/webview.ts +9 -3
- package/src/types.ts +10 -0
package/package.json
CHANGED
package/src/browser.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface BrowserCapture extends BrowserSession {
|
|
|
10
10
|
stop(): Promise<void>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** "better-t-stack.dev" → "https://better-t-stack.dev"; localhost defaults to http. */
|
|
14
|
+
export function normalizeUrl(url: string): string {
|
|
15
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url) || /^(about|data|file|blob):/i.test(url)) return url;
|
|
16
|
+
return /^(localhost|127\.|0\.0\.0\.0|\[::1\])/.test(url) ? `http://${url}` : `https://${url}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
const toRegExp = (pattern: RegExp | string): RegExp =>
|
|
14
20
|
typeof pattern === "string" ? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) : pattern;
|
|
15
21
|
|
|
@@ -59,21 +65,28 @@ export function startBrowserCapture(config: ResolvedConfig, stamp: () => number,
|
|
|
59
65
|
* or take a long first load (Vite pre-bundling, then a reload), so success is judged by the document's
|
|
60
66
|
* readyState at the target URL rather than by the navigate() promise alone.
|
|
61
67
|
*/
|
|
62
|
-
const goto = async (
|
|
68
|
+
const goto = async (rawUrl: string): Promise<void> => {
|
|
69
|
+
const url = normalizeUrl(rawUrl);
|
|
63
70
|
const deadline = performance.now() + config.waitTimeout;
|
|
64
71
|
const target = url.replace(/\/$/, "");
|
|
65
72
|
let navigation: Promise<"ok" | "pending" | "failed"> | null = null;
|
|
66
73
|
for (;;) {
|
|
67
|
-
|
|
74
|
+
if (!running) return; // stop() closed the view mid-retry; abort quietly
|
|
75
|
+
try {
|
|
76
|
+
navigation ??= view.navigate(url).then(
|
|
68
77
|
() => "ok" as const,
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
(err: unknown) => (/pending/i.test(String(err)) ? ("pending" as const) : ("failed" as const)),
|
|
79
|
+
);
|
|
80
|
+
} catch {
|
|
81
|
+
return; // navigate threw synchronously: the view is closed
|
|
82
|
+
}
|
|
71
83
|
const outcome = await Promise.race([navigation, Bun.sleep(750).then(() => "tick" as const)]);
|
|
72
84
|
if (outcome === "ok") {
|
|
73
85
|
currentUrl = url;
|
|
74
86
|
return;
|
|
75
87
|
}
|
|
76
88
|
if (outcome === "failed") navigation = null;
|
|
89
|
+
if (!running) return;
|
|
77
90
|
const state = await within(view.evaluate("document.readyState"), 3000, "goto").catch(() => "");
|
|
78
91
|
if (state === "complete" && (view.url ?? "").replace(/\/$/, "").startsWith(target)) {
|
|
79
92
|
currentUrl = url;
|
package/src/config.ts
CHANGED
|
@@ -68,6 +68,11 @@ export function resolveConfig(input: VideoConfig): ResolvedConfig {
|
|
|
68
68
|
position: (config.keys === true ? undefined : config.keys.position) ?? "bottom",
|
|
69
69
|
ttl: toMs(config.keys === true ? undefined : config.keys.ttl, 1200),
|
|
70
70
|
merge: toMs(config.keys === true ? undefined : config.keys.merge, 350),
|
|
71
|
+
limit: (config.keys === true ? undefined : config.keys.limit) ?? 1,
|
|
72
|
+
font: (config.keys === true ? undefined : config.keys.font) ?? Math.max(15, Math.round(font.size * 0.9)),
|
|
73
|
+
color: (config.keys === true ? undefined : config.keys.color) ?? "#fff",
|
|
74
|
+
background: (config.keys === true ? undefined : config.keys.background) ?? "rgba(15, 15, 20, 0.85)",
|
|
75
|
+
radius: (config.keys === true ? undefined : config.keys.radius) ?? 8,
|
|
71
76
|
},
|
|
72
77
|
}),
|
|
73
78
|
fps: config.fps ?? 60,
|
package/src/keylabels.ts
CHANGED
|
@@ -59,23 +59,37 @@ export interface KeyChip {
|
|
|
59
59
|
* Turn timed input events into chips. Printable keystrokes within `mergeWithin` seconds of each other are merged
|
|
60
60
|
* into one chip (so typing reads as words, not a flood of letters); named keys always get their own chip.
|
|
61
61
|
*/
|
|
62
|
-
export
|
|
62
|
+
export interface ChipBuilder {
|
|
63
|
+
chips: KeyChip[];
|
|
64
|
+
push(vt: number, data: string): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Incremental version of keyChips: feed events as the clock passes them and a word chip grows while it is typed. */
|
|
68
|
+
export function chipBuilder(mergeWithin = 0.35): ChipBuilder {
|
|
63
69
|
const chips: KeyChip[] = [];
|
|
64
70
|
let lastPrintable: KeyChip | null = null;
|
|
65
71
|
let lastTime = -Infinity;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
lastPrintable
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
return {
|
|
73
|
+
chips,
|
|
74
|
+
push(vt, data) {
|
|
75
|
+
for (const label of keyLabels(data)) {
|
|
76
|
+
const printable = !/^[⏎⇥⌫␣↑↓→←⌃⌥⇧]|^(esc|home|end|del|pgup|pgdn|wheel|mouse)/.test(label);
|
|
77
|
+
if (printable && lastPrintable && vt - lastTime <= mergeWithin) {
|
|
78
|
+
lastPrintable.label += label;
|
|
79
|
+
lastPrintable.at = vt;
|
|
80
|
+
} else {
|
|
81
|
+
const chip = { at: vt, label };
|
|
82
|
+
chips.push(chip);
|
|
83
|
+
lastPrintable = printable ? chip : null;
|
|
84
|
+
}
|
|
85
|
+
lastTime = vt;
|
|
76
86
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function keyChips(inputs: Array<{ vt: number; data: string }>, mergeWithin = 0.35): KeyChip[] {
|
|
92
|
+
const builder = chipBuilder(mergeWithin);
|
|
93
|
+
for (const { vt, data } of inputs) builder.push(vt, data);
|
|
94
|
+
return builder.chips;
|
|
81
95
|
}
|
package/src/renderer/page.ts
CHANGED
|
@@ -146,12 +146,17 @@ export function renderHtml(config: ResolvedConfig): string {
|
|
|
146
146
|
display: ${config.keys ? "flex" : "none"}; justify-content: center; gap: 6px; pointer-events: none; z-index: 5;
|
|
147
147
|
}
|
|
148
148
|
#keys span {
|
|
149
|
-
font: 600 ${
|
|
150
|
-
color: #fff; background:
|
|
151
|
-
border
|
|
149
|
+
font: 600 ${config.keys?.font ?? 15}px ${font.family};
|
|
150
|
+
color: ${config.keys?.color ?? "#fff"}; background: ${config.keys?.background ?? "rgba(15, 15, 20, 0.85)"};
|
|
151
|
+
border: 1px solid rgba(255,255,255,0.12); border-radius: ${config.keys?.radius ?? 8}px;
|
|
152
|
+
padding: 0.4em 0.9em; letter-spacing: 0.02em; white-space: pre;
|
|
153
|
+
box-shadow: 0 6px 18px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.08);
|
|
152
154
|
}
|
|
153
|
-
/* Zoom: the terminal grid is scaled inside its frame; the renderer sets the transform per frame.
|
|
155
|
+
/* Zoom: the terminal grid is scaled inside its frame; the renderer sets the transform per frame.
|
|
156
|
+
The frame clips it so magnified content never spills over the bar or the rounded corners. */
|
|
154
157
|
#zoom { transform-origin: 0 0; will-change: transform; }
|
|
158
|
+
#frame { overflow: hidden; }
|
|
159
|
+
#frame > *:not(#zoom) { position: relative; z-index: 2; }
|
|
155
160
|
</style>
|
|
156
161
|
<style id="blink"></style>
|
|
157
162
|
</head>
|
package/src/renderer/webview.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { fitFrame, loopOffsetFrames, rotateFrames } from "../loop";
|
|
|
6
6
|
import { buildTimeline, withReinjection, type TimedEvent } from "../timeline";
|
|
7
7
|
import { pageAssets } from "./bundle";
|
|
8
8
|
import { createSinks, type Chapter } from "./encoder";
|
|
9
|
-
import {
|
|
9
|
+
import { chipBuilder } from "../keylabels";
|
|
10
10
|
import { BROWSER_GAP, barHeight, renderHtml, themeOsc } from "./page";
|
|
11
11
|
|
|
12
12
|
export interface RenderResult {
|
|
@@ -97,7 +97,9 @@ export async function render(
|
|
|
97
97
|
const lite = config.core === "lite";
|
|
98
98
|
const events = lite ? timeline.events : withReinjection(timeline.events, osc);
|
|
99
99
|
// Key overlay chips come from input events; zoom/chapter markers are read as the clock passes them.
|
|
100
|
-
const
|
|
100
|
+
const keyEvents = config.keys ? events.filter((e) => e.type === "i") : [];
|
|
101
|
+
const chipper = chipBuilder((config.keys?.merge ?? 350) / 1000);
|
|
102
|
+
let keyIdx = 0;
|
|
101
103
|
const chapters: Chapter[] = events
|
|
102
104
|
.filter((e) => e.type === "m" && e.data.startsWith(MARKER.chapter))
|
|
103
105
|
.map((e) => ({ title: e.data.slice(MARKER.chapter.length), start: e.vt }));
|
|
@@ -232,7 +234,11 @@ export async function render(
|
|
|
232
234
|
let chipsChanged = false;
|
|
233
235
|
if (config.keys) {
|
|
234
236
|
const ttl = config.keys.ttl / 1000;
|
|
235
|
-
|
|
237
|
+
while (keyIdx < keyEvents.length && keyEvents[keyIdx]!.vt <= time + 1e-9) {
|
|
238
|
+
chipper.push(keyEvents[keyIdx]!.vt, keyEvents[keyIdx]!.data);
|
|
239
|
+
keyIdx += 1;
|
|
240
|
+
}
|
|
241
|
+
const visible = chipper.chips.filter((c) => time - c.at < ttl).slice(-config.keys.limit).map((c) => c.label);
|
|
236
242
|
const key = visible.join("\u0000");
|
|
237
243
|
if (key !== lastChips) {
|
|
238
244
|
await view.evaluate(`window.__vt.keys(${JSON.stringify(visible)})`);
|
package/src/types.ts
CHANGED
|
@@ -54,6 +54,16 @@ export interface KeysConfig {
|
|
|
54
54
|
ttl?: Duration;
|
|
55
55
|
/** Printable keys pressed within this window merge into one chip. Default "350ms". */
|
|
56
56
|
merge?: Duration;
|
|
57
|
+
/** How many chips are visible at once. Default 1 (the latest press replaces the previous). */
|
|
58
|
+
limit?: number;
|
|
59
|
+
/** Chip font size in px. Default ~0.9× the terminal font. */
|
|
60
|
+
font?: number;
|
|
61
|
+
/** Chip text color. Default "#fff". */
|
|
62
|
+
color?: string;
|
|
63
|
+
/** Chip background (any CSS color). Default "rgba(15, 15, 20, 0.85)". */
|
|
64
|
+
background?: string;
|
|
65
|
+
/** Chip corner radius in px. Default 8. */
|
|
66
|
+
radius?: number;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/** A region of the terminal grid to magnify. */
|