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.
@@ -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,22 +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
294
  if (event.code === 1001) {
220
- term.write('\\r\\n\\x1b[33mServer stopped.\\x1b[0m\\r\\n');
295
+ term.write(msg('Server stopped.'));
221
296
  setTimeout(() => window.close(), 500);
222
297
  return;
223
298
  }
224
- console.log('[webtty] disconnected, reconnecting in 2s...');
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
- console.error('[webtty] websocket error');
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 fs from "node:fs";
359
+ import fs2 from "node:fs";
287
360
  import { createRequire as createRequire2 } from "node:module";
288
- import path2 from "node:path";
361
+ import path3 from "node:path";
289
362
  import { fileURLToPath } from "node:url";
290
363
 
291
364
  // src/utils.ts
292
- import path from "node:path";
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 = path.extname(filePath);
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 = path2.dirname(__filename2);
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 = path2.join(ghosttyWebRoot, "dist");
321
- const wasmPath = path2.join(ghosttyWebRoot, "ghostty-vt.wasm");
322
- 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)) {
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
- fs.readFile(filePath, (err, data) => {
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
- for (const client of session.clients)
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 = path3.resolve(distPath, pathname.slice(6));
517
- 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)) {
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 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;
656
774
  var { distPath, wasmPath } = findGhosttyWeb();
657
- var httpServer = http.createServer((req, res) => {
658
- handleRequest(req, res, distPath, wasmPath, () => {
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, "127.0.0.1", () => {
687
- 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}`);
688
797
  });
689
798
 
690
- //# debugId=243D0B6704BD6DB364756E2164756E21
799
+ //# debugId=DDDEAF9DCC3651C664756E2164756E21