From 201a1e0914ccab406d738f450e3736e71b7d51a4 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Sat, 31 Oct 2026 23:07:12 -0400 Subject: [PATCH] Add floating windows and btop system monitor --- content/desktop.json | 3 +- src/lib/btop/simulate.ts | 26 ++++ src/lib/btop/types.ts | 35 ++++++ src/lib/components/btop/BtopBox.svelte | 46 +++++++ .../components/btop/ContributionPanel.svelte | 104 +++++++++++++++ src/lib/components/btop/CpuPanel.svelte | 118 ++++++++++++++++++ src/lib/components/btop/MemoryPanel.svelte | 105 ++++++++++++++++ src/lib/components/btop/ProcessPanel.svelte | 99 +++++++++++++++ src/lib/components/btop/SystemMonitor.svelte | 112 +++++++++++++++++ src/lib/components/desktop/Desktop.svelte | 71 +++++++---- src/lib/components/desktop/Launcher.svelte | 9 +- src/lib/components/desktop/Window.svelte | 110 ++++++++++++++-- src/lib/components/terminal/Terminal.svelte | 9 +- src/lib/content/types.ts | 4 +- src/lib/desktop/actions.ts | 7 +- src/lib/desktop/floating.ts | 49 ++++++++ src/lib/desktop/keybindGuide.ts | 1 + src/lib/desktop/keybinds.ts | 1 + src/lib/desktop/windows.svelte.ts | 43 ++++++- src/lib/server/btop.ts | 73 +++++++++++ src/lib/server/contributions.ts | 49 ++++++++ src/lib/terminal/commands/types.ts | 2 + src/routes/api/btop/+server.ts | 9 ++ 23 files changed, 1049 insertions(+), 36 deletions(-) create mode 100644 src/lib/btop/simulate.ts create mode 100644 src/lib/btop/types.ts create mode 100644 src/lib/components/btop/BtopBox.svelte create mode 100644 src/lib/components/btop/ContributionPanel.svelte create mode 100644 src/lib/components/btop/CpuPanel.svelte create mode 100644 src/lib/components/btop/MemoryPanel.svelte create mode 100644 src/lib/components/btop/ProcessPanel.svelte create mode 100644 src/lib/components/btop/SystemMonitor.svelte create mode 100644 src/lib/desktop/floating.ts create mode 100644 src/lib/server/btop.ts create mode 100644 src/lib/server/contributions.ts create mode 100644 src/routes/api/btop/+server.ts diff --git a/content/desktop.json b/content/desktop.json index 75e426e..69f2838 100644 --- a/content/desktop.json +++ b/content/desktop.json @@ -29,7 +29,8 @@ { "label": "fastfetch.sh", "icon": "ph:cpu-duotone", "open": { "terminal": "fastfetch" } }, { "label": "whoami.sh", "icon": "ph:identification-card-duotone", "open": { "terminal": "whoami" } }, { "label": "skills.sh", "icon": "ph:lightning-duotone", "open": { "terminal": "skills" } }, - { "label": "contact.sh", "icon": "ph:envelope-simple-duotone", "open": { "terminal": "contact" } } + { "label": "contact.sh", "icon": "ph:envelope-simple-duotone", "open": { "terminal": "contact" } }, + { "label": "btop", "icon": "ph:chart-line-duotone", "open": { "app": "btop" } } ] }, { diff --git a/src/lib/btop/simulate.ts b/src/lib/btop/simulate.ts new file mode 100644 index 0000000..47ef9a0 --- /dev/null +++ b/src/lib/btop/simulate.ts @@ -0,0 +1,26 @@ +export function randomWalk(value: number, step: number, minimum = 0, maximum = 100): number { + const next = value + (Math.random() * 2 - 1) * step; + return Math.min(maximum, Math.max(minimum, next)); +} + +export function usageTone(percent: number): string { + if (percent >= 80) return 'red'; + if (percent >= 50) return 'yellow'; + return 'green'; +} + +export function relativeTime(isoDate: string): string { + const elapsedMinutes = Math.floor((Date.now() - new Date(isoDate).getTime()) / 60000); + if (elapsedMinutes < 60) return `${elapsedMinutes}m`; + const elapsedHours = Math.floor(elapsedMinutes / 60); + if (elapsedHours < 24) return `${elapsedHours}h`; + const elapsedDays = Math.floor(elapsedHours / 24); + if (elapsedDays < 60) return `${elapsedDays}d`; + return `${Math.floor(elapsedDays / 30)}mo`; +} + +export function formatSize(sizeKb: number): string { + if (sizeKb >= 1024 * 1024) return `${(sizeKb / 1024 / 1024).toFixed(1)}G`; + if (sizeKb >= 1024) return `${(sizeKb / 1024).toFixed(1)}M`; + return `${sizeKb}K`; +} diff --git a/src/lib/btop/types.ts b/src/lib/btop/types.ts new file mode 100644 index 0000000..fc73400 --- /dev/null +++ b/src/lib/btop/types.ts @@ -0,0 +1,35 @@ +export interface ContributionDay { + date: string; + level: number; + count: number; +} + +export interface ContributionCalendar { + total: number; + weeks: ContributionDay[][]; + currentStreak: number; + bestDay: ContributionDay | null; +} + +export interface RepoProcess { + name: string; + url: string; + language: string | null; + stars: number; + forks: number; + sizeKb: number; + pushedAt: string; +} + +export interface GithubSummary { + username: string; + profileUrl: string; + publicRepos: number; + followers: number; +} + +export interface BtopData { + github: GithubSummary; + contributions: ContributionCalendar; + repos: RepoProcess[]; +} diff --git a/src/lib/components/btop/BtopBox.svelte b/src/lib/components/btop/BtopBox.svelte new file mode 100644 index 0000000..aaa75d4 --- /dev/null +++ b/src/lib/components/btop/BtopBox.svelte @@ -0,0 +1,46 @@ + + +
+ {number}{title} + {@render children()} +
+ + diff --git a/src/lib/components/btop/ContributionPanel.svelte b/src/lib/components/btop/ContributionPanel.svelte new file mode 100644 index 0000000..a0e0d77 --- /dev/null +++ b/src/lib/components/btop/ContributionPanel.svelte @@ -0,0 +1,104 @@ + + + + {#if calendar && data} +
+ {calendar.total.toLocaleString('en-US')} contributions in the last year + streak {calendar.currentStreak}d + {#if calendar.bestDay} + peak {calendar.bestDay.count} on {calendar.bestDay.date} + {/if} +
+ + + {#each calendar.weeks as week, weekIndex (weekIndex)} + + {#each week as day (day.date)} + + {/each} + + {/each} + + +
+ less + {#each levelOpacity as opacity, level (level)} + + {/each} + more +
+ {:else} +

connecting to github...

+ {/if} +
+ + diff --git a/src/lib/components/btop/CpuPanel.svelte b/src/lib/components/btop/CpuPanel.svelte new file mode 100644 index 0000000..84d756f --- /dev/null +++ b/src/lib/components/btop/CpuPanel.svelte @@ -0,0 +1,118 @@ + + + +
+ Brain 9000X @ 4.2 GHz (caffeinated) + {totalUsage}% +
+ + + +
+ {#each cores as core (core.name)} +
+ {core.name} + + {Math.round(core.usage)}% +
+ {/each} +
+
+ + diff --git a/src/lib/components/btop/MemoryPanel.svelte b/src/lib/components/btop/MemoryPanel.svelte new file mode 100644 index 0000000..f344e71 --- /dev/null +++ b/src/lib/components/btop/MemoryPanel.svelte @@ -0,0 +1,105 @@ + + + +
+ {#each meters as meter (meter.label)} +
+
+ {meter.label} + {Math.round(meter.value)}% +
+ +
+ {/each} +
+ + {#if disks.length} +
+ disks + {#each disks as disk (disk.label)} +
{disk.label}{disk.value}
+ {/each} +
+ {/if} +
+ + diff --git a/src/lib/components/btop/ProcessPanel.svelte b/src/lib/components/btop/ProcessPanel.svelte new file mode 100644 index 0000000..24c25ce --- /dev/null +++ b/src/lib/components/btop/ProcessPanel.svelte @@ -0,0 +1,99 @@ + + + + {#if data} +
+ + + + + + + + + + + + + + {#each data.repos as repo, index (repo.name)} + + + + + + + + + + {/each} + +
PidProgramLangStarsForksMemPushed
{firstProcessId + index}{repo.name}{repo.language ?? '-'}{repo.stars}{repo.forks}{formatSize(repo.sizeKb)}{relativeTime(repo.pushedAt)}
+
+ {:else} +

reading /proc...

+ {/if} +
+ + diff --git a/src/lib/components/btop/SystemMonitor.svelte b/src/lib/components/btop/SystemMonitor.svelte new file mode 100644 index 0000000..42a38b7 --- /dev/null +++ b/src/lib/components/btop/SystemMonitor.svelte @@ -0,0 +1,112 @@ + + +
+
+ btop + github: {data?.github.username ?? '...'} + {clockLabel} +
+ +
+
+
+
+
+
+
+ + diff --git a/src/lib/components/desktop/Desktop.svelte b/src/lib/components/desktop/Desktop.svelte index 2c1d490..8933664 100644 --- a/src/lib/components/desktop/Desktop.svelte +++ b/src/lib/components/desktop/Desktop.svelte @@ -6,6 +6,8 @@ import Terminal from '#lib/components/terminal/Terminal.svelte'; import ImageViewer from '#lib/components/viewer/ImageViewer.svelte'; import { imageFileName } from '#lib/desktop/imageViewer'; + import { defaultFloatingRect, floatingStyle, moveRect, resizeRect } from '#lib/desktop/floating'; + import SystemMonitor from '../btop/SystemMonitor.svelte'; import DesktopIcons from './DesktopIcons.svelte'; import Window from './Window.svelte'; @@ -25,6 +27,9 @@ const isNarrow = $derived(areaWidth < narrowWidth); const openWindowIds = $derived([...(pageOpen ? [pageWindowId] : []), ...windowManager.extraWindowIds]); + const floatingIds = $derived(isNarrow ? [] : openWindowIds.filter((id) => windowManager.isFloating(id))); + const tiledIds = $derived(openWindowIds.filter((id) => !floatingIds.includes(id))); + const area = $derived({ width: areaWidth, height: areaHeight }); const layout = $derived.by(() => { const rects = new Map(); @@ -33,10 +38,16 @@ rects.set(soloId, fullscreenLayout()); return rects; } - dwindleLayout(openWindowIds.length, areaWidth, areaHeight).forEach((rect, index) => rects.set(openWindowIds[index], rect)); + dwindleLayout(tiledIds.length, areaWidth, areaHeight).forEach((rect, index) => rects.set(tiledIds[index], rect)); return rects; }); + $effect(() => { + floatingIds.forEach((id, index) => { + if (!windowManager.floatingRects[id]) windowManager.setFloatingRect(id, defaultFloatingRect(area, index)); + }); + }); + $effect(() => { if (openWindowIds.length && !openWindowIds.includes(windowManager.focusedId)) { windowManager.focus(openWindowIds.at(-1)!); @@ -44,9 +55,31 @@ }); function styleFor(id: string): string { + const floatingRect = windowManager.floatingRects[id]; + if (floatingIds.includes(id) && !layout.has(id)) { + return floatingRect ? floatingStyle(floatingRect, windowManager.focusedId === id) : 'display: none'; + } const rect = layout.get(id); return rect ? tileStyle(rect, isNarrow ? 4 : 10) : 'display: none'; } + + function updateFloatingRect(id: string, change: typeof moveRect, deltaX: number, deltaY: number) { + const rect = windowManager.floatingRects[id]; + if (rect) windowManager.setFloatingRect(id, change(rect, deltaX, deltaY, area)); + } + + function windowProps(id: string) { + return { + focused: windowManager.focusedId === id, + tileStyle: styleFor(id), + floating: floatingIds.includes(id) && !layout.has(id), + canFloat: !isNarrow, + onfocus: () => windowManager.focus(id), + ontogglefloat: () => windowManager.toggleFloating(id), + ondrag: (deltaX: number, deltaY: number) => updateFloatingRect(id, moveRect, deltaX, deltaY), + onresize: (deltaX: number, deltaY: number) => updateFloatingRect(id, resizeRect, deltaX, deltaY) + }; + }
@@ -66,29 +99,13 @@
{#if pageOpen} - windowManager.focus(pageWindowId)} - onclose={closePage} - > + {@render children()} {/if} {#each windowManager.terminals as terminal (terminal.id)} - windowManager.focus(terminal.id)} - onclose={() => windowManager.closeWindow(terminal.id)} - > + windowManager.closeWindow(terminal.id)}> windowManager.focus(viewer.id)} + {...windowProps(viewer.id)} onclose={() => windowManager.closeWindow(viewer.id)} > {/each} + + {#each windowManager.systemMonitors as monitor (monitor.id)} + windowManager.closeWindow(monitor.id)} + > + + + {/each}
diff --git a/src/lib/components/desktop/Launcher.svelte b/src/lib/components/desktop/Launcher.svelte index 25dc4be..aa57424 100644 --- a/src/lib/components/desktop/Launcher.svelte +++ b/src/lib/components/desktop/Launcher.svelte @@ -24,7 +24,7 @@ .filter((item) => !('page' in item.open)) .map((item) => ({ label: item.label, - detail: 'terminal' in item.open ? `kitty ${item.open.terminal}`.trim() : 'url' in item.open ? item.open.url : '', + detail: launcherDetail(item.open), icon: item.icon, open: item.open })); @@ -66,6 +66,13 @@ selectedIndex = Math.max(0, selectedIndex - 1); } else if (event.key === 'Enter') launch(filteredEntries[selectedIndex]); } + + function launcherDetail(action: DesktopAction): string { + if ('terminal' in action) return `kitty ${action.terminal}`.trim(); + if ('url' in action) return action.url; + if ('app' in action) return action.app; + return ''; + } diff --git a/src/lib/components/desktop/Window.svelte b/src/lib/components/desktop/Window.svelte index 38447a4..b47ab21 100644 --- a/src/lib/components/desktop/Window.svelte +++ b/src/lib/components/desktop/Window.svelte @@ -9,8 +9,13 @@ focused, tileStyle, scrollKey = '', + floating = false, + canFloat = false, onfocus, onclose, + ontogglefloat, + ondrag, + onresize, children }: { title: string; @@ -19,12 +24,44 @@ focused: boolean; tileStyle: string; scrollKey?: string; + floating?: boolean; + canFloat?: boolean; onfocus: () => void; onclose: () => void; + ontogglefloat?: () => void; + ondrag?: (deltaX: number, deltaY: number) => void; + onresize?: (deltaX: number, deltaY: number) => void; children: Snippet; } = $props(); let contentElement = $state(); + let isDragging = $state(false); + + function trackPointer(event: PointerEvent, onmove?: (deltaX: number, deltaY: number) => void) { + if (!floating || !onmove || event.button !== 0) return; + if (event.target instanceof Element && event.target.closest('button')) return; + event.preventDefault(); + + let lastX = event.clientX; + let lastY = event.clientY; + isDragging = true; + + const handleMove = (moveEvent: PointerEvent) => { + onmove(moveEvent.clientX - lastX, moveEvent.clientY - lastY); + lastX = moveEvent.clientX; + lastY = moveEvent.clientY; + }; + const handleEnd = () => { + isDragging = false; + window.removeEventListener('pointermove', handleMove); + window.removeEventListener('pointerup', handleEnd); + window.removeEventListener('pointercancel', handleEnd); + }; + + window.addEventListener('pointermove', handleMove); + window.addEventListener('pointerup', handleEnd); + window.addEventListener('pointercancel', handleEnd); + } $effect(() => { scrollKey; @@ -35,27 +72,49 @@
-
+
trackPointer(event, ondrag)}> {title} {#if path} {path} {/if} - + + {#if canFloat && ontogglefloat} + + {/if} + +
{@render children()}
+ {#if floating && onresize} + trackPointer(event, onresize)} + > + {/if}