btop and reactive theme in neofetch

This commit is contained in:
Michael Freno
2025-12-31 12:52:26 -05:00
parent ad287a2be0
commit a6417c650f
5 changed files with 279 additions and 7 deletions

247
src/components/Btop.tsx Normal file
View File

@@ -0,0 +1,247 @@
import { createSignal, onMount, onCleanup, Show } from "solid-js";
interface BtopProps {
onClose: () => void;
}
export function Btop(props: BtopProps) {
const [cpuUsage, setCpuUsage] = createSignal(64);
const [memUsage, setMemUsage] = createSignal(80);
const [netDown, setNetDown] = createSignal(404);
const [netUp, setNetUp] = createSignal(0);
const [processes, setProcesses] = createSignal([
{
pid: 404,
name: "error-handler",
cpu: 32.0,
mem: 15.2,
status: "Running"
},
{
pid: 128,
name: "glitch-generator",
cpu: 18.4,
mem: 8.7,
status: "Running"
},
{ pid: 256, name: "terminal-shell", cpu: 8.2, mem: 4.1, status: "Running" },
{
pid: 512,
name: "random-process",
cpu: 5.4,
mem: 2.3,
status: "Sleeping"
},
{ pid: 1024, name: "mystery-daemon", cpu: 0.0, mem: 0.0, status: "???" }
]);
const [isMobile, setIsMobile] = createSignal(false);
onMount(() => {
// Check if mobile
if (typeof window !== "undefined") {
setIsMobile(window.innerWidth < 768);
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener("resize", handleResize);
onCleanup(() => window.removeEventListener("resize", handleResize));
}
// Animate CPU usage
const cpuInterval = setInterval(() => {
setCpuUsage((prev) => {
const change = (Math.random() - 0.5) * 10;
const newVal = Math.max(30, Math.min(95, prev + change));
return Math.round(newVal);
});
}, 1000);
// Animate memory usage
const memInterval = setInterval(() => {
setMemUsage((prev) => {
const change = (Math.random() - 0.5) * 5;
const newVal = Math.max(60, Math.min(90, prev + change));
return Math.round(newVal);
});
}, 1500);
// Animate network
const netInterval = setInterval(() => {
setNetDown(Math.floor(Math.random() * 1000));
setNetUp(Math.floor(Math.random() * 100));
}, 800);
// Animate processes
const procInterval = setInterval(() => {
setProcesses((prev) =>
prev.map((proc) => ({
...proc,
cpu:
proc.name === "mystery-daemon"
? 0.0
: Math.max(0, proc.cpu + (Math.random() - 0.5) * 5),
mem:
proc.name === "mystery-daemon"
? 0.0
: Math.max(0, proc.mem + (Math.random() - 0.5) * 2)
}))
);
}, 2000);
// Keyboard handler for :q
const handleKeyPress = (e: KeyboardEvent) => {
if (!isMobile() && e.key === "q" && e.shiftKey && e.key === ":") {
props.onClose();
}
// Simple 'q' press to quit
if (!isMobile() && e.key === "q") {
props.onClose();
}
};
window.addEventListener("keydown", handleKeyPress);
onCleanup(() => {
clearInterval(cpuInterval);
clearInterval(memInterval);
clearInterval(netInterval);
clearInterval(procInterval);
window.removeEventListener("keydown", handleKeyPress);
});
});
const createBar = (percentage: number, width: number = 30) => {
const filled = Math.round((percentage / 100) * width);
const empty = width - filled;
return "█".repeat(filled) + "░".repeat(empty);
};
return (
<div class="bg-crust fixed inset-0 z-[10000] flex items-center justify-center p-4">
{/* Main btop container */}
<div class="bg-mantle border-surface0 text-text relative h-full w-full max-w-6xl overflow-hidden rounded-lg border-2 font-mono text-sm shadow-2xl md:h-auto md:max-h-[90vh]">
{/* Header */}
<div class="border-surface0 bg-surface0 border-b px-4 py-2">
<div class="flex items-center justify-between">
<span class="text-blue">
btop <span class="text-subtext0">v1.404.0</span> - ErrorOS System
Monitor
</span>
<Show
when={!isMobile()}
fallback={
<button
onClick={props.onClose}
class="bg-red hover:bg-red/80 rounded px-3 py-1 text-base transition-colors"
>
Close
</button>
}
>
<span class="text-subtext1 text-xs">Press 'q' to quit</span>
</Show>
</div>
</div>
{/* Content */}
<div class="space-y-4 p-4">
{/* System Stats */}
<div class="border-surface0 bg-base rounded border p-3">
<div class="text-green mb-2 font-bold">System Resources</div>
<div class="space-y-2">
{/* CPU */}
<div class="flex items-center gap-2">
<span class="text-subtext1 w-12">CPU</span>
<span class="text-blue">[{createBar(cpuUsage())}]</span>
<span class="text-text w-16 text-right">{cpuUsage()}%</span>
<span class="text-subtext0">2.4 GHz</span>
</div>
{/* Memory */}
<div class="flex items-center gap-2">
<span class="text-subtext1 w-12">MEM</span>
<span class="text-blue">[{createBar(memUsage())}]</span>
<span class="text-text w-16 text-right">{memUsage()}%</span>
<span class="text-subtext0">
{((memUsage() / 100) * 16).toFixed(1)}/16 GB
</span>
</div>
{/* Network */}
<div class="flex items-center gap-2">
<span class="text-subtext1 w-12">NET</span>
<span class="text-green"> {netDown()} KB/s</span>
<span class="text-red"> {netUp()} KB/s</span>
</div>
</div>
</div>
{/* Process List */}
<div class="border-surface0 bg-base rounded border">
<div class="border-surface0 border-b px-3 py-2">
<span class="text-green font-bold">Processes</span>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-surface0 text-subtext1 sticky top-0">
<tr>
<th class="px-3 py-2 text-left">PID</th>
<th class="px-3 py-2 text-left">Name</th>
<th class="px-3 py-2 text-right">CPU%</th>
<th class="px-3 py-2 text-right">MEM%</th>
<th class="px-3 py-2 text-left">Status</th>
</tr>
</thead>
<tbody>
{processes().map((proc) => (
<tr class="hover:bg-surface0 border-surface0 border-t transition-colors">
<td class="text-yellow px-3 py-2">{proc.pid}</td>
<td class="text-text px-3 py-2 font-medium">
{proc.name}
</td>
<td class="text-blue px-3 py-2 text-right">
{proc.cpu.toFixed(1)}
</td>
<td class="text-peach px-3 py-2 text-right">
{proc.mem.toFixed(1)}
</td>
<td
class="px-3 py-2"
classList={{
"text-green": proc.status === "Running",
"text-subtext1": proc.status === "Sleeping",
"text-mauve": proc.status === "???"
}}
>
{proc.status}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Footer info */}
<div class="text-subtext1 text-center text-xs">
<Show
when={!isMobile()}
fallback={<span>Tap the Close button above to exit</span>}
>
<span>
Type <span class="text-green">q</span> to quit btop
</span>
</Show>
</div>
</div>
</div>
{/* Overlay background */}
<div
class="absolute inset-0 -z-10 bg-black/80 backdrop-blur-sm"
onClick={props.onClose}
/>
</div>
);
}

View File

@@ -20,13 +20,14 @@ export default function ErrorBoundaryFallback(
}; };
} }
// Try to get dark mode, fallback to true (dark) if context unavailable // Try to get dark mode, fallback to a function returning true (dark) if context unavailable
let isDark = true; let isDark: () => boolean;
try { try {
const darkMode = useDarkMode(); const darkMode = useDarkMode();
isDark = darkMode.isDark(); isDark = darkMode.isDark;
} catch (e) { } catch (e) {
// Context not available, use default // Context not available, use default
isDark = () => true;
} }
const [glitchText, setGlitchText] = createSignal("ERROR"); const [glitchText, setGlitchText] = createSignal("ERROR");

