webtty 1.1.1 → 1.3.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/README.md +9 -1
- package/dist/cli/index.js +265 -60
- package/dist/cli/index.js.map +7 -6
- package/dist/client-browser.js +3 -0
- package/dist/client.css +18 -0
- package/dist/client.html +14 -0
- package/dist/server/index.js +345 -294
- package/dist/server/index.js.map +12 -13
- package/package.json +2 -3
package/dist/server/index.js
CHANGED
|
@@ -53,7 +53,7 @@ __export(exports_bun, {
|
|
|
53
53
|
spawn: () => spawn
|
|
54
54
|
});
|
|
55
55
|
import { homedir } from "node:os";
|
|
56
|
-
function spawn(shell, cols, rows) {
|
|
56
|
+
function spawn(shell, cols, rows, term, colorTerm) {
|
|
57
57
|
let onDataCb;
|
|
58
58
|
let onExitCb;
|
|
59
59
|
const proc = Bun.spawn([shell], {
|
|
@@ -65,7 +65,7 @@ function spawn(shell, cols, rows) {
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
cwd: homedir(),
|
|
68
|
-
env: { ...process.env, TERM:
|
|
68
|
+
env: { ...process.env, TERM: term, COLORTERM: colorTerm }
|
|
69
69
|
});
|
|
70
70
|
proc.exited.then((exitCode) => {
|
|
71
71
|
onExitCb?.({ exitCode: exitCode ?? 0 });
|
|
@@ -97,13 +97,13 @@ __export(exports_node, {
|
|
|
97
97
|
});
|
|
98
98
|
import { homedir as homedir2 } from "node:os";
|
|
99
99
|
import nodePty from "@lydell/node-pty";
|
|
100
|
-
function spawn2(shell, cols, rows) {
|
|
100
|
+
function spawn2(shell, cols, rows, term, colorTerm) {
|
|
101
101
|
const ptyProc = nodePty.spawn(shell, [], {
|
|
102
|
-
name:
|
|
102
|
+
name: term,
|
|
103
103
|
cols,
|
|
104
104
|
rows,
|
|
105
105
|
cwd: homedir2(),
|
|
106
|
-
env: { ...process.env, TERM:
|
|
106
|
+
env: { ...process.env, TERM: term, COLORTERM: colorTerm }
|
|
107
107
|
});
|
|
108
108
|
return {
|
|
109
109
|
onData(cb) {
|
|
@@ -126,135 +126,116 @@ function spawn2(shell, cols, rows) {
|
|
|
126
126
|
var init_node = () => {};
|
|
127
127
|
|
|
128
128
|
// src/server/index.ts
|
|
129
|
+
import fs3 from "node:fs";
|
|
129
130
|
import http from "node:http";
|
|
131
|
+
import path4 from "node:path";
|
|
132
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
133
|
+
|
|
134
|
+
// src/config.ts
|
|
135
|
+
import fs from "node:fs";
|
|
136
|
+
import os from "node:os";
|
|
137
|
+
import path from "node:path";
|
|
138
|
+
function configDir() {
|
|
139
|
+
return path.join(process.env.HOME ?? os.homedir(), ".config", "webtty");
|
|
140
|
+
}
|
|
141
|
+
function getConfigPath() {
|
|
142
|
+
return path.join(configDir(), "config.json");
|
|
143
|
+
}
|
|
144
|
+
var DEFAULT_THEME = {
|
|
145
|
+
background: "#000000",
|
|
146
|
+
foreground: "#CCCCCC",
|
|
147
|
+
cursor: "#FFFFFF",
|
|
148
|
+
selection: "#FFFFFF",
|
|
149
|
+
black: "#0C0C0C",
|
|
150
|
+
red: "#C50F1F",
|
|
151
|
+
green: "#13A10E",
|
|
152
|
+
yellow: "#C19C00",
|
|
153
|
+
blue: "#0037DA",
|
|
154
|
+
purple: "#881798",
|
|
155
|
+
cyan: "#3A96DD",
|
|
156
|
+
white: "#CCCCCC",
|
|
157
|
+
brightBlack: "#767676",
|
|
158
|
+
brightRed: "#E74856",
|
|
159
|
+
brightGreen: "#16C60C",
|
|
160
|
+
brightYellow: "#F9F1A5",
|
|
161
|
+
brightBlue: "#3B78FF",
|
|
162
|
+
brightPurple: "#B4009E",
|
|
163
|
+
brightCyan: "#61D6D6",
|
|
164
|
+
brightWhite: "#F2F2F2"
|
|
165
|
+
};
|
|
166
|
+
var DEFAULT_CONFIG = {
|
|
167
|
+
port: 2346,
|
|
168
|
+
host: "127.0.0.1",
|
|
169
|
+
shell: process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "/bin/bash",
|
|
170
|
+
term: process.env.TERM ?? "xterm-256color",
|
|
171
|
+
colorTerm: "truecolor",
|
|
172
|
+
scrollback: 256 * 1024,
|
|
173
|
+
cols: 80,
|
|
174
|
+
rows: 24,
|
|
175
|
+
fontSize: 13,
|
|
176
|
+
fontFamily: "Menlo, Consolas, 'DejaVu Sans Mono', monospace",
|
|
177
|
+
cursorBlink: true,
|
|
178
|
+
copyOnSelect: true,
|
|
179
|
+
rightClickBehavior: "default",
|
|
180
|
+
logs: false,
|
|
181
|
+
theme: DEFAULT_THEME
|
|
182
|
+
};
|
|
183
|
+
function loadConfig() {
|
|
184
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
185
|
+
try {
|
|
186
|
+
saveConfig(DEFAULT_CONFIG);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.warn(`webtty: failed to write default config to ${getConfigPath()}: ${err.message}`);
|
|
189
|
+
return { ...DEFAULT_CONFIG };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let raw;
|
|
193
|
+
try {
|
|
194
|
+
raw = fs.readFileSync(getConfigPath(), "utf8");
|
|
195
|
+
} catch (err) {
|
|
196
|
+
throw new Error(`webtty: failed to read config at ${getConfigPath()}: ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(raw);
|
|
201
|
+
} catch {
|
|
202
|
+
throw new Error(`webtty: invalid JSON in config file ${getConfigPath()}`);
|
|
203
|
+
}
|
|
204
|
+
const p = parsed;
|
|
205
|
+
return {
|
|
206
|
+
...DEFAULT_CONFIG,
|
|
207
|
+
...typeof p.port === "number" && { port: p.port },
|
|
208
|
+
...typeof p.host === "string" && { host: p.host },
|
|
209
|
+
...typeof p.shell === "string" && { shell: p.shell },
|
|
210
|
+
...typeof p.term === "string" && { term: p.term },
|
|
211
|
+
...typeof p.colorTerm === "string" && { colorTerm: p.colorTerm },
|
|
212
|
+
...typeof p.scrollback === "number" && { scrollback: p.scrollback },
|
|
213
|
+
...typeof p.cols === "number" && { cols: p.cols },
|
|
214
|
+
...typeof p.rows === "number" && { rows: p.rows },
|
|
215
|
+
...typeof p.fontSize === "number" && { fontSize: p.fontSize },
|
|
216
|
+
...typeof p.fontFamily === "string" && { fontFamily: p.fontFamily },
|
|
217
|
+
...typeof p.cursorBlink === "boolean" && { cursorBlink: p.cursorBlink },
|
|
218
|
+
...typeof p.copyOnSelect === "boolean" && { copyOnSelect: p.copyOnSelect },
|
|
219
|
+
...typeof p.rightClickBehavior === "string" && {
|
|
220
|
+
rightClickBehavior: p.rightClickBehavior === "copyPaste" ? "copyPaste" : "default"
|
|
221
|
+
},
|
|
222
|
+
...typeof p.logs === "boolean" && { logs: p.logs },
|
|
223
|
+
...p.theme && typeof p.theme === "object" && { theme: { ...DEFAULT_THEME, ...p.theme } }
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function saveConfig(_config) {
|
|
227
|
+
fs.mkdirSync(path.dirname(getConfigPath()), { recursive: true });
|
|
228
|
+
const content = JSON.stringify({
|
|
229
|
+
port: DEFAULT_CONFIG.port,
|
|
230
|
+
host: DEFAULT_CONFIG.host
|
|
231
|
+
}, null, 2);
|
|
232
|
+
fs.writeFileSync(getConfigPath(), content, "utf8");
|
|
233
|
+
}
|
|
130
234
|
|
|
131
235
|
// src/server/routes.ts
|
|
132
236
|
import path3 from "node:path";
|
|
133
237
|
|
|
134
|
-
// src/server/client.ts
|
|
135
|
-
function render(sessionId) {
|
|
136
|
-
return `<!doctype html>
|
|
137
|
-
<html lang="en">
|
|
138
|
-
<head>
|
|
139
|
-
<meta charset="UTF-8" />
|
|
140
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
141
|
-
<title>webtty — ${sessionId}</title>
|
|
142
|
-
<style>
|
|
143
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
144
|
-
|
|
145
|
-
html, body, #terminal { width: 100%; height: 100%; overflow: hidden; background: #282A36; }
|
|
146
|
-
|
|
147
|
-
#terminal canvas { display: block; }
|
|
148
|
-
</style>
|
|
149
|
-
</head>
|
|
150
|
-
<body>
|
|
151
|
-
<div id="terminal"></div>
|
|
152
|
-
|
|
153
|
-
<script type="module">
|
|
154
|
-
import { init, Terminal, FitAddon } from '/dist/ghostty-web.js';
|
|
155
|
-
|
|
156
|
-
await init();
|
|
157
|
-
const term = new Terminal({
|
|
158
|
-
cols: 80,
|
|
159
|
-
rows: 24,
|
|
160
|
-
cursorBlink: true,
|
|
161
|
-
fontSize: 14,
|
|
162
|
-
fontFamily: "'FiraMono Nerd Font', Menlo, Monaco, 'Courier New', monospace",
|
|
163
|
-
scrollback: 10000,
|
|
164
|
-
theme: {
|
|
165
|
-
background: '#282A36',
|
|
166
|
-
foreground: '#F8F8F2',
|
|
167
|
-
cursor: '#F8F8F2',
|
|
168
|
-
selection: '#44475A',
|
|
169
|
-
black: '#21222C',
|
|
170
|
-
red: '#FF5555',
|
|
171
|
-
green: '#50FA7B',
|
|
172
|
-
yellow: '#F1FA8C',
|
|
173
|
-
blue: '#BD93F9',
|
|
174
|
-
purple: '#FF79C6',
|
|
175
|
-
cyan: '#8BE9FD',
|
|
176
|
-
white: '#F8F8F2',
|
|
177
|
-
brightBlack: '#6272A4',
|
|
178
|
-
brightRed: '#FF6E6E',
|
|
179
|
-
brightGreen: '#69FF94',
|
|
180
|
-
brightYellow: '#FFFFA5',
|
|
181
|
-
brightBlue: '#D6ACFF',
|
|
182
|
-
brightPurple: '#FF92DF',
|
|
183
|
-
brightCyan: '#A4FFFF',
|
|
184
|
-
brightWhite: '#FFFFFF',
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const fitAddon = new FitAddon();
|
|
189
|
-
term.loadAddon(fitAddon);
|
|
190
|
-
|
|
191
|
-
const container = document.getElementById('terminal');
|
|
192
|
-
await term.open(container);
|
|
193
|
-
fitAddon.fit();
|
|
194
|
-
fitAddon.observeResize();
|
|
195
|
-
|
|
196
|
-
const sessionId = ${JSON.stringify(sessionId)};
|
|
197
|
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
198
|
-
let ws;
|
|
199
|
-
|
|
200
|
-
function connect() {
|
|
201
|
-
const wsUrl = protocol + '//' + window.location.host + '/ws/' + sessionId + '?cols=' + term.cols + '&rows=' + term.rows;
|
|
202
|
-
ws = new WebSocket(wsUrl);
|
|
203
|
-
|
|
204
|
-
ws.onopen = () => {
|
|
205
|
-
console.log('[webtty] connected to session ' + sessionId);
|
|
206
|
-
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
ws.onmessage = (event) => {
|
|
210
|
-
term.write(event.data);
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
ws.onclose = (event) => {
|
|
214
|
-
if (event.code === 4001) {
|
|
215
|
-
term.write('\\r\\n\\x1b[31mSession removed.\\x1b[0m\\r\\n');
|
|
216
|
-
setTimeout(() => window.close(), 500);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (event.code === 1001) {
|
|
220
|
-
term.write('\\r\\n\\x1b[33mServer stopped.\\x1b[0m\\r\\n');
|
|
221
|
-
setTimeout(() => window.close(), 500);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
console.log('[webtty] disconnected, reconnecting in 2s...');
|
|
225
|
-
term.write('\\r\\n\\x1b[31mConnection closed. Reconnecting in 2s...\\x1b[0m\\r\\n');
|
|
226
|
-
setTimeout(connect, 2000);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
ws.onerror = () => {
|
|
230
|
-
console.error('[webtty] websocket error');
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
connect();
|
|
235
|
-
|
|
236
|
-
term.onData((data) => {
|
|
237
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
238
|
-
ws.send(data);
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
term.onResize(({ cols, rows }) => {
|
|
243
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
244
|
-
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
window.addEventListener('resize', () => {
|
|
249
|
-
fitAddon.fit();
|
|
250
|
-
});
|
|
251
|
-
</script>
|
|
252
|
-
</body>
|
|
253
|
-
</html>`;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
238
|
// src/server/session.ts
|
|
257
|
-
var SCROLLBACK_MAX = 256 * 1024;
|
|
258
239
|
var sessionRegistry = new Map;
|
|
259
240
|
var lastUsedId = null;
|
|
260
241
|
function setLastUsedId(id) {
|
|
@@ -283,13 +264,13 @@ function sessionToJson(s) {
|
|
|
283
264
|
}
|
|
284
265
|
|
|
285
266
|
// src/server/static.ts
|
|
286
|
-
import
|
|
267
|
+
import fs2 from "node:fs";
|
|
287
268
|
import { createRequire as createRequire2 } from "node:module";
|
|
288
269
|
import path2 from "node:path";
|
|
289
270
|
import { fileURLToPath } from "node:url";
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
271
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
272
|
+
var __dirname2 = path2.dirname(__filename2);
|
|
273
|
+
var require2 = createRequire2(import.meta.url);
|
|
293
274
|
var MIME_TYPES = {
|
|
294
275
|
".html": "text/html",
|
|
295
276
|
".js": "application/javascript",
|
|
@@ -302,24 +283,19 @@ var MIME_TYPES = {
|
|
|
302
283
|
".ico": "image/x-icon"
|
|
303
284
|
};
|
|
304
285
|
function mimeType(filePath) {
|
|
305
|
-
const ext =
|
|
286
|
+
const ext = path2.extname(filePath);
|
|
306
287
|
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
307
288
|
}
|
|
308
289
|
function ghosttyWebRootFromMain(mainPath) {
|
|
309
290
|
return mainPath.replace(/[/\\]dist[/\\].*$/, "");
|
|
310
291
|
}
|
|
311
|
-
|
|
312
|
-
// src/server/static.ts
|
|
313
|
-
var __filename2 = fileURLToPath(import.meta.url);
|
|
314
|
-
var __dirname2 = path2.dirname(__filename2);
|
|
315
|
-
var require2 = createRequire2(import.meta.url);
|
|
316
292
|
function findGhosttyWeb() {
|
|
317
293
|
try {
|
|
318
294
|
const ghosttyWebMain = require2.resolve("ghostty-web");
|
|
319
295
|
const ghosttyWebRoot = ghosttyWebRootFromMain(ghosttyWebMain);
|
|
320
296
|
const distPath = path2.join(ghosttyWebRoot, "dist");
|
|
321
297
|
const wasmPath = path2.join(ghosttyWebRoot, "ghostty-vt.wasm");
|
|
322
|
-
if (
|
|
298
|
+
if (fs2.existsSync(path2.join(distPath, "ghostty-web.js")) && fs2.existsSync(wasmPath)) {
|
|
323
299
|
return { distPath, wasmPath };
|
|
324
300
|
}
|
|
325
301
|
} catch {}
|
|
@@ -328,7 +304,7 @@ function findGhosttyWeb() {
|
|
|
328
304
|
}
|
|
329
305
|
function serveFile(filePath, res) {
|
|
330
306
|
const contentType = mimeType(filePath);
|
|
331
|
-
|
|
307
|
+
fs2.readFile(filePath, (err, data) => {
|
|
332
308
|
if (err) {
|
|
333
309
|
res.writeHead(404);
|
|
334
310
|
res.end("Not Found");
|
|
@@ -339,6 +315,178 @@ function serveFile(filePath, res) {
|
|
|
339
315
|
});
|
|
340
316
|
}
|
|
341
317
|
|
|
318
|
+
// src/server/websocket.ts
|
|
319
|
+
import { WebSocketServer } from "ws";
|
|
320
|
+
|
|
321
|
+
// src/pty/index.ts
|
|
322
|
+
var isBun = !!process.versions.bun;
|
|
323
|
+
console.log(`pty: ${isBun ? "Bun.Terminal" : "node-pty"}`);
|
|
324
|
+
var { spawn: _spawn } = await (isBun ? Promise.resolve().then(() => (init_bun(), exports_bun)) : Promise.resolve().then(() => (init_node(), exports_node)));
|
|
325
|
+
function spawnForSession(cols, rows, shell, term, colorTerm) {
|
|
326
|
+
return _spawn(shell, cols, rows, term, colorTerm);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/server/websocket.ts
|
|
330
|
+
var WS_CLOSE = {
|
|
331
|
+
SERVER_STOPPED: 1001,
|
|
332
|
+
BAD_REQUEST: 1008,
|
|
333
|
+
SESSION_GONE: 4001
|
|
334
|
+
};
|
|
335
|
+
function closeClients(session, code, reason) {
|
|
336
|
+
session.pty?.kill();
|
|
337
|
+
for (const client of session.clients)
|
|
338
|
+
client.close(code, reason);
|
|
339
|
+
}
|
|
340
|
+
function closeSession(session) {
|
|
341
|
+
closeClients(session, WS_CLOSE.SESSION_GONE, "session deleted");
|
|
342
|
+
}
|
|
343
|
+
function closeAllSessions() {
|
|
344
|
+
for (const session of sessionRegistry.values()) {
|
|
345
|
+
closeClients(session, WS_CLOSE.SERVER_STOPPED, "server stopped");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
var onLastSessionClosed = null;
|
|
349
|
+
function setLastSessionClosedHandler(handler) {
|
|
350
|
+
onLastSessionClosed = handler;
|
|
351
|
+
}
|
|
352
|
+
var DIM = "\x1B[2m";
|
|
353
|
+
var RESET = "\x1B[0m";
|
|
354
|
+
var BOLD = "\x1B[1m";
|
|
355
|
+
var ITALIC = "\x1B[3m";
|
|
356
|
+
var YELLOW = "\x1B[1;33m";
|
|
357
|
+
var CYAN = "\x1B[1;36m";
|
|
358
|
+
var isBun2 = process.execPath.includes("bun");
|
|
359
|
+
var PKG_RUNNER = isBun2 ? "bunx" : "npx";
|
|
360
|
+
function sessionBanner() {
|
|
361
|
+
const W = 48;
|
|
362
|
+
const pipe = (content) => `${CYAN}║${RESET}${content}${CYAN}║${RESET}`;
|
|
363
|
+
const blank = pipe(" ".repeat(W));
|
|
364
|
+
const titleVis = " [ webtty ] Terminal UI in the browser";
|
|
365
|
+
const titleStr = ` ${DIM}[${RESET} ${BOLD}${YELLOW}webtty${RESET} ${DIM}] Terminal UI in the browser${RESET}`;
|
|
366
|
+
const titleLine = pipe(titleStr + " ".repeat(W - titleVis.length));
|
|
367
|
+
const cmd = `${PKG_RUNNER} webtty help`;
|
|
368
|
+
const helpVis = ` Run \`${cmd}\` for more information.`;
|
|
369
|
+
const helpStr = ` ${DIM}Run \`${RESET}${ITALIC}${cmd}${RESET}${DIM}\` for more information.${RESET}`;
|
|
370
|
+
const helpLine = pipe(helpStr + " ".repeat(W - helpVis.length));
|
|
371
|
+
return [
|
|
372
|
+
`${CYAN}╔${"═".repeat(W)}╗${RESET}`,
|
|
373
|
+
`\r
|
|
374
|
+
`,
|
|
375
|
+
blank,
|
|
376
|
+
`\r
|
|
377
|
+
`,
|
|
378
|
+
titleLine,
|
|
379
|
+
`\r
|
|
380
|
+
`,
|
|
381
|
+
blank,
|
|
382
|
+
`\r
|
|
383
|
+
`,
|
|
384
|
+
helpLine,
|
|
385
|
+
`\r
|
|
386
|
+
`,
|
|
387
|
+
blank,
|
|
388
|
+
`\r
|
|
389
|
+
`,
|
|
390
|
+
`${CYAN}╚${"═".repeat(W)}╝${RESET}`,
|
|
391
|
+
`\r
|
|
392
|
+
`,
|
|
393
|
+
`\r
|
|
394
|
+
`
|
|
395
|
+
].join("");
|
|
396
|
+
}
|
|
397
|
+
function createWebSocketServer(httpServer) {
|
|
398
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
399
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
400
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
401
|
+
if (url.pathname.match(/^\/ws\/([^/]+)$/)) {
|
|
402
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
403
|
+
} else {
|
|
404
|
+
socket.destroy();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
wss.on("connection", (ws, req) => {
|
|
408
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
409
|
+
const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/);
|
|
410
|
+
if (!wsMatch) {
|
|
411
|
+
ws.close();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
let id;
|
|
415
|
+
try {
|
|
416
|
+
id = decodeURIComponent(wsMatch[1]);
|
|
417
|
+
} catch {
|
|
418
|
+
ws.close(WS_CLOSE.BAD_REQUEST, "Bad Request");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const cols = Math.max(1, Math.min(1000, Number.parseInt(url.searchParams.get("cols") ?? "80", 10) || 80));
|
|
422
|
+
const rows = Math.max(1, Math.min(500, Number.parseInt(url.searchParams.get("rows") ?? "24", 10) || 24));
|
|
423
|
+
if (!sessionRegistry.has(id)) {
|
|
424
|
+
ws.close(WS_CLOSE.SESSION_GONE, "session deleted");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const session = sessionRegistry.get(id);
|
|
428
|
+
if (!session) {
|
|
429
|
+
ws.close(WS_CLOSE.SESSION_GONE, "session deleted");
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
session.clients.add(ws);
|
|
433
|
+
setLastUsedId(id);
|
|
434
|
+
if (!session.pty) {
|
|
435
|
+
const config = loadConfig();
|
|
436
|
+
session.pty = spawnForSession(cols, rows, config.shell, config.term, config.colorTerm);
|
|
437
|
+
session.pty.onData((data) => {
|
|
438
|
+
session.scrollback = (session.scrollback + data).slice(-config.scrollback);
|
|
439
|
+
for (const client of session.clients) {
|
|
440
|
+
if (client.readyState === client.OPEN) {
|
|
441
|
+
client.send(data, { binary: false });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
session.pty.onExit(() => {
|
|
446
|
+
sessionRegistry.delete(session.id);
|
|
447
|
+
for (const client of session.clients) {
|
|
448
|
+
if (client.readyState === client.OPEN) {
|
|
449
|
+
client.close(WS_CLOSE.SESSION_GONE, "shell exited");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
session.pty = null;
|
|
453
|
+
if (sessionRegistry.size === 0)
|
|
454
|
+
onLastSessionClosed?.();
|
|
455
|
+
});
|
|
456
|
+
const banner = sessionBanner();
|
|
457
|
+
ws.send(banner);
|
|
458
|
+
session.scrollback = banner;
|
|
459
|
+
session.pty.write(`
|
|
460
|
+
`);
|
|
461
|
+
} else {
|
|
462
|
+
if (session.scrollback) {
|
|
463
|
+
ws.send(session.scrollback, { binary: false });
|
|
464
|
+
}
|
|
465
|
+
session.pty.resize(cols, rows);
|
|
466
|
+
}
|
|
467
|
+
ws.on("message", (data) => {
|
|
468
|
+
const message = data.toString("utf8");
|
|
469
|
+
if (message.startsWith("{")) {
|
|
470
|
+
try {
|
|
471
|
+
const msg = JSON.parse(message);
|
|
472
|
+
if (msg.type === "resize") {
|
|
473
|
+
const c = Math.max(1, Math.min(1000, Math.trunc(msg.cols) || 80));
|
|
474
|
+
const r = Math.max(1, Math.min(500, Math.trunc(msg.rows) || 24));
|
|
475
|
+
session.pty?.resize(c, r);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
} catch {}
|
|
479
|
+
}
|
|
480
|
+
session.pty?.write(message);
|
|
481
|
+
});
|
|
482
|
+
ws.on("close", () => {
|
|
483
|
+
session.clients.delete(ws);
|
|
484
|
+
});
|
|
485
|
+
ws.on("error", () => {});
|
|
486
|
+
});
|
|
487
|
+
return wss;
|
|
488
|
+
}
|
|
489
|
+
|
|
342
490
|
// src/server/routes.ts
|
|
343
491
|
var MAX_BODY = 64 * 1024;
|
|
344
492
|
function decodeId(raw) {
|
|
@@ -368,7 +516,7 @@ function readJson(req) {
|
|
|
368
516
|
req.on("error", reject);
|
|
369
517
|
});
|
|
370
518
|
}
|
|
371
|
-
async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
519
|
+
async function handleRequest(req, res, distPath, wasmPath, clientDistPath, onStop) {
|
|
372
520
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
373
521
|
const pathname = url.pathname;
|
|
374
522
|
if (req.method === "POST" && pathname === "/api/server/stop") {
|
|
@@ -377,6 +525,23 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
377
525
|
onStop();
|
|
378
526
|
return;
|
|
379
527
|
}
|
|
528
|
+
if (req.method === "GET" && pathname === "/api/config") {
|
|
529
|
+
const config = loadConfig();
|
|
530
|
+
const clientConfig = {
|
|
531
|
+
cols: config.cols,
|
|
532
|
+
rows: config.rows,
|
|
533
|
+
fontSize: config.fontSize,
|
|
534
|
+
fontFamily: config.fontFamily,
|
|
535
|
+
cursorBlink: config.cursorBlink,
|
|
536
|
+
scrollback: config.scrollback,
|
|
537
|
+
theme: config.theme,
|
|
538
|
+
copyOnSelect: config.copyOnSelect,
|
|
539
|
+
rightClickBehavior: config.rightClickBehavior
|
|
540
|
+
};
|
|
541
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
542
|
+
res.end(JSON.stringify(clientConfig));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
380
545
|
if (pathname === "/api/sessions") {
|
|
381
546
|
if (req.method === "GET") {
|
|
382
547
|
const list = [...sessionRegistry.values()].map(sessionToJson);
|
|
@@ -481,10 +646,8 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
481
646
|
sessionRegistry.delete(id);
|
|
482
647
|
if (lastUsedId === id)
|
|
483
648
|
setLastUsedId(null);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
session.pty?.kill();
|
|
487
|
-
res.writeHead(204);
|
|
649
|
+
closeSession(session);
|
|
650
|
+
res.writeHead(204, { "X-Sessions-Remaining": String(sessionRegistry.size) });
|
|
488
651
|
res.end();
|
|
489
652
|
return;
|
|
490
653
|
}
|
|
@@ -508,12 +671,21 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
508
671
|
res.end("Not Found");
|
|
509
672
|
return;
|
|
510
673
|
}
|
|
511
|
-
|
|
512
|
-
res
|
|
674
|
+
const clientHtml = path3.resolve(clientDistPath, "client.html");
|
|
675
|
+
serveFile(clientHtml, res);
|
|
513
676
|
return;
|
|
514
677
|
}
|
|
515
678
|
if (pathname.startsWith("/dist/")) {
|
|
516
|
-
const
|
|
679
|
+
const relativePath = pathname.slice(6);
|
|
680
|
+
const ownFile = path3.resolve(clientDistPath, relativePath);
|
|
681
|
+
if (ownFile.startsWith(clientDistPath + path3.sep)) {
|
|
682
|
+
const fs3 = await import("node:fs");
|
|
683
|
+
if (fs3.existsSync(ownFile)) {
|
|
684
|
+
serveFile(ownFile, res);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const filePath = path3.resolve(distPath, relativePath);
|
|
517
689
|
if (!filePath.startsWith(path3.resolve(distPath) + path3.sep) && filePath !== path3.resolve(distPath)) {
|
|
518
690
|
res.writeHead(404);
|
|
519
691
|
res.end("Not Found");
|
|
@@ -530,151 +702,19 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
530
702
|
res.end("Not Found");
|
|
531
703
|
}
|
|
532
704
|
|
|
533
|
-
// src/server/websocket.ts
|
|
534
|
-
import { WebSocketServer } from "ws";
|
|
535
|
-
|
|
536
|
-
// src/pty/index.ts
|
|
537
|
-
var isBun = !!process.versions.bun;
|
|
538
|
-
console.log(`pty: ${isBun ? "Bun.Terminal" : "node-pty"}`);
|
|
539
|
-
var { spawn: _spawn } = await (isBun ? Promise.resolve().then(() => (init_bun(), exports_bun)) : Promise.resolve().then(() => (init_node(), exports_node)));
|
|
540
|
-
function spawnForSession(cols, rows) {
|
|
541
|
-
const shell = process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "/bin/bash";
|
|
542
|
-
return _spawn(shell, cols, rows);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// src/server/websocket.ts
|
|
546
|
-
function createWebSocketServer(httpServer) {
|
|
547
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
548
|
-
httpServer.on("upgrade", (req, socket, head) => {
|
|
549
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
550
|
-
if (url.pathname.match(/^\/ws\/([^/]+)$/)) {
|
|
551
|
-
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
552
|
-
} else {
|
|
553
|
-
socket.destroy();
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
wss.on("connection", (ws, req) => {
|
|
557
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
558
|
-
const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/);
|
|
559
|
-
if (!wsMatch) {
|
|
560
|
-
ws.close();
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
let id;
|
|
564
|
-
try {
|
|
565
|
-
id = decodeURIComponent(wsMatch[1]);
|
|
566
|
-
} catch {
|
|
567
|
-
ws.close(1008, "Bad Request");
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
const cols = Math.max(1, Math.min(1000, Number.parseInt(url.searchParams.get("cols") ?? "80", 10) || 80));
|
|
571
|
-
const rows = Math.max(1, Math.min(500, Number.parseInt(url.searchParams.get("rows") ?? "24", 10) || 24));
|
|
572
|
-
if (!sessionRegistry.has(id)) {
|
|
573
|
-
ws.close(4001, "session deleted");
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
const session = sessionRegistry.get(id);
|
|
577
|
-
if (!session) {
|
|
578
|
-
ws.close(4001, "session deleted");
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
session.clients.add(ws);
|
|
582
|
-
setLastUsedId(id);
|
|
583
|
-
if (!session.pty) {
|
|
584
|
-
session.pty = spawnForSession(cols, rows);
|
|
585
|
-
session.pty.onData((data) => {
|
|
586
|
-
session.scrollback = (session.scrollback + data).slice(-SCROLLBACK_MAX);
|
|
587
|
-
for (const client of session.clients) {
|
|
588
|
-
if (client.readyState === client.OPEN) {
|
|
589
|
-
client.send(data, { binary: false });
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
session.pty.onExit(() => {
|
|
594
|
-
sessionRegistry.delete(session.id);
|
|
595
|
-
for (const client of session.clients) {
|
|
596
|
-
if (client.readyState === client.OPEN) {
|
|
597
|
-
client.close(4001, "shell exited");
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
session.pty = null;
|
|
601
|
-
});
|
|
602
|
-
const C = "\x1B[1;36m";
|
|
603
|
-
const G = "\x1B[1;32m";
|
|
604
|
-
const Y = "\x1B[1;33m";
|
|
605
|
-
const R = "\x1B[0m";
|
|
606
|
-
const banner = [
|
|
607
|
-
`${C}╔══════════════════════════════════════════════════════════════╗${R}\r
|
|
608
|
-
`,
|
|
609
|
-
`${C}║${R} ${G}Welcome to webtty!${R} ${C}║${R}\r
|
|
610
|
-
`,
|
|
611
|
-
`${C}║${R} ${C}║${R}\r
|
|
612
|
-
`,
|
|
613
|
-
`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r
|
|
614
|
-
`,
|
|
615
|
-
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r
|
|
616
|
-
`,
|
|
617
|
-
`${C}╚══════════════════════════════════════════════════════════════╝${R}\r
|
|
618
|
-
\r
|
|
619
|
-
`
|
|
620
|
-
].join("");
|
|
621
|
-
ws.send(banner);
|
|
622
|
-
session.scrollback = banner;
|
|
623
|
-
session.pty.write(`
|
|
624
|
-
`);
|
|
625
|
-
} else {
|
|
626
|
-
if (session.scrollback) {
|
|
627
|
-
ws.send(session.scrollback, { binary: false });
|
|
628
|
-
}
|
|
629
|
-
session.pty.resize(cols, rows);
|
|
630
|
-
}
|
|
631
|
-
ws.on("message", (data) => {
|
|
632
|
-
const message = data.toString("utf8");
|
|
633
|
-
if (message.startsWith("{")) {
|
|
634
|
-
try {
|
|
635
|
-
const msg = JSON.parse(message);
|
|
636
|
-
if (msg.type === "resize") {
|
|
637
|
-
const c = Math.max(1, Math.min(1000, Math.trunc(msg.cols) || 80));
|
|
638
|
-
const r = Math.max(1, Math.min(500, Math.trunc(msg.rows) || 24));
|
|
639
|
-
session.pty?.resize(c, r);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
} catch {}
|
|
643
|
-
}
|
|
644
|
-
session.pty?.write(message);
|
|
645
|
-
});
|
|
646
|
-
ws.on("close", () => {
|
|
647
|
-
session.clients.delete(ws);
|
|
648
|
-
});
|
|
649
|
-
ws.on("error", () => {});
|
|
650
|
-
});
|
|
651
|
-
return wss;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
705
|
// src/server/index.ts
|
|
655
|
-
var
|
|
706
|
+
var __filename3 = fileURLToPath2(import.meta.url);
|
|
707
|
+
var __dirname3 = path4.dirname(__filename3);
|
|
708
|
+
var config = loadConfig();
|
|
709
|
+
var HTTP_PORT = Number(process.env.PORT) || config.port;
|
|
710
|
+
var HTTP_HOST = config.host;
|
|
656
711
|
var { distPath, wasmPath } = findGhosttyWeb();
|
|
657
|
-
var
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
wss.close();
|
|
665
|
-
httpServer.close(() => process.exit(0));
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
var wss = createWebSocketServer(httpServer);
|
|
669
|
-
process.on("SIGINT", () => {
|
|
670
|
-
console.log(`
|
|
671
|
-
|
|
672
|
-
Shutting down...`);
|
|
673
|
-
for (const session of sessionRegistry.values()) {
|
|
674
|
-
session.pty?.kill();
|
|
675
|
-
for (const client of session.clients)
|
|
676
|
-
client.close(1001, "server stopped");
|
|
677
|
-
}
|
|
712
|
+
var projectRoot = path4.resolve(__dirname3, "..", "..");
|
|
713
|
+
var builtDistPath = path4.join(projectRoot, "dist");
|
|
714
|
+
var srcClientPath = path4.join(projectRoot, "src", "client");
|
|
715
|
+
var clientDistPath = fs3.existsSync(path4.join(builtDistPath, "client.html")) ? builtDistPath : srcClientPath;
|
|
716
|
+
function shutdown() {
|
|
717
|
+
closeAllSessions();
|
|
678
718
|
wss.close();
|
|
679
719
|
const exit = () => process.exit(0);
|
|
680
720
|
const shutdownTimeout = setTimeout(exit, 1000);
|
|
@@ -682,9 +722,20 @@ Shutting down...`);
|
|
|
682
722
|
clearTimeout(shutdownTimeout);
|
|
683
723
|
exit();
|
|
684
724
|
});
|
|
725
|
+
}
|
|
726
|
+
var httpServer = http.createServer((req, res) => {
|
|
727
|
+
handleRequest(req, res, distPath, wasmPath, clientDistPath, shutdown);
|
|
728
|
+
});
|
|
729
|
+
var wss = createWebSocketServer(httpServer);
|
|
730
|
+
setLastSessionClosedHandler(shutdown);
|
|
731
|
+
process.on("SIGINT", () => {
|
|
732
|
+
console.log(`
|
|
733
|
+
|
|
734
|
+
Shutting down...`);
|
|
735
|
+
shutdown();
|
|
685
736
|
});
|
|
686
|
-
httpServer.listen(HTTP_PORT,
|
|
687
|
-
console.log(`listening on http
|
|
737
|
+
httpServer.listen(HTTP_PORT, HTTP_HOST, () => {
|
|
738
|
+
console.log(`listening on http://${HTTP_HOST}:${HTTP_PORT}`);
|
|
688
739
|
});
|
|
689
740
|
|
|
690
|
-
//# debugId=
|
|
741
|
+
//# debugId=D4A6F10E2C8CA53E64756E2164756E21
|