feat(quickshell): cap the rail's open chamfers and trace between them

A chamfer with no neighbour behind it now carries the corner it removed,
put back OUTSIDE the panel as a detached accent triangle. capGap is the
perpendicular distance from the cut, hence the per-axis shift of capGap
over root 2: the cap moves along the cut's normal, not along an axis.

The trace joining two caps belongs to the RAIL, not the panel — the run it
draws is the gap BETWEEN two panels, which no panel can see. HyprChromeBar
filters the row down to panels (a slack Item has no `rightChamfer`, so
spacers drop out, and dropping them is exactly what makes a trace span
them), then joins each right cap to the next left cap: straight, one 45°
step at the midpoint of the gap, straight. It meets the middle of each
cap's outward face rather than its tip.

TestPanel is a staging slot between two spacers. It counts seconds since
load, which is the cheapest proof the panel is live, and standing alone it
exercises a cap and a trace at both ends — something a rail of butted
panels never does.

Restructures the module in the same commit, since the moves and the edits
above land in the same files: folders are PascalCase, the bar and its
widgets moved under Widgets/Bar, and DebugWindow takes the HyprChrome Theme
instead of the legacy one. The two singletons are identical today, so that
is not a visual fix — it is a palette edit reaching the debug stage in
future. Rename detection needs -M40% to follow HyprChromeBar, which grew
past the default similarity threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U19X5LGTxtq4pb4jdNVivv
This commit is contained in:
2026-08-31 00:30:58 +02:00
co-authored by Claude Opus 5
parent 3aecaf9de5
commit 2bf71494f4
18 changed files with 366 additions and 141 deletions
@@ -0,0 +1,142 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.HyprChrome.Theme
import qs.HyprChrome.Widgets.Bar.Panels
import qs.widgets.vitals
// Host vitals in the hyprchrome panel chrome, in both of BarPanel's densities.
//
// Expanded: CPU load and temperature, memory usage, GPU load and temperature,
// each as a segmented meter with its readout.
// Collapsed: the same five numbers as percentages behind Nerd Font glyphs.
//
// Both bodies read the same properties below, so the two densities cannot
// disagree — they are one set of numbers rendered twice.
//
// The scrape comes from the shared VitalsData (node_exporter over loopback);
// GPU busy is the one reading that scrape does not carry, so it comes off
// sysfs through GpuBusy.
BarPanel {
id: panel
panelId: "MON"
title: "RESOURCE MONITOR"
meta: vitals.failed ? "OFFLINE" : vitals.ready ? "REALTIME" : "PRIMING"
// Poll only while the panel exists on screen; both sources idle otherwise.
property bool polling: true
// Temperatures are metered against a 0100 °C span so a bar means the same
// thing on every row.
readonly property real tempCeiling: 100
readonly property real memoryFraction: vitals.memTotal > 0 ? vitals.memUsed / vitals.memTotal : 0
readonly property bool cpuTempReady: isFinite(vitals.cpuTemp)
readonly property bool gpuTempReady: isFinite(vitals.gpuTemp)
function pct(value, ready) {
return ready ? Math.round(Math.max(0, Math.min(1, value)) * 100) + "%" : "--";
}
function tempFraction(celsius) {
return isFinite(celsius) ? Math.max(0, Math.min(1, celsius / panel.tempCeiling)) : 0;
}
VitalsData {
id: vitals
active: panel.polling
}
GpuBusy {
id: gpu
active: panel.polling
}
// Collapsed: glyph + percentage, in the same order as the rows below.
// Codepoints are Nerd Fonts v3 — oct-cpu, fa-thermometer-half,
// md-memory, md-expansion-card-variant — all present in DepartureMono.
summary: RowLayout {
spacing: 12
Item { Layout.fillWidth: true }
Readout { icon: "CPU:"; value: panel.pct(vitals.cpu, vitals.ratesReady) }
Readout { icon: "CPU Temp:"; value: vitals.fmtTemp(vitals.cpuTemp) }
Readout { icon: "Mem:"; value: panel.pct(panel.memoryFraction, vitals.ready) }
Readout { icon: "GPU:"; value: panel.pct(gpu.value, gpu.ready) }
Readout { icon: "GPU Temp:"; value: vitals.fmtTemp(vitals.gpuTemp) }
Item { Layout.fillWidth: true }
}
// Expanded: the same five, metered.
Column {
width: parent.width
spacing: 5
VitalRow {
width: parent.width
label: "CPU"
value: vitals.cpu
ready: vitals.ratesReady && !vitals.failed
readout: panel.pct(vitals.cpu, vitals.ratesReady)
}
VitalRow {
width: parent.width
label: "CPU TMP"
value: panel.tempFraction(vitals.cpuTemp)
ready: panel.cpuTempReady
readout: vitals.fmtTemp(vitals.cpuTemp)
warn: 0.85
}
VitalRow {
width: parent.width
label: "MEM"
value: panel.memoryFraction
ready: vitals.ready && !vitals.failed
readout: panel.pct(panel.memoryFraction, vitals.ready)
}
VitalRow {
width: parent.width
label: "GPU"
value: gpu.value
ready: gpu.ready
readout: panel.pct(gpu.value, gpu.ready)
}
VitalRow {
width: parent.width
label: "GPU TMP"
value: panel.tempFraction(vitals.gpuTemp)
ready: panel.gpuTempReady
readout: vitals.fmtTemp(vitals.gpuTemp)
warn: 0.85
}
}
component Readout: Row {
property string icon: ""
property string value: "--"
spacing: 4
Text {
text: parent.icon
color: Theme.accent
font.family: Theme.displayFont
font.pixelSize: 12
}
Text {
text: parent.value
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 12
font.bold: true
}
}
}