feat(quickshell): add dense telemetry bar
This commit is contained in:
@@ -14,3 +14,6 @@ keys.txt
|
|||||||
|
|
||||||
# local env (PATH etc.)
|
# local env (PATH etc.)
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# local visual-verification output
|
||||||
|
.artifacts/
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
@@ -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.
|
||||||
@@ -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 "$@"
|
||||||
Executable
+58
@@ -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"
|
||||||
Reference in New Issue
Block a user