webtty 1.1.0 → 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 +20 -1
- package/dist/cli/index.js +48 -18
- package/dist/cli/index.js.map +4 -4
- package/dist/server/index.js +318 -199
- 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,17 +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
|
-
|
|
220
|
-
|
|
294
|
+
if (event.code === 1001) {
|
|
295
|
+
term.write(msg('Server stopped.'));
|
|
296
|
+
setTimeout(() => window.close(), 500);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
term.write(msg('Connection lost. Reconnecting in 2s...'));
|
|
221
300
|
setTimeout(connect, 2000);
|
|
222
301
|
};
|
|
223
302
|
|
|
224
303
|
ws.onerror = () => {
|
|
225
|
-
|
|
304
|
+
term.write(msg('WebSocket error.'));
|
|
226
305
|
};
|
|
227
306
|
}
|
|
228
307
|
|
|
@@ -249,7 +328,6 @@ function render(sessionId) {
|
|
|
249
328
|
}
|
|
250
329
|
|
|
251
330
|
// src/server/session.ts
|
|
252
|
-
var SCROLLBACK_MAX = 256 * 1024;
|
|
253
331
|
var sessionRegistry = new Map;
|
|
254
332
|
var lastUsedId = null;
|
|
255
333
|
function setLastUsedId(id) {
|
|
@@ -278,13 +356,13 @@ function sessionToJson(s) {
|
|
|
278
356
|
}
|
|
279
357
|
|
|
280
358
|
// src/server/static.ts
|
|
281
|
-
import
|
|
359
|
+
import fs2 from "node:fs";
|
|
282
360
|
import { createRequire as createRequire2 } from "node:module";
|
|
283
|
-
import
|
|
361
|
+
import path3 from "node:path";
|
|
284
362
|
import { fileURLToPath } from "node:url";
|
|
285
363
|
|
|
286
364
|
// src/utils.ts
|
|
287
|
-
import
|
|
365
|
+
import path2 from "node:path";
|
|
288
366
|
var MIME_TYPES = {
|
|
289
367
|
".html": "text/html",
|
|
290
368
|
".js": "application/javascript",
|
|
@@ -297,7 +375,7 @@ var MIME_TYPES = {
|
|
|
297
375
|
".ico": "image/x-icon"
|
|
298
376
|
};
|
|
299
377
|
function mimeType(filePath) {
|
|
300
|
-
const ext =
|
|
378
|
+
const ext = path2.extname(filePath);
|
|
301
379
|
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
302
380
|
}
|
|
303
381
|
function ghosttyWebRootFromMain(mainPath) {
|
|
@@ -306,15 +384,15 @@ function ghosttyWebRootFromMain(mainPath) {
|
|
|
306
384
|
|
|
307
385
|
// src/server/static.ts
|
|
308
386
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
309
|
-
var __dirname2 =
|
|
387
|
+
var __dirname2 = path3.dirname(__filename2);
|
|
310
388
|
var require2 = createRequire2(import.meta.url);
|
|
311
389
|
function findGhosttyWeb() {
|
|
312
390
|
try {
|
|
313
391
|
const ghosttyWebMain = require2.resolve("ghostty-web");
|
|
314
392
|
const ghosttyWebRoot = ghosttyWebRootFromMain(ghosttyWebMain);
|
|
315
|
-
const distPath =
|
|
316
|
-
const wasmPath =
|
|
317
|
-
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)) {
|
|
318
396
|
return { distPath, wasmPath };
|
|
319
397
|
}
|
|
320
398
|
} catch {}
|
|
@@ -323,7 +401,7 @@ function findGhosttyWeb() {
|
|
|
323
401
|
}
|
|
324
402
|
function serveFile(filePath, res) {
|
|
325
403
|
const contentType = mimeType(filePath);
|
|
326
|
-
|
|
404
|
+
fs2.readFile(filePath, (err, data) => {
|
|
327
405
|
if (err) {
|
|
328
406
|
res.writeHead(404);
|
|
329
407
|
res.end("Not Found");
|
|
@@ -334,6 +412,172 @@ function serveFile(filePath, res) {
|
|
|
334
412
|
});
|
|
335
413
|
}
|
|
336
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
|
+
|
|
337
581
|
// src/server/routes.ts
|
|
338
582
|
var MAX_BODY = 64 * 1024;
|
|
339
583
|
function decodeId(raw) {
|
|
@@ -476,9 +720,7 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
476
720
|
sessionRegistry.delete(id);
|
|
477
721
|
if (lastUsedId === id)
|
|
478
722
|
setLastUsedId(null);
|
|
479
|
-
|
|
480
|
-
client.close(4001, "session deleted");
|
|
481
|
-
session.pty?.kill();
|
|
723
|
+
closeSession(session);
|
|
482
724
|
res.writeHead(204);
|
|
483
725
|
res.end();
|
|
484
726
|
return;
|
|
@@ -504,12 +746,12 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
504
746
|
return;
|
|
505
747
|
}
|
|
506
748
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
507
|
-
res.end(render(id));
|
|
749
|
+
res.end(render(id, loadConfig()));
|
|
508
750
|
return;
|
|
509
751
|
}
|
|
510
752
|
if (pathname.startsWith("/dist/")) {
|
|
511
|
-
const filePath =
|
|
512
|
-
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)) {
|
|
513
755
|
res.writeHead(404);
|
|
514
756
|
res.end("Not Found");
|
|
515
757
|
return;
|
|
@@ -525,156 +767,33 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
|
|
|
525
767
|
res.end("Not Found");
|
|
526
768
|
}
|
|
527
769
|
|
|
528
|
-
// src/server/websocket.ts
|
|
529
|
-
import { WebSocketServer } from "ws";
|
|
530
|
-
|
|
531
|
-
// src/pty/index.ts
|
|
532
|
-
var isBun = !!process.versions.bun;
|
|
533
|
-
console.log(`pty: ${isBun ? "Bun.Terminal" : "node-pty"}`);
|
|
534
|
-
var { spawn: _spawn } = await (isBun ? Promise.resolve().then(() => (init_bun(), exports_bun)) : Promise.resolve().then(() => (init_node(), exports_node)));
|
|
535
|
-
function spawnForSession(cols, rows) {
|
|
536
|
-
const shell = process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "/bin/bash";
|
|
537
|
-
return _spawn(shell, cols, rows);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// src/server/websocket.ts
|
|
541
|
-
function createWebSocketServer(httpServer) {
|
|
542
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
543
|
-
httpServer.on("upgrade", (req, socket, head) => {
|
|
544
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
545
|
-
if (url.pathname.match(/^\/ws\/([^/]+)$/)) {
|
|
546
|
-
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
547
|
-
} else {
|
|
548
|
-
socket.destroy();
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
wss.on("connection", (ws, req) => {
|
|
552
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
553
|
-
const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/);
|
|
554
|
-
if (!wsMatch) {
|
|
555
|
-
ws.close();
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
let id;
|
|
559
|
-
try {
|
|
560
|
-
id = decodeURIComponent(wsMatch[1]);
|
|
561
|
-
} catch {
|
|
562
|
-
ws.close(1008, "Bad Request");
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const cols = Math.max(1, Math.min(1000, Number.parseInt(url.searchParams.get("cols") ?? "80", 10) || 80));
|
|
566
|
-
const rows = Math.max(1, Math.min(500, Number.parseInt(url.searchParams.get("rows") ?? "24", 10) || 24));
|
|
567
|
-
if (!sessionRegistry.has(id)) {
|
|
568
|
-
ws.close(4001, "session deleted");
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
const session = sessionRegistry.get(id);
|
|
572
|
-
if (!session) {
|
|
573
|
-
ws.close(4001, "session deleted");
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
session.clients.add(ws);
|
|
577
|
-
setLastUsedId(id);
|
|
578
|
-
if (!session.pty) {
|
|
579
|
-
session.pty = spawnForSession(cols, rows);
|
|
580
|
-
session.pty.onData((data) => {
|
|
581
|
-
session.scrollback = (session.scrollback + data).slice(-SCROLLBACK_MAX);
|
|
582
|
-
for (const client of session.clients) {
|
|
583
|
-
if (client.readyState === client.OPEN) {
|
|
584
|
-
client.send(data, { binary: false });
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
session.pty.onExit(() => {
|
|
589
|
-
sessionRegistry.delete(session.id);
|
|
590
|
-
for (const client of session.clients) {
|
|
591
|
-
if (client.readyState === client.OPEN) {
|
|
592
|
-
client.close(4001, "shell exited");
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
session.pty = null;
|
|
596
|
-
});
|
|
597
|
-
const C = "\x1B[1;36m";
|
|
598
|
-
const G = "\x1B[1;32m";
|
|
599
|
-
const Y = "\x1B[1;33m";
|
|
600
|
-
const R = "\x1B[0m";
|
|
601
|
-
const banner = [
|
|
602
|
-
`${C}╔══════════════════════════════════════════════════════════════╗${R}\r
|
|
603
|
-
`,
|
|
604
|
-
`${C}║${R} ${G}Welcome to webtty!${R} ${C}║${R}\r
|
|
605
|
-
`,
|
|
606
|
-
`${C}║${R} ${C}║${R}\r
|
|
607
|
-
`,
|
|
608
|
-
`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r
|
|
609
|
-
`,
|
|
610
|
-
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r
|
|
611
|
-
`,
|
|
612
|
-
`${C}╚══════════════════════════════════════════════════════════════╝${R}\r
|
|
613
|
-
\r
|
|
614
|
-
`
|
|
615
|
-
].join("");
|
|
616
|
-
ws.send(banner);
|
|
617
|
-
session.scrollback = banner;
|
|
618
|
-
session.pty.write(`
|
|
619
|
-
`);
|
|
620
|
-
} else {
|
|
621
|
-
if (session.scrollback) {
|
|
622
|
-
ws.send(session.scrollback, { binary: false });
|
|
623
|
-
}
|
|
624
|
-
session.pty.resize(cols, rows);
|
|
625
|
-
}
|
|
626
|
-
ws.on("message", (data) => {
|
|
627
|
-
const message = data.toString("utf8");
|
|
628
|
-
if (message.startsWith("{")) {
|
|
629
|
-
try {
|
|
630
|
-
const msg = JSON.parse(message);
|
|
631
|
-
if (msg.type === "resize") {
|
|
632
|
-
const c = Math.max(1, Math.min(1000, Math.trunc(msg.cols) || 80));
|
|
633
|
-
const r = Math.max(1, Math.min(500, Math.trunc(msg.rows) || 24));
|
|
634
|
-
session.pty?.resize(c, r);
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
} catch {}
|
|
638
|
-
}
|
|
639
|
-
session.pty?.write(message);
|
|
640
|
-
});
|
|
641
|
-
ws.on("close", () => {
|
|
642
|
-
session.clients.delete(ws);
|
|
643
|
-
});
|
|
644
|
-
ws.on("error", () => {});
|
|
645
|
-
});
|
|
646
|
-
return wss;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
770
|
// src/server/index.ts
|
|
650
|
-
var
|
|
771
|
+
var config = loadConfig();
|
|
772
|
+
var HTTP_PORT = Number(process.env.PORT) || config.port;
|
|
773
|
+
var HTTP_HOST = config.host;
|
|
651
774
|
var { distPath, wasmPath } = findGhosttyWeb();
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
httpServer.close(() => process.exit(0));
|
|
775
|
+
function shutdown() {
|
|
776
|
+
closeAllSessions();
|
|
777
|
+
wss.close();
|
|
778
|
+
const exit = () => process.exit(0);
|
|
779
|
+
const shutdownTimeout = setTimeout(exit, 1000);
|
|
780
|
+
httpServer.close(() => {
|
|
781
|
+
clearTimeout(shutdownTimeout);
|
|
782
|
+
exit();
|
|
661
783
|
});
|
|
784
|
+
}
|
|
785
|
+
var httpServer = http.createServer((req, res) => {
|
|
786
|
+
handleRequest(req, res, distPath, wasmPath, shutdown);
|
|
662
787
|
});
|
|
663
788
|
var wss = createWebSocketServer(httpServer);
|
|
664
789
|
process.on("SIGINT", () => {
|
|
665
790
|
console.log(`
|
|
666
791
|
|
|
667
792
|
Shutting down...`);
|
|
668
|
-
|
|
669
|
-
session.pty?.kill();
|
|
670
|
-
for (const client of session.clients)
|
|
671
|
-
client.close();
|
|
672
|
-
}
|
|
673
|
-
wss.close();
|
|
674
|
-
process.exit(0);
|
|
793
|
+
shutdown();
|
|
675
794
|
});
|
|
676
|
-
httpServer.listen(HTTP_PORT,
|
|
677
|
-
console.log(`listening on http
|
|
795
|
+
httpServer.listen(HTTP_PORT, HTTP_HOST, () => {
|
|
796
|
+
console.log(`listening on http://${HTTP_HOST}:${HTTP_PORT}`);
|
|
678
797
|
});
|
|
679
798
|
|
|
680
|
-
//# debugId=
|
|
799
|
+
//# debugId=DDDEAF9DCC3651C664756E2164756E21
|