feat(quickshell): add hyprchrome bar with collapsible panels

A second shell chrome under dotfiles/quickshell/hyprchrome, built around a
BarPanel that carries TWO renderings of its data: the default children are the
expanded detail view, `summary` the terse one shown while collapsed. Both stay
bound to the same sources, so the densities cannot disagree, and the panel
cross-fades between them while its height animates.

Panels: HostPanel (hostname, user, timezone, clock), VitalsPanel (CPU load and
temperature, memory, GPU load and temperature, all metered), TrayPanel (system
tray, self-sizing). HyprChromeBar pins them to DP-2 and owns `expanded` for the
whole rail — SUPER A, via GlobalShortcut "chrome".

GPU busy comes off sysfs rather than the node_exporter scrape VitalsData
already does: the hwmon collector carries the card's temps, power and clocks
but not its utilisation.

The bar's height binds to each panel's `targetHeight` — where it will settle,
not where the animation currently is — because the exclusive zone is
window-sized by default, and binding to the animated height relayouts every
tiled window on the output twelve times per toggle. The zone follows the target
immediately so the desktop reflows once, at the start; the surface itself
shrinks only after the panels finish, or it would clip them mid-animation.

DebugWindow stages a widget in the middle of the secondary monitor
(SUPER CTRL D), masked so only the staged widget takes pointer input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
This commit is contained in:
2026-08-29 02:38:00 +02:00
co-authored by Claude Opus 5
parent 6406e06330
commit e38a8403ac
15 changed files with 1444 additions and 2 deletions
@@ -0,0 +1,60 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import QtQuick
// amdgpu utilisation, straight off sysfs.
//
// node_exporter's hwmon collector carries the card's temperatures, power and
// clocks — which is where VitalsData gets them — but not its busy percentage,
// so this is the one vital that cannot come from the same scrape.
//
// The card number is globbed rather than pinned: it is card1 on terra today,
// but it depends on probe order and moves when a GPU is added or removed.
Scope {
id: root
// Poll only while something is showing it, like VitalsData.
property bool active: false
property int interval: 2000
property real value: 0 // 0..1 busy
property bool ready: false
onActiveChanged: {
if (!root.active)
root.ready = false;
}
Process {
id: probe
command: ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | head -n1"]
stdout: StdioCollector {
onStreamFinished: {
const busy = parseInt(this.text.trim(), 10);
if (isFinite(busy)) {
root.value = Math.max(0, Math.min(1, busy / 100));
root.ready = true;
} else {
// No amdgpu (or no permission) — leave the meter blank
// rather than pinning it at zero, which would read as idle.
root.ready = false;
}
}
}
}
Timer {
interval: root.interval
running: root.active
repeat: true
triggeredOnStart: true
onTriggered: {
if (!probe.running)
probe.running = true;
}
}
}