webtun 1.4.2 → 1.4.5
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 +44 -30
- package/bin/webtun.js +16 -22
- package/package.json +7 -2
- package/postinstall.js +148 -61
- package/public/index.html +267 -18
- package/server.js +349 -136
package/public/index.html
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
15
15
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-search@0.15.0/lib/addon-search.min.js"></script>
|
|
16
16
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.7.0/lib/addon-unicode11.min.js"></script>
|
|
18
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.1/lib/addon-webgl.min.js"></script>
|
|
17
19
|
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
|
18
20
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
|
|
19
21
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.css">
|
|
@@ -166,6 +168,8 @@ svg { display: block; }
|
|
|
166
168
|
.term-wrapper.active { display: flex; }
|
|
167
169
|
.xterm { flex: 1; }
|
|
168
170
|
.xterm-viewport { border-radius: 0; }
|
|
171
|
+
.term-bell-flash { animation: bellFlash 0.15s ease-out; }
|
|
172
|
+
@keyframes bellFlash { 0% { filter: brightness(1.8); } 100% { filter: brightness(1); } }
|
|
169
173
|
#terminals.tiles-mode { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-auto-rows: 1fr; gap: 4px; padding: 4px; position: relative; overflow: auto; min-height: 0; overscroll-behavior: none; }
|
|
170
174
|
#terminals.tiles-mode .term-wrapper { position: relative; inset: unset; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; min-height: 150px; }
|
|
171
175
|
#terminals.tiles-mode .term-wrapper.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
@@ -1152,6 +1156,7 @@ svg { display: block; }
|
|
|
1152
1156
|
<div style="display:flex;align-items:center;gap:8px">
|
|
1153
1157
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
|
1154
1158
|
<span>WebTun</span>
|
|
1159
|
+
<span id="about-version" style="color:var(--fg3);font-size:11px"></span>
|
|
1155
1160
|
</div>
|
|
1156
1161
|
<div style="display:flex;align-items:center;gap:8px">
|
|
1157
1162
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--fg2)" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
@@ -1192,6 +1197,15 @@ svg { display: block; }
|
|
|
1192
1197
|
<div class="ctx-item danger" id="ctx-delete" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> Delete</div>
|
|
1193
1198
|
</div>
|
|
1194
1199
|
|
|
1200
|
+
<!-- Terminal context menu -->
|
|
1201
|
+
<div id="term-ctx-menu" role="menu" style="position:fixed;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 4px 20px var(--shadow-color);z-index:500;display:none;min-width:160px;overflow:hidden">
|
|
1202
|
+
<div class="ctx-item" id="term-ctx-copy" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy</div>
|
|
1203
|
+
<div class="ctx-item" id="term-ctx-paste" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="14" y="2" width="8" height="8" rx="2"/><path d="M16 14H8M8 18h8"/><path d="M5 4h4"/><rect x="2" y="6" width="16" height="16" rx="2"/></svg> Paste</div>
|
|
1204
|
+
<div class="ctx-item" id="term-ctx-select-all" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/><path d="M3 9h18"/><path d="M3 15h18"/></svg> Select All</div>
|
|
1205
|
+
<div class="ctx-sep"></div>
|
|
1206
|
+
<div class="ctx-item" id="term-ctx-clear" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/></svg> Clear</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
|
|
1195
1209
|
<!-- Fuzzy Finder Modal -->
|
|
1196
1210
|
<div class="overlay" id="finder-overlay">
|
|
1197
1211
|
<div class="modal modal-md" style="padding:12px">
|
|
@@ -1361,6 +1375,7 @@ let settings = {
|
|
|
1361
1375
|
cursor: 'block', blink: true, scrollback: 5000, bell: false,
|
|
1362
1376
|
mobilekeys: false, confirmclose: true, datasaver: false, autostart: false, keepAwake: false
|
|
1363
1377
|
};
|
|
1378
|
+
let serverPlatform = '';
|
|
1364
1379
|
|
|
1365
1380
|
const isElectron = !!(window.electronAPI && window.electronAPI.isElectron);
|
|
1366
1381
|
|
|
@@ -1470,6 +1485,7 @@ async function unlockApp() {
|
|
|
1470
1485
|
const info = await api('/api/home');
|
|
1471
1486
|
homeDir = info.home;
|
|
1472
1487
|
currentPath = homeDir;
|
|
1488
|
+
serverPlatform = info.platform || '';
|
|
1473
1489
|
document.getElementById('hostname-badge').textContent = info.hostname || '—';
|
|
1474
1490
|
|
|
1475
1491
|
// Check tmux availability
|
|
@@ -1818,10 +1834,77 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1818
1834
|
setupVisualViewport();
|
|
1819
1835
|
};
|
|
1820
1836
|
|
|
1837
|
+
// OSC sequence handler: intercepts OSC 7, 133, 52 before passing to xterm.js
|
|
1838
|
+
let oscBuf = '';
|
|
1839
|
+
function processTerminalOutput(data) {
|
|
1840
|
+
const str = typeof data === 'string' ? data : new TextDecoder().decode(data);
|
|
1841
|
+
let out = '';
|
|
1842
|
+
let i = 0;
|
|
1843
|
+
while (i < str.length) {
|
|
1844
|
+
// Look for ESC ] (OSC introducer)
|
|
1845
|
+
if (str[i] === '\x1b' && str[i + 1] === ']') {
|
|
1846
|
+
const endIdx = str.indexOf('\x07', i + 2);
|
|
1847
|
+
const stIdx = str.indexOf('\x1b\\', i + 2);
|
|
1848
|
+
let oscEnd = -1;
|
|
1849
|
+
if (endIdx !== -1 && (stIdx === -1 || endIdx < stIdx)) oscEnd = endIdx;
|
|
1850
|
+
else if (stIdx !== -1) oscEnd = stIdx;
|
|
1851
|
+
|
|
1852
|
+
if (oscEnd !== -1) {
|
|
1853
|
+
const oscData = str.substring(i + 2, oscEnd);
|
|
1854
|
+
const semi = oscData.indexOf(';');
|
|
1855
|
+
if (semi !== -1) {
|
|
1856
|
+
const code = oscData.substring(0, semi);
|
|
1857
|
+
const value = oscData.substring(semi + 1);
|
|
1858
|
+
if (code === '7') {
|
|
1859
|
+
// OSC 7: CWD update — file://hostname/path
|
|
1860
|
+
try {
|
|
1861
|
+
const url = new URL(value);
|
|
1862
|
+
tab.cwd = decodeURIComponent(url.pathname);
|
|
1863
|
+
} catch (_) {}
|
|
1864
|
+
} else if (code === '133') {
|
|
1865
|
+
// OSC 133: Shell integration markers (prompt/cmd start/end/done)
|
|
1866
|
+
// Handled silently — available for future command tracking
|
|
1867
|
+
} else if (code === '52') {
|
|
1868
|
+
// OSC 52: Clipboard operations
|
|
1869
|
+
const parts = value.split(';');
|
|
1870
|
+
const targets = parts[0] || 'c';
|
|
1871
|
+
const b64 = parts.slice(1).join(';');
|
|
1872
|
+
if (b64) {
|
|
1873
|
+
// SET clipboard
|
|
1874
|
+
try {
|
|
1875
|
+
const decoded = atob(b64);
|
|
1876
|
+
if (targets.includes('c') || targets.includes('p')) {
|
|
1877
|
+
navigator.clipboard.writeText(decoded).catch(() => {});
|
|
1878
|
+
}
|
|
1879
|
+
} catch (_) {}
|
|
1880
|
+
} else {
|
|
1881
|
+
// GET clipboard — respond with current clipboard contents
|
|
1882
|
+
navigator.clipboard.readText().then(text => {
|
|
1883
|
+
const response = '\x1b]52;c;' + btoa(text) + '\x07';
|
|
1884
|
+
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
|
|
1885
|
+
const enc = new TextEncoder().encode(response);
|
|
1886
|
+
const buf = new Uint8Array(1 + enc.length);
|
|
1887
|
+
buf[0] = 0x00; buf.set(enc, 1);
|
|
1888
|
+
tab.ws.send(buf.buffer);
|
|
1889
|
+
}
|
|
1890
|
+
}).catch(() => {});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
i = oscEnd + (str[oscEnd] === '\x07' ? 1 : 2);
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
out += str[i];
|
|
1899
|
+
i++;
|
|
1900
|
+
}
|
|
1901
|
+
if (out) tab.term.write(out);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1821
1904
|
ws.onmessage = e => {
|
|
1822
1905
|
const buf = new Uint8Array(e.data);
|
|
1823
1906
|
const type = buf[0], payload = buf.slice(1);
|
|
1824
|
-
if (type === 0x00)
|
|
1907
|
+
if (type === 0x00) processTerminalOutput(payload);
|
|
1825
1908
|
else if (type === 0x01) tab.term.writeln('\r\n\x1b[31m[Process exited]\x1b[0m');
|
|
1826
1909
|
else if (type === 0x02) tab.term.writeln('\r\n\x1b[31m' + new TextDecoder().decode(payload) + '\x1b[0m');
|
|
1827
1910
|
};
|
|
@@ -1918,7 +2001,7 @@ function initTerminal(tab) {
|
|
|
1918
2001
|
scrollback: settings.scrollback,
|
|
1919
2002
|
allowTransparency: false,
|
|
1920
2003
|
theme: getXtermTheme(),
|
|
1921
|
-
windowsMode:
|
|
2004
|
+
windowsMode: serverPlatform === 'win32',
|
|
1922
2005
|
convertEol: false,
|
|
1923
2006
|
bellStyle: settings.bell ? 'sound' : 'none',
|
|
1924
2007
|
smoothScrollDuration: 80,
|
|
@@ -1938,6 +2021,19 @@ function initTerminal(tab) {
|
|
|
1938
2021
|
term.loadAddon(searchAddon);
|
|
1939
2022
|
term.loadAddon(webLinksAddon);
|
|
1940
2023
|
|
|
2024
|
+
try {
|
|
2025
|
+
const unicodeAddon = new Unicode11Addon.Unicode11Addon();
|
|
2026
|
+
term.loadAddon(unicodeAddon);
|
|
2027
|
+
term.unicode.activeVersion = '11';
|
|
2028
|
+
} catch (_) {}
|
|
2029
|
+
|
|
2030
|
+
// GPU-accelerated renderer — falls back to canvas if WebGL unavailable
|
|
2031
|
+
try {
|
|
2032
|
+
const webglAddon = new WebglAddon.WebglAddon();
|
|
2033
|
+
webglAddon.onContextLoss(() => { webglAddon.dispose(); });
|
|
2034
|
+
term.loadAddon(webglAddon);
|
|
2035
|
+
} catch (_) {}
|
|
2036
|
+
|
|
1941
2037
|
term.open(tab.wrapper);
|
|
1942
2038
|
|
|
1943
2039
|
tab.term = term;
|
|
@@ -1959,6 +2055,23 @@ function initTerminal(tab) {
|
|
|
1959
2055
|
tab.el.querySelector('.tab-title').textContent = tab.title;
|
|
1960
2056
|
});
|
|
1961
2057
|
|
|
2058
|
+
// Bell: visual flash + desktop notification for background tabs
|
|
2059
|
+
term.onBell(() => {
|
|
2060
|
+
// Visual flash on terminal screen
|
|
2061
|
+
const screen = term.element?.querySelector('.xterm-screen');
|
|
2062
|
+
if (screen) {
|
|
2063
|
+
screen.classList.remove('term-bell-flash');
|
|
2064
|
+
void screen.offsetWidth; // reflow to restart animation
|
|
2065
|
+
screen.classList.add('term-bell-flash');
|
|
2066
|
+
}
|
|
2067
|
+
// Desktop notification if tab is in the background
|
|
2068
|
+
if (document.hidden && settings.bell && Notification.permission === 'granted') {
|
|
2069
|
+
try {
|
|
2070
|
+
new Notification(tab.title || 'WebTun', { body: 'Terminal bell', tag: 'webtun-bell' });
|
|
2071
|
+
} catch (_) {}
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
|
|
1962
2075
|
// Resize observer
|
|
1963
2076
|
tab.resizeObserver = new ResizeObserver(() => {
|
|
1964
2077
|
if (activeTabId === tab.id) { try { fitTerm(tab); } catch(e) { console.warn(e); } }
|
|
@@ -1978,6 +2091,55 @@ function initTerminal(tab) {
|
|
|
1978
2091
|
}
|
|
1979
2092
|
});
|
|
1980
2093
|
|
|
2094
|
+
// Terminal right-click context menu
|
|
2095
|
+
term.element.addEventListener('contextmenu', e => {
|
|
2096
|
+
e.preventDefault();
|
|
2097
|
+
const menu = document.getElementById('term-ctx-menu');
|
|
2098
|
+
const sel = term.getSelection();
|
|
2099
|
+
document.getElementById('term-ctx-copy').style.opacity = sel ? '' : '0.5';
|
|
2100
|
+
menu.style.left = Math.min(e.clientX, window.innerWidth - 170) + 'px';
|
|
2101
|
+
menu.style.top = Math.min(e.clientY, window.innerHeight - 160) + 'px';
|
|
2102
|
+
menu.style.display = 'block';
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
// Touch-to-mouse translation for TUI apps (mobile)
|
|
2106
|
+
let _tapTimeout = null, _tapPos = null, _longPressTimer = null;
|
|
2107
|
+
term.element.addEventListener('touchstart', e => {
|
|
2108
|
+
if (e.touches.length !== 1) return;
|
|
2109
|
+
const touch = e.touches[0];
|
|
2110
|
+
_tapPos = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
|
2111
|
+
// Long press detection for right-click
|
|
2112
|
+
_longPressTimer = setTimeout(() => {
|
|
2113
|
+
if (_tapPos) {
|
|
2114
|
+
const evt = new MouseEvent('mousedown', { clientX: _tapPos.x, clientY: _tapPos.y, button: 2, bubbles: true });
|
|
2115
|
+
term.element.dispatchEvent(evt);
|
|
2116
|
+
const upEvt = new MouseEvent('mouseup', { clientX: _tapPos.x, clientY: _tapPos.y, button: 2, bubbles: true });
|
|
2117
|
+
term.element.dispatchEvent(upEvt);
|
|
2118
|
+
_tapPos = null;
|
|
2119
|
+
}
|
|
2120
|
+
}, 500);
|
|
2121
|
+
}, { passive: true });
|
|
2122
|
+
term.element.addEventListener('touchend', e => {
|
|
2123
|
+
clearTimeout(_longPressTimer);
|
|
2124
|
+
if (!_tapPos || e.changedTouches.length !== 1) return;
|
|
2125
|
+
const touch = e.changedTouches[0];
|
|
2126
|
+
const dx = Math.abs(touch.clientX - _tapPos.x);
|
|
2127
|
+
const dy = Math.abs(touch.clientY - _tapPos.y);
|
|
2128
|
+
const dt = Date.now() - _tapPos.time;
|
|
2129
|
+
if (dx < 10 && dy < 10 && dt < 300) {
|
|
2130
|
+
// Short tap → left click
|
|
2131
|
+
const evt = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY, button: 0, bubbles: true });
|
|
2132
|
+
term.element.dispatchEvent(evt);
|
|
2133
|
+
const upEvt = new MouseEvent('mouseup', { clientX: touch.clientX, clientY: touch.clientY, button: 0, bubbles: true });
|
|
2134
|
+
term.element.dispatchEvent(upEvt);
|
|
2135
|
+
}
|
|
2136
|
+
_tapPos = null;
|
|
2137
|
+
}, { passive: true });
|
|
2138
|
+
term.element.addEventListener('touchmove', () => {
|
|
2139
|
+
clearTimeout(_longPressTimer);
|
|
2140
|
+
_tapPos = null;
|
|
2141
|
+
}, { passive: true });
|
|
2142
|
+
|
|
1981
2143
|
// Fit terminal then connect WebSocket — ensures WS uses correct dimensions
|
|
1982
2144
|
requestAnimationFrame(() => {
|
|
1983
2145
|
fitTerm(tab);
|
|
@@ -2109,6 +2271,10 @@ function sendKey(key) {
|
|
|
2109
2271
|
altLatch = false;
|
|
2110
2272
|
}
|
|
2111
2273
|
updateModifierButtons();
|
|
2274
|
+
} else {
|
|
2275
|
+
shiftLatch = false;
|
|
2276
|
+
altLatch = false;
|
|
2277
|
+
updateModifierButtons();
|
|
2112
2278
|
}
|
|
2113
2279
|
const tab = getActiveTab();
|
|
2114
2280
|
if (tab?.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -2437,27 +2603,74 @@ function formatSize(bytes) {
|
|
|
2437
2603
|
return (bytes / 1073741824).toFixed(1) + ' GB';
|
|
2438
2604
|
}
|
|
2439
2605
|
|
|
2440
|
-
function renderBreadcrumb(
|
|
2606
|
+
function renderBreadcrumb(fullPath) {
|
|
2441
2607
|
const el = document.getElementById('file-breadcrumb');
|
|
2442
2608
|
if (!el) return;
|
|
2443
|
-
const
|
|
2609
|
+
const isWin = /\\/.test(fullPath) || /^[A-Za-z]:/.test(fullPath);
|
|
2610
|
+
const sep = isWin ? '\\' : '/';
|
|
2444
2611
|
let html = '';
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
const
|
|
2449
|
-
|
|
2450
|
-
|
|
2612
|
+
|
|
2613
|
+
if (isWin) {
|
|
2614
|
+
// e.g. C:\Users\name or \\server\share\path
|
|
2615
|
+
const normalized = fullPath.replace(/\//g, '\\');
|
|
2616
|
+
const unc = normalized.startsWith('\\\\');
|
|
2617
|
+
let rest = normalized;
|
|
2618
|
+
let accumulated = '';
|
|
2619
|
+
const segments = [];
|
|
2620
|
+
|
|
2621
|
+
if (unc) {
|
|
2622
|
+
const m = normalized.match(/^\\\\[^\\]+\\[^\\]+/);
|
|
2623
|
+
if (m) {
|
|
2624
|
+
segments.push({ label: m[0], path: m[0] });
|
|
2625
|
+
rest = normalized.slice(m[0].length).replace(/^\\+/, '');
|
|
2626
|
+
accumulated = m[0];
|
|
2627
|
+
}
|
|
2451
2628
|
} else {
|
|
2452
|
-
|
|
2453
|
-
|
|
2629
|
+
const driveMatch = normalized.match(/^([A-Za-z]:)(.*)$/);
|
|
2630
|
+
if (driveMatch) {
|
|
2631
|
+
const root = driveMatch[1] + '\\';
|
|
2632
|
+
segments.push({ label: driveMatch[1], path: root });
|
|
2633
|
+
rest = (driveMatch[2] || '').replace(/^\\+/, '');
|
|
2634
|
+
accumulated = root;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
const parts = rest.split('\\').filter(Boolean);
|
|
2639
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2640
|
+
accumulated = accumulated.endsWith('\\') ? accumulated + parts[i] : accumulated + '\\' + parts[i];
|
|
2641
|
+
segments.push({ label: parts[i], path: accumulated });
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
if (!segments.length) {
|
|
2645
|
+
html = `<span style="color:var(--fg2)">\\</span><span style="color:var(--fg2);font-size:11px;margin-left:6px">(root)</span>`;
|
|
2646
|
+
} else {
|
|
2647
|
+
html = segments.map((seg, i) => {
|
|
2648
|
+
const isLast = i === segments.length - 1;
|
|
2649
|
+
if (isLast) return `<span style="color:var(--fg);font-weight:500">${escHtml(seg.label)}</span>`;
|
|
2650
|
+
return `<a href="#" data-path="${escHtml(seg.path)}" style="color:var(--accent);text-decoration:none;padding:4px 2px;display:inline-block">${escHtml(seg.label)}</a>` +
|
|
2651
|
+
`<span style="color:var(--fg2);margin:0 2px">\\</span>`;
|
|
2652
|
+
}).join('');
|
|
2653
|
+
}
|
|
2654
|
+
} else {
|
|
2655
|
+
const parts = fullPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
2656
|
+
let accumulated = '';
|
|
2657
|
+
for (const part of parts) {
|
|
2658
|
+
accumulated += '/' + part;
|
|
2659
|
+
const isLast = part === parts[parts.length - 1];
|
|
2660
|
+
if (isLast) {
|
|
2661
|
+
html += `<span style="color:var(--fg);font-weight:500">${escHtml(part)}</span>`;
|
|
2662
|
+
} else {
|
|
2663
|
+
html += `<a href="#" data-path="${escHtml(accumulated)}" style="color:var(--accent);text-decoration:none;padding:4px 2px;display:inline-block">${escHtml(part)}</a>`;
|
|
2664
|
+
html += `<span style="color:var(--fg2);margin:0 2px">/</span>`;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (!parts.length) {
|
|
2668
|
+
html = `<span style="color:var(--fg2)">/</span><span style="color:var(--fg2);font-size:11px;margin-left:6px">(root)</span>`;
|
|
2454
2669
|
}
|
|
2455
2670
|
}
|
|
2456
|
-
|
|
2457
|
-
html = `<span style="color:var(--fg2)">/</span><span style="color:var(--fg2);font-size:11px;margin-left:6px">(root)</span>`;
|
|
2458
|
-
}
|
|
2671
|
+
|
|
2459
2672
|
el.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--fg2);margin-right:4px;vertical-align:middle"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>' + html;
|
|
2460
|
-
|
|
2673
|
+
|
|
2461
2674
|
if (!el.dataset.delegated) {
|
|
2462
2675
|
el.addEventListener('click', e => {
|
|
2463
2676
|
const a = e.target.closest('a[data-path]');
|
|
@@ -2472,6 +2685,27 @@ function renderBreadcrumb(path) {
|
|
|
2472
2685
|
// ═══════════════════════════════════════════════════════
|
|
2473
2686
|
let fsClipboard = null;
|
|
2474
2687
|
|
|
2688
|
+
// Terminal context menu actions (registered once)
|
|
2689
|
+
document.getElementById('term-ctx-copy').addEventListener('click', () => {
|
|
2690
|
+
const t = getActiveTab();
|
|
2691
|
+
if (t?.term) { const sel = t.term.getSelection(); if (sel) navigator.clipboard.writeText(sel); }
|
|
2692
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2693
|
+
});
|
|
2694
|
+
document.getElementById('term-ctx-paste').addEventListener('click', () => {
|
|
2695
|
+
pasteToTerminal();
|
|
2696
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2697
|
+
});
|
|
2698
|
+
document.getElementById('term-ctx-select-all').addEventListener('click', () => {
|
|
2699
|
+
const t = getActiveTab();
|
|
2700
|
+
if (t?.term) t.term.selectAll();
|
|
2701
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2702
|
+
});
|
|
2703
|
+
document.getElementById('term-ctx-clear').addEventListener('click', () => {
|
|
2704
|
+
const t = getActiveTab();
|
|
2705
|
+
if (t?.term) t.term.clear();
|
|
2706
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2707
|
+
});
|
|
2708
|
+
|
|
2475
2709
|
function showCtxMenu(e, file) {
|
|
2476
2710
|
e.preventDefault();
|
|
2477
2711
|
ctxTarget = file;
|
|
@@ -2504,7 +2738,11 @@ function showCtxMenu(e, file) {
|
|
|
2504
2738
|
});
|
|
2505
2739
|
}
|
|
2506
2740
|
|
|
2507
|
-
document.addEventListener('click', () =>
|
|
2741
|
+
document.addEventListener('click', () => {
|
|
2742
|
+
document.getElementById('ctx-menu').classList.remove('open');
|
|
2743
|
+
const termMenu = document.getElementById('term-ctx-menu');
|
|
2744
|
+
if (termMenu) termMenu.style.display = 'none';
|
|
2745
|
+
});
|
|
2508
2746
|
|
|
2509
2747
|
document.getElementById('ctx-open').onclick = () => { if (!ctxTarget) return; if (isImageFile(ctxTarget.path)) openImageViewer(ctxTarget.path); else loadFiles(ctxTarget.path); };
|
|
2510
2748
|
document.getElementById('ctx-edit').onclick = () => ctxTarget && openFileEditor(ctxTarget.path);
|
|
@@ -3450,6 +3688,9 @@ function toggleSetting(k) {
|
|
|
3450
3688
|
const ctrlBtn = document.getElementById('ctrl-toggle-btn');
|
|
3451
3689
|
if (ctrlBtn) ctrlBtn.style.background = '';
|
|
3452
3690
|
}
|
|
3691
|
+
if (k === 'bell' && settings.bell && 'Notification' in window && Notification.permission === 'default') {
|
|
3692
|
+
Notification.requestPermission();
|
|
3693
|
+
}
|
|
3453
3694
|
if (k === 'datasaver') {
|
|
3454
3695
|
if (settings.datasaver) {
|
|
3455
3696
|
if (sysStatsTimer) { clearInterval(sysStatsTimer); sysStatsTimer = null; }
|
|
@@ -4103,7 +4344,9 @@ function pasteToTerminal() {
|
|
|
4103
4344
|
if (!text) return;
|
|
4104
4345
|
const tab = getActiveTab();
|
|
4105
4346
|
if (tab?.ws && tab.ws.readyState === WebSocket.OPEN) {
|
|
4106
|
-
|
|
4347
|
+
// Bracketed paste: wrap in escape sequences so the shell buffers the input
|
|
4348
|
+
const bracketed = '\x1b[200~' + text + '\x1b[201~';
|
|
4349
|
+
const enc = new TextEncoder().encode(bracketed);
|
|
4107
4350
|
const buf = new Uint8Array(1 + enc.length);
|
|
4108
4351
|
buf[0] = 0x00; buf.set(enc, 1);
|
|
4109
4352
|
tab.ws.send(buf.buffer);
|
|
@@ -4947,6 +5190,12 @@ function registerSW() {
|
|
|
4947
5190
|
// ═══════════════════════════════════════════════════════
|
|
4948
5191
|
init().catch(e => console.error('Init failed:', e));
|
|
4949
5192
|
|
|
5193
|
+
// Set version in About section
|
|
5194
|
+
fetch('/api/version').then(r => r.json()).then(d => {
|
|
5195
|
+
const el = document.getElementById('about-version');
|
|
5196
|
+
if (el) el.textContent = 'v' + (d.version || '');
|
|
5197
|
+
}).catch(() => {});
|
|
5198
|
+
|
|
4950
5199
|
// Warn before closing if there are unsaved changes or active terminals
|
|
4951
5200
|
window.addEventListener('beforeunload', e => {
|
|
4952
5201
|
const editorOpen = document.getElementById('editor-view').classList.contains('open');
|