Compare commits

..

4 Commits

16 changed files with 641 additions and 86 deletions

81
website/boot.js Normal file
View File

@@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -7,10 +7,13 @@
<meta name="description" content="Interactive terminal interface for Lit.ruv.wtf - A retro-styled command line experience with chat, documentation, and more."> <meta name="description" content="Interactive terminal interface for Lit.ruv.wtf - A retro-styled command line experience with chat, documentation, and more.">
<!-- Favicons --> <!-- Favicons -->
<link rel="icon" type="image/png" sizes="16x16" href="/icons/16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/logos/16px.png">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/logos/32px.png">
<link rel="icon" type="image/png" sizes="48x48" href="/icons/48.png"> <link rel="icon" type="image/png" sizes="48x48" href="/logos/48px.png">
<link rel="apple-touch-icon" sizes="180x180" href="/icons/180.png"> <link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.png">
<link rel="icon" type="image/png" sizes="128x128" href="/logos/128px.png">
<link rel="icon" type="image/png" sizes="256x256" href="/logos/256px.png">
<link rel="apple-touch-icon" sizes="180x180" href="/logos/256px.png">
<!-- Open Graph / Social Media --> <!-- Open Graph / Social Media -->
<meta property="og:type" content="website"> <meta property="og:type" content="website">
@@ -30,6 +33,10 @@
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<div class="page-fade-overlay" id="pageFade"></div>
<div class="boot-screen" id="bootScreen">
<pre id="biosText"></pre>
</div>
<div class="crt-border-overlay"></div> <div class="crt-border-overlay"></div>
<div class="container"> <div class="container">
<div class="terminal-header"> <div class="terminal-header">
@@ -48,13 +55,21 @@
<div id="terminal"></div> <div id="terminal"></div>
<div class="status-bar"> <div class="status-bar">
<span class="status-left">Ready</span> <span class="status-left">Ready</span>
<span class="status-right">Type 'help' for available commands</span> <span class="status-right">
<span class="quick-commands">
<button class="quick-cmd" onclick="runQuickCommand('help')" title="Show all commands">help</button>
<button class="quick-cmd" onclick="runQuickCommand('about')" title="About this terminal">about</button>
<button class="quick-cmd" onclick="runQuickCommand('chat')" title="Join chat room">chat</button>
<button class="quick-cmd" onclick="runQuickCommand('clear')" title="Clear screen">clear</button>
</span>
</span>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
<script src="boot.js"></script>
<script src="terminal.js"></script> <script src="terminal.js"></script>
</body> </body>
</html> </html>

BIN
website/logos/1024px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

