working multisession web linux terminal
This commit is contained in:
+29
-85
@@ -8,11 +8,10 @@ const TERM_THEME = {
|
||||
};
|
||||
|
||||
// ── Global state ──────────────────────────────────────────────────────
|
||||
let tabs = [];
|
||||
let activeTab = null;
|
||||
let tabCounter = 0;
|
||||
let isAuthenticated = AUTHED; // updated to true after successful login
|
||||
let saveTimer = null;
|
||||
let tabs = [];
|
||||
let activeTab = null;
|
||||
let tabCounter = 0;
|
||||
let saveTimer = null;
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
function randHexClient(n) {
|
||||
@@ -48,7 +47,7 @@ function saveWorkspace() {
|
||||
}
|
||||
|
||||
async function _doSaveWorkspace() {
|
||||
if (!isAuthenticated || !tabs.length) return;
|
||||
if (!tabs.length) return;
|
||||
const ws = {
|
||||
id: WORKSPACE_ID,
|
||||
tabs: tabs.map(t => ({
|
||||
@@ -138,12 +137,11 @@ document.addEventListener('keydown', function(e) {
|
||||
case 'KeyT': e.preventDefault(); newTab(); return;
|
||||
case 'KeyW': e.preventDefault(); if (activeTab) closeTab(activeTab); return;
|
||||
case 'KeyX': e.preventDefault(); closePane(); return;
|
||||
case 'Backslash': e.preventDefault(); splitPane('h'); return;
|
||||
case 'Minus': e.preventDefault(); splitPane('v'); return;
|
||||
case 'KeyH': e.preventDefault(); splitPane('h'); return; // Alt+H → side-by-side
|
||||
case 'KeyV': e.preventDefault(); splitPane('v'); return; // Alt+V → top/bottom
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault(); sendToActive('\x1bb'); return;
|
||||
case 'ArrowRight':
|
||||
e.preventDefault(); sendToActive('\x1bf'); return;
|
||||
return; // let xterm pass Ctrl+Arrow to shell readline
|
||||
}
|
||||
} else {
|
||||
if (e.code === 'ArrowLeft') {
|
||||
@@ -161,10 +159,9 @@ document.addEventListener('keydown', function(e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Block ALL Ctrl+key browser shortcuts; xterm still gets the event.
|
||||
// Block browser Ctrl+key defaults (find, save, etc.); xterm still gets event.
|
||||
if (C && !A && !M) {
|
||||
e.preventDefault();
|
||||
if (!S && e.code === 'KeyW') sendToActive('\x17');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,68 +231,12 @@ function bgClose(evt, id) {
|
||||
}
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') {
|
||||
['upOverlay', 'dlOverlay'].forEach(id => {
|
||||
['upOverlay', 'dlOverlay', 'infoOverlay'].forEach(id => {
|
||||
if (!document.getElementById(id).classList.contains('hidden')) closeModal(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ── Auth ───────────────────────────────────────────────────────────────
|
||||
async function doAuth() {
|
||||
const username = document.getElementById('fUser').value.trim();
|
||||
const password = document.getElementById('fPass').value;
|
||||
const btn = document.getElementById('authBtn');
|
||||
const errDiv = document.getElementById('authErr');
|
||||
|
||||
if (!username || !password) { showAuthErr('Please enter username and password'); return; }
|
||||
|
||||
btn.disabled = true; btn.classList.add('busy');
|
||||
errDiv.classList.remove('show');
|
||||
|
||||
const form = new URLSearchParams();
|
||||
form.append('username', username);
|
||||
form.append('password', password);
|
||||
|
||||
try {
|
||||
const res = await fetch('/auth', { method: 'POST', body: form,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
isAuthenticated = true;
|
||||
document.getElementById('authOverlay').classList.add('hidden');
|
||||
// Load or create workspace after login
|
||||
const ws = await loadWorkspace();
|
||||
if (ws && ws.tabs && ws.tabs.length) {
|
||||
restoreWorkspace(ws);
|
||||
} else {
|
||||
newTab();
|
||||
}
|
||||
const leaf = activeTab && activeTab.activeLeaf;
|
||||
if (leaf) leaf.term.focus();
|
||||
} else {
|
||||
showAuthErr(data.error || 'Authentication failed');
|
||||
}
|
||||
} catch (_) {
|
||||
showAuthErr('Network error — try again');
|
||||
} finally {
|
||||
btn.disabled = false; btn.classList.remove('busy');
|
||||
}
|
||||
}
|
||||
|
||||
function showAuthErr(msg) {
|
||||
const e = document.getElementById('authErr');
|
||||
e.textContent = msg; e.classList.add('show');
|
||||
document.getElementById('fPass').value = '';
|
||||
document.getElementById('fPass').focus();
|
||||
}
|
||||
|
||||
document.getElementById('fUser').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('fPass').focus();
|
||||
});
|
||||
document.getElementById('fPass').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') doAuth();
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// PANE TREE
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
@@ -394,15 +335,23 @@ function createLeaf(sessionId) {
|
||||
term.attachCustomKeyEventHandler(evt => {
|
||||
if (evt.type !== 'keydown') return true;
|
||||
const C = evt.ctrlKey, S = evt.shiftKey, A = evt.altKey;
|
||||
|
||||
// Copy selection
|
||||
if (C && S && evt.code === 'KeyC') {
|
||||
const sel = term.getSelection();
|
||||
if (sel) navigator.clipboard.writeText(sel).then(
|
||||
() => toast('Copied!', 'ok'), () => toast('Copy failed', 'err'));
|
||||
return false;
|
||||
}
|
||||
// Paste
|
||||
if (C && evt.code === 'KeyV') { pasteToTerminal(); return false; }
|
||||
if (C && !S && !A && evt.code === 'KeyW') return false;
|
||||
if (C && S && evt.code === 'KeyW') return false;
|
||||
|
||||
// Block app-level Alt shortcuts from reaching the PTY
|
||||
if (A && !C) {
|
||||
if (!S && ['KeyT','KeyW','KeyX','KeyH','KeyV'].includes(evt.code)) return false;
|
||||
if ( S && (evt.code === 'ArrowLeft' || evt.code === 'ArrowRight')) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -440,7 +389,7 @@ function splitPane(dir) {
|
||||
applyFlex(splitNode);
|
||||
setupDivider(splitNode);
|
||||
|
||||
if (isAuthenticated) connectLeaf(newLeaf);
|
||||
connectLeaf(newLeaf);
|
||||
|
||||
setActiveLeaf(tab, newLeaf);
|
||||
refreshActiveHighlight(tab);
|
||||
@@ -592,7 +541,7 @@ function newTab(tabId, sessionId) {
|
||||
addTabButton(tab);
|
||||
switchTab(tab);
|
||||
saveWorkspace();
|
||||
if (isAuthenticated) connectLeaf(rootLeaf);
|
||||
connectLeaf(rootLeaf);
|
||||
return tab;
|
||||
}
|
||||
|
||||
@@ -618,9 +567,7 @@ function restoreTab(tabData) {
|
||||
tabs.push(tab);
|
||||
addTabButton(tab);
|
||||
|
||||
if (isAuthenticated) {
|
||||
walkLeaves(root, l => connectLeaf(l));
|
||||
}
|
||||
walkLeaves(root, l => connectLeaf(l));
|
||||
return tab;
|
||||
}
|
||||
|
||||
@@ -762,16 +709,13 @@ function modalFb(id, msg, type) {
|
||||
}
|
||||
|
||||
// ── Bootstrap ──────────────────────────────────────────────────────────
|
||||
// Terminal page is only served to authenticated users (server redirects
|
||||
// unauthenticated requests to /login). Load saved workspace or start fresh.
|
||||
(async function bootstrap() {
|
||||
if (isAuthenticated) {
|
||||
document.getElementById('authOverlay').classList.add('hidden');
|
||||
const ws = await loadWorkspace();
|
||||
if (ws && ws.tabs && ws.tabs.length) {
|
||||
restoreWorkspace(ws);
|
||||
} else {
|
||||
newTab();
|
||||
}
|
||||
const ws = await loadWorkspace();
|
||||
if (ws && ws.tabs && ws.tabs.length) {
|
||||
restoreWorkspace(ws);
|
||||
} else {
|
||||
setTimeout(() => document.getElementById('fUser').focus(), 80);
|
||||
newTab();
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user