webtty 1.1.1 → 1.2.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 +46 -17
- package/dist/cli/index.js.map +4 -4
- package/dist/server/index.js +314 -205
- package/dist/server/index.js.map +12 -11
- 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) {
|
|
@@ -128,21 +128,114 @@ var init_node = () => {};
|
|
|
128
128
|
// src/server/index.ts
|
|
129
129
|
import http from "node:http";
|
|
130
130
|
|
|
131
|
+
// src/config.ts
|
|
132
|
+
import fs from "node:fs";
|
|
133
|
+
import os from "node:os";
|
|
134
|
+
import path from "node:path";
|
|
135
|
+
function getConfigPath() {
|
|
136
|
+
return path.join(os.homedir(), ".config", "webtty", "config.json");
|
|
137
|
+
}
|
|
138
|
+
var DEFAULT_THEME = {
|
|
139
|
+
background: "#000000",
|
|
140
|
+
foreground: "#CCCCCC",
|
|
141
|
+
cursor: "#FFFFFF",
|
|
142
|
+
selection: "#FFFFFF",
|
|
143
|
+
black: "#0C0C0C",
|
|
144
|
+
red: "#C50F1F",
|
|
145
|
+
green: "#13A10E",
|
|
146
|
+
yellow: "#C19C00",
|
|
147
|
+
blue: "#0037DA",
|
|
148
|
+
purple: "#881798",
|
|
149
|
+
cyan: "#3A96DD",
|
|
150
|
+
white: "#CCCCCC",
|
|
151
|
+
brightBlack: "#767676",
|
|
152
|
+
brightRed: "#E74856",
|
|
153
|
+
brightGreen: "#16C60C",
|
|
154
|
+
brightYellow: "#F9F1A5",
|
|
155
|
+
brightBlue: "#3B78FF",
|
|
156
|
+
brightPurple: "#B4009E",
|
|
157
|
+
brightCyan: "#61D6D6",
|
|
158
|
+
brightWhite: "#F2F2F2"
|
|
159
|
+
};
|
|
160
|
+
var DEFAULT_CONFIG = {
|
|
161
|
+
port: 2346,
|
|
162
|
+
host: "127.0.0.1",
|
|
163
|
+
shell: process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "/bin/bash",
|
|
164
|
+
term: process.env.TERM ?? "xterm-256color",
|
|
165
|
+
colorTerm: "truecolor",
|
|
166
|
+
scrollback: 256 * 1024,
|
|
167
|
+
cols: 80,
|
|
168
|
+
rows: 24,
|
|
169
|
+
fontSize: 13,
|
|
170
|
+
fontFamily: "Menlo, Consolas, 'DejaVu Sans Mono', monospace",
|
|
171
|
+
cursorBlink: true,
|
|
172
|
+
theme: DEFAULT_THEME
|
|
173
|
+
};
|
|
174
|
+
function loadConfig() {
|
|
175
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
176
|
+
try {
|
|
177
|
+
saveConfig(DEFAULT_CONFIG);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.warn(`webtty: failed to write default config to ${getConfigPath()}: ${err.message}`);
|
|
180
|
+
return { ...DEFAULT_CONFIG };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
let raw;
|
|
184
|
+
try {
|
|
185
|
+
raw = fs.readFileSync(getConfigPath(), "utf8");
|
|
186
|
+
} catch (err) {
|
|
187
|
+
throw new Error(`webtty: failed to read config at ${getConfigPath()}: ${err.message}`);
|
|
188
|
+
}
|
|
189
|
+
let parsed;
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(raw);
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error(`webtty: invalid JSON in config file ${getConfigPath()}`);
|
|
194
|
+
}
|
|
195
|
+
const p = parsed;
|
|
196
|
+
return {
|
|
197
|
+
...DEFAULT_CONFIG,
|
|
198
|
+
...typeof p.port === "number" && { port: p.port },
|
|
199
|
+
...typeof p.host === "string" && { host: p.host },
|
|
200
|
+
...typeof p.shell === "string" && { shell: p.shell },
|
|
201
|
+
...typeof p.term === "string" && { term: p.term },
|
|
202
|
+
...typeof p.colorTerm === "string" && { colorTerm: p.colorTerm },
|
|
203
|
+
...typeof p.scrollback === "number" && { scrollback: p.scrollback },
|
|
204
|
+
...typeof p.cols === "number" && { cols: p.cols },
|
|
205
|
+
...typeof p.rows === "number" && { rows: p.rows },
|
|
206
|
+
...typeof p.fontSize === "number" && { fontSize: p.fontSize },
|
|
207
|
+
...typeof p.fontFamily === "string" && { fontFamily: p.fontFamily },
|
|
208
|
+
...typeof p.cursorBlink === "boolean" && { cursorBlink: p.cursorBlink },
|
|
209
|
+
...p.theme && typeof p.theme === "object" && { theme: { ...DEFAULT_THEME, ...p.theme } }
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function saveConfig(_config) {
|
|
213
|
+
fs.mkdirSync(path.dirname(getConfigPath()), { recursive: true });
|
|
214
|
+
const content = JSON.stringify({
|
|
215
|
+
port: DEFAULT_CONFIG.port,
|
|
216
|
+
host: DEFAULT_CONFIG.host
|
|
217
|
+
}, null, 2);
|
|
218
|
+
fs.writeFileSync(getConfigPath(), content, "utf8");
|
|
219
|
+
}
|
|
220
|
+
|
|
131
221
|
// src/server/routes.ts
|
|
132
|
-
import
|
|
222
|
+
import path4 from "node:path";
|
|
133
223
|
|
|
134
224
|
// src/server/client.ts
|
|
135
|
-
function render(sessionId) {
|
|
225
|
+
function render(sessionId, config) {
|
|
226
|
+
const theme = config.theme;
|
|
227
|
+
const themeJson = JSON.stringify(theme, null, 8).replace(/^/gm, " ").trimStart();
|
|
136
228
|
return `<!doctype html>
|
|
137
229
|
<html lang="en">
|
|
138
230
|
<head>
|
|
139
231
|
<meta charset="UTF-8" />
|
|
140
232
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
141
|
-
<title
|
|
233
|
+
<title>${sessionId} | webtty</title>
|
|
234
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect x='5' y='5' width='90' height='90' rx='18' fill='white'/><text x='8' y='73' font-size='58' font-family='monospace' font-weight='bold' fill='%23161b22'>>_</text></svg>">
|
|
142
235
|
<style>
|
|
143
236
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
144
237
|
|
|
145
|
-
html, body, #terminal { width: 100%; height: 100%; overflow: hidden; background: #282A36; }
|
|
238
|
+
html, body, #terminal { width: 100%; height: 100%; overflow: hidden; background: ${theme.background ?? "#282A36"}; }
|
|
146
239
|
|
|
147
240
|
#terminal canvas { display: block; }
|
|
148
241
|
</style>
|
|
@@ -155,34 +248,13 @@ function render(sessionId) {
|
|
|
155
248
|
|
|
156
249
|
await init();
|
|
157
250
|
const term = new Terminal({
|
|
158
|
-
cols:
|
|
159
|
-
rows:
|
|
160
|
-
cursorBlink:
|
|
161
|
-
fontSize:
|
|
162
|
-
fontFamily:
|
|
163
|
-
scrollback:
|
|
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
|
-
},
|
|
251
|
+
cols: ${config.cols},
|
|
252
|
+
rows: ${config.rows},
|
|
253
|
+
cursorBlink: ${config.cursorBlink},
|
|
254
|
+
fontSize: ${config.fontSize},
|
|
255
|
+
fontFamily: ${JSON.stringify(config.fontFamily)},
|
|
256
|
+
scrollback: ${Math.ceil(config.scrollback / 80)},
|
|
257
|
+
theme: ${themeJson},
|
|
186
258
|
});
|
|
187
259
|
|
|
188
260
|
const fitAddon = new FitAddon();
|
|
@@ -201,8 +273,11 @@ function render(sessionId) {
|
|
|
201
273
|
const wsUrl = protocol + '//' + window.location.host + '/ws/' + sessionId + '?cols=' + term.cols + '&rows=' + term.rows;
|
|
202
274
|
ws = new WebSocket(wsUrl);
|
|
203
275
|
|
|
276
|
+
const DIM = '\\x1b[2m', YELLOW = '\\x1b[1;33m', ITALIC = '\\x1b[3m', RESET = '\\x1b[0m';
|
|
277
|
+
const tag = DIM + '[' + RESET + ' ' + YELLOW + 'webtty' + RESET + ' ' + DIM + ']' + RESET;
|
|
278
|
+
const msg = (text) => '\\r\\n' + tag + ' ' + DIM + ITALIC + text + RESET + '\\r\\n';
|
|
279
|
+
|
|
204
280
|
ws.onopen = () => {
|
|
205
|
-
console.log('[webtty] connected to session ' + sessionId);
|
|
206
281
|
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
207
282
|
};
|
|
208
283
|
|
|
@@ -212,22 +287,21 @@ function render(sessionId) {
|
|
|
212
287
|
|
|
213
288
|
ws.onclose = (event) => {
|
|
214
289
|
if (event.code === 4001) {
|
|
215
|
-
term.write('
|
|
290
|
+
term.write(msg('Session removed.'));
|
|
216
291
|
setTimeout(() => window.close(), 500);
|
|
217
292
|
return;
|
|
218
293
|
}
|
|
219
294
|
if (event.code === 1001) {
|
|
220
|
-
term.write('
|
|
295
|
+
term.write(msg('Server stopped.'));
|
|
221
296
|
setTimeout(() => window.close(), 500);
|
|
222
297
|
return;
|
|
223
298
|
}
|
|
224
|
-
|
|
225
|
-
term.write('\\r\\n\\x1b[31mConnection closed. Reconnecting in 2s...\\x1b[0m\\r\\n');
|
|
299
|
+
term.write(msg('Connection lost. Reconnecting in 2s...'));
|
|
226
300
|
setTimeout(connect, 2000);
|
|
227
301
|
};
|
|
228
302
|
|
|
229
303
|
ws.onerror = () => {
|
|
230
|
-
|
|
304
|
+
term.write(msg('WebSocket error.'));
|
|
231
305
|
};
|
|
232
306
|
}
|
|
233
307
|
|
|
@@ -254,7 +328,6 @@ function render(sessionId) {
|
|
|
254
328
|
}
|
|
255
329
|
|
|
256
330
|
// src/server/session.ts
|
|
257
|
-
var SCROLLBACK_MAX = 256 * 1024;
|
|
258
331
|
var sessionRegistry = new Map;
|
|
259
332
|
var lastUsedId = null;
|
|
260
333
|
function setLastUsedId(id) {
|
|
@@ -283,13 +356,13 @@ function sessionToJson(s) {
|
|
|
283
356
|
}
|
|
284
357
|
|
|
285
358
|
// src/server/static.ts
|
|
286
|
-
import
|
|
359
|
+
import fs2 from "node:fs";
|
|
287
360
|
import { createRequire as createRequire2 } from "node:module";
|
|
288
|
-
import
|
|
361
|
+
import path3 from "node:path";
|
|
289
362
|
import { fileURLToPath } from "node:url";
|
|
290
363
|
|
|
291
364
|
// src/utils.ts
|
|
292
|
-
import
|
|
365
|
+
import path2 from "node:path";
|
|
293
366
|
var MIME_TYPES = {
|
|
294
367
|
".html": "text/html",
|
|
295
368
|
".js": "application/javascript",
|
|
@@ -302,7 +375,7 @@ var MIME_TYPES = {
|
|
|
302
375
|
".ico": "image/x-icon"
|
|
303
376
|
};
|
|
304
377
|
function mimeType(filePath) {
|
|
305
|
-
const ext =
|
|
378
|
+
const ext = path2.extname(filePath);
|
|
306
379
|
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
307
380
|
}
|
|
308
381
|
function ghosttyWebRootFromMain(mainPath) {
|
|
@@ -311,15 +384,15 @@ function ghosttyWebRootFromMain(mainPath) {
|
|
|
311
384
|
|
|
312
385
|
// src/server/static.ts
|
|
313
386
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
314
|
-
var __dirname2 =
|
|
387
|
+
var __dirname2 = path3.dirname(__filename2);
|
|
315
388
|
var require2 = createRequire2(import.meta.url);
|
|
316
389
|
function findGhosttyWeb() {
|
|
317
390
|
try {
|
|
318
391
|
const ghosttyWebMain = require2.resolve("ghostty-web");
|
|
319
392
|
const ghosttyWebRoot = ghosttyWebRootFromMain(ghosttyWebMain);
|
|
320
|
-
const distPath =
|
|
321
|
-
const wasmPath =
|
|
322
|
-
if (
|
|
393
|
+
const distPath = path3.join(ghosttyWebRoot, "dist");
|
|
394
|
+
const wasmPath = path3.join(ghosttyWebRoot, "ghostty-vt.wasm");
|
|
395
|
+
if (fs2.existsSync(path3.join(distPath, "ghostty-web.js")) && fs2.existsSync(wasmPath)) {
|
|
323
396
|
return { distPath, wasmPath };
|
|
324
397
|
}
|
|
325
398
|
} catch {}
|
|
@@ -328,7 +401,7 @@ function findGhosttyWeb() {
|
|
|
328
401
|
}
|
|
329
402
|
function serveFile(filePath, res) {
|
|
330
403
|
const contentType = mimeType(filePath);
|
|
331
|
-
|
|
404
|
+
fs2.readFile(filePath, (err, data) => {
|
|
332
405
|
if (err) {
|
|
333
406
|
res.writeHead(404);
|
|
334
407
|
res.end("Not Found");
|
|
@@ -339,6 +412,172 @@ function serveFile(filePath, res) {
|
|
|
339
412
|
});
|
|
340
413
|
}
|
|
341
414
|
|
|
415
|
+
// src/server/websocket.ts
|
|
416
|
+
import { WebSocketServer } from "ws";
|
|
417
|
+
|
|
418
|
+
// src/pty/index.ts
|
|
419
|
+
var isBun = !!process.versions.bun;
|
|
420
|
+
console.log(`pty: ${isBun ? "Bun.Terminal" : "node-pty"}`);
|
|
421
|
+
var { spawn: _spawn } = await (isBun ? Promise.resolve().then(() => (init_bun(), exports_bun)) : Promise.resolve().then(() => (init_node(), exports_node)));
|
|
422
|
+
function spawnForSession(cols, rows, shell, term, colorTerm) {
|
|
423
|
+
return _spawn(shell, cols, rows, term, colorTerm);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/server/websocket.ts
|
|
427
|
+
var WS_CLOSE = {
|
|
428
|
+
SERVER_STOPPED: 1001,
|
|
429
|
+
BAD_REQUEST: 1008,
|
|
430
|
+
SESSION_GONE: 4001
|
|
431
|
+
};
|
|
432
|
+
function closeClients(session, code, reason) {
|
|
433
|
+
session.pty?.kill();
|
|
434
|
+
for (const client of session.clients)
|
|
435
|
+
client.close(code, reason);
|
|
436
|
+
}
|
|
437
|
+
function closeSession(session) {
|
|
438
|
+
closeClients(session, WS_CLOSE.SESSION_GONE, "session deleted");
|
|
439
|
+
}
|
|
440
|
+
function closeAllSessions() {
|
|
441
|
+
for (const session of sessionRegistry.values()) {
|
|
442
|
+
closeClients(session, WS_CLOSE.SERVER_STOPPED, "server stopped");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
var DIM = "\x1B[2m";
|
|
446
|
+
var RESET = "\x1B[0m";
|
|
447
|
+
var BOLD = "\x1B[1m";
|
|
448
|
+
var ITALIC = "\x1B[3m";
|
|
449
|
+
var YELLOW = "\x1B[1;33m";
|
|
450
|
+
var CYAN = "\x1B[1;36m";
|
|
451
|
+
var isBun2 = process.execPath.includes("bun");
|
|
452
|
+
var PKG_RUNNER = isBun2 ? "bunx" : "npx";
|
|
453
|
+
function sessionBanner() {
|
|
454
|
+
const W = 48;
|
|
455
|
+
const pipe = (content) => `${CYAN}║${RESET}${content}${CYAN}║${RESET}`;
|
|
456
|
+
const blank = pipe(" ".repeat(W));
|
|
457
|
+
const titleVis = " [ webtty ] Terminal UI in the browser";
|
|
458
|
+
const titleStr = ` ${DIM}[${RESET} ${BOLD}${YELLOW}webtty${RESET} ${DIM}] Terminal UI in the browser${RESET}`;
|
|
459
|
+
const titleLine = pipe(titleStr + " ".repeat(W - titleVis.length));
|
|
460
|
+
const cmd = `${PKG_RUNNER} webtty help`;
|
|
461
|
+
const helpVis = ` Run \`${cmd}\` for more information.`;
|
|
462
|
+
const helpStr = ` ${DIM}Run \`${RESET}${ITALIC}${cmd}${RESET}${DIM}\` for more information.${RESET}`;
|
|
463
|
+
const helpLine = pipe(helpStr + " ".repeat(W - helpVis.length));
|
|
464
|
+
return [
|
|
465
|
+
`${CYAN}╔${"═".repeat(W)}╗${RESET}`,
|
|
466
|
+
`\r
|
|
467
|
+
`,
|
|
468
|
+
blank,
|
|
469
|
+
`\r
|
|
470
|
+
`,
|
|
471
|
+
titleLine,
|
|
472
|
+
`\r
|
|
473
|
+
`,
|
|
474
|
+
blank,
|
|
475
|
+
`\r
|
|
476
|
+
`,
|
|
477
|
+
helpLine,
|
|
478
|
+
`\r
|
|
479
|
+
`,
|
|
480
|
+
blank,
|
|
481
|
+
`\r
|
|
482
|
+
`,
|
|
483
|
+
`${CYAN}╚${"═".repeat(W)}╝${RESET}`,
|
|
484
|
+
`\r
|
|
485
|
+
`,
|
|
486
|
+
`\r
|
|
487
|
+
`
|
|
488
|
+
].join("");
|
|
489
|
+
}
|
|
490
|
+
function createWebSocketServer(httpServer) {
|
|
491
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
492
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
493
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
494
|
+
if (url.pathname.match(/^\/ws\/([^/]+)$/)) {
|
|
495
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
496
|
+
} else {
|
|
497
|
+
socket.destroy();
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
wss.on("connection", (ws, req) => {
|
|
501
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
502
|
+
const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/);
|
|
503
|
+
if (!wsMatch) {
|
|
504
|
+
ws.close();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
let id;
|
|
508
|
+
try {
|
|
509
|
+
id = decodeURIComponent(wsMatch[1]);
|
|
510
|
+
} catch {
|
|
511
|
+
ws.close(WS_CLOSE.BAD_REQUEST, "Bad Request");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const cols = Math.max(1, Math.min(1000, Number.parseInt(url.searchParams.get("cols") ?? "80", 10) || 80));
|
|
515
|
+
const rows = Math.max(1, Math.min(500, Number.parseInt(url.searchParams.get("rows") ?? "24", 10) || 24));
|
|
516
|
+
if (!sessionRegistry.has(id)) {
|
|
517
|
+
ws.close(WS_CLOSE.SESSION_GONE, "session deleted");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const session = sessionRegistry.get(id);
|
|
521
|
+
if (!session) {
|
|
522
|
+
ws.close(WS_CLOSE.SESSION_GONE, "session deleted");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
session.clients.add(ws);
|
|
526
|
+
setLastUsedId(id);
|
|
527
|
+
if (!session.pty) {
|
|
528
|
+
const config = loadConfig();
|
|
529
|
+
session.pty = spawnForSession(cols, rows, config.shell, config.term, config.colorTerm);
|
|
530
|
+
session.pty.onData((data) => {
|
|
531
|
+
session.scrollback = (session.scrollback + data).slice(-config.scrollback);
|
|
532
|
+
for (const client of session.clients) {
|
|
533
|
+
if (client.readyState === client.OPEN) {
|
|
534
|
+
client.send(data, { binary: false });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
session.pty.onExit(() => {
|
|
539
|
+
sessionRegistry.delete(session.id);
|
|
540
|
+
for (const client of session.clients) {
|
|
541
|
+
if (client.readyState === client.OPEN) {
|
|
542
|
+
client.close(WS_CLOSE.SESSION_GONE, "shell exited");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
session.pty = null;
|
|
546
|
+
});
|
|
547
|
+
const banner = sessionBanner();
|
|
548
|
+
ws.send(banner);
|
|
549
|
+
session.scrollback = banner;
|
|
550
|
+
session.pty.write(`
|
|
551
|
+
`);
|
|
552
|
+
} else {
|
|
553
|
+
if (session.scrollback) {
|
|
554
|
+
ws.send(session.scrollback, { binary: false });
|
|
555
|
+
}
|
|
556
|
+
session.pty.resize(cols, rows);
|
|
557
|
+
}
|
|
558
|
+
ws.on("message", (data) => {
|
|
559
|
+
const message = data.toString("utf8");
|
|
560
|
+
if (message.startsWith("{")) {
|
|
561
|
+
try {
|
|
562
|
+
const msg = JSON.parse(message);
|
|
563
|
+
if (msg.type === "resize") {
|
|
564
|
+
const c = Math.max(1, Math.min(1000, Math.trunc(msg.cols) || 80));
|
|
565
|
+
const r = Math.max(1, Math.min(500, Math.trunc(msg.rows) || 24));
|
|
566
|
+
session.pty?.resize(c, r);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
} catch {}
|
|
570
|
+
}
|
|
571
|
+
session.pty?.write(message);
|
|
572
|
+
});
|
|
573
|
+
ws.on("close", () => {
|
|
574
|
+
session.clients.delete(ws);
|
|
575
|
+
});
|
|
576
|
+
ws.on("error", () => {});
|
|
577
|
+
});
|
|
578
|
+
return wss;
|
|
579
|
+
}
|
|
580
|
+
|
|
342
581
|
// src/server/routes.ts
|
|
343
582
|
var MAX_BODY = 64 * 1024;
|
|
344
583
|
function decodeId(raw) {
|
|
@@ -481,9 +720,7 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
481
720
|
sessionRegistry.delete(id);
|
|
482
721
|
if (lastUsedId === id)
|
|
483
722
|
setLastUsedId(null);
|
|
484
|
-
|
|
485
|
-
client.close(4001, "session deleted");
|
|
486
|
-
session.pty?.kill();
|
|
723
|
+
closeSession(session);
|
|
487
724
|
res.writeHead(204);
|
|
488
725
|
res.end();
|
|
489
726
|
return;
|
|
@@ -509,12 +746,12 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
509
746
|
return;
|
|
510
747
|
}
|
|
511
748
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
512
|
-
res.end(render(id));
|
|
749
|
+
res.end(render(id, loadConfig()));
|
|
513
750
|
return;
|
|
514
751
|
}
|
|
515
752
|
if (pathname.startsWith("/dist/")) {
|
|
516
|
-
const filePath =
|
|
517
|
-
if (!filePath.startsWith(
|
|
753
|
+
const filePath = path4.resolve(distPath, pathname.slice(6));
|
|
754
|
+
if (!filePath.startsWith(path4.resolve(distPath) + path4.sep) && filePath !== path4.resolve(distPath)) {
|
|
518
755
|
res.writeHead(404);
|
|
519
756
|
res.end("Not Found");
|
|
520
757
|
return;
|
|
@@ -530,151 +767,13 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
530
767
|
res.end("Not Found");
|
|
531
768
|
}
|
|
532
769
|
|
|
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
770
|
// src/server/index.ts
|
|
655
|
-
var
|
|
771
|
+
var config = loadConfig();
|
|
772
|
+
var HTTP_PORT = Number(process.env.PORT) || config.port;
|
|
773
|
+
var HTTP_HOST = config.host;
|
|
656
774
|
var { distPath, wasmPath } = findGhosttyWeb();
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
for (const session of sessionRegistry.values()) {
|
|
660
|
-
session.pty?.kill();
|
|
661
|
-
for (const client of session.clients)
|
|
662
|
-
client.close(1001, "server stopped");
|
|
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
|
-
}
|
|
775
|
+
function shutdown() {
|
|
776
|
+
closeAllSessions();
|
|
678
777
|
wss.close();
|
|
679
778
|
const exit = () => process.exit(0);
|
|
680
779
|
const shutdownTimeout = setTimeout(exit, 1000);
|
|
@@ -682,9 +781,19 @@ Shutting down...`);
|
|
|
682
781
|
clearTimeout(shutdownTimeout);
|
|
683
782
|
exit();
|
|
684
783
|
});
|
|
784
|
+
}
|
|
785
|
+
var httpServer = http.createServer((req, res) => {
|
|
786
|
+
handleRequest(req, res, distPath, wasmPath, shutdown);
|
|
787
|
+
});
|
|
788
|
+
var wss = createWebSocketServer(httpServer);
|
|
789
|
+
process.on("SIGINT", () => {
|
|
790
|
+
console.log(`
|
|
791
|
+
|
|
792
|
+
Shutting down...`);
|
|
793
|
+
shutdown();
|
|
685
794
|
});
|
|
686
|
-
httpServer.listen(HTTP_PORT,
|
|
687
|
-
console.log(`listening on http
|
|
795
|
+
httpServer.listen(HTTP_PORT, HTTP_HOST, () => {
|
|
796
|
+
console.log(`listening on http://${HTTP_HOST}:${HTTP_PORT}`);
|
|
688
797
|
});
|
|
689
798
|
|
|
690
|
-
//# debugId=
|
|
799
|
+
//# debugId=DDDEAF9DCC3651C664756E2164756E21
|