Add floating windows and btop system monitor
This commit is contained in:
1 parent
fd0b3b20a2
commit
201a1e0914
23 files changed
+1049
-36
No files matched your search
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user