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.
@@ -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: "xterm-256color", COLORTERM: "truecolor" }
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: "xterm-256color",
102
+ name: term,
103
103
  cols,
104
104
  rows,
105
105
  cwd: homedir2(),
106
- env: { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor" }
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 path3 from "node:path";
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>webtty — ${sessionId}</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: 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
- },
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('\\r\\n\\x1b[31mSession removed.\\x1b[0m\\r\\n');
290
+ term.write(msg('Session removed.'));
216
291
  setTimeout(() => window.close(), 500);
217
292
  return;
218
293
  }
219
- console.log('[webtty] disconnected, reconnecting in 2s...');
220
- term.write('\\r\\n\\x1b[31mConnection closed. Reconnecting in 2s...\\x1b[0m\\r\\n');
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
- console.error('[webtty] websocket error');
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 fs from "node:fs";
359
+ import fs2 from "node:fs";
282
360
  import { createRequire as createRequire2 } from "node:module";
283
- import path2 from "node:path";
361
+ import path3 from "node:path";
284
362
  import { fileURLToPath } from "node:url";
285
363
 
286
364
  // src/utils.ts
287
- import path from "node:path";
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 = path.extname(filePath);
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 = path2.dirname(__filename2);
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 = path2.join(ghosttyWebRoot, "dist");
316
- const wasmPath = path2.join(ghosttyWebRoot, "ghostty-vt.wasm");
317
- if (fs.existsSync(path2.join(distPath, "ghostty-web.js")) && fs.existsSync(wasmPath)) {
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
- fs.readFile(filePath, (err, data) => {
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
- for (const client of session.clients)
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 = path3.resolve(distPath, pathname.slice(6));
512
- if (!filePath.startsWith(path3.resolve(distPath) + path3.sep) && filePath !== path3.resolve(distPath)) {
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 HTTP_PORT = Number(process.env.PORT) || 2346;
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
- var httpServer = http.createServer((req, res) => {
653
- handleRequest(req, res, distPath, wasmPath, () => {
654
- for (const session of sessionRegistry.values()) {
655
- session.pty?.kill();
656
- for (const client of session.clients)
657
- client.close();
658
- }
659
- wss.close();
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
- for (const session of sessionRegistry.values()) {
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, "127.0.0.1", () => {
677
- console.log(`listening on http://127.0.0.1:${HTTP_PORT}`);
795
+ httpServer.listen(HTTP_PORT, HTTP_HOST, () => {
796
+ console.log(`listening on http://${HTTP_HOST}:${HTTP_PORT}`);
678
797
  });
679
798
 
680
- //# debugId=102CAB57A485E36364756E2164756E21
799
+ //# debugId=DDDEAF9DCC3651C664756E2164756E21