Add floating windows and btop system monitor

This commit is contained in:
SirBlob committed 2026-10-31 23:07:12 -04:00
1 parent fd0b3b20a2
commit 201a1e0914
23 files changed
+1049 -36

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let {
title,
number,
color = 'accent',
children
}: { title: string; number: number; color?: string; children: Snippet } = $props();
</script>
<section class="btop-box" style:--box-color="var(--{color})">
<span class="box-title"><sup>{number}</sup>{title}</span>
{@render children()}
</section>
<style>
.btop-box {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 0.9rem 0.75rem 0.6rem;
border: 1px solid var(--box-color);
border-radius: 8px;
}
.box-title {
position: absolute;
top: -0.65em;
left: 0.9rem;
padding: 0 0.35rem;
background: var(--background);
color: var(--box-color);
font-size: 0.8rem;
font-weight: 700;
line-height: 1.2;
}
sup {
margin-right: 0.15rem;
color: var(--foreground);
font-size: 0.6rem;
}
</style>
@@ -0,0 +1,104 @@
<script lang="ts">
import type { BtopData } from '#lib/btop/types';
import BtopBox from './BtopBox.svelte';
let { data }: { data: BtopData | null } = $props();
const levelOpacity = [0, 0.3, 0.55, 0.8, 1];
const calendar = $derived(data?.contributions ?? null);
</script>
<BtopBox title="net · commits" number={3} color="blue">
{#if calendar && data}
<div class="stats">
<span><b class="value">{calendar.total.toLocaleString('en-US')}</b> contributions in the last year</span>
<span class="muted">streak <b class="value">{calendar.currentStreak}d</b></span>
{#if calendar.bestDay}
<span class="muted">peak <b class="value">{calendar.bestDay.count}</b> on {calendar.bestDay.date}</span>
{/if}
</div>
<a class="heatmap" href={data.github.profileUrl} target="_blank" rel="noopener noreferrer" aria-label="GitHub contribution graph">
{#each calendar.weeks as week, weekIndex (weekIndex)}
<span class="week">
{#each week as day (day.date)}
<span
class="day"
style:opacity={day.level === 0 ? 1 : levelOpacity[day.level]}
class:empty={day.level === 0}
title="{day.count} contributions on {day.date}"
></span>
{/each}
</span>
{/each}
</a>
<div class="legend muted">
less
{#each levelOpacity as opacity, level (level)}
<span class="day" class:empty={level === 0} style:opacity={level === 0 ? 1 : opacity}></span>
{/each}
more
</div>
{:else}
<p class="muted loading">connecting to github...</p>
{/if}
</BtopBox>
<style>
.stats {
display: flex;
flex-wrap: wrap;
gap: 0.2rem 1rem;
margin-bottom: 0.6rem;
font-size: 0.75rem;
}
.value {
color: var(--blue);
}
.heatmap {
display: grid;
grid-auto-columns: minmax(0, 1fr);
grid-auto-flow: column;
gap: 2px;
}
.week {
display: grid;
grid-template-rows: repeat(7, auto);
gap: 2px;
}
.day {
display: block;
aspect-ratio: 1;
min-width: 4px;
background: var(--green);
border-radius: 1px;
}
.day.empty {
background: color-mix(in srgb, var(--foreground) 10%, transparent);
}
.legend {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 3px;
margin-top: 0.4rem;
font-size: 0.7rem;
}
.legend .day {
width: 9px;
}
.loading {
margin: 0;
font-size: 0.8rem;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<script lang="ts">
import { onMount } from 'svelte';
import { profile } from '#lib/content';
import { randomWalk, usageTone } from '#lib/btop/simulate';
import BtopBox from './BtopBox.svelte';
const historyLength = 48;
const updateInterval = 1000;
const coreNames = (profile.skills.Languages ?? []).slice(0, 6);
let history = $state<number[]>(Array.from({ length: historyLength }, () => 20 + Math.random() * 30));
let cores = $state(coreNames.map((name) => ({ name, usage: 20 + Math.random() * 60 })));
const totalUsage = $derived(Math.round(history.at(-1) ?? 0));
onMount(() => {
const timer = setInterval(() => {
history = [...history.slice(1), randomWalk(history.at(-1) ?? 40, 18, 5, 98)];
cores = cores.map((core) => ({ ...core, usage: randomWalk(core.usage, 15, 3, 100) }));
}, updateInterval);
return () => clearInterval(timer);
});
</script>
<BtopBox title="cpu" number={1} color="green">
<div class="cpu-header">
<span class="model">Brain 9000X @ 4.2 GHz (caffeinated)</span>
<span class="total" style:color="var(--{usageTone(totalUsage)})">{totalUsage}%</span>
</div>
<div class="graph" aria-hidden="true">
{#each history as value, index (index)}
<span class="bar" style:height="{value}%" style:background="var(--{usageTone(value)})"></span>
{/each}
</div>
<div class="cores">
{#each cores as core (core.name)}
<div class="core">
<span class="core-name">{core.name}</span>
<span class="core-meter"><span style:width="{core.usage}%" style:background="var(--{usageTone(core.usage)})"></span></span>
<span class="core-value">{Math.round(core.usage)}%</span>
</div>
{/each}
</div>
</BtopBox>
<style>
.cpu-header {
display: flex;
justify-content: space-between;
gap: 0.5rem;
font-size: 0.75rem;
}
.model {
overflow: hidden;
color: var(--muted);
white-space: nowrap;
text-overflow: ellipsis;
}
.total {
font-weight: 700;
}
.graph {
display: flex;
align-items: flex-end;
gap: 1px;
height: 70px;
margin: 0.4rem 0 0.6rem;
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 15%, transparent);
}
.bar {
flex: 1;
min-height: 1px;
opacity: 0.85;
transition: height 0.4s ease;
}
.cores {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 0.2rem 1rem;
font-size: 0.72rem;
}
.core {
display: grid;
grid-template-columns: 6.5rem 1fr 2.5rem;
align-items: center;
gap: 0.4rem;
}
.core-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.core-meter {
height: 6px;
background: color-mix(in srgb, var(--foreground) 10%, transparent);
}
.core-meter span {
display: block;
height: 100%;
transition: width 0.4s ease;
}
.core-value {
color: var(--muted);
text-align: right;
}
</style>
+105
View File
@@ -0,0 +1,105 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { BtopData } from '#lib/btop/types';
import { randomWalk, usageTone } from '#lib/btop/simulate';
import BtopBox from './BtopBox.svelte';
let { data }: { data: BtopData | null } = $props();
const updateInterval = 2000;
let meters = $state([
{ label: 'Caffeine', value: 82 },
{ label: 'Sleep debt', value: 64 },
{ label: 'Side projects', value: 91 },
{ label: 'Free time', value: 12 }
]);
const disks = $derived(
data
? [
{ label: 'Public repos', value: `${data.github.publicRepos}` },
{ label: 'Followers', value: `${data.github.followers}` },
{ label: 'Total stars', value: `${data.repos.reduce((sum, repo) => sum + repo.stars, 0)}` },
{ label: 'Total forks', value: `${data.repos.reduce((sum, repo) => sum + repo.forks, 0)}` }
]
: []
);
onMount(() => {
const timer = setInterval(() => {
meters = meters.map((meter) => ({ ...meter, value: randomWalk(meter.value, 4, 2, 99) }));
}, updateInterval);
return () => clearInterval(timer);
});
</script>
<BtopBox title="mem" number={2} color="yellow">
<div class="meters">
{#each meters as meter (meter.label)}
<div class="meter">
<div class="meter-label">
<span>{meter.label}</span>
<span style:color="var(--{usageTone(meter.value)})">{Math.round(meter.value)}%</span>
</div>
<span class="meter-track"><span style:width="{meter.value}%" style:background="var(--{usageTone(meter.value)})"></span></span>
</div>
{/each}
</div>
{#if disks.length}
<div class="disks">
<span class="disks-title">disks</span>
{#each disks as disk (disk.label)}
<div class="disk"><span class="muted">{disk.label}</span><span>{disk.value}</span></div>
{/each}
</div>
{/if}
</BtopBox>
<style>
.meters {
display: flex;
flex-direction: column;
gap: 0.45rem;
font-size: 0.75rem;
}
.meter-label {
display: flex;
justify-content: space-between;
margin-bottom: 0.15rem;
}
.meter-track {
display: block;
height: 6px;
background: color-mix(in srgb, var(--foreground) 10%, transparent);
}
.meter-track span {
display: block;
height: 100%;
transition: width 0.6s ease;
}
.disks {
display: flex;
flex-direction: column;
gap: 0.2rem;
margin-top: 0.7rem;
padding-top: 0.5rem;
border-top: 1px dashed color-mix(in srgb, var(--foreground) 20%, transparent);
font-size: 0.75rem;
}
.disks-title {
color: var(--yellow);
font-weight: 700;
}
.disk {
display: flex;
justify-content: space-between;
}
</style>
@@ -0,0 +1,99 @@
<script lang="ts">
import type { BtopData } from '#lib/btop/types';
import { formatSize, relativeTime } from '#lib/btop/simulate';
import BtopBox from './BtopBox.svelte';
let { data }: { data: BtopData | null } = $props();
const firstProcessId = 1337;
</script>
<BtopBox title="proc · repos" number={4} color="magenta">
{#if data}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th class="number">Pid</th>
<th>Program</th>
<th>Lang</th>
<th class="number">Stars</th>
<th class="number">Forks</th>
<th class="number">Mem</th>
<th class="number">Pushed</th>
</tr>
</thead>
<tbody>
{#each data.repos as repo, index (repo.name)}
<tr>
<td class="number muted">{firstProcessId + index}</td>
<td><a href={repo.url} target="_blank" rel="noopener noreferrer">{repo.name}</a></td>
<td class="muted">{repo.language ?? '-'}</td>
<td class="number stars">{repo.stars}</td>
<td class="number forks">{repo.forks}</td>
<td class="number muted">{formatSize(repo.sizeKb)}</td>
<td class="number muted">{relativeTime(repo.pushedAt)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<p class="muted loading">reading /proc...</p>
{/if}
</BtopBox>
<style>
.table-wrapper {
flex: 1;
min-height: 0;
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
white-space: nowrap;
}
th {
position: sticky;
top: 0;
padding: 0.15rem 0.4rem;
background: var(--background);
color: var(--magenta);
font-weight: 700;
text-align: left;
}
td {
padding: 0.1rem 0.4rem;
}
tbody tr:hover {
background: color-mix(in srgb, var(--magenta) 12%, transparent);
}
.number {
text-align: right;
}
a:hover {
color: var(--accent);
text-decoration: underline;
}
.stars {
color: var(--yellow);
}
.forks {
color: var(--cyan);
}
.loading {
margin: 0;
font-size: 0.8rem;
}
</style>
@@ -0,0 +1,112 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { BtopData } from '#lib/btop/types';
import CpuPanel from './CpuPanel.svelte';
import MemoryPanel from './MemoryPanel.svelte';
import ContributionPanel from './ContributionPanel.svelte';
import ProcessPanel from './ProcessPanel.svelte';
let data = $state<BtopData | null>(null);
let clock = $state(new Date());
const clockLabel = $derived(clock.toLocaleTimeString('en-US', { hour12: false }));
onMount(() => {
fetch('/api/btop')
.then((response) => (response.ok ? response.json() : null))
.then((result) => (data = result))
.catch(() => (data = null));
const timer = setInterval(() => (clock = new Date()), 1000);
return () => clearInterval(timer);
});
</script>
<div class="system-monitor">
<header class="top-line">
<span class="accent">btop</span>
<span class="muted">github: {data?.github.username ?? '...'}</span>
<span>{clockLabel}</span>
</header>
<div class="panels">
<div class="cpu"><CpuPanel /></div>
<div class="net"><ContributionPanel {data} /></div>
<div class="mem"><MemoryPanel {data} /></div>
<div class="proc"><ProcessPanel {data} /></div>
</div>
</div>
<style>
.system-monitor {
display: flex;
flex-direction: column;
gap: 0.6rem;
height: 100%;
min-height: 0;
padding: 0.5rem 0.75rem 0.75rem;
container-type: inline-size;
}
.top-line {
display: flex;
justify-content: space-between;
gap: 1rem;
font-size: 0.8rem;
}
.accent {
color: var(--accent);
font-weight: 700;
}
.panels {
display: grid;
flex: 1;
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
grid-template-rows: auto minmax(0, 1fr);
grid-template-areas:
'cpu net'
'proc mem';
gap: 1rem 0.75rem;
min-height: 0;
}
.cpu {
grid-area: cpu;
}
.net {
grid-area: net;
}
.mem {
grid-area: mem;
}
.proc {
grid-area: proc;
display: flex;
min-height: 0;
}
.proc > :global(.btop-box) {
flex: 1;
}
@container (max-width: 760px) {
.panels {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: none;
grid-template-areas:
'cpu'
'net'
'mem'
'proc';
}
.proc {
min-height: 220px;
}
}
</style>