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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.HyprChrome.Theme
|
||||
|
||||
// Segmented horizontal meter: a row of cells lit up to `value`.
|
||||
//
|
||||
// A copy of the dense bar's meter rather than a reuse of it — that one is an
|
||||
// inline component inside DenseBarContent.qml and so is not visible from any
|
||||
// other file (the same reason BarPanel inlines its own MicroText).
|
||||
Row {
|
||||
id: meter
|
||||
|
||||
property int segments: 16
|
||||
property real value: 0 // 0..1
|
||||
property bool ready: false
|
||||
property real warn: 0.85 // fraction at which the lit cells go hot
|
||||
|
||||
readonly property real fraction: Math.max(0, Math.min(1, meter.value))
|
||||
readonly property bool hot: meter.ready && meter.fraction >= meter.warn
|
||||
readonly property color litColor: meter.hot ? Theme.hot : Theme.accent
|
||||
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: meter.segments
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
|
||||
readonly property bool lit: meter.ready && index < Math.round(meter.fraction * meter.segments)
|
||||
|
||||
width: Math.max(2, (meter.width - (meter.segments - 1) * meter.spacing) / meter.segments)
|
||||
height: meter.height
|
||||
color: lit ? meter.litColor : Theme.textAlpha(0.06)
|
||||
border.width: 1
|
||||
border.color: lit ? meter.litColor : Theme.textAlpha(0.18)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.HyprChrome.Theme
|
||||
|
||||
// One metric of the expanded vitals panel: label, meter, readout on a line.
|
||||
//
|
||||
// The label and readout columns are fixed so the meters of stacked rows line
|
||||
// up on both edges regardless of how long any one readout gets.
|
||||
Item {
|
||||
id: row
|
||||
|
||||
property string label: ""
|
||||
property real value: 0 // 0..1
|
||||
property string readout: "--"
|
||||
property bool ready: false
|
||||
property real warn: 0.85
|
||||
|
||||
property int labelWidth: 52
|
||||
property int readoutWidth: 46
|
||||
|
||||
readonly property bool hot: row.ready && row.value >= row.warn
|
||||
|
||||
implicitHeight: 11
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: row.labelWidth
|
||||
text: row.label
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 0.7
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
SegmentMeter {
|
||||
x: row.labelWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.max(0, row.width - row.labelWidth - row.readoutWidth)
|
||||
height: 9
|
||||
value: row.value
|
||||
ready: row.ready
|
||||
warn: row.warn
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: row.readoutWidth
|
||||
text: row.readout
|
||||
color: row.hot ? Theme.hot : Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
@@ -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 0–100 °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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user