@sanohiro/casty 0.5.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.ja.md +178 -0
- package/README.md +178 -0
- package/bin/casty +200 -0
- package/bin/casty.js +245 -0
- package/lib/bookmarks.js +32 -0
- package/lib/browser.js +305 -0
- package/lib/cdp.js +76 -0
- package/lib/chrome.js +155 -0
- package/lib/config.js +43 -0
- package/lib/hints.js +255 -0
- package/lib/input.js +545 -0
- package/lib/keys.js +51 -0
- package/lib/kitty.js +117 -0
- package/lib/urlbar.js +348 -0
- package/package.json +41 -0
package/lib/kitty.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Kitty graphics protocol output
|
|
2
|
+
//
|
|
3
|
+
// Protocol parameters:
|
|
4
|
+
// a=T : transmit and display
|
|
5
|
+
// a=d : delete image(s)
|
|
6
|
+
// f=100: PNG format
|
|
7
|
+
// t=f : file transfer (send file path as base64)
|
|
8
|
+
// t=d : inline transfer (send image data as base64)
|
|
9
|
+
// q=2 : suppress response (no OK/ERR from terminal)
|
|
10
|
+
// C=1 : no cursor movement (keep cursor position after display)
|
|
11
|
+
// i=N : image ID (replace existing image with same ID)
|
|
12
|
+
// m=0/1: chunk continuation (1=more chunks follow, 0=final chunk)
|
|
13
|
+
// d=A : delete all images (used with a=d)
|
|
14
|
+
//
|
|
15
|
+
// Two transfer modes:
|
|
16
|
+
// File transfer (t=f): fast, sends only the path (bcon, etc.)
|
|
17
|
+
// Inline (t=d): sends base64 data directly in 4096B chunks (Ghostty, kitty)
|
|
18
|
+
//
|
|
19
|
+
// ~/.casty/config.json transport setting:
|
|
20
|
+
// "auto" → file transfer for bcon, inline for others
|
|
21
|
+
// "file" → force file transfer
|
|
22
|
+
// "inline" → force inline
|
|
23
|
+
|
|
24
|
+
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { tmpdir } from 'node:os';
|
|
27
|
+
import { loadConfig } from './config.js';
|
|
28
|
+
|
|
29
|
+
const tmpFile = join(tmpdir(), `casty-frame-${process.pid}.png`);
|
|
30
|
+
const tmpPathB64 = Buffer.from(tmpFile).toString('base64');
|
|
31
|
+
|
|
32
|
+
// Detect transfer mode
|
|
33
|
+
function detectTransport() {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
const setting = config.transport || 'auto';
|
|
36
|
+
|
|
37
|
+
if (setting === 'file') return 'file';
|
|
38
|
+
if (setting === 'inline') return 'inline';
|
|
39
|
+
|
|
40
|
+
// auto: bcon supports t=f
|
|
41
|
+
const termProg = process.env.TERM_PROGRAM || '';
|
|
42
|
+
if (/bcon/i.test(termProg)) return 'file';
|
|
43
|
+
|
|
44
|
+
return 'inline';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const transport = detectTransport();
|
|
48
|
+
|
|
49
|
+
// Cursor to line 2 (line 1 is reserved for URL bar)
|
|
50
|
+
const CURSOR_HOME = '\x1b[2;1H';
|
|
51
|
+
export function cursorHome() {
|
|
52
|
+
process.stdout.write(CURSOR_HOME);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Clear screen (also delete all Kitty images)
|
|
56
|
+
export function clearScreen() {
|
|
57
|
+
process.stdout.write('\x1b_Ga=d,d=A,q=2;\x1b\\\x1b[2J\x1b[H');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Hide cursor
|
|
61
|
+
export function hideCursor() {
|
|
62
|
+
process.stdout.write('\x1b[?25l');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Show cursor
|
|
66
|
+
export function showCursor() {
|
|
67
|
+
process.stdout.write('\x1b[?25h');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clean up temp file
|
|
71
|
+
export function cleanup() {
|
|
72
|
+
try { unlinkSync(tmpFile); } catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Frame deduplication — skip identical consecutive frames
|
|
76
|
+
let lastFrameData = '';
|
|
77
|
+
|
|
78
|
+
// File transfer mode (fast: sends only path)
|
|
79
|
+
// Prepends cursor-home to batch into a single write
|
|
80
|
+
function sendFrameFile(base64Data) {
|
|
81
|
+
if (base64Data.length === lastFrameData.length && base64Data === lastFrameData) return;
|
|
82
|
+
lastFrameData = base64Data;
|
|
83
|
+
writeFileSync(tmpFile, Buffer.from(base64Data, 'base64'));
|
|
84
|
+
process.stdout.write(`${CURSOR_HOME}\x1b_Ga=T,f=100,t=f,q=2,C=1,i=1;${tmpPathB64}\x1b\\`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Inline mode (4096B chunked, PNG only)
|
|
88
|
+
// Prepends cursor-home and batches all chunks into a single stdout.write
|
|
89
|
+
function sendFrameInline(pngBase64) {
|
|
90
|
+
if (pngBase64.length === lastFrameData.length && pngBase64 === lastFrameData) return;
|
|
91
|
+
lastFrameData = pngBase64;
|
|
92
|
+
const CHUNK = 4096;
|
|
93
|
+
if (pngBase64.length <= CHUNK) {
|
|
94
|
+
process.stdout.write(`${CURSOR_HOME}\x1b_Ga=T,f=100,q=2,C=1,i=1;${pngBase64}\x1b\\`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const parts = [CURSOR_HOME];
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < pngBase64.length) {
|
|
100
|
+
const chunk = pngBase64.slice(i, i + CHUNK);
|
|
101
|
+
const more = i + CHUNK < pngBase64.length ? 1 : 0;
|
|
102
|
+
if (i === 0) {
|
|
103
|
+
parts.push(`\x1b_Ga=T,f=100,q=2,C=1,i=1,m=${more};${chunk}\x1b\\`);
|
|
104
|
+
} else {
|
|
105
|
+
parts.push(`\x1b_Gm=${more};${chunk}\x1b\\`);
|
|
106
|
+
}
|
|
107
|
+
i += CHUNK;
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(parts.join(''));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Reset dedup state (e.g. after resize)
|
|
113
|
+
export function resetFrameCache() {
|
|
114
|
+
lastFrameData = '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const sendFrame = transport === 'file' ? sendFrameFile : sendFrameInline;
|
package/lib/urlbar.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// Address bar / search bar (always visible)
|
|
2
|
+
// Line 1 of the terminal always shows the current URL
|
|
3
|
+
// Alt+L to enter edit mode → Enter to navigate, Escape to cancel
|
|
4
|
+
|
|
5
|
+
import { showCursor, hideCursor } from './kitty.js';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { searchBookmarks } from './bookmarks.js';
|
|
8
|
+
|
|
9
|
+
function isURL(str) {
|
|
10
|
+
return /^https?:\/\//.test(str) || /^[a-zA-Z0-9-]+(\.[a-zA-Z]{2,})(\/|$)/.test(str);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toURL(input) {
|
|
14
|
+
if (/^https?:\/\//.test(input)) return input;
|
|
15
|
+
if (isURL(input)) return 'https://' + input;
|
|
16
|
+
|
|
17
|
+
// /b [query] → bookmark search
|
|
18
|
+
const bm = input.match(/^\/b(?:\s+(.+))?$/);
|
|
19
|
+
if (bm) {
|
|
20
|
+
const results = searchBookmarks(bm[1] || '');
|
|
21
|
+
if (results.length > 0) return results[0].url;
|
|
22
|
+
return null; // No match
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
return config.searchUrl + encodeURIComponent(input);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// East Asian Width (UAX #11) full-width detection
|
|
30
|
+
// Character display width (full-width=2, half-width=1)
|
|
31
|
+
function charWidth(cp) {
|
|
32
|
+
if (
|
|
33
|
+
(cp >= 0x1100 && cp <= 0x115F) || // Hangul Jamo
|
|
34
|
+
(cp >= 0x2E80 && cp <= 0x303E) || // CJK Radicals Supplement, Symbols
|
|
35
|
+
(cp >= 0x3040 && cp <= 0x33BF) || // Hiragana, Katakana, CJK Compatibility
|
|
36
|
+
(cp >= 0x3400 && cp <= 0x4DBF) || // CJK Unified Ideographs Extension A
|
|
37
|
+
(cp >= 0x4E00 && cp <= 0xA4CF) || // CJK Unified Ideographs, Yi Syllables
|
|
38
|
+
(cp >= 0xAC00 && cp <= 0xD7FF) || // Hangul Syllables
|
|
39
|
+
(cp >= 0xF900 && cp <= 0xFAFF) || // CJK Compatibility Ideographs
|
|
40
|
+
(cp >= 0xFE30 && cp <= 0xFE6F) || // CJK Compatibility Forms, Small Forms
|
|
41
|
+
(cp >= 0xFF01 && cp <= 0xFF60) || // Fullwidth Latin, Symbols
|
|
42
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) || // Fullwidth Currency, Symbols
|
|
43
|
+
(cp >= 0x20000 && cp <= 0x2FA1F) // CJK Extensions B-F, Compatibility Supplement
|
|
44
|
+
) return 2;
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function strWidth(str) {
|
|
49
|
+
let w = 0;
|
|
50
|
+
for (const ch of str) w += charWidth(ch.codePointAt(0));
|
|
51
|
+
return w;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Truncate by display width (keep rightmost maxW columns)
|
|
55
|
+
function sliceByWidth(str, maxW) {
|
|
56
|
+
const chars = [...str];
|
|
57
|
+
let w = 0;
|
|
58
|
+
let start = chars.length;
|
|
59
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
60
|
+
const cw = charWidth(chars[i].codePointAt(0));
|
|
61
|
+
if (w + cw > maxW) break;
|
|
62
|
+
w += cw;
|
|
63
|
+
start = i;
|
|
64
|
+
}
|
|
65
|
+
return { text: chars.slice(start).join(''), width: w };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// padEnd by display width
|
|
69
|
+
function padEndByWidth(str, totalW) {
|
|
70
|
+
const w = strWidth(str);
|
|
71
|
+
return w >= totalW ? str : str + ' '.repeat(totalW - w);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
75
|
+
|
|
76
|
+
export class UrlBar {
|
|
77
|
+
constructor() {
|
|
78
|
+
this.currentUrl = '';
|
|
79
|
+
this.editing = false;
|
|
80
|
+
this.loading = false;
|
|
81
|
+
this._spinIdx = 0;
|
|
82
|
+
this._status = null;
|
|
83
|
+
this.text = '';
|
|
84
|
+
this.cursor = 0;
|
|
85
|
+
this.selectAll = false; // Select-all state
|
|
86
|
+
this._resolve = null;
|
|
87
|
+
this._dirty = true; // Needs re-render
|
|
88
|
+
this._spinTimer = null; // Spinner update interval
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Update current URL and re-render (if not editing)
|
|
92
|
+
setUrl(url) {
|
|
93
|
+
this.currentUrl = url;
|
|
94
|
+
this._dirty = true;
|
|
95
|
+
if (!this.editing) this.render();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Status message (downloads, etc.)
|
|
99
|
+
setStatus(msg) { this._status = msg; this._dirty = true; }
|
|
100
|
+
clearStatus() { this._status = null; this._dirty = true; }
|
|
101
|
+
|
|
102
|
+
// Render only if content changed (for frame callback)
|
|
103
|
+
renderIfDirty() {
|
|
104
|
+
if (!this._dirty) return;
|
|
105
|
+
this.render();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Render on line 1
|
|
109
|
+
render() {
|
|
110
|
+
this._dirty = false;
|
|
111
|
+
this._updateSpinTimer();
|
|
112
|
+
const cols = process.stdout.columns || 80;
|
|
113
|
+
const line = this.editing ? this._editLine(cols) : this._displayLine(cols);
|
|
114
|
+
process.stdout.write(`\x1b[1;1H${line}\x1b[0m`);
|
|
115
|
+
if (this.editing) {
|
|
116
|
+
const prefix = ' > ';
|
|
117
|
+
const textBeforeCursor = [...this.text].slice(0, this.cursor).join('');
|
|
118
|
+
const cursorCol = prefix.length + strWidth(textBeforeCursor) + 1;
|
|
119
|
+
process.stdout.write(`\x1b[1;${cursorCol}H`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Start/stop spinner timer (100ms interval instead of every frame)
|
|
124
|
+
_updateSpinTimer() {
|
|
125
|
+
if (this.loading && !this._spinTimer) {
|
|
126
|
+
this._spinTimer = setInterval(() => { this._dirty = true; }, 100);
|
|
127
|
+
} else if (!this.loading && this._spinTimer) {
|
|
128
|
+
clearInterval(this._spinTimer);
|
|
129
|
+
this._spinTimer = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_displayLine(cols) {
|
|
134
|
+
let prefix;
|
|
135
|
+
if (this.loading) {
|
|
136
|
+
prefix = ' ' + SPINNER[this._spinIdx++ % SPINNER.length] + ' ';
|
|
137
|
+
} else {
|
|
138
|
+
prefix = ' ';
|
|
139
|
+
}
|
|
140
|
+
const content = this._status || this.currentUrl;
|
|
141
|
+
const full = prefix + content;
|
|
142
|
+
return `\x1b[38;5;250m\x1b[48;5;236m${padEndByWidth(full, cols)}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_editLine(cols) {
|
|
146
|
+
const prefix = ' > ';
|
|
147
|
+
const maxW = cols - prefix.length;
|
|
148
|
+
const tw = strWidth(this.text);
|
|
149
|
+
const display = tw > maxW ? sliceByWidth(this.text, maxW).text : this.text;
|
|
150
|
+
const dw = strWidth(display);
|
|
151
|
+
const pad = Math.max(0, maxW - dw);
|
|
152
|
+
if (this.selectAll) {
|
|
153
|
+
return `\x1b[48;5;24m\x1b[97m${prefix}\x1b[7m${display}\x1b[27m${' '.repeat(pad)}`;
|
|
154
|
+
}
|
|
155
|
+
return `\x1b[97m\x1b[48;5;24m${prefix}${display}${' '.repeat(pad)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Start editing mode (returns URL via Promise)
|
|
159
|
+
startEditing() {
|
|
160
|
+
this.editing = true;
|
|
161
|
+
this.selectAll = true;
|
|
162
|
+
this.text = this.currentUrl;
|
|
163
|
+
this.cursor = [...this.text].length;
|
|
164
|
+
showCursor();
|
|
165
|
+
this.render();
|
|
166
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
167
|
+
this._resolve = resolve;
|
|
168
|
+
return promise;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// End editing mode
|
|
172
|
+
_finishEditing(result) {
|
|
173
|
+
this.editing = false;
|
|
174
|
+
hideCursor();
|
|
175
|
+
|
|
176
|
+
let url = null;
|
|
177
|
+
if (result) {
|
|
178
|
+
url = toURL(result);
|
|
179
|
+
if (url === null) {
|
|
180
|
+
// Bookmark not found
|
|
181
|
+
this.setStatus('Bookmark not found');
|
|
182
|
+
setTimeout(() => this.clearStatus(), 2000);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.render();
|
|
187
|
+
if (this._resolve) {
|
|
188
|
+
this._resolve(url);
|
|
189
|
+
this._resolve = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Clear selection
|
|
194
|
+
_deselect() { this.selectAll = false; }
|
|
195
|
+
|
|
196
|
+
// If selected, replace all on input/delete
|
|
197
|
+
_clearIfSelected() {
|
|
198
|
+
if (this.selectAll) {
|
|
199
|
+
this.text = '';
|
|
200
|
+
this.cursor = 0;
|
|
201
|
+
this.selectAll = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Insert text (for paste)
|
|
206
|
+
insertText(str) {
|
|
207
|
+
if (!this.editing) return;
|
|
208
|
+
this._clearIfSelected();
|
|
209
|
+
const chars = [...this.text];
|
|
210
|
+
const input = [...str];
|
|
211
|
+
this.text = chars.slice(0, this.cursor).join('') + str + chars.slice(this.cursor).join('');
|
|
212
|
+
this.cursor += input.length;
|
|
213
|
+
this.render();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Handle key input during editing (returns true if consumed)
|
|
217
|
+
handleInput(str) {
|
|
218
|
+
if (!this.editing) return false;
|
|
219
|
+
|
|
220
|
+
// Enter → confirm
|
|
221
|
+
if (str === '\r' || str === '\n') {
|
|
222
|
+
this._finishEditing(this.text.trim() || null);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Escape → cancel
|
|
227
|
+
if (str === '\x1b') {
|
|
228
|
+
this._finishEditing(null);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Ctrl+C → cancel
|
|
233
|
+
if (str === '\x03') {
|
|
234
|
+
this._finishEditing(null);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Ctrl+U → clear all
|
|
239
|
+
if (str === '\x15') {
|
|
240
|
+
this.text = '';
|
|
241
|
+
this.cursor = 0;
|
|
242
|
+
this._deselect();
|
|
243
|
+
this.render();
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Ctrl+A → select all
|
|
248
|
+
if (str === '\x01') {
|
|
249
|
+
this.selectAll = true;
|
|
250
|
+
this.cursor = [...this.text].length;
|
|
251
|
+
this.render();
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Ctrl+E → move to end
|
|
256
|
+
if (str === '\x05') {
|
|
257
|
+
this._deselect();
|
|
258
|
+
this.cursor = [...this.text].length;
|
|
259
|
+
this.render();
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Ctrl+W → delete word
|
|
264
|
+
if (str === '\x17') {
|
|
265
|
+
this._clearIfSelected();
|
|
266
|
+
const chars = [...this.text];
|
|
267
|
+
const before = chars.slice(0, this.cursor).join('');
|
|
268
|
+
const after = chars.slice(this.cursor).join('');
|
|
269
|
+
const trimmed = before.replace(/\S+\s*$/, '');
|
|
270
|
+
this.text = trimmed + after;
|
|
271
|
+
this.cursor = [...trimmed].length;
|
|
272
|
+
this.render();
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Backspace
|
|
277
|
+
if (str === '\x7f' || str === '\x08') {
|
|
278
|
+
if (this.selectAll) {
|
|
279
|
+
this._clearIfSelected();
|
|
280
|
+
} else if (this.cursor > 0) {
|
|
281
|
+
const chars = [...this.text];
|
|
282
|
+
chars.splice(this.cursor - 1, 1);
|
|
283
|
+
this.text = chars.join('');
|
|
284
|
+
this.cursor--;
|
|
285
|
+
}
|
|
286
|
+
this.render();
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Delete
|
|
291
|
+
if (str === '\x1b[3~') {
|
|
292
|
+
if (this.selectAll) {
|
|
293
|
+
this._clearIfSelected();
|
|
294
|
+
} else if (this.cursor < [...this.text].length) {
|
|
295
|
+
const chars = [...this.text];
|
|
296
|
+
chars.splice(this.cursor, 1);
|
|
297
|
+
this.text = chars.join('');
|
|
298
|
+
}
|
|
299
|
+
this.render();
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Left arrow → deselect and move to start
|
|
304
|
+
if (str === '\x1b[D') {
|
|
305
|
+
if (this.selectAll) { this.cursor = 0; this._deselect(); }
|
|
306
|
+
else if (this.cursor > 0) this.cursor--;
|
|
307
|
+
this.render();
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Right arrow → deselect and move to end
|
|
312
|
+
if (str === '\x1b[C') {
|
|
313
|
+
if (this.selectAll) { this._deselect(); }
|
|
314
|
+
else if (this.cursor < [...this.text].length) this.cursor++;
|
|
315
|
+
this.render();
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Home
|
|
320
|
+
if (str === '\x1b[H') {
|
|
321
|
+
this._deselect();
|
|
322
|
+
this.cursor = 0;
|
|
323
|
+
this.render();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// End
|
|
328
|
+
if (str === '\x1b[F') {
|
|
329
|
+
this._deselect();
|
|
330
|
+
this.cursor = [...this.text].length;
|
|
331
|
+
this.render();
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Normal character input → replace all if selected
|
|
336
|
+
if (!str.startsWith('\x1b') && str.charCodeAt(0) >= 32) {
|
|
337
|
+
this._clearIfSelected();
|
|
338
|
+
const chars = [...this.text];
|
|
339
|
+
const input = [...str];
|
|
340
|
+
this.text = chars.slice(0, this.cursor).join('') + str + chars.slice(this.cursor).join('');
|
|
341
|
+
this.cursor += input.length;
|
|
342
|
+
this.render();
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return true; // Consume all input while editing
|
|
347
|
+
}
|
|
348
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sanohiro/casty",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "TTY web browser using raw CDP and Kitty graphics protocol",
|
|
5
|
+
"main": "bin/casty.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"casty": "bin/casty"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "./bin/casty",
|
|
16
|
+
"postinstall": "echo 'Run casty to auto-install Chrome Headless Shell to ~/.casty/browsers/'"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"browser",
|
|
20
|
+
"tty",
|
|
21
|
+
"terminal",
|
|
22
|
+
"kitty",
|
|
23
|
+
"cdp",
|
|
24
|
+
"headless",
|
|
25
|
+
"chrome"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/sanohiro/casty.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/sanohiro/casty",
|
|
32
|
+
"author": "Hironobu Sano",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"type": "module",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"ws": "^8.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|