<script lang="ts">
/**
* The host, at a glance.
*
* Five measures in the title bar, each one sparkline and one number. It is
* not a dashboard and must never become one: what it answers is "is this
* machine all right", which is a question somebody answers by looking, not
* by reading. So each measure gets a line eight pixels tall and a number
* short enough to take in without focusing — free space for the disk,
* a percentage for everything else.
*
* A measure this host cannot answer is LEFT OUT rather than shown empty. A
* flat line at the bottom reads as idle, which is a claim; a machine with no
* discrete GPU is not a machine whose GPU is idle.
*
* The graphs are drawn as one SVG path each — no chart library, no canvas,
* no per-frame work. They redraw when a reading arrives, which is every two
* seconds.
*/
import { onMount } from "svelte";
import { host, human, percent, ratio, HISTORY } from "../../stores/host.svelte.js";
onMount(() => {
host.connect();
return () => host.disconnect();
});
/** One sparkline's path, over a fixed 0..1 range. */
function path(series: (number | null)[], width: number, height: number): string {
const points = series.length;
if (points === 0) return "";
// Fixed range, so the height of the line means the same thing in every
// graph and at every moment. An auto-scaled sparkline makes an idle
// machine look busy, which is the opposite of what this is for.
const step = points > 1 ? width / (HISTORY - 1) : width;
// A line only where there are readings; a gap where there are not.
let out = "";
let pen = false;
series.forEach((value, index) => {
if (value === null) {
pen = false;
return;
}
const x = index * step;
const y = height - Math.max(0, Math.min(1, value)) * height;
out += `${pen ? "L" : "M"}${x.toFixed(1)},${y.toFixed(1)}`;
pen = true;
});
return out;
}
const W = 34;
const H = 12;
/// The measures, in the order they are read. Absent ones fall out here, so
/// the markup never has to ask whether a thing exists.
const measures = $derived.by(() => {
const sample = host.latest;
if (!sample) return [];
const rows: {
id: string;
label: string;
series: (number | null)[];
value: string;
title: string;
warn: boolean;
}[] = [];
if (sample.cpu !== null) {
rows.push({
id: "cpu",
label: "CPU",
series: host.cpu,
value: percent(sample.cpu),
title: `Processor ${percent(sample.cpu)} busy${
sample.cpu_count ? ` · ${sample.cpu_count} logical cores` : ""
}`,
warn: sample.cpu > 0.9,
});
}
if (sample.memory) {
const used = ratio(sample.memory) ?? 0;
rows.push({
id: "ram",
label: "RAM",
series: host.memory,
// The number is what is LEFT, because that is the number somebody is
// deciding on: whether to start something else.
value: human(sample.memory.total - sample.memory.used),
title: `Memory: ${human(sample.memory.total - sample.memory.used)} free of ${human(
sample.memory.total,
)} (${percent(used)} in use)`,
warn: used > 0.9,
});
}
if (sample.vram) {
const used = ratio(sample.vram) ?? 0;
rows.push({
id: "vram",
label: "VRAM",
series: host.vram,
value: human(sample.vram.total - sample.vram.used),
title: `${sample.gpu_name ?? "Graphics"} memory: ${human(
sample.vram.total - sample.vram.used,
)} free of ${human(sample.vram.total)} (${percent(used)} in use)`,
warn: used > 0.9,
});
}
if (sample.gpu !== null) {
rows.push({
id: "gpu",
label: "GPU",
series: host.gpu,
value: percent(sample.gpu),
title: `${sample.gpu_name ?? "Graphics"} ${percent(sample.gpu)} busy`,
warn: sample.gpu > 0.9,
});
}
if (sample.disk) {
const used = ratio(sample.disk) ?? 0;
rows.push({
id: "disk",
label: "Disk",
series: host.disk,
value: human(sample.disk.total - sample.disk.used),
title: `Worktree volume: ${human(sample.disk.total - sample.disk.used)} free of ${human(
sample.disk.total,
)} (${percent(used)} in use)`,
warn: used > 0.9,
});
}
return rows;
});
</script>
{#if measures.length > 0}
<div class="host-strip" role="group" aria-label="Host system">
{#each measures as measure (measure.id)}
<div class="measure" class:warn={measure.warn} title={measure.title}>
<svg class="spark" width={W} height={H} viewBox="0 0 {W} {H}" aria-hidden="true">
<!-- The full range, so a line near the top always means the same
thing. Drawn faintly rather than labelled: an axis in a graph
this size is more ink than signal. -->
<path class="spark-line" d={path(measure.series, W, H)} />
</svg>
<span class="value">{measure.value}</span>
<span class="sr-only">{measure.label}: {measure.value}</span>
</div>
{/each}
</div>
{/if}
<style>
.host-strip {
display: flex;
align-items: center;
gap: var(--wb-spacing-10);
padding: 0 var(--wb-spacing-4);
/* Never the reason the bar grows or the layout jumps: the strip is the
least important thing in the title bar and yields first. */
min-width: 0;
overflow: hidden;
}
.measure {
display: flex;
align-items: center;
gap: var(--wb-spacing-4);
color: var(--vscode-descriptionForeground, #999);
font-size: var(--wb-font-size-10);
font-variant-numeric: tabular-nums;
line-height: 1;
flex-shrink: 0;
}
/* One state worth colouring, and it is the one somebody should act on. */
.measure.warn {
color: var(--vscode-editorWarning-foreground, #cca700);
}
.spark {
display: block;
overflow: visible;
}
.spark-line {
fill: none;
stroke: currentColor;
stroke-width: 1;
stroke-linejoin: round;
stroke-linecap: round;
opacity: 0.75;
}
.value {
min-width: var(--wb-control-size-34);
text-align: right;
}
/* Named for a screen reader, which cannot read a sparkline. */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: var(--wb-spacing-neg-1);
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 900px) {
/* The two that matter least when there is no room. */
.measure:nth-child(3),
.measure:nth-child(4) {
display: none;
}
}
</style>