termbeam 0.0.4 → 0.0.6
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 -0
- package/package.json +2 -2
- package/public/index.html +237 -70
- package/public/terminal.html +286 -100
- package/src/cli.js +52 -1
- package/src/routes.js +9 -1
- package/src/sessions.js +6 -1
- package/src/shells.js +77 -0
- package/src/tunnel.js +61 -8
package/public/terminal.html
CHANGED
|
@@ -8,13 +8,53 @@
|
|
|
8
8
|
/>
|
|
9
9
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
10
10
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
11
|
-
<meta name="theme-color" content="#
|
|
11
|
+
<meta name="theme-color" content="#1e1e1e" />
|
|
12
12
|
<title>TermBeam — Terminal</title>
|
|
13
13
|
<link
|
|
14
14
|
rel="stylesheet"
|
|
15
15
|
href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
|
|
16
16
|
/>
|
|
17
17
|
<style>
|
|
18
|
+
:root {
|
|
19
|
+
--bg: #1e1e1e;
|
|
20
|
+
--surface: #252526;
|
|
21
|
+
--border: #3c3c3c;
|
|
22
|
+
--border-subtle: #474747;
|
|
23
|
+
--text: #d4d4d4;
|
|
24
|
+
--text-secondary: #858585;
|
|
25
|
+
--text-dim: #6e6e6e;
|
|
26
|
+
--text-muted: #555555;
|
|
27
|
+
--accent: #0078d4;
|
|
28
|
+
--accent-hover: #1a8ae8;
|
|
29
|
+
--accent-active: #005a9e;
|
|
30
|
+
--danger: #f14c4c;
|
|
31
|
+
--danger-hover: #d73a3a;
|
|
32
|
+
--success: #89d185;
|
|
33
|
+
--key-bg: #2d2d2d;
|
|
34
|
+
--key-border: #404040;
|
|
35
|
+
--key-shadow: rgba(0,0,0,0.4);
|
|
36
|
+
--overlay-bg: rgba(0,0,0,0.85);
|
|
37
|
+
}
|
|
38
|
+
[data-theme="light"] {
|
|
39
|
+
--bg: #ffffff;
|
|
40
|
+
--surface: #f3f3f3;
|
|
41
|
+
--border: #e0e0e0;
|
|
42
|
+
--border-subtle: #d0d0d0;
|
|
43
|
+
--text: #1e1e1e;
|
|
44
|
+
--text-secondary: #616161;
|
|
45
|
+
--text-dim: #767676;
|
|
46
|
+
--text-muted: #a0a0a0;
|
|
47
|
+
--accent: #0078d4;
|
|
48
|
+
--accent-hover: #106ebe;
|
|
49
|
+
--accent-active: #005a9e;
|
|
50
|
+
--danger: #e51400;
|
|
51
|
+
--danger-hover: #c20000;
|
|
52
|
+
--success: #16825d;
|
|
53
|
+
--key-bg: #e8e8e8;
|
|
54
|
+
--key-border: #d0d0d0;
|
|
55
|
+
--key-shadow: rgba(0,0,0,0.08);
|
|
56
|
+
--overlay-bg: rgba(0,0,0,0.5);
|
|
57
|
+
}
|
|
18
58
|
@font-face {
|
|
19
59
|
font-family: 'NerdFont';
|
|
20
60
|
src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
|
|
@@ -41,53 +81,117 @@
|
|
|
41
81
|
body {
|
|
42
82
|
height: 100%;
|
|
43
83
|
width: 100%;
|
|
44
|
-
background:
|
|
45
|
-
color:
|
|
84
|
+
background: var(--bg);
|
|
85
|
+
color: var(--text);
|
|
46
86
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
47
87
|
overflow: hidden;
|
|
48
88
|
touch-action: manipulation;
|
|
49
|
-
/* Use dvh to account for mobile browser chrome + keyboard */
|
|
50
89
|
height: 100dvh;
|
|
90
|
+
transition: background 0.3s, color 0.3s;
|
|
51
91
|
}
|
|
52
92
|
|
|
53
93
|
#status-bar {
|
|
54
|
-
height:
|
|
94
|
+
height: 40px;
|
|
55
95
|
display: flex;
|
|
56
96
|
align-items: center;
|
|
57
97
|
justify-content: space-between;
|
|
58
|
-
padding: 0
|
|
59
|
-
background:
|
|
60
|
-
border-bottom: 1px solid
|
|
98
|
+
padding: 0 8px;
|
|
99
|
+
background: var(--surface);
|
|
100
|
+
border-bottom: 1px solid var(--border);
|
|
61
101
|
font-size: 13px;
|
|
102
|
+
transition: background 0.3s, border-color 0.3s;
|
|
62
103
|
}
|
|
63
104
|
#status-bar .left {
|
|
64
105
|
display: flex;
|
|
65
106
|
align-items: center;
|
|
66
|
-
gap:
|
|
107
|
+
gap: 6px;
|
|
108
|
+
}
|
|
109
|
+
#status-bar .right {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 4px;
|
|
67
113
|
}
|
|
68
|
-
|
|
114
|
+
.bar-btn {
|
|
69
115
|
background: none;
|
|
70
116
|
border: none;
|
|
71
|
-
color:
|
|
72
|
-
|
|
117
|
+
color: var(--text-dim);
|
|
118
|
+
width: 30px;
|
|
119
|
+
height: 30px;
|
|
120
|
+
border-radius: 8px;
|
|
73
121
|
cursor: pointer;
|
|
74
|
-
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
transition: background 0.15s, color 0.15s;
|
|
126
|
+
-webkit-tap-highlight-color: transparent;
|
|
127
|
+
}
|
|
128
|
+
.bar-btn:hover {
|
|
129
|
+
background: var(--border);
|
|
130
|
+
color: var(--text);
|
|
131
|
+
}
|
|
132
|
+
.bar-btn:active {
|
|
133
|
+
background: var(--accent);
|
|
134
|
+
color: #ffffff;
|
|
135
|
+
}
|
|
136
|
+
.bar-btn svg {
|
|
137
|
+
display: block;
|
|
138
|
+
}
|
|
139
|
+
.bar-group {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
background: var(--bg);
|
|
143
|
+
border-radius: 8px;
|
|
144
|
+
padding: 2px;
|
|
145
|
+
gap: 1px;
|
|
146
|
+
transition: background 0.3s;
|
|
75
147
|
}
|
|
76
|
-
|
|
77
|
-
|
|
148
|
+
.bar-group .bar-btn {
|
|
149
|
+
width: 26px;
|
|
150
|
+
height: 26px;
|
|
151
|
+
border-radius: 6px;
|
|
152
|
+
font-size: 14px;
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
}
|
|
155
|
+
#stop-btn {
|
|
156
|
+
background: none;
|
|
157
|
+
border: none;
|
|
158
|
+
color: var(--danger);
|
|
159
|
+
height: 30px;
|
|
160
|
+
border-radius: 8px;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
gap: 4px;
|
|
166
|
+
padding: 0 10px;
|
|
167
|
+
font-size: 11px;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
margin-left: 2px;
|
|
170
|
+
transition: background 0.15s, color 0.15s, transform 0.1s;
|
|
171
|
+
-webkit-tap-highlight-color: transparent;
|
|
172
|
+
}
|
|
173
|
+
#stop-btn:hover {
|
|
174
|
+
background: var(--danger);
|
|
175
|
+
color: #ffffff;
|
|
176
|
+
}
|
|
177
|
+
#stop-btn:active {
|
|
178
|
+
background: var(--danger-hover);
|
|
179
|
+
color: #ffffff;
|
|
180
|
+
transform: scale(0.9);
|
|
78
181
|
}
|
|
79
182
|
#status-dot {
|
|
80
183
|
width: 8px;
|
|
81
184
|
height: 8px;
|
|
82
185
|
border-radius: 50%;
|
|
83
|
-
background:
|
|
186
|
+
background: var(--danger);
|
|
84
187
|
display: inline-block;
|
|
188
|
+
transition: background 0.3s;
|
|
85
189
|
}
|
|
86
190
|
#status-dot.connected {
|
|
87
|
-
background:
|
|
191
|
+
background: var(--success);
|
|
88
192
|
}
|
|
89
193
|
#status-text {
|
|
90
|
-
color:
|
|
194
|
+
color: var(--text-secondary);
|
|
91
195
|
}
|
|
92
196
|
#session-name {
|
|
93
197
|
font-weight: 600;
|
|
@@ -95,10 +199,10 @@
|
|
|
95
199
|
|
|
96
200
|
#terminal-container {
|
|
97
201
|
position: absolute;
|
|
98
|
-
top:
|
|
202
|
+
top: 40px;
|
|
99
203
|
left: 0;
|
|
100
204
|
right: 0;
|
|
101
|
-
bottom:
|
|
205
|
+
bottom: 52px;
|
|
102
206
|
padding: 2px;
|
|
103
207
|
overflow: hidden;
|
|
104
208
|
}
|
|
@@ -108,46 +212,67 @@
|
|
|
108
212
|
bottom: 0;
|
|
109
213
|
left: 0;
|
|
110
214
|
right: 0;
|
|
111
|
-
height:
|
|
215
|
+
height: 52px;
|
|
112
216
|
display: flex;
|
|
113
217
|
align-items: center;
|
|
114
|
-
background:
|
|
115
|
-
border-top: 1px solid
|
|
116
|
-
padding: 0
|
|
218
|
+
background: var(--surface);
|
|
219
|
+
border-top: 1px solid var(--border);
|
|
220
|
+
padding: 0 4px;
|
|
117
221
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
118
|
-
gap:
|
|
119
|
-
overflow-x: auto;
|
|
222
|
+
gap: 4px;
|
|
120
223
|
z-index: 50;
|
|
224
|
+
transition: background 0.3s, border-color 0.3s;
|
|
121
225
|
}
|
|
122
226
|
.key-btn {
|
|
123
|
-
min-width:
|
|
124
|
-
height:
|
|
125
|
-
background:
|
|
126
|
-
color:
|
|
127
|
-
border: 1px solid
|
|
128
|
-
border-radius:
|
|
129
|
-
font-size:
|
|
227
|
+
min-width: 0;
|
|
228
|
+
height: 40px;
|
|
229
|
+
background: var(--key-bg);
|
|
230
|
+
color: var(--text);
|
|
231
|
+
border: 1px solid var(--key-border);
|
|
232
|
+
border-radius: 8px;
|
|
233
|
+
font-size: 12px;
|
|
130
234
|
font-weight: 600;
|
|
131
235
|
cursor: pointer;
|
|
132
236
|
display: flex;
|
|
237
|
+
flex-direction: column;
|
|
133
238
|
align-items: center;
|
|
134
239
|
justify-content: center;
|
|
135
240
|
-webkit-tap-highlight-color: transparent;
|
|
136
241
|
user-select: none;
|
|
137
242
|
white-space: nowrap;
|
|
138
|
-
padding:
|
|
139
|
-
flex
|
|
243
|
+
padding: 2px 8px;
|
|
244
|
+
flex: 1 1 0;
|
|
245
|
+
gap: 0;
|
|
246
|
+
line-height: 1;
|
|
247
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
|
|
248
|
+
box-shadow: 0 1px 3px var(--key-shadow), inset 0 1px 0 rgba(255,255,255,0.05);
|
|
249
|
+
}
|
|
250
|
+
.key-btn .hint {
|
|
251
|
+
font-size: 8px;
|
|
252
|
+
font-weight: 400;
|
|
253
|
+
opacity: 0.5;
|
|
254
|
+
margin-top: 1px;
|
|
255
|
+
letter-spacing: 0.02em;
|
|
256
|
+
}
|
|
257
|
+
.key-btn:hover {
|
|
258
|
+
background: var(--border);
|
|
259
|
+
border-color: var(--accent);
|
|
260
|
+
box-shadow: 0 2px 6px var(--key-shadow);
|
|
140
261
|
}
|
|
141
262
|
.key-btn:active {
|
|
142
|
-
background:
|
|
263
|
+
background: var(--accent);
|
|
264
|
+
color: #ffffff;
|
|
265
|
+
border-color: var(--accent);
|
|
266
|
+
transform: scale(0.93);
|
|
267
|
+
box-shadow: none;
|
|
143
268
|
}
|
|
144
269
|
.key-btn.wide {
|
|
145
|
-
|
|
270
|
+
flex: 1.4 1 0;
|
|
146
271
|
}
|
|
147
272
|
.key-sep {
|
|
148
273
|
width: 1px;
|
|
149
274
|
height: 20px;
|
|
150
|
-
background:
|
|
275
|
+
background: var(--border);
|
|
151
276
|
flex-shrink: 0;
|
|
152
277
|
}
|
|
153
278
|
|
|
@@ -165,7 +290,7 @@
|
|
|
165
290
|
left: 0;
|
|
166
291
|
right: 0;
|
|
167
292
|
bottom: 0;
|
|
168
|
-
background:
|
|
293
|
+
background: var(--overlay-bg);
|
|
169
294
|
z-index: 100;
|
|
170
295
|
flex-direction: column;
|
|
171
296
|
align-items: center;
|
|
@@ -177,6 +302,7 @@
|
|
|
177
302
|
}
|
|
178
303
|
#reconnect-overlay .msg {
|
|
179
304
|
font-size: 17px;
|
|
305
|
+
color: #ffffff;
|
|
180
306
|
}
|
|
181
307
|
.overlay-actions {
|
|
182
308
|
display: flex;
|
|
@@ -189,47 +315,60 @@
|
|
|
189
315
|
font-size: 15px;
|
|
190
316
|
font-weight: 600;
|
|
191
317
|
cursor: pointer;
|
|
318
|
+
transition: background 0.15s, transform 0.1s;
|
|
319
|
+
}
|
|
320
|
+
.overlay-actions button:active {
|
|
321
|
+
transform: scale(0.95);
|
|
192
322
|
}
|
|
193
323
|
#reconnect-btn {
|
|
194
|
-
background:
|
|
195
|
-
color:
|
|
324
|
+
background: var(--accent);
|
|
325
|
+
color: #ffffff;
|
|
326
|
+
}
|
|
327
|
+
#reconnect-btn:hover {
|
|
328
|
+
background: var(--accent-hover);
|
|
196
329
|
}
|
|
197
330
|
#back-to-sessions {
|
|
198
|
-
background:
|
|
199
|
-
color: #
|
|
331
|
+
background: rgba(255,255,255,0.15);
|
|
332
|
+
color: #ffffff;
|
|
333
|
+
}
|
|
334
|
+
#back-to-sessions:hover {
|
|
335
|
+
background: rgba(255,255,255,0.25);
|
|
200
336
|
}
|
|
337
|
+
|
|
201
338
|
</style>
|
|
202
339
|
</head>
|
|
203
340
|
<body>
|
|
204
341
|
<div id="status-bar">
|
|
205
342
|
<div class="left">
|
|
206
|
-
<button id="back-btn" onclick="location.href = '/'"
|
|
343
|
+
<button class="bar-btn" id="back-btn" onclick="location.href = '/'" title="Back to sessions"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>
|
|
207
344
|
<span id="status-dot"></span>
|
|
208
345
|
<span id="session-name">…</span>
|
|
209
346
|
</div>
|
|
210
|
-
<
|
|
211
|
-
|
|
347
|
+
<div class="right">
|
|
348
|
+
<span id="status-text">Connecting…</span>
|
|
349
|
+
<span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
|
|
350
|
+
<div class="bar-group">
|
|
351
|
+
<button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
|
|
352
|
+
<button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
|
|
353
|
+
</div>
|
|
354
|
+
<button class="bar-btn" id="theme-toggle" title="Toggle theme"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button>
|
|
355
|
+
<button id="stop-btn" title="Stop session"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>Stop</button>
|
|
356
|
+
</div>
|
|
212
357
|
</div>
|
|
213
358
|
|
|
214
359
|
<div id="terminal-container"></div>
|
|
215
360
|
|
|
216
361
|
<div id="key-bar">
|
|
217
|
-
<button class="key-btn" data-key="[A"
|
|
218
|
-
<button class="key-btn" data-key="[B"
|
|
219
|
-
<button class="key-btn" data-key="[D">←</button>
|
|
220
|
-
<button class="key-btn" data-key="[C">→</button>
|
|
362
|
+
<button class="key-btn" data-key="[A" title="Previous command">↑<span class="hint">prev</span></button>
|
|
363
|
+
<button class="key-btn" data-key="[B" title="Next command">↓<span class="hint">next</span></button>
|
|
364
|
+
<button class="key-btn" data-key="[D" title="Left">←</button>
|
|
365
|
+
<button class="key-btn" data-key="[C" title="Right">→</button>
|
|
366
|
+
<button class="key-btn" data-key="[H" title="Home">Home</button>
|
|
367
|
+
<button class="key-btn" data-key="[F" title="End">End</button>
|
|
221
368
|
<div class="key-sep"></div>
|
|
222
|
-
<button class="key-btn wide" data-key="	">Tab</button>
|
|
223
|
-
<button class="key-btn wide" data-key="
">Enter</button>
|
|
224
|
-
<button class="key-btn" data-key="">Esc</button>
|
|
369
|
+
<button class="key-btn wide" data-key="	" title="Autocomplete">Tab</button>
|
|
225
370
|
<div class="key-sep"></div>
|
|
226
|
-
<button class="key-btn" data-key="">^C</button>
|
|
227
|
-
<button class="key-btn" data-key="">^D</button>
|
|
228
|
-
<button class="key-btn" data-key="">^Z</button>
|
|
229
|
-
<button class="key-btn" data-key="">^L</button>
|
|
230
|
-
<div class="key-sep"></div>
|
|
231
|
-
<button class="key-btn" id="zoom-out">A-</button>
|
|
232
|
-
<button class="key-btn" id="zoom-in">A+</button>
|
|
371
|
+
<button class="key-btn" data-key="" title="Interrupt process">^C<span class="hint">stop</span></button>
|
|
233
372
|
</div>
|
|
234
373
|
|
|
235
374
|
<div id="reconnect-overlay">
|
|
@@ -253,6 +392,50 @@
|
|
|
253
392
|
const statusText = document.getElementById('status-text');
|
|
254
393
|
const sessionName = document.getElementById('session-name');
|
|
255
394
|
const reconnectOverlay = document.getElementById('reconnect-overlay');
|
|
395
|
+
let sessionExited = false;
|
|
396
|
+
let reconnectTimer = null;
|
|
397
|
+
let reconnectDelay = 3000;
|
|
398
|
+
const MAX_RECONNECT_DELAY = 30000;
|
|
399
|
+
|
|
400
|
+
// Terminal themes
|
|
401
|
+
const darkTermTheme = {
|
|
402
|
+
background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#aeafad', cursorAccent: '#1e1e1e',
|
|
403
|
+
selectionBackground: 'rgba(38, 79, 120, 0.5)',
|
|
404
|
+
black: '#000000', red: '#cd3131', green: '#0dbc79', yellow: '#e5e510',
|
|
405
|
+
blue: '#2472c8', magenta: '#bc3fbc', cyan: '#11a8cd', white: '#e5e5e5',
|
|
406
|
+
brightBlack: '#666666', brightRed: '#f14c4c', brightGreen: '#23d18b',
|
|
407
|
+
brightYellow: '#f5f543', brightBlue: '#3b8eea', brightMagenta: '#d670d6',
|
|
408
|
+
brightCyan: '#29b8db', brightWhite: '#e5e5e5',
|
|
409
|
+
};
|
|
410
|
+
const lightTermTheme = {
|
|
411
|
+
background: '#ffffff', foreground: '#1e1e1e', cursor: '#000000', cursorAccent: '#ffffff',
|
|
412
|
+
selectionBackground: 'rgba(0, 120, 215, 0.3)',
|
|
413
|
+
black: '#000000', red: '#cd3131', green: '#00bc7c', yellow: '#949800',
|
|
414
|
+
blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555',
|
|
415
|
+
brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14',
|
|
416
|
+
brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc',
|
|
417
|
+
brightCyan: '#0598bc', brightWhite: '#a5a5a5',
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
function getTheme() { return localStorage.getItem('termbeam-theme') || 'dark'; }
|
|
421
|
+
function applyTheme(theme) {
|
|
422
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
423
|
+
document.querySelector('meta[name="theme-color"]').content =
|
|
424
|
+
theme === 'light' ? '#f3f3f3' : '#1e1e1e';
|
|
425
|
+
const btn = document.getElementById('theme-toggle');
|
|
426
|
+
if (btn) btn.innerHTML = theme === 'light'
|
|
427
|
+
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
|
|
428
|
+
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
|
|
429
|
+
localStorage.setItem('termbeam-theme', theme);
|
|
430
|
+
if (window._term) {
|
|
431
|
+
window._term.options.theme = theme === 'light' ? lightTermTheme : darkTermTheme;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
applyTheme(getTheme());
|
|
435
|
+
|
|
436
|
+
document.getElementById('theme-toggle').addEventListener('click', () => {
|
|
437
|
+
applyTheme(getTheme() === 'light' ? 'dark' : 'light');
|
|
438
|
+
});
|
|
256
439
|
|
|
257
440
|
// Load Nerd Font, then init terminal
|
|
258
441
|
const nerdFont = new FontFace(
|
|
@@ -283,32 +466,11 @@
|
|
|
283
466
|
fontWeightBold: 'bold',
|
|
284
467
|
letterSpacing: 0,
|
|
285
468
|
lineHeight: 1.1,
|
|
286
|
-
theme:
|
|
287
|
-
background: '#1a1a2e',
|
|
288
|
-
foreground: '#e0e0e0',
|
|
289
|
-
cursor: '#533483',
|
|
290
|
-
cursorAccent: '#1a1a2e',
|
|
291
|
-
selectionBackground: 'rgba(83, 52, 131, 0.4)',
|
|
292
|
-
black: '#1a1a2e',
|
|
293
|
-
red: '#e74c3c',
|
|
294
|
-
green: '#2ecc71',
|
|
295
|
-
yellow: '#f1c40f',
|
|
296
|
-
blue: '#3498db',
|
|
297
|
-
magenta: '#9b59b6',
|
|
298
|
-
cyan: '#1abc9c',
|
|
299
|
-
white: '#ecf0f1',
|
|
300
|
-
brightBlack: '#636e72',
|
|
301
|
-
brightRed: '#ff6b6b',
|
|
302
|
-
brightGreen: '#55efc4',
|
|
303
|
-
brightYellow: '#ffeaa7',
|
|
304
|
-
brightBlue: '#74b9ff',
|
|
305
|
-
brightMagenta: '#a29bfe',
|
|
306
|
-
brightCyan: '#81ecec',
|
|
307
|
-
brightWhite: '#ffffff',
|
|
308
|
-
},
|
|
469
|
+
theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
|
|
309
470
|
allowProposedApi: true,
|
|
310
471
|
scrollback: 10000,
|
|
311
472
|
});
|
|
473
|
+
window._term = term;
|
|
312
474
|
|
|
313
475
|
const fitAddon = new window.FitAddon.FitAddon();
|
|
314
476
|
const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
|
|
@@ -327,8 +489,9 @@
|
|
|
327
489
|
|
|
328
490
|
ws.onopen = () => {
|
|
329
491
|
statusDot.className = 'connected';
|
|
330
|
-
statusText.textContent = '
|
|
492
|
+
statusText.textContent = '';
|
|
331
493
|
reconnectOverlay.classList.remove('visible');
|
|
494
|
+
reconnectDelay = 3000;
|
|
332
495
|
// Attach to session
|
|
333
496
|
ws.send(JSON.stringify({ type: 'attach', sessionId }));
|
|
334
497
|
};
|
|
@@ -345,6 +508,8 @@
|
|
|
345
508
|
ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
|
|
346
509
|
}
|
|
347
510
|
} else if (msg.type === 'exit') {
|
|
511
|
+
sessionExited = true;
|
|
512
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
348
513
|
statusText.textContent = `Exited (code ${msg.code})`;
|
|
349
514
|
statusDot.className = '';
|
|
350
515
|
reconnectOverlay.querySelector('.msg').textContent =
|
|
@@ -364,6 +529,9 @@
|
|
|
364
529
|
statusDot.className = '';
|
|
365
530
|
statusText.textContent = 'Disconnected';
|
|
366
531
|
reconnectOverlay.classList.add('visible');
|
|
532
|
+
if (!sessionExited) {
|
|
533
|
+
scheduleReconnect();
|
|
534
|
+
}
|
|
367
535
|
};
|
|
368
536
|
|
|
369
537
|
ws.onerror = () => {
|
|
@@ -391,7 +559,7 @@
|
|
|
391
559
|
window.addEventListener('resize', doResize);
|
|
392
560
|
screen.orientation?.addEventListener('change', () => setTimeout(doResize, 150));
|
|
393
561
|
|
|
394
|
-
// Key bar
|
|
562
|
+
// Key bar
|
|
395
563
|
// Use mousedown + preventDefault to stop buttons from stealing focus/opening keyboard
|
|
396
564
|
document.getElementById('key-bar').addEventListener('mousedown', (e) => {
|
|
397
565
|
// Only prevent default on buttons, not the scrollable bar itself
|
|
@@ -405,38 +573,56 @@
|
|
|
405
573
|
.addEventListener('touchstart', () => {}, { passive: true });
|
|
406
574
|
document.getElementById('key-bar').addEventListener('click', (e) => {
|
|
407
575
|
const btn = e.target.closest('.key-btn');
|
|
408
|
-
if (!btn
|
|
576
|
+
if (!btn) return;
|
|
409
577
|
if (ws && ws.readyState === 1) {
|
|
410
578
|
ws.send(JSON.stringify({ type: 'input', data: btn.dataset.key }));
|
|
411
579
|
}
|
|
412
580
|
// Don't call term.focus() here — it opens the soft keyboard
|
|
413
581
|
});
|
|
414
582
|
|
|
415
|
-
// Zoom
|
|
416
|
-
const MIN_FONT = 2,
|
|
417
|
-
|
|
418
|
-
let fontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
|
|
419
|
-
|
|
583
|
+
// Zoom (status bar)
|
|
584
|
+
const MIN_FONT = 2, MAX_FONT = 28;
|
|
585
|
+
let fontSize = savedFontSize;
|
|
420
586
|
function applyZoom(size) {
|
|
421
587
|
fontSize = Math.max(MIN_FONT, Math.min(MAX_FONT, size));
|
|
422
588
|
term.options.fontSize = fontSize;
|
|
423
589
|
localStorage.setItem('termbeam-fontsize', fontSize);
|
|
424
590
|
doResize();
|
|
425
591
|
}
|
|
426
|
-
|
|
427
|
-
document.getElementById('zoom-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
592
|
+
document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
|
|
593
|
+
document.getElementById('zoom-out').addEventListener('click', () => applyZoom(fontSize - 2));
|
|
594
|
+
|
|
595
|
+
function scheduleReconnect() {
|
|
596
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
597
|
+
const seconds = Math.round(reconnectDelay / 1000);
|
|
598
|
+
reconnectOverlay.querySelector('.msg').textContent =
|
|
599
|
+
`Disconnected — reconnecting in ${seconds}s…`;
|
|
600
|
+
reconnectTimer = setTimeout(() => {
|
|
601
|
+
reconnectTimer = null;
|
|
602
|
+
reconnectOverlay.querySelector('.msg').textContent = 'Reconnecting…';
|
|
603
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY);
|
|
604
|
+
connect();
|
|
605
|
+
}, reconnectDelay);
|
|
606
|
+
}
|
|
433
607
|
|
|
434
608
|
// Reconnect
|
|
435
609
|
document.getElementById('reconnect-btn').addEventListener('click', () => {
|
|
610
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
611
|
+
sessionExited = false;
|
|
612
|
+
reconnectDelay = 3000;
|
|
436
613
|
term.clear();
|
|
437
614
|
connect();
|
|
438
615
|
});
|
|
439
616
|
|
|
617
|
+
// Stop session
|
|
618
|
+
document.getElementById('stop-btn').addEventListener('click', async () => {
|
|
619
|
+
if (!confirm('Stop this session? The process will be killed.')) return;
|
|
620
|
+
try {
|
|
621
|
+
await fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' });
|
|
622
|
+
} catch {}
|
|
623
|
+
location.href = '/';
|
|
624
|
+
});
|
|
625
|
+
|
|
440
626
|
// Tap terminal area to toggle keyboard (intentional user action)
|
|
441
627
|
container.addEventListener('click', () => term.focus());
|
|
442
628
|
|
|
@@ -453,11 +639,11 @@
|
|
|
453
639
|
if (keyboardHeight > 50) {
|
|
454
640
|
// Keyboard is open — move key bar above it
|
|
455
641
|
keyBar.style.bottom = keyboardHeight + 'px';
|
|
456
|
-
container.style.bottom =
|
|
642
|
+
container.style.bottom = 52 + keyboardHeight + 'px';
|
|
457
643
|
} else {
|
|
458
644
|
// Keyboard closed
|
|
459
645
|
keyBar.style.bottom = '0px';
|
|
460
|
-
container.style.bottom = '
|
|
646
|
+
container.style.bottom = '52px';
|
|
461
647
|
}
|
|
462
648
|
// Refit terminal to new available space
|
|
463
649
|
setTimeout(() => doResize(), 50);
|
package/src/cli.js
CHANGED
|
@@ -32,10 +32,61 @@ Environment:
|
|
|
32
32
|
`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function getDefaultShell() {
|
|
36
|
+
const { execFileSync } = require('child_process');
|
|
37
|
+
const ppid = process.ppid;
|
|
38
|
+
console.log(`[termbeam] Detecting shell (parent PID: ${ppid}, platform: ${os.platform()})`);
|
|
39
|
+
|
|
40
|
+
if (os.platform() === 'win32') {
|
|
41
|
+
// Detect parent process on Windows via WMIC
|
|
42
|
+
try {
|
|
43
|
+
const result = execFileSync(
|
|
44
|
+
'wmic',
|
|
45
|
+
['process', 'where', `ProcessId=${ppid}`, 'get', 'Name', '/value'],
|
|
46
|
+
{ stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf8', timeout: 3000 },
|
|
47
|
+
);
|
|
48
|
+
const match = result.match(/Name=(.+)/);
|
|
49
|
+
if (match) {
|
|
50
|
+
const name = match[1].trim().toLowerCase();
|
|
51
|
+
console.log(`[termbeam] Detected parent process: ${name}`);
|
|
52
|
+
if (name === 'pwsh.exe') return 'pwsh.exe';
|
|
53
|
+
if (name === 'powershell.exe') return 'powershell.exe';
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.log(`[termbeam] Could not detect parent process: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
const fallback = process.env.COMSPEC || 'cmd.exe';
|
|
59
|
+
console.log(`[termbeam] Falling back to: ${fallback}`);
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Unix: detect parent shell via ps
|
|
64
|
+
try {
|
|
65
|
+
const result = execFileSync('ps', ['-o', 'comm=', '-p', String(ppid)], {
|
|
66
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
timeout: 3000,
|
|
69
|
+
});
|
|
70
|
+
const comm = result.trim();
|
|
71
|
+
if (comm) {
|
|
72
|
+
const shell = comm.startsWith('-') ? comm.slice(1) : comm;
|
|
73
|
+
console.log(`[termbeam] Detected parent shell: ${shell}`);
|
|
74
|
+
return shell;
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.log(`[termbeam] Could not detect parent shell: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback to SHELL env or /bin/sh
|
|
81
|
+
const fallback = process.env.SHELL || '/bin/sh';
|
|
82
|
+
console.log(`[termbeam] Falling back to: ${fallback}`);
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
85
|
+
|
|
35
86
|
function parseArgs() {
|
|
36
87
|
let port = parseInt(process.env.PORT || '3456', 10);
|
|
37
88
|
let host = '0.0.0.0';
|
|
38
|
-
const defaultShell =
|
|
89
|
+
const defaultShell = getDefaultShell();
|
|
39
90
|
const cwd = process.env.TERMBEAM_CWD || process.env.PTY_CWD || process.cwd();
|
|
40
91
|
let password = process.env.TERMBEAM_PASSWORD || process.env.PTY_PASSWORD || null;
|
|
41
92
|
let useTunnel = false;
|