termbeam 1.3.0 → 1.5.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/src/auth.js CHANGED
@@ -19,18 +19,69 @@ const LOGIN_HTML = `<!DOCTYPE html>
19
19
  --border-subtle:#d0d0d0; --text:#1e1e1e; --text-secondary:#616161;
20
20
  --text-dim:#767676; --accent:#0078d4; --accent-hover:#106ebe;
21
21
  --accent-active:#005a9e; --danger:#e51400; --shadow:rgba(0,0,0,0.06); }
22
+ [data-theme='monokai'] { --bg:#272822; --surface:#1e1f1c; --border:#49483e;
23
+ --border-subtle:#5c5c4f; --text:#f8f8f2; --text-secondary:#a59f85; --text-dim:#75715e;
24
+ --accent:#a6e22e; --accent-hover:#b8f53c; --accent-active:#8acc16;
25
+ --danger:#f92672; --shadow:rgba(0,0,0,0.3); }
26
+ [data-theme='solarized-dark'] { --bg:#002b36; --surface:#073642; --border:#586e75;
27
+ --border-subtle:#657b83; --text:#839496; --text-secondary:#657b83; --text-dim:#586e75;
28
+ --accent:#268bd2; --accent-hover:#379ce3; --accent-active:#1a7abf;
29
+ --danger:#dc322f; --shadow:rgba(0,0,0,0.25); }
30
+ [data-theme='solarized-light'] { --bg:#fdf6e3; --surface:#eee8d5; --border:#93a1a1;
31
+ --border-subtle:#839496; --text:#657b83; --text-secondary:#93a1a1; --text-dim:#a0a0a0;
32
+ --accent:#268bd2; --accent-hover:#379ce3; --accent-active:#1a7abf;
33
+ --danger:#dc322f; --shadow:rgba(0,0,0,0.08); }
34
+ [data-theme='nord'] { --bg:#2e3440; --surface:#3b4252; --border:#434c5e;
35
+ --border-subtle:#4c566a; --text:#d8dee9; --text-secondary:#b0bac9; --text-dim:#7b88a1;
36
+ --accent:#88c0d0; --accent-hover:#9fd4e4; --accent-active:#6aafbf;
37
+ --danger:#bf616a; --shadow:rgba(0,0,0,0.2); }
38
+ [data-theme='dracula'] { --bg:#282a36; --surface:#343746; --border:#44475a;
39
+ --border-subtle:#525568; --text:#f8f8f2; --text-secondary:#c1c4d2; --text-dim:#8e92a4;
40
+ --accent:#bd93f9; --accent-hover:#d0b0ff; --accent-active:#a77de7;
41
+ --danger:#ff5555; --shadow:rgba(0,0,0,0.25); }
42
+ [data-theme='github-dark'] { --bg:#0d1117; --surface:#161b22; --border:#30363d;
43
+ --border-subtle:#3d444d; --text:#c9d1d9; --text-secondary:#8b949e; --text-dim:#6e7681;
44
+ --accent:#58a6ff; --accent-hover:#79b8ff; --accent-active:#388bfd;
45
+ --danger:#f85149; --shadow:rgba(0,0,0,0.3); }
46
+ [data-theme='one-dark'] { --bg:#282c34; --surface:#21252b; --border:#3e4452;
47
+ --border-subtle:#4b5263; --text:#abb2bf; --text-secondary:#7f848e; --text-dim:#5c6370;
48
+ --accent:#61afef; --accent-hover:#7dc0ff; --accent-active:#4d9ede;
49
+ --danger:#e06c75; --shadow:rgba(0,0,0,0.25); }
50
+ [data-theme='catppuccin'] { --bg:#1e1e2e; --surface:#313244; --border:#45475a;
51
+ --border-subtle:#585b70; --text:#cdd6f4; --text-secondary:#a6adc8; --text-dim:#7f849c;
52
+ --accent:#89b4fa; --accent-hover:#b4d0ff; --accent-active:#5c9de3;
53
+ --danger:#f38ba8; --shadow:rgba(0,0,0,0.2); }
54
+ [data-theme='gruvbox'] { --bg:#282828; --surface:#3c3836; --border:#504945;
55
+ --border-subtle:#665c54; --text:#ebdbb2; --text-secondary:#d5c4a1; --text-dim:#a89984;
56
+ --accent:#83a598; --accent-hover:#9dbfb4; --accent-active:#6a8f8a;
57
+ --danger:#fb4934; --shadow:rgba(0,0,0,0.25); }
58
+ [data-theme='night-owl'] { --bg:#011627; --surface:#0d2a45; --border:#1d3b53;
59
+ --border-subtle:#264863; --text:#d6deeb; --text-secondary:#8badc1; --text-dim:#5f7e97;
60
+ --accent:#7fdbca; --accent-hover:#9ff0e0; --accent-active:#62c5b5;
61
+ --danger:#ef5350; --shadow:rgba(0,0,0,0.3); }
22
62
  * { margin:0; padding:0; box-sizing:border-box; }
23
63
  html, body { height:100%; background:var(--bg); color:var(--text);
24
64
  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
25
65
  display:flex; flex-direction:column; align-items:center; justify-content:center;
26
66
  transition:background 0.3s,color 0.3s;
27
67
  padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
28
- .theme-toggle { position:fixed; top:16px; right:16px; background:none;
29
- border:1px solid var(--border); color:var(--text-dim); width:32px; height:32px;
30
- border-radius:8px; cursor:pointer; display:flex; align-items:center;
31
- justify-content:center; font-size:16px; transition:color 0.15s,border-color 0.15s,background 0.15s;
32
- -webkit-tap-highlight-color:transparent; z-index:10; }
68
+ .theme-wrap { position:fixed; top:16px; right:16px; z-index:10; }
69
+ .theme-toggle { background:none; border:1px solid var(--border); color:var(--text-dim);
70
+ width:32px; height:32px; border-radius:8px; cursor:pointer; display:flex;
71
+ align-items:center; justify-content:center; font-size:16px;
72
+ transition:color 0.15s,border-color 0.15s,background 0.15s;
73
+ -webkit-tap-highlight-color:transparent; }
33
74
  .theme-toggle:hover { color:var(--text); border-color:var(--border-subtle); background:var(--border); }
75
+ .theme-picker { display:none; position:absolute; top:calc(100% + 4px); right:0;
76
+ background:var(--surface); border:1px solid var(--border); border-radius:8px;
77
+ min-width:160px; padding:4px 0; box-shadow:0 4px 12px var(--shadow); }
78
+ .theme-picker.open { display:block; }
79
+ .theme-option { display:flex; align-items:center; gap:8px; padding:7px 12px;
80
+ cursor:pointer; font-size:13px; color:var(--text); transition:background 0.1s; white-space:nowrap; }
81
+ .theme-option:hover { background:var(--border); }
82
+ .theme-option.active { color:var(--accent); }
83
+ .theme-swatch { width:14px; height:14px; border-radius:50%; display:inline-block;
84
+ flex-shrink:0; border:1px solid rgba(128,128,128,0.3); }
34
85
  .card { background:var(--surface); border:1px solid var(--border); border-radius:12px;
35
86
  padding:32px 24px; width:320px; max-width:calc(100vw - 32px); text-align:center;
36
87
  box-shadow:0 2px 8px var(--shadow); transition:background 0.3s,border-color 0.3s,box-shadow 0.3s; }
@@ -51,7 +102,28 @@ const LOGIN_HTML = `<!DOCTYPE html>
51
102
  </style>