View File

@@ -13,6 +13,7 @@ import {
executeTerminalCommand, executeTerminalCommand,
CommandContext CommandContext
} from "~/lib/terminal-commands"; } from "~/lib/terminal-commands";
import { Btop } from "~/components/Btop";
interface TerminalErrorPageProps { interface TerminalErrorPageProps {
glitchText: string; glitchText: string;
@@ -34,6 +35,7 @@ export function TerminalErrorPage(props: TerminalErrorPageProps) {
const [command, setCommand] = createSignal(""); const [command, setCommand] = createSignal("");
const [history, setHistory] = createSignal<CommandHistoryItem[]>([]); const [history, setHistory] = createSignal<CommandHistoryItem[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1); const [historyIndex, setHistoryIndex] = createSignal(-1);
const [btopOpen, setBtopOpen] = createSignal(false);
let inputRef: HTMLInputElement | undefined; let inputRef: HTMLInputElement | undefined;
let footerRef: HTMLDivElement | undefined; let footerRef: HTMLDivElement | undefined;
@@ -62,6 +64,7 @@ export function TerminalErrorPage(props: TerminalErrorPageProps) {
navigate: props.navigate, navigate: props.navigate,
location: props.location, location: props.location,
addToHistory, addToHistory,
openBtop: () => setBtopOpen(true),
...props.commandContext ...props.commandContext
}); });
@@ -159,7 +162,7 @@ export function TerminalErrorPage(props: TerminalErrorPageProps) {
</div> </div>
{/* Main content */} {/* Main content */}
<div class="relative z-10 flex min-h-screen flex-col items-start justify-start py-16 lg:px-16"> <div class="relative z-10 flex min-h-screen flex-col items-start justify-start px-4 py-16 lg:px-16">
{/* Terminal header */} {/* Terminal header */}
<div class="mb-8 w-full max-w-4xl"> <div class="mb-8 w-full max-w-4xl">
<div class="border-surface0 text-subtext0 flex items-center gap-2 border-b pb-2 font-mono text-sm"> <div class="border-surface0 text-subtext0 flex items-center gap-2 border-b pb-2 font-mono text-sm">
@@ -220,6 +223,7 @@ export function TerminalErrorPage(props: TerminalErrorPageProps) {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
class="text-text caret-text ml-1 flex-1 border-none bg-transparent outline-none" class="text-text caret-text ml-1 flex-1 border-none bg-transparent outline-none"
autocomplete="off" autocomplete="off"
autocapitalize="off"
spellcheck={false} spellcheck={false}
/> />
</div> </div>
@@ -234,6 +238,11 @@ export function TerminalErrorPage(props: TerminalErrorPageProps) {
</div> </div>
</div> </div>
{/* Btop overlay */}
<Show when={btopOpen()}>
<Btop onClose={() => setBtopOpen(false)} />
</Show>
{/* Custom styles */} {/* Custom styles */}
<style>{` <style>{`
@keyframes scanline { @keyframes scanline {

View File

@@ -13,7 +13,8 @@ export interface CommandContext {
type: "success" | "error" | "info" type: "success" | "error" | "info"
) => void; ) => void;
triggerCrash?: () => void; triggerCrash?: () => void;
isDark?: boolean; isDark?: () => boolean;
openBtop?: () => void;
} }
export const createTerminalCommands = (context: CommandContext) => { export const createTerminalCommands = (context: CommandContext) => {
@@ -164,7 +165,7 @@ export const createTerminalCommands = (context: CommandContext) => {
}, },
neofetch: { neofetch: {
action: () => { action: () => {
const theme = context.isDark ? "Catppuccin-mocha" : "Gruvbox-light"; const theme = context.isDark?.() ? "Catppuccin-mocha" : "Gruvbox-light";
context.addToHistory( context.addToHistory(
"neofetch", "neofetch",
` _,met$$$$$gg. guest@freno.dev\n ,g$$$$$$$$$$$$$$$P. ----------------\n ,g$$P\"\" \"\"\"Y$$.\" OS: 404 Not Found\n ,$$P' \`$$$. Shell: terminal-shell\n',$$P ,ggs. \`$$b: Resolution: Lost\n\`d$$' ,$P\"' . $$$ Theme: ${theme}\n $$P d$' , $$P Terminal: web-terminal\n $$: $$. - ,d$$' CPU: Confusion (404)\n $$; Y$b._ _,d$P' Memory: ???\n Y$$. \`.\`\"Y$$$$P\"' \n \`$$b \"-.__ \n \`Y$$ \n \`Y$$. \n \`$$b. \n \`Y$$b. \n \`\"Y$b._ \n \`\"\"\"\" `, ` _,met$$$$$gg. guest@freno.dev\n ,g$$$$$$$$$$$$$$$P. ----------------\n ,g$$P\"\" \"\"\"Y$$.\" OS: 404 Not Found\n ,$$P' \`$$$. Shell: terminal-shell\n',$$P ,ggs. \`$$b: Resolution: Lost\n\`d$$' ,$P\"' . $$$ Theme: ${theme}\n $$P d$' , $$P Terminal: web-terminal\n $$: $$. - ,d$$' CPU: Confusion (404)\n $$; Y$b._ _,d$P' Memory: ???\n Y$$. \`.\`\"Y$$$$P\"' \n \`$$b \"-.__ \n \`Y$$ \n \`Y$$. \n \`$$b. \n \`Y$$b. \n \`\"Y$b._ \n \`\"\"\"\" `,
@@ -211,6 +212,20 @@ export const createTerminalCommands = (context: CommandContext) => {
); );
}, },
description: "Print system information" description: "Print system information"
},
btop: {
action: () => {
if (context.openBtop) {
context.openBtop();
} else {
context.addToHistory(
"btop",
"btop: command not available in this context",
"error"
);
}
},
description: "System resource monitor"
} }
}; };

View File

@@ -126,7 +126,7 @@ export default function NotFound() {
onGlitchTextChange={setGlitchText} onGlitchTextChange={setGlitchText}
commandContext={{ commandContext={{
triggerCrash: () => setShouldCrash(true), triggerCrash: () => setShouldCrash(true),
isDark: isDark() isDark: isDark
}} }}
/> />
</> </>