BIN
website/logos/128px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
website/logos/16px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
website/logos/256px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
website/logos/32px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
website/logos/48px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
website/logos/512px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
website/logos/64px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -6,13 +6,69 @@
body { body {
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
background: #000; background: #001800;
color: #0f0; color: #0f0;
overflow: hidden; overflow: hidden;
height: 100vh; height: 100vh;
position: relative; 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 { .crt-border-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -115,6 +171,11 @@ body {
cursor: text; cursor: text;
} }
/* Hide xterm cursor completely */
.xterm-cursor-layer {
display: none !important;
}
/* Inline terminal input */ /* Inline terminal input */
.terminal-inline-input { .terminal-inline-input {
position: absolute; position: absolute;
@@ -131,6 +192,22 @@ body {
z-index: 10; z-index: 10;
min-width: 50%; min-width: 50%;
max-width: 80%; 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 { .terminal-inline-input::placeholder {
@@ -155,6 +232,36 @@ body {
opacity: 0.8; 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 */ /* CRT scanlines effect */
.container::before { .container::before {
content: " "; content: " ";
@@ -189,7 +296,8 @@ body {
border-image-width: 40px; border-image-width: 40px;
} }
.container { .container,
.boot-screen {
top: 40px; top: 40px;
left: 40px; left: 40px;
right: 40px; right: 40px;
@@ -219,51 +327,168 @@ body {
padding: 4px 12px; padding: 4px 12px;
font-size: 10px; font-size: 10px;
} }
.quick-commands {
gap: 6px;
}
.quick-cmd {
padding: 2px 6px;
font-size: 10px;
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.crt-border-overlay { .crt-border-overlay {
border-width: 25px; border-width: 15px;
border-image-width: 25px; border-image-width: 15px;
} }
.container { .container,
top: 25px; .boot-screen {
left: 25px; top: 15px;
right: 25px; left: 15px;
bottom: 35px; right: 15px;
bottom: 20px;
border-width: 2px;
}
.boot-screen {
font-size: 10px;
padding: 8px;
}
.terminal-header {
padding: 4px 8px;
} }
.terminal-title { .terminal-title {
letter-spacing: 1px; letter-spacing: 0;
font-size: 10px; font-size: 9px;
} }
.header-left { .header-left {
flex-direction: column; flex-direction: row;
align-items: flex-start; align-items: center;
gap: 8px; gap: 6px;
} }
.header-links { .header-links {
gap: 6px; gap: 4px;
font-size: 9px; font-size: 8px;
} }
.header-link { .header-link {
padding: 2px 4px; padding: 2px 4px;
} }
.header-right {
display: none;
}
.status-bar {
padding: 3px 8px;
font-size: 9px;
}
.status-right { .status-right {
display: none; display: none;
} }
#terminal {
padding: 3px 5px;
}
/* Prevent iOS auto-zoom on input focus */ /* Prevent iOS auto-zoom on input focus */
.terminal-inline-input { .terminal-inline-input {
font-size: 16px; font-size: 16px;
} }
} }
/* Very small screens - minimal border */
@media (max-width: 380px) {
.crt-border-overlay {
border-width: 8px;
border-image-width: 8px;
}
.container,
.boot-screen {
top: 8px;
left: 8px;
right: 8px;
bottom: 12px;
border-width: 1px;
border-radius: 4px;
}
.terminal-header {
padding: 3px 6px;
}
.terminal-title {
font-size: 8px;
}
.header-links {
display: none;
}
.status-bar {
padding: 2px 6px;
}
}
/* Very skinny screens - no border at all */
@media (max-width: 350px) {
.crt-border-overlay {
display: none;
}
.container,
.boot-screen {
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 0;
border: none;
}
.terminal-header {
border-bottom: 1px solid #0f0;
}
.status-bar {
border-top: 1px solid #0f0;
}
}
/* Keyboard open on mobile - detected via JS */
body.keyboard-open .crt-border-overlay {
display: none;
}
body.keyboard-open .container,
body.keyboard-open .boot-screen {
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 0;
border: none;
}
body.keyboard-open .terminal-header {
border-bottom: 1px solid #0f0;
padding: 3px 8px;
}
body.keyboard-open .status-bar {
border-top: 1px solid #0f0;
padding: 3px 8px;
}
/* Scrollbar styling */ /* Scrollbar styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;

View File

@@ -13,14 +13,15 @@ inlineInput.spellcheck = false;
// Initialize xterm.js terminal // Initialize xterm.js terminal
const term = new Terminal({ const term = new Terminal({
cursorBlink: false, cursorBlink: false,
cursorStyle: 'bar', cursorStyle: 'underline',
cursorInactiveStyle: 'none',
fontFamily: '"Courier New", Courier, monospace', fontFamily: '"Courier New", Courier, monospace',
fontSize: 14, fontSize: 14,
theme: { theme: {
background: '#001800', background: '#001800',
foreground: '#00ff00', foreground: '#00ff00',
cursor: 'transparent', cursor: 'transparent',
cursorAccent: '#001800', cursorAccent: 'transparent',
selection: 'rgba(0, 255, 0, 0.3)', selection: 'rgba(0, 255, 0, 0.3)',
black: '#000000', black: '#000000',
red: '#ff0000', red: '#ff0000',
@@ -55,6 +56,55 @@ term.loadAddon(webLinksAddon);
term.open(document.getElementById('terminal')); term.open(document.getElementById('terminal'));
fitAddon.fit(); 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 // Adjust font size based on screen width
function adjustFontSize() { function adjustFontSize() {
const width = window.innerWidth; const width = window.innerWidth;
@@ -75,6 +125,40 @@ window.addEventListener('resize', () => {
adjustFontSize(); adjustFontSize();
}); });
// Mobile keyboard detection using Visual Viewport API
let initialViewportHeight = window.innerHeight;
/**
* Detect if mobile keyboard is open based on viewport height reduction
*/
function checkKeyboardState() {
if (window.visualViewport) {
const viewportHeight = window.visualViewport.height;
const heightReduction = initialViewportHeight - viewportHeight;
// If viewport shrank by more than 150px, keyboard is likely open
if (heightReduction > 150) {
document.body.classList.add('keyboard-open');
} else {
document.body.classList.remove('keyboard-open');
}
}
}
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', () => {
checkKeyboardState();
adjustFontSize();
setTimeout(positionInlineInput, 50);
});
}
// Update initial height on orientation change
window.addEventListener('orientationchange', () => {
setTimeout(() => {
initialViewportHeight = window.innerHeight;
}, 300);
});
// Update system time // Update system time
function updateTime() { function updateTime() {
const now = new Date(); const now = new Date();
@@ -223,6 +307,7 @@ async function enterChatMode() {
term.writeln('─'.repeat(term.cols || 60)); term.writeln('─'.repeat(term.cols || 60));
term.write('\x1b[1;32m>\x1b[0m '); term.write('\x1b[1;32m>\x1b[0m ');
showInlineInput(); showInlineInput();
updateQuickCommands('chat');
} }
// Exit chat mode // Exit chat mode
@@ -237,8 +322,57 @@ function exitChatMode() {
term.writeln(' Exited chat mode.\r\n'); term.writeln(' Exited chat mode.\r\n');
term.write(promptColored); term.write(promptColored);
showInlineInput(); showInlineInput();
updateQuickCommands('terminal');
} }
/**
* Update quick command buttons based on context
* @param {string} mode - 'terminal' or 'chat'
*/
function updateQuickCommands(mode) {
const container = document.querySelector('.quick-commands');
const statusLeft = document.querySelector('.status-left');
if (!container) return;
if (mode === 'chat') {
if (statusLeft) statusLeft.textContent = 'Chat Mode';
container.innerHTML = `
<button class="quick-cmd" onclick="runChatCommand('/help')" title="Show chat help">/help</button>
<button class="quick-cmd" onclick="runChatCommand('/nick')" title="Change nickname">/nick</button>
<button class="quick-cmd" onclick="runChatCommand('/quit')" title="Exit chat">/quit</button>
`;
} else {
if (statusLeft) statusLeft.textContent = 'Ready';
container.innerHTML = `
<button class="quick-cmd" onclick="runQuickCommand('help')" title="Show all commands">help</button>
<button class="quick-cmd" onclick="runQuickCommand('about')" title="About this terminal">about</button>
<button class="quick-cmd" onclick="runQuickCommand('chat')" title="Join chat room">chat</button>
<button class="quick-cmd" onclick="runQuickCommand('clear')" title="Clear screen">clear</button>
`;
}
}
/**
* Run a chat command from button click
* @param {string} command - The chat command to run
*/
function runChatCommand(command) {
if (!chatMode.active) return;
if (command === '/nick') {
// For /nick, just fill in the command prefix
inlineInput.value = '/nick ';
inlineInput.focus();
return;
}
// Execute the command
inlineInput.value = command;
submitInlineInput();
}
window.runChatCommand = runChatCommand;
// Sync messages from Matrix // Sync messages from Matrix
async function syncChatMessages(onlyNew = false) { async function syncChatMessages(onlyNew = false) {
try { try {
@@ -346,6 +480,9 @@ async function sendChatMessage(message) {
chatMode.inputLine = ''; chatMode.inputLine = '';
renderChatPrompt(); renderChatPrompt();
// Reposition input after render
setTimeout(() => positionInlineInput(), 10);
// Immediately sync to show our message // Immediately sync to show our message
setTimeout(() => syncChatMessages(true), 500); setTimeout(() => syncChatMessages(true), 500);
} catch (error) { } catch (error) {
@@ -354,6 +491,7 @@ async function sendChatMessage(message) {
term.writeln(`\x1b[31mError: ${error.message}\x1b[0m`); term.writeln(`\x1b[31mError: ${error.message}\x1b[0m`);
term.writeln('─'.repeat(term.cols || 60)); term.writeln('─'.repeat(term.cols || 60));
term.write(`\x1b[1;32m>\x1b[0m `); term.write(`\x1b[1;32m>\x1b[0m `);
setTimeout(() => positionInlineInput(), 10);
} }
} }
@@ -362,31 +500,30 @@ const commands = {
help: { help: {
description: 'Display available commands', description: 'Display available commands',
execute: () => { execute: () => {
return [ term.writeln('');
'', term.writeln('╔════════════════════════════════════════════════════════════╗');
'╔════════════════════════════════════════════════════════════╗', term.writeln('║ AVAILABLE COMMANDS ║');
'║ AVAILABLE COMMANDS ║', term.writeln('╚════════════════════════════════════════════════════════════╝');
'╚════════════════════════════════════════════════════════════╝', term.writeln('');
'', writeClickable(' [command=help] - Display this help message');
' help - Display this help message', writeClickable(' [command=about] - Information about this terminal');
' about - Information about this terminal', writeClickable(' [command=clear] - Clear the terminal screen');
' clear - Clear the terminal screen', term.writeln(' echo - Echo back your message (usage: echo [message])');
' echo - Echo back your message (usage: echo [message])', writeClickable(' [command=date] - Display current date and time');
' date - Display current date and time', writeClickable(' [command=whoami] - Display current user information');
' whoami - Display current user information', writeClickable(' [command=history] - Show command history');
' history - Show command history', writeClickable(' [command=color] - Change terminal color scheme');
' color - Change terminal color scheme', writeClickable(' [command=banner] - Display welcome banner');
' banner - Display welcome banner', writeClickable(' [command=bluesky] - Fetch recent posts from Bluesky');
' bluesky - Fetch recent posts from Bluesky', writeClickable(' [command=chat] - Enter interactive chat (type /quit to exit)');
' chat - Enter interactive chat (type /quit to exit)', writeClickable(' [command=github] - Visit GitHub repository');
' github - Visit GitHub repository', writeClickable(' [command=contact] - Display contact information');
' contact - Display contact information', writeClickable(' [command=privacy] - Display privacy policy');
' privacy - Display privacy policy', term.writeln('');
'', term.writeln('Navigate: Use ↑/↓ arrows for command history');
'Navigate: Use ↑/↓ arrows for command history', term.writeln('Mouse: Click commands to run them');
'Mouse: Click to position cursor, scroll to view history', term.writeln('');
'' return null;
].join('\r\n');
} }
}, },
about: { about: {
@@ -505,7 +642,22 @@ const commands = {
banner: { banner: {
description: 'Display welcome banner', description: 'Display welcome banner',
execute: () => { execute: () => {
return getWelcomeBanner(); const cols = term.cols;
if (cols >= 78) {
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: { github: {
@@ -842,25 +994,15 @@ const commands = {
} }
}; };
// Welcome banner - full size (62 chars wide) // Welcome banner - full size - NFO style
const welcomeBannerFull = [ const welcomeBannerFull = [
'', '',
'╔════════════════════════════════════════════════════════════╗', ' ██╗ ██╗████████╗██████╗ ██╗ ██╗██╗ ██╗ ██╗ ██╗████████╗███████╗',
'║ ║', ' ██║ ██║╚══██╔══╝██╔══██╗██║ ██║██║ ██║ ██║ ██║╚══██╔══╝██╔════╝',
'║ ██ ██████████╗ ██████╗ ██ ██╗██╗ ██╗ ', ' ████║ ██ ██████╔╝██║ ██║██║ ██║ ██║ █╗ ██ ██ █████ ',
'║ ██║ ██║╚══██╔══╝ ██╔══██╗██║ ██║██║ ██║ ║', ' ████║ ██║ ██╔══██╗██║ ██║╚██╗ ██╔╝ ████╗██║ ██║ ██╔══╝ ',
'██║ ██║ ██║ ██████╔╝██║ ██║██║ ██║ ', ' ███████╗██║ ██║██╗██║ ██║╚██████╔╝ ╚████╔╝██╗╚███╔███╔╝ ██║ ██║ ',
'║ ██║ ██║ ██║ ██╗ ██╔══██╗██║ ██║╚██╗ ██╔╝ ║', ' ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ',
'║ ███████╗██║ ██║ ╚═╝ ██║ ██║╚██████╔╝ ╚████╔╝ ║',
'║ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ║',
'║ ██╗ ██╗████████╗███████╗ ║',
'║ ██║ ██║╚══██╔══╝██╔════╝ ║',
'║ ██║ █╗ ██║ ██║ █████╗ ║',
'║ ██║███╗██║ ██║ ██╔══╝ ║',
'║ ╚███╔███╔╝ ██║ ██║ ║',
'║ ╚══╝╚══╝ ╚═╝ ╚═╝ ║',
'║ ║',
'╚════════════════════════════════════════════════════════════╝',
'', '',
' Type "help" for available commands.', ' Type "help" for available commands.',
' Use ↑/↓ arrows to navigate command history.', ' Use ↑/↓ arrows to navigate command history.',
@@ -870,9 +1012,13 @@ const welcomeBannerFull = [
// Welcome banner - compact (40 chars wide) // Welcome banner - compact (40 chars wide)
const welcomeBannerCompact = [ const welcomeBannerCompact = [
'', '',
'╔══════════════════════════════════════╗', ' ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄',
`║ LIT.RUV.WTF TERMINAL v${VERSION}`, ' ▄█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▄',
'╚══════════════════════════════════════╝', ' ██░ LIT.RUV.WTF TERMINAL ░██',
' ██░ ═══════════════════════ ░██',
' ██░ v' + VERSION + ' · Litruv ░██',
' ▀█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▀',
' ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀',
'', '',
' Welcome! Type "help" for commands.', ' Welcome! Type "help" for commands.',
'' ''
@@ -881,9 +1027,10 @@ const welcomeBannerCompact = [
// Welcome banner - minimal (25 chars wide) // Welcome banner - minimal (25 chars wide)
const welcomeBannerMinimal = [ const welcomeBannerMinimal = [
'', '',
' ═══════════════════════', ' ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄',
' LIT.RUV.WTF TERMINAL', ' █░ LIT.RUV.WTF ░█',
' ═══════════════════════', ' █░ TERMINAL v' + VERSION + ' ░█',
' ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀',
'', '',
' Type "help"', ' Type "help"',
'' ''
@@ -892,7 +1039,7 @@ const welcomeBannerMinimal = [
// Get appropriate banner based on terminal width // Get appropriate banner based on terminal width
function getWelcomeBanner() { function getWelcomeBanner() {
const cols = term.cols; const cols = term.cols;
if (cols >= 62) { if (cols >= 78) {
return welcomeBannerFull; return welcomeBannerFull;
} else if (cols >= 40) { } else if (cols >= 40) {
return welcomeBannerCompact; return welcomeBannerCompact;
@@ -918,11 +1065,11 @@ function positionInlineInput() {
const charWidth = term._core._renderService.dimensions.css.cell.width; const charWidth = term._core._renderService.dimensions.css.cell.width;
const charHeight = term._core._renderService.dimensions.css.cell.height; 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 cursorY = term.buffer.active.cursorY;
const cursorX = term.buffer.active.cursorX; const cursorX = term.buffer.active.cursorX;
// Position input // Position input at cursor (xterm-screen handles positioning)
inlineInput.style.left = (cursorX * charWidth) + 'px'; inlineInput.style.left = (cursorX * charWidth) + 'px';
inlineInput.style.top = (cursorY * charHeight) + 'px'; inlineInput.style.top = (cursorY * charHeight) + 'px';
inlineInput.style.height = charHeight + 'px'; inlineInput.style.height = charHeight + 'px';
@@ -937,7 +1084,7 @@ function showInlineInput() {
const terminalEl = document.getElementById('terminal'); const terminalEl = document.getElementById('terminal');
const xtermEl = terminalEl.querySelector('.xterm-screen'); const xtermEl = terminalEl.querySelector('.xterm-screen');
if (!xtermEl.contains(inlineInput)) { if (xtermEl && !xtermEl.contains(inlineInput)) {
xtermEl.appendChild(inlineInput); xtermEl.appendChild(inlineInput);
} }
@@ -980,7 +1127,7 @@ async function submitInlineInput() {
term.writeln(' /quit - Exit chat mode'); term.writeln(' /quit - Exit chat mode');
term.writeln('─'.repeat(term.cols || 60)); term.writeln('─'.repeat(term.cols || 60));
term.write('\x1b[1;32m>\x1b[0m '); term.write('\x1b[1;32m>\x1b[0m ');
positionInlineInput(); setTimeout(() => positionInlineInput(), 10);
return; return;
} }
@@ -1019,21 +1166,21 @@ async function submitInlineInput() {
} }
term.writeln('─'.repeat(term.cols || 60)); term.writeln('─'.repeat(term.cols || 60));
term.write('\x1b[1;32m>\x1b[0m '); term.write('\x1b[1;32m>\x1b[0m ');
positionInlineInput(); setTimeout(() => positionInlineInput(), 10);
return; return;
} }
if (cmd && !cmd.startsWith('/')) { if (cmd && !cmd.startsWith('/')) {
await sendChatMessage(cmd); await sendChatMessage(cmd);
positionInlineInput(); setTimeout(() => positionInlineInput(), 10);
} else if (cmd.startsWith('/')) { } else if (cmd.startsWith('/')) {
term.writeln(`\x1b[31mUnknown command: ${cmd.split(' ')[0]}. Type /help\x1b[0m`); term.writeln(`\x1b[31mUnknown command: ${cmd.split(' ')[0]}. Type /help\x1b[0m`);
term.writeln('─'.repeat(term.cols || 60)); term.writeln('─'.repeat(term.cols || 60));
term.write('\x1b[1;32m>\x1b[0m '); term.write('\x1b[1;32m>\x1b[0m ');
positionInlineInput(); setTimeout(() => positionInlineInput(), 10);
} else { } else {
term.write('\x1b[1;32m>\x1b[0m '); term.write('\x1b[1;32m>\x1b[0m ');
positionInlineInput(); setTimeout(() => positionInlineInput(), 10);
} }
return; return;
} }
@@ -1048,7 +1195,10 @@ async function submitInlineInput() {
// Show prompt if not in chat mode // Show prompt if not in chat mode
if (!chatMode.active) { if (!chatMode.active) {
term.write(promptColored); term.write(promptColored);
showInlineInput(); // Delay to ensure terminal has rendered before positioning
setTimeout(() => {
positionInlineInput();
}, 10);
} }
term.scrollToBottom(); term.scrollToBottom();
@@ -1056,7 +1206,23 @@ async function submitInlineInput() {
// Initialize terminal // Initialize terminal
function init() { function init() {
term.writeln(getWelcomeBanner()); // Display welcome banner with clickable commands
const cols = term.cols;
if (cols >= 78) {
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); term.write(promptColored);
// Add inline input after a short delay to ensure terminal is rendered // Add inline input after a short delay to ensure terminal is rendered
@@ -1109,6 +1275,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 // Execute command
async function executeCommand(input) { async function executeCommand(input) {
if (!input) return; if (!input) return;
@@ -1130,9 +1360,13 @@ async function executeCommand(input) {
} }
} else { } else {
term.writeln(`\r\n Command not found: ${cmd}`); 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 // Initialize when loaded - wait for boot animation first
init(); if (window.runBootAnimation) {
runBootAnimation().then(init);
} else {
init();
}