@sanohiro/casty 1.0.0 → 1.1.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.ja.md +26 -2
- package/README.md +25 -1
- package/lib/browser.js +22 -8
- package/lib/config.js +6 -5
- package/lib/kitty.js +14 -5
- package/package.json +2 -1
package/README.ja.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
casty は w3m や lynx のようなテキストブラウザではありません。ヘッドレス Chrome を起動し、CDP でレンダリング結果を取得して、Kitty graphics protocol でターミナルに描画します。Chrome のリモートデスクトップがターミナルに収まった感じです。
|
|
8
8
|
|
|
9
|
+

|
|
10
|
+
|
|
9
11
|
<video src="https://github.com/user-attachments/assets/552f1972-bb53-481e-9516-c36b7e5085d8" autoplay loop muted playsinline></video>
|
|
10
12
|
|
|
11
13
|
## 仕組み
|
|
@@ -21,7 +23,7 @@ casty は w3m や lynx のようなテキストブラウザではありません
|
|
|
21
23
|
└──────────────┘ └──────────────┘ └──────────────┘
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
レンダリングはすべて Chrome がやります。casty はフレームをターミナルに流して入力を返すだけのブリッジ(約
|
|
26
|
+
レンダリングはすべて Chrome がやります。casty はフレームをターミナルに流して入力を返すだけのブリッジ(約2300行)。Playwright も puppeteer も使わず、WebSocket で生 CDP を叩いています。
|
|
25
27
|
|
|
26
28
|
本物の Chrome なので JavaScript, CSS, Canvas, WebGL 全部動きます。ステルスパッチで Google ログインも通ります。マウスのクリック、スクロール、ドラッグ、キーボード入力 — 普通のブラウザと同じ操作ができます。
|
|
27
29
|
|
|
@@ -52,6 +54,15 @@ cd casty && npm install
|
|
|
52
54
|
- Node.js >= 18
|
|
53
55
|
- `unzip`(Chrome 自動インストールに必要)
|
|
54
56
|
|
|
57
|
+
### tmux
|
|
58
|
+
|
|
59
|
+
tmux 内で casty を使う場合は、Kitty graphics のエスケープシーケンスを
|
|
60
|
+
ターミナルまで通すために passthrough を有効にしてください:
|
|
61
|
+
|
|
62
|
+
```tmux
|
|
63
|
+
set -g allow-passthrough on
|
|
64
|
+
```
|
|
65
|
+
|
|
55
66
|
## 使い方
|
|
56
67
|
|
|
57
68
|
```bash
|
|
@@ -127,7 +138,7 @@ casty # ホームページを開く
|
|
|
127
138
|
<details>
|
|
128
139
|
<summary>技術詳細</summary>
|
|
129
140
|
|
|
130
|
-
全体で約
|
|
141
|
+
全体で約2300行の JavaScript です。中でやっていること:
|
|
131
142
|
|
|
132
143
|
- chrome-headless-shell を起動して生 CDP WebSocket で通信
|
|
133
144
|
- `Runtime.enable` は絶対に送らない(Google ログインが壊れる。これは苦労して発見した)
|
|
@@ -155,6 +166,19 @@ lib/bookmarks.js ブックマーク検索
|
|
|
155
166
|
|
|
156
167
|
## トラブルシューティング
|
|
157
168
|
|
|
169
|
+
### YouTube で音が出ない(Ubuntu Server)
|
|
170
|
+
|
|
171
|
+
Chrome はシステムのオーディオサーバーを通じて直接音声を再生します。音が出ない場合:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
sudo apt install pulseaudio
|
|
175
|
+
sudo usermod -aG audio $USER
|
|
176
|
+
# ログアウトして再ログイン後:
|
|
177
|
+
pulseaudio --start
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Chrome がクラッシュする
|
|
181
|
+
|
|
158
182
|
casty が起動しない、または Chrome がクラッシュする場合、ブラウザキャッシュを削除してください:
|
|
159
183
|
|
|
160
184
|
```bash
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Run a real Chrome browser inside your terminal.
|
|
|
6
6
|
|
|
7
7
|
casty is not a text-mode browser like w3m or lynx. It launches headless Chrome, grabs the rendered frames over CDP, and draws them in your terminal via Kitty graphics protocol. Think of it as a remote desktop for Chrome that fits in a terminal window.
|
|
8
8
|
|
|
9
|
+

|
|
10
|
+
|
|
9
11
|
<video src="https://github.com/user-attachments/assets/552f1972-bb53-481e-9516-c36b7e5085d8" autoplay loop muted playsinline></video>
|
|
10
12
|
|
|
11
13
|
## How It Works
|
|
@@ -21,7 +23,7 @@ Terminal (you) casty Chrome (headless)
|
|
|
21
23
|
└──────────────┘ └──────────────┘ └──────────────┘
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
Chrome does all the rendering. casty is just a bridge (~
|
|
26
|
+
Chrome does all the rendering. casty is just a bridge (~2300 lines) that streams frames to your terminal and sends input back. No Playwright, no puppeteer — raw CDP over WebSocket.
|
|
25
27
|
|
|
26
28
|
Since it's real Chrome, JavaScript, CSS, Canvas, and WebGL all work. Google login works too (stealth patches bypass bot detection). Mouse clicks, scrolling, dragging, typing — everything you'd expect.
|
|
27
29
|
|
|
@@ -52,6 +54,15 @@ Chrome Headless Shell is auto-installed to `~/.casty/browsers/` on first run.
|
|
|
52
54
|
- Node.js >= 18
|
|
53
55
|
- `unzip` (for Chrome auto-install)
|
|
54
56
|
|
|
57
|
+
### tmux
|
|
58
|
+
|
|
59
|
+
If you run casty inside tmux, enable passthrough so Kitty graphics escape
|
|
60
|
+
sequences can reach your terminal:
|
|
61
|
+
|
|
62
|
+
```tmux
|
|
63
|
+
set -g allow-passthrough on
|
|
64
|
+
```
|
|
65
|
+
|
|
55
66
|
## Usage
|
|
56
67
|
|
|
57
68
|
```bash
|
|
@@ -155,6 +166,19 @@ lib/bookmarks.js Bookmark search
|
|
|
155
166
|
|
|
156
167
|
## Troubleshooting
|
|
157
168
|
|
|
169
|
+
### No audio on YouTube (Ubuntu Server)
|
|
170
|
+
|
|
171
|
+
Chrome plays audio directly through the system audio server. If there's no sound:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
sudo apt install pulseaudio
|
|
175
|
+
sudo usermod -aG audio $USER
|
|
176
|
+
# Log out and back in, then:
|
|
177
|
+
pulseaudio --start
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Chrome crashes
|
|
181
|
+
|
|
158
182
|
If casty fails to start or Chrome crashes, try removing the browser cache:
|
|
159
183
|
|
|
160
184
|
```bash
|
package/lib/browser.js
CHANGED
|
@@ -7,8 +7,18 @@ import { CDPClient } from './cdp.js';
|
|
|
7
7
|
import { launchChrome } from './chrome.js';
|
|
8
8
|
import { loadConfig } from './config.js';
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const DEFAULT_CHROME_VERSION = '146.0.7680.80';
|
|
11
|
+
|
|
12
|
+
// Platform-specific identity (must match real OS to avoid TLS/TCP fingerprint mismatch)
|
|
13
|
+
const PLATFORM = process.platform === 'darwin'
|
|
14
|
+
? { ua: 'Macintosh; Intel Mac OS X 10_15_7', nav: 'MacIntel',
|
|
15
|
+
glVendor: 'Google Inc. (Apple)', glRenderer: 'ANGLE (Apple, Apple M1, OpenGL 4.1)' }
|
|
16
|
+
: { ua: `X11; Linux ${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}`,
|
|
17
|
+
nav: `Linux ${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}`,
|
|
18
|
+
glVendor: 'Google Inc. (Intel)', glRenderer: 'ANGLE (Intel, Mesa Intel UHD Graphics, OpenGL 4.5)' };
|
|
19
|
+
|
|
20
|
+
const buildUserAgent = (v) =>
|
|
21
|
+
`Mozilla/5.0 (${PLATFORM.ua}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${v} Safari/537.36`;
|
|
12
22
|
const PROFILE_DIR = join(homedir(), '.casty', 'profile');
|
|
13
23
|
|
|
14
24
|
// Screencast settings (low-res stream for change detection)
|
|
@@ -36,9 +46,8 @@ function buildStealthScript(lang) {
|
|
|
36
46
|
Object.defineProperty(navigator, 'plugins', {
|
|
37
47
|
get: () => {
|
|
38
48
|
const arr = [
|
|
39
|
-
{ name: '
|
|
40
|
-
{ name: 'Chrome PDF Viewer', filename: '
|
|
41
|
-
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
|
|
49
|
+
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
|
|
50
|
+
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
|
|
42
51
|
];
|
|
43
52
|
arr.refresh = () => {};
|
|
44
53
|
return arr;
|
|
@@ -89,8 +98,8 @@ navigator.permissions.query = (params) => {
|
|
|
89
98
|
// 0x9245 = UNMASKED_VENDOR_WEBGL, 0x9246 = UNMASKED_RENDERER_WEBGL
|
|
90
99
|
const getParam = WebGLRenderingContext.prototype.getParameter;
|
|
91
100
|
WebGLRenderingContext.prototype.getParameter = function(p) {
|
|
92
|
-
if (p === 0x9245) return '
|
|
93
|
-
if (p === 0x9246) return '
|
|
101
|
+
if (p === 0x9245) return '${PLATFORM.glVendor}';
|
|
102
|
+
if (p === 0x9246) return '${PLATFORM.glRenderer}';
|
|
94
103
|
return getParam.call(this, p);
|
|
95
104
|
};
|
|
96
105
|
|
|
@@ -132,6 +141,11 @@ export async function setupPage({ port, wsUrl }, { width, height, zoom = 1 } = {
|
|
|
132
141
|
const lang = config.language;
|
|
133
142
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
134
143
|
|
|
144
|
+
// Detect Chrome version from CDP endpoint
|
|
145
|
+
const versionInfo = await fetchJson(`${baseUrl}/json/version`);
|
|
146
|
+
const chromeVersion = versionInfo?.Browser?.replace(/^Chrome\//, '') || DEFAULT_CHROME_VERSION;
|
|
147
|
+
const userAgent = buildUserAgent(chromeVersion);
|
|
148
|
+
|
|
135
149
|
// Try /json/new (single HTTP request, fastest)
|
|
136
150
|
let target = await fetchJson(`${baseUrl}/json/new?about:blank`);
|
|
137
151
|
if (!target?.webSocketDebuggerUrl) {
|
|
@@ -162,7 +176,7 @@ export async function setupPage({ port, wsUrl }, { width, height, zoom = 1 } = {
|
|
|
162
176
|
width: cssWidth, height: cssHeight, deviceScaleFactor: zoom, mobile: false,
|
|
163
177
|
}),
|
|
164
178
|
client.send('Network.setUserAgentOverride', {
|
|
165
|
-
userAgent:
|
|
179
|
+
userAgent: userAgent, platform: PLATFORM.nav,
|
|
166
180
|
acceptLanguage: buildAcceptLanguage(lang),
|
|
167
181
|
}),
|
|
168
182
|
client.send('Page.addScriptToEvaluateOnNewDocument', { source: buildStealthScript(lang) }),
|
package/lib/config.js
CHANGED
|
@@ -15,15 +15,16 @@ const configPath = join(homedir(), '.casty', 'config.json');
|
|
|
15
15
|
|
|
16
16
|
// Detect system locale (e.g. "en-US", "ja", "zh-CN")
|
|
17
17
|
function detectLocale() {
|
|
18
|
-
// Intl
|
|
18
|
+
// Environment variables first (Intl may lack ICU data and return en-US)
|
|
19
|
+
const env = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || '';
|
|
20
|
+
const m = env.match(/^([a-z]{2})(?:[_-]([A-Z]{2}))?/);
|
|
21
|
+
if (m) return m[2] ? `${m[1]}-${m[2]}` : m[1];
|
|
22
|
+
// Fallback to Intl API
|
|
19
23
|
try {
|
|
20
24
|
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
21
25
|
if (locale) return locale;
|
|
22
26
|
} catch {}
|
|
23
|
-
|
|
24
|
-
const env = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || '';
|
|
25
|
-
const m = env.match(/^([a-z]{2}(?:[_-][A-Z]{2})?)/);
|
|
26
|
-
return m ? m[1].replace('_', '-') : 'en-US';
|
|
27
|
+
return 'en-US';
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const DEFAULTS = {
|
package/lib/kitty.js
CHANGED
|
@@ -62,9 +62,16 @@ export function cursorHome() {
|
|
|
62
62
|
process.stdout.write(CURSOR_HOME);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// tmux only forwards kitty graphics if they are wrapped in a DCS passthrough
|
|
66
|
+
// envelope. Regular cursor/control sequences must stay outside that wrapper.
|
|
67
|
+
function wrapKitty(seq) {
|
|
68
|
+
if (!process.env.TMUX) return seq;
|
|
69
|
+
return `\x1bPtmux;${seq.replaceAll('\x1b', '\x1b\x1b')}\x1b\\`;
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
// Clear screen (also delete all Kitty images)
|
|
66
73
|
export function clearScreen() {
|
|
67
|
-
process.stdout.write('\x1b_Ga=d,d=A,q=2;\x1b
|
|
74
|
+
process.stdout.write(`${wrapKitty('\x1b_Ga=d,d=A,q=2;\x1b\\')}\x1b[2J\x1b[H`);
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
// Hide cursor
|
|
@@ -103,7 +110,8 @@ function sendFrameFile(base64Data) {
|
|
|
103
110
|
lastFrameData = base64Data;
|
|
104
111
|
writeFileSync(tmpFile, Buffer.from(base64Data, 'base64'));
|
|
105
112
|
const crFile = _cols && _rows ? `,c=${_cols},r=${_rows}` : '';
|
|
106
|
-
|
|
113
|
+
const seq = `\x1b_Ga=T,f=100,t=f,q=2,C=1,i=1${crFile};${tmpPathB64}\x1b\\`;
|
|
114
|
+
process.stdout.write(`${CURSOR_HOME}${wrapKitty(seq)}`);
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
// Inline mode (4096B chunked, PNG only)
|
|
@@ -116,10 +124,11 @@ function sendFrameInline(pngBase64) {
|
|
|
116
124
|
const CHUNK = 4096;
|
|
117
125
|
const crInline = _cols && _rows ? `,c=${_cols},r=${_rows}` : '';
|
|
118
126
|
if (pngBase64.length <= CHUNK) {
|
|
119
|
-
|
|
127
|
+
const seq = `\x1b_Ga=T,f=100,q=2,C=1,i=1${crInline};${pngBase64}\x1b\\`;
|
|
128
|
+
process.stdout.write(`${CURSOR_HOME}${wrapKitty(seq)}`);
|
|
120
129
|
return;
|
|
121
130
|
}
|
|
122
|
-
const parts = [
|
|
131
|
+
const parts = [];
|
|
123
132
|
let i = 0;
|
|
124
133
|
while (i < pngBase64.length) {
|
|
125
134
|
const chunk = pngBase64.slice(i, i + CHUNK);
|
|
@@ -131,7 +140,7 @@ function sendFrameInline(pngBase64) {
|
|
|
131
140
|
}
|
|
132
141
|
i += CHUNK;
|
|
133
142
|
}
|
|
134
|
-
process.stdout.write(parts.join(''));
|
|
143
|
+
process.stdout.write(`${CURSOR_HOME}${wrapKitty(parts.join(''))}`);
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
// Reset dedup state (e.g. after resize)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanohiro/casty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "TTY web browser using raw CDP and Kitty graphics protocol",
|
|
5
5
|
"main": "bin/casty.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "./bin/casty",
|
|
16
|
+
"test": "node --test",
|
|
16
17
|
"postinstall": "echo 'Run casty to auto-install Chrome Headless Shell to ~/.casty/browsers/'"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|