UNPKG

shell-mirror

Version:

Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.

1,099 lines (973 loc) 48.1 kB
<!DOCTYPE html> <html> <head> <title>Terminal Mirror</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <style> html, body { margin: 0; padding: 0; height: 100vh; width: 100vw; overflow: hidden; background-color: #000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } #auth-container { display: flex; justify-content: center; align-items: center; height: 100%; } #login-btn { padding: 15px 30px; font-size: 18px; color: white; background-color: #4285F4; border: none; border-radius: 5px; cursor: pointer; } #app-container { display: none; /* Initially hidden */ flex-direction: column; width: 100%; height: 100%; } #terminal-container { flex-grow: 1; width: 100%; overflow: hidden; position: relative; } #connection-status { position: absolute; top: 10px; right: 10px; padding: 5px 10px; border-radius: 15px; font-size: 12px; font-weight: bold; z-index: 1000; } .status-connected { background-color: #4CAF50; color: white; } .status-connecting { background-color: #FF9800; color: white; } .status-disconnected { background-color: #f44336; color: white; } .status-agent-offline { background-color: #ff9800; color: white; } .status-error { background-color: #9C27B0; color: white; } #keyboard-bar { flex-shrink: 0; width: 100%; background-color: #222; display: flex; justify-content: space-around; padding: 8px 0; box-sizing: border-box; } .key-btn { color: white; background-color: #444; border: 1px solid #666; border-radius: 5px; font-size: 16px; padding: 10px 12px; margin: 0 2px; touch-action: manipulation; -webkit-tap-highlight-color: transparent; } </style> </head> <body> <!-- Client-side logging --> <script src="/js/client-logger.js"></script> <div id="auth-container"> <button id="login-btn">Login with Google</button> </div> <div id="app-container"> <div id="terminal-container"> <div id="connection-status" class="status-connecting">Connecting...</div> </div> <div id="keyboard-bar"> <button class="key-btn" data-key-name="ESC">Esc</button> <button class="key-btn" data-key-name="TAB">Tab</button> <button class="key-btn" data-key-name="ARROW_UP"></button> <button class="key-btn" data-key-name="ARROW_DOWN"></button> <button class="key-btn" data-key-name="ARROW_LEFT"></button> <button class="key-btn" data-key-name="ARROW_RIGHT"></button> <button class="key-btn" data-key-name="ENTER">Enter</button> </div> </div> <!-- Dynamically load xterm scripts only after authentication --> <script> const authContainer = document.getElementById('auth-container'); const appContainer = document.getElementById('app-container'); const loginBtn = document.getElementById('login-btn'); loginBtn.addEventListener('click', () => { window.location.href = '/php-backend/api/auth-login.php'; }); // Check authentication status on page load fetch('/php-backend/api/auth-status.php', { credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }) .then(res => { if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } return res.json(); }) .then(data => { console.log('Auth status response:', data); if (data.success && data.data && data.data.authenticated) { console.log('✅ User is authenticated, loading terminal...'); authContainer.style.display = 'none'; appContainer.style.display = 'flex'; console.log('📦 Loading xterm scripts...'); loadXtermScripts(loadTerminal); } else { console.log('❌ User not authenticated, showing login form'); } }) .catch(error => { console.error('Auth status check failed:', error); console.log('Falling back to login form due to auth check failure'); }); function loadXtermScripts(callback) { console.log('🔄 Starting to load xterm CSS...'); const xtermCss = document.createElement('link'); xtermCss.rel = 'stylesheet'; xtermCss.href = 'https://unpkg.com/xterm@5.3.0/css/xterm.css'; document.head.appendChild(xtermCss); console.log('🔄 Starting to load xterm JS...'); const xtermJs = document.createElement('script'); xtermJs.src = 'https://unpkg.com/xterm@5.3.0/lib/xterm.js'; document.head.appendChild(xtermJs); xtermJs.onload = () => { console.log('✅ Xterm JS loaded, loading fit addon...'); const fitAddonJs = document.createElement('script'); fitAddonJs.src = 'https://unpkg.com/@xterm/addon-fit@0.10.0/lib/addon-fit.js'; document.head.appendChild(fitAddonJs); fitAddonJs.onload = () => { console.log('✅ Fit addon loaded, initializing terminal...'); callback(); }; fitAddonJs.onerror = (error) => { console.error('❌ Failed to load fit addon:', error); }; }; xtermJs.onerror = (error) => { console.error('❌ Failed to load xterm JS:', error); }; } function loadTerminal() { console.log('🚀 Initializing terminal...'); const termContainer = document.getElementById('terminal-container'); const connectionStatus = document.getElementById('connection-status'); if (!termContainer) { console.error('❌ Terminal container not found!'); return; } if (!connectionStatus) { console.error('❌ Connection status element not found!'); return; } console.log('📦 Creating Terminal instance...'); const term = new Terminal({ cursorBlink: true, convertEol: true, fontSize: 14, }); const fitAddon = new FitAddon.FitAddon(); term.loadAddon(fitAddon); term.open(termContainer); const keyMap = { ARROW_UP: "\u001b[A", ARROW_DOWN: "\u001b[B", ARROW_RIGHT: "\u001b[C", ARROW_LEFT: "\u001b[D", TAB: "\u0009", ESC: "\u001b", ENTER: "\r" }; let sessionId = 'terminal_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); let currentCommand = ''; let commandHistory = []; let historyIndex = -1; let tabCompletions = []; let tabCompletionIndex = -1; let lastTabPrefix = ''; // Performance enhancement variables let commandCache = new Map(); let directoryCache = new Map(); let currentWorkingDir = '~'; let currentPrompt = '$ '; // Store the actual shell prompt let isExecuting = false; let executionQueue = []; let pendingInput = ''; // Buffer input during execution function updateConnectionStatus(status, message) { connectionStatus.className = `status-${status}`; connectionStatus.textContent = message; } function initializeTerminal() { console.log('🚀 Initializing terminal...'); clientLogger.log('INFO', 'Terminal initialization started', { userAgent: navigator.userAgent, viewport: `${window.innerWidth}x${window.innerHeight}`, timestamp: new Date().toISOString() }); updateConnectionStatus('connecting', 'Checking connection...'); // Load cached state loadCachedState(); // Show welcome message term.write('\x1b[32mTerminal Mirror - Ready\x1b[0m\r\n'); term.write('Checking for Mac connection...\r\n\r\n'); // Check for real Mac agent connection checkMacAgents().then(result => { clientLogger.log('INFO', 'Mac agent check completed', { hasMacAgents: result.hasMacAgents, agentName: result.agentName, error: result.error }); if (result.hasMacAgents) { updateConnectionStatus('connected', `Connected to ${result.agentName}`); term.write(`\x1b[32m✅ Connected to Mac: ${result.agentName}\x1b[0m\r\n`); term.write('Loading terminal environment...\r\n\r\n'); // Get initial prompt from Mac agent executeServerCommand('pwd', true); } else { updateConnectionStatus('disconnected', 'No Mac connected'); term.write('\x1b[31m❌ No Mac connected to your account.\x1b[0m\r\n\r\n'); if (result.error) { term.write(`\x1b[33m⚠️ Debug info: ${result.error}\x1b[0m\r\n\r\n`); clientLogger.log('ERROR', 'Mac agent check failed', { error: result.error }); } term.write('To connect your Mac:\r\n'); term.write('1. Install: npm install -g shell-mirror\r\n'); term.write('2. Run: shell-mirror\r\n'); term.write('3. Follow setup instructions\r\n\r\n'); term.write('🔍 Press Ctrl+Shift+L to view debug logs\r\n\r\n'); } showPrompt(); }).catch(error => { console.error('Connection check failed:', error); updateConnectionStatus('error', 'Connection check failed'); term.write('\x1b[31m❌ Failed to check Mac connection\x1b[0m\r\n\r\n'); showPrompt(); }); setTimeout(() => { fitAndResize(); term.focus(); console.log('✅ Terminal focused and ready'); // DISABLED: Background caching disabled to prevent stale results // startBackgroundCaching(); }, 100); } function loadCachedState() { // Restore working directory const savedCwd = localStorage.getItem('terminal_cwd'); if (savedCwd) { currentWorkingDir = savedCwd; } // Restore command history const savedHistory = localStorage.getItem('terminal_history'); if (savedHistory) { try { commandHistory = JSON.parse(savedHistory); historyIndex = commandHistory.length; } catch (e) { console.warn('Failed to restore command history'); } } // Store user info for instant whoami if (window.userInfo) { localStorage.setItem('terminal_user_info', JSON.stringify(window.userInfo)); } } function startBackgroundCaching() { // Pre-cache current directory listing preCacheDirectoryContents(); // Cache common system info setTimeout(() => { ['whoami', 'uname -a'].forEach(cmd => { if (!commandCache.has(`${currentWorkingDir}:${cmd}`)) { executeServerCommand(cmd, true); } }); }, 500); // DISABLED: Periodic cache refresh disabled // setInterval(() => { // refreshStaleCache(); // }, 60000); // Every minute // Start connection monitoring startConnectionMonitoring(); // Start periodic directory sync startDirectorySync(); } function refreshStaleCache() { const now = Date.now(); const staleThreshold = 60000; // 1 minute (reduced from 5 minutes) // Only refresh directory listings for tab completion for (const [key, entry] of commandCache.entries()) { if (now - entry.timestamp > staleThreshold) { const [dir, command] = key.split(':'); if (dir === currentWorkingDir && command.startsWith('ls')) { executeServerCommand(command, true); } } } } function invalidateDirectoryCache(directory) { // Remove all cache entries for the specified directory const keysToDelete = []; for (const [key, entry] of commandCache.entries()) { const [dir, command] = key.split(':'); if (dir === directory) { keysToDelete.push(key); } } keysToDelete.forEach(key => { commandCache.delete(key); console.log('🗑️ Invalidated cache entry:', key); }); // Also clear directory-specific cache if (directoryCache.has(directory)) { directoryCache.delete(directory); console.log('🗑️ Invalidated directory cache for:', directory); } } let connectionMonitorInterval; let directorySyncInterval; let lastConnectionCheck = Date.now(); let consecutiveFailures = 0; function startConnectionMonitoring() { console.log('🔍 Starting connection monitoring...'); connectionMonitorInterval = setInterval(() => { validateConnection(); }, 30000); // Check every 30 seconds } function validateConnection() { const now = Date.now(); // Check both API and Mac agent availability fetch('/php-backend/api/agents-list.php', { method: 'GET', credentials: 'include', timeout: 5000 }) .then(response => response.json()) .then(data => { if (data.success && data.data.agents && data.data.agents.length > 0) { const onlineAgents = data.data.agents.filter(agent => agent.onlineStatus === 'online'); if (onlineAgents.length > 0) { // Mac agents are online consecutiveFailures = 0; lastConnectionCheck = now; updateConnectionStatus('connected', 'Connected to Mac'); console.log('✅ Mac agent connection verified'); } else { // No online Mac agents updateConnectionStatus('agent-offline', 'Mac Agent Offline'); console.log('📱 Authenticated but no Mac agents online'); } } else { // No Mac agents registered updateConnectionStatus('agent-offline', 'No Mac Agent Found'); console.log('📱 Authenticated but no Mac agents registered'); } }) .catch(error => { console.warn('🚨 Connection check failed:', error); handleConnectionFailure(); }); } function handleConnectionFailure() { consecutiveFailures++; console.log(`🚨 Connection failure #${consecutiveFailures}`); if (consecutiveFailures >= 2) { updateConnectionStatus('disconnected', 'Connection Lost - Attempting Reconnect...'); // Attempt to refresh the page after 3 consecutive failures if (consecutiveFailures >= 3) { console.log('🔄 Multiple connection failures, refreshing page...'); setTimeout(() => { window.location.reload(); }, 5000); } } } function startDirectorySync() { console.log('📍 Starting directory sync monitoring...'); // Sync directory every 2 minutes to catch any mismatches directorySyncInterval = setInterval(() => { executeServerCommand('pwd', true); }, 120000); // Every 2 minutes } function saveState() { // Save command history localStorage.setItem('terminal_history', JSON.stringify(commandHistory.slice(-100))); // Keep last 100 commands // Save working directory localStorage.setItem('terminal_cwd', currentWorkingDir); } function checkMacAgents() { console.log('🔍 Checking for Mac agents...'); return fetch('/php-backend/api/agents-list.php', { credentials: 'include' // Include session cookies }) .then(response => { console.log(` Response status: ${response.status}`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); }) .then(data => { console.log(' Response data:', data); if (data.success && data.data.agents && data.data.agents.length > 0) { console.log(` Found ${data.data.agents.length} agents`); const onlineAgents = data.data.agents.filter(agent => agent.onlineStatus === 'online'); console.log(` ${onlineAgents.length} agents are online`); if (onlineAgents.length > 0) { const agent = onlineAgents[0]; console.log(` Using agent: ${agent.machineName || agent.agentId}`); return { hasMacAgents: true, agentName: agent.machineName || agent.agentId, ownerName: agent.ownerEmail, agentId: agent.agentId }; } } console.log(' No online agents found'); return { hasMacAgents: false, agentName: null, ownerName: null }; }) .catch(error => { console.error('❌ Failed to check Mac agents:', error); return { hasMacAgents: false, agentName: null, ownerName: null, error: error.message }; }); } function showPrompt() { // Show actual shell prompt instead of simplified version if (currentPrompt && currentPrompt.trim() && !currentPrompt.includes('$ ')) { // Use the actual prompt from the shell if it contains directory info term.write(currentPrompt + ' '); } else { // Always show current directory in prompt let shortDir = currentWorkingDir || '~'; // Simplify directory display for very long paths try { if (shortDir.length > 40) { const parts = shortDir.split('/'); if (parts.length > 2) { shortDir = '.../' + parts.slice(-2).join('/'); } } } catch (e) { shortDir = '~'; } term.write('\x1b[36m' + shortDir + ' $\x1b[0m '); } } function executeCommand(command) { const trimmedCommand = command.trim(); if (!trimmedCommand) { term.write('\r\n'); showPrompt(); return; } // Log the exact command being executed console.log('🎯 Executing exact command:', JSON.stringify(trimmedCommand)); // Add to history immediately commandHistory.push(trimmedCommand); historyIndex = commandHistory.length; // Save state for persistence saveState(); // Set executing state to prevent input bleeding isExecuting = true; pendingInput = ''; // Move to new line (preserve the typed command line) term.write('\r\n'); // Check for instant commands first const instantResult = executeInstantCommand(trimmedCommand); if (instantResult !== null) { if (instantResult) { term.write(instantResult); } term.write('\r\n'); isExecuting = false; // Re-enable input showPrompt(); processPendingInput(); return; } // DISABLED: Aggressive caching was causing stale results // Only cache directory listings for tab completion, not command output // Cache invalidation was unreliable when working directory changed // Show execution status without cluttering terminal updateConnectionStatus('connecting', 'Executing...'); // Execute on server executeServerCommand(trimmedCommand, false); } function executeInstantCommand(command) { const cmd = command.trim().toLowerCase(); const now = new Date(); switch (cmd) { case 'pwd': return currentWorkingDir; case 'date': return now.toString(); case 'whoami': // Return cached user info if available const userInfo = localStorage.getItem('terminal_user_info'); if (userInfo) { return JSON.parse(userInfo).username || 'user'; } break; case 'clear': term.clear(); return ''; case 'history': return commandHistory.map((cmd, i) => `${i + 1} ${cmd}`).join('\n'); } return null; // Not an instant command } function executeServerCommand(command, isBackgroundRefresh = false) { const startTime = Date.now(); fetch('/php-backend/api/terminal-execute.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ command: command, session_id: sessionId }) }) .then(response => response.json()) .then(data => { const responseTime = Date.now() - startTime; if (!isBackgroundRefresh) { // Check if command was executed by Mac agent or returned error if (data.success && data.data.mac_agent) { updateConnectionStatus('connected', 'Connected to Mac'); } else if (data.success && !data.data.mac_agent) { updateConnectionStatus('agent-offline', 'Mac Agent Offline'); } else { updateConnectionStatus('error', 'Command Failed'); } } if (data.success) { const output = data.data.output || ''; // DISABLED: All caching disabled to prevent stale results // No caching of any command results // Update working directory if cd command updateWorkingDirectory(command, output); // Update prompt from Mac agent response if (data.data.prompt) { currentPrompt = data.data.prompt; console.log('🎯 Updated prompt from Mac agent:', currentPrompt); } // DISABLED: Pre-caching disabled to prevent stale results // if (command.startsWith('cd ')) { // preCacheDirectoryContents(); // } if (!isBackgroundRefresh) { if (output) { const formattedOutput = output.replace(/\n/g, '\r\n'); term.write(formattedOutput); // Only add newline if output doesn't already end with one if (!output.endsWith('\n')) { term.write('\r\n'); } } isExecuting = false; // Re-enable input showPrompt(); processPendingInput(); } } else { if (!isBackgroundRefresh) { // Provide clearer error messages based on the failure type let errorMessage = data.message || 'Command execution failed'; if (errorMessage.includes('No Mac connected') || errorMessage.includes('No agents available')) { updateConnectionStatus('agent-offline', 'Mac Agent Required'); term.write('\x1b[33m📱 No Mac agent connected. Please start the Mac agent on your computer.\x1b[0m\r\n'); term.write('\x1b[90mRun: shell-mirror start\x1b[0m\r\n'); } else { updateConnectionStatus('error', 'Command Failed'); term.write('\x1b[31m❌ Error: ' + errorMessage + '\x1b[0m\r\n'); } isExecuting = false; // Re-enable input showPrompt(); processPendingInput(); } } }) .catch(error => { if (!isBackgroundRefresh) { updateConnectionStatus('disconnected', 'Connection Lost'); console.error('Command execution failed:', error); term.write('\x1b[31m🚫 Lost connection to server. Checking connectivity...\x1b[0m\r\n'); // Trigger immediate connection validation setTimeout(() => { validateConnection(); }, 1000); isExecuting = false; // Re-enable input showPrompt(); processPendingInput(); } }); } function updateWorkingDirectory(command, output) { if (command.startsWith('cd ')) { // Multiple methods to detect successful directory change let newDir = null; // Method 1: Look for "Changed directory to:" message if (output && output.includes('Changed directory to:')) { const match = output.match(/Changed directory to:\s*(.+)/); if (match) { newDir = match[1].trim(); } } // Method 2: If no error message, assume success for simple cases if (!newDir && output && !output.includes('no such file') && !output.includes('Not a directory') && !output.includes('Permission denied')) { const target = command.substring(3).trim(); if (target === '' || target === '~') { newDir = '~'; } // Don't try to construct relative paths - will verify with pwd } if (newDir) { const oldDir = currentWorkingDir; currentWorkingDir = newDir; localStorage.setItem('terminal_cwd', currentWorkingDir); console.log('🗂️ Updated working directory to:', currentWorkingDir); // Invalidate old directory cache entries invalidateDirectoryCache(oldDir); // Verify with pwd in background to ensure sync setTimeout(() => { executeServerCommand('pwd', true); }, 100); return; } // Check if command failed - do NOT update directory if (output && (output.includes('no such file or directory') || output.includes('Not a directory') || output.includes('Permission denied') || output.includes('cd:'))) { console.log('❌ cd command failed, keeping current directory:', currentWorkingDir); return; // Don't update directory on failure } // Only fallback to manual construction if no output (shouldn't happen with Mac agent) console.warn('⚠️ No output from cd command, using fallback logic'); const target = command.substring(3).trim(); if (target === '' || target === '~') { const oldDir = currentWorkingDir; currentWorkingDir = '~'; localStorage.setItem('terminal_cwd', currentWorkingDir); console.log('🗂️ Updated working directory to home:', currentWorkingDir); invalidateDirectoryCache(oldDir); } // Don't attempt relative path construction without confirmation } else if (command === 'pwd' && output) { const oldDir = currentWorkingDir; const newDir = output.trim(); // Only update if we got a valid directory path if (newDir && newDir.startsWith('/') || newDir === '~') { currentWorkingDir = newDir; localStorage.setItem('terminal_cwd', currentWorkingDir); console.log('🗂️ Updated working directory from pwd:', currentWorkingDir); if (oldDir !== currentWorkingDir) { invalidateDirectoryCache(oldDir); console.log('📍 Directory sync: was', oldDir, 'now', currentWorkingDir); } } } } function preCacheDirectoryContents() { // Pre-cache ls results for current directory setTimeout(() => { ['ls', 'ls -la', 'ls -l'].forEach(lsCmd => { const cacheKey = `${currentWorkingDir}:${lsCmd}`; if (!commandCache.has(cacheKey)) { executeServerCommand(lsCmd, true); } }); }, 200); } function handleSpecialKeys(sequence) { switch (sequence) { case keyMap.ARROW_UP: // History up if (historyIndex > 0) { historyIndex--; replaceCurrentCommand(commandHistory[historyIndex] || ''); resetTabCompletion(); } break; case keyMap.ARROW_DOWN: // History down if (historyIndex < commandHistory.length - 1) { historyIndex++; replaceCurrentCommand(commandHistory[historyIndex] || ''); } else { historyIndex = commandHistory.length; replaceCurrentCommand(''); } resetTabCompletion(); break; case keyMap.ENTER: executeCommand(currentCommand); currentCommand = ''; break; case keyMap.TAB: handleTabCompletion(); break; default: return false; // Not handled } return true; // Handled } function replaceCurrentCommand(newCommand) { // Clear current line term.write('\x1b[2K\r'); // Clear line and return to start showPrompt(); term.write(newCommand); currentCommand = newCommand; console.log('🔄 Command replaced with:', JSON.stringify(newCommand)); } function handleTabCompletion() { // Reset tab completion if command changed const words = currentCommand.split(' '); const currentWord = words[words.length - 1] || ''; if (lastTabPrefix !== currentWord) { tabCompletions = []; tabCompletionIndex = -1; lastTabPrefix = currentWord; } if (tabCompletions.length === 0) { // First tab - get completions from server getTabCompletions(currentWord).then(completions => { if (completions.length === 0) { // No completions available return; } else if (completions.length === 1) { // Single completion - auto-complete immediately completeCurrentWord(completions[0]); } else { // Multiple completions - start cycling tabCompletions = completions; tabCompletionIndex = 0; completeCurrentWord(completions[0]); } }); } else { // Subsequent tabs - cycle through completions tabCompletionIndex = (tabCompletionIndex + 1) % tabCompletions.length; completeCurrentWord(tabCompletions[tabCompletionIndex]); } } function completeCurrentWord(completion) { const words = currentCommand.split(' '); words[words.length - 1] = completion; const newCommand = words.join(' '); // Clear current line and write new command term.write('\x1b[2K\r'); showPrompt(); term.write(newCommand); currentCommand = newCommand; console.log('🔤 Tab completed to:', JSON.stringify(newCommand)); } function getTabCompletions(prefix) { // DISABLED: Cache-based completions disabled // Use only basic command completion for now const cachedCompletions = getCachedCompletions(prefix); if (cachedCompletions.length > 0) { return Promise.resolve(cachedCompletions); } // Fallback to server request return fetch('/php-backend/api/terminal-complete.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ prefix: prefix, command: currentCommand, session_id: sessionId }) }) .then(response => response.json()) .then(data => { if (data.success && data.data && data.data.completions) { return data.data.completions; } return []; }) .catch(error => { console.error('Tab completion failed:', error); return getCachedCompletions(prefix); // Fallback to basic commands only }); } function getCachedCompletions(prefix) { const words = currentCommand.split(' '); const isFirstWord = words.length <= 1; if (isFirstWord) { // Command completion const commands = ['ls', 'cd', 'pwd', 'cat', 'echo', 'grep', 'find', 'ps', 'df', 'du', 'uname', 'date', 'which', 'whoami', 'clear', 'history', 'mkdir', 'rmdir', 'rm', 'cp', 'mv', 'chmod', 'chown', 'head', 'tail', 'less', 'more', 'wc', 'sort', 'uniq']; return commands.filter(cmd => cmd.startsWith(prefix.toLowerCase())); } else { // DISABLED: File completion from cache disabled // Live file completion would require fresh ls call } return []; } function parseLsOutput(lsOutput) { if (!lsOutput) return []; const lines = lsOutput.split('\n'); const files = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('total')) continue; // Parse ls -la format: permissions user group size date filename const parts = trimmed.split(/\s+/); if (parts.length >= 9) { const filename = parts.slice(8).join(' '); if (filename !== '.' && filename !== '..') { files.push(filename); } } } return files; } function resetTabCompletion() { tabCompletions = []; tabCompletionIndex = -1; lastTabPrefix = ''; } function fitAndResize() { appContainer.style.height = `${window.visualViewport.height}px`; fitAddon.fit(); } // Initialize terminal initializeTerminal(); // Terminal input handling term.onData((data) => { try { // If command is executing, buffer the input instead of processing it if (isExecuting) { pendingInput += data; console.log('⏳ Buffering input during execution:', JSON.stringify(data)); return; } // Handle special key sequences if (handleSpecialKeys(data)) { return; } // Handle regular characters if (data === '\x7f') { // Backspace if (currentCommand.length > 0) { currentCommand = currentCommand.slice(0, -1); term.write('\b \b'); resetTabCompletion(); } } else if (data === '\r') { // Enter console.log('📤 Executing command:', JSON.stringify(currentCommand)); executeCommand(currentCommand); clearCurrentCommand(); } else if (data === '\t') { // Tab key handleTabCompletion(); } else if (data.charCodeAt(0) >= 32) { // Printable characters currentCommand += data; term.write(data); console.log('⌨️ Current command buffer:', JSON.stringify(currentCommand)); // Reset tab completion when user types resetTabCompletion(); } } catch (error) { console.error('Terminal input error:', error); term.write('\r\n\x1b[31mInput error occurred\x1b[0m\r\n'); clearCurrentCommand(); showPrompt(); } }); function processPendingInput() { if (pendingInput) { console.log('🔄 Processing pending input:', JSON.stringify(pendingInput)); // Process buffered input character by character for (let i = 0; i < pendingInput.length; i++) { const char = pendingInput[i]; if (char.charCodeAt(0) >= 32) { // Only printable characters currentCommand += char; term.write(char); } } pendingInput = ''; } } function clearCurrentCommand() { currentCommand = ''; console.log('🧹 Command buffer cleared'); } // Virtual keyboard handling document.getElementById('keyboard-bar').addEventListener('click', (e) => { if (e.target.classList.contains('key-btn')) { e.preventDefault(); const keyName = e.target.dataset.keyName; const sequence = keyMap[keyName]; if (sequence) { handleSpecialKeys(sequence); } term.focus(); } }); // Focus terminal on body click document.body.addEventListener('click', (e) => { if (e.target.classList.contains('key-btn')) return; term.focus(); }, true); // Handle window resize window.visualViewport.addEventListener('resize', fitAndResize); term.focus(); // Load version info for footer loadVersionInfo(); } // Load version info async function loadVersionInfo() { try { const response = await fetch('/build-info.json'); const buildInfo = await response.json(); if (buildInfo) { const buildDate = new Date(buildInfo.buildTime).toLocaleDateString(); // Add version info to page title or somewhere visible document.title = `Terminal - Shell Mirror v${buildInfo.version}`; } } catch (error) { console.log('Could not load build info:', error); } } </script> </body> </html>