termbeam 1.2.3 → 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 +84 -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;
|
|
@@ -2119,6 +2147,7 @@
|
|
|
2119
2147
|
document.documentElement.scrollTop = 0;
|
|
2120
2148
|
document.body.scrollTop = 0;
|
|
2121
2149
|
}
|
|
2150
|
+
let wasKeyboardOpen = false;
|
|
2122
2151
|
function onViewportResize() {
|
|
2123
2152
|
const vv = window.visualViewport;
|
|
2124
2153
|
const keyboardHeight = window.innerHeight - vv.height;
|
|
@@ -2134,11 +2163,22 @@
|
|
|
2134
2163
|
keyBar.style.paddingBottom = '';
|
|
2135
2164
|
terminalsWrapper.style.bottom = '';
|
|
2136
2165
|
}
|
|
2166
|
+
const keyboardJustClosed = wasKeyboardOpen && !keyboardOpen;
|
|
2167
|
+
wasKeyboardOpen = keyboardOpen;
|
|
2137
2168
|
resetScroll();
|
|
2138
2169
|
clearTimeout(vpResizeTimer);
|
|
2139
2170
|
vpResizeTimer = setTimeout(() => {
|
|
2140
2171
|
resetScroll();
|
|
2141
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
|
+
}
|
|
2142
2182
|
}, 150);
|
|
2143
2183
|
}
|
|
2144
2184
|
function onViewportScroll() {
|
|
@@ -2228,6 +2268,7 @@
|
|
|
2228
2268
|
theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
|
|
2229
2269
|
allowProposedApi: true,
|
|
2230
2270
|
scrollback: 10000,
|
|
2271
|
+
scrollOnOutput: false,
|
|
2231
2272
|
});
|
|
2232
2273
|
|
|
2233
2274
|
const fitAddon = new window.FitAddon.FitAddon();
|
|
@@ -2237,9 +2278,44 @@
|
|
|
2237
2278
|
|
|
2238
2279
|
const container = document.createElement('div');
|
|
2239
2280
|
container.className = 'terminal-pane';
|
|
2281
|
+
container.style.position = 'relative';
|
|
2240
2282
|
terminalsWrapper.appendChild(container);
|
|
2241
2283
|
term.open(container);
|
|
2242
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
|
+
|
|
2243
2319
|
// Pointer-event scroll handler — uses setPointerCapture so scrolling
|
|
2244
2320
|
// survives xterm DOM re-renders under the finger (touch on a letter).
|
|
2245
2321
|
(function () {
|
|
@@ -2349,6 +2425,8 @@
|
|
|
2349
2425
|
term,
|
|
2350
2426
|
fitAddon,
|
|
2351
2427
|
container,
|
|
2428
|
+
coalescedWrite,
|
|
2429
|
+
scrollBtn,
|
|
2352
2430
|
ws: null,
|
|
2353
2431
|
exited: false,
|
|
2354
2432
|
reconnectTimer: null,
|
|
@@ -2361,6 +2439,10 @@
|
|
|
2361
2439
|
// Terminal input → WebSocket
|
|
2362
2440
|
term.onData((input) => {
|
|
2363
2441
|
if (ms.ws && ms.ws.readyState === 1) {
|
|
2442
|
+
// Auto-scroll to bottom on user input
|
|
2443
|
+
if (userScrolledUp) {
|
|
2444
|
+
ms.term.scrollToBottom();
|
|
2445
|
+
}
|
|
2364
2446
|
let data = input;
|
|
2365
2447
|
if (ctrlActive && input.length === 1) {
|
|
2366
2448
|
const code = input.toLowerCase().charCodeAt(0);
|
|
@@ -2412,7 +2494,7 @@
|
|
|
2412
2494
|
try {
|
|
2413
2495
|
const msg = JSON.parse(event.data);
|
|
2414
2496
|
if (msg.type === 'output') {
|
|
2415
|
-
ms.
|
|
2497
|
+
ms.coalescedWrite(msg.data);
|
|
2416
2498
|
ms.lastActivity = Date.now();
|
|
2417
2499
|
} else if (msg.type === 'attached') {
|
|
2418
2500
|
if (ms.container.classList.contains('visible')) {
|
|
@@ -2448,7 +2530,7 @@
|
|
|
2448
2530
|
}
|
|
2449
2531
|
}
|
|
2450
2532
|
} catch {
|
|
2451
|
-
ms.
|
|
2533
|
+
ms.coalescedWrite(event.data);
|
|
2452
2534
|
}
|
|
2453
2535
|
};
|
|
2454
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,
|