You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
198 lines
5.5 KiB
JavaScript
198 lines
5.5 KiB
JavaScript
// Main application orchestrator
|
|
(async function () {
|
|
// Check if auth is required
|
|
const authStatus = await fetch('/api/auth/status').then((r) => r.json()).catch(() => ({ authRequired: true }));
|
|
if (authStatus.authRequired) {
|
|
const token = localStorage.getItem('cmux-remote-token');
|
|
if (!token) {
|
|
window.location.href = '/login.html';
|
|
return;
|
|
}
|
|
}
|
|
|
|
// State
|
|
let currentWorkspace = null;
|
|
let currentSurface = null;
|
|
|
|
// Components
|
|
const ws = new WebSocketClient();
|
|
const terminal = new TerminalView(document.getElementById('terminal-output'));
|
|
const statusDot = document.getElementById('connection-status');
|
|
const terminalContainer = document.getElementById('terminal-container');
|
|
const sidebar = new Sidebar((wsRef, surfaceRef) => {
|
|
switchSurface(wsRef, surfaceRef);
|
|
});
|
|
|
|
const gestures = new GestureHandler(sidebar);
|
|
const theme = new ThemeSwitcher();
|
|
|
|
// QR modal
|
|
const qrOverlay = document.getElementById('qr-modal-overlay');
|
|
const qrBtn = document.getElementById('qr-btn');
|
|
const qrCloseBtn = document.getElementById('qr-modal-close');
|
|
const qrUrlEl = document.getElementById('qr-modal-url');
|
|
|
|
function _qrKeyHandler(e) {
|
|
if (e.key === 'Escape') {
|
|
closeQrModal();
|
|
} else if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
qrCloseBtn.focus();
|
|
}
|
|
}
|
|
|
|
async function openQrModal() {
|
|
try {
|
|
const { url } = await fetch('/api/access-url').then(r => r.json());
|
|
qrUrlEl.textContent = url;
|
|
} catch {
|
|
qrUrlEl.textContent = window.location.origin;
|
|
}
|
|
// Reload QR image so it's always fresh
|
|
document.getElementById('qr-modal-image').src = '/api/qr.svg?' + Date.now();
|
|
qrOverlay.hidden = false;
|
|
qrCloseBtn.focus();
|
|
document.addEventListener('keydown', _qrKeyHandler);
|
|
}
|
|
|
|
function closeQrModal() {
|
|
qrOverlay.hidden = true;
|
|
document.removeEventListener('keydown', _qrKeyHandler);
|
|
qrBtn.focus();
|
|
}
|
|
|
|
qrBtn.addEventListener('click', openQrModal);
|
|
qrCloseBtn.addEventListener('click', closeQrModal);
|
|
qrOverlay.addEventListener('click', (e) => {
|
|
if (e.target === qrOverlay) closeQrModal();
|
|
});
|
|
|
|
const keyboard = new VirtualKeyboard(
|
|
(text) => {
|
|
if (currentWorkspace && currentSurface) {
|
|
ws.sendText(currentWorkspace, currentSurface, text);
|
|
}
|
|
},
|
|
(key) => {
|
|
if (currentWorkspace && currentSurface) {
|
|
ws.sendKey(currentWorkspace, currentSurface, key);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Keyboard toggle (desktop)
|
|
const keyboardToggle = document.getElementById('keyboard-toggle');
|
|
const virtualKeyboardEl = document.getElementById('virtual-keyboard');
|
|
|
|
if (window.innerWidth >= 1024) {
|
|
virtualKeyboardEl.classList.add('collapsed');
|
|
keyboardToggle.hidden = false;
|
|
}
|
|
|
|
keyboardToggle.addEventListener('click', () => {
|
|
virtualKeyboardEl.classList.toggle('collapsed');
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (window.innerWidth < 1024) {
|
|
keyboardToggle.hidden = true;
|
|
virtualKeyboardEl.classList.remove('collapsed');
|
|
} else {
|
|
keyboardToggle.hidden = false;
|
|
}
|
|
});
|
|
|
|
// Connection events
|
|
ws.on('connected', () => {
|
|
statusDot.classList.add('connected');
|
|
statusDot.classList.remove('reconnecting');
|
|
statusDot.textContent = 'Connected';
|
|
terminalContainer.classList.remove('stale');
|
|
});
|
|
|
|
ws.on('disconnected', () => {
|
|
statusDot.classList.remove('connected');
|
|
statusDot.classList.add('reconnecting');
|
|
statusDot.textContent = 'Reconnecting...';
|
|
terminalContainer.classList.add('stale');
|
|
});
|
|
|
|
ws.on('reconnecting', ({ attempt }) => {
|
|
statusDot.textContent = `Reconnecting (${attempt})...`;
|
|
});
|
|
|
|
ws.on('auth-ok', () => {
|
|
// Request workspace list after auth
|
|
ws.listWorkspaces();
|
|
});
|
|
|
|
ws.on('workspaces', (msg) => {
|
|
sidebar.setWorkspaces(msg.workspaces);
|
|
|
|
// Auto-subscribe to first terminal surface if none selected
|
|
if (!currentSurface && msg.workspaces.length > 0) {
|
|
const firstWs = msg.workspaces[0];
|
|
for (const pane of firstWs.panes) {
|
|
for (const surface of pane.surfaces) {
|
|
if (surface.type === 'terminal') {
|
|
switchSurface(firstWs.ref, surface.ref);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// Fallback: first surface of any type
|
|
if (firstWs.panes.length > 0 && firstWs.panes[0].surfaces.length > 0) {
|
|
const s = firstWs.panes[0].surfaces[0];
|
|
switchSurface(firstWs.ref, s.ref);
|
|
}
|
|
}
|
|
});
|
|
|
|
ws.on('screen', (msg) => {
|
|
if (msg.surface === currentSurface) {
|
|
terminal.setContent(msg.lines);
|
|
}
|
|
});
|
|
|
|
ws.on('screen-diff', (msg) => {
|
|
if (msg.surface === currentSurface) {
|
|
terminal.applyDiff(msg.patches);
|
|
}
|
|
});
|
|
|
|
function switchSurface(wsRef, surfaceRef) {
|
|
// Unsubscribe from current
|
|
if (currentSurface) {
|
|
ws.unsubscribe(currentSurface);
|
|
}
|
|
|
|
currentWorkspace = wsRef;
|
|
currentSurface = surfaceRef;
|
|
|
|
terminal.clear();
|
|
ws.subscribe(wsRef, surfaceRef);
|
|
sidebar.setActiveSurface(surfaceRef);
|
|
}
|
|
|
|
// Visibility API: pause polling when hidden
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
if (currentSurface) ws.unsubscribe(currentSurface);
|
|
} else {
|
|
if (currentWorkspace && currentSurface) {
|
|
ws.subscribe(currentWorkspace, currentSurface);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Periodic workspace refresh
|
|
setInterval(() => {
|
|
if (ws.isConnected()) {
|
|
ws.listWorkspaces();
|
|
}
|
|
}, 10000);
|
|
|
|
// Start
|
|
ws.connect(authStatus.authRequired);
|
|
})();
|