termbeam 0.0.5 → 0.0.7
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 +4 -0
- package/package.json +1 -1
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon.svg +6 -0
- package/public/index.html +200 -68
- package/public/manifest.json +14 -0
- package/public/sw.js +54 -0
- package/public/terminal.html +393 -104
- package/src/cli.js +7 -2
- package/src/routes.js +4 -0
- package/src/server.js +11 -4
- package/src/tunnel.js +105 -13
package/public/terminal.html
CHANGED
|
@@ -8,13 +8,55 @@
|
|
|
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
|
+
<link rel="manifest" href="/manifest.json" />
|
|
13
|
+
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
12
14
|
<title>TermBeam — Terminal</title>
|
|
13
15
|
<link
|
|
14
16
|
rel="stylesheet"
|
|
15
17
|
href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
|
|
16
18
|
/>
|
|
17
19
|
<style>
|
|
20
|
+
:root {
|
|
21
|
+
--bg: #1e1e1e;
|
|
22
|
+
--surface: #252526;
|
|
23
|
+
--border: #3c3c3c;
|
|
24
|
+
--border-subtle: #474747;
|
|
25
|
+
--text: #d4d4d4;
|
|
26
|
+
--text-secondary: #858585;
|
|
27
|
+
--text-dim: #6e6e6e;
|
|
28
|
+
--text-muted: #555555;
|
|
29
|
+
--accent: #0078d4;
|
|
30
|
+
--accent-hover: #1a8ae8;
|
|
31
|
+
--accent-active: #005a9e;
|
|
32
|
+
--danger: #f14c4c;
|
|
33
|
+
--danger-hover: #d73a3a;
|
|
34
|
+
--success: #89d185;
|
|
35
|
+
--key-bg: #2d2d2d;
|
|
36
|
+
--key-border: #404040;
|
|
37
|
+
--key-shadow: rgba(0,0,0,0.4);
|
|
38
|
+
--overlay-bg: rgba(0,0,0,0.85);
|
|
39
|
+
}
|
|
40
|
+
[data-theme="light"] {
|
|
41
|
+
--bg: #ffffff;
|
|
42
|
+
--surface: #f3f3f3;
|
|
43
|
+
--border: #e0e0e0;
|
|
44
|
+
--border-subtle: #d0d0d0;
|
|
45
|
+
--text: #1e1e1e;
|
|
46
|
+
--text-secondary: #616161;
|
|
47
|
+
--text-dim: #767676;
|
|
48
|
+
--text-muted: #a0a0a0;
|
|
49
|
+
--accent: #0078d4;
|
|
50
|
+
--accent-hover: #106ebe;
|
|
51
|
+
--accent-active: #005a9e;
|
|
52
|
+
--danger: #e51400;
|
|
53
|
+
--danger-hover: #c20000;
|
|
54
|
+
--success: #16825d;
|
|
55
|
+
--key-bg: #e8e8e8;
|
|
56
|
+
--key-border: #d0d0d0;
|
|
57
|
+
--key-shadow: rgba(0,0,0,0.08);
|
|
58
|
+
--overlay-bg: rgba(0,0,0,0.5);
|
|
59
|
+
}
|
|
18
60
|
@font-face {
|
|
19
61
|
font-family: 'NerdFont';
|
|
20
62
|
src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
|
|
@@ -41,74 +83,117 @@
|
|
|
41
83
|
body {
|
|
42
84
|
height: 100%;
|
|
43
85
|
width: 100%;
|
|
44
|
-
background:
|
|
45
|
-
color:
|
|
86
|
+
background: var(--bg);
|
|
87
|
+
color: var(--text);
|
|
46
88
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
47
89
|
overflow: hidden;
|
|
48
90
|
touch-action: manipulation;
|
|
49
|
-
/* Use dvh to account for mobile browser chrome + keyboard */
|
|
50
91
|
height: 100dvh;
|
|
92
|
+
transition: background 0.3s, color 0.3s;
|
|
51
93
|
}
|
|
52
94
|
|
|
53
95
|
#status-bar {
|
|
54
|
-
height:
|
|
96
|
+
height: 40px;
|
|
55
97
|
display: flex;
|
|
56
98
|
align-items: center;
|
|
57
99
|
justify-content: space-between;
|
|
58
|
-
padding: 0
|
|
59
|
-
background:
|
|
60
|
-
border-bottom: 1px solid
|
|
100
|
+
padding: 0 8px;
|
|
101
|
+
background: var(--surface);
|
|
102
|
+
border-bottom: 1px solid var(--border);
|
|
61
103
|
font-size: 13px;
|
|
104
|
+
transition: background 0.3s, border-color 0.3s;
|
|
62
105
|
}
|
|
63
106
|
#status-bar .left {
|
|
64
107
|
display: flex;
|
|
65
108
|
align-items: center;
|
|
66
|
-
gap:
|
|
109
|
+
gap: 6px;
|
|
67
110
|
}
|
|
68
111
|
#status-bar .right {
|
|
69
112
|
display: flex;
|
|
70
113
|
align-items: center;
|
|
71
|
-
gap:
|
|
114
|
+
gap: 4px;
|
|
72
115
|
}
|
|
73
|
-
|
|
116
|
+
.bar-btn {
|
|
74
117
|
background: none;
|
|
75
118
|
border: none;
|
|
76
|
-
color:
|
|
77
|
-
|
|
119
|
+
color: var(--text-dim);
|
|
120
|
+
width: 30px;
|
|
121
|
+
height: 30px;
|
|
122
|
+
border-radius: 8px;
|
|
78
123
|
cursor: pointer;
|
|
79
|
-
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
transition: background 0.15s, color 0.15s;
|
|
128
|
+
-webkit-tap-highlight-color: transparent;
|
|
129
|
+
}
|
|
130
|
+
.bar-btn:hover {
|
|
131
|
+
background: var(--border);
|
|
132
|
+
color: var(--text);
|
|
80
133
|
}
|
|
81
|
-
|
|
82
|
-
|
|
134
|
+
.bar-btn:active {
|
|
135
|
+
background: var(--accent);
|
|
136
|
+
color: #ffffff;
|
|
137
|
+
}
|
|
138
|
+
.bar-btn svg {
|
|
139
|
+
display: block;
|
|
140
|
+
}
|
|
141
|
+
.bar-group {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
background: var(--bg);
|
|
145
|
+
border-radius: 8px;
|
|
146
|
+
padding: 2px;
|
|
147
|
+
gap: 1px;
|
|
148
|
+
transition: background 0.3s;
|
|
149
|
+
}
|
|
150
|
+
.bar-group .bar-btn {
|
|
151
|
+
width: 26px;
|
|
152
|
+
height: 26px;
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
font-size: 14px;
|
|
155
|
+
font-weight: 600;
|
|
83
156
|
}
|
|
84
157
|
#stop-btn {
|
|
85
|
-
background:
|
|
158
|
+
background: none;
|
|
86
159
|
border: none;
|
|
87
|
-
color:
|
|
88
|
-
|
|
89
|
-
|
|
160
|
+
color: var(--danger);
|
|
161
|
+
height: 30px;
|
|
162
|
+
border-radius: 8px;
|
|
90
163
|
cursor: pointer;
|
|
91
|
-
padding: 4px 10px;
|
|
92
|
-
border-radius: 6px;
|
|
93
164
|
display: flex;
|
|
94
165
|
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
95
167
|
gap: 4px;
|
|
168
|
+
padding: 0 10px;
|
|
169
|
+
font-size: 11px;
|
|
170
|
+
font-weight: 600;
|
|
171
|
+
margin-left: 2px;
|
|
172
|
+
transition: background 0.15s, color 0.15s, transform 0.1s;
|
|
173
|
+
-webkit-tap-highlight-color: transparent;
|
|
174
|
+
}
|
|
175
|
+
#stop-btn:hover {
|
|
176
|
+
background: var(--danger);
|
|
177
|
+
color: #ffffff;
|
|
96
178
|
}
|
|
97
179
|
#stop-btn:active {
|
|
98
|
-
background:
|
|
180
|
+
background: var(--danger-hover);
|
|
181
|
+
color: #ffffff;
|
|
182
|
+
transform: scale(0.9);
|
|
99
183
|
}
|
|
100
184
|
#status-dot {
|
|
101
185
|
width: 8px;
|
|
102
186
|
height: 8px;
|
|
103
187
|
border-radius: 50%;
|
|
104
|
-
background:
|
|
188
|
+
background: var(--danger);
|
|
105
189
|
display: inline-block;
|
|
190
|
+
transition: background 0.3s;
|
|
106
191
|
}
|
|
107
192
|
#status-dot.connected {
|
|
108
|
-
background:
|
|
193
|
+
background: var(--success);
|
|
109
194
|
}
|
|
110
195
|
#status-text {
|
|
111
|
-
color:
|
|
196
|
+
color: var(--text-secondary);
|
|
112
197
|
}
|
|
113
198
|
#session-name {
|
|
114
199
|
font-weight: 600;
|
|
@@ -116,10 +201,10 @@
|
|
|
116
201
|
|
|
117
202
|
#terminal-container {
|
|
118
203
|
position: absolute;
|
|
119
|
-
top:
|
|
204
|
+
top: 40px;
|
|
120
205
|
left: 0;
|
|
121
206
|
right: 0;
|
|
122
|
-
bottom:
|
|
207
|
+
bottom: 52px;
|
|
123
208
|
padding: 2px;
|
|
124
209
|
overflow: hidden;
|
|
125
210
|
}
|
|
@@ -129,46 +214,67 @@
|
|
|
129
214
|
bottom: 0;
|
|
130
215
|
left: 0;
|
|
131
216
|
right: 0;
|
|
132
|
-
height:
|
|
217
|
+
height: 52px;
|
|
133
218
|
display: flex;
|
|
134
219
|
align-items: center;
|
|
135
|
-
background:
|
|
136
|
-
border-top: 1px solid
|
|
137
|
-
padding: 0
|
|
220
|
+
background: var(--surface);
|
|
221
|
+
border-top: 1px solid var(--border);
|
|
222
|
+
padding: 0 4px;
|
|
138
223
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
139
|
-
gap:
|
|
140
|
-
overflow-x: auto;
|
|
224
|
+
gap: 4px;
|
|
141
225
|
z-index: 50;
|
|
226
|
+
transition: background 0.3s, border-color 0.3s;
|
|
142
227
|
}
|
|
143
228
|
.key-btn {
|
|
144
|
-
min-width:
|
|
145
|
-
height:
|
|
146
|
-
background:
|
|
147
|
-
color:
|
|
148
|
-
border: 1px solid
|
|
149
|
-
border-radius:
|
|
150
|
-
font-size:
|
|
229
|
+
min-width: 0;
|
|
230
|
+
height: 40px;
|
|
231
|
+
background: var(--key-bg);
|
|
232
|
+
color: var(--text);
|
|
233
|
+
border: 1px solid var(--key-border);
|
|
234
|
+
border-radius: 8px;
|
|
235
|
+
font-size: 12px;
|
|
151
236
|
font-weight: 600;
|
|
152
237
|
cursor: pointer;
|
|
153
238
|
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
154
240
|
align-items: center;
|
|
155
241
|
justify-content: center;
|
|
156
242
|
-webkit-tap-highlight-color: transparent;
|
|
157
243
|
user-select: none;
|
|
158
244
|
white-space: nowrap;
|
|
159
|
-
padding:
|
|
160
|
-
flex
|
|
245
|
+
padding: 2px 8px;
|
|
246
|
+
flex: 1 1 0;
|
|
247
|
+
gap: 0;
|
|
248
|
+
line-height: 1;
|
|
249
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
|
|
250
|
+
box-shadow: 0 1px 3px var(--key-shadow), inset 0 1px 0 rgba(255,255,255,0.05);
|
|
251
|
+
}
|
|
252
|
+
.key-btn .hint {
|
|
253
|
+
font-size: 8px;
|
|
254
|
+
font-weight: 400;
|
|
255
|
+
opacity: 0.5;
|
|
256
|
+
margin-top: 1px;
|
|
257
|
+
letter-spacing: 0.02em;
|
|
258
|
+
}
|
|
259
|
+
.key-btn:hover {
|
|
260
|
+
background: var(--border);
|
|
261
|
+
border-color: var(--accent);
|
|
262
|
+
box-shadow: 0 2px 6px var(--key-shadow);
|
|
161
263
|
}
|
|
162
264
|
.key-btn:active {
|
|
163
|
-
background:
|
|
265
|
+
background: var(--accent);
|
|
266
|
+
color: #ffffff;
|
|
267
|
+
border-color: var(--accent);
|
|
268
|
+
transform: scale(0.93);
|
|
269
|
+
box-shadow: none;
|
|
164
270
|
}
|
|
165
271
|
.key-btn.wide {
|
|
166
|
-
|
|
272
|
+
flex: 1.4 1 0;
|
|
167
273
|
}
|
|
168
274
|
.key-sep {
|
|
169
275
|
width: 1px;
|
|
170
276
|
height: 20px;
|
|
171
|
-
background:
|
|
277
|
+
background: var(--border);
|
|
172
278
|
flex-shrink: 0;
|
|
173
279
|
}
|
|
174
280
|
|
|
@@ -179,6 +285,64 @@
|
|
|
179
285
|
overflow-y: hidden !important;
|
|
180
286
|
}
|
|
181
287
|
|
|
288
|
+
#copy-toast {
|
|
289
|
+
position: fixed;
|
|
290
|
+
top: 56px;
|
|
291
|
+
left: 50%;
|
|
292
|
+
transform: translateX(-50%) translateY(-8px);
|
|
293
|
+
background: var(--surface);
|
|
294
|
+
color: var(--text);
|
|
295
|
+
border: 1px solid var(--border);
|
|
296
|
+
padding: 6px 16px;
|
|
297
|
+
border-radius: 8px;
|
|
298
|
+
font-size: 13px;
|
|
299
|
+
font-weight: 600;
|
|
300
|
+
opacity: 0;
|
|
301
|
+
pointer-events: none;
|
|
302
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
303
|
+
z-index: 200;
|
|
304
|
+
}
|
|
305
|
+
#copy-toast.visible {
|
|
306
|
+
opacity: 1;
|
|
307
|
+
transform: translateX(-50%) translateY(0);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#paste-overlay {
|
|
311
|
+
display: none;
|
|
312
|
+
position: fixed;
|
|
313
|
+
top: 0;
|
|
314
|
+
left: 0;
|
|
315
|
+
right: 0;
|
|
316
|
+
bottom: 0;
|
|
317
|
+
background: var(--overlay-bg);
|
|
318
|
+
z-index: 150;
|
|
319
|
+
flex-direction: column;
|
|
320
|
+
align-items: center;
|
|
321
|
+
justify-content: center;
|
|
322
|
+
gap: 12px;
|
|
323
|
+
}
|
|
324
|
+
#paste-overlay.visible {
|
|
325
|
+
display: flex;
|
|
326
|
+
}
|
|
327
|
+
#paste-overlay label {
|
|
328
|
+
font-size: 15px;
|
|
329
|
+
color: #ffffff;
|
|
330
|
+
font-weight: 600;
|
|
331
|
+
}
|
|
332
|
+
#paste-input {
|
|
333
|
+
width: 80%;
|
|
334
|
+
max-width: 400px;
|
|
335
|
+
min-height: 80px;
|
|
336
|
+
background: var(--surface);
|
|
337
|
+
color: var(--text);
|
|
338
|
+
border: 1px solid var(--border);
|
|
339
|
+
border-radius: 8px;
|
|
340
|
+
padding: 10px;
|
|
341
|
+
font-size: 14px;
|
|
342
|
+
font-family: 'NerdFont', 'JetBrains Mono', monospace;
|
|
343
|
+
resize: vertical;
|
|
344
|
+
}
|
|
345
|
+
|
|
182
346
|
#reconnect-overlay {
|
|
183
347
|
display: none;
|
|
184
348
|
position: fixed;
|
|
@@ -186,7 +350,7 @@
|
|
|
186
350
|
left: 0;
|
|
187
351
|
right: 0;
|
|
188
352
|
bottom: 0;
|
|
189
|
-
background:
|
|
353
|
+
background: var(--overlay-bg);
|
|
190
354
|
z-index: 100;
|
|
191
355
|
flex-direction: column;
|
|
192
356
|
align-items: center;
|
|
@@ -198,6 +362,7 @@
|
|
|
198
362
|
}
|
|
199
363
|
#reconnect-overlay .msg {
|
|
200
364
|
font-size: 17px;
|
|
365
|
+
color: #ffffff;
|
|
201
366
|
}
|
|
202
367
|
.overlay-actions {
|
|
203
368
|
display: flex;
|
|
@@ -210,50 +375,63 @@
|
|
|
210
375
|
font-size: 15px;
|
|
211
376
|
font-weight: 600;
|
|
212
377
|
cursor: pointer;
|
|
378
|
+
transition: background 0.15s, transform 0.1s;
|
|
379
|
+
}
|
|
380
|
+
.overlay-actions button:active {
|
|
381
|
+
transform: scale(0.95);
|
|
213
382
|
}
|
|
214
383
|
#reconnect-btn {
|
|
215
|
-
background:
|
|
216
|
-
color:
|
|
384
|
+
background: var(--accent);
|
|
385
|
+
color: #ffffff;
|
|
386
|
+
}
|
|
387
|
+
#reconnect-btn:hover {
|
|
388
|
+
background: var(--accent-hover);
|
|
217
389
|
}
|
|
218
390
|
#back-to-sessions {
|
|
219
|
-
background:
|
|
220
|
-
color: #
|
|
391
|
+
background: rgba(255,255,255,0.15);
|
|
392
|
+
color: #ffffff;
|
|
393
|
+
}
|
|
394
|
+
#back-to-sessions:hover {
|
|
395
|
+
background: rgba(255,255,255,0.25);
|
|
221
396
|
}
|
|
397
|
+
|
|
222
398
|
</style>
|
|
223
399
|
</head>
|
|
224
400
|
<body>
|
|
225
401
|
<div id="status-bar">
|
|
226
402
|
<div class="left">
|
|
227
|
-
<button id="back-btn" onclick="location.href = '/'"
|
|
403
|
+
<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>
|
|
228
404
|
<span id="status-dot"></span>
|
|
229
405
|
<span id="session-name">…</span>
|
|
230
406
|
</div>
|
|
231
407
|
<div class="right">
|
|
232
408
|
<span id="status-text">Connecting…</span>
|
|
233
|
-
<span id="version-text" style="font-size: 11px; color:
|
|
234
|
-
<
|
|
409
|
+
<span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
|
|
410
|
+
<div class="bar-group">
|
|
411
|
+
<button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
|
|
412
|
+
<button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
|
|
413
|
+
</div>
|
|
414
|
+
<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>
|
|
415
|
+
<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>
|
|
235
416
|
</div>
|
|
236
417
|
</div>
|
|
237
418
|
|
|
238
419
|
<div id="terminal-container"></div>
|
|
420
|
+
<div id="copy-toast">Copied!</div>
|
|
239
421
|
|
|
240
422
|
<div id="key-bar">
|
|
241
|
-
<button class="key-btn" data-key="[A"
|
|
242
|
-
<button class="key-btn" data-key="[B"
|
|
243
|
-
<button class="key-btn" data-key="[D">←</button>
|
|
244
|
-
<button class="key-btn" data-key="[C">→</button>
|
|
423
|
+
<button class="key-btn" data-key="[A" title="Previous command">↑<span class="hint">prev</span></button>
|
|
424
|
+
<button class="key-btn" data-key="[B" title="Next command">↓<span class="hint">next</span></button>
|
|
425
|
+
<button class="key-btn" data-key="[D" title="Left">←</button>
|
|
426
|
+
<button class="key-btn" data-key="[C" title="Right">→</button>
|
|
427
|
+
<button class="key-btn" data-key="[H" title="Home">Home</button>
|
|
428
|
+
<button class="key-btn" data-key="[F" title="End">End</button>
|
|
245
429
|
<div class="key-sep"></div>
|
|
246
|
-
<button class="key-btn
|
|
247
|
-
<button class="key-btn wide" data-key="
">Enter</button>
|
|
248
|
-
<button class="key-btn" data-key="">Esc</button>
|
|
430
|
+
<button class="key-btn" id="paste-btn" title="Paste from clipboard">Paste</button>
|
|
249
431
|
<div class="key-sep"></div>
|
|
250
|
-
<button class="key-btn" data-key="&#
|
|
251
|
-
<button class="key-btn" data-key="">^D</button>
|
|
252
|
-
<button class="key-btn" data-key="">^Z</button>
|
|
253
|
-
<button class="key-btn" data-key="">^L</button>
|
|
432
|
+
<button class="key-btn wide" data-key="	" title="Autocomplete">Tab</button>
|
|
254
433
|
<div class="key-sep"></div>
|
|
255
|
-
<button class="key-btn"
|
|
256
|
-
<button class="key-btn" id="zoom-in">A+</button>
|
|
434
|
+
<button class="key-btn" data-key="" title="Interrupt process">^C<span class="hint">stop</span></button>
|
|
257
435
|
</div>
|
|
258
436
|
|
|
259
437
|
<div id="reconnect-overlay">
|
|
@@ -264,6 +442,15 @@
|
|
|
264
442
|
</div>
|
|
265
443
|
</div>
|
|
266
444
|
|
|
445
|
+
<div id="paste-overlay">
|
|
446
|
+
<label for="paste-input">Paste your text below</label>
|
|
447
|
+
<textarea id="paste-input" placeholder="Long-press here and paste…"></textarea>
|
|
448
|
+
<div class="overlay-actions">
|
|
449
|
+
<button id="paste-cancel" style="background: rgba(255,255,255,0.15); color: #ffffff;">Cancel</button>
|
|
450
|
+
<button id="paste-send" style="background: var(--accent); color: #ffffff;">Send</button>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
267
454
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
268
455
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
269
456
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
@@ -277,6 +464,50 @@
|
|
|
277
464
|
const statusText = document.getElementById('status-text');
|
|
278
465
|
const sessionName = document.getElementById('session-name');
|
|
279
466
|
const reconnectOverlay = document.getElementById('reconnect-overlay');
|
|
467
|
+
let sessionExited = false;
|
|
468
|
+
let reconnectTimer = null;
|
|
469
|
+
let reconnectDelay = 3000;
|
|
470
|
+
const MAX_RECONNECT_DELAY = 30000;
|
|
471
|
+
|
|
472
|
+
// Terminal themes
|
|
473
|
+
const darkTermTheme = {
|
|
474
|
+
background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#aeafad', cursorAccent: '#1e1e1e',
|
|
475
|
+
selectionBackground: 'rgba(38, 79, 120, 0.5)',
|
|
476
|
+
black: '#000000', red: '#cd3131', green: '#0dbc79', yellow: '#e5e510',
|
|
477
|
+
blue: '#2472c8', magenta: '#bc3fbc', cyan: '#11a8cd', white: '#e5e5e5',
|
|
478
|
+
brightBlack: '#666666', brightRed: '#f14c4c', brightGreen: '#23d18b',
|
|
479
|
+
brightYellow: '#f5f543', brightBlue: '#3b8eea', brightMagenta: '#d670d6',
|
|
480
|
+
brightCyan: '#29b8db', brightWhite: '#e5e5e5',
|
|
481
|
+
};
|
|
482
|
+
const lightTermTheme = {
|
|
483
|
+
background: '#ffffff', foreground: '#1e1e1e', cursor: '#000000', cursorAccent: '#ffffff',
|
|
484
|
+
selectionBackground: 'rgba(0, 120, 215, 0.3)',
|
|
485
|
+
black: '#000000', red: '#cd3131', green: '#00bc7c', yellow: '#949800',
|
|
486
|
+
blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555',
|
|
487
|
+
brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14',
|
|
488
|
+
brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc',
|
|
489
|
+
brightCyan: '#0598bc', brightWhite: '#a5a5a5',
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
function getTheme() { return localStorage.getItem('termbeam-theme') || 'dark'; }
|
|
493
|
+
function applyTheme(theme) {
|
|
494
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
495
|
+
document.querySelector('meta[name="theme-color"]').content =
|
|
496
|
+
theme === 'light' ? '#f3f3f3' : '#1e1e1e';
|
|
497
|
+
const btn = document.getElementById('theme-toggle');
|
|
498
|
+
if (btn) btn.innerHTML = theme === 'light'
|
|
499
|
+
? '<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>'
|
|
500
|
+
: '<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>';
|
|
501
|
+
localStorage.setItem('termbeam-theme', theme);
|
|
502
|
+
if (window._term) {
|
|
503
|
+
window._term.options.theme = theme === 'light' ? lightTermTheme : darkTermTheme;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
applyTheme(getTheme());
|
|
507
|
+
|
|
508
|
+
document.getElementById('theme-toggle').addEventListener('click', () => {
|
|
509
|
+
applyTheme(getTheme() === 'light' ? 'dark' : 'light');
|
|
510
|
+
});
|
|
280
511
|
|
|
281
512
|
// Load Nerd Font, then init terminal
|
|
282
513
|
const nerdFont = new FontFace(
|
|
@@ -307,32 +538,11 @@
|
|
|
307
538
|
fontWeightBold: 'bold',
|
|
308
539
|
letterSpacing: 0,
|
|
309
540
|
lineHeight: 1.1,
|
|
310
|
-
theme:
|
|
311
|
-
background: '#1a1a2e',
|
|
312
|
-
foreground: '#e0e0e0',
|
|
313
|
-
cursor: '#533483',
|
|
314
|
-
cursorAccent: '#1a1a2e',
|
|
315
|
-
selectionBackground: 'rgba(83, 52, 131, 0.4)',
|
|
316
|
-
black: '#1a1a2e',
|
|
317
|
-
red: '#e74c3c',
|
|
318
|
-
green: '#2ecc71',
|
|
319
|
-
yellow: '#f1c40f',
|
|
320
|
-
blue: '#3498db',
|
|
321
|
-
magenta: '#9b59b6',
|
|
322
|
-
cyan: '#1abc9c',
|
|
323
|
-
white: '#ecf0f1',
|
|
324
|
-
brightBlack: '#636e72',
|
|
325
|
-
brightRed: '#ff6b6b',
|
|
326
|
-
brightGreen: '#55efc4',
|
|
327
|
-
brightYellow: '#ffeaa7',
|
|
328
|
-
brightBlue: '#74b9ff',
|
|
329
|
-
brightMagenta: '#a29bfe',
|
|
330
|
-
brightCyan: '#81ecec',
|
|
331
|
-
brightWhite: '#ffffff',
|
|
332
|
-
},
|
|
541
|
+
theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
|
|
333
542
|
allowProposedApi: true,
|
|
334
543
|
scrollback: 10000,
|
|
335
544
|
});
|
|
545
|
+
window._term = term;
|
|
336
546
|
|
|
337
547
|
const fitAddon = new window.FitAddon.FitAddon();
|
|
338
548
|
const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
|
|
@@ -351,8 +561,9 @@
|
|
|
351
561
|
|
|
352
562
|
ws.onopen = () => {
|
|
353
563
|
statusDot.className = 'connected';
|
|
354
|
-
statusText.textContent = '
|
|
564
|
+
statusText.textContent = '';
|
|
355
565
|
reconnectOverlay.classList.remove('visible');
|
|
566
|
+
reconnectDelay = 3000;
|
|
356
567
|
// Attach to session
|
|
357
568
|
ws.send(JSON.stringify({ type: 'attach', sessionId }));
|
|
358
569
|
};
|
|
@@ -369,6 +580,8 @@
|
|
|
369
580
|
ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
|
|
370
581
|
}
|
|
371
582
|
} else if (msg.type === 'exit') {
|
|
583
|
+
sessionExited = true;
|
|
584
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
372
585
|
statusText.textContent = `Exited (code ${msg.code})`;
|
|
373
586
|
statusDot.className = '';
|
|
374
587
|
reconnectOverlay.querySelector('.msg').textContent =
|
|
@@ -388,6 +601,9 @@
|
|
|
388
601
|
statusDot.className = '';
|
|
389
602
|
statusText.textContent = 'Disconnected';
|
|
390
603
|
reconnectOverlay.classList.add('visible');
|
|
604
|
+
if (!sessionExited) {
|
|
605
|
+
scheduleReconnect();
|
|
606
|
+
}
|
|
391
607
|
};
|
|
392
608
|
|
|
393
609
|
ws.onerror = () => {
|
|
@@ -415,7 +631,7 @@
|
|
|
415
631
|
window.addEventListener('resize', doResize);
|
|
416
632
|
screen.orientation?.addEventListener('change', () => setTimeout(doResize, 150));
|
|
417
633
|
|
|
418
|
-
// Key bar
|
|
634
|
+
// Key bar
|
|
419
635
|
// Use mousedown + preventDefault to stop buttons from stealing focus/opening keyboard
|
|
420
636
|
document.getElementById('key-bar').addEventListener('mousedown', (e) => {
|
|
421
637
|
// Only prevent default on buttons, not the scrollable bar itself
|
|
@@ -429,34 +645,103 @@
|
|
|
429
645
|
.addEventListener('touchstart', () => {}, { passive: true });
|
|
430
646
|
document.getElementById('key-bar').addEventListener('click', (e) => {
|
|
431
647
|
const btn = e.target.closest('.key-btn');
|
|
432
|
-
if (!btn || btn.
|
|
648
|
+
if (!btn || !btn.dataset.key) return;
|
|
433
649
|
if (ws && ws.readyState === 1) {
|
|
434
650
|
ws.send(JSON.stringify({ type: 'input', data: btn.dataset.key }));
|
|
435
651
|
}
|
|
436
652
|
// Don't call term.focus() here — it opens the soft keyboard
|
|
437
653
|
});
|
|
438
654
|
|
|
439
|
-
// Zoom
|
|
440
|
-
const MIN_FONT = 2,
|
|
441
|
-
|
|
442
|
-
let fontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
|
|
443
|
-
|
|
655
|
+
// Zoom (status bar)
|
|
656
|
+
const MIN_FONT = 2, MAX_FONT = 28;
|
|
657
|
+
let fontSize = savedFontSize;
|
|
444
658
|
function applyZoom(size) {
|
|
445
659
|
fontSize = Math.max(MIN_FONT, Math.min(MAX_FONT, size));
|
|
446
660
|
term.options.fontSize = fontSize;
|
|
447
661
|
localStorage.setItem('termbeam-fontsize', fontSize);
|
|
448
662
|
doResize();
|
|
449
663
|
}
|
|
664
|
+
document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
|
|
665
|
+
document.getElementById('zoom-out').addEventListener('click', () => applyZoom(fontSize - 2));
|
|
666
|
+
|
|
667
|
+
// Clipboard: copy on selection
|
|
668
|
+
function showToast(msg) {
|
|
669
|
+
const toast = document.getElementById('copy-toast');
|
|
670
|
+
toast.textContent = msg;
|
|
671
|
+
toast.classList.add('visible');
|
|
672
|
+
clearTimeout(toast._timer);
|
|
673
|
+
toast._timer = setTimeout(() => toast.classList.remove('visible'), 1500);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
term.onSelectionChange(() => {
|
|
677
|
+
const sel = term.getSelection();
|
|
678
|
+
if (sel && navigator.clipboard && navigator.clipboard.writeText) {
|
|
679
|
+
navigator.clipboard.writeText(sel).then(() => {
|
|
680
|
+
showToast('Copied!');
|
|
681
|
+
}).catch(() => {});
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// Clipboard: paste button
|
|
686
|
+
const pasteOverlay = document.getElementById('paste-overlay');
|
|
687
|
+
const pasteInput = document.getElementById('paste-input');
|
|
688
|
+
|
|
689
|
+
function openPasteModal() {
|
|
690
|
+
pasteInput.value = '';
|
|
691
|
+
pasteOverlay.classList.add('visible');
|
|
692
|
+
pasteInput.focus();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function closePasteModal() {
|
|
696
|
+
pasteOverlay.classList.remove('visible');
|
|
697
|
+
pasteInput.value = '';
|
|
698
|
+
}
|
|
450
699
|
|
|
451
|
-
document.getElementById('
|
|
452
|
-
|
|
700
|
+
document.getElementById('paste-btn').addEventListener('mousedown', (e) => {
|
|
701
|
+
e.preventDefault();
|
|
453
702
|
});
|
|
454
|
-
document.getElementById('
|
|
455
|
-
|
|
703
|
+
document.getElementById('paste-btn').addEventListener('click', () => {
|
|
704
|
+
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
705
|
+
navigator.clipboard.readText().then((text) => {
|
|
706
|
+
if (text && ws && ws.readyState === 1) {
|
|
707
|
+
ws.send(JSON.stringify({ type: 'input', data: text }));
|
|
708
|
+
showToast('Pasted!');
|
|
709
|
+
}
|
|
710
|
+
}).catch(() => {
|
|
711
|
+
openPasteModal();
|
|
712
|
+
});
|
|
713
|
+
} else {
|
|
714
|
+
openPasteModal();
|
|
715
|
+
}
|
|
456
716
|
});
|
|
457
717
|
|
|
718
|
+
document.getElementById('paste-send').addEventListener('click', () => {
|
|
719
|
+
const text = pasteInput.value;
|
|
720
|
+
if (text && ws && ws.readyState === 1) {
|
|
721
|
+
ws.send(JSON.stringify({ type: 'input', data: text }));
|
|
722
|
+
}
|
|
723
|
+
closePasteModal();
|
|
724
|
+
});
|
|
725
|
+
document.getElementById('paste-cancel').addEventListener('click', closePasteModal);
|
|
726
|
+
|
|
727
|
+
function scheduleReconnect() {
|
|
728
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
729
|
+
const seconds = Math.round(reconnectDelay / 1000);
|
|
730
|
+
reconnectOverlay.querySelector('.msg').textContent =
|
|
731
|
+
`Disconnected — reconnecting in ${seconds}s…`;
|
|
732
|
+
reconnectTimer = setTimeout(() => {
|
|
733
|
+
reconnectTimer = null;
|
|
734
|
+
reconnectOverlay.querySelector('.msg').textContent = 'Reconnecting…';
|
|
735
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY);
|
|
736
|
+
connect();
|
|
737
|
+
}, reconnectDelay);
|
|
738
|
+
}
|
|
739
|
+
|
|
458
740
|
// Reconnect
|
|
459
741
|
document.getElementById('reconnect-btn').addEventListener('click', () => {
|
|
742
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
743
|
+
sessionExited = false;
|
|
744
|
+
reconnectDelay = 3000;
|
|
460
745
|
term.clear();
|
|
461
746
|
connect();
|
|
462
747
|
});
|
|
@@ -486,11 +771,11 @@
|
|
|
486
771
|
if (keyboardHeight > 50) {
|
|
487
772
|
// Keyboard is open — move key bar above it
|
|
488
773
|
keyBar.style.bottom = keyboardHeight + 'px';
|
|
489
|
-
container.style.bottom =
|
|
774
|
+
container.style.bottom = 52 + keyboardHeight + 'px';
|
|
490
775
|
} else {
|
|
491
776
|
// Keyboard closed
|
|
492
777
|
keyBar.style.bottom = '0px';
|
|
493
|
-
container.style.bottom = '
|
|
778
|
+
container.style.bottom = '52px';
|
|
494
779
|
}
|
|
495
780
|
// Refit terminal to new available space
|
|
496
781
|
setTimeout(() => doResize(), 50);
|
|
@@ -518,6 +803,10 @@
|
|
|
518
803
|
|
|
519
804
|
connect();
|
|
520
805
|
}
|
|
806
|
+
|
|
807
|
+
if ('serviceWorker' in navigator) {
|
|
808
|
+
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
|
809
|
+
}
|
|
521
810
|
</script>
|
|
522
811
|
</body>
|
|
523
812
|
</html>
|