The shell was running two unrelated palettes: an amber one (#FFD063 accent, #EEEEEE text, #0F1012 panels) hardcoded as ~200 raw hex literals across the launchers, sidebar, systray, vitals and notifications, and an orange one (#e8722a) that only the dense bar had, tokenized as per-file properties. This unifies on the ORANGE values under the AMBER naming scheme, and moves the lot into widgets/theme/Theme.qml. `surface` takes the dense bar's void (#0a0a0a) rather than the old panel background. Zero color and font literals remain anywhere under widgets/ outside Theme.qml. Collisions resolved, all near-duplicates that wanted to be one token: - #0F1012 + #0A0A0C + #0a0a0a -> surface - #EEEEEE + #dedede -> text - #7A7B7D + #858585 -> muted - #292C30 + #22262C -> raised - #FFD063 + #e8722a -> accent Two derived things rather than literals. accentSoft (the pale flash the top/bottom bars show while a launcher is open) was a hand-picked #FFF3C0 against amber, which is simply wrong against orange; it is now Qt.tint(accent, white 55%), a ratio checked against the original (amber tinted 55% gives #FFE9B8 vs the hand-picked #FFF3C0). And the dense bar had been hand-encoding Qt.rgba(0.87,0.87,0.87,a) and Qt.rgba(0.91,0.45,0.16,a), which are just text and accent at alpha -- now textAlpha(a)/accentAlpha(a), so they track a palette change instead of silently drifting. Fonts came along too. Digital-7 Mono is dropped for DepartureMono: it was never packaged, relying on a manual ~/.dots/fonts/digital_7 install that does not exist on terra, so `fc-match "Digital-7 Mono"` resolved to DejaVu Sans and all 38 of those sites -- the launcher lists, sidebar clock, systray labels, every vitals readout -- were silently rendering in a PROPORTIONAL fallback. Numeric columns should visibly improve. readoutFont is an alias of displayFont rather than a second literal so the two roles cannot drift apart. quickshell/CLAUDE.md updated: it said "No shared theme/tokens file yet" and told contributors to grep for the existing hex color, which would now reintroduce exactly what this removes. Verified: no file references Theme. without the import, none imports it unused, and the whole shell -- launchers, sidebar, vitals, systray, notifications, not just the harness -- hot-reloaded clean on terra. tests/HeadlessSmoke.qml deliberately keeps its own copies; its value is having no dependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgcWkm3t6BDQktYHQvb8Hx
545 lines
20 KiB
QML
545 lines
20 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Shapes
|
|
import qs.widgets.theme
|
|
|
|
// Host vitals HUD — the "Slant" language (chamfered panel, trapezoid bevels,
|
|
// outward corner chunks, floating cap, slanted dividers) carried over from the
|
|
// sidebar and launcher V6, wrapped around this box's own node_exporter feed.
|
|
// Toggled with SUPER CTRL V.
|
|
Scope {
|
|
id: root
|
|
|
|
property bool active: false
|
|
|
|
function toggle() {
|
|
root.active = !root.active;
|
|
}
|
|
|
|
GlobalShortcut {
|
|
name: "vitals"
|
|
description: "Toggle the host vitals panel"
|
|
onPressed: root.toggle()
|
|
}
|
|
|
|
VitalsData {
|
|
id: vitals
|
|
active: root.active
|
|
}
|
|
|
|
PanelWindow {
|
|
id: win
|
|
|
|
visible: root.active
|
|
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
|
|
// Ignore the bars' exclusive zones so the backdrop covers the screen.
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
color: "transparent"
|
|
|
|
anchors {
|
|
top: true
|
|
left: true
|
|
right: true
|
|
bottom: true
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (visible)
|
|
keys.forceActiveFocus();
|
|
}
|
|
|
|
// Dim backdrop — click anywhere to dismiss.
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Theme.surface
|
|
opacity: 0.5
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: root.active = false
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: keys
|
|
anchors.fill: parent
|
|
focus: true
|
|
|
|
Keys.onPressed: event => {
|
|
if (event.key === Qt.Key_Escape) {
|
|
root.active = false;
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: frame
|
|
|
|
anchors.centerIn: parent
|
|
width: 620
|
|
height: content.implicitHeight + 2 * 26
|
|
|
|
readonly property int chamfer: 18
|
|
readonly property int bevel: 4 // inward thickness of the bevel borders
|
|
readonly property int chunkThick: 8 // outward thickness of the heavy chunks
|
|
readonly property int chunkSlant: 12 // slant of their end pieces
|
|
readonly property int capSize: 10 // floating triangle cap
|
|
readonly property int smallChamfer: 6
|
|
|
|
// Chamfered panel — top-left / bottom-right cut, accent edge.
|
|
Shape {
|
|
id: panelShape
|
|
anchors.fill: parent
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
fillColor: Theme.surface
|
|
strokeColor: Theme.accent
|
|
strokeWidth: 2
|
|
|
|
startX: frame.chamfer
|
|
startY: 0
|
|
PathLine { x: panelShape.width - frame.smallChamfer; y: 0 }
|
|
PathLine { x: panelShape.width; y: frame.smallChamfer }
|
|
PathLine { x: panelShape.width; y: panelShape.height - frame.chamfer }
|
|
PathLine { x: panelShape.width - frame.chamfer; y: panelShape.height }
|
|
PathLine { x: frame.smallChamfer; y: panelShape.height }
|
|
PathLine { x: 0; y: panelShape.height - frame.smallChamfer }
|
|
PathLine { x: 0; y: frame.chamfer }
|
|
PathLine { x: frame.chamfer; y: 0 }
|
|
}
|
|
|
|
// Thick top-left bevel accent.
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
|
|
startX: 0
|
|
startY: frame.chamfer
|
|
PathLine { x: frame.chamfer; y: 0 }
|
|
PathLine { x: frame.chamfer + 2 * frame.bevel; y: 0 }
|
|
PathLine { x: 0; y: frame.chamfer + 2 * frame.bevel }
|
|
PathLine { x: 0; y: frame.chamfer }
|
|
}
|
|
|
|
// Thick bottom-right bevel accent.
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
|
|
startX: panelShape.width
|
|
startY: panelShape.height - frame.chamfer
|
|
PathLine { x: panelShape.width - frame.chamfer; y: panelShape.height }
|
|
PathLine { x: panelShape.width - frame.chamfer - 2 * frame.bevel; y: panelShape.height }
|
|
PathLine { x: panelShape.width; y: panelShape.height - frame.chamfer - 2 * frame.bevel }
|
|
PathLine { x: panelShape.width; y: panelShape.height - frame.chamfer }
|
|
}
|
|
|
|
// Floating triangle cap in the bottom-right notch.
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
|
|
startX: panelShape.width
|
|
startY: panelShape.height
|
|
PathLine { x: panelShape.width; y: panelShape.height - frame.capSize }
|
|
PathLine { x: panelShape.width - frame.capSize; y: panelShape.height }
|
|
PathLine { x: panelShape.width; y: panelShape.height }
|
|
}
|
|
|
|
// Heavy outward chunk wrapping the bottom-left corner.
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
|
|
startX: panelShape.width / 3
|
|
startY: panelShape.height
|
|
PathLine { x: panelShape.width / 3 - frame.chunkSlant; y: panelShape.height + frame.chunkThick }
|
|
PathLine { x: frame.smallChamfer; y: panelShape.height + frame.chunkThick }
|
|
PathLine { x: -frame.chunkThick; y: panelShape.height - frame.smallChamfer }
|
|
PathLine { x: -frame.chunkThick; y: panelShape.height - panelShape.height / 7 + frame.chunkSlant }
|
|
PathLine { x: 0; y: panelShape.height - panelShape.height / 7 }
|
|
PathLine { x: 0; y: panelShape.height - frame.smallChamfer }
|
|
PathLine { x: frame.smallChamfer; y: panelShape.height }
|
|
PathLine { x: panelShape.width / 3; y: panelShape.height }
|
|
}
|
|
|
|
// Heavy outward chunk wrapping the top-right corner.
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
|
|
startX: panelShape.width
|
|
startY: panelShape.height / 3
|
|
PathLine { x: panelShape.width + frame.chunkThick; y: panelShape.height / 3 - frame.chunkSlant }
|
|
PathLine { x: panelShape.width + frame.chunkThick; y: frame.smallChamfer }
|
|
PathLine { x: panelShape.width - frame.smallChamfer; y: -frame.chunkThick }
|
|
PathLine { x: panelShape.width - panelShape.width / 5 + frame.chunkSlant; y: -frame.chunkThick }
|
|
PathLine { x: panelShape.width - panelShape.width / 5; y: 0 }
|
|
PathLine { x: panelShape.width - frame.smallChamfer; y: 0 }
|
|
PathLine { x: panelShape.width; y: frame.smallChamfer }
|
|
PathLine { x: panelShape.width; y: panelShape.height / 3 }
|
|
}
|
|
}
|
|
|
|
// ── Content ────────────────────────────────────────────────────
|
|
ColumnLayout {
|
|
id: content
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.topMargin: 26
|
|
anchors.leftMargin: 30
|
|
anchors.rightMargin: 26
|
|
spacing: 12
|
|
|
|
// Header — slash trio, host, uptime.
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
Shape {
|
|
id: slashes
|
|
implicitWidth: 26
|
|
implicitHeight: 22
|
|
Layout.alignment: Qt.AlignVCenter
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
startX: 6
|
|
startY: 0
|
|
PathLine { x: 10; y: 0 }
|
|
PathLine { x: 4; y: slashes.height }
|
|
PathLine { x: 0; y: slashes.height }
|
|
PathLine { x: 6; y: 0 }
|
|
}
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
startX: 15
|
|
startY: 0
|
|
PathLine { x: 19; y: 0 }
|
|
PathLine { x: 13; y: slashes.height }
|
|
PathLine { x: 9; y: slashes.height }
|
|
PathLine { x: 15; y: 0 }
|
|
}
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
startX: 24
|
|
startY: 0
|
|
PathLine { x: 28; y: 0 }
|
|
PathLine { x: 22; y: slashes.height }
|
|
PathLine { x: 18; y: slashes.height }
|
|
PathLine { x: 24; y: 0 }
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: (vitals.host || "vitals").toUpperCase()
|
|
color: Theme.text
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 20
|
|
font.letterSpacing: 2
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Text {
|
|
text: "UP"
|
|
color: Theme.muted
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 11
|
|
}
|
|
|
|
Text {
|
|
text: vitals.fmtUptime(vitals.uptime)
|
|
color: Theme.accent
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 16
|
|
}
|
|
}
|
|
|
|
Slant {}
|
|
|
|
// Unreachable exporter — say so rather than drawing zeroes.
|
|
Text {
|
|
Layout.fillWidth: true
|
|
visible: vitals.failed
|
|
text: "NODE_EXPORTER UNREACHABLE ON :" + vitals.port
|
|
color: Theme.hot
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 13
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
visible: !vitals.failed
|
|
spacing: 12
|
|
|
|
Metric {
|
|
label: "CPU"
|
|
value: vitals.cpu
|
|
unknown: !vitals.ratesReady
|
|
warn: 0.9
|
|
readout: vitals.ratesReady ? Math.round(vitals.cpu * 100) + "%" : "--"
|
|
detail: "LOAD " + vitals.load1.toFixed(2) + " / " + vitals.load5.toFixed(2) + " / " + vitals.load15.toFixed(2)
|
|
aside: vitals.cpuThreads + "T " + vitals.fmtTemp(vitals.cpuTemp)
|
|
asideHot: vitals.cpuTemp >= 85
|
|
}
|
|
|
|
Metric {
|
|
label: "MEM"
|
|
value: vitals.memTotal > 0 ? vitals.memUsed / vitals.memTotal : 0
|
|
unknown: !vitals.ready
|
|
readout: vitals.memTotal > 0 ? Math.round(vitals.memUsed / vitals.memTotal * 100) + "%" : "--"
|
|
detail: vitals.fmtBytes(vitals.memUsed) + " / " + vitals.fmtBytes(vitals.memTotal)
|
|
aside: ""
|
|
}
|
|
|
|
// No GPU-busy counter exists in node_exporter, so the bar
|
|
// tracks power draw against the card's cap — a real
|
|
// reading, labelled for what it is rather than faked as
|
|
// utilisation.
|
|
Metric {
|
|
label: "GPU"
|
|
visible: isFinite(vitals.gpuTemp)
|
|
value: isFinite(vitals.gpuPower) && vitals.gpuPowerCap > 0 ? vitals.gpuPower / vitals.gpuPowerCap : 0
|
|
unknown: !isFinite(vitals.gpuPower)
|
|
warn: 0.9
|
|
readout: isFinite(vitals.gpuPower) ? Math.round(vitals.gpuPower) + "W" : "--"
|
|
detail: (vitals.gpuPowerCap > 0 ? "CAP " + Math.round(vitals.gpuPowerCap) + "W" : "") + (isFinite(vitals.gpuClock) ? " SCLK " + Math.round(vitals.gpuClock) + " MHZ" : "")
|
|
aside: vitals.fmtTemp(vitals.gpuTemp) + (isFinite(vitals.gpuHotspot) ? " / " + vitals.fmtTemp(vitals.gpuHotspot) : "")
|
|
asideHot: vitals.gpuHotspot >= 95
|
|
}
|
|
}
|
|
|
|
Slant {
|
|
visible: !vitals.failed
|
|
}
|
|
|
|
// Disks.
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
visible: !vitals.failed
|
|
spacing: 6
|
|
|
|
Repeater {
|
|
model: vitals.disks
|
|
|
|
delegate: RowLayout {
|
|
id: diskRow
|
|
required property var modelData
|
|
|
|
readonly property real frac: diskRow.modelData.size > 0 ? diskRow.modelData.used / diskRow.modelData.size : 0
|
|
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
Text {
|
|
Layout.preferredWidth: 96
|
|
text: diskRow.modelData.mount
|
|
color: Theme.muted
|
|
font.pointSize: 9
|
|
elide: Text.ElideMiddle
|
|
}
|
|
|
|
VitalBar {
|
|
Layout.fillWidth: true
|
|
implicitHeight: 11
|
|
value: diskRow.frac
|
|
warn: 0.9
|
|
}
|
|
|
|
Text {
|
|
Layout.preferredWidth: 48
|
|
horizontalAlignment: Text.AlignRight
|
|
text: Math.round(diskRow.frac * 100) + "%"
|
|
color: diskRow.frac >= 0.9 ? Theme.hot : Theme.text
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 13
|
|
}
|
|
|
|
Text {
|
|
Layout.preferredWidth: 104
|
|
horizontalAlignment: Text.AlignRight
|
|
text: vitals.fmtBytes(diskRow.modelData.size - diskRow.modelData.used) + " FREE"
|
|
color: Theme.muted
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 11
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Slant {
|
|
visible: !vitals.failed
|
|
}
|
|
|
|
// Network.
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
visible: !vitals.failed
|
|
spacing: 12
|
|
|
|
Text {
|
|
Layout.preferredWidth: 96
|
|
text: vitals.netIface || "NET"
|
|
color: Theme.muted
|
|
font.pointSize: 9
|
|
}
|
|
|
|
Text {
|
|
text: "RX"
|
|
color: Theme.muted
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 11
|
|
}
|
|
|
|
Text {
|
|
text: vitals.fmtRate(vitals.netRx)
|
|
color: Theme.accent
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 14
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Text {
|
|
text: "TX"
|
|
color: Theme.muted
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 11
|
|
}
|
|
|
|
Text {
|
|
text: vitals.fmtRate(vitals.netTx)
|
|
color: Theme.accent
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 14
|
|
|
|
// Right-align the TX readout against the panel edge the
|
|
// disk rows' "FREE" column already lines up with.
|
|
Layout.preferredWidth: 104
|
|
horizontalAlignment: Text.AlignRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Local pieces ───────────────────────────────────────────────────────
|
|
|
|
// A labelled bar with a readout, a sub-line and a right-hand aside.
|
|
component Metric: ColumnLayout {
|
|
id: metric
|
|
|
|
property string label: ""
|
|
property string readout: ""
|
|
property string detail: ""
|
|
property string aside: ""
|
|
property bool asideHot: false
|
|
property real value: 0
|
|
property real warn: 0.85
|
|
property bool unknown: false
|
|
|
|
Layout.fillWidth: true
|
|
spacing: 3
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
Text {
|
|
Layout.preferredWidth: 46
|
|
text: metric.label
|
|
color: Theme.accent
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 15
|
|
font.letterSpacing: 1
|
|
}
|
|
|
|
VitalBar {
|
|
Layout.fillWidth: true
|
|
value: metric.value
|
|
warn: metric.warn
|
|
unknown: metric.unknown
|
|
}
|
|
|
|
Text {
|
|
Layout.preferredWidth: 54
|
|
horizontalAlignment: Text.AlignRight
|
|
text: metric.readout
|
|
color: Theme.text
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 16
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 58
|
|
spacing: 12
|
|
|
|
Text {
|
|
text: metric.detail
|
|
color: Theme.muted
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 11
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Text {
|
|
visible: metric.aside.length > 0
|
|
text: metric.aside
|
|
color: metric.asideHot ? Theme.hot : Theme.muted
|
|
font.family: Theme.readoutFont
|
|
font.pointSize: 11
|
|
}
|
|
}
|
|
}
|
|
|
|
// Slanted divider — the launcher/sidebar motif.
|
|
component Slant: Item {
|
|
Layout.fillWidth: true
|
|
implicitHeight: 3
|
|
|
|
Shape {
|
|
id: divider
|
|
anchors.fill: parent
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
fillColor: Theme.accent
|
|
startX: 6
|
|
startY: 0
|
|
PathLine { x: divider.width; y: 0 }
|
|
PathLine { x: divider.width - 6; y: divider.height }
|
|
PathLine { x: 0; y: divider.height }
|
|
PathLine { x: 6; y: 0 }
|
|
}
|
|
}
|
|
}
|
|
}
|