Compare commits

1 Commits
Author SHA1 Message Date
luna 22fe8ab778 feat(quickshell): add dense telemetry bar 2026-08-27 23:52:47 +00:00
10 changed files with 1219 additions and 0 deletions
+3
View File
@@ -14,3 +14,6 @@ keys.txt
# local env (PATH etc.) # local env (PATH etc.)
.env .env
# local visual-verification output
.artifacts/
+3
View File
@@ -10,6 +10,9 @@ import qs.widgets.systray
import qs.widgets.vitals import qs.widgets.vitals
Scope { Scope {
// Dense multi-monitor status rail; visual core is headlessly renderable.
DenseBar {}
// Left sidebar in the Slant (V6) style — toggle with SUPER CTRL S. // Left sidebar in the Slant (V6) style — toggle with SUPER CTRL S.
SideBar {} SideBar {}
BarBottom {} BarBottom {}
@@ -0,0 +1,27 @@
import QtQuick
import qs.widgets.bar
DenseBarContent {
width: 1920
height: 164
now: new Date(2026, 7, 27, 21, 47, 0)
hostName: "TERRA"
telemetryReady: true
ratesReady: true
cpuFraction: 0.62
cpuThreads: 16
memoryFraction: 0.78
memoryUsedText: "24.9G"
temperatureCelsius: 54
networkInterface: "enp7s0"
networkRxText: "842.6M/S"
networkTxText: "116.2M/S"
storageFraction: 0.69
storageFreeText: "1.82T FREE"
workspaceIds: [1, 2, 3, 4]
activeWorkspaceId: 1
trayCount: 3
audioFraction: 0.50
audioMuted: false
}
+155
View File
@@ -0,0 +1,155 @@
import QtQuick
import QtQuick.Shapes
Item {
id: root
implicitWidth: 720
implicitHeight: 120
readonly property color voidColor: "#0a0a0a"
readonly property color inkColor: "#dedede"
readonly property color mutedColor: "#858585"
readonly property color accentColor: "#e8722a"
Rectangle {
anchors.fill: parent
color: root.voidColor
}
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: root.voidColor
strokeColor: Qt.rgba(0.87, 0.87, 0.87, 0.28)
strokeWidth: 1
startX: 1
startY: 1
PathLine { x: root.width - 14; y: 1 }
PathLine { x: root.width - 1; y: 14 }
PathLine { x: root.width - 1; y: root.height - 9 }
PathLine { x: root.width - 9; y: root.height - 1 }
PathLine { x: 17; y: root.height - 1 }
PathLine { x: 1; y: root.height - 17 }
PathLine { x: 1; y: 1 }
}
ShapePath {
fillColor: root.accentColor
strokeWidth: 0
startX: 1
startY: 1
PathLine { x: 92; y: 1 }
PathLine { x: 92; y: 3 }
PathLine { x: 1; y: 3 }
PathLine { x: 1; y: 1 }
}
}
Rectangle {
x: 14
y: 12
width: 29
height: 14
color: root.accentColor
Text {
anchors.centerIn: parent
text: "001"
color: root.voidColor
font.pixelSize: 8
font.bold: true
}
}
Text {
x: 52
y: 13
text: "HEADLESS RENDER ARRAY"
color: root.inkColor
font.family: "monospace"
font.pixelSize: 9
font.bold: true
font.letterSpacing: 1.4
}
Rectangle {
x: 14
y: 34
width: root.width - 28
height: 1
color: root.inkColor
opacity: 0.18
}
Text {
x: 16
y: 48
text: "ORBITAL"
color: root.inkColor
font.family: "sans-serif-condensed"
font.pixelSize: 28
font.bold: true
font.letterSpacing: 2
}
Text {
x: 18
y: 80
text: "QML // GRABTOIMAGE // SOFTWARE RHI"
color: root.accentColor
font.family: "monospace"
font.pixelSize: 7
font.letterSpacing: 1.4
}
Row {
x: 250
y: 56
spacing: 4
Repeater {
model: 16
Rectangle {
required property int index
width: 20
height: 12
color: index < 11 ? root.accentColor : Qt.rgba(0.87, 0.87, 0.87, 0.06)
border.width: 1
border.color: index < 11 ? root.accentColor : Qt.rgba(0.87, 0.87, 0.87, 0.2)
}
}
}
Text {
x: 250
y: 78
text: "RENDER PIPELINE"
color: root.mutedColor
font.family: "monospace"
font.pixelSize: 7
font.letterSpacing: 1.1
}
Text {
x: 585
y: 77
text: "68.75%"
color: root.inkColor
font.family: "monospace"
font.pixelSize: 15
font.bold: true
}
Rectangle {
x: 14
y: root.height - 9
width: root.width - 28
height: 3
color: root.accentColor
opacity: 0.6
}
}
@@ -0,0 +1,86 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Pipewire
import Quickshell.Services.SystemTray
import QtQuick
import qs.widgets.vitals
// Production/layer-shell adapter. All visual composition lives in the
// Item-rooted DenseBarContent so the exact bar can be rendered headlessly.
Scope {
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property real volume: sink?.audio?.volume ?? 0
readonly property bool muted: sink?.audio?.muted ?? false
readonly property var workspaceIds: Hyprland.workspaces.values
.filter(workspace => workspace.id > 0)
.map(workspace => workspace.id)
readonly property int activeWorkspaceId: Hyprland.focusedWorkspace?.id ?? 1
readonly property var rootDisk: vitals.disks.length > 0 ? vitals.disks[0] : null
PwObjectTracker {
objects: [root.sink]
}
VitalsData {
id: vitals
active: true
}
Variants {
model: Quickshell.screens
PanelWindow {
id: window
required property var modelData
screen: modelData
color: "transparent"
implicitHeight: 164
anchors {
top: true
left: true
right: true
}
DenseBarContent {
anchors.fill: parent
hostName: vitals.host || "LOCAL"
telemetryReady: vitals.ready && !vitals.failed
ratesReady: vitals.ratesReady && !vitals.failed
cpuFraction: vitals.cpu
cpuThreads: vitals.cpuThreads
memoryFraction: vitals.memTotal > 0 ? vitals.memUsed / vitals.memTotal : 0
memoryUsedText: vitals.fmtBytes(vitals.memUsed)
temperatureCelsius: vitals.cpuTemp
networkInterface: vitals.netIface || "NET"
networkRxText: vitals.fmtRate(vitals.netRx)
networkTxText: vitals.fmtRate(vitals.netTx)
storageFraction: root.rootDisk && root.rootDisk.size > 0
? root.rootDisk.used / root.rootDisk.size : 0
storageFreeText: root.rootDisk
? vitals.fmtBytes(root.rootDisk.size - root.rootDisk.used) + " FREE"
: "-- FREE"
workspaceIds: root.workspaceIds.length > 0 ? root.workspaceIds : [1, 2, 3, 4]
activeWorkspaceId: root.activeWorkspaceId
trayCount: SystemTray.items.values.length
audioFraction: Math.max(0, Math.min(1, root.volume))
audioMuted: root.muted
onWorkspaceActivated: workspaceId => {
const workspace = Hyprland.workspaces.values.find(item => item.id === workspaceId);
if (workspace)
workspace.activate();
else
Hyprland.dispatch("workspace " + workspaceId);
}
}
}
}
}
@@ -0,0 +1,799 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
// Renderable visual core for the desktop telemetry rail. Runtime services stay
// in DenseBar.qml so this Item can be exercised without Wayland or Hyprland.
Item {
id: root
implicitWidth: 1920
implicitHeight: 164
readonly property color voidColor: "#0a0a0a"
readonly property color inkColor: "#dedede"
readonly property color mutedColor: "#858585"
readonly property color accentColor: "#e8722a"
readonly property color hairColor: Qt.rgba(0.87, 0.87, 0.87, 0.28)
readonly property bool compact: width <= 1400
readonly property string displayFont: "DepartureMono Nerd Font"
readonly property string microFont: "DejaVu Sans Mono"
property bool autoClock: true
property date now: new Date()
property string hostName: "LOCAL"
property bool telemetryReady: false
property bool ratesReady: false
property real cpuFraction: 0
property int cpuThreads: 0
property real memoryFraction: 0
property string memoryUsedText: "--"
property real temperatureCelsius: NaN
property string networkInterface: "NET"
property string networkRxText: "--"
property string networkTxText: "--"
property real storageFraction: 0
property string storageFreeText: "-- FREE"
property var workspaceIds: [1, 2, 3, 4]
property int activeWorkspaceId: 1
property int trayCount: 0
property real audioFraction: 0
property bool audioMuted: false
signal workspaceActivated(int workspaceId)
function pct(value, ready) {
return ready ? Math.round(Math.max(0, Math.min(1, value)) * 100) : 0;
}
function two(value) {
return value < 10 ? "0" + value : String(value);
}
function timeText(value) {
return root.two(value.getHours()) + ":" + root.two(value.getMinutes());
}
function dateText(value) {
const days = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
return days[value.getDay()] + " // " + root.two(value.getDate()) + " " + months[value.getMonth()];
}
Timer {
interval: 30000
running: root.autoClock
repeat: true
triggeredOnStart: true
onTriggered: root.now = new Date()
}
Rectangle {
anchors.fill: parent
color: root.voidColor
}
// Faint drafting grid; no gradient and deliberately subordinate to data.
Repeater {
model: Math.ceil(root.width / 40)
Rectangle {
required property int index
x: index * 40
width: 1
height: root.height
color: root.inkColor
opacity: 0.018
}
}
Repeater {
model: Math.ceil(root.height / 40)
Rectangle {
required property int index
y: index * 40
width: root.width
height: 1
color: root.inkColor
opacity: 0.018
}
}
Item {
id: array
x: 12
y: 8
width: root.width - 24
height: 92
readonly property real identityWidth: root.compact ? 250 : 320
readonly property real radarWidth: root.compact ? 130 : 164
readonly property real stateWidth: root.compact ? 210 : 250
readonly property real flexWidth: Math.max(250, (width - identityWidth - radarWidth - stateWidth - 32) / 2)
Row {
anchors.fill: parent
spacing: 8
TelemetryPanel {
width: array.identityWidth
height: array.height
panelId: "001"
title: "COMMAND LAYER"
Text {
x: 12
y: 34
text: root.hostName.toUpperCase()
color: root.inkColor
font.family: root.displayFont
font.pixelSize: root.compact ? 20 : 25
font.bold: true
font.letterSpacing: 2
elide: Text.ElideRight
width: parent.width - 92
}
Text {
x: 13
y: 63
text: "STATUS ARRAY // " + (root.telemetryReady ? "LIVE" : "STANDBY")
color: root.accentColor
font.family: root.microFont
font.pixelSize: 7
font.letterSpacing: 1.4
}
Rectangle {
x: parent.width - 75
y: 36
width: 1
height: 43
color: root.hairColor
}
Rectangle {
x: parent.width - 62
y: 42
width: 5
height: 5
color: root.accentColor
opacity: root.telemetryReady ? 1 : 0.35
}
Text {
x: parent.width - 50
y: 38
text: root.telemetryReady ? "ONLINE" : "LOCAL"
color: root.accentColor
font.family: root.microFont
font.pixelSize: 7
}
Text {
x: parent.width - 63
y: 57
text: "NODE // " + (root.hostName || "--").toUpperCase().slice(0, 7)
color: root.mutedColor
font.family: root.microFont
font.pixelSize: 6
}
Shape {
x: parent.width - 63
y: 71
width: 48
height: 10
ShapePath {
fillColor: "transparent"
strokeColor: root.accentColor
strokeWidth: 1
startX: 0; startY: 6
PathLine { x: 11; y: 6 }
PathLine { x: 16; y: 1 }
PathLine { x: 22; y: 9 }
PathLine { x: 27; y: 6 }
PathLine { x: 48; y: 6 }
}
}
}
TelemetryPanel {
width: array.flexWidth
height: array.height
panelId: "02"
title: "RESOURCE LATTICE"
meta: root.telemetryReady ? "REALTIME" : "FALLBACK"
Row {
x: 11
y: 32
width: parent.width - 22
height: 51
spacing: root.compact ? 7 : 12
MetricBlock {
width: (parent.width - parent.spacing * (root.compact ? 1 : 2)) / (root.compact ? 2 : 3)
label: "CPU"
value: root.cpuFraction
ready: root.ratesReady
readout: root.ratesReady ? root.pct(root.cpuFraction, true) + "%" : "--"
detailLeft: "CORE:" + (root.cpuThreads || "--")
detailRight: root.ratesReady ? "BUSY" : "PRIME"
}
MetricBlock {
width: (parent.width - parent.spacing * (root.compact ? 1 : 2)) / (root.compact ? 2 : 3)
label: "MEM"
value: root.memoryFraction
ready: root.telemetryReady
readout: root.telemetryReady ? root.pct(root.memoryFraction, true) + "%" : "--"
detailLeft: "ALLOC"
detailRight: root.memoryUsedText
}
MetricBlock {
visible: !root.compact
width: (parent.width - parent.spacing * 2) / 3
label: "THERM"
value: isFinite(root.temperatureCelsius) ? root.temperatureCelsius / 100 : 0
ready: isFinite(root.temperatureCelsius)
readout: isFinite(root.temperatureCelsius) ? Math.round(root.temperatureCelsius) + "°C" : "--"
detailLeft: "ZONE:01"
detailRight: isFinite(root.temperatureCelsius) && root.temperatureCelsius >= 85 ? "HOT" : "NOMINAL"
}
}
}
TelemetryPanel {
width: array.radarWidth
height: array.height
panelId: "03"
title: "SCAN"
RadarGauge {
anchors.horizontalCenter: parent.horizontalCenter
y: 28
size: 57
level: root.cpuFraction
}
}
TelemetryPanel {
width: array.flexWidth
height: array.height
panelId: "04"
title: "CARRIER UPLINK"
meta: root.networkInterface.toUpperCase()
NetworkTrace {
x: 12
y: 43
width: parent.width - (root.compact ? 117 : 145)
height: 33
level: root.ratesReady ? root.cpuFraction : 0.25
}
Column {
anchors.right: parent.right
anchors.rightMargin: 14
y: 34
width: root.compact ? 92 : 116
spacing: 1
Text {
width: parent.width
text: root.networkRxText
horizontalAlignment: Text.AlignRight
color: root.accentColor
font.family: root.displayFont
font.pixelSize: root.compact ? 11 : 13
font.bold: true
elide: Text.ElideLeft
}
MicroText { width: parent.width; text: "RX // DOWN"; horizontalAlignment: Text.AlignRight }
Text {
width: parent.width
text: root.networkTxText
horizontalAlignment: Text.AlignRight
color: root.inkColor
font.family: root.displayFont
font.pixelSize: root.compact ? 11 : 13
font.bold: true
elide: Text.ElideLeft
}
MicroText { width: parent.width; text: "TX // UP"; horizontalAlignment: Text.AlignRight }
}
}
TelemetryPanel {
width: array.stateWidth
height: array.height
panelId: "05"
title: "SYSTEM STATE"
Row {
x: 10
y: 35
spacing: 6
StateCell { code: "N"; active: root.ratesReady; label: "NET" }
StateCell { code: root.audioMuted ? "M" : "A"; active: !root.audioMuted; label: "AUD" }
StateCell { visible: !root.compact; code: String(root.trayCount); active: root.trayCount > 0; label: "TRAY" }
}
Rectangle {
x: root.compact ? 101 : 151
y: 33
width: 1
height: 46
color: root.hairColor
}
Column {
anchors.right: parent.right
anchors.rightMargin: 12
y: 34
width: root.compact ? 91 : 82
spacing: 2
Text {
width: parent.width
text: root.timeText(root.now)
horizontalAlignment: Text.AlignRight
color: root.inkColor
font.family: root.displayFont
font.pixelSize: root.compact ? 20 : 22
font.bold: true
font.letterSpacing: 1
}
Text {
width: parent.width
text: root.dateText(root.now)
horizontalAlignment: Text.AlignRight
color: root.accentColor
font.family: root.microFont
font.pixelSize: 6
elide: Text.ElideLeft
}
MicroText { width: parent.width; text: "LOCAL TIME"; horizontalAlignment: Text.AlignRight }
}
}
}
}
TelemetryPanel {
id: rail
x: 12
y: 108
width: root.width - 24
height: 34
showHeader: false
chamfer: 10
Row {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
RailSection {
width: root.compact ? 252 : 310
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "06 // DESKTOP"
color: root.accentColor
font.family: root.microFont
font.pixelSize: 7
font.letterSpacing: 1
}
Row {
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Repeater {
model: root.workspaceIds.slice(0, root.compact ? 4 : 6)
Rectangle {
required property var modelData
width: 24
height: 17
color: Number(modelData) === root.activeWorkspaceId ? Qt.rgba(0.91, 0.45, 0.16, 0.18) : "transparent"
border.width: 1
border.color: Number(modelData) === root.activeWorkspaceId ? root.accentColor : root.hairColor
Text {
anchors.centerIn: parent
text: root.two(Number(parent.modelData))
color: Number(parent.modelData) === root.activeWorkspaceId ? root.accentColor : root.mutedColor
font.family: root.microFont
font.pixelSize: 7
}
MouseArea {
anchors.fill: parent
onClicked: root.workspaceActivated(Number(parent.modelData))
}
}
}
}
}
RailSection {
width: root.compact ? 260 : 410
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "PROCESS" }
Text {
x: 76
anchors.verticalCenter: parent.verticalCenter
text: "COMPOSITOR // NODE_EXPORTER // SHELL"
color: root.telemetryReady ? root.inkColor : root.mutedColor
font.family: root.microFont
font.pixelSize: 7
elide: Text.ElideRight
width: parent.width - 84
}
}
RailSection {
width: root.compact ? 300 : 385
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "VOL // ROOT" }
Text {
x: 93
anchors.verticalCenter: parent.verticalCenter
text: root.storageFreeText
color: root.inkColor
font.family: root.microFont
font.pixelSize: 8
}
SegmentMeter {
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: root.compact ? 95 : 170
height: 7
segments: root.compact ? 10 : 16
value: root.storageFraction
ready: root.telemetryReady
}
}
RailSection {
visible: !root.compact
width: 350
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "AUDIO BUS" }
Text {
x: 89
anchors.verticalCenter: parent.verticalCenter
text: root.audioMuted ? "MUTED" : Math.round(root.audioFraction * 100) + "%"
color: root.audioMuted ? root.mutedColor : root.accentColor
font.family: root.microFont
font.pixelSize: 8
}
SegmentMeter {
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: 155
height: 7
segments: 16
value: root.audioFraction
ready: true
}
}
RailSection {
width: Math.max(150, rail.width - (root.compact ? 812 : 1455) - 20)
borderVisible: false
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: root.compact ? "SYS // " + root.timeText(root.now) : "EVENTS" }
Text {
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: root.telemetryReady ? "WARN:00 // ERR:00" : "TELEMETRY WAIT"
color: root.telemetryReady ? root.mutedColor : root.accentColor
font.family: root.microFont
font.pixelSize: 7
elide: Text.ElideLeft
width: parent.width - 64
horizontalAlignment: Text.AlignRight
}
}
}
}
// Bottom routing trace and caution hatch.
Shape {
x: 12
y: 150
width: root.width - 24
height: 12
ShapePath {
fillColor: "transparent"
strokeColor: Qt.rgba(0.91, 0.45, 0.16, 0.55)
strokeWidth: 1
startX: 0; startY: 1
PathLine { x: 220; y: 1 }
PathLine { x: 232; y: 11 }
PathLine { x: 330; y: 11 }
}
}
Row {
x: 360
y: 151
width: root.width - 372
height: 3
spacing: 5
clip: true
Repeater {
model: Math.ceil(parent.width / 10)
Rectangle {
required property int index
width: 5
height: 3
color: index % 2 === 0 ? root.accentColor : "transparent"
opacity: 0.58
}
}
}
component MicroText: Text {
color: root.mutedColor
font.family: root.microFont
font.pixelSize: 6
font.letterSpacing: 0.7
}
component RailSection: Item {
property bool borderVisible: true
height: rail.height
Rectangle {
visible: parent.borderVisible
anchors.right: parent.right
width: 1
height: parent.height
color: root.hairColor
opacity: 0.65
}
}
component TelemetryPanel: Item {
id: panel
property string panelId: ""
property string title: ""
property string meta: ""
property bool showHeader: true
property int chamfer: 13
Shape {
id: panelShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: root.voidColor
strokeColor: root.hairColor
strokeWidth: 1
startX: 1; startY: 1
PathLine { x: panelShape.width - panel.chamfer; y: 1 }
PathLine { x: panelShape.width - 1; y: panel.chamfer }
PathLine { x: panelShape.width - 1; y: panelShape.height - 8 }
PathLine { x: panelShape.width - 8; y: panelShape.height - 1 }
PathLine { x: 16; y: panelShape.height - 1 }
PathLine { x: 1; y: panelShape.height - 16 }
PathLine { x: 1; y: 1 }
}
ShapePath {
fillColor: root.accentColor
strokeWidth: 0
startX: 1; startY: 1
PathLine { x: Math.min(49, panelShape.width / 3); y: 1 }
PathLine { x: Math.min(49, panelShape.width / 3); y: 3 }
PathLine { x: 1; y: 3 }
PathLine { x: 1; y: 1 }
}
}
Rectangle {
visible: panel.showHeader
x: 1; y: 20
width: parent.width - 2
height: 1
color: root.inkColor
opacity: 0.12
}
Rectangle {
visible: panel.showHeader
x: 9; y: 5
width: panel.panelId.length > 2 ? 29 : 24
height: 11
color: root.accentColor
Text {
anchors.centerIn: parent
text: panel.panelId
color: root.voidColor
font.family: root.microFont
font.pixelSize: 7
font.bold: true
}
}
Text {
visible: panel.showHeader
x: 40; y: 5
width: parent.width - 105
text: panel.title
color: root.inkColor
font.family: root.displayFont
font.pixelSize: 9
font.bold: true
font.letterSpacing: 1.1
elide: Text.ElideRight
}
MicroText {
visible: panel.showHeader && panel.meta.length > 0
anchors.right: parent.right
anchors.rightMargin: 12
y: 6
text: panel.meta
width: Math.min(80, parent.width / 4)
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
}
Row {
visible: panel.showHeader
anchors.right: parent.right
anchors.rightMargin: 10
y: 14
spacing: 2
Repeater {
model: 5
Rectangle { required property int index; width: 3; height: 1; color: root.accentColor }
}
}
}
component MetricBlock: Item {
id: metric
property string label: ""
property real value: 0
property bool ready: false
property string readout: "--"
property string detailLeft: ""
property string detailRight: ""
height: 51
MicroText { x: 0; y: 1; text: metric.label; font.pixelSize: 7 }
Text {
anchors.right: parent.right
y: -3
text: metric.readout
color: root.inkColor
font.family: root.displayFont
font.pixelSize: root.compact ? 14 : 17
font.bold: true
}
SegmentMeter {
x: 0; y: 19
width: parent.width
height: 9
segments: 10
value: metric.value
ready: metric.ready
}
MicroText { x: 0; y: 34; text: metric.detailLeft }
MicroText { anchors.right: parent.right; y: 34; text: metric.detailRight; horizontalAlignment: Text.AlignRight }
}
component SegmentMeter: Row {
id: meter
property int segments: 10
property real value: 0
property bool ready: false
spacing: 2
Repeater {
model: meter.segments
Rectangle {
required property int index
width: Math.max(2, (meter.width - (meter.segments - 1) * meter.spacing) / meter.segments)
height: meter.height
readonly property bool on: meter.ready && index < Math.round(Math.max(0, Math.min(1, meter.value)) * meter.segments)
color: on ? root.accentColor : Qt.rgba(0.87, 0.87, 0.87, 0.06)
border.width: 1
border.color: on ? root.accentColor : Qt.rgba(0.87, 0.87, 0.87, 0.18)
}
}
}
component StateCell: Item {
id: state
property string code: "--"
property string label: ""
property bool active: false
width: root.compact ? 39 : 43
height: 42
Shape {
anchors.fill: parent
ShapePath {
fillColor: state.active ? Qt.rgba(0.91, 0.45, 0.16, 0.18) : "transparent"
strokeColor: state.active ? root.accentColor : root.hairColor
strokeWidth: 1
startX: 1; startY: 1
PathLine { x: parent.width - 7; y: 1 }
PathLine { x: parent.width - 1; y: 7 }
PathLine { x: parent.width - 1; y: parent.height - 1 }
PathLine { x: 7; y: parent.height - 1 }
PathLine { x: 1; y: parent.height - 7 }
PathLine { x: 1; y: 1 }
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
y: 7
text: state.code
color: state.active ? root.accentColor : root.inkColor
font.family: root.displayFont
font.pixelSize: 13
font.bold: true
}
MicroText { anchors.horizontalCenter: parent.horizontalCenter; y: 27; text: state.label }
}
component RadarGauge: Item {
id: radar
property real size: 58
property real level: 0
width: size
height: size
Repeater {
model: [1, 0.72, 0.36]
Rectangle {
required property int index
required property var modelData
anchors.centerIn: parent
width: radar.size * Number(modelData)
height: width
radius: width / 2
color: "transparent"
border.width: 1
border.color: index === 0 ? root.accentColor : root.hairColor
}
}
Rectangle { x: 0; y: parent.height / 2; width: parent.width; height: 1; color: root.hairColor }
Rectangle { x: parent.width / 2; y: 0; width: 1; height: parent.height; color: root.hairColor }
Rectangle { x: 12; y: 18; width: 4; height: 4; color: root.accentColor }
Rectangle { x: parent.width - 14; y: parent.height - 20; width: 4; height: 4; color: root.accentColor }
Rectangle { x: parent.width / 2; y: parent.height - 11; width: 4; height: 4; color: root.accentColor }
MicroText { anchors.right: parent.right; y: 3; text: "R:" + Math.round(radar.level * 99); color: root.accentColor }
}
component NetworkTrace: Item {
id: trace
property real level: 0
Rectangle { x: 0; y: parent.height - 1; width: parent.width; height: 1; color: root.hairColor }
Rectangle { x: 0; y: 0; width: 1; height: parent.height; color: root.hairColor }
Shape {
anchors.fill: parent
ShapePath {
fillColor: "transparent"
strokeColor: root.accentColor
strokeWidth: 1.2
startX: 0; startY: trace.height * 0.65
PathLine { x: trace.width * 0.10; y: trace.height * 0.65 }
PathLine { x: trace.width * 0.15; y: trace.height * (0.25 + trace.level * 0.15) }
PathLine { x: trace.width * 0.21; y: trace.height * 0.78 }
PathLine { x: trace.width * 0.31; y: trace.height * 0.56 }
PathLine { x: trace.width * 0.43; y: trace.height * 0.62 }
PathLine { x: trace.width * 0.49; y: trace.height * 0.18 }
PathLine { x: trace.width * 0.57; y: trace.height * 0.82 }
PathLine { x: trace.width * 0.68; y: trace.height * 0.58 }
PathLine { x: trace.width * 0.79; y: trace.height * 0.62 }
PathLine { x: trace.width * 0.85; y: trace.height * 0.34 }
PathLine { x: trace.width * 0.91; y: trace.height * 0.75 }
PathLine { x: trace.width; y: trace.height * 0.60 }
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1
FROM nixos/nix:2.30.2
ARG NIXPKGS_REV=e5bdc4a41d4c072fe1e3787eaa0320a384741d44
ARG QSMCP_REV=76b1b008f0352064121af9334065012e31d80fc2
ENV NIX_CONFIG="experimental-features = nix-command flakes" \
PATH="/nix/var/nix/profiles/default/bin:${PATH}"
RUN nix profile install --profile /nix/var/nix/profiles/default \
"github:NixOS/nixpkgs/${NIXPKGS_REV}#quickshell" \
"github:NixOS/nixpkgs/${NIXPKGS_REV}#nodejs_22"
RUN mkdir -p /opt/qsmcp \
&& git init /opt/qsmcp \
&& git -C /opt/qsmcp remote add origin https://github.com/fedsfarm/qsmcp.git \
&& git -C /opt/qsmcp fetch --depth 1 origin "${QSMCP_REV}" \
&& git -C /opt/qsmcp checkout --detach FETCH_HEAD \
&& test "$(git -C /opt/qsmcp rev-parse HEAD)" = "${QSMCP_REV}" \
&& cd /opt/qsmcp \
&& npm ci --omit=dev \
&& rm -rf /opt/qsmcp/.git
# qsmcp's CLI hard-codes a 1568px safety cap. Desktop bar previews need their
# true target width so responsive layout and text collisions are testable.
RUN node -e 'const fs=require("fs"); const p="/opt/qsmcp/src/index.ts"; const s=fs.readFileSync(p,"utf8"); const old="max_dimension: 1568"; if (!s.includes(old)) throw new Error("qsmcp max_dimension source changed"); fs.writeFileSync(p,s.replace(old,"max_dimension: 4096"))'
ENV QSMCP_SHELL_ROOT=/workspace/quickshell \
QSMCP_QS_BIN=/nix/var/nix/profiles/default/bin/qs \
QT_QUICK_BACKEND=software
COPY entrypoint.sh /usr/local/bin/quickshell-preview
RUN chmod +x /usr/local/bin/quickshell-preview
RUN mkdir -p /workspace/quickshell
WORKDIR /workspace/quickshell
ENTRYPOINT ["/usr/local/bin/quickshell-preview"]
+39
View File
@@ -0,0 +1,39 @@
# Headless Quickshell previews
This directory provides a reproducible Docker wrapper around
[qsmcp](https://github.com/fedsfarm/qsmcp). It renders a Quickshell `Item` to a
PNG with Qt's `grabToImage()` without connecting to the host Wayland session.
Both nixpkgs and qsmcp are pinned in `Dockerfile`. The runner uses
`docker create` plus `docker cp` rather than bind mounts, so it also works with
rootless/remote daemons whose mount namespace cannot see the checkout.
## Smoke test
Start Docker, then run from the repository root:
```sh
./tools/quickshell-preview/render.sh
```
The default output is:
```text
/tmp/homelab-quickshell-preview/smoke.png
```
## Render another Item-rooted component
```sh
./tools/quickshell-preview/render.sh \
tests/DenseBarHeadless.qml \
.artifacts/quickshell-preview/dense-bar-1920.png \
1920 164
```
The component path is relative to `dotfiles/quickshell`. Keep production
`PanelWindow` wrappers thin and put their visual content in an `Item` component
so it can be rendered through this offscreen path.
`PanelWindow`-rooted previews require a nested compositor and are intentionally
outside this first smoke-test image.
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
mkdir -p /tmp/qsmcp-runtime /tmp/qsmcp-home /output
chmod 700 /tmp/qsmcp-runtime
export HOME=/tmp/qsmcp-home
export XDG_RUNTIME_DIR=/tmp/qsmcp-runtime
exec /nix/var/nix/profiles/default/bin/node \
/opt/qsmcp/src/index.ts render "$@"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)"
tool_dir="$repo_root/tools/quickshell-preview"
shell_root="$repo_root/dotfiles/quickshell"
image="homelab-quickshell-preview:qsmcp-76b1b008"
component="${1:-tests/HeadlessSmoke.qml}"
output="${2:-${TMPDIR:-/tmp}/homelab-quickshell-preview/smoke.png}"
width="${3:-720}"
height="${4:-120}"
if ! docker info >/dev/null 2>&1; then
echo "error: Docker-compatible daemon is unavailable" >&2
exit 69
fi
case "$output" in
/*) ;;
*) output="$PWD/$output" ;;
esac
output_dir="$(dirname "$output")"
output_name="$(basename "$output")"
mkdir -p "$output_dir"
container_id=""
cleanup() {
if [[ -n "$container_id" ]]; then
docker rm --force "$container_id" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
docker build --tag "$image" --file "$tool_dir/Dockerfile" "$tool_dir"
container_id="$(docker create \
--network none \
--tmpfs /tmp:rw,noexec,nosuid,size=256m \
"$image" \
"$component" \
--width "$width" \
--height "$height" \
--dpr 1 \
--padding 0 \
--background solid \
--bg '#0a0a0a' \
--out "/output/$output_name")"
# docker cp streams files through the API, so this works with rootless and
# remote daemons whose filesystem cannot see the client's repository path.
docker cp "$shell_root/." "$container_id:/workspace/quickshell"
docker start --attach "$container_id"
docker cp "$container_id:/output/$output_name" "$output"
test -s "$output"
printf 'rendered=%s\n' "$output"