// Simulated vim editor for the fake terminal. // The overlay is appended to while active; on exit the terminal's // content is reset and focus returns to it. // // Parameters: // content .terminal-content element (reset on exit) // terminal .terminal-display element (refocused on exit) // mobileInput the hidden ; hidden while vim owns the keyboard // filename argument to `vim ` (may be '' for the splash) // setVimMode callback(active) so the host keeps its own state flag function launchVimSimulator({ content, terminal, mobileInput, filename, setVimMode }) { let vimBuffer = ['Welcome to vim (simulated)!']; let vimLineNum = 0; let vimStatus = '~'; let vimCol = 0; let showSplash = false; setVimMode(true); // Hide the mobile input so it doesn't intercept vim keystrokes mobileInput.style.display = 'none'; // Create a full-screen vim overlay const vimOverlay = document.createElement('div'); vimOverlay.style.position = 'fixed'; vimOverlay.style.inset = '0'; vimOverlay.style.background = '#0a0a0c'; vimOverlay.style.fontFamily = "'SF Mono', 'Fira Code', 'Cascadia Code', monospace"; vimOverlay.style.fontSize = '0.85rem'; vimOverlay.style.lineHeight = '1.5'; vimOverlay.style.color = '#22c55e'; vimOverlay.style.display = 'flex'; vimOverlay.style.flexDirection = 'column'; vimOverlay.style.zIndex = '10000'; vimOverlay.style.padding = '12px 24px'; vimOverlay.style.overflow = 'hidden'; document.body.appendChild(vimOverlay); const lineNums = []; const lineTexts = []; const vimLines = []; const linesContainer = document.createElement('div'); linesContainer.style.flexGrow = '1'; linesContainer.style.minHeight = '0'; linesContainer.style.overflowY = 'auto'; linesContainer.style.display = 'flex'; linesContainer.style.flexDirection = 'column'; linesContainer.style.scrollbarWidth = 'none'; linesContainer.style.msOverflowStyle = 'none'; function createLineElement() { const line = document.createElement('div'); line.style.whiteSpace = 'pre'; line.style.flexShrink = '0'; const numSpan = document.createElement('span'); numSpan.style.color = '#666'; numSpan.style.marginRight = '8px'; numSpan.style.userSelect = 'none'; numSpan.style.display = 'inline-block'; numSpan.style.width = '3ch'; const textSpan = document.createElement('span'); line.appendChild(numSpan); line.appendChild(textSpan); return line; } function createSplashLine(text) { const line = document.createElement('div'); line.style.whiteSpace = 'pre'; line.style.flexShrink = '0'; const textSpan = document.createElement('span'); line.appendChild(textSpan); return { line, textSpan }; } if (!filename) { showSplash = true; const splashText = [ '', ' VIM - Vi sIgnificantly worse M', '', ' version 0.0.0-alpha', ' by disappointed programmers et al.', ' Modified by https://www.reddit.com/r/vim/', ' Vim is open source and you will figure it out', '', ' Please send help.', '', ' type :q to escape this regret', ' type :help or for on-line help', ' type :help version0 for version info', '', ]; for (const text of splashText) { const { line, textSpan } = createSplashLine(text); linesContainer.appendChild(line); lineTexts.push(textSpan); vimLines.push(line); } vimBuffer = splashText; vimLineNum = 0; vimCol = 0; } else { const line = createLineElement(); linesContainer.appendChild(line); lineNums.push(line.children[0]); lineTexts.push(line.children[1]); vimLines.push(line); vimBuffer = ['Welcome to ' + filename + '!']; vimLineNum = 0; vimCol = 0; } vimOverlay.appendChild(linesContainer); // Status bar at the bottom const statusLine = document.createElement('div'); statusLine.style.marginTop = 'auto'; statusLine.style.borderTop = '1px solid #444'; statusLine.style.paddingTop = '4px'; statusLine.style.color = '#ccc'; statusLine.style.flexShrink = '0'; vimOverlay.appendChild(statusLine); // File info at the very bottom const fileInfo = document.createElement('div'); fileInfo.style.color = '#888'; fileInfo.style.flexShrink = '0'; vimOverlay.appendChild(fileInfo); function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function vimRender() { // Ensure we have enough line elements while (vimLines.length < vimBuffer.length) { let newLine; if (showSplash) { const { line, textSpan } = createSplashLine(''); newLine = line; linesContainer.appendChild(line); lineTexts.push(textSpan); } else { newLine = createLineElement(); linesContainer.appendChild(newLine); lineNums.push(newLine.children[0]); lineTexts.push(newLine.children[1]); } vimLines.push(newLine); } // Update all visible lines for (let i = 0; i < vimBuffer.length; i++) { if (showSplash) { lineTexts[i].textContent = vimBuffer[i] || ''; } else { lineNums[i].textContent = (i + 1); const lineText = vimBuffer[i] || ''; if (i === vimLineNum && (vimStatus === '-- INSERT --' || vimStatus === '~')) { const col = vimCol; const before = lineText.slice(0, col); const after = lineText.slice(col); if (after.length > 0) { const cursorChar = after.charAt(0); lineTexts[i].innerHTML = escapeHtml(before) + '' + escapeHtml(cursorChar) + '' + escapeHtml(after.slice(1)); } else { lineTexts[i].innerHTML = escapeHtml(before) + ' '; } } else { lineTexts[i].textContent = lineText; } } vimLines[i].style.display = ''; } // Hide unused line elements for (let i = vimBuffer.length; i < vimLines.length; i++) { vimLines[i].style.display = 'none'; } // Auto-scroll when line is above or below the viewport const lineEl = vimLines[vimLineNum]; if (lineEl && lineEl.style.display !== 'none') { const lineBottom = lineEl.offsetTop + lineEl.offsetHeight; const lineTop = lineEl.offsetTop; if (lineBottom > linesContainer.scrollTop + linesContainer.clientHeight) { linesContainer.scrollTop = lineBottom - linesContainer.clientHeight; } else if (lineTop < linesContainer.scrollTop + 12) { linesContainer.scrollTop = lineTop - 12; } } if (showSplash) { statusLine.textContent = ''; fileInfo.textContent = '"VIM - Vi sIgnificantly worse M"'; } else { statusLine.textContent = vimStatus; fileInfo.textContent = '"' + filename + '"'; } } function vimExit(message) { document.removeEventListener('keydown', vimHandler); setVimMode(false); mobileInput.style.display = ''; vimOverlay.remove(); content.innerHTML = ''; if (message) { const msgLine = document.createElement('div'); msgLine.style.whiteSpace = 'pre-wrap'; msgLine.style.color = '#ccc'; msgLine.textContent = message; content.appendChild(msgLine); } const newLine = document.createElement('div'); newLine.innerHTML = '$ '; content.appendChild(newLine); const newCursor = document.createElement('span'); newCursor.className = 'terminal-cursor'; newLine.appendChild(newCursor); content.scrollTop = content.scrollHeight; terminal.focus(); } const vimHandler = (e) => { e.preventDefault(); if (showSplash) { if (e.key === 'q' && e.shiftKey) { vimExit(); return; } if (e.key === 'Escape' || e.key === 'q') { vimExit(); return; } vimRender(); return; } if (vimStatus.startsWith(':')) { if (e.key === 'Enter') { const cmd = vimStatus.slice(1).trim(); if (cmd === 'q' || cmd === 'q!') { vimExit(); return; } if (cmd === 'wq' || cmd === 'x') { vimExit('File saved (simulated).'); return; } if (cmd === 'w') { vimStatus = 'File written (simulated).'; vimRender(); setTimeout(() => { vimStatus = ':'; vimRender(); }, 1000); return; } if (cmd.startsWith('%s/')) { vimStatus = 'Pattern not found (simulated).'; vimRender(); setTimeout(() => { vimStatus = ':'; vimRender(); }, 1000); return; } if (cmd === 'help') { vimStatus = ':q quit :w save :wq save & quit :q! force quit dd delete line i insert o open line'; vimRender(); setTimeout(() => { vimStatus = ':'; vimRender(); }, 3000); return; } vimStatus = 'Unknown command: ' + cmd; vimRender(); setTimeout(() => { vimStatus = ':'; vimRender(); }, 2000); return; } if (e.key === 'Escape') { vimStatus = '~'; vimRender(); return; } if (e.key === 'Backspace') { vimStatus = vimStatus.slice(0, -1); vimRender(); return; } if (e.key.length === 1) { vimStatus += e.key; vimRender(); } return; } if (vimStatus === '-- INSERT --') { if (e.key === 'Escape') { vimStatus = '~'; vimRender(); return; } if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { vimBuffer[vimLineNum] = vimBuffer[vimLineNum].slice(0, vimCol) + e.key + vimBuffer[vimLineNum].slice(vimCol); vimCol++; vimRender(); return; } if (e.key === 'Enter') { vimBuffer.splice(vimLineNum + 1, 0, ''); vimLineNum++; vimCol = 0; vimRender(); return; } if (e.key === 'Backspace') { if (vimCol > 0) { vimCol--; vimBuffer[vimLineNum] = vimBuffer[vimLineNum].slice(0, vimCol) + vimBuffer[vimLineNum].slice(vimCol + 1); } vimRender(); return; } if (e.key === 'ArrowUp') { if (vimLineNum > 0) { vimLineNum--; vimCol = Math.min(vimCol, (vimBuffer[vimLineNum] || '').length); vimRender(); } return; } if (e.key === 'ArrowDown') { if (vimLineNum < vimBuffer.length - 1) { vimLineNum++; vimCol = Math.min(vimCol, (vimBuffer[vimLineNum] || '').length); vimRender(); } return; } if (e.key === 'ArrowLeft') { if (vimCol > 0) { vimCol--; vimRender(); } return; } if (e.key === 'ArrowRight') { const maxCol = (vimBuffer[vimLineNum] || '').length; if (vimCol < maxCol) { vimCol++; vimRender(); } return; } vimRender(); return; } // Normal mode (~) if (e.key === 'Escape') { vimStatus = '~'; vimRender(); return; } if (e.key === 'i') { vimStatus = '-- INSERT --'; vimRender(); return; } if (e.key === 'I') { vimCol = 0; vimStatus = '-- INSERT --'; vimRender(); return; } if (e.key === 'a') { vimCol = Math.min(vimCol + 1, (vimBuffer[vimLineNum] || '').length); vimStatus = '-- INSERT --'; vimRender(); return; } if (e.key === 'A') { vimCol = (vimBuffer[vimLineNum] || '').length; vimStatus = '-- INSERT --'; vimRender(); return; } if (e.key === 'o') { vimBuffer.splice(vimLineNum + 1, 0, ''); vimLineNum++; vimCol = 0; vimStatus = '-- INSERT --'; vimRender(); return; } if (e.key === 'O') { vimBuffer.splice(vimLineNum, 0, ''); vimLineNum++; vimCol = 0; vimStatus = '-- INSERT --'; vimRender(); return; } if (e.key === 'x') { if (vimBuffer[vimLineNum]) { vimBuffer[vimLineNum] = vimBuffer[vimLineNum].slice(0, -1); } vimRender(); return; } // "dd" — delete the current line (Shift+D, or Alt+D as a fallback). // The buffer always keeps at least one (empty) line so insert // mode can never index a missing line. if ((e.key === 'D' && e.shiftKey) || (e.key === 'd' && e.altKey)) { vimBuffer.splice(vimLineNum, 1); if (vimBuffer.length === 0) vimBuffer.push(''); if (vimLineNum >= vimBuffer.length) vimLineNum = vimBuffer.length - 1; vimCol = Math.min(vimCol, (vimBuffer[vimLineNum] || '').length); vimRender(); return; } if (e.key === 'ArrowUp') { if (vimLineNum > 0) { vimLineNum--; vimCol = Math.min(vimCol, (vimBuffer[vimLineNum] || '').length); vimRender(); } return; } if (e.key === 'ArrowDown') { if (vimLineNum < vimBuffer.length - 1) { vimLineNum++; vimCol = Math.min(vimCol, (vimBuffer[vimLineNum] || '').length); vimRender(); } return; } if (e.key === 'ArrowLeft') { if (vimCol > 0) { vimCol--; vimRender(); } return; } if (e.key === 'ArrowRight') { const maxCol = (vimBuffer[vimLineNum] || '').length; if (vimCol < maxCol) { vimCol++; vimRender(); } return; } if (e.key === 'G') { vimLineNum = vimBuffer.length - 1; vimRender(); return; } if (e.key === 'g') { vimLineNum = 0; vimRender(); return; } if (e.key === ':') { vimStatus = ':'; vimRender(); return; } vimRender(); }; document.addEventListener('keydown', vimHandler); }