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/README.md +10 -1
- package/package.json +2 -2
- package/public/index.html +326 -33
- package/public/terminal.html +1493 -113
- package/src/auth.js +96 -12
- package/src/routes.js +6 -2
- package/src/sessions.js +15 -3
- package/src/websocket.js +31 -4
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-
|
|
29
|
-
|
|
30
|
-
border-radius:8px; cursor:pointer; display:flex;
|
|
31
|
-
justify-content:center; font-size:16px;
|
|
32
|
-
-
|
|
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
|
-
<
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
29
|
-
rows
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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) {
|