Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>XortronOS v8 // CyberDesktop Enhanced</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
/* --- Base Styles (Keep previous styles, add/modify as needed) --- */ | |
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700&display=swap'); | |
:root { /* ... keep color variables ... */ } | |
body { /* ... keep body styles ... */ } | |
.cyber-font { /* ... */ } | |
.window { /* ... keep window styles, ensure position: fixed/absolute logic is solid ... */ } | |
.window-header { /* ... keep header styles ... */ cursor: grab; } | |
.window-header:active { cursor: grabbing; } | |
.window-content { /* ... keep content flex/scroll styles ... */ } | |
.terminal { /* ... keep terminal styles ... */ } | |
.glow-text { /* ... */ } | |
.taskbar { /* ... keep taskbar styles ... */ } | |
.icon { /* ... keep icon styles ... */ } | |
.file { /* ... keep file styles ... */ cursor: pointer; } | |
.cursor-blink { /* ... */ } | |
.pulse { /* ... */ } | |
::-webkit-scrollbar { /* ... */ } | |
::-webkit-scrollbar-track { /* ... */ } | |
::-webkit-scrollbar-thumb { /* ... */ } | |
.matrix-bg { /* ... */ } | |
.browser-nav { /* ... */ } | |
.url-bar { /* ... */ } | |
.theme-button { /* ... */ } | |
/* --- New Styles --- */ | |
/* System Monitor */ | |
.progress-bar-container { | |
background-color: rgba(255, 255, 255, 0.1); | |
border-radius: 2px; overflow: hidden; height: 16px; border: 1px solid rgba(255, 255, 255, 0.2); | |
} | |
.progress-bar { | |
background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
height: 100%; transition: width 0.5s ease-out; text-align: right; padding-right: 5px; font-size: 10px; line-height: 14px; color: #000; | |
} | |
.process-list-item { | |
background-color: rgba(0,0,0,0.2); padding: 3px 6px; margin-bottom: 4px; border-radius: 2px; display: flex; justify-content: space-between; align-items: center; | |
} | |
/* Text Editor */ | |
#editor-textarea { | |
width: 100%; height: 100%; background: rgba(0,0,0,0.7); border: none; outline: none; | |
color: var(--light); font-family: 'Share Tech Mono', monospace; padding: 10px; font-size: 14px; | |
resize: none; | |
} | |
.editor-statusbar { | |
background: rgba(0,0,0,0.5); padding: 3px 10px; font-size: 12px; border-top: 1px solid var(--primary); flex-shrink: 0; | |
} | |
/* Code Breaker Game */ | |
.codebreaker-board { display: flex; flex-direction: column; gap: 10px; } | |
.codebreaker-row { display: flex; align-items: center; gap: 8px; } | |
.codebreaker-guess-peg { width: 30px; height: 30px; border: 1px solid #ccc; border-radius: 50%; cursor: pointer; transition: background-color 0.2s; } | |
.codebreaker-feedback-area { display: grid; grid-template-columns: repeat(2, 10px); gap: 3px; width: 23px; height: 23px; border: 1px dashed #555; padding: 1px;} | |
.codebreaker-feedback-peg { width: 10px; height: 10px; border-radius: 50%; border: 1px solid #333; } | |
.feedback-black { background-color: #FFF; border-color: #FFF;} /* Correct color and position */ | |
.feedback-white { background-color: #888; border-color: #888;} /* Correct color, wrong position */ | |
.codebreaker-palette { display: flex; gap: 5px; margin-top: 15px; } | |
.palette-color { width: 25px; height: 25px; border: 2px solid transparent; border-radius: 50%; cursor: pointer; } | |
.palette-color.selected { border-color: var(--primary); transform: scale(1.1); } | |
/* File Explorer Enhancements */ | |
.file-explorer-toolbar { padding: 5px; border-bottom: 1px solid var(--primary); flex-shrink: 0; display: flex; gap: 5px; } | |
.file-explorer-pathbar { padding: 3px 8px; background: rgba(0,0,0,0.3); margin-bottom: 5px; font-size: 12px; } | |
.file-item .fa-folder { color: #FFCA28; } /* Yellow folder */ | |
.file-item .fa-file-alt { color: #B0BEC5; } /* Grey text file */ | |
.file-item .fa-file-code { color: #42A5F5; } /* Blue code file */ | |
.file-item .fa-lock { color: #EF5350; margin-left: 4px; font-size: 0.8em; } /* Red lock for encrypted */ | |
</style> | |
</head> | |
<body class="flex flex-col md:h-screen"> | |
<canvas id="matrix" class="matrix-bg"></canvas> | |
<div id="desktop-area" class="flex-grow relative overflow-hidden"> | |
<div class="absolute left-0 top-0 p-2 md:p-4 flex flex-col space-y-1 md:space-y-0"> | |
<div class="icon" onclick="openApp('terminal')"> | |
<div class="text-3xl text-center mb-1"><i class="fas fa-terminal" style="color: var(--green);"></i></div> <div class="text-xs text-center">Terminal</div> | |
</div> | |
<div class="icon" onclick="openApp('file-explorer')"> | |
<div class="text-3xl text-center mb-1"><i class="fas fa-folder-open text-yellow-500"></i></div> <div class="text-xs text-center">Files</div> | |
</div> | |
<div class="icon" onclick="openApp('editor')"> <div class="text-3xl text-center mb-1"><i class="fas fa-edit text-blue-400"></i></div> <div class="text-xs text-center">Editor</div> | |
</div> | |
<div class="icon" onclick="openApp('monitor')"> <div class="text-3xl text-center mb-1"><i class="fas fa-tachometer-alt text-red-400"></i></div> <div class="text-xs text-center">Monitor</div> | |
</div> | |
<div class="icon" onclick="openApp('hack-tools')"> | |
<div class="text-3xl text-center mb-1"><i class="fas fa-user-secret text-red-500"></i></div> <div class="text-xs text-center">Hack Tools</div> | |
</div> | |
<div class="icon" onclick="openApp('network')"> | |
<div class="text-3xl text-center mb-1"><i class="fas fa-network-wired text-blue-500"></i></div> <div class="text-xs text-center">Network</div> | |
</div> | |
<div class="icon" onclick="openApp('browser')"> | |
<div class="text-3xl text-center mb-1"><i class="fas fa-globe text-purple-500"></i></div> <div class="text-xs text-center">DarkBrowser</div> | |
</div> | |
<div class="icon" onclick="openApp('codebreaker')"> <div class="text-3xl text-center mb-1"><i class="fas fa-puzzle-piece text-green-400"></i></div> <div class="text-xs text-center">CodeBreaker</div> | |
</div> | |
<div class="icon" onclick="openApp('settings')"> | |
<div class="text-3xl text-center mb-1"><i class="fas fa-cog text-gray-400"></i></div> <div class="text-xs text-center">Settings</div> | |
</div> | |
</div> | |
<div id="windows-container"> | |
<div id="terminal-window" class="window hidden" style="/* initial desktop styles */"> | |
<div class="window-content p-0"> | |
<div class="terminal p-2 h-full overflow-auto" id="terminal-content"> | |
<div class="output-area mb-2"></div> | |
<div class="prompt-line flex items-center"> | |
<span class="text-green-500" id="terminal-prompt">root@xortron-os:~#</span> | |
<input type="text" class="bg-transparent border-none outline-none flex-1 ml-2 text-green-500 terminal-input-field" autofocus autocomplete="off" spellcheck="false"> | |
<span class="cursor-blink">█</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="file-explorer-window" class="window hidden flex flex-col" style="/* initial desktop styles */"> | |
<div class="file-explorer-toolbar"> | |
<button onclick="FileExplorer.navigateBack()" class="px-2 py-1 bg-gray-700 hover:bg-gray-600 text-xs"><i class="fas fa-arrow-left mr-1"></i> Back</button> | |
<button onclick="FileExplorer.createNew('folder')" class="px-2 py-1 bg-gray-700 hover:bg-gray-600 text-xs"><i class="fas fa-folder-plus mr-1"></i> New Folder</button> | |
<button onclick="FileExplorer.createNew('file')" class="px-2 py-1 bg-gray-700 hover:bg-gray-600 text-xs"><i class="fas fa-file-plus mr-1"></i> New File</button> | |
<button onclick="FileExplorer.deleteSelected()" class="px-2 py-1 bg-red-800 hover:bg-red-700 text-xs"><i class="fas fa-trash mr-1"></i> Delete</button> | |
</div> | |
<div class="file-explorer-pathbar" id="file-explorer-path">/</div> | |
<div class="window-content p-2 flex-grow"> <div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4 gap-3" id="file-explorer-grid"> | |
</div> | |
</div> | |
</div> | |
<div id="editor-window" class="window hidden flex flex-col" style="z-index: 11; top: 6rem; left: 10rem; width: 60%; height: 65%;"> | |
<div class="window-header flex justify-between items-center"> | |
<div class="flex items-center"><div class="w-3 h-3 r-f bg-r-500 mr-2"></div><div class="w-3 h-3 r-f bg-y-500 mr-2"></div><div class="w-3 h-3 r-f bg-g-500 mr-2"></div> <span class="cyber-font ml-2 text-sm md:text-base" id="editor-title">Editor</span></div> | |
<div class="flex"><button class="px-2" onclick="Editor.save()">Save</button><button class="px-2" onclick="minimizeWindow('editor')">_</button><button class="px-2" onclick="maximizeWindow('editor')">□</button><button class="px-2" onclick="closeWindow('editor')">×</button></div> | |
</div> | |
<div class="window-content p-0 flex-grow"> <textarea id="editor-textarea" spellcheck="false"></textarea> | |
</div> | |
<div class="editor-statusbar" id="editor-statusbar">Ready.</div> | |
</div> | |
<div id="monitor-window" class="window hidden flex flex-col" style="z-index: 8; top: 14rem; left: 18rem; width: 450px; height: 350px;"> | |
<div class="window-header flex justify-between items-center"> | |
<div class="flex items-center"><div class="w-3 h-3 r-f bg-r-500 mr-2"></div><div class="w-3 h-3 r-f bg-y-500 mr-2"></div><div class="w-3 h-3 r-f bg-g-500 mr-2"></div> <span class="cyber-font ml-2 text-sm md:text-base">System Monitor</span></div> | |
<div class="flex"><button class="px-2" onclick="minimizeWindow('monitor')">_</button><button class="px-2" onclick="maximizeWindow('monitor')">□</button><button class="px-2" onclick="closeWindow('monitor')">×</button></div> | |
</div> | |
<div class="window-content p-4 overflow-auto"> | |
<div class="mb-4"> | |
<div class="flex justify-between items-center mb-1"><span>CPU Usage</span><span id="monitor-cpu-text">0%</span></div> | |
<div class="progress-bar-container"><div id="monitor-cpu-bar" class="progress-bar" style="width: 0%;"></div></div> | |
</div> | |
<div class="mb-4"> | |
<div class="flex justify-between items-center mb-1"><span>RAM Usage</span><span id="monitor-ram-text">0 MB / 16384 MB</span></div> | |
<div class="progress-bar-container"><div id="monitor-ram-bar" class="progress-bar" style="width: 0%;"></div></div> | |
</div> | |
<div> | |
<h3 class="cyber-font text-lg mb-2 glow-text">Running Processes</h3> | |
<div id="monitor-process-list" class="space-y-1 text-sm"> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="codebreaker-window" class="window hidden flex flex-col" style="z-index: 9; top: 10rem; left: 22rem; width: 400px; height: 550px;"> | |
<div class="window-header flex justify-between items-center"> | |
<div class="flex items-center"><div class="w-3 h-3 r-f bg-r-500 mr-2"></div><div class="w-3 h-3 r-f bg-y-500 mr-2"></div><div class="w-3 h-3 r-f bg-g-500 mr-2"></div> <span class="cyber-font ml-2 text-sm md:text-base">Code Breaker v1.0</span></div> | |
<div class="flex"><button class="px-2" onclick="minimizeWindow('codebreaker')">_</button><button class="px-2" onclick="maximizeWindow('codebreaker')">□</button><button class="px-2" onclick="closeWindow('codebreaker')">×</button></div> | |
</div> | |
<div class="window-content p-4 overflow-auto flex flex-col items-center"> | |
<h3 class="cyber-font text-lg mb-2 glow-text">Break the 4-Digit Code</h3> | |
<div class="text-xs mb-3 text-gray-400">Colors: Red, Green, Blue, Yellow, Magenta, Cyan</div> | |
<div id="codebreaker-game-board" class="codebreaker-board mb-4"> | |
</div> | |
<div class="codebreaker-palette mb-4" id="codebreaker-color-palette"> | |
</div> | |
<button id="codebreaker-submit" class="px-4 py-2 bg-[--primary] text-black hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed" disabled>Submit Guess</button> | |
<div id="codebreaker-message" class="mt-4 text-center font-bold"></div> | |
<button id="codebreaker-restart" class="mt-2 px-3 py-1 bg-gray-600 hover:bg-gray-500 text-xs hidden">New Game</button> | |
</div> | |
</div> | |
<div id="hack-tools-window" class="window hidden" style="/* ... */">...</div> | |
<div id="network-window" class="window hidden" style="/* ... */">...</div> | |
<div id="browser-window" class="window hidden flex flex-col" style="/* ... */">...</div> | |
<div id="settings-window" class="window hidden" style="/* ... */"> | |
<div class="window-content p-4"> | |
<div class="mt-6"> | |
<h3 class="cyber-font text-lg mb-2 glow-text text-red-500">System Reset</h3> | |
<button class="theme-button bg-red-800 hover:bg-red-700" onclick="resetXortronOS()">Reset File System & Settings</button> | |
<p class="text-xs text-gray-400 mt-1">Warning: This will clear all saved files, history, and settings.</p> | |
</div> | |
</div> | |
</div> | |
</div> </div> <div class="taskbar fixed bottom-0 left-0 right-0 h-10 flex justify-between items-center px-2 md:px-4"> | |
<div class="flex items-center taskbar-app-icons"> | |
<button class="cyber-font text-lg md:text-xl mr-2 md:mr-4 glow-text">XORTRON</button> | |
<button class="mx-1 md:mx-2 text-sm hover:text-[--primary]" onclick="openApp('terminal')"><i class="fas fa-terminal mr-1"></i> <span class="hidden sm:inline">Terminal</span></button> | |
<button class="mx-1 md:mx-2 text-sm hover:text-[--primary]" onclick="openApp('file-explorer')"><i class="fas fa-folder-open mr-1"></i> <span class="hidden sm:inline">Files</span></button> | |
<button class="mx-1 md:mx-2 text-sm hover:text-[--primary]" onclick="openApp('editor')"><i class="fas fa-edit mr-1"></i> <span class="hidden sm:inline">Editor</span></button> | |
<button class="mx-1 md:mx-2 text-sm hover:text-[--primary]" onclick="openApp('browser')"><i class="fas fa-globe mr-1"></i> <span class="hidden sm:inline">Browser</span></button> | |
<button class="mx-1 md:mx-2 text-sm hover:text-[--primary]" onclick="openApp('codebreaker')"><i class="fas fa-puzzle-piece mr-1"></i> <span class="hidden sm:inline">Game</span></button> | |
</div> | |
</div> | |
<div id="notification" class="fixed bottom-12 md:bottom-16 right-4 bg-gray-900 border border-[--primary] p-3 text-sm max-w-xs rounded hidden z-[6000]">...</div> | |
<audio id="audio-open" src="https://cdn.pixabay.com/download/audio/2022/03/15/audio_00f666c791.mp3" preload="auto"></audio> | |
<audio id="audio-close" src="https://cdn.pixabay.com/download/audio/2021/08/04/audio_a4723ae51a.mp3" preload="auto"></audio> | |
<audio id="audio-notify" src="https://cdn.pixabay.com/download/audio/2022/11/17/audio_73659bf4de.mp3" preload="auto"></audio> | |
<audio id="audio-error" src="https://cdn.pixabay.com/download/audio/2022/03/10/audio_c4cc4c47ce.mp3" preload="auto"></audio> <audio id="audio-success" src="https://cdn.pixabay.com/download/audio/2022/03/10/audio_17cc2a1796.mp3" preload="auto"></audio> <script> | |
// --- VFS (Virtual File System using localStorage) --- | |
const VFS = (() => { | |
const STORAGE_KEY = 'xortron_vfs'; | |
const CWD_KEY = 'xortron_cwd'; | |
let fsData = {}; | |
let currentDirectory = '/'; | |
const load = () => { | |
try { | |
const storedFs = localStorage.getItem(STORAGE_KEY); | |
const storedCwd = localStorage.getItem(CWD_KEY); | |
if (storedFs) { | |
fsData = JSON.parse(storedFs); | |
} else { | |
// Initialize default FS | |
fsData = { | |
'/': { type: 'dir', content: ['root'] }, | |
'/root': { type: 'dir', content: ['Documents', 'Downloads', 'exploit.py', 'targets.txt.enc'] }, | |
'/root/Documents': { type: 'dir', content: ['notes.txt'] }, | |
'/root/Downloads': { type: 'dir', content: [] }, | |
'/root/exploit.py': { type: 'file', content: '#!/usr/bin/env python\nprint("Running exploit...")\n# TODO: Add actual exploit logic simulation' }, | |
'/root/Documents/notes.txt': { type: 'file', content: 'Meeting notes:\n- Project Xortron funding secured.\n- Need more neon.\n- Remember to buy synthwave.' }, | |
'/root/targets.txt.enc': { type: 'file', content: 'U2FsdGVkX1+ABC... [Simulated Encrypted Data]' } // Fake encrypted file | |
}; | |
save(); | |
} | |
currentDirectory = storedCwd || '/root'; // Default to /root | |
// Ensure current directory exists, otherwise reset to root | |
if (!exists(currentDirectory) || !isDirectory(currentDirectory)) { | |
currentDirectory = '/root'; | |
localStorage.setItem(CWD_KEY, currentDirectory); | |
} | |
} catch (e) { | |
console.error("Error loading VFS:", e); | |
reset(); // Reset if corrupted | |
} | |
}; | |
const save = () => { | |
try { | |
localStorage.setItem(STORAGE_KEY, JSON.stringify(fsData)); | |
localStorage.setItem(CWD_KEY, currentDirectory); | |
} catch (e) { | |
console.error("Error saving VFS:", e); | |
showNotification("Error saving file system state!"); | |
} | |
}; | |
const reset = () => { | |
localStorage.removeItem(STORAGE_KEY); | |
localStorage.removeItem(CWD_KEY); | |
fsData = {}; // Clear in-memory cache | |
load(); // Reload initial state | |
FileExplorer.render(); // Update file explorer display after reset | |
Terminal.updatePrompt(); // Update terminal prompt | |
showNotification("XortronOS state reset."); | |
}; | |
const normalizePath = (path) => { | |
if (!path) return currentDirectory; | |
let newPath; | |
if (path.startsWith('/')) { | |
newPath = path; // Absolute path | |
} else { | |
// Relative path: join with current directory | |
newPath = joinPath(currentDirectory, path); | |
} | |
// Resolve .. and . components | |
const parts = newPath.split('/').filter(p => p !== ''); | |
const resolvedParts = []; | |
for (const part of parts) { | |
if (part === '..') { | |
resolvedParts.pop(); | |
} else if (part !== '.') { | |
resolvedParts.push(part); | |
} | |
} | |
// Handle edge case of resolving back to root or beyond | |
if (resolvedParts.length === 0) return '/'; | |
return '/' + resolvedParts.join('/'); | |
}; | |
const joinPath = (base, part) => { | |
if (base === '/') return '/' + part; | |
return base + '/' + part; | |
}; | |
const getParentPath = (path) => { | |
if (path === '/') return '/'; | |
const parts = path.split('/'); | |
parts.pop(); | |
return parts.join('/') || '/'; | |
}; | |
const getItemName = (path) => { | |
if (path === '/') return '/'; | |
return path.substring(path.lastIndexOf('/') + 1); | |
}; | |
const exists = (path) => fsData.hasOwnProperty(normalizePath(path)); | |
const isDirectory = (path) => exists(path) && fsData[normalizePath(path)].type === 'dir'; | |
const isFile = (path) => exists(path) && fsData[normalizePath(path)].type === 'file'; | |
const readFile = (path) => { | |
const normPath = normalizePath(path); | |
if (isFile(normPath)) { | |
return fsData[normPath].content; | |
} | |
return null; // Or throw error | |
}; | |
const writeFile = (path, content) => { | |
const normPath = normalizePath(path); | |
const parentPath = getParentPath(normPath); | |
const itemName = getItemName(normPath); | |
if (!isDirectory(parentPath)) return false; // Parent must exist and be a directory | |
// If file exists, just update content | |
if (isFile(normPath)) { | |
fsData[normPath].content = content; | |
save(); | |
return true; | |
} | |
// If it doesn't exist or is a directory, create/replace | |
else if (isDirectory(parentPath)) { | |
fsData[normPath] = { type: 'file', content: content }; | |
// Add to parent directory's content list if not already there | |
if (!fsData[parentPath].content.includes(itemName)) { | |
fsData[parentPath].content.push(itemName); | |
} | |
save(); | |
return true; | |
} | |
return false; // Should not happen if parent is dir | |
}; | |
const createDirectory = (path) => { | |
const normPath = normalizePath(path); | |
const parentPath = getParentPath(normPath); | |
const itemName = getItemName(normPath); | |
if (exists(normPath)) return false; // Already exists | |
if (!isDirectory(parentPath)) return false; // Parent must exist and be directory | |
fsData[normPath] = { type: 'dir', content: [] }; | |
fsData[parentPath].content.push(itemName); | |
save(); | |
return true; | |
}; | |
const createFile = (path, content = '') => { | |
const normPath = normalizePath(path); | |
const parentPath = getParentPath(normPath); | |
const itemName = getItemName(normPath); | |
if (exists(normPath)) return false; // Already exists | |
if (!isDirectory(parentPath)) return false; | |
fsData[normPath] = { type: 'file', content: content }; | |
fsData[parentPath].content.push(itemName); | |
save(); | |
return true; | |
}; | |
const listDirectory = (path) => { | |
const normPath = normalizePath(path); | |
if (isDirectory(normPath)) { | |
// Return objects with name and type | |
return fsData[normPath].content.map(name => { | |
const itemPath = joinPath(normPath, name); | |
return { name, type: fsData[itemPath]?.type || 'unknown' }; | |
}).sort((a, b) => { // Sort folders first, then alphabetically | |
if (a.type === 'dir' && b.type !== 'dir') return -1; | |
if (a.type !== 'dir' && b.type === 'dir') return 1; | |
return a.name.localeCompare(b.name); | |
}); | |
} | |
return null; // Or throw error | |
}; | |
const deleteItem = (path) => { | |
const normPath = normalizePath(path); | |
if (!exists(normPath) || normPath === '/') return false; // Cannot delete root | |
const parentPath = getParentPath(normPath); | |
const itemName = getItemName(normPath); | |
// Check if directory is empty before deleting (simple check) | |
if (isDirectory(normPath) && fsData[normPath].content.length > 0) { | |
// Could implement recursive delete later (rm -r) | |
return 'dir_not_empty'; // Special return value | |
} | |
// Remove from parent listing | |
if (fsData[parentPath] && fsData[parentPath].content) { | |
fsData[parentPath].content = fsData[parentPath].content.filter(name => name !== itemName); | |
} | |
// Remove the item itself | |
delete fsData[normPath]; | |
save(); | |
return true; | |
}; | |
const getCurrentDirectory = () => currentDirectory; | |
const setCurrentDirectory = (path) => { | |
const normPath = normalizePath(path); | |
if (isDirectory(normPath)) { | |
currentDirectory = normPath; | |
save(); // Persist CWD change | |
Terminal.updatePrompt(); // Update terminal prompt | |
FileExplorer.render(currentDirectory); // Update file explorer | |
return true; | |
} | |
return false; | |
}; | |
// Initial load | |
load(); | |
return { | |
load, save, reset, normalizePath, joinPath, exists, isDirectory, isFile, | |
readFile, writeFile, createDirectory, createFile, listDirectory, deleteItem, | |
getCurrentDirectory, setCurrentDirectory, getParentPath, getItemName | |
}; | |
})(); | |
// --- Global App Management --- | |
const AppManager = (() => { | |
let highestZ = 10; // Keep track of highest z-index | |
let openWindows = {}; // Track open window elements { appId: windowElement } | |
const getOpenWindows = () => Object.keys(openWindows); | |
const openApp = (appId, data = null) => { | |
const windowEl = document.getElementById(`${appId}-window`); | |
if (!windowEl) { | |
console.error(`Window element not found for app: ${appId}`); | |
return; | |
} | |
windowEl.classList.remove('hidden'); | |
bringToFront(appId); | |
playSound('audio-open'); | |
openWindows[appId] = windowEl; | |
if (window.innerWidth < 768 && !windowEl.classList.contains('maximized')) { | |
maximizeWindow(appId, false); | |
} | |
showNotification(`${appId} application launched`); | |
// App-specific opening logic | |
switch (appId) { | |
case 'terminal': Terminal.focusInput(); break; | |
case 'browser': Browser.loadInitial(); break; | |
case 'editor': Editor.open(data); break; // Pass file path if provided | |
case 'file-explorer': FileExplorer.render(); break; // Render current dir | |
case 'monitor': Monitor.start(); break; | |
case 'codebreaker': CodeBreaker.initGame(); break; | |
} | |
}; | |
const closeWindow = (appId) => { | |
const windowEl = document.getElementById(`${appId}-window`); | |
if (windowEl) { | |
windowEl.classList.add('hidden'); | |
windowEl.classList.remove('maximized'); | |
delete openWindows[appId]; // Remove from tracking | |
if (window.innerWidth >= 768) restoreOriginalStyle(windowEl); | |
playSound('audio-close'); | |
// App-specific closing logic | |
if (appId === 'monitor') Monitor.stop(); | |
if (appId === 'editor') Editor.close(); // Handle unsaved changes? | |
} | |
}; | |
const minimizeWindow = (appId) => { | |
// Simple close on mobile for now | |
if (window.innerWidth < 768) { | |
closeWindow(appId); | |
} else { | |
showNotification(`${appId} minimized (simulation)`); | |
// Could hide and add to taskbar state here | |
} | |
}; | |
const maximizeWindow = (appId, toggle = true) => { | |
const windowEl = document.getElementById(`${appId}-window`); | |
if (!windowEl) return; | |
if (toggle && windowEl.classList.contains('maximized')) { | |
// Restore | |
windowEl.classList.remove('maximized'); | |
if (window.innerWidth >= 768) restoreOriginalStyle(windowEl); | |
else windowEl.classList.remove('hidden'); // Ensure visible on mobile restore | |
} else { | |
// Maximize | |
if (toggle || !windowEl.classList.contains('maximized')) { | |
if (window.innerWidth >= 768) storeOriginalStyle(windowEl); | |
windowEl.classList.add('maximized'); | |
windowEl.style.top = '0'; windowEl.style.left = '0'; | |
windowEl.style.width = '100%'; windowEl.style.height = `calc(100% - 40px)`; | |
windowEl.style.position = 'fixed'; | |
bringToFront(appId); | |
} | |
} | |
}; | |
const bringToFront = (appId) => { | |
const windowEl = document.getElementById(`${appId}-window`); | |
if (windowEl) { | |
highestZ++; | |
windowEl.style.zIndex = highestZ; | |
windowEl.dataset.zIndex = highestZ; | |
} | |
}; | |
// --- Style Helpers for Maximize --- | |
const storeOriginalStyle = (windowEl) => { | |
const style = windowEl.getAttribute('style'); | |
if (style && !windowEl.dataset.originalStyle) { // Only store if not already stored | |
windowEl.dataset.originalStyle = style; | |
} | |
}; | |
const restoreOriginalStyle = (windowEl) => { | |
if (windowEl.dataset.originalStyle) { | |
windowEl.setAttribute('style', windowEl.dataset.originalStyle); | |
// Ensure z-index is restored correctly from dataset or highestZ | |
windowEl.style.zIndex = windowEl.dataset.zIndex || highestZ; | |
delete windowEl.dataset.originalStyle; // Clear stored style after restoring | |
} else { // Fallback needed if style wasn't stored | |
windowEl.style.position = 'absolute'; | |
windowEl.style.top = '5rem'; windowEl.style.left = '5rem'; | |
windowEl.style.width = '50%'; windowEl.style.height = '50%'; | |
windowEl.style.zIndex = windowEl.dataset.zIndex || highestZ; | |
} | |
}; | |
// --- Dragging Logic (Keep existing touch/mouse handler) --- | |
const makeWindowsDraggable = () => { | |
document.querySelectorAll('.window-header').forEach(header => { | |
// ... (Existing mousedown/touchstart -> mousemove/touchmove -> mouseup/touchend logic) ... | |
// Ensure bringToFront is called inside onDragStart | |
function onDragStart(e) { | |
const windowEl = this.parentElement; | |
if (window.innerWidth >= 768 && windowEl.classList.contains('maximized')) return; // Don't drag maximized | |
// ... rest of drag start logic ... | |
bringToFront(windowEl.id.split('-')[0]); // Critical line | |
// ... rest of drag start logic ... | |
} | |
// Remove previous listeners before adding new ones to prevent duplicates | |
header.removeEventListener('mousedown', header._dragStartHandler); | |
header.removeEventListener('touchstart', header._dragStartHandler); | |
header._dragStartHandler = onDragStart; // Store handler reference | |
header.addEventListener('mousedown', header._dragStartHandler); | |
header.addEventListener('touchstart', header._dragStartHandler, { passive: false }); | |
// ... rest of drag logic with onDragMove, onDragEnd ... | |
}); | |
}; | |
// --- Public API --- | |
return { | |
openApp, closeWindow, minimizeWindow, maximizeWindow, bringToFront, | |
makeWindowsDraggable, getOpenWindows | |
}; | |
})(); | |
// --- Terminal Module --- | |
const Terminal = (() => { | |
const terminalContent = document.getElementById('terminal-content'); | |
const outputArea = terminalContent?.querySelector('.output-area'); | |
let currentInput = null; | |
let commandHistory = []; | |
let historyIndex = -1; | |
const createNewPrompt = () => { | |
if (!terminalContent) return; | |
const newPromptLine = document.createElement('div'); | |
newPromptLine.className = 'prompt-line flex items-center mt-1'; | |
newPromptLine.innerHTML = ` | |
<span class="text-green-500" id="terminal-prompt">${getPromptText()}</span> | |
<input type="text" class="bg-transparent border-none outline-none flex-1 ml-2 text-green-500 terminal-input-field" autofocus autocomplete="off" spellcheck="false"> | |
<span class="cursor-blink">█</span>`; | |
terminalContent.appendChild(newPromptLine); | |
currentInput = newPromptLine.querySelector('.terminal-input-field'); | |
currentInput.addEventListener('keydown', handleKeyDown); | |
currentInput.focus(); | |
terminalContent.scrollTop = terminalContent.scrollHeight; | |
}; | |
const getPromptText = () => { | |
const cwd = VFS.getCurrentDirectory(); | |
// Replace /root with ~ for classic look | |
const displayPath = cwd.startsWith('/root') ? '~' + cwd.substring(5) : cwd; | |
return `root@xortron-os:<span class="text-blue-400">${displayPath}</span># `; | |
}; | |
const updatePrompt = () => { | |
const promptSpan = terminalContent?.querySelector('.prompt-line:last-child #terminal-prompt'); | |
if (promptSpan) { | |
promptSpan.innerHTML = getPromptText(); | |
} | |
}; | |
const handleKeyDown = (e) => { | |
if (e.key === 'Enter' && currentInput.value.trim() !== '') { | |
const command = currentInput.value.trim(); | |
commandHistory.push(command); | |
historyIndex = -1; | |
currentInput.disabled = true; // Disable old input | |
// Display command entered | |
const promptText = currentInput.previousElementSibling.innerHTML; // Get the prompt span HTML | |
const commandDiv = document.createElement('div'); | |
commandDiv.innerHTML = `${promptText}<span class="ml-2">${escapeHtml(command)}</span>`; // Use escaped command | |
outputArea.appendChild(commandDiv); | |
processCommand(command); // Process | |
createNewPrompt(); // Create new input line | |
} else if (e.key === 'ArrowUp') { /* History Up */ } | |
else if (e.key === 'ArrowDown') { /* History Down */ } | |
else if (e.key === 'Tab') { | |
e.preventDefault(); // Prevent focus change | |
handleTabCompletion(currentInput); | |
} | |
}; | |
const handleTabCompletion = (inputElement) => { | |
const text = inputElement.value; | |
const lastSpaceIndex = text.lastIndexOf(' ') + 1; | |
const currentWord = text.substring(lastSpaceIndex); | |
if (!currentWord) return; // Nothing to complete | |
const isPathCompletion = text.trim().split(' ').length > 1 || currentWord.includes('/'); // Crude check if likely a path | |
const currentPath = VFS.normalizePath(currentWord); | |
const parentPath = VFS.getParentPath(currentPath); | |
const partialName = VFS.getItemName(currentPath); | |
const itemsInDir = VFS.listDirectory(parentPath); | |
if (!itemsInDir) return; | |
const matches = itemsInDir.filter(item => item.name.startsWith(partialName)); | |
if (matches.length === 1) { | |
// Complete single match | |
const completion = matches[0].name + (matches[0].type === 'dir' ? '/' : ' '); | |
inputElement.value = text.substring(0, lastSpaceIndex) + VFS.joinPath(parentPath === '/' ? '' : parentPath, completion); // Build full path for completion | |
inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length); | |
} else if (matches.length > 1) { | |
// Show multiple matches | |
const output = document.createElement('div'); | |
output.className = 'text-gray-400'; | |
output.textContent = matches.map(m => m.name + (m.type === 'dir' ? '/' : '')).join(' '); | |
outputArea.appendChild(output); | |
playSound('audio-notify'); // Hint sound | |
} | |
}; | |
const processCommand = (command) => { | |
const output = document.createElement('div'); | |
output.className = 'mb-2 whitespace-pre-wrap'; | |
const args = command.match(/(?:[^\s"]+|"[^"]*")+/g) || []; // Handle quoted args basic | |
const cmd = args[0]?.toLowerCase(); | |
const cleanArgs = args.map(arg => arg.startsWith('"') && arg.endsWith('"') ? arg.slice(1, -1) : arg); // Remove quotes | |
const writeOutput = (text) => { | |
output.innerHTML = text; // Use innerHTML for potential formatting | |
outputArea.appendChild(output); | |
}; | |
const commands = { | |
'help': () => writeOutput(`Available: clear, ls, cd, pwd, cat, touch, mkdir, rm, echo, neo, nmap, decrypt, browser, theme, date, whoami, exit`), | |
'clear': () => { outputArea.innerHTML = ''; return; }, // Special case, no output div | |
'ls': (path = '.') => { | |
const targetPath = VFS.normalizePath(path || '.'); | |
const items = VFS.listDirectory(targetPath); | |
if (items) { | |
const listOutput = items.map(item => { | |
const itemPath = VFS.joinPath(targetPath, item.name); | |
const isDir = item.type === 'dir'; | |
const isEnc = item.name.endsWith('.enc'); | |
const color = isDir ? 'text-blue-400' : (isEnc ? 'text-red-500' : 'text-gray-300'); | |
const icon = isDir ? '<i class="fas fa-folder"></i>' : (isEnc ? '<i class="fas fa-lock"></i>' : '<i class="fas fa-file-alt"></i>'); | |
return `<span class="${color}">${icon} ${escapeHtml(item.name)}</span>`; | |
}).join('\n'); | |
writeOutput(listOutput || '(empty directory)'); | |
} else { | |
writeOutput(`ls: cannot access '${escapeHtml(path)}': No such file or directory`); | |
playSound('audio-error'); | |
} | |
}, | |
'cd': (path) => { | |
if (!path) { path = '/root'; } // Default cd goes to ~ (/root) | |
const targetPath = VFS.normalizePath(path); | |
if (VFS.setCurrentDirectory(targetPath)) { | |
// Success, prompt updates automatically via setCurrentDirectory | |
} else { | |
writeOutput(`cd: no such file or directory: ${escapeHtml(path)}`); | |
playSound('audio-error'); | |
} | |
}, | |
'pwd': () => writeOutput(VFS.getCurrentDirectory()), | |
'cat': (path) => { | |
if (!path) { writeOutput("cat: missing file operand"); playSound('audio-error'); return; } | |
const normPath = VFS.normalizePath(path); | |
if (VFS.isFile(normPath)) { | |
if (normPath.endsWith('.enc')) { | |
writeOutput(`cat: ${escapeHtml(VFS.getItemName(normPath))} is encrypted. Use 'decrypt'.`); | |
playSound('audio-error'); | |
} else { | |
writeOutput(escapeHtml(VFS.readFile(normPath))); | |
} | |
} else if (VFS.isDirectory(normPath)) { | |
writeOutput(`cat: ${escapeHtml(VFS.getItemName(normPath))}: Is a directory`); | |
playSound('audio-error'); | |
} else { | |
writeOutput(`cat: ${escapeHtml(path)}: No such file or directory`); | |
playSound('audio-error'); | |
} | |
}, | |
'touch': (path) => { | |
if (!path) { writeOutput("touch: missing file operand"); playSound('audio-error'); return; } | |
const normPath = VFS.normalizePath(path); | |
if (VFS.createFile(normPath)) { | |
// Silent success | |
} else if (VFS.exists(normPath)) { | |
// File exists, update timestamp (simulation - do nothing here) | |
} else { | |
writeOutput(`touch: cannot touch '${escapeHtml(path)}': No such file or directory or invalid path`); | |
playSound('audio-error'); | |
} | |
FileExplorer.render(); // Update file explorer view | |
}, | |
'mkdir': (path) => { | |
if (!path) { writeOutput("mkdir: missing operand"); playSound('audio-error'); return; } | |
const normPath = VFS.normalizePath(path); | |
if (VFS.createDirectory(normPath)) { | |
// Silent success | |
} else if (VFS.exists(normPath)) { | |
writeOutput(`mkdir: cannot create directory ‘${escapeHtml(path)}’: File exists`); | |
playSound('audio-error'); | |
} else { | |
writeOutput(`mkdir: cannot create directory ‘${escapeHtml(path)}’: No such file or directory or invalid path`); | |
playSound('audio-error'); | |
} | |
FileExplorer.render(); // Update file explorer view | |
}, | |
'rm': (path) => { | |
if (!path) { writeOutput("rm: missing operand"); playSound('audio-error'); return; } | |
const normPath = VFS.normalizePath(path); | |
if (!VFS.exists(normPath)) { | |
writeOutput(`rm: cannot remove '${escapeHtml(path)}': No such file or directory`); | |
playSound('audio-error'); return; | |
} | |
if (VFS.isDirectory(normPath)) { | |
writeOutput(`rm: cannot remove '${escapeHtml(path)}': Is a directory (use rmdir or rm -r)`); | |
playSound('audio-error'); return; | |
} | |
if (VFS.deleteItem(normPath)) { | |
// Silent success | |
} else { | |
writeOutput(`rm: cannot remove '${escapeHtml(path)}': Operation failed`); // Generic failure | |
playSound('audio-error'); | |
} | |
FileExplorer.render(); // Update file explorer view | |
}, | |
'echo': () => writeOutput(escapeHtml(cleanArgs.slice(1).join(' '))), | |
'neo': () => writeOutput(`<pre class="text-sm leading-tight"> | |
<span class="text-cyan-400">/////////////</span> <span class="text-green-500">root@xortron-os</span> | |
<span class="text-cyan-400">/////////////////</span> ----------------- | |
<span class="text-cyan-400">///////</span><span class="text-purple-400">///////</span><span class="text-cyan-400">///////</span> OS: <span class="text-white">XortronOS v8.1 Cyberpunk</span> | |
<span class="text-cyan-400">//////</span><span class="text-purple-400">///////////</span><span class="text-cyan-400">//////</span> Kernel: <span class="text-white">7.7.7-xanmod-xos</span> | |
<span class="text-cyan-400">//////</span><span class="text-purple-400">///////////</span><span class="text-cyan-400">//////</span> Uptime: <span class="text-white">42d 13h 37m</span> | |
<span class="text-cyan-400">///////</span><span class="text-purple-400">///////</span><span class="text-cyan-400">///////</span> Shell: <span class="text-white">zsh (hax0r edition)</span> | |
<span class="text-cyan-400">/////////////////</span> Resolution: <span class="text-white">${window.screen.width}x${window.screen.height}</span> | |
<span class="text-cyan-400">/////////////</span> WM: <span class="text-white">XorgWM (GPU Accelerated)</span> | |
Theme: <span class="text-white">CyberNeon [GTK3]</span> | |
Icons: <span class="text-white">XOS-Icons [GTK3]</span> | |
CPU: <span class="text-white">Quantum Entangler @ 10 THz (16 Cores)</span> | |
GPU: <span class="text-white">Nvidia RTX 9090 Ti CyberDrive</span> | |
Memory: <span class="text-white">128GiB / 256GiB DDR9</span></pre>`), | |
'nmap': (target = 'scanme.nmap.org') => { | |
writeOutput(`Starting Nmap 9.99 ( https://nmap.org ) at ${new Date().toISOString()} | |
Nmap scan report for ${escapeHtml(target)} (192.168.1.X) | |
Host is up (0.01s latency). | |
Not shown: 990 filtered tcp ports (no-response) | |
PORT STATE SERVICE VERSION | |
22/tcp open ssh OpenSSH 9.8p1 (protocol 2.0) | |
80/tcp open http nginx 1.27.1 | |
135/tcp open msrpc Microsoft Windows RPC | |
443/tcp open ssl/http nginx 1.27.1 | |
445/tcp open microsoft-ds Windows 10 SMB (unsafe!) | |
3306/tcp open mysql MySQL 8.0.32 (Bruteforce possible) | |
3389/tcp open ms-wbt-server Microsoft Terminal Services (RDP) | |
5900/tcp open vnc RealVNC Enterprise 6.x (Weak Auth) | |
8080/tcp closed http-proxy | |
Nmap done: 1 IP address (1 host up) scanned in 3.50 seconds`); | |
}, | |
'decrypt': (path) => { | |
if (!path) { writeOutput("decrypt: missing file operand"); playSound('audio-error'); return; } | |
const normPath = VFS.normalizePath(path); | |
if (VFS.isFile(normPath) && normPath.endsWith('.enc')) { | |
let progress = 0; | |
const interval = setInterval(() => { | |
progress += Math.random() * 15; | |
if (progress >= 100) { | |
clearInterval(interval); | |
const decryptedName = normPath.replace('.enc', ''); | |
VFS.writeFile(decryptedName, `--- DECRYPTED DATA ---\nOriginal File: ${VFS.getItemName(normPath)}\nContent simulation: Access codes, coordinates, secrets...\n--- END DECRYPTION ---`); | |
VFS.deleteItem(normPath); // Delete encrypted version | |
writeOutput(`Decrypting ${escapeHtml(VFS.getItemName(normPath))}... [##########] 100% Done.\nDecrypted content saved to ${escapeHtml(VFS.getItemName(decryptedName))}`); | |
playSound('audio-success'); | |
FileExplorer.render(); // Update file explorer | |
} else { | |
writeOutput(`Decrypting ${escapeHtml(VFS.getItemName(normPath))}... [${'#'.repeat(Math.floor(progress / 10)).padEnd(10, ' ')}] ${Math.floor(progress)}%`); | |
} | |
}, 200); | |
} else { | |
writeOutput(`decrypt: file '${escapeHtml(path)}' not found or not encrypted (.enc)`); | |
playSound('audio-error'); | |
} | |
}, | |
// Keep other commands: browser, theme, date, whoami, exit | |
'browser': () => { AppManager.openApp('browser'); return writeOutput('Launching DarkBrowser...'); }, | |
'theme': (color) => { /* Keep theme logic */ }, | |
'date': () => writeOutput(new Date().toString()), | |
'whoami': () => writeOutput('root'), | |
'exit': () => { setTimeout(() => AppManager.closeWindow('terminal'), 300); return writeOutput('Closing terminal session...'); } | |
}; | |
if (commands[cmd]) { | |
commands[cmd](cleanArgs[1], cleanArgs.slice(2)); // Pass args | |
} else { | |
writeOutput(`XortronOS: command not found: ${escapeHtml(cmd)}`); | |
playSound('audio-error'); | |
} | |
// Scroll after potential output | |
terminalContent.scrollTop = terminalContent.scrollHeight; | |
}; | |
const focusInput = () => { | |
setTimeout(() => currentInput?.focus(), 50); | |
}; | |
const escapeHtml = (unsafe) => { | |
if (typeof unsafe !== 'string') return unsafe; | |
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); | |
} | |
// Initial setup | |
const init = () => { | |
const firstInput = terminalContent?.querySelector('.terminal-input-field'); | |
if (firstInput) { | |
currentInput = firstInput; | |
currentInput.addEventListener('keydown', handleKeyDown); | |
} else { | |
createNewPrompt(); // Create if not present initially | |
} | |
updatePrompt(); // Set initial prompt text | |
}; | |
return { init, focusInput, updatePrompt }; | |
})(); | |
// --- Editor Module --- | |
const Editor = (() => { | |
const windowEl = document.getElementById('editor-window'); | |
const textarea = document.getElementById('editor-textarea'); | |
const titleEl = document.getElementById('editor-title'); | |
const statusbar = document.getElementById('editor-statusbar'); | |
let currentFilePath = null; | |
let originalContent = ''; | |
let unsavedChanges = false; | |
const open = (filePath) => { | |
if (!windowEl || !textarea || !titleEl || !statusbar) return; | |
currentFilePath = filePath ? VFS.normalizePath(filePath) : null; | |
if (currentFilePath && VFS.isFile(currentFilePath)) { | |
originalContent = VFS.readFile(currentFilePath) || ''; | |
textarea.value = originalContent; | |
titleEl.textContent = `Editor - ${VFS.getItemName(currentFilePath)}`; | |
statusbar.textContent = `Opened: ${currentFilePath}`; | |
} else { | |
// New file or invalid path | |
originalContent = ''; | |
textarea.value = ''; | |
currentFilePath = null; // Explicitly null for new file | |
titleEl.textContent = 'Editor - New File'; | |
statusbar.textContent = 'New file. Use Save button or terminal "touch" first.'; | |
} | |
unsavedChanges = false; | |
textarea.oninput = () => { | |
unsavedChanges = (textarea.value !== originalContent); | |
statusbar.textContent = currentFilePath ? `${currentFilePath} ${unsavedChanges ? '[Modified]' : '[Saved]'}` : `New File ${unsavedChanges ? '[Modified]' : ''}`; | |
}; | |
}; | |
const save = () => { | |
if (!currentFilePath) { | |
// Handle saving a new file - needs a prompt or default location | |
const newPath = prompt("Save As (full path, e.g., /root/Documents/new.txt):", VFS.getCurrentDirectory() + '/untitled.txt'); | |
if (!newPath) return; // User cancelled | |
currentFilePath = VFS.normalizePath(newPath); | |
if (VFS.exists(currentFilePath) && !confirm("File exists. Overwrite?")) { | |
currentFilePath = null; // Reset if overwrite cancelled | |
return; | |
} | |
// Attempt to create parent directories if needed (basic) | |
const parentDir = VFS.getParentPath(currentFilePath); | |
if (!VFS.exists(parentDir)) VFS.createDirectory(parentDir); // Naive creation | |
} | |
if (VFS.writeFile(currentFilePath, textarea.value)) { | |
originalContent = textarea.value; | |
unsavedChanges = false; | |
statusbar.textContent = `Saved: ${currentFilePath}`; | |
titleEl.textContent = `Editor - ${VFS.getItemName(currentFilePath)}`; | |
showNotification(`File saved: ${currentFilePath}`); | |
playSound('audio-success'); | |
FileExplorer.render(); // Update file explorer in case it's a new file | |
} else { | |
statusbar.textContent = `Error saving: ${currentFilePath}`; | |
showNotification(`Error: Could not save file to ${currentFilePath}`); | |
playSound('audio-error'); | |
} | |
}; | |
const close = () => { | |
// Basic close, could add unsaved changes check later | |
if (unsavedChanges) { | |
// Simple alert for now | |
// alert("You have unsaved changes!"); | |
// A more integrated dialog would be better | |
} | |
currentFilePath = null; | |
textarea.value = ''; | |
originalContent = ''; | |
unsavedChanges = false; | |
}; | |
return { open, save, close }; | |
})(); | |
// --- File Explorer Module --- | |
const FileExplorer = (() => { | |
const windowEl = document.getElementById('file-explorer-window'); | |
const gridEl = document.getElementById('file-explorer-grid'); | |
const pathEl = document.getElementById('file-explorer-path'); | |
let currentPath = '/root'; // Start in user's home | |
let selectedItem = null; | |
const render = (path = currentPath) => { | |
if (!windowEl || !gridEl || !pathEl) return; | |
currentPath = VFS.normalizePath(path); | |
pathEl.textContent = currentPath; | |
gridEl.innerHTML = ''; // Clear current view | |
selectedItem = null; // Clear selection | |
const items = VFS.listDirectory(currentPath); | |
if (!items) { | |
gridEl.innerHTML = '<p class="text-gray-400 col-span-full">Error listing directory.</p>'; | |
return; | |
} | |
if (items.length === 0) { | |
gridEl.innerHTML = '<p class="text-gray-400 col-span-full">(Directory is empty)</p>'; | |
} | |
items.forEach(item => { | |
const itemPath = VFS.joinPath(currentPath, item.name); | |
const div = document.createElement('div'); | |
div.className = 'file-item file text-center p-2 rounded hover:bg-[--primary]/20 focus:bg-[--primary]/30 outline-none'; | |
div.setAttribute('tabindex', '0'); // Make focusable | |
div.dataset.path = itemPath; | |
div.dataset.type = item.type; | |
let iconClass = 'fa-file-alt text-gray-400'; // Default file | |
if (item.type === 'dir') iconClass = 'fa-folder text-yellow-500'; | |
else if (item.name.endsWith('.py') || item.name.endsWith('.js')) iconClass = 'fa-file-code text-blue-400'; | |
else if (item.name.endsWith('.zip') || item.name.endsWith('.tar')) iconClass = 'fa-file-archive text-purple-400'; | |
else if (item.name.endsWith('.png') || item.name.endsWith('.jpg')) iconClass = 'fa-file-image text-pink-400'; | |
else if (item.name.endsWith('.enc')) iconClass = 'fa-lock text-red-500'; | |
div.innerHTML = ` | |
<div class="text-3xl mb-1"><i class="fas ${iconClass}"></i></div> | |
<div class="text-xs truncate">${Terminal.escapeHtml(item.name)}</div> | |
`; | |
// --- Event Listeners --- | |
div.addEventListener('dblclick', () => handleItemOpen(itemPath, item.type)); | |
div.addEventListener('click', () => handleItemSelect(div, itemPath)); | |
div.addEventListener('keydown', (e) => { // Keyboard nav/open | |
if (e.key === 'Enter') handleItemOpen(itemPath, item.type); | |
// Add arrow key navigation later if needed | |
}); | |
gridEl.appendChild(div); | |
}); | |
}; | |
const handleItemSelect = (element, path) => { | |
// Remove selection from previous item | |
if (selectedItem && selectedItem.element) { | |
selectedItem.element.classList.remove('bg-[--primary]/30'); | |
} | |
// Set new selection | |
element.classList.add('bg-[--primary]/30'); | |
selectedItem = { element, path }; | |
}; | |
const handleItemOpen = (path, type) => { | |
if (type === 'dir') { | |
render(path); // Navigate into directory | |
} else if (type === 'file') { | |
if (path.endsWith('.txt') || path.endsWith('.py') || path.endsWith('.js') || path.endsWith('.md') || !path.includes('.')) { // Open text-like files in editor | |
AppManager.openApp('editor', path); | |
} else if (path.endsWith('.enc')) { | |
showNotification(`File ${VFS.getItemName(path)} is encrypted. Use terminal 'decrypt' command.`); | |
playSound('audio-error'); | |
} else { | |
showNotification(`No application available to open ${VFS.getItemName(path)}.`); | |
playSound('audio-notify'); | |
} | |
} | |
}; | |
const navigateBack = () => { | |
const parentPath = VFS.getParentPath(currentPath); | |
render(parentPath); | |
}; | |
const createNew = (type) => { | |
const name = prompt(`Enter name for new ${type}:`); | |
if (!name || !name.trim()) return; | |
const newPath = VFS.joinPath(currentPath, name.trim()); | |
if (VFS.exists(newPath)) { | |
showNotification(`${type} '${name}' already exists.`); | |
playSound('audio-error'); return; | |
} | |
let success = false; | |
if (type === 'folder') { | |
success = VFS.createDirectory(newPath); | |
} else if (type === 'file') { | |
success = VFS.createFile(newPath, ''); // Create empty file | |
} | |
if (success) { | |
render(); // Refresh view | |
showNotification(`${type} '${name}' created.`); | |
playSound('audio-success'); | |
} else { | |
showNotification(`Failed to create ${type}.`); | |
playSound('audio-error'); | |
} | |
}; | |
const deleteSelected = () => { | |
if (!selectedItem) { | |
showNotification("No item selected to delete."); | |
playSound('audio-notify'); return; | |
} | |
const path = selectedItem.path; | |
const itemName = VFS.getItemName(path); | |
const itemType = VFS.isDirectory(path) ? 'directory' : 'file'; | |
if (!confirm(`Are you sure you want to delete ${itemType} "${itemName}"?`)) { | |
return; | |
} | |
const result = VFS.deleteItem(path); | |
if (result === true) { | |
render(); // Refresh view | |
showNotification(`${itemType} '${itemName}' deleted.`); | |
playSound('audio-success'); | |
} else if (result === 'dir_not_empty') { | |
showNotification(`Directory '${itemName}' is not empty. Cannot delete.`); | |
playSound('audio-error'); | |
} else { | |
showNotification(`Failed to delete '${itemName}'.`); | |
playSound('audio-error'); | |
} | |
}; | |
return { render, navigateBack, createNew, deleteSelected }; | |
})(); | |
// --- System Monitor Module --- | |
const Monitor = (() => { | |
let intervalId = null; | |
const cpuText = document.getElementById('monitor-cpu-text'); | |
const cpuBar = document.getElementById('monitor-cpu-bar'); | |
const ramText = document.getElementById('monitor-ram-text'); | |
const ramBar = document.getElementById('monitor-ram-bar'); | |
const processList = document.getElementById('monitor-process-list'); | |
const update = () => { | |
if (!cpuText || !cpuBar || !ramText || !ramBar || !processList) { stop(); return; } // Stop if elements missing | |
// Simulate CPU (more spikes) | |
const cpuUsage = Math.min(100, Math.max(5, Math.random() * 20 + (Math.random() > 0.8 ? Math.random() * 60 : 0))); | |
cpuText.textContent = `${cpuUsage.toFixed(1)}%`; | |
cpuBar.style.width = `${cpuUsage}%`; | |
// Simulate RAM | |
const ramUsage = Math.min(16384, Math.max(2048, Math.random() * 8192 + 2048)); | |
const ramPercent = (ramUsage / 16384) * 100; | |
ramText.textContent = `${ramUsage.toFixed(0)} MB / 16384 MB`; | |
ramBar.style.width = `${ramPercent}%`; | |
// Update Process List | |
processList.innerHTML = ''; // Clear previous list | |
const openApps = AppManager.getOpenWindows(); | |
openApps.forEach(appId => { | |
const procDiv = document.createElement('div'); | |
procDiv.className = 'process-list-item'; | |
const nameSpan = document.createElement('span'); | |
nameSpan.textContent = `${appId}.exe`; // Simulate process name | |
const cpuSpan = document.createElement('span'); | |
cpuSpan.textContent = `CPU: ${(Math.random() * (cpuUsage / openApps.length)).toFixed(1)}%`; // Distribute some CPU | |
cpuSpan.className = 'text-xs text-gray-400'; | |
procDiv.appendChild(nameSpan); | |
procDiv.appendChild(cpuSpan); | |
processList.appendChild(procDiv); | |
}); | |
// Add some fake system processes | |
['xkernel.sys', 'svchost.exe', 'dwm.exe', 'explorer.xos'].forEach(p => { | |
const procDiv = document.createElement('div'); | |
procDiv.className = 'process-list-item'; | |
procDiv.innerHTML = `<span>${p}</span><span class="text-xs text-gray-400">CPU: ${(Math.random() * 2).toFixed(1)}%</span>`; | |
processList.appendChild(procDiv); | |
}); | |
}; | |
const start = () => { | |
if (!intervalId) { | |
update(); // Initial update | |
intervalId = setInterval(update, 2000); // Update every 2 seconds | |
} | |
}; | |
const stop = () => { | |
clearInterval(intervalId); | |
intervalId = null; | |
}; | |
return { start, stop }; | |
})(); | |
// --- Code Breaker Game Module --- | |
const CodeBreaker = (() => { | |
const windowEl = document.getElementById('codebreaker-window'); | |
const gameBoard = document.getElementById('codebreaker-game-board'); | |
const palette = document.getElementById('codebreaker-color-palette'); | |
const submitButton = document.getElementById('codebreaker-submit'); | |
const messageEl = document.getElementById('codebreaker-message'); | |
const restartButton = document.getElementById('codebreaker-restart'); | |
const COLORS = ['#EF5350', '#66BB6A', '#42A5F5', '#FFEE58', '#EC407A', '#26C6DA']; // Red, Green, Blue, Yellow, Magenta, Cyan | |
const CODE_LENGTH = 4; | |
const MAX_GUESSES = 10; | |
let secretCode = []; | |
let guesses = []; | |
let currentGuess = Array(CODE_LENGTH).fill(null); | |
let currentRowIndex = 0; | |
let selectedColor = COLORS[0]; | |
let gameOver = false; | |
const initGame = () => { | |
if (!gameBoard || !palette || !submitButton || !messageEl) return; | |
secretCode = generateSecretCode(); | |
guesses = []; | |
currentRowIndex = 0; | |
gameOver = false; | |
messageEl.textContent = ''; | |
restartButton.classList.add('hidden'); | |
// Setup Palette | |
palette.innerHTML = ''; | |
COLORS.forEach((color, index) => { | |
const colorDiv = document.createElement('div'); | |
colorDiv.className = 'palette-color'; | |
colorDiv.style.backgroundColor = color; | |
if (index === 0) { | |
colorDiv.classList.add('selected'); | |
selectedColor = color; | |
} | |
colorDiv.onclick = () => selectColor(color, colorDiv); | |
palette.appendChild(colorDiv); | |
}); | |
// Setup Board | |
gameBoard.innerHTML = ''; | |
for (let i = 0; i < MAX_GUESSES; i++) { | |
gameBoard.appendChild(createGuessRow(i)); | |
} | |
updateSubmitButtonState(); | |
// Add event listener for restart | |
restartButton.onclick = initGame; | |
console.log("Secret Code (for debugging):", secretCode); // Debugging | |
}; | |
const generateSecretCode = () => { | |
const code = []; | |
for (let i = 0; i < CODE_LENGTH; i++) { | |
code.push(COLORS[Math.floor(Math.random() * COLORS.length)]); | |
} | |
return code; | |
}; | |
const createGuessRow = (rowIndex) => { | |
const rowDiv = document.createElement('div'); | |
rowDiv.className = 'codebreaker-row'; | |
rowDiv.dataset.rowIndex = rowIndex; | |
// Guess Pegs | |
for (let i = 0; i < CODE_LENGTH; i++) { | |
const peg = document.createElement('div'); | |
peg.className = 'codebreaker-guess-peg'; | |
peg.dataset.pegIndex = i; | |
peg.onclick = () => placeColor(rowIndex, i, peg); | |
rowDiv.appendChild(peg); | |
} | |
// Feedback Area | |
const feedbackArea = document.createElement('div'); | |
feedbackArea.className = 'codebreaker-feedback-area'; | |
// Add placeholder feedback pegs | |
for (let i = 0; i < CODE_LENGTH; i++) { | |
const fbPeg = document.createElement('div'); | |
fbPeg.className = 'codebreaker-feedback-peg'; | |
feedbackArea.appendChild(fbPeg); | |
} | |
rowDiv.appendChild(feedbackArea); | |
return rowDiv; | |
}; | |
const selectColor = (color, element) => { | |
if (gameOver) return; | |
selectedColor = color; | |
document.querySelectorAll('.palette-color.selected').forEach(el => el.classList.remove('selected')); | |
element.classList.add('selected'); | |
}; | |
const placeColor = (rowIndex, pegIndex, pegElement) => { | |
if (gameOver || rowIndex !== currentRowIndex) return; // Only allow current row | |
currentGuess[pegIndex] = selectedColor; | |
pegElement.style.backgroundColor = selectedColor; | |
updateSubmitButtonState(); | |
}; | |
const updateSubmitButtonState = () => { | |
submitButton.disabled = gameOver || currentGuess.some(color => color === null); | |
}; | |
const submitGuess = () => { | |
if (gameOver || submitButton.disabled) return; | |
guesses.push([...currentGuess]); // Store a copy | |
const feedback = checkGuess(currentGuess); | |
displayFeedback(currentRowIndex, feedback); | |
// Disable current row pegs | |
document.querySelectorAll(`.codebreaker-row[data-row-index="${currentRowIndex}"] .codebreaker-guess-peg`).forEach(peg => peg.onclick = null); | |
if (feedback.black === CODE_LENGTH) { | |
// Win condition | |
messageEl.textContent = `Code Cracked in ${currentRowIndex + 1} guesses!`; | |
messageEl.className = 'mt-4 text-center font-bold text-green-400'; | |
gameOver = true; | |
restartButton.classList.remove('hidden'); | |
playSound('audio-success'); | |
} else if (currentRowIndex >= MAX_GUESSES - 1) { | |
// Lose condition | |
messageEl.textContent = `Game Over! Code was:`; | |
messageEl.className = 'mt-4 text-center font-bold text-red-400'; | |
displaySecretCode(); // Show the answer | |
gameOver = true; | |
restartButton.classList.remove('hidden'); | |
playSound('audio-error'); | |
} else { | |
// Continue to next row | |
currentRowIndex++; | |
currentGuess = Array(CODE_LENGTH).fill(null); // Reset for next guess | |
} | |
updateSubmitButtonState(); | |
}; | |
const checkGuess = (guess) => { | |
let blackPegs = 0; | |
let whitePegs = 0; | |
let secretCopy = [...secretCode]; | |
let guessCopy = [...guess]; | |
// First pass for black pegs (correct color and position) | |
for (let i = CODE_LENGTH - 1; i >= 0; i--) { | |
if (guessCopy[i] === secretCopy[i]) { | |
blackPegs++; | |
secretCopy.splice(i, 1); // Remove matched item | |
guessCopy.splice(i, 1); | |
} | |
} | |
// Second pass for white pegs (correct color, wrong position) | |
for (let i = guessCopy.length - 1; i >= 0; i--) { | |
const indexInSecret = secretCopy.indexOf(guessCopy[i]); | |
if (indexInSecret !== -1) { | |
whitePegs++; | |
secretCopy.splice(indexInSecret, 1); // Remove matched item from secret | |
} | |
} | |
return { black: blackPegs, white: whitePegs }; | |
}; | |
const displayFeedback = (rowIndex, feedback) => { | |
const feedbackArea = document.querySelector(`.codebreaker-row[data-row-index="${rowIndex}"] .codebreaker-feedback-area`); | |
const pegs = feedbackArea.querySelectorAll('.codebreaker-feedback-peg'); | |
let pegIndex = 0; | |
for (let i = 0; i < feedback.black; i++) { | |
if (pegs[pegIndex]) pegs[pegIndex].className = 'codebreaker-feedback-peg feedback-black'; | |
pegIndex++; | |
} | |
for (let i = 0; i < feedback.white; i++) { | |
if (pegs[pegIndex]) pegs[pegIndex].className = 'codebreaker-feedback-peg feedback-white'; | |
pegIndex++; | |
} | |
}; | |
const displaySecretCode = () => { | |
const secretDiv = document.createElement('div'); | |
secretDiv.className = 'flex gap-2 mt-1'; | |
secretCode.forEach(color => { | |
const peg = document.createElement('div'); | |
peg.style.width = '20px'; peg.style.height = '20px'; | |
peg.style.backgroundColor = color; peg.style.borderRadius = '50%'; | |
secretDiv.appendChild(peg); | |
}); | |
messageEl.appendChild(secretDiv); | |
}; | |
// Attach submit listener | |
submitButton.onclick = submitGuess; | |
return { initGame }; | |
})(); | |
// --- Browser Module (Simple Enhancements) --- | |
const Browser = (() => { | |
const iframe = document.getElementById('browser-frame'); | |
const urlInput = document.getElementById('browser-url'); | |
const loadInitial = () => { | |
if (iframe && iframe.src === 'about:blank') { | |
// Load a custom internal start page instead | |
iframe.srcdoc = ` | |
<html style="background-color: #050508; color: #e0e0ff; font-family: 'Share Tech Mono', monospace; padding: 20px;"> | |
<head><title>XOS DarkBrowser</title></head> | |
<body> | |
<h1 style="font-family: 'Orbitron', sans-serif; color: var(--primary); text-shadow: 0 0 5px var(--primary);">Xortron DarkBrowser</h1> | |
<p>Welcome, agent. Use the URL bar above.</p> | |
<p>Bookmarks:</p> | |
<ul> | |
<li><a href="#" onclick="parent.Browser.navigate('https://github.com'); return false;">GitHub (Clearnet)</a></li> | |
<li><a href="#" onclick="parent.Browser.navigate('xos://intel-feed'); return false;">XOS Intel Feed (Internal)</a></li> | |
<li><a href="#" onclick="parent.Browser.navigate('xos://black-market-hub'); return false;">Black Market Hub (Simulated)</a></li> | |
</ul> | |
</body></html>`; | |
urlInput.value = 'xos://darkbrowser/home'; | |
} | |
}; | |
const navigate = (url) => { | |
if (!iframe || !urlInput) return; | |
// Simple internal page simulation | |
if (url === 'xos://intel-feed') { | |
iframe.srcdoc = `<html style="background-color: black; color: lime; font-family: monospace; padding: 10px; font-size: 12px;"><head><title>Intel Feed</title></head><body><h1>[CLASSIFIED] Intel Feed</h1><p>Timestamp: ${new Date().toISOString()}</p><pre>TARGET ACQUIRED: Project Chimera server farm (Coordinates: XX.XXX, YY.YYY)\nVULNERABILITY DETECTED: Zero-day exploit in firewall firmware v3.1\nASSET TRANSFER: Package ZETA en route to safe house GAMMA.\nCOUNTER-INTEL: Operation NIGHTFALL compromised. Agent SILAS extracted.</pre></body></html>`; | |
urlInput.value = url; | |
} else if (url === 'xos://black-market-hub') { | |
iframe.srcdoc = `<html style="background-color: #1a001a; color: #ff00ff; font-family: monospace; padding: 10px;"><head><title>Black Market</title></head><body><h1>The Shadow Market</h1><p>Listings:</p><ul><li>Zero-Day Exploits (Inquire)</li><li>Stolen Credentials (Bulk Available)</li><li>Botnet Rental (DDOS Services)</li><li>Custom Malware Development</li></ul><p style="color: red;">WARNING: Monitored Network. Use caution.</p></body></html>`; | |
urlInput.value = url; | |
} else { | |
// Treat as external URL | |
if (!url.includes('://')) url = 'https://' + url; | |
iframe.src = url; // Navigate external | |
urlInput.value = url; | |
} | |
}; | |
// Expose navigate for the srcdoc links | |
window.Browser = { navigate }; | |
return { loadInitial, navigate }; | |
})(); | |
// --- Utility Functions --- | |
function playSound(id) { /* ... keep existing sound function ... */ } | |
function showNotification(message) { /* ... keep existing notification function ... */ } | |
function hideNotification() { /* ... keep existing notification function ... */ } | |
function updateClock() { /* ... keep existing clock function ... */ } | |
function changeTheme(color) { /* ... keep existing theme function ... */ } | |
function toggleMatrix(enable) { /* ... keep existing matrix function ... */ } | |
function setupMatrix() { /* ... keep existing matrix setup ... */ } | |
function resetXortronOS() { | |
if (confirm("Confirm Reset? This will erase all files and settings.")) { | |
VFS.reset(); // This handles clearing localStorage and reloading defaults | |
// Optionally close all windows except settings? | |
// AppManager.getOpenWindows().forEach(id => { if (id !== 'settings') AppManager.closeWindow(id); }); | |
showNotification("System Reset Complete."); | |
} | |
} | |
// --- Initialization --- | |
document.addEventListener('DOMContentLoaded', () => { | |
// F: Restore theme from localStorage | |
const savedTheme = localStorage.getItem('themeColor'); | |
if (savedTheme) changeTheme(savedTheme); | |
// F: Restore matrix state from localStorage | |
const matrixEnabled = localStorage.getItem('matrixEnabled'); | |
toggleMatrix(matrixEnabled !== 'false'); // Enable by default | |
Terminal.init(); // Initialize Terminal first as VFS relies on it implicitly via CWD | |
FileExplorer.render(); // Initial render | |
AppManager.makeWindowsDraggable(); // Setup dragging for all window headers | |
setInterval(updateClock, 1000); updateClock(); // Start clock | |
// Start random notifications (moved here) | |
const systemMessages = [ /* ... */ ]; | |
setInterval(() => { /* ... random notification logic ... */ }, 45000); | |
// Debounced resize handler (keep existing logic) | |
let resizeTimeout; | |
window.addEventListener('resize', () => { /* ... existing resize logic ... */ }); | |
// Open browser by default (optional) | |
// AppManager.openApp('browser'); | |
console.log("XortronOS v8 Initialized. Welcome, agent."); | |
showNotification("XortronOS v8 Boot Complete."); | |
}); | |
</script> | |
</body> | |
</html> |