From fb86ebbd0dbbf535bcbaea2971c347297a9deabc Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Thu, 5 Mar 2026 19:20:07 +1100 Subject: [PATCH] Implement BIOS boot animation and enhance terminal interactivity with clickable commands --- website/boot.js | 81 +++++++++++++ website/index.html | 14 ++- website/styles.css | 129 +++++++++++++++++++- website/terminal.js | 282 +++++++++++++++++++++++++++++++++++--------- 4 files changed, 443 insertions(+), 63 deletions(-) create mode 100644 website/boot.js diff --git a/website/boot.js b/website/boot.js new file mode 100644 index 0000000..b305998 --- /dev/null +++ b/website/boot.js @@ -0,0 +1,81 @@ +/** + * BIOS Boot Animation + * Displays a retro BIOS-style boot sequence before the terminal loads + */ + +const BIOS_LINES = [ + { text: 'LitRuv BIOS v4.20.69', class: 'white', delay: 300 }, + { text: 'Copyright (C) 2024-2026 LitRuv Industries', class: '', delay: 200 }, + { text: '', delay: 400 }, + { text: 'CPU: Quantum Core i9-42069 @ 6.9GHz', class: '', delay: 300 }, + { text: 'Memory Test: ', class: '', inline: true, delay: 200 }, + { text: '65536K OK', class: 'highlight', delay: 600 }, + { text: '', delay: 300 }, + { text: 'Detecting Primary Master... LitDrive SSD 2TB', class: '', delay: 400 }, + { text: 'Detecting Primary Slave... None', class: '', delay: 300 }, + { text: '', delay: 400 }, + { text: 'Initializing Terminal Interface...', class: 'highlight', delay: 500 }, + { text: 'Loading modules: [', class: '', inline: true, delay: 200 }, + { text: '████████████████████', class: 'highlight', inline: true, delay: 800 }, + { text: '] 100%', class: '', delay: 300 }, + { text: '', delay: 200 }, + { text: 'Starting LIT.RUV.WTF Terminal...', class: 'highlight', delay: 400 } +]; + +/** + * Run the BIOS boot animation + * @returns {Promise} Resolves when animation is complete + */ +function runBootAnimation() { + return new Promise((resolve) => { + const bootScreen = document.getElementById('bootScreen'); + const biosText = document.getElementById('biosText'); + const pageFade = document.getElementById('pageFade'); + + if (!bootScreen || !biosText) { + if (pageFade) pageFade.classList.add('fade-out'); + resolve(); + return; + } + + // Start fade in from black after brief delay + setTimeout(() => { + if (pageFade) pageFade.classList.add('fade-out'); + }, 200); + + let lineIndex = 0; + let totalDelay = 800; // Start after fade begins + const baseDelay = 150; + + BIOS_LINES.forEach((line, index) => { + const delay = totalDelay; + totalDelay += line.delay || baseDelay; + + setTimeout(() => { + const span = document.createElement('span'); + if (line.class) { + span.className = line.class; + } + span.textContent = line.text; + biosText.appendChild(span); + + if (!line.inline) { + biosText.appendChild(document.createTextNode('\n')); + } + }, delay); + }); + + // Fade out and remove boot screen + setTimeout(() => { + bootScreen.classList.add('fade-out'); + setTimeout(() => { + bootScreen.style.display = 'none'; + if (pageFade) pageFade.style.display = 'none'; + resolve(); + }, 500); + }, totalDelay + 300); + }); +} + +// Export for use in terminal.js +window.runBootAnimation = runBootAnimation; diff --git a/website/index.html b/website/index.html index 5c5af6e..5b03be4 100644 --- a/website/index.html +++ b/website/index.html @@ -30,6 +30,10 @@ +
+
+

+    
@@ -48,13 +52,21 @@
Ready - Type 'help' for available commands + + + + + + + +
+ diff --git a/website/styles.css b/website/styles.css index cc03cec..3444e8f 100644 --- a/website/styles.css +++ b/website/styles.css @@ -6,13 +6,69 @@ body { font-family: 'Courier New', Courier, monospace; - background: #000; + background: #001800; color: #0f0; overflow: hidden; height: 100vh; position: relative; } +/* Initial black screen fade-in */ +.page-fade-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #000; + z-index: 20000; + pointer-events: none; + opacity: 1; + transition: opacity 0.8s ease-out; +} + +.page-fade-overlay.fade-out { + opacity: 0; +} + +/* Boot screen */ +.boot-screen { + position: absolute; + top: 80px; + left: 80px; + right: 80px; + bottom: 100px; + background: #001800; + z-index: 100; + padding: 20px; + font-family: 'Courier New', Courier, monospace; + font-size: 14px; + color: #aaa; + overflow: hidden; + transition: opacity 0.5s ease-out; + border: 3px solid #0f0; + border-radius: 8px; +} + +.boot-screen.fade-out { + opacity: 0; + pointer-events: none; +} + +.boot-screen pre { + margin: 0; + white-space: pre-wrap; + line-height: 1.4; +} + +.boot-screen .highlight { + color: #0f0; +} + +.boot-screen .white { + color: #fff; +} + .crt-border-overlay { position: fixed; top: 0; @@ -115,6 +171,11 @@ body { cursor: text; } +/* Hide xterm cursor completely */ +.xterm-cursor-layer { + display: none !important; +} + /* Inline terminal input */ .terminal-inline-input { position: absolute; @@ -131,6 +192,22 @@ body { z-index: 10; min-width: 50%; max-width: 80%; + pointer-events: auto; +} + +/* Clickable commands in terminal */ +.xterm-screen a, +.xterm .xterm-link-layer a { + color: #0f0 !important; + text-decoration: underline !important; + cursor: pointer !important; + text-shadow: 0 0 5px #0f0; +} + +.xterm-screen a:hover, +.xterm .xterm-link-layer a:hover { + color: #0ff !important; + text-shadow: 0 0 8px #0ff !important; } .terminal-inline-input::placeholder { @@ -155,6 +232,36 @@ body { opacity: 0.8; } +/* Quick command buttons */ +.quick-commands { + display: flex; + gap: 8px; + align-items: center; +} + +.quick-cmd { + background: transparent; + color: #0f0; + border: 1px solid #0f0; + border-radius: 3px; + padding: 2px 8px; + font-family: 'Courier New', Courier, monospace; + font-size: 11px; + cursor: pointer; + transition: all 0.2s; + text-shadow: 0 0 3px #0f0; +} + +.quick-cmd:hover { + background: #0f0; + color: #001800; + box-shadow: 0 0 8px #0f0; +} + +.quick-cmd:active { + transform: scale(0.95); +} + /* CRT scanlines effect */ .container::before { content: " "; @@ -189,7 +296,8 @@ body { border-image-width: 40px; } - .container { + .container, + .boot-screen { top: 40px; left: 40px; right: 40px; @@ -219,6 +327,15 @@ body { padding: 4px 12px; font-size: 10px; } + + .quick-commands { + gap: 6px; + } + + .quick-cmd { + padding: 2px 6px; + font-size: 10px; + } } @media (max-width: 480px) { @@ -227,13 +344,19 @@ body { border-image-width: 25px; } - .container { + .container, + .boot-screen { top: 25px; left: 25px; right: 25px; bottom: 35px; } + .boot-screen { + font-size: 11px; + padding: 10px; + } + .terminal-title { letter-spacing: 1px; font-size: 10px; diff --git a/website/terminal.js b/website/terminal.js index 229839c..9013f6c 100644 --- a/website/terminal.js +++ b/website/terminal.js @@ -13,14 +13,15 @@ inlineInput.spellcheck = false; // Initialize xterm.js terminal const term = new Terminal({ cursorBlink: false, - cursorStyle: 'bar', + cursorStyle: 'underline', + cursorInactiveStyle: 'none', fontFamily: '"Courier New", Courier, monospace', fontSize: 14, theme: { background: '#001800', foreground: '#00ff00', cursor: 'transparent', - cursorAccent: '#001800', + cursorAccent: 'transparent', selection: 'rgba(0, 255, 0, 0.3)', black: '#000000', red: '#ff0000', @@ -55,6 +56,55 @@ term.loadAddon(webLinksAddon); term.open(document.getElementById('terminal')); fitAddon.fit(); +// Register custom link provider for clickable commands +term.registerLinkProvider({ + provideLinks: (bufferLineNumber, callback) => { + const line = term.buffer.active.getLine(bufferLineNumber - 1); + if (!line) { + callback(undefined); + return; + } + + const lineText = line.translateToString(); + const links = []; + + // Find all command names that match our known commands + const commandNames = ['help', 'about', 'clear', 'echo', 'date', 'whoami', 'history', 'color', 'banner', 'bluesky', 'chat', 'github', 'contact', 'privacy']; + + commandNames.forEach(cmd => { + let startIndex = 0; + while (true) { + const index = lineText.indexOf(cmd, startIndex); + if (index === -1) break; + + // Check if it's a standalone command word (surrounded by spaces, brackets, or at start/end) + const charBefore = index > 0 ? lineText[index - 1] : ' '; + const charAfter = index + cmd.length < lineText.length ? lineText[index + cmd.length] : ' '; + + const validBefore = /[\s\[\]]/.test(charBefore); + const validAfter = /[\s\[\]\-]/.test(charAfter); + + if (validBefore && validAfter) { + links.push({ + range: { + start: { x: index + 1, y: bufferLineNumber }, + end: { x: index + cmd.length + 1, y: bufferLineNumber } + }, + text: cmd, + activate: () => { + runQuickCommand(cmd); + } + }); + } + + startIndex = index + 1; + } + }); + + callback(links.length > 0 ? links : undefined); + } +}); + // Adjust font size based on screen width function adjustFontSize() { const width = window.innerWidth; @@ -362,31 +412,30 @@ const commands = { help: { description: 'Display available commands', execute: () => { - return [ - '', - '╔════════════════════════════════════════════════════════════╗', - '║ AVAILABLE COMMANDS ║', - '╚════════════════════════════════════════════════════════════╝', - '', - ' help - Display this help message', - ' about - Information about this terminal', - ' clear - Clear the terminal screen', - ' echo - Echo back your message (usage: echo [message])', - ' date - Display current date and time', - ' whoami - Display current user information', - ' history - Show command history', - ' color - Change terminal color scheme', - ' banner - Display welcome banner', - ' bluesky - Fetch recent posts from Bluesky', - ' chat - Enter interactive chat (type /quit to exit)', - ' github - Visit GitHub repository', - ' contact - Display contact information', - ' privacy - Display privacy policy', - '', - 'Navigate: Use ↑/↓ arrows for command history', - 'Mouse: Click to position cursor, scroll to view history', - '' - ].join('\r\n'); + term.writeln(''); + term.writeln('╔════════════════════════════════════════════════════════════╗'); + term.writeln('║ AVAILABLE COMMANDS ║'); + term.writeln('╚════════════════════════════════════════════════════════════╝'); + term.writeln(''); + writeClickable(' [command=help] - Display this help message'); + writeClickable(' [command=about] - Information about this terminal'); + writeClickable(' [command=clear] - Clear the terminal screen'); + term.writeln(' echo - Echo back your message (usage: echo [message])'); + writeClickable(' [command=date] - Display current date and time'); + writeClickable(' [command=whoami] - Display current user information'); + writeClickable(' [command=history] - Show command history'); + writeClickable(' [command=color] - Change terminal color scheme'); + writeClickable(' [command=banner] - Display welcome banner'); + writeClickable(' [command=bluesky] - Fetch recent posts from Bluesky'); + writeClickable(' [command=chat] - Enter interactive chat (type /quit to exit)'); + writeClickable(' [command=github] - Visit GitHub repository'); + writeClickable(' [command=contact] - Display contact information'); + writeClickable(' [command=privacy] - Display privacy policy'); + term.writeln(''); + term.writeln('Navigate: Use ↑/↓ arrows for command history'); + term.writeln('Mouse: Click commands to run them'); + term.writeln(''); + return null; } }, about: { @@ -505,7 +554,22 @@ const commands = { banner: { description: 'Display welcome banner', execute: () => { - return getWelcomeBanner(); + const cols = term.cols; + if (cols >= 62) { + term.writeln(welcomeBannerFull.split('\r\n').slice(0, -3).join('\r\n')); + writeClickable(' Type [command=help] for available commands.'); + term.writeln(' Use ↑/↓ arrows to navigate command history.'); + term.writeln(''); + } else if (cols >= 40) { + term.writeln(welcomeBannerCompact.split('\r\n').slice(0, -2).join('\r\n')); + writeClickable(' Welcome! Type [command=help] for commands.'); + term.writeln(''); + } else { + term.writeln(welcomeBannerMinimal.split('\r\n').slice(0, -2).join('\r\n')); + writeClickable(' Type [command=help]'); + term.writeln(''); + } + return null; } }, github: { @@ -842,25 +906,33 @@ const commands = { } }; -// Welcome banner - full size (62 chars wide) +// Welcome banner - full size (62 chars wide) - NFO style const welcomeBannerFull = [ '', - '╔════════════════════════════════════════════════════════════╗', - '║ ║', - '║ ██╗ ██╗████████╗ ██████╗ ██╗ ██╗██╗ ██╗ ║', - '║ ██║ ██║╚══██╔══╝ ██╔══██╗██║ ██║██║ ██║ ║', - '║ ██║ ██║ ██║ ██████╔╝██║ ██║██║ ██║ ║', - '║ ██║ ██║ ██║ ██╗ ██╔══██╗██║ ██║╚██╗ ██╔╝ ║', - '║ ███████╗██║ ██║ ╚═╝ ██║ ██║╚██████╔╝ ╚████╔╝ ║', - '║ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ║', - '║ ██╗ ██╗████████╗███████╗ ║', - '║ ██║ ██║╚══██╔══╝██╔════╝ ║', - '║ ██║ █╗ ██║ ██║ █████╗ ║', - '║ ██║███╗██║ ██║ ██╔══╝ ║', - '║ ╚███╔███╔╝ ██║ ██║ ║', - '║ ╚══╝╚══╝ ╚═╝ ╚═╝ ║', - '║ ║', - '╚════════════════════════════════════════════════════════════╝', + ' ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄', + ' ▄█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▄', + ' ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██', + ' ██░ ░██', + ' ██░ ██╗ ██╗████████╗ ██████╗ ██╗ ██╗ ░██', + ' ██░ ██║ ██║╚══██╔══╝ ██╔══██╗██║ ██║ ░██', + ' ██░ ██║ ██║ ██║ ██████╔╝██║ ██║ ░██', + ' ██░ ██║ ██║ ██║ ██╔══██╗██║ ██║ ░██', + ' ██░ ███████╗██║ ██║ ██╗██║ ██║╚██████╔╝ ░██', + ' ██░ ╚══════╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ░██', + ' ██░ ───────────────────────────────────────────── ░██', + ' ██░ ██╗ ██╗████████╗███████╗ ░██', + ' ██░ ██║ ██║╚══██╔══╝██╔════╝ ░██', + ' ██░ ██║ █╗ ██║ ██║ █████╗ ░██', + ' ██░ ██║███╗██║ ██║ ██╔══╝ ░██', + ' ██░ ╚███╔███╔╝ ██║ ██║ ░██', + ' ██░ ╚══╝╚══╝ ╚═╝ ╚═╝ ░██', + ' ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██', + ' ▀█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▀', + ' ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀', + '', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓', + ' ░░ TERMINAL v' + VERSION + ' · EST 2024 · LITRUV ░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓', '', ' Type "help" for available commands.', ' Use ↑/↓ arrows to navigate command history.', @@ -870,9 +942,13 @@ const welcomeBannerFull = [ // Welcome banner - compact (40 chars wide) const welcomeBannerCompact = [ '', - '╔══════════════════════════════════════╗', - `║ LIT.RUV.WTF TERMINAL v${VERSION} ║`, - '╚══════════════════════════════════════╝', + ' ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄', + ' ▄█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▄', + ' ██░ LIT.RUV.WTF TERMINAL ░██', + ' ██░ ═══════════════════════ ░██', + ' ██░ v' + VERSION + ' · Est 2024 · LitRuv ░██', + ' ▀█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▀', + ' ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀', '', ' Welcome! Type "help" for commands.', '' @@ -881,9 +957,10 @@ const welcomeBannerCompact = [ // Welcome banner - minimal (25 chars wide) const welcomeBannerMinimal = [ '', - ' ═══════════════════════', - ' LIT.RUV.WTF TERMINAL', - ' ═══════════════════════', + ' ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄', + ' █░ LIT.RUV.WTF ░█', + ' █░ TERMINAL v' + VERSION + ' ░█', + ' ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀', '', ' Type "help"', '' @@ -918,11 +995,11 @@ function positionInlineInput() { const charWidth = term._core._renderService.dimensions.css.cell.width; const charHeight = term._core._renderService.dimensions.css.cell.height; - // Get cursor position (rows from bottom for scrollback) + // Get cursor position const cursorY = term.buffer.active.cursorY; const cursorX = term.buffer.active.cursorX; - // Position input + // Position input at cursor (xterm-screen handles positioning) inlineInput.style.left = (cursorX * charWidth) + 'px'; inlineInput.style.top = (cursorY * charHeight) + 'px'; inlineInput.style.height = charHeight + 'px'; @@ -937,7 +1014,7 @@ function showInlineInput() { const terminalEl = document.getElementById('terminal'); const xtermEl = terminalEl.querySelector('.xterm-screen'); - if (!xtermEl.contains(inlineInput)) { + if (xtermEl && !xtermEl.contains(inlineInput)) { xtermEl.appendChild(inlineInput); } @@ -1048,7 +1125,10 @@ async function submitInlineInput() { // Show prompt if not in chat mode if (!chatMode.active) { term.write(promptColored); - showInlineInput(); + // Delay to ensure terminal has rendered before positioning + setTimeout(() => { + positionInlineInput(); + }, 10); } term.scrollToBottom(); @@ -1056,7 +1136,23 @@ async function submitInlineInput() { // Initialize terminal function init() { - term.writeln(getWelcomeBanner()); + // Display welcome banner with clickable commands + const cols = term.cols; + if (cols >= 62) { + term.writeln(welcomeBannerFull.split('\r\n').slice(0, -3).join('\r\n')); + writeClickable(' Type [command=help] for available commands.'); + term.writeln(' Use ↑/↓ arrows to navigate command history.'); + term.writeln(''); + } else if (cols >= 40) { + term.writeln(welcomeBannerCompact.split('\r\n').slice(0, -2).join('\r\n')); + writeClickable(' Welcome! Type [command=help] for commands.'); + term.writeln(''); + } else { + term.writeln(welcomeBannerMinimal.split('\r\n').slice(0, -2).join('\r\n')); + writeClickable(' Type [command=help]'); + term.writeln(''); + } + term.write(promptColored); // Add inline input after a short delay to ensure terminal is rendered @@ -1109,6 +1205,70 @@ function init() { }); } +/** + * Run a quick command from a button click + * @param {string} command - The command to execute + */ +async function runQuickCommand(command) { + if (!command) return; + + // Write command to terminal + term.writeln(`\x1b[1;32m${promptText}\x1b[0m ${command}`); + + // Execute the command + await executeCommand(command.trim()); + + // Show prompt if not in chat mode + if (!chatMode.active) { + term.write(promptColored); + // Delay to ensure terminal has rendered before positioning + setTimeout(() => { + positionInlineInput(); + }, 10); + } + + term.scrollToBottom(); +} + +// Make function available globally for onclick handlers +window.runQuickCommand = runQuickCommand; + +/** + * Write text to terminal with clickable commands + * Parses [command=X] syntax to create clickable command links + * The link provider makes these clickable automatically + * @param {string} text - Text to write (can include [command=X] syntax) + * @param {boolean} newline - Whether to add newline after text + */ +function writeClickable(text, newline = true) { + // Parse for [command=X] syntax + const regex = /\[command=([^\]]+)\]/g; + let lastIndex = 0; + let match; + + while ((match = regex.exec(text)) !== null) { + // Write text before the match + if (match.index > lastIndex) { + term.write(text.substring(lastIndex, match.index)); + } + + // Write command with underline styling (link provider makes it clickable) + const command = match[1]; + term.write(`\x1b[4;32m${command}\x1b[0m`); + + lastIndex = regex.lastIndex; + } + + // Write remaining text + if (lastIndex < text.length) { + term.write(text.substring(lastIndex)); + } + + if (newline) { + term.write('\r\n'); + } +} + // Execute command async function executeCommand(input) { if (!input) return; @@ -1130,9 +1290,13 @@ async function executeCommand(input) { } } else { term.writeln(`\r\n Command not found: ${cmd}`); - term.writeln(' Type "help" for available commands.\r\n'); + writeClickable(' Type [command=help] for available commands.\r\n'); } } -// Initialize when loaded -init(); +// Initialize when loaded - wait for boot animation first +if (window.runBootAnimation) { + runBootAnimation().then(init); +} else { + init(); +}