52
103
  </head>
53
104
  <body>
54
- <button class="theme-toggle" id="themeBtn" aria-label="Toggle theme">🌙</button>
105
+ <div class="theme-wrap" id="themeWrap">
106
+ <button class="theme-toggle" id="themeBtn" aria-label="Switch theme">
107
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
108
+ <circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/>
109
+ <path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>
110
+ </svg>
111
+ </button>
112
+ <div class="theme-picker" id="themePicker">
113
+ <div class="theme-option" data-theme-option="dark"><span class="theme-swatch" style="background:#1e1e1e"></span>Dark</div>
114
+ <div class="theme-option" data-theme-option="light"><span class="theme-swatch" style="background:#ffffff"></span>Light</div>
115
+ <div class="theme-option" data-theme-option="monokai"><span class="theme-swatch" style="background:#272822"></span>Monokai</div>
116
+ <div class="theme-option" data-theme-option="solarized-dark"><span class="theme-swatch" style="background:#002b36"></span>Solarized Dark</div>
117
+ <div class="theme-option" data-theme-option="solarized-light"><span class="theme-swatch" style="background:#fdf6e3"></span>Solarized Light</div>
118
+ <div class="theme-option" data-theme-option="nord"><span class="theme-swatch" style="background:#2e3440"></span>Nord</div>
119
+ <div class="theme-option" data-theme-option="dracula"><span class="theme-swatch" style="background:#282a36"></span>Dracula</div>
120
+ <div class="theme-option" data-theme-option="github-dark"><span class="theme-swatch" style="background:#0d1117"></span>GitHub Dark</div>
121
+ <div class="theme-option" data-theme-option="one-dark"><span class="theme-swatch" style="background:#282c34"></span>One Dark</div>
122
+ <div class="theme-option" data-theme-option="catppuccin"><span class="theme-swatch" style="background:#1e1e2e"></span>Catppuccin</div>
123
+ <div class="theme-option" data-theme-option="gruvbox"><span class="theme-swatch" style="background:#282828"></span>Gruvbox</div>
124
+ <div class="theme-option" data-theme-option="night-owl"><span class="theme-swatch" style="background:#011627"></span>Night Owl</div>
125
+ </div>
126
+ </div>
55
127
  <div class="card">
