@sanohiro/casty 0.5.6 → 0.5.8
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/bin/casty.js +59 -7
- package/lib/kitty.js +30 -5
- package/package.json +1 -1
package/bin/casty.js
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
// casty - TTY web browser using raw CDP and Kitty graphics protocol
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { dirname, join } from 'node:path';
|
|
7
8
|
|
|
9
|
+
// --version / -v
|
|
10
|
+
if (process.argv[2] === '--version' || process.argv[2] === '-v') {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
12
|
+
console.log(`casty ${pkg.version}`);
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
// Ensure Chrome is installed (skip if launched from bin/casty shell script)
|
|
9
17
|
if (!process.env.CASTY_ENSURE_CHROME) {
|
|
10
18
|
const __bin = dirname(fileURLToPath(import.meta.url));
|
|
@@ -19,7 +27,7 @@ if (!process.env.CASTY_ENSURE_CHROME) {
|
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
import { startBrowser, setupPage, startScreencast, stopScreencast } from '../lib/browser.js';
|
|
22
|
-
import { sendFrame, resetFrameCache, clearScreen, hideCursor, showCursor, cleanup as cleanupTmp, transport } from '../lib/kitty.js';
|
|
30
|
+
import { sendFrame, resetFrameCache, clearScreen, hideCursor, showCursor, cleanup as cleanupTmp, transport, setDisplaySize, disableDedup } from '../lib/kitty.js';
|
|
23
31
|
import { enableMouse, disableMouse, startInputHandling } from '../lib/input.js';
|
|
24
32
|
import { loadKeyBindings } from '../lib/keys.js';
|
|
25
33
|
import { loadConfig } from '../lib/config.js';
|
|
@@ -89,10 +97,14 @@ async function getTermInfo({ keepAlive = false } = {}) {
|
|
|
89
97
|
|
|
90
98
|
const pixelSize = await queryTermPixelSize({ keepAlive });
|
|
91
99
|
if (pixelSize) {
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
// Align to cell boundaries: floor cell size, then multiply back
|
|
101
|
+
// This ensures image pixels == display pixels (no GPU interpolation blur)
|
|
102
|
+
const cellWidth = Math.floor(pixelSize.width / cols);
|
|
103
|
+
const cellHeight = Math.floor(pixelSize.height / rows);
|
|
104
|
+
const width = cellWidth * cols;
|
|
105
|
+
const height = cellHeight * rows;
|
|
94
106
|
const zoom = calcZoom(cellWidth);
|
|
95
|
-
return { cols, rows, width
|
|
107
|
+
return { cols, rows, width, height, cellWidth, cellHeight, zoom };
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
const cellWidth = parseInt(process.env.CASTY_CELL_WIDTH) || 10;
|
|
@@ -114,8 +126,9 @@ async function main() {
|
|
|
114
126
|
const browser = await browserP;
|
|
115
127
|
|
|
116
128
|
// Reserve line 1 for URL bar, use the rest for browser display
|
|
117
|
-
const barHeight =
|
|
129
|
+
const barHeight = term.cellHeight;
|
|
118
130
|
const viewHeight = term.height - barHeight;
|
|
131
|
+
setDisplaySize(term.cols, term.rows - 1);
|
|
119
132
|
|
|
120
133
|
// Phase 2: CDP connection + page setup
|
|
121
134
|
const { client, cssWidth, cssHeight } = await setupPage(browser, { ...term, height: viewHeight });
|
|
@@ -215,25 +228,64 @@ async function main() {
|
|
|
215
228
|
clearTimeout(resizeTimer);
|
|
216
229
|
resizeTimer = setTimeout(handleResize, 150);
|
|
217
230
|
});
|
|
231
|
+
// Direct screenshot — bypasses screencast's capturing flag
|
|
232
|
+
const screenshotOpts = { format: screenshotFormat, optimizeForSpeed: true, captureBeyondViewport: false };
|
|
233
|
+
if (screenshotFormat === 'jpeg') screenshotOpts.quality = 85;
|
|
234
|
+
async function directCapture() {
|
|
235
|
+
try {
|
|
236
|
+
const { data } = await client.send('Page.captureScreenshot', screenshotOpts);
|
|
237
|
+
if (data) onFrame(data);
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
|
|
218
241
|
async function handleResize() {
|
|
219
242
|
if (resizing) { pendingResize = true; return; }
|
|
220
243
|
resizing = true;
|
|
221
244
|
try {
|
|
245
|
+
// Stop old screencast FIRST to prevent stale frames
|
|
246
|
+
await stopScreencast(client, screencastCleanup);
|
|
247
|
+
|
|
222
248
|
const t = await getTermInfo({ keepAlive: true });
|
|
223
|
-
const vh = t.height -
|
|
249
|
+
const vh = t.height - t.cellHeight;
|
|
224
250
|
const cw = Math.round(t.width / t.zoom);
|
|
225
251
|
const ch = Math.round(vh / t.zoom);
|
|
252
|
+
setDisplaySize(t.cols, t.rows - 1);
|
|
226
253
|
console.error(`casty: resize ${cw}x${ch} (dev:${t.width}x${vh}) zoom:${t.zoom.toFixed(2)}`);
|
|
227
254
|
|
|
228
255
|
urlBar.updateCellSize(t.cellWidth, t.cellHeight);
|
|
229
256
|
clearScreen();
|
|
230
257
|
resetFrameCache();
|
|
258
|
+
disableDedup(3000); // Force re-send for 3s (bcon may not display first frame)
|
|
231
259
|
|
|
232
|
-
await stopScreencast(client, screencastCleanup);
|
|
233
260
|
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
234
261
|
width: cw, height: ch, deviceScaleFactor: t.zoom, mobile: false,
|
|
235
262
|
});
|
|
236
263
|
|
|
264
|
+
// Wait for Chrome to finish re-rendering by watching for a screencast frame
|
|
265
|
+
await new Promise(resolve => {
|
|
266
|
+
const onFirstFrame = ({ sessionId }) => {
|
|
267
|
+
client.send('Page.screencastFrameAck', { sessionId }).catch(() => {});
|
|
268
|
+
client.removeListener('Page.screencastFrame', onFirstFrame);
|
|
269
|
+
resolve();
|
|
270
|
+
};
|
|
271
|
+
client.on('Page.screencastFrame', onFirstFrame);
|
|
272
|
+
client.send('Page.startScreencast', {
|
|
273
|
+
format: 'jpeg', quality: 10,
|
|
274
|
+
maxWidth: Math.round(cw / 4), maxHeight: Math.round(ch / 4),
|
|
275
|
+
everyNthFrame: 1,
|
|
276
|
+
}).catch(() => resolve());
|
|
277
|
+
setTimeout(() => {
|
|
278
|
+
client.removeListener('Page.screencastFrame', onFirstFrame);
|
|
279
|
+
resolve();
|
|
280
|
+
}, 2000);
|
|
281
|
+
});
|
|
282
|
+
await client.send('Page.stopScreencast').catch(() => {});
|
|
283
|
+
|
|
284
|
+
// Capture hi-res frame (Chrome has finished rendering)
|
|
285
|
+
await directCapture();
|
|
286
|
+
urlBar.render();
|
|
287
|
+
|
|
288
|
+
// Restart screencast for ongoing change detection
|
|
237
289
|
({ forceCapture, cleanup: screencastCleanup } = await startScreencast(client, {
|
|
238
290
|
width: cw,
|
|
239
291
|
height: ch,
|
package/lib/kitty.js
CHANGED
|
@@ -29,6 +29,16 @@ import { loadConfig } from './config.js';
|
|
|
29
29
|
const tmpFile = join(tmpdir(), `casty-frame-${process.pid}.png`);
|
|
30
30
|
const tmpPathB64 = Buffer.from(tmpFile).toString('base64');
|
|
31
31
|
|
|
32
|
+
// Display size in cells (set by caller, used for c=/r= parameters)
|
|
33
|
+
let _cols = 0;
|
|
34
|
+
let _rows = 0;
|
|
35
|
+
|
|
36
|
+
// Set display size (cols = terminal columns, rows = display rows excluding URL bar)
|
|
37
|
+
export function setDisplaySize(cols, rows) {
|
|
38
|
+
_cols = cols;
|
|
39
|
+
_rows = rows;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
// Detect transfer mode
|
|
33
43
|
function detectTransport() {
|
|
34
44
|
const config = loadConfig();
|
|
@@ -74,24 +84,39 @@ export function cleanup() {
|
|
|
74
84
|
|
|
75
85
|
// Frame deduplication — skip identical consecutive frames
|
|
76
86
|
let lastFrameData = '';
|
|
87
|
+
let _dedupDisabled = false;
|
|
88
|
+
let _dedupTimer = null;
|
|
89
|
+
|
|
90
|
+
// Temporarily disable dedup (e.g. after resize, bcon needs re-send)
|
|
91
|
+
export function disableDedup(ms = 3000) {
|
|
92
|
+
_dedupDisabled = true;
|
|
93
|
+
clearTimeout(_dedupTimer);
|
|
94
|
+
_dedupTimer = setTimeout(() => { _dedupDisabled = false; }, ms);
|
|
95
|
+
}
|
|
77
96
|
|
|
78
97
|
// File transfer mode (fast: sends only path)
|
|
79
98
|
// Prepends cursor-home to batch into a single write
|
|
80
99
|
function sendFrameFile(base64Data) {
|
|
81
|
-
if (base64Data.length === lastFrameData.length && base64Data === lastFrameData)
|
|
100
|
+
if (!_dedupDisabled && base64Data.length === lastFrameData.length && base64Data === lastFrameData) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
82
103
|
lastFrameData = base64Data;
|
|
83
104
|
writeFileSync(tmpFile, Buffer.from(base64Data, 'base64'));
|
|
84
|
-
|
|
105
|
+
const crFile = _cols && _rows ? `,c=${_cols},r=${_rows}` : '';
|
|
106
|
+
process.stdout.write(`${CURSOR_HOME}\x1b_Ga=T,f=100,t=f,q=2,C=1,i=1${crFile};${tmpPathB64}\x1b\\`);
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
// Inline mode (4096B chunked, PNG only)
|
|
88
110
|
// Prepends cursor-home and batches all chunks into a single stdout.write
|
|
89
111
|
function sendFrameInline(pngBase64) {
|
|
90
|
-
if (pngBase64.length === lastFrameData.length && pngBase64 === lastFrameData)
|
|
112
|
+
if (!_dedupDisabled && pngBase64.length === lastFrameData.length && pngBase64 === lastFrameData) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
91
115
|
lastFrameData = pngBase64;
|
|
92
116
|
const CHUNK = 4096;
|
|
117
|
+
const crInline = _cols && _rows ? `,c=${_cols},r=${_rows}` : '';
|
|
93
118
|
if (pngBase64.length <= CHUNK) {
|
|
94
|
-
process.stdout.write(`${CURSOR_HOME}\x1b_Ga=T,f=100,q=2,C=1,i=1;${pngBase64}\x1b\\`);
|
|
119
|
+
process.stdout.write(`${CURSOR_HOME}\x1b_Ga=T,f=100,q=2,C=1,i=1${crInline};${pngBase64}\x1b\\`);
|
|
95
120
|
return;
|
|
96
121
|
}
|
|
97
122
|
const parts = [CURSOR_HOME];
|
|
@@ -100,7 +125,7 @@ function sendFrameInline(pngBase64) {
|
|
|
100
125
|
const chunk = pngBase64.slice(i, i + CHUNK);
|
|
101
126
|
const more = i + CHUNK < pngBase64.length ? 1 : 0;
|
|
102
127
|
if (i === 0) {
|
|
103
|
-
parts.push(`\x1b_Ga=T,f=100,q=2,C=1,i=1,m=${more};${chunk}\x1b\\`);
|
|
128
|
+
parts.push(`\x1b_Ga=T,f=100,q=2,C=1,i=1${crInline},m=${more};${chunk}\x1b\\`);
|
|
104
129
|
} else {
|
|
105
130
|
parts.push(`\x1b_Gm=${more};${chunk}\x1b\\`);
|
|
106
131
|
}
|