termbeam 1.2.2 → 1.2.4
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/package.json +4 -3
- package/public/terminal.html +85 -2
- package/src/server.js +1 -1
- package/src/shells.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termbeam",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/termbeam.js",
|
|
11
11
|
"dev": "node bin/termbeam.js --generate-password",
|
|
12
|
-
"test": "node -e \"require('child_process').execFileSync(process.execPath,['--test',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
|
|
13
|
-
"test:coverage": "c8 --exclude=src/tunnel.js --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"require('child_process').execFileSync(process.execPath,['--test','--test-reporter=spec','--test-reporter-destination=stdout',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
|
|
12
|
+
"test": "node -e \"require('child_process').execFileSync(process.execPath,['--test',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
|
|
13
|
+
"test:coverage": "c8 --exclude=src/tunnel.js --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"require('child_process').execFileSync(process.execPath,['--test','--test-reporter=spec','--test-reporter-destination=stdout',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
|
|
14
14
|
"prepare": "husky",
|
|
15
15
|
"format": "prettier --write .",
|
|
16
16
|
"lint": "node --check src/*.js bin/*.js",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"ws": "^8.19.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
+
"@playwright/test": "^1.58.2",
|
|
76
77
|
"c8": "^11.0.0",
|
|
77
78
|
"husky": "^9.1.7",
|
|
78
79
|
"lint-staged": "^16.2.7",
|
package/public/terminal.html
CHANGED
|
@@ -591,6 +591,34 @@
|
|
|
591
591
|
touch-action: none;
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
+
/* ===== Scroll-to-bottom indicator ===== */
|
|
595
|
+
.scroll-bottom-btn {
|
|
596
|
+
display: none;
|
|
597
|
+
position: absolute;
|
|
598
|
+
bottom: 8px;
|
|
599
|
+
right: 16px;
|
|
600
|
+
width: 36px;
|
|
601
|
+
height: 36px;
|
|
602
|
+
border-radius: 50%;
|
|
603
|
+
background: var(--accent);
|
|
604
|
+
color: #fff;
|
|
605
|
+
border: none;
|
|
606
|
+
font-size: 18px;
|
|
607
|
+
line-height: 36px;
|
|
608
|
+
text-align: center;
|
|
609
|
+
cursor: pointer;
|
|
610
|
+
z-index: 50;
|
|
611
|
+
opacity: 0.85;
|
|
612
|
+
transition: opacity 0.15s;
|
|
613
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
|
614
|
+
}
|
|
615
|
+
.scroll-bottom-btn:hover {
|
|
616
|
+
opacity: 1;
|
|
617
|
+
}
|
|
618
|
+
.scroll-bottom-btn.visible {
|
|
619
|
+
display: block;
|
|
620
|
+
}
|
|
621
|
+
|
|
594
622
|
/* ===== Toasts & Overlays ===== */
|
|
595
623
|
#copy-toast {
|
|
596
624
|
position: fixed;
|
|
@@ -1245,6 +1273,7 @@
|
|
|
1245
1273
|
border-radius: 10px;
|
|
1246
1274
|
cursor: pointer;
|
|
1247
1275
|
overflow: hidden;
|
|
1276
|
+
flex-shrink: 0;
|
|
1248
1277
|
transition:
|
|
1249
1278
|
background 0.15s,
|
|
1250
1279
|
border-color 0.15s;
|
|
@@ -2118,6 +2147,7 @@
|
|
|
2118
2147
|
document.documentElement.scrollTop = 0;
|
|
2119
2148
|
document.body.scrollTop = 0;
|
|
2120
2149
|
}
|
|
2150
|
+
let wasKeyboardOpen = false;
|
|
2121
2151
|
function onViewportResize() {
|
|
2122
2152
|
const vv = window.visualViewport;
|
|
2123
2153
|
const keyboardHeight = window.innerHeight - vv.height;
|
|
@@ -2133,11 +2163,22 @@
|
|
|
2133
2163
|
keyBar.style.paddingBottom = '';
|
|
2134
2164
|
terminalsWrapper.style.bottom = '';
|
|
2135
2165
|
}
|
|
2166
|
+
const keyboardJustClosed = wasKeyboardOpen && !keyboardOpen;
|
|
2167
|
+
wasKeyboardOpen = keyboardOpen;
|
|
2136
2168
|
resetScroll();
|
|
2137
2169
|
clearTimeout(vpResizeTimer);
|
|
2138
2170
|
vpResizeTimer = setTimeout(() => {
|
|
2139
2171
|
resetScroll();
|
|
2140
2172
|
doResize();
|
|
2173
|
+
// When keyboard closes, the terminal grows taller — scroll to
|
|
2174
|
+
// bottom so the cursor stays visible instead of appearing offset.
|
|
2175
|
+
if (keyboardJustClosed) {
|
|
2176
|
+
for (const [, ms] of managed) {
|
|
2177
|
+
if (ms.container.classList.contains('visible')) {
|
|
2178
|
+
ms.term.scrollToBottom();
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2141
2182
|
}, 150);
|
|
2142
2183
|
}
|
|
2143
2184
|
function onViewportScroll() {
|
|
@@ -2227,6 +2268,7 @@
|
|
|
2227
2268
|
theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
|
|
2228
2269
|
allowProposedApi: true,
|
|
2229
2270
|
scrollback: 10000,
|
|
2271
|
+
scrollOnOutput: false,
|
|
2230
2272
|
});
|
|
2231
2273
|
|
|
2232
2274
|
const fitAddon = new window.FitAddon.FitAddon();
|
|
@@ -2236,9 +2278,44 @@
|
|
|
2236
2278
|
|
|
2237
2279
|
const container = document.createElement('div');
|
|
2238
2280
|
container.className = 'terminal-pane';
|
|
2281
|
+
container.style.position = 'relative';
|
|
2239
2282
|
terminalsWrapper.appendChild(container);
|
|
2240
2283
|
term.open(container);
|
|
2241
2284
|
|
|
2285
|
+
// Scroll-to-bottom button
|
|
2286
|
+
const scrollBtn = document.createElement('button');
|
|
2287
|
+
scrollBtn.className = 'scroll-bottom-btn';
|
|
2288
|
+
scrollBtn.textContent = '↓';
|
|
2289
|
+
scrollBtn.title = 'Scroll to bottom';
|
|
2290
|
+
scrollBtn.addEventListener('click', (e) => {
|
|
2291
|
+
e.stopPropagation();
|
|
2292
|
+
term.scrollToBottom();
|
|
2293
|
+
});
|
|
2294
|
+
container.appendChild(scrollBtn);
|
|
2295
|
+
|
|
2296
|
+
// Write coalescer — batch rapid term.write() calls into one per frame
|
|
2297
|
+
// so ANSI cursor-up/down pairs within a frame are processed atomically
|
|
2298
|
+
let writeBuf = '';
|
|
2299
|
+
let writeRaf = null;
|
|
2300
|
+
function coalescedWrite(data) {
|
|
2301
|
+
writeBuf += data;
|
|
2302
|
+
if (!writeRaf) {
|
|
2303
|
+
writeRaf = requestAnimationFrame(() => {
|
|
2304
|
+
term.write(writeBuf);
|
|
2305
|
+
writeBuf = '';
|
|
2306
|
+
writeRaf = null;
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// Track whether user has scrolled away from bottom
|
|
2312
|
+
let userScrolledUp = false;
|
|
2313
|
+
term.onScroll(() => {
|
|
2314
|
+
const buf = term.buffer.active;
|
|
2315
|
+
userScrolledUp = buf.viewportY < buf.baseY;
|
|
2316
|
+
scrollBtn.classList.toggle('visible', userScrolledUp);
|
|
2317
|
+
});
|
|
2318
|
+
|
|
2242
2319
|
// Pointer-event scroll handler — uses setPointerCapture so scrolling
|
|
2243
2320
|
// survives xterm DOM re-renders under the finger (touch on a letter).
|
|
2244
2321
|
(function () {
|
|
@@ -2348,6 +2425,8 @@
|
|
|
2348
2425
|
term,
|
|
2349
2426
|
fitAddon,
|
|
2350
2427
|
container,
|
|
2428
|
+
coalescedWrite,
|
|
2429
|
+
scrollBtn,
|
|
2351
2430
|
ws: null,
|
|
2352
2431
|
exited: false,
|
|
2353
2432
|
reconnectTimer: null,
|
|
@@ -2360,6 +2439,10 @@
|
|
|
2360
2439
|
// Terminal input → WebSocket
|
|
2361
2440
|
term.onData((input) => {
|
|
2362
2441
|
if (ms.ws && ms.ws.readyState === 1) {
|
|
2442
|
+
// Auto-scroll to bottom on user input
|
|
2443
|
+
if (userScrolledUp) {
|
|
2444
|
+
ms.term.scrollToBottom();
|
|
2445
|
+
}
|
|
2363
2446
|
let data = input;
|
|
2364
2447
|
if (ctrlActive && input.length === 1) {
|
|
2365
2448
|
const code = input.toLowerCase().charCodeAt(0);
|
|
@@ -2411,7 +2494,7 @@
|
|
|
2411
2494
|
try {
|
|
2412
2495
|
const msg = JSON.parse(event.data);
|
|
2413
2496
|
if (msg.type === 'output') {
|
|
2414
|
-
ms.
|
|
2497
|
+
ms.coalescedWrite(msg.data);
|
|
2415
2498
|
ms.lastActivity = Date.now();
|
|
2416
2499
|
} else if (msg.type === 'attached') {
|
|
2417
2500
|
if (ms.container.classList.contains('visible')) {
|
|
@@ -2447,7 +2530,7 @@
|
|
|
2447
2530
|
}
|
|
2448
2531
|
}
|
|
2449
2532
|
} catch {
|
|
2450
|
-
ms.
|
|
2533
|
+
ms.coalescedWrite(event.data);
|
|
2451
2534
|
}
|
|
2452
2535
|
};
|
|
2453
2536
|
|
package/src/server.js
CHANGED
|
@@ -53,7 +53,7 @@ function createTermBeamServer(overrides = {}) {
|
|
|
53
53
|
res.setHeader('Cache-Control', 'no-store');
|
|
54
54
|
res.setHeader(
|
|
55
55
|
'Content-Security-Policy',
|
|
56
|
-
"default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; connect-src 'self' ws: wss: https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net",
|
|
56
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; connect-src 'self' ws: wss: https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net",
|
|
57
57
|
);
|
|
58
58
|
next();
|
|
59
59
|
});
|
package/src/shells.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const os = require('os');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
-
const
|
|
3
|
+
const child_process = require('child_process');
|
|
4
4
|
|
|
5
5
|
const KNOWN_WINDOWS_SHELLS = [
|
|
6
6
|
{ name: 'PowerShell (Core)', cmd: 'pwsh.exe' },
|
|
@@ -21,7 +21,7 @@ function detectWindowsShells() {
|
|
|
21
21
|
const shells = [];
|
|
22
22
|
for (const { name, cmd } of KNOWN_WINDOWS_SHELLS) {
|
|
23
23
|
try {
|
|
24
|
-
const result = execFileSync('where', [cmd], {
|
|
24
|
+
const result = child_process.execFileSync('where', [cmd], {
|
|
25
25
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
26
26
|
encoding: 'utf8',
|
|
27
27
|
timeout: 3000,
|