56
128
  <h1>📡 Term<span>Beam</span></h1>
57
129
  <p class="subtitle">Enter the access password</p>
@@ -63,12 +135,24 @@ const LOGIN_HTML = `<!DOCTYPE html>
63
135
  </div>
64
136
  <p class="tagline">Beam your terminal to any device</p>
65
137
  <script>
66
- const t=document.getElementById('themeBtn'), h=document.documentElement;
67
- function applyTheme(light){h.setAttribute('data-theme',light?'light':'');t.textContent=light?'☀️':'🌙';
68
- document.querySelector('meta[name=theme-color]').content=light?'#ffffff':'#1e1e1e';}
69
- applyTheme(localStorage.getItem('theme')==='light');
70
- t.addEventListener('click',()=>{const light=h.getAttribute('data-theme')!=='light';
71
- localStorage.setItem('theme',light?'light':'dark');applyTheme(light);});
138
+ const THEMES=[{id:'dark',bg:'#1e1e1e'},{id:'light',bg:'#f3f3f3'},{id:'monokai',bg:'#272822'},
139
+ {id:'solarized-dark',bg:'#002b36'},{id:'solarized-light',bg:'#fdf6e3'},{id:'nord',bg:'#2e3440'},
140
+ {id:'dracula',bg:'#282a36'},{id:'github-dark',bg:'#0d1117'},{id:'one-dark',bg:'#282c34'},
141
+ {id:'catppuccin',bg:'#1e1e2e'},{id:'gruvbox',bg:'#282828'},{id:'night-owl',bg:'#011627'}];
142
+ const h=document.documentElement, picker=document.getElementById('themePicker');
143
+ function applyTheme(theme){
144
+ h.setAttribute('data-theme',theme);
145
+ const t=THEMES.find(x=>x.id===theme)||THEMES[0];
146
+ document.querySelector('meta[name=theme-color]').content=t.bg;
147
+ localStorage.setItem('termbeam-theme',theme);
148
+ document.querySelectorAll('.theme-option').forEach(el=>el.classList.toggle('active',el.dataset.themeOption===theme));
149
+ }
150
+ applyTheme(localStorage.getItem('termbeam-theme')||'dark');
151
+ document.getElementById('themeBtn').addEventListener('click',e=>{e.stopPropagation();picker.classList.toggle('open');});
152
+ document.addEventListener('click',()=>picker.classList.remove('open'));
153
+ document.querySelectorAll('.theme-option').forEach(el=>{
154
+ el.addEventListener('click',e=>{e.stopPropagation();applyTheme(el.dataset.themeOption);picker.classList.remove('open');});
155
+ });
72
156
  document.getElementById('form').addEventListener('submit', async (e) => {
73
157
  e.preventDefault();
74
158
  const pw = document.getElementById('pw').value;
package/src/routes.js CHANGED
@@ -114,7 +114,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
114
114
  });
115
115
 
116
116
  app.post('/api/sessions', auth.middleware, (req, res) => {
117
- const { name, shell, args: shellArgs, cwd, initialCommand, color } = req.body || {};
117
+ const { name, shell, args: shellArgs, cwd, initialCommand, color, cols, rows } = req.body || {};
118
118
 
119
119
  // Validate shell field
120
120
  if (shell) {
@@ -146,6 +146,8 @@ function setupRoutes(app, { auth, sessions, config, state }) {
146
146
  cwd: cwd || config.cwd,
147
147
  initialCommand: initialCommand || null,
148
148
  color: color || null,
149
+ cols: typeof cols === 'number' && cols > 0 && cols <= 500 ? Math.floor(cols) : undefined,
150
+ rows: typeof rows === 'number' && rows > 0 && rows <= 200 ? Math.floor(rows) : undefined,
149
151
  });
150
152
  res.json({ id, url: `/terminal?id=${id}` });
151
153
  });
@@ -153,7 +155,9 @@ function setupRoutes(app, { auth, sessions, config, state }) {
153
155
  // Available shells
154
156
  app.get('/api/shells', auth.middleware, (_req, res) => {
155
157
  const shells = detectShells();
156
- res.json({ shells, default: config.defaultShell, cwd: config.cwd });
158
+ const ds = config.defaultShell;
159
+ const match = shells.find((s) => s.cmd === ds || s.path === ds || s.name === ds);
160
+ res.json({ shells, default: match ? match.cmd : ds, cwd: config.cwd });
157
161
  });
158
162
 
159
163
  app.get('/api/sessions/:id/detect-port', auth.middleware, (req, res) => {
package/src/sessions.js CHANGED
@@ -18,15 +18,24 @@ class SessionManager {
18
18
  this.sessions = new Map();
19
19
  }
20
20
 
21
- create({ name, shell, args = [], cwd, initialCommand = null, color = null }) {
21
+ create({
22
+ name,
23
+ shell,
24
+ args = [],
25
+ cwd,
26
+ initialCommand = null,
27
+ color = null,
28
+ cols = 120,
29
+ rows = 30,
30
+ }) {
22
31
  const id = crypto.randomBytes(16).toString('hex');
23
32
  if (!color) {
24
33
  color = SESSION_COLORS[this.sessions.size % SESSION_COLORS.length];
25
34
  }
26
35
  const ptyProcess = pty.spawn(shell, args, {
27
36
  name: 'xterm-256color',
28
- cols: 120,
29
- rows: 30,
37
+ cols,
38
+ rows,
30
39
  cwd,
31
40
  env: { ...process.env, TERM: 'xterm-256color' },
32
41
  });
@@ -47,6 +56,9 @@ class SessionManager {
47
56
  clients: new Set(),
48
57
  scrollback: [],
49
58
  scrollbackBuf: '',
59
+ hasHadClient: false,
60
+ _lastCols: cols,
61
+ _lastRows: rows,
50
62
  };
51
63
 
52
64
  ptyProcess.onData((data) => {
package/src/websocket.js CHANGED
@@ -77,9 +77,16 @@ function setupWebSocket(wss, { auth, sessions }) {
77
77
  return;
78
78
  }
79
79
  attached = session;
80
- session.clients.add(ws);
81
- if (session.scrollbackBuf.length > 0) {
82
- ws.send(JSON.stringify({ type: 'output', data: session.scrollbackBuf }));
80
+ // First client: defer adding to session.clients until after the
81
+ // first resize so we can decide whether the PTY needs resizing.
82
+ if (!session.hasHadClient) {
83
+ session.hasHadClient = true;
84
+ ws._pendingResize = true;
85
+ } else {
86
+ session.clients.add(ws);
87
+ if (session.scrollbackBuf.length > 0) {
88
+ ws.send(JSON.stringify({ type: 'output', data: session.scrollbackBuf }));
89
+ }
83
90
  }
84
91
  ws.send(JSON.stringify({ type: 'attached', sessionId: msg.sessionId }));
85
92
  log.info(`Client attached to session ${msg.sessionId}`);
@@ -95,7 +102,27 @@ function setupWebSocket(wss, { auth, sessions }) {
95
102
  const rows = Math.floor(msg.rows);
96
103
  if (cols > 0 && cols <= 500 && rows > 0 && rows <= 200) {
97
104
  ws._dims = { cols, rows };
98
- recalcPtySize(attached);
105
+ if (ws._pendingResize) {
106
+ ws._pendingResize = false;
107
+ // Only discard scrollback and send SIGWINCH if the PTY was
108
+ // spawned at a different size (e.g. default 120×30).
109
+ // If the PTY already matches (new session sent dims in POST),
110
+ // just add the client and replay scrollback — no SIGWINCH,
111
+ // no duplicate prompt from slow themes like oh-my-posh.
112
+ const sizeChanged = cols !== attached._lastCols || rows !== attached._lastRows;
113
+ if (sizeChanged) {
114
+ attached.scrollbackBuf = '';
115
+ attached.clients.add(ws);
116
+ recalcPtySize(attached);
117
+ } else {
118
+ attached.clients.add(ws);
119
+ if (attached.scrollbackBuf.length > 0) {
120
+ ws.send(JSON.stringify({ type: 'output', data: attached.scrollbackBuf }));
121
+ }
122
+ }
123
+ } else {
124
+ recalcPtySize(attached);
125
+ }
99
126
  }
100
127
  }
101
128
  } catch (err) {