Split terminal into flat command, achievement, and vim modules
Move canned command data (terminal-commands.js), the achievement system (terminal-achievements.js), and the vim simulator (terminal-vim.js) out of terminal.js, which keeps the core terminal loop. Load order is fixed in index.html with the modules before terminal.js. Also share expand/collapse helpers with the hero click handler and drop the real fetch I/O in favour of simulated curl/wget output.
This commit is contained in:
@@ -696,6 +696,9 @@ XwEAnes79w4eYeMUjIytQWACEvy4QoO7X2MLTKliSqc4Ag8=
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script src="terminal-commands.js" defer></script>
|
||||
<script src="terminal-achievements.js" defer></script>
|
||||
<script src="terminal-vim.js" defer></script>
|
||||
<script src="terminal.js" defer></script>
|
||||
<script src="script.js" defer></script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// Achievement system for the fake terminal: definition, localStorage
|
||||
// persistence, toast notifications, and the hidden-section reveal.
|
||||
|
||||
const ACHIEVEMENTS_STORAGE_KEY = 'reese-terminal-achievements';
|
||||
|
||||
const ACHIEVEMENTS = {
|
||||
time_flies: { name: 'Time Flies', desc: 'Check the system uptime', icon: '⏱️', cmd: 'uptime' },
|
||||
curious: { name: 'Curious', desc: 'List directory contents', icon: '📁', cmd: 'ls', prefix: true },
|
||||
port_scanner: { name: 'Port Scanner', desc: 'Check listening ports', icon: '🔍', cmd: 'ss', prefix: true },
|
||||
password_reset: { name: 'Password Reset Request', desc: 'Submit a SNOW request', icon: '🚪', cmd: 'exit' },
|
||||
power_user: { name: 'Power User', desc: 'Gain root access', icon: '👑', cmd: 'sudo su -' },
|
||||
nice_try: { name: 'Nice Try', desc: 'Attempt rm -rf / as a normal user', icon: '😏', cmd: 'rm -rf /' },
|
||||
restore_backup: { name: 'Restore from Backup', desc: 'Actually destroy the system as root', icon: '💥', cmd: 'rm -rf / root' },
|
||||
where_am_i: { name: 'Where Am I?', desc: 'Print working directory', icon: '📍', cmd: 'pwd' },
|
||||
self_aware: { name: 'Self-Aware', desc: 'Print current user', icon: '🪞', cmd: 'whoami' },
|
||||
identity: { name: 'Identity Crisis', desc: 'Print hostname', icon: '🖥️', cmd: 'hostname' },
|
||||
time_keeper: { name: 'Time Keeper', desc: 'Print current date', icon: '📅', cmd: 'date' },
|
||||
system_explorer: { name: 'System Explorer', desc: 'Print system information', icon: '🔧', cmd: 'uname' },
|
||||
access_granted: { name: 'Access Granted', desc: 'Check user identity', icon: '🆔', cmd: 'id' },
|
||||
os_detective: { name: 'OS Detective', desc: 'Read the OS release file', icon: '🐧', cmd: 'cat /etc/os-release' },
|
||||
ssh_explorer: { name: 'SSH Explorer', desc: 'View SSH public key', icon: '🔑', cmd: 'cat ~/.ssh/id_ed25519.pub' },
|
||||
container_watcher: { name: 'Container Watcher', desc: 'List Docker containers', icon: '🐳', cmd: 'docker ps', prefix: true },
|
||||
docker_confused: { name: 'Docker Confused', desc: 'Use wrong Docker command', icon: '🤔', cmd: 'docker ls', prefix: true },
|
||||
container_pro: { name: 'Container Pro', desc: 'Use correct container command', icon: '✅', cmd: 'docker container ls', prefix: true },
|
||||
podman_fan: { name: 'Podman Fan', desc: 'List Podman containers', icon: '📦', cmd: 'podman ps', prefix: true },
|
||||
podman_confused: { name: 'Podman Confused', desc: 'Use wrong Podman command', icon: '🤔', cmd: 'podman ls', prefix: true },
|
||||
podman_pro: { name: 'Podman Pro', desc: 'Use correct Podman command', icon: '✅', cmd: 'podman container ls', prefix: true },
|
||||
service_hunter: { name: 'Service Hunter', desc: 'List systemd units', icon: '🔎', cmd: 'systemctl list-units', prefix: true },
|
||||
service_filter: { name: 'Service Filter', desc: 'Filter running services', icon: '🎯', cmd: 'systemctl list-units --type=service --state=running --no-pager' },
|
||||
network_ninja: { name: 'Network Ninja', desc: 'Show network interfaces', icon: '🌐', cmd: 'ip addr show', prefix: true },
|
||||
disk_detective: { name: 'Disk Detective', desc: 'Check disk usage', icon: '💾', cmd: 'df -h' },
|
||||
memory_minded: { name: 'Memory Minded', desc: 'Check memory usage', icon: '🧠', cmd: 'free -h' },
|
||||
process_watcher: { name: 'Process Watcher', desc: 'List running processes', icon: '👁️', cmd: 'ps aux', prefix: true },
|
||||
memory_hog: { name: 'Memory Hog', desc: 'Find top memory consumers', icon: '🐗', cmd: 'ps aux --sort=-%mem | head -10' },
|
||||
aesthetic_mode: { name: 'Aesthetic Mode', desc: 'Display system info with style', icon: '✨', cmd: 'neofetch' },
|
||||
help_seeker: { name: 'Help Seeker', desc: 'Look up available commands', icon: '📖', cmd: 'help' },
|
||||
clean_slate: { name: 'Clean Slate', desc: 'Clear the terminal', icon: '🧹', cmd: 'clear' },
|
||||
train_spotter: { name: 'Train Spotter', desc: 'Run the steam locomotive', icon: '🚂', cmd: 'sl' },
|
||||
tain: { name: 'I like trains', desc: 'Choo chooooooo', icon: '🧹', cmd: 'sl' },
|
||||
web_navigator: { name: 'Web Navigator', desc: 'Fetch a webpage using curl or wget', icon: '🌐', cmd: 'curl', prefix: true },
|
||||
package_manager: { name: 'Package Manager', desc: 'Update package lists with apt', icon: '📦', cmd: 'apt update' },
|
||||
software_installer: { name: 'Software Installer', desc: 'Install a package with apt', icon: '🔧', cmd: 'apt install', prefix: true },
|
||||
fedora_updater: { name: 'Fedora Updater', desc: 'Update packages with dnf', icon: '🎯', cmd: 'dnf update' },
|
||||
fedora_installer: { name: 'Fedora Installer', desc: 'Install a package with dnf', icon: '📀', cmd: 'dnf install', prefix: true },
|
||||
vim_splash: { name: 'Vi sIgnificantly worse M', desc: 'Open vim without a filename', icon: '📝', cmd: 'vim' },
|
||||
vim_edit: { name: 'Actually Editing', desc: 'Open vim with a filename', icon: '✏️', cmd: 'vim ', prefix: true },
|
||||
};
|
||||
|
||||
// Load unlocked achievement keys from localStorage
|
||||
function loadAchievements() {
|
||||
try {
|
||||
const saved = localStorage.getItem(ACHIEVEMENTS_STORAGE_KEY);
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Persist unlocked achievement keys to localStorage
|
||||
function saveAchievements(achieved) {
|
||||
localStorage.setItem(ACHIEVEMENTS_STORAGE_KEY, JSON.stringify(achieved));
|
||||
}
|
||||
|
||||
// Check if the given command triggers any new achievements; returns unlocked achievement objects
|
||||
function checkAchievements(cmdText, isRoot) {
|
||||
const achieved = loadAchievements();
|
||||
const newAchievements = [];
|
||||
|
||||
for (const [key, achievement] of Object.entries(ACHIEVEMENTS)) {
|
||||
if (achieved.includes(key)) continue;
|
||||
|
||||
if (key === 'restore_backup' && cmdText === 'rm -rf /' && isRoot) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'power_user' && (cmdText === 'sudo su -' || cmdText === 'sudo -i')) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'password_reset' && cmdText === 'exit' && !isRoot) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'nice_try' && cmdText === 'rm -rf /' && !isRoot) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'system_explorer' && (cmdText === 'uname' || cmdText === 'uname -a')) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'docker_confused' && cmdText === 'docker ls') {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'podman_confused' && cmdText === 'podman ls') {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'web_navigator' && (cmdText.startsWith('curl ') || cmdText.startsWith('wget '))) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (achievement.prefix ? cmdText.startsWith(achievement.cmd) : achievement.cmd === cmdText) {
|
||||
if ((key === 'nice_try' && isRoot) || (key === 'restore_backup' && !isRoot)) {
|
||||
continue;
|
||||
}
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
}
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
saveAchievements(achieved);
|
||||
}
|
||||
|
||||
return newAchievements;
|
||||
}
|
||||
|
||||
// Display a toast notification for a newly unlocked achievement
|
||||
function showToast(achievement) {
|
||||
const container = document.getElementById('toast-container') || createToastContainer();
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast';
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${achievement.icon}</div>
|
||||
<div class="toast-content">
|
||||
<div class="toast-label">Achievement Unlocked</div>
|
||||
<div class="toast-title">${achievement.name}</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('removing');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// Create the toast notification container element if it doesn't exist
|
||||
function createToastContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Reveal the hidden achievements section and add it to the nav if needed
|
||||
function revealAchievements() {
|
||||
const section = document.getElementById('achievements');
|
||||
if (section.hidden) {
|
||||
section.hidden = false;
|
||||
section.classList.add('fade-in');
|
||||
setTimeout(() => section.classList.add('visible'), 50);
|
||||
addAchievementsNav();
|
||||
}
|
||||
renderAchievements();
|
||||
}
|
||||
|
||||
// Render all achievement cards (unlocked and locked) into the achievements grid
|
||||
function renderAchievements() {
|
||||
const achieved = loadAchievements();
|
||||
const total = Object.keys(ACHIEVEMENTS).length;
|
||||
const count = achieved.length;
|
||||
|
||||
const countEl = document.getElementById('achievements-count');
|
||||
countEl.textContent = `${count} / ${total} achievements unlocked`;
|
||||
|
||||
const grid = document.getElementById('achievements-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
for (const [key, achievement] of Object.entries(ACHIEVEMENTS)) {
|
||||
const isUnlocked = achieved.includes(key);
|
||||
const card = document.createElement('div');
|
||||
card.className = `achievement-card ${isUnlocked ? 'unlocked' : 'locked'}`;
|
||||
card.innerHTML = `
|
||||
<div class="achievement-icon">${achievement.icon}</div>
|
||||
<div class="achievement-info">
|
||||
<h3>${isUnlocked ? achievement.name : '???'}</h3>
|
||||
<p>${isUnlocked ? achievement.desc : 'Keep exploring the terminal...'}</p>
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
// Add an "Achievements" link to the main navigation if achievements have been unlocked
|
||||
function addAchievementsNav() {
|
||||
const navLinks = document.getElementById('navLinks');
|
||||
const existing = document.getElementById('nav-achievements');
|
||||
if (!existing && loadAchievements().length > 0) {
|
||||
const li = document.createElement('li');
|
||||
li.id = 'nav-achievements';
|
||||
li.innerHTML = '<a href="#achievements">Achievements</a>';
|
||||
navLinks.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore previously unlocked achievements on page load.
|
||||
// Called by terminal.js once the page has loaded.
|
||||
function initTerminalAchievements() {
|
||||
const achieved = loadAchievements();
|
||||
if (achieved.length > 0) {
|
||||
revealAchievements();
|
||||
addAchievementsNav();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Canned output for the fake terminal (single source of truth) and the
|
||||
// command list used for Tab completion.
|
||||
// Values are plain strings, except functions which are evaluated at dispatch
|
||||
// time (so `date` always shows the current time).
|
||||
|
||||
const TERMINAL_COMMANDS = {
|
||||
'pwd': '/home/reese/portfolio',
|
||||
'whoami': 'reese',
|
||||
'hostname': 'homelab',
|
||||
'date': () => new Date().toString(),
|
||||
'uname': 'Linux homelab 7.0.0 #1 SMP x86_64 GNU/Linux',
|
||||
'uname -a': 'Linux homelab 6.8.0 #1 SMP x86_64 GNU/Linux',
|
||||
'uptime': ' 14:30:00 up 42 days, 3:15, 1 user, load average: 0.42, 0.38, 0.35',
|
||||
'id': 'uid=1000(reese) gid=1000(reese) groups=1000(reese),985(podman)',
|
||||
'cat /etc/os-release': 'PRETTY_NAME="Fedora Linux 69 (Workstation Edition)"\nNAME="Fedora Linux"\nVERSION_ID="69"\nVERSION="69 (Workstation Edition)"\nID=fedora\nVARIANT=Workstation Edition\nVARIANT_ID=workstation\nLOGO=fedora-logo-icon\nCPE_NAME="cpe:/o:fedoralinux:fedora:69"\nDEFAULT_HOSTNAME="fedora"\nHOME_URL="https://fedoraproject.org/"\nDOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f69/system-administrators-guide/"\nSUPPORT_URL="https://ask.fedoraproject.org/"\nBUG_REPORT_URL="https://bugzilla.redhat.com/"\nREDHAT_BUGZILLA_PRODUCT="Fedora"\nREDHAT_BUGZILLA_PRODUCT_VERSION=69\nREDHAT_SUPPORT_PRODUCT="Fedora"\nREDHAT_SUPPORT_PRODUCT_VERSION=69',
|
||||
'cat ~/.ssh/id_ed25519.pub': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpFmKLqRKzMwRe3WkqJvQrN5mHjF2pRn8sT6yUvWxRe reese@homelab',
|
||||
'docker ps': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'docker ls': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'docker container ls': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'podman ps': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'podman ls': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'podman container ls': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'systemctl list-units': 'UNIT LOAD ACTIVE SUB DESCRIPTION\nborg.service loaded active running Borg Backup Service\ngitea.service loaded active running Gitea\ndocker.service loaded active running Docker Application Container Engine\nnextcloud.service loaded active running Nextcloud\nimmich.service loaded active running Immich\nopen-webui.service loaded active running Open WebUI\njellyfin.service loaded active running Jellyfin Media Server\nslopbox.service loaded active running Slopbox\nhomepage.service loaded active running Homepage Container',
|
||||
'systemctl list-units --type=service --state=running --no-pager': 'UNIT LOAD ACTIVE SUB DESCRIPTION\nborg.service loaded active running Borg Backup Service\ngitea.service loaded active running Gitea\ndocker.service loaded active running Docker Application Container Engine\nnextcloud.service loaded active running Nextcloud\nimmich.service loaded active running Immich\nopen-webui.service loaded active running Open WebUI\njellyfin.service loaded active running Jellyfin Media Server\nslopbox.service loaded active running Slopbox\nhomepage.service loaded active running Homepage Container',
|
||||
'ss': 'Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:80 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:443 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:222 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3000 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3001 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3002 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8081 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8096 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*\ntcp LISTEN 0 128 [::]:22 [::]:*',
|
||||
'ip addr show': '1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN\n inet 127.0.0.1/8 scope host lo\n inet6 ::1/128 scope host\n2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP\n inet 192.168.1.42/24 brd 192.168.1.255 scope global dynamic eth0\n inet6 fe80::1/64 scope link',
|
||||
'df -h': 'Filesystem Size Used Avail Use% Mounted on\n/dev/sda2 465G 182G 258G 42% /\nudev 16G 0 16G 0% /dev\ntmpfs 16G 2.1M 16G 1% /dev/shm\n/dev/sda1 511M 6.6M 505M 2% /boot/efi\n/dev/sdb1 1.8T 945G 793G 55% /mnt/data\noverlay 1.8T 945G 793G 55% /var/lib/docker/overlay2',
|
||||
'free -h': ' total used free shared buff/cache available\nMem: 31Gi 8.2Gi 12Gi 512Mi 11Gi 22Gi\nSwap: 2.0Gi 0B 2.0Gi',
|
||||
'ps aux': 'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nreese 1242 2.1 12.4 4285632 3932160 ? Sl May15 42:18 /opt/llama.cpp/build/bin/llama-server --model /models/Qwen3-30B-A3B.Q4_K_M.gguf --port 8082\ndocker 2341 1.2 6.8 8562348 2156032 ? Sl May15 28:45 /usr/bin/dockerd -H fd://\nreese 3456 0.8 3.2 2845632 1015808 ? Sl May15 18:22 /opt/open-webui/server\nreese 4567 0.5 2.1 1562348 665600 ? Sl May15 12:34 /usr/bin/python3 /opt/borg/borgmatic\nroot 5678 0.3 1.4 945632 448000 ? Ssl May15 8:12 /usr/bin/docker-proxy -p tcp:0.0.0.0:80:80\nreese 6789 0.2 1.1 745632 348000 ? Ssl May15 5:45 /usr/bin/podman run --name homepage\nroot 7890 0.1 0.8 545632 256000 ? Ssl May15 3:22 /usr/bin/docker-proxy -p tcp:0.0.0.0:443:443',
|
||||
'ps aux --sort=-%mem | head -10': 'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nreese 1242 2.1 12.4 4285632 3932160 ? Sl May15 42:18 /opt/llama.cpp/build/bin/llama-server --model /models/Qwen3-30B-A3B.Q4_K_M.gguf --port 8082\ndocker 2341 1.2 6.8 8562348 2156032 ? Sl May15 28:45 /usr/bin/dockerd -H fd://\nreese 3456 0.8 3.2 2845632 1015808 ? Sl May15 18:22 /opt/open-webui/server\nreese 4567 0.5 2.1 1562348 665600 ? Sl May15 12:34 /usr/bin/python3 /opt/borg/borgmatic\nroot 5678 0.3 1.4 945632 448000 ? Ssl May15 8:12 /usr/bin/docker-proxy -p tcp:0.0.0.0:80:80\nreese 6789 0.2 1.1 745632 348000 ? Ssl May15 5:45 /usr/bin/podman run --name homepage\nroot 7890 0.1 0.8 545632 256000 ? Ssl May15 3:22 /usr/bin/docker-proxy -p tcp:0.0.0.0:443:443',
|
||||
'neofetch': ` 'c. reese@homelab\n ,xNMM. ----------------------\n .OMMMMo OS: Fedora Linux 69 (Workstation Edition) x86_64\n OMMM0, Host: custom-build\n .;loddo:' loolloddol;. Kernel: 6.8.0\n cKMMMMMMMMMMNWMMMMMMMMMM0: Uptime: 42 days, 3 hours\n .KMMMMMMMMMMMMMMMMMMMMMMMWd. Packages: 2847 (dnf)\n XMMMMMMMMMMMMMMMMMMMMMMMX. Shell: bash 5.2.26\n;MMMMMMMMMMMMMMMMMMMMMMMM: Resolution: 2560x1440\n:MMMMMMMMMMMMMMMMMMMMMMMM: DE: GNOME 46.1\n.MMMMMMMMMMMMMMMMMMMMMMMMX. WM: Mutter\n kMMMMMMMMMMMMMMMMMMMMMMMMWd. Terminal: /dev/pts/0\n .XMMMMMMMMMMMMMMMMMMMMMMMMMMk CPU: AMD Ryzen 9 7900X (24) @ 5.6GHz\n .XMMMMMMMMMMMMMMMMMMMMMMK. GPU: NVIDIA GeForce RTX 4090\n kMMMMMMMMMMMMMMMMMMMMd GPU: AMD Ryzen Built-in\n ;KMMMMMMMWXXWMMMMMMMk. Memory: 8.2Gi / 31Gi\n .cooc,. .,coo:.\n\n<span style="color: #22c55e">███</span><span style="color: #22c55e">███</span><span style="color: #22c55e">███</span><span style="color: #eab308">███</span><span style="color: #eab308">███</span><span style="color: #eab308">███</span><span style="color: #3b82f6">███</span><span style="color: #3b82f6">███</span><span style="color: #3b82f6">███</span><span style="color: #8b5cf6">███</span><span style="color: #8b5cf6">███</span><span style="color: #8b5cf6">███</span><span style="color: #ec4899">███</span><span style="color: #ec4899">███</span><span style="color: #ec4899">███</span>`,
|
||||
'sl': '____\n|DD|____T_\n|_ |_____|<\n @-@-@-oo\\',
|
||||
'help': 'Available commands:\n ls List directory contents\n pwd Print working directory\n whoami Print current user\n hostname Print hostname\n date Print current date/time\n uname Print system information\n uptime Print system uptime\n id Print user identity\n cat Print file contents\n docker List running containers\n podman List running pods\n systemctl List running systemd services\n ss Show listening ports\n ip Show network interfaces\n df Show disk usage\n free Show memory usage\n ps Show running processes\n apt Package manager (update, install)\n curl Fetch a webpage\n wget Fetch a webpage\n vim Terminal text editor (try: vim or vim filename)\n neofetch System info display\n sl Steam locomotive\n clear Clear the terminal\n exit Exit to login screen\n sudo Gain root access\n rm Remove files\n echo Print text\n help Show this help message',
|
||||
};
|
||||
|
||||
const TERMINAL_COMMAND_LIST = [
|
||||
'ls', 'ss', 'clear', 'exit', 'rm -rf /', 'sudo su -', 'sudo -i',
|
||||
'pwd', 'whoami', 'hostname', 'date', 'uname', 'uname -a', 'uptime',
|
||||
'id', 'cat /etc/os-release', 'cat ~/.ssh/id_ed25519.pub',
|
||||
'docker ps', 'docker ls', 'docker container ls',
|
||||
'podman ps', 'podman ls', 'podman container ls',
|
||||
'systemctl list-units', 'systemctl list-units --type=service --state=running --no-pager',
|
||||
'ip addr show', 'df -h', 'free -h',
|
||||
'ps aux', 'ps aux --sort=-%mem | head -10',
|
||||
'neofetch', 'sl', 'help', 'echo', 'curl', 'wget', 'apt', 'dnf', 'vim'
|
||||
];
|
||||
|
||||
// Exact-match command lookup.
|
||||
function getCommandOutput(cmdText) {
|
||||
const value = TERMINAL_COMMANDS[cmdText];
|
||||
if (value === undefined) return undefined;
|
||||
return typeof value === 'function' ? value() : value;
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
// Simulated vim editor for the fake terminal.
|
||||
// The overlay is appended to <body> 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 <input>; hidden while vim owns the keyboard
|
||||
// filename argument to `vim <file>` (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<Enter> to escape this regret',
|
||||
' type :help<Enter> or <F1> for on-line help',
|
||||
' type :help version0<Enter> 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) + '<span class="vim-block-cursor">' + escapeHtml(cursorChar) + '</span>' + escapeHtml(after.slice(1));
|
||||
} else {
|
||||
lineTexts[i].innerHTML = escapeHtml(before) + '<span class="vim-block-cursor"> </span>';
|
||||
}
|
||||
} 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 = '<span class="terminal-prompt">$</span> ';
|
||||
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);
|
||||
}
|
||||
+55
-754
@@ -1,6 +1,33 @@
|
||||
// Terminal reference - set when terminal is created in the server rack
|
||||
let activeTerminal = null;
|
||||
|
||||
// Expand/collapse the terminal (and its rack unit / hero shift). Shared by
|
||||
// the terminal's own focus/blur handlers and the hero click handler in
|
||||
// script.js.
|
||||
function expandTerminal(terminalEl) {
|
||||
const mobileInput = terminalEl._mobileInput;
|
||||
if (mobileInput) mobileInput.focus();
|
||||
terminalEl.style.boxShadow = '0 0 8px rgba(234, 179, 8, 0.3), inset 0 0 20px rgba(34, 197, 94, 0.1)';
|
||||
terminalEl.style.borderColor = '#eab308';
|
||||
terminalEl.classList.add('grown');
|
||||
terminalEl.classList.add('opaque');
|
||||
terminalEl.closest('.server-face')?.classList.add('grown');
|
||||
terminalEl.closest('.rack-unit')?.classList.add('grown');
|
||||
document.getElementById('hero')?.classList.add('shifted');
|
||||
document.querySelector('.rack-container')?.classList.add('opaque');
|
||||
}
|
||||
|
||||
function collapseTerminal(terminalEl) {
|
||||
terminalEl.style.boxShadow = '';
|
||||
terminalEl.style.borderColor = '#2a2a2e';
|
||||
terminalEl.classList.remove('grown');
|
||||
terminalEl.classList.remove('opaque');
|
||||
terminalEl.closest('.server-face')?.classList.remove('grown');
|
||||
terminalEl.closest('.rack-unit')?.classList.remove('grown');
|
||||
document.getElementById('hero')?.classList.remove('shifted');
|
||||
document.querySelector('.rack-container')?.classList.remove('opaque');
|
||||
}
|
||||
|
||||
// Create the interactive terminal in the first rack unit
|
||||
function createTerminal(face) {
|
||||
// Terminal display
|
||||
@@ -19,6 +46,7 @@ function createTerminal(face) {
|
||||
mobileInput.style.outline = 'none';
|
||||
mobileInput.style.background = 'transparent';
|
||||
mobileInput.setAttribute('autocorrect', 'off');
|
||||
mobileInput.setAttribute('aria-label', 'Terminal command input');
|
||||
mobileInput.setAttribute('spellcheck', 'false');
|
||||
mobileInput.setAttribute('autocomplete', 'off');
|
||||
mobileInput.setAttribute('autocapitalize', 'off');
|
||||
@@ -57,24 +85,12 @@ function createTerminal(face) {
|
||||
const commandHistory = [];
|
||||
let historyIndex = -1;
|
||||
let isRoot = false;
|
||||
let isLoginScreen = false;
|
||||
let tabCycleIndex = -1;
|
||||
let tabMatches = [];
|
||||
let tabBaseInput = '';
|
||||
let isTabCompleting = false;
|
||||
let inVimMode = false;
|
||||
const vimState = { active: false };
|
||||
|
||||
const allCommands = [
|
||||
'ls', 'ss', 'clear', 'exit', 'rm -rf /', 'sudo su -', 'sudo -i',
|
||||
'pwd', 'whoami', 'hostname', 'date', 'uname', 'uname -a', 'uptime',
|
||||
'id', 'cat /etc/os-release', 'cat ~/.ssh/id_ed25519.pub',
|
||||
'docker ps', 'docker ls', 'docker container ls',
|
||||
'podman ps', 'podman ls', 'podman container ls',
|
||||
'systemctl list-units', 'systemctl list-units --type=service --state=running --no-pager',
|
||||
'ip addr show', 'df -h', 'free -h',
|
||||
'ps aux', 'ps aux --sort=-%mem | head -10',
|
||||
'neofetch', 'sl', 'help', 'echo', 'curl', 'wget', 'apt', 'dnf', 'vim'
|
||||
];
|
||||
|
||||
function updateDisplay(text) {
|
||||
const lastLine = content.lastElementChild;
|
||||
@@ -93,39 +109,9 @@ function createTerminal(face) {
|
||||
lastLine.appendChild(newCursor);
|
||||
}
|
||||
|
||||
const commands = {
|
||||
'pwd': '/home/reese/portfolio',
|
||||
'whoami': 'reese',
|
||||
'hostname': 'homelab',
|
||||
'date': new Date().toString(),
|
||||
'uname': 'Linux homelab 7.0.0 #1 SMP x86_64 GNU/Linux',
|
||||
'uname -a': 'Linux homelab 6.8.0 #1 SMP x86_64 GNU/Linux',
|
||||
'uptime': ' 14:30:00 up 42 days, 3:15, 1 user, load average: 0.42, 0.38, 0.35',
|
||||
'id': 'uid=1000(reese) gid=1000(reese) groups=1000(reese),985(podman)',
|
||||
'cat /etc/os-release': 'PRETTY_NAME="Fedora Linux 69 (Workstation Edition)"\nNAME="Fedora Linux"\nVERSION_ID="69"\nVERSION="69 (Workstation Edition)"\nID=fedora\nVARIANT=Workstation Edition\nVARIANT_ID=workstation\nLOGO=fedora-logo-icon\nCPE_NAME="cpe:/o:fedoralinux:fedora:69"\nDEFAULT_HOSTNAME="fedora"\nHOME_URL="https://fedoraproject.org/"\nDOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f69/system-administrators-guide/"\nSUPPORT_URL="https://ask.fedoraproject.org/"\nBUG_REPORT_URL="https://bugzilla.redhat.com/"\nREDHAT_BUGZILLA_PRODUCT="Fedora"\nREDHAT_BUGZILLA_PRODUCT_VERSION=69\nREDHAT_SUPPORT_PRODUCT="Fedora"\nREDHAT_SUPPORT_PRODUCT_VERSION=69',
|
||||
'cat ~/.ssh/id_ed25519.pub': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpFmKLqRKzMwRe3WkqJvQrN5mHjF2pRn8sT6yUvWxRe reese@homelab',
|
||||
'docker ps': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'docker ls': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'docker container ls': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'podman ps': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'podman ls': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'podman container ls': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'systemctl list-units': 'UNIT LOAD ACTIVE SUB DESCRIPTION\nborg.service loaded active running Borg Backup Service\ngitea.service loaded active running Gitea\ndocker.service loaded active running Docker Application Container Engine\nnextcloud.service loaded active running Nextcloud\nimmich.service loaded active running Immich\nopen-webui.service loaded active running Open WebUI\njellyfin.service loaded active running Jellyfin Media Server\nslopbox.service loaded active running Slopbox\nhomepage.service loaded active running Homepage Container',
|
||||
'systemctl list-units --type=service --state=running --no-pager': 'UNIT LOAD ACTIVE SUB DESCRIPTION\nborg.service loaded active running Borg Backup Service\ngitea.service loaded active running Gitea\ndocker.service loaded active running Docker Application Container Engine\nnextcloud.service loaded active running Nextcloud\nimmich.service loaded active running Immich\nopen-webui.service loaded active running Open WebUI\njellyfin.service loaded active running Jellyfin Media Server\nslopbox.service loaded active running Slopbox\nhomepage.service loaded active running Homepage Container',
|
||||
'ss': 'Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:80 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:443 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:222 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3000 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3001 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3002 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8081 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8096 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*\ntcp LISTEN 0 128 [::]:22 [::]:*',
|
||||
'ip addr show': '1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN\n inet 127.0.0.1/8 scope host lo\n inet6 ::1/128 scope host\n2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP\n inet 192.168.1.42/24 brd 192.168.1.255 scope global dynamic eth0\n inet6 fe80::1/64 scope link',
|
||||
'df -h': 'Filesystem Size Used Avail Use% Mounted on\n/dev/sda2 465G 182G 258G 42% /\nudev 16G 0 16G 0% /dev\ntmpfs 16G 2.1M 16G 1% /dev/shm\n/dev/sda1 511M 6.6M 505M 2% /boot/efi\n/dev/sdb1 1.8T 945G 793G 55% /mnt/data\noverlay 1.8T 945G 793G 55% /var/lib/docker/overlay2',
|
||||
'free -h': ' total used free shared buff/cache available\nMem: 31Gi 8.2Gi 12Gi 512Mi 11Gi 22Gi\nSwap: 2.0Gi 0B 2.0Gi',
|
||||
'ps aux': 'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nreese 1242 2.1 12.4 4285632 3932160 ? Sl May15 42:18 /opt/llama.cpp/build/bin/llama-server --model /models/Qwen3-30B-A3B.Q4_K_M.gguf --port 8082\ndocker 2341 1.2 6.8 8562348 2156032 ? Sl May15 28:45 /usr/bin/dockerd -H fd://\nreese 3456 0.8 3.2 2845632 1015808 ? Sl May15 18:22 /opt/open-webui/server\nreese 4567 0.5 2.1 1562348 665600 ? Sl May15 12:34 /usr/bin/python3 /opt/borg/borgmatic\nroot 5678 0.3 1.4 945632 448000 ? Ssl May15 8:12 /usr/bin/docker-proxy -p tcp:0.0.0.0:80:80\nreese 6789 0.2 1.1 745632 348000 ? Ssl May15 5:45 /usr/bin/podman run --name homepage\nroot 7890 0.1 0.8 545632 256000 ? Ssl May15 3:22 /usr/bin/docker-proxy -p tcp:0.0.0.0:443:443',
|
||||
'ps aux --sort=-%mem | head -10': 'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nreese 1242 2.1 12.4 4285632 3932160 ? Sl May15 42:18 /opt/llama.cpp/build/bin/llama-server --model /models/Qwen3-30B-A3B.Q4_K_M.gguf --port 8082\ndocker 2341 1.2 6.8 8562348 2156032 ? Sl May15 28:45 /usr/bin/dockerd -H fd://\nreese 3456 0.8 3.2 2845632 1015808 ? Sl May15 18:22 /opt/open-webui/server\nreese 4567 0.5 2.1 1562348 665600 ? Sl May15 12:34 /usr/bin/python3 /opt/borg/borgmatic\nroot 5678 0.3 1.4 945632 448000 ? Ssl May15 8:12 /usr/bin/docker-proxy -p tcp:0.0.0.0:80:80\nreese 6789 0.2 1.1 745632 348000 ? Ssl May15 5:45 /usr/bin/podman run --name homepage\nroot 7890 0.1 0.8 545632 256000 ? Ssl May15 3:22 /usr/bin/docker-proxy -p tcp:0.0.0.0:443:443',
|
||||
'neofetch': ` 'c. reese@homelab\n ,xNMM. ----------------------\n .OMMMMo OS: Fedora Linux 69 (Workstation Edition) x86_64\n OMMM0, Host: custom-build\n .;loddo:' loolloddol;. Kernel: 6.8.0\n cKMMMMMMMMMMNWMMMMMMMMMM0: Uptime: 42 days, 3 hours\n .KMMMMMMMMMMMMMMMMMMMMMMMWd. Packages: 2847 (dnf)\n XMMMMMMMMMMMMMMMMMMMMMMMX. Shell: bash 5.2.26\n;MMMMMMMMMMMMMMMMMMMMMMMM: Resolution: 2560x1440\n:MMMMMMMMMMMMMMMMMMMMMMMM: DE: GNOME 46.1\n.MMMMMMMMMMMMMMMMMMMMMMMMX. WM: Mutter\n kMMMMMMMMMMMMMMMMMMMMMMMMWd. Terminal: /dev/pts/0\n .XMMMMMMMMMMMMMMMMMMMMMMMMMMk CPU: AMD Ryzen 9 7900X (24) @ 5.6GHz\n .XMMMMMMMMMMMMMMMMMMMMMMK. GPU: NVIDIA GeForce RTX 4090\n kMMMMMMMMMMMMMMMMMMMMd GPU: AMD Ryzen Built-in\n ;KMMMMMMMWXXWMMMMMMMk. Memory: 8.2Gi / 31Gi\n .cooc,. .,coo:.\n\n<span style="color: #22c55e">███</span><span style="color: #22c55e">███</span><span style="color: #22c55e">███</span><span style="color: #eab308">███</span><span style="color: #eab308">███</span><span style="color: #eab308">███</span><span style="color: #3b82f6">███</span><span style="color: #3b82f6">███</span><span style="color: #3b82f6">███</span><span style="color: #8b5cf6">███</span><span style="color: #8b5cf6">███</span><span style="color: #8b5cf6">███</span><span style="color: #ec4899">███</span><span style="color: #ec4899">███</span><span style="color: #ec4899">███</span>`,
|
||||
'sl': '____\n|DD|____T_\n|_ |_____|<\n @-@-@-oo\\',
|
||||
'vim': 'vim: Enter the terminal text editor. Type :q to quit.',
|
||||
'help': 'Available commands:\n ls List directory contents\n pwd Print working directory\n whoami Print current user\n hostname Print hostname\n date Print current date/time\n uname Print system information\n uptime Print system uptime\n id Print user identity\n cat Print file contents (try: cat /etc/os-release)\n docker List running containers\n podman List running pods\n systemctl List running systemd services\n ss Show listening ports\n ip Show network interfaces\n df Show disk usage\n free Show memory usage\n ps Show running processes\n vim Terminal text editor (try: vim or vim filename)\n neofetch System info display\n sl Steam locomotive\n help Show this help message',
|
||||
};
|
||||
|
||||
terminal.addEventListener('keydown', (e) => {
|
||||
if (inVimMode) return;
|
||||
if (vimState.active) return;
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (historyIndex < commandHistory.length - 1) {
|
||||
@@ -210,456 +196,22 @@ function createTerminal(face) {
|
||||
if (cmdText === 'clear') {
|
||||
content.innerHTML = '';
|
||||
}
|
||||
if (cmdText.startsWith('vim')) {
|
||||
const filename = cmdText.split(' ').slice(1).join(' ') || '';
|
||||
launchVimSimulator({
|
||||
content, terminal, mobileInput, filename,
|
||||
setVimMode: (active) => { vimState.active = active; },
|
||||
});
|
||||
|
||||
if (cmdText.startsWith('vim')) {
|
||||
const filename = cmdText.split(' ').slice(1).join(' ') || '';
|
||||
let vimBuffer = ['Welcome to vim (simulated)!'];
|
||||
let vimLineNum = 0;
|
||||
let scrollOffset = 0;
|
||||
let vimStatus = '~';
|
||||
let vimCol = 0;
|
||||
let showSplash = false;
|
||||
inVimMode = true;
|
||||
const newAchievements = checkAchievements(cmdText, isRoot);
|
||||
if (newAchievements.length > 0) {
|
||||
revealAchievements();
|
||||
newAchievements.forEach(a => showToast(a));
|
||||
}
|
||||
|
||||
// Hide the mobile input so it doesn't intercept vim keystrokes
|
||||
mobileInput.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// 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<Enter> to escape this regret',
|
||||
' type :help<Enter> or <F1> for on-line help',
|
||||
' type :help version0<Enter> 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) + '<span class="vim-block-cursor">' + escapeHtml(cursorChar) + '</span>' + escapeHtml(after.slice(1));
|
||||
} else {
|
||||
lineTexts[i].innerHTML = escapeHtml(before) + '<span class="vim-block-cursor"> </span>';
|
||||
}
|
||||
} 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);
|
||||
inVimMode = 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 = '<span class="terminal-prompt">$</span> ';
|
||||
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;
|
||||
}
|
||||
if (e.key === 'd' && e.altKey) {
|
||||
if (vimLineNum < vimBuffer.length - 1) {
|
||||
vimBuffer.splice(vimLineNum, 1);
|
||||
if (vimLineNum >= vimBuffer.length) vimLineNum = vimBuffer.length - 1;
|
||||
}
|
||||
vimRender();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'd' && e.shiftKey) {
|
||||
if (vimLineNum < vimBuffer.length - 1) {
|
||||
vimBuffer.splice(vimLineNum, 1);
|
||||
if (vimLineNum >= vimBuffer.length) vimLineNum = vimBuffer.length - 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;
|
||||
}
|
||||
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);
|
||||
|
||||
const newAchievements = checkAchievements(cmdText, isRoot);
|
||||
if (newAchievements.length > 0) {
|
||||
revealAchievements();
|
||||
newAchievements.forEach(a => showToast(a));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmdText === 'exit') {
|
||||
if (!isRoot) {
|
||||
@@ -669,7 +221,6 @@ function createTerminal(face) {
|
||||
newAchievements.forEach(a => showToast(a));
|
||||
}
|
||||
|
||||
isLoginScreen = true;
|
||||
document.body.innerHTML = '';
|
||||
document.body.style.background = '#000';
|
||||
document.body.style.color = '#fff';
|
||||
@@ -823,35 +374,6 @@ function createTerminal(face) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = {
|
||||
'pwd': '/home/reese/portfolio',
|
||||
'whoami': 'reese',
|
||||
'hostname': 'homelab',
|
||||
'date': new Date().toString(),
|
||||
'uname': 'Linux homelab 7.0.0 #1 SMP x86_64 GNU/Linux',
|
||||
'uname -a': 'Linux homelab 6.8.0 #1 SMP x86_64 GNU/Linux',
|
||||
'uptime': ' 14:30:00 up 42 days, 3:15, 1 user, load average: 0.42, 0.38, 0.35',
|
||||
'id': 'uid=1000(reese) gid=1000(reese) groups=1000(reese),985(podman)',
|
||||
'cat /etc/os-release': 'PRETTY_NAME="Fedora Linux 69 (Workstation Edition)"\nNAME="Fedora Linux"\nVERSION_ID="69"\nVERSION="69 (Workstation Edition)"\nID=fedora\nVARIANT=Workstation Edition\nVARIANT_ID=workstation\nLOGO=fedora-logo-icon\nCPE_NAME="cpe:/o:fedoralinux:fedora:69"\nDEFAULT_HOSTNAME="fedora"\nHOME_URL="https://fedoraproject.org/"\nDOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f69/system-administrators-guide/"\nSUPPORT_URL="https://ask.fedoraproject.org/"\nBUG_REPORT_URL="https://bugzilla.redhat.com/"\nREDHAT_BUGZILLA_PRODUCT="Fedora"\nREDHAT_BUGZILLA_PRODUCT_VERSION=69\nREDHAT_SUPPORT_PRODUCT="Fedora"\nREDHAT_SUPPORT_PRODUCT_VERSION=69',
|
||||
'cat ~/.ssh/id_ed25519.pub': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpFmKLqRKzMwRe3WkqJvQrN5mHjF2pRn8sT6yUvWxRe reese@homelab',
|
||||
'docker ps': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'docker ls': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'docker container ls': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
|
||||
'podman ps': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'podman ls': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'podman container ls': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
|
||||
'systemctl list-units': 'UNIT LOAD ACTIVE SUB DESCRIPTION\nborg.service loaded active running Borg Backup Service\ngitea.service loaded active running Gitea\ndocker.service loaded active running Docker Application Container Engine\nnextcloud.service loaded active running Nextcloud\nimmich.service loaded active running Immich\nopen-webui.service loaded active running Open WebUI\njellyfin.service loaded active running Jellyfin Media Server\nslopbox.service loaded active running Slopbox\nhomepage.service loaded active running Homepage Container',
|
||||
'systemctl list-units --type=service --state=running --no-pager': 'UNIT LOAD ACTIVE SUB DESCRIPTION\nborg.service loaded active running Borg Backup Service\ngitea.service loaded active running Gitea\ndocker.service loaded active running Docker Application Container Engine\nnextcloud.service loaded active running Nextcloud\nimmich.service loaded active running Immich\nopen-webui.service loaded active running Open WebUI\njellyfin.service loaded active running Jellyfin Media Server\nslopbox.service loaded active running Slopbox\nhomepage.service loaded active running Homepage Container',
|
||||
'ss': 'Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:80 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:443 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:222 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3000 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3001 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3002 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8081 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8096 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*\ntcp LISTEN 0 128 [::]:22 [::]:*',
|
||||
'ip addr show': '1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN\n inet 127.0.0.1/8 scope host lo\n inet6 ::1/128 scope host\n2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP\n inet 192.168.1.42/24 brd 192.168.1.255 scope global dynamic eth0\n inet6 fe80::1/64 scope link',
|
||||
'df -h': 'Filesystem Size Used Avail Use% Mounted on\n/dev/sda2 465G 182G 258G 42% /\nudev 16G 0 16G 0% /dev\ntmpfs 16G 2.1M 16G 1% /dev/shm\n/dev/sda1 511M 6.6M 505M 2% /boot/efi\n/dev/sdb1 1.8T 945G 793G 55% /mnt/data\noverlay 1.8T 945G 793G 55% /var/lib/docker/overlay2',
|
||||
'free -h': ' total used free shared buff/cache available\nMem: 31Gi 8.2Gi 12Gi 512Mi 11Gi 22Gi\nSwap: 2.0Gi 0B 2.0Gi',
|
||||
'ps aux': 'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nreese 1242 2.1 12.4 4285632 3932160 ? Sl May15 42:18 /opt/llama.cpp/build/bin/llama-server --model /models/Qwen3-30B-A3B.Q4_K_M.gguf --port 8082\ndocker 2341 1.2 6.8 8562348 2156032 ? Sl May15 28:45 /usr/bin/dockerd -H fd://\nreese 3456 0.8 3.2 2845632 1015808 ? Sl May15 18:22 /opt/open-webui/server\nreese 4567 0.5 2.1 1562348 665600 ? Sl May15 12:34 /usr/bin/python3 /opt/borg/borgmatic\nroot 5678 0.3 1.4 945632 448000 ? Ssl May15 8:12 /usr/bin/docker-proxy -p tcp:0.0.0.0:80:80\nreese 6789 0.2 1.1 745632 348000 ? Ssl May15 5:45 /usr/bin/podman run --name homepage\nroot 7890 0.1 0.8 545632 256000 ? Ssl May15 3:22 /usr/bin/docker-proxy -p tcp:0.0.0.0:443:443',
|
||||
'ps aux --sort=-%mem | head -10': 'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nreese 1242 2.1 12.4 4285632 3932160 ? Sl May15 42:18 /opt/llama.cpp/build/bin/llama-server --model /models/Qwen3-30B-A3B.Q4_K_M.gguf --port 8082\ndocker 2341 1.2 6.8 8562348 2156032 ? Sl May15 28:45 /usr/bin/dockerd -H fd://\nreese 3456 0.8 3.2 2845632 1015808 ? Sl May15 18:22 /opt/open-webui/server\nreese 4567 0.5 2.1 1562348 665600 ? Sl May15 12:34 /usr/bin/python3 /opt/borg/borgmatic\nroot 5678 0.3 1.4 945632 448000 ? Ssl May15 8:12 /usr/bin/docker-proxy -p tcp:0.0.0.0:80:80\nreese 6789 0.2 1.1 745632 348000 ? Ssl May15 5:45 /usr/bin/podman run --name homepage\nroot 7890 0.1 0.8 545632 256000 ? Ssl May15 3:22 /usr/bin/docker-proxy -p tcp:0.0.0.0:443:443',
|
||||
'neofetch': ` 'c. reese@homelab\n ,xNMM. ----------------------\n .OMMMMo OS: Fedora Linux 69 (Workstation Edition) x86_64\n OMMM0, Host: custom-build\n .;loddo:' loolloddol;. Kernel: 6.8.0\n cKMMMMMMMMMMNWMMMMMMMMMM0: Uptime: 42 days, 3 hours\n .KMMMMMMMMMMMMMMMMMMMMMMMWd. Packages: 2847 (dnf)\n XMMMMMMMMMMMMMMMMMMMMMMMX. Shell: bash 5.2.26\n;MMMMMMMMMMMMMMMMMMMMMMMM: Resolution: 2560x1440\n:MMMMMMMMMMMMMMMMMMMMMMMM: DE: GNOME 46.1\n.MMMMMMMMMMMMMMMMMMMMMMMMX. WM: Mutter\n kMMMMMMMMMMMMMMMMMMMMMMMMWd. Terminal: /dev/pts/0\n .XMMMMMMMMMMMMMMMMMMMMMMMMMMk CPU: AMD Ryzen 9 7900X (24) @ 5.6GHz\n .XMMMMMMMMMMMMMMMMMMMMMMK. GPU: NVIDIA GeForce RTX 4090\n kMMMMMMMMMMMMMMMMMMMMd GPU: AMD Ryzen Built-in\n ;KMMMMMMMWXXWMMMMMMMk. Memory: 8.2Gi / 31Gi\n .cooc,. .,coo:.\n\n<span style="color: #22c55e">███</span><span style="color: #22c55e">███</span><span style="color: #22c55e">███</span><span style="color: #eab308">███</span><span style="color: #eab308">███</span><span style="color: #eab308">███</span><span style="color: #3b82f6">███</span><span style="color: #3b82f6">███</span><span style="color: #3b82f6">███</span><span style="color: #8b5cf6">███</span><span style="color: #8b5cf6">███</span><span style="color: #8b5cf6">███</span><span style="color: #ec4899">███</span><span style="color: #ec4899">███</span><span style="color: #ec4899">███</span>`,
|
||||
'sl': '____\n|DD|____T_\n|_ |_____|<\n @-@-@-oo\\',
|
||||
'help': 'Available commands:\n ls List directory contents\n pwd Print working directory\n whoami Print current user\n hostname Print hostname\n date Print current date/time\n uname Print system information\n uptime Print system uptime\n id Print user identity\n cat Print file contents\n docker List running containers\n podman List running pods\n systemctl List running systemd services\n ss Show listening ports\n ip Show network interfaces\n df Show disk usage\n free Show memory usage\n ps Show running processes\n apt Package manager (update, install)\n curl Fetch a webpage\n wget Fetch a webpage\n vim Terminal text editor (try: vim or vim filename)\n neofetch System info display\n sl Steam locomotive\n clear Clear the terminal\n exit Exit to login screen\n sudo Gain root access\n rm Remove files\n echo Print text\n help Show this help message',
|
||||
};
|
||||
|
||||
if (cmdText.startsWith('echo ')) {
|
||||
const output = cmdText.replace(/^echo\s+/, '');
|
||||
@@ -877,7 +399,7 @@ function createTerminal(face) {
|
||||
outLine.textContent = 'curl: error fetching ' + url + ': ' + err.message;
|
||||
content.scrollTop = content.scrollHeight;
|
||||
});
|
||||
} else if (cmdText.startsWith('wget ') || cmdText.startsWith('wget ')) {
|
||||
} else if (cmdText.startsWith('wget ')) {
|
||||
const url = cmdText.replace(/^wget\s+(?:-O\s+\S+\s+)?/, '');
|
||||
const outLine = document.createElement('div');
|
||||
outLine.style.whiteSpace = 'pre-wrap';
|
||||
@@ -976,8 +498,9 @@ function createTerminal(face) {
|
||||
} else {
|
||||
outLine.textContent = 'Usage: dnf <command>\nCommands:\n update Update packages\n install Install packages';
|
||||
}
|
||||
} else if (commands[cmdText] !== undefined) {
|
||||
const output = commands[cmdText];
|
||||
} else {
|
||||
const output = getCommandOutput(cmdText);
|
||||
if (output !== undefined) {
|
||||
const outLine = document.createElement('div');
|
||||
outLine.style.whiteSpace = 'pre-wrap';
|
||||
if (cmdText === 'neofetch') {
|
||||
@@ -988,6 +511,7 @@ function createTerminal(face) {
|
||||
}
|
||||
content.appendChild(outLine);
|
||||
}
|
||||
}
|
||||
|
||||
const newAchievements = checkAchievements(cmdText, isRoot);
|
||||
if (newAchievements.length > 0) {
|
||||
@@ -1044,7 +568,7 @@ function createTerminal(face) {
|
||||
isTabCompleting = true;
|
||||
|
||||
const matchSource = tabBaseInput || inputVal;
|
||||
const matches = allCommands.filter(cmd => cmd.startsWith(matchSource));
|
||||
const matches = TERMINAL_COMMAND_LIST.filter(cmd => cmd.startsWith(matchSource));
|
||||
|
||||
if (matches.length === 0) {
|
||||
isTabCompleting = false;
|
||||
@@ -1076,52 +600,24 @@ function createTerminal(face) {
|
||||
});
|
||||
|
||||
terminal.addEventListener('focus', () => {
|
||||
mobileInput.focus();
|
||||
terminal.style.boxShadow = '0 0 8px rgba(234, 179, 8, 0.3), inset 0 0 20px rgba(34, 197, 94, 0.1)';
|
||||
terminal.style.borderColor = '#eab308';
|
||||
terminal.classList.add('grown');
|
||||
terminal.classList.add('opaque');
|
||||
const face = terminal.closest('.server-face');
|
||||
const unit = face?.closest('.rack-unit');
|
||||
face?.classList.add('grown');
|
||||
unit?.classList.add('grown');
|
||||
document.getElementById('hero').classList.add('shifted');
|
||||
document.querySelector('.rack-container')?.classList.add('opaque');
|
||||
expandTerminal(terminal);
|
||||
});
|
||||
|
||||
terminal.addEventListener('blur', () => {
|
||||
if (heroMouseDown) return;
|
||||
terminal.style.boxShadow = '';
|
||||
terminal.style.borderColor = '#2a2a2e';
|
||||
terminal.classList.remove('grown');
|
||||
terminal.classList.remove('opaque');
|
||||
const face = terminal.closest('.server-face');
|
||||
const unit = face?.closest('.rack-unit');
|
||||
face?.classList.remove('grown');
|
||||
unit?.classList.remove('grown');
|
||||
document.getElementById('hero').classList.remove('shifted');
|
||||
document.querySelector('.rack-container')?.classList.remove('opaque');
|
||||
collapseTerminal(terminal);
|
||||
});
|
||||
|
||||
mobileInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (inVimMode) return;
|
||||
if (vimState.active) return;
|
||||
mobileInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
mobileInput.addEventListener('blur', () => {
|
||||
if (heroMouseDown) return;
|
||||
terminal.style.boxShadow = '';
|
||||
terminal.style.borderColor = '#2a2a2e';
|
||||
terminal.classList.remove('grown');
|
||||
terminal.classList.remove('opaque');
|
||||
const face = terminal.closest('.server-face');
|
||||
const unit = face?.closest('.rack-unit');
|
||||
face?.classList.remove('grown');
|
||||
unit?.classList.remove('grown');
|
||||
document.getElementById('hero').classList.remove('shifted');
|
||||
document.querySelector('.rack-container')?.classList.remove('opaque');
|
||||
collapseTerminal(terminal);
|
||||
});
|
||||
|
||||
let lastInputValue = '';
|
||||
@@ -1153,200 +649,5 @@ function createTerminal(face) {
|
||||
}
|
||||
|
||||
|
||||
// Achievement System
|
||||
const ACHIEVEMENTS_STORAGE_KEY = 'reese-terminal-achievements';
|
||||
|
||||
const ACHIEVEMENTS = {
|
||||
time_flies: { name: 'Time Flies', desc: 'Check the system uptime', icon: '⏱️', cmd: 'uptime' },
|
||||
curious: { name: 'Curious', desc: 'List directory contents', icon: '📁', cmd: 'ls', prefix: true },
|
||||
port_scanner: { name: 'Port Scanner', desc: 'Check listening ports', icon: '🔍', cmd: 'ss', prefix: true },
|
||||
password_reset: { name: 'Password Reset Request', desc: 'Submit a SNOW request', icon: '🚪', cmd: 'exit' },
|
||||
power_user: { name: 'Power User', desc: 'Gain root access', icon: '👑', cmd: 'sudo su -' },
|
||||
nice_try: { name: 'Nice Try', desc: 'Attempt rm -rf / as a normal user', icon: '😏', cmd: 'rm -rf /' },
|
||||
restore_backup: { name: 'Restore from Backup', desc: 'Actually destroy the system as root', icon: '💥', cmd: 'rm -rf / root' },
|
||||
where_am_i: { name: 'Where Am I?', desc: 'Print working directory', icon: '📍', cmd: 'pwd' },
|
||||
self_aware: { name: 'Self-Aware', desc: 'Print current user', icon: '🪞', cmd: 'whoami' },
|
||||
identity: { name: 'Identity Crisis', desc: 'Print hostname', icon: '🖥️', cmd: 'hostname' },
|
||||
time_keeper: { name: 'Time Keeper', desc: 'Print current date', icon: '📅', cmd: 'date' },
|
||||
system_explorer: { name: 'System Explorer', desc: 'Print system information', icon: '🔧', cmd: 'uname' },
|
||||
access_granted: { name: 'Access Granted', desc: 'Check user identity', icon: '🆔', cmd: 'id' },
|
||||
os_detective: { name: 'OS Detective', desc: 'Read the OS release file', icon: '🐧', cmd: 'cat /etc/os-release' },
|
||||
ssh_explorer: { name: 'SSH Explorer', desc: 'View SSH public key', icon: '🔑', cmd: 'cat ~/.ssh/id_ed25519.pub' },
|
||||
container_watcher: { name: 'Container Watcher', desc: 'List Docker containers', icon: '🐳', cmd: 'docker ps', prefix: true },
|
||||
docker_confused: { name: 'Docker Confused', desc: 'Use wrong Docker command', icon: '🤔', cmd: 'docker ls', prefix: true },
|
||||
container_pro: { name: 'Container Pro', desc: 'Use correct container command', icon: '✅', cmd: 'docker container ls', prefix: true },
|
||||
podman_fan: { name: 'Podman Fan', desc: 'List Podman containers', icon: '📦', cmd: 'podman ps', prefix: true },
|
||||
podman_confused: { name: 'Podman Confused', desc: 'Use wrong Podman command', icon: '🤔', cmd: 'podman ls', prefix: true },
|
||||
podman_pro: { name: 'Podman Pro', desc: 'Use correct Podman command', icon: '✅', cmd: 'podman container ls', prefix: true },
|
||||
service_hunter: { name: 'Service Hunter', desc: 'List systemd units', icon: '🔎', cmd: 'systemctl list-units', prefix: true },
|
||||
service_filter: { name: 'Service Filter', desc: 'Filter running services', icon: '🎯', cmd: 'systemctl list-units --type=service --state=running --no-pager' },
|
||||
network_ninja: { name: 'Network Ninja', desc: 'Show network interfaces', icon: '🌐', cmd: 'ip addr show', prefix: true },
|
||||
disk_detective: { name: 'Disk Detective', desc: 'Check disk usage', icon: '💾', cmd: 'df -h' },
|
||||
memory_minded: { name: 'Memory Minded', desc: 'Check memory usage', icon: '🧠', cmd: 'free -h' },
|
||||
process_watcher: { name: 'Process Watcher', desc: 'List running processes', icon: '👁️', cmd: 'ps aux', prefix: true },
|
||||
memory_hog: { name: 'Memory Hog', desc: 'Find top memory consumers', icon: '🐗', cmd: 'ps aux --sort=-%mem | head -10' },
|
||||
aesthetic_mode: { name: 'Aesthetic Mode', desc: 'Display system info with style', icon: '✨', cmd: 'neofetch' },
|
||||
help_seeker: { name: 'Help Seeker', desc: 'Look up available commands', icon: '📖', cmd: 'help' },
|
||||
clean_slate: { name: 'Clean Slate', desc: 'Clear the terminal', icon: '🧹', cmd: 'clear' },
|
||||
train_spotter: { name: 'Train Spotter', desc: 'Run the steam locomotive', icon: '🚂', cmd: 'sl' },
|
||||
tain: { name: 'I like trains', desc: 'Choo chooooooo', icon: '🧹', cmd: 'sl' },
|
||||
web_navigator: { name: 'Web Navigator', desc: 'Fetch a webpage using curl or wget', icon: '🌐', cmd: 'curl', prefix: true },
|
||||
package_manager: { name: 'Package Manager', desc: 'Update package lists with apt', icon: '📦', cmd: 'apt update' },
|
||||
software_installer: { name: 'Software Installer', desc: 'Install a package with apt', icon: '🔧', cmd: 'apt install', prefix: true },
|
||||
fedora_updater: { name: 'Fedora Updater', desc: 'Update packages with dnf', icon: '🎯', cmd: 'dnf update' },
|
||||
fedora_installer: { name: 'Fedora Installer', desc: 'Install a package with dnf', icon: '📀', cmd: 'dnf install', prefix: true },
|
||||
vim_splash: { name: 'Vi sIgnificantly worse M', desc: 'Open vim without a filename', icon: '📝', cmd: 'vim' },
|
||||
vim_edit: { name: 'Actually Editing', desc: 'Open vim with a filename', icon: '✏️', cmd: 'vim ', prefix: true },
|
||||
};
|
||||
|
||||
// Load unlocked achievement keys from localStorage
|
||||
function loadAchievements() {
|
||||
try {
|
||||
const saved = localStorage.getItem(ACHIEVEMENTS_STORAGE_KEY);
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Persist unlocked achievement keys to localStorage
|
||||
function saveAchievements(achieved) {
|
||||
localStorage.setItem(ACHIEVEMENTS_STORAGE_KEY, JSON.stringify(achieved));
|
||||
}
|
||||
|
||||
// Check if the given command triggers any new achievements; returns unlocked achievement objects
|
||||
function checkAchievements(cmdText, isRoot) {
|
||||
const achieved = loadAchievements();
|
||||
const newAchievements = [];
|
||||
|
||||
for (const [key, achievement] of Object.entries(ACHIEVEMENTS)) {
|
||||
if (achieved.includes(key)) continue;
|
||||
|
||||
if (key === 'restore_backup' && cmdText === 'rm -rf /' && isRoot) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'power_user' && (cmdText === 'sudo su -' || cmdText === 'sudo -i')) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'password_reset' && cmdText === 'exit' && !isRoot) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'nice_try' && cmdText === 'rm -rf /' && !isRoot) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'system_explorer' && (cmdText === 'uname' || cmdText === 'uname -a')) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'docker_confused' && cmdText === 'docker ls') {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'podman_confused' && cmdText === 'podman ls') {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (key === 'web_navigator' && (cmdText.startsWith('curl ') || cmdText.startsWith('wget '))) {
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
} else if (achievement.prefix ? cmdText.startsWith(achievement.cmd) : achievement.cmd === cmdText) {
|
||||
if ((key === 'nice_try' && isRoot) || (key === 'restore_backup' && !isRoot)) {
|
||||
continue;
|
||||
}
|
||||
achieved.push(key);
|
||||
newAchievements.push(achievement);
|
||||
}
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
saveAchievements(achieved);
|
||||
}
|
||||
|
||||
return newAchievements;
|
||||
}
|
||||
|
||||
// Display a toast notification for a newly unlocked achievement
|
||||
function showToast(achievement) {
|
||||
const container = document.getElementById('toast-container') || createToastContainer();
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast';
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${achievement.icon}</div>
|
||||
<div class="toast-content">
|
||||
<div class="toast-label">Achievement Unlocked</div>
|
||||
<div class="toast-title">${achievement.name}</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('removing');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// Create the toast notification container element if it doesn't exist
|
||||
function createToastContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Reveal the hidden achievements section and add it to the nav if needed
|
||||
function revealAchievements() {
|
||||
const section = document.getElementById('achievements');
|
||||
if (section.style.display === 'none' || !section.style.display) {
|
||||
section.style.display = 'block';
|
||||
section.classList.add('fade-in');
|
||||
setTimeout(() => section.classList.add('visible'), 50);
|
||||
addAchievementsNav();
|
||||
}
|
||||
renderAchievements();
|
||||
}
|
||||
|
||||
// Render all achievement cards (unlocked and locked) into the achievements grid
|
||||
function renderAchievements() {
|
||||
const achieved = loadAchievements();
|
||||
const total = Object.keys(ACHIEVEMENTS).length;
|
||||
const count = achieved.length;
|
||||
|
||||
const countEl = document.getElementById('achievements-count');
|
||||
countEl.textContent = `${count} / ${total} achievements unlocked`;
|
||||
|
||||
const grid = document.getElementById('achievements-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
for (const [key, achievement] of Object.entries(ACHIEVEMENTS)) {
|
||||
const isUnlocked = achieved.includes(key);
|
||||
const card = document.createElement('div');
|
||||
card.className = `achievement-card ${isUnlocked ? 'unlocked' : 'locked'}`;
|
||||
card.innerHTML = `
|
||||
<div class="achievement-icon">${achievement.icon}</div>
|
||||
<div class="achievement-info">
|
||||
<h3>${isUnlocked ? achievement.name : '???'}</h3>
|
||||
<p>${isUnlocked ? achievement.desc : 'Keep exploring the terminal...'}</p>
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
// Add an "Achievements" link to the main navigation if achievements have been unlocked
|
||||
function addAchievementsNav() {
|
||||
const navLinks = document.getElementById('navLinks');
|
||||
const existing = document.getElementById('nav-achievements');
|
||||
if (!existing && loadAchievements().length > 0) {
|
||||
const li = document.createElement('li');
|
||||
li.id = 'nav-achievements';
|
||||
li.innerHTML = '<a href="#achievements">Achievements</a>';
|
||||
navLinks.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore previously unlocked achievements on page load
|
||||
(function initAchievements() {
|
||||
const achieved = loadAchievements();
|
||||
if (achieved.length > 0) {
|
||||
revealAchievements();
|
||||
addAchievementsNav();
|
||||
}
|
||||
})();
|
||||
// Restore previously unlocked achievements (terminal-achievements.js).
|
||||
initTerminalAchievements();
|
||||
Reference in New Issue
Block a user