import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import Quickshell.Services.SystemTray import Quickshell.Widgets import QtQuick import QtQuick.Layouts import QtQuick.Window import "../../shared" as Shared Item { id: root implicitWidth: Shared.Theme.popoutWidth implicitHeight: Math.min(flickable.contentHeight + Shared.Theme.popoutPadding * 2, Screen.height - 32) PopoutBackground { anchors.fill: parent } // --- State --- readonly property int historySize: 30 property var cpuHistory: [] property var memHistory: [] property var gpuHistory: [] function pushHistory(arr, val) { let a = arr.slice(); a.push(val); if (a.length > historySize) a.shift(); return a; } property real cpuVal: 0 property string memText: "--" property real memVal: 0 property string tempText: "--" property int tempVal: 0 property string gpuText: "--" property real gpuUsage: 0 property int gpuTemp: 0 property string nvmeTempText: "--" property int nvmeTempVal: 0 property string diskRootText: "--" property real diskRootVal: 0 property string diskDataText: "--" property real diskDataVal: 0 property string updatesText: "" property string updatesClass: "" property bool updatesExpanded: false property var updatesPackages: [] property string alhpText: "" property string alhpClass: "" property bool alhpExpanded: false property var alhpPackages: [] property string networkIp: "--" property string networkIface: "--" property bool idleActive: false property var panelWindow: null property string audioDrawer: "" // "" = closed, "sink" or "source" function thresholdColor(val, warn, crit) { if (val >= crit) return Shared.Theme.danger; if (val >= warn) return Shared.Theme.warning; return Shared.Theme.success; } function tempColor(t) { if (t >= 85) return Shared.Theme.danger; if (t >= 70) return Shared.Theme.warning; return Shared.Theme.success; } MouseArea { anchors.fill: parent } Flickable { id: flickable anchors.fill: parent anchors.margins: Shared.Theme.popoutPadding contentHeight: col.implicitHeight clip: true boundsBehavior: Flickable.StopAtBounds ColumnLayout { id: col width: flickable.width spacing: 6 // ─── UPDATES ───────────────────────── // updates pending — clickable, expands package list Loader { Layout.fillWidth: true active: root.updatesClass === "pending" || root.updatesClass === "many" visible: active sourceComponent: Rectangle { implicitHeight: updRow.implicitHeight + 14 radius: Shared.Theme.radiusSmall color: updMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0 Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } } RowLayout { id: updRow anchors.fill: parent anchors.margins: 8 spacing: 10 Text { text: "\u{f0ab7}"; color: Shared.Theme.warning; font.pixelSize: 14; font.family: Shared.Theme.iconFont } Text { text: root.updatesText + " updates available"; color: Shared.Theme.text; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily; Layout.fillWidth: true } Text { text: "\u{f0140}" color: Shared.Theme.overlay0 font.pixelSize: 12 font.family: Shared.Theme.iconFont rotation: root.updatesExpanded ? 180 : 0 Behavior on rotation { NumberAnimation { duration: Shared.Theme.animFast } } } } MouseArea { id: updMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.updatesExpanded = !root.updatesExpanded } } } // updates expanded package list Loader { Layout.fillWidth: true active: root.updatesExpanded && root.updatesPackages.length > 0 visible: active sourceComponent: Rectangle { radius: Shared.Theme.radiusSmall color: Shared.Theme.surface0 implicitHeight: pkgCol.implicitHeight + 12 ColumnLayout { id: pkgCol anchors.fill: parent anchors.margins: 6 spacing: 2 Repeater { model: root.updatesPackages Text { required property string modelData text: modelData color: Shared.Theme.subtext0 font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily Layout.fillWidth: true elide: Text.ElideRight } } } } } // system up to date Loader { Layout.fillWidth: true active: root.updatesClass !== "pending" && root.updatesClass !== "many" && root.updatesClass !== "" visible: active sourceComponent: RowLayout { spacing: 10 Text { text: "\u{f05e0}"; color: Shared.Theme.success; font.pixelSize: 14; font.family: Shared.Theme.iconFont } Text { text: "System up to date"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily } } } // ALHP good Loader { Layout.fillWidth: true active: root.alhpClass === "good" visible: active sourceComponent: RowLayout { spacing: 10 Text { text: "\u{f05e0}"; color: Shared.Theme.success; font.pixelSize: 14; font.family: Shared.Theme.iconFont } Text { text: "ALHP \u2713"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily } } } // ALHP mirror stale Loader { Layout.fillWidth: true active: root.alhpClass === "stale" visible: active sourceComponent: RowLayout { spacing: 10 Text { text: "\u{f0026}"; color: Shared.Theme.warning; font.pixelSize: 14; font.family: Shared.Theme.iconFont } Text { text: "ALHP mirror outdated"; color: Shared.Theme.warning; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily } } } // ALHP packages building — clickable, expands package list Loader { Layout.fillWidth: true active: root.alhpClass === "bad" visible: active sourceComponent: RowLayout { spacing: 10 Text { text: "\u{f0954}"; color: Shared.Theme.danger; font.pixelSize: 14; font.family: Shared.Theme.iconFont } Text { text: "ALHP " + root.alhpText + " building" color: Shared.Theme.danger font.pixelSize: Shared.Theme.fontSize font.family: Shared.Theme.fontFamily Layout.fillWidth: true } Text { visible: root.alhpPackages.length > 0 text: "\u{f0140}" color: Shared.Theme.overlay0 font.pixelSize: 12 font.family: Shared.Theme.iconFont rotation: root.alhpExpanded ? 180 : 0 Behavior on rotation { NumberAnimation { duration: Shared.Theme.animFast } } } MouseArea { anchors.fill: parent hoverEnabled: root.alhpPackages.length > 0 cursorShape: root.alhpPackages.length > 0 ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: if (root.alhpPackages.length > 0) root.alhpExpanded = !root.alhpExpanded } } } // ALHP expanded package list Loader { Layout.fillWidth: true active: root.alhpExpanded && root.alhpPackages.length > 0 visible: active sourceComponent: Rectangle { radius: Shared.Theme.radiusSmall color: Shared.Theme.surface0 implicitHeight: alhpPkgCol.implicitHeight + 12 ColumnLayout { id: alhpPkgCol anchors.fill: parent anchors.margins: 6 spacing: 2 Repeater { model: root.alhpPackages Text { required property string modelData text: modelData color: Shared.Theme.subtext0 font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily Layout.fillWidth: true elide: Text.ElideRight } } } } } // ─── separator ─── Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 } // ─── STORAGE & PERFORMANCE ─────────── MetricBar { label: Shared.Config.diskMount1Label; value: root.diskRootText; fill: root.diskRootVal; barColor: thresholdColor(root.diskRootVal * 100, 70, 90); valueColor: thresholdColor(root.diskRootVal * 100, 70, 90) } MetricBar { label: Shared.Config.diskMount2Label; value: root.diskDataText; fill: root.diskDataVal; barColor: thresholdColor(root.diskDataVal * 100, 70, 90); valueColor: thresholdColor(root.diskDataVal * 100, 70, 90) } Item { implicitHeight: 2 } MetricBar { label: "CPU" value: root.cpuVal.toFixed(0) + "%" fill: root.cpuVal / 100 barColor: thresholdColor(root.cpuVal, 50, 80) valueColor: thresholdColor(root.cpuVal, 50, 80) suffix: root.tempText suffixColor: tempColor(root.tempVal) history: root.cpuHistory } MetricBar { label: "Mem" value: root.memText fill: root.memVal barColor: thresholdColor(root.memVal * 100, 60, 85) valueColor: thresholdColor(root.memVal * 100, 60, 85) history: root.memHistory } MetricBar { label: "GPU" value: root.gpuText fill: root.gpuUsage / 100 barColor: thresholdColor(root.gpuUsage, 50, 80) valueColor: thresholdColor(root.gpuUsage, 50, 80) history: root.gpuHistory } MetricBar { label: "NVMe" value: root.nvmeTempText fill: root.nvmeTempVal / 100 barColor: tempColor(root.nvmeTempVal) valueColor: tempColor(root.nvmeTempVal) } // ─── separator ─── Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 } // ─── AUDIO ─────────────────────────── RowLayout { Layout.fillWidth: true spacing: 8 VolumeSlider { Layout.fillWidth: true audio: Pipewire.defaultAudioSink?.audio ?? null icon: (!audio || audio.muted) ? "\u{f057f}" : "\u{f057e}" label: "Out" accentColor: Shared.Theme.sky drawerActive: root.audioDrawer === "sink" onDrawerToggled: root.audioDrawer = root.audioDrawer === "sink" ? "" : "sink" } VolumeSlider { Layout.fillWidth: true audio: Pipewire.defaultAudioSource?.audio ?? null icon: (!audio || audio.muted) ? "\u{f036d}" : "\u{f036c}" label: "Mic" accentColor: Shared.Theme.pink drawerActive: root.audioDrawer === "source" onDrawerToggled: root.audioDrawer = root.audioDrawer === "source" ? "" : "source" } } // Device drawer Loader { Layout.fillWidth: true active: root.audioDrawer !== "" visible: active sourceComponent: ColumnLayout { spacing: 3 Repeater { model: Pipewire.nodes Rectangle { id: devItem required property var modelData readonly property bool isSinkDrawer: root.audioDrawer === "sink" readonly property bool matchesFilter: modelData.audio && !modelData.isStream && modelData.isSink === isSinkDrawer readonly property bool isDefault: isSinkDrawer ? Pipewire.defaultAudioSink === modelData : Pipewire.defaultAudioSource === modelData visible: matchesFilter Layout.fillWidth: true implicitHeight: matchesFilter ? 28 : 0 radius: Shared.Theme.radiusSmall color: devMouse.containsMouse ? Shared.Theme.surface1 : (isDefault ? Shared.Theme.surface0 : "transparent") Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } } RowLayout { anchors.fill: parent anchors.leftMargin: 8 anchors.rightMargin: 8 spacing: 6 Text { text: devItem.isDefault ? "\u{f012c}" : "\u{f0765}" color: devItem.isDefault ? (devItem.isSinkDrawer ? Shared.Theme.sky : Shared.Theme.pink) : Shared.Theme.overlay0 font.pixelSize: 12 font.family: Shared.Theme.iconFont } Text { text: devItem.modelData.description || devItem.modelData.name color: devItem.isDefault ? Shared.Theme.text : Shared.Theme.subtext0 font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily font.bold: devItem.isDefault Layout.fillWidth: true elide: Text.ElideRight } } MouseArea { id: devMouse anchors.fill: parent hoverEnabled: true onClicked: { if (devItem.isSinkDrawer) Pipewire.preferredDefaultAudioSink = devItem.modelData; else Pipewire.preferredDefaultAudioSource = devItem.modelData; root.audioDrawer = ""; } } } } } } // ─── separator ─── Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 } // ─── NETWORK ───────────────────────── RowLayout { Layout.fillWidth: true spacing: 10 Text { text: "\u{f0bf0}"; color: Shared.Theme.overlay0; font.pixelSize: 14; font.family: Shared.Theme.iconFont } Text { text: root.networkIface; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily } Item { Layout.fillWidth: true } Text { text: root.networkIp; color: Shared.Theme.text; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily; font.bold: true } } // ─── separator ─── Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 } // ─── SYSTRAY ───────────────────────── Flow { Layout.fillWidth: true spacing: 6 Repeater { model: SystemTray.items Rectangle { id: trayItem required property var modelData visible: modelData.status !== Status.Passive width: 32 height: 32 radius: Shared.Theme.radiusSmall color: trayMouse.containsMouse ? Shared.Theme.surface1 : "transparent" Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } } IconImage { anchors.centerIn: parent source: trayItem.modelData.icon implicitSize: 22 } // Tooltip — positioned above icon, clamped to popout width Rectangle { id: tooltip visible: trayMouse.containsMouse && tipText.text !== "" width: Math.min(tipText.implicitWidth + 12, Shared.Theme.popoutWidth - Shared.Theme.popoutPadding * 2) height: tipText.implicitHeight + 8 radius: 6 color: Shared.Theme.surface0 z: 10 // Position above the icon, clamp horizontally within the popout y: -height - 4 x: { let centered = (trayItem.width - width) / 2; let globalX = trayItem.mapToItem(col, centered, 0).x; let maxX = col.width - width; let clampedGlobalX = Math.max(0, Math.min(globalX, maxX)); return centered + (clampedGlobalX - globalX); } Text { id: tipText anchors.centerIn: parent width: parent.width - 12 text: trayItem.modelData.tooltipTitle || trayItem.modelData.title || "" color: Shared.Theme.text font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily elide: Text.ElideRight maximumLineCount: 1 } } MouseArea { id: trayMouse anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onClicked: event => { let item = trayItem.modelData; if (event.button === Qt.RightButton || item.onlyMenu) { if (item.hasMenu && root.panelWindow) { let pos = trayItem.mapToItem(null, trayItem.width / 2, trayItem.height / 2); item.display(root.panelWindow, pos.x, pos.y); } } else if (event.button === Qt.MiddleButton) { item.secondaryActivate(); } else { item.activate(); } } onWheel: event => { trayItem.modelData.scroll(event.angleDelta.y, false); } } } } } // ─── separator ─── Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 6; Layout.bottomMargin: 6 } // ─── ACTIONS ───────────────────────── RowLayout { Layout.fillWidth: true spacing: 6 ActionIcon { icon: "\u{f1436}" label: "Idle" iconColor: root.idleActive ? Shared.Theme.green : Shared.Theme.overlay0 onActivated: { if (root.idleActive) idleKill.running = true; else idleStart.running = true; root.idleActive = !root.idleActive; } } ActionIcon { icon: "\u{f033e}" label: "Lock" iconColor: Shared.Theme.subtext0 onActivated: { Shared.PopoutState.close(); lockProc.running = true; } } Item { Layout.fillWidth: true } HoldAction { icon: "\u{f0425}" label: "Logout" iconColor: Shared.Theme.subtext0 holdColor: Shared.Theme.green onConfirmed: { Shared.PopoutState.close(); logoutProc.running = true; } } HoldAction { icon: "\u{f0709}" label: "Reboot" iconColor: Shared.Theme.peach holdColor: Shared.Theme.peach onConfirmed: { Shared.PopoutState.close(); rebootProc.running = true; } } HoldAction { icon: "\u{f0425}" label: "Off" iconColor: Shared.Theme.danger holdColor: Shared.Theme.danger onConfirmed: { Shared.PopoutState.close(); offProc.running = true; } } } } // ColumnLayout } // Flickable // ═══════════════════════════════════════ // COMPONENTS // ═══════════════════════════════════════ component Sparkline: Canvas { id: spark property var history: [] property color lineColor: Shared.Theme.overlay0 onHistoryChanged: requestPaint() onWidthChanged: requestPaint() onHeightChanged: requestPaint() onPaint: { let ctx = getContext("2d"); ctx.clearRect(0, 0, width, height); if (history.length < 2) return; ctx.strokeStyle = Qt.alpha(lineColor, 0.5); ctx.lineWidth = 1; ctx.beginPath(); let step = width / (root.historySize - 1); let offset = root.historySize - history.length; for (let i = 0; i < history.length; i++) { let x = (offset + i) * step; let y = height - history[i] * height; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } } component MetricBar: RowLayout { property string label property string value property real fill: 0 property color barColor: Shared.Theme.green property color valueColor: Shared.Theme.text property string suffix: "" property color suffixColor: Shared.Theme.overlay0 property var history: null Layout.fillWidth: true implicitHeight: 22 spacing: 10 Text { text: parent.label color: Shared.Theme.overlay0 font.pixelSize: Shared.Theme.fontSize font.family: Shared.Theme.fontFamily Layout.preferredWidth: 40 } Rectangle { Layout.fillWidth: true implicitHeight: 10 radius: 5 color: Shared.Theme.surface0 Sparkline { anchors.fill: parent visible: history && history.length > 1 history: parent.parent.history ?? [] lineColor: barColor } Rectangle { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: parent.width * Math.min(1, Math.max(0, fill)) radius: 5 color: barColor opacity: Shared.Theme.opacityFill Behavior on width { NumberAnimation { duration: Shared.Theme.animFast; easing.type: Easing.OutCubic } } Behavior on color { ColorAnimation { duration: Shared.Theme.animNormal } } } } Text { text: parent.value color: parent.valueColor font.pixelSize: Shared.Theme.fontSize font.family: Shared.Theme.fontFamily font.bold: true horizontalAlignment: Text.AlignRight Layout.preferredWidth: 86 } Loader { active: parent.suffix !== "" visible: active sourceComponent: Text { text: suffix color: suffixColor font.pixelSize: Shared.Theme.fontSize font.family: Shared.Theme.fontFamily font.bold: true } } } component VolumeSlider: Rectangle { id: vs property var audio: null property string icon property string label property color accentColor property bool drawerActive: false signal drawerToggled() property real vol: 0 property bool muted: audio?.muted ?? false function updateVol() { if (audio && !isNaN(audio.volume) && audio.volume >= 0) vol = audio.volume; } onAudioChanged: { updateVol(); bindRetry.retries = 0; bindRetry.running = true; } Connections { target: vs.audio function onVolumeChanged() { vs.updateVol(); } } // Retry binding pickup after async PwObjectTracker re-bind Timer { id: bindRetry interval: 300 running: false repeat: true property int retries: 0 onTriggered: { vs.updateVol(); retries++; if (vs.vol > 0 || retries >= 5) running = false; } } Layout.fillWidth: true implicitHeight: 36 radius: Shared.Theme.radiusSmall color: drawerActive ? Qt.alpha(accentColor, Shared.Theme.opacityLight) : Shared.Theme.surface0 border.width: drawerActive ? 1 : 0 border.color: Qt.alpha(accentColor, Shared.Theme.opacityMedium) opacity: muted ? 0.45 : 1.0 clip: true Behavior on opacity { NumberAnimation { duration: Shared.Theme.animFast } } Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } } Rectangle { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: vs.muted ? 0 : parent.width * Math.min(1, vs.vol) radius: Shared.Theme.radiusSmall color: Qt.alpha(vs.accentColor, Shared.Theme.opacityMedium) Behavior on width { NumberAnimation { duration: Shared.Theme.animFast; easing.type: Easing.OutCubic } } } RowLayout { anchors.fill: parent anchors.margins: 8 spacing: 8 Text { text: vs.icon; color: vs.accentColor; font.pixelSize: 14; font.family: Shared.Theme.iconFont } Text { text: vs.label; color: vs.accentColor; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily } Item { Layout.fillWidth: true } Text { text: vs.muted ? "Muted" : Math.round(vs.vol * 100) + "%" color: vs.muted ? Shared.Theme.overlay0 : Shared.Theme.text font.pixelSize: Shared.Theme.fontSize font.family: Shared.Theme.fontFamily font.bold: !vs.muted } } MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton property bool dragging: false property real startX: 0 property int pressedBtn: Qt.NoButton onClicked: event => { if (event.button === Qt.MiddleButton) { pavuProc.running = true; return; } if (event.button === Qt.RightButton) { if (vs.audio) vs.audio.muted = !vs.audio.muted; return; } if (!dragging) vs.drawerToggled(); } onPressed: event => { dragging = false; startX = event.x; pressedBtn = event.button; } onPositionChanged: event => { if (pressed && pressedBtn === Qt.LeftButton) { if (Math.abs(event.x - startX) > 5) dragging = true; if (dragging && vs.audio) vs.audio.volume = Math.max(0, Math.min(1, event.x / vs.width)); } } onWheel: event => { if (!vs.audio) return; let step = 0.05; if (event.angleDelta.y > 0) vs.audio.volume = Math.min(1.0, vs.vol + step); else vs.audio.volume = Math.max(0.0, vs.vol - step); } } } component ActionIcon: Rectangle { property string icon property string label property color iconColor signal activated() implicitWidth: actCol.implicitWidth + 16 implicitHeight: actCol.implicitHeight + 14 radius: Shared.Theme.radiusSmall color: actMouse.containsMouse ? Shared.Theme.surface1 : "transparent" border.width: actMouse.containsMouse ? 1 : 0 border.color: Shared.Theme.surface2 Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } } Column { id: actCol anchors.centerIn: parent spacing: 3 Text { anchors.horizontalCenter: parent.horizontalCenter; text: icon; color: iconColor; font.pixelSize: 18; font.family: Shared.Theme.iconFont } Text { anchors.horizontalCenter: parent.horizontalCenter; text: label; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily } } MouseArea { id: actMouse; anchors.fill: parent; hoverEnabled: true; onClicked: parent.activated() } } component HoldAction: Rectangle { id: ha property string icon property string label property color iconColor property color holdColor: Shared.Theme.red signal confirmed() readonly property real holdDuration: 800 property real holdProgress: 0 property bool holding: false implicitWidth: haCol.implicitWidth + 16 implicitHeight: haCol.implicitHeight + 14 radius: Shared.Theme.radiusSmall color: haMouse.containsMouse ? Shared.Theme.surface1 : "transparent" border.width: haMouse.containsMouse || holding ? 1 : 0 border.color: holding ? Qt.alpha(holdColor, Shared.Theme.opacityStrong) : Shared.Theme.surface2 Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } } // Fill overlay Rectangle { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: parent.width * ha.holdProgress radius: Shared.Theme.radiusSmall color: Qt.alpha(ha.holdColor, Shared.Theme.opacityLight) } Column { id: haCol anchors.centerIn: parent spacing: 3 Text { anchors.horizontalCenter: parent.horizontalCenter; text: ha.icon; color: ha.holding ? ha.holdColor : ha.iconColor; font.pixelSize: 18; font.family: Shared.Theme.iconFont } Text { anchors.horizontalCenter: parent.horizontalCenter; text: ha.holding ? "Hold…" : ha.label; color: ha.holding ? ha.holdColor : Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily } } Timer { id: holdTimer interval: 16 running: ha.holding repeat: true onTriggered: { ha.holdProgress += interval / ha.holdDuration; if (ha.holdProgress >= 1.0) { ha.holding = false; ha.holdProgress = 0; ha.confirmed(); } } } MouseArea { id: haMouse anchors.fill: parent hoverEnabled: true onPressed: { ha.holding = true; ha.holdProgress = 0; } onReleased: { ha.holding = false; ha.holdProgress = 0; } onCanceled: { ha.holding = false; ha.holdProgress = 0; } } } // ═══════════════════════════════════════ // DATA FETCHING // ═══════════════════════════════════════ property real prevCpuActive: -1 property real prevCpuTotal: -1 Process { id: cpuProc; command: ["sh", "-c", "awk '/^cpu / {print $2+$4, $2+$4+$5}' /proc/stat"]; stdout: StdioCollector { onStreamFinished: { let p = this.text.trim().split(" "); let active = parseFloat(p[0]), total = parseFloat(p[1]); if (!isNaN(active) && root.prevCpuTotal > 0) { let da = active - root.prevCpuActive; let dt = total - root.prevCpuTotal; if (dt > 0) { root.cpuVal = (da / dt) * 100; root.cpuHistory = root.pushHistory(root.cpuHistory, root.cpuVal / 100); } } root.prevCpuActive = active; root.prevCpuTotal = total; } }} Process { id: memProc; command: ["sh", "-c", "free -m | awk '/Mem:/ {printf \"%.1f/%.0fG %.4f\", $3/1024, $2/1024, $3/$2}'"]; stdout: StdioCollector { onStreamFinished: { let p = this.text.trim().split(" "); if (p.length >= 2) { root.memText = p[0]; root.memVal = parseFloat(p[1]) || 0; root.memHistory = root.pushHistory(root.memHistory, root.memVal); } } }} Process { id: tempProc; command: ["sh", "-c", "sensors 2>/dev/null | awk '/Tctl:|Tdie:|Package id 0:/ {gsub(/\\+|°C/,\"\",$2); printf \"%d\", $2; exit}'"]; stdout: StdioCollector { onStreamFinished: { let v = parseInt(this.text.trim()); if (!isNaN(v)) { root.tempVal = v; root.tempText = v + "°C"; } } }} Process { id: gpuProc; command: [Shared.Config.gpuScript]; stdout: StdioCollector { onStreamFinished: { try { let d = JSON.parse(this.text); let tt = d.tooltip || ""; let lines = tt.split("\n"); let usage = 0, temp = 0, power = 0; for (let i = 0; i < lines.length; i++) { let v = parseInt(lines[i].replace(/[^0-9]/g, "")); if (isNaN(v)) continue; if (lines[i].indexOf("Usage") >= 0) usage = v; else if (lines[i].indexOf("Temp") >= 0) temp = v; else if (lines[i].indexOf("Power") >= 0) power = v; } root.gpuUsage = usage; root.gpuTemp = temp; root.gpuText = usage + "% " + temp + "°C " + power + "W"; root.gpuHistory = root.pushHistory(root.gpuHistory, usage / 100); } catch(e) {} } }} Process { id: nvmeTempProc; command: ["sh", "-c", "for d in /sys/class/hwmon/hwmon*; do if grep -q nvme \"$d/name\" 2>/dev/null; then awk '{printf \"%d\", $1/1000}' \"$d/temp1_input\" 2>/dev/null; break; fi; done"]; stdout: StdioCollector { onStreamFinished: { let v = parseInt(this.text.trim()); if (!isNaN(v) && v > 0) { root.nvmeTempVal = v; root.nvmeTempText = v + "°C"; } } }} Process { id: diskProc; command: ["sh", "-c", "df -h " + Shared.Config.diskMount1 + " --output=pcent,size 2>/dev/null | tail -1 && " + "df -h " + Shared.Config.diskMount2 + " --output=pcent,size 2>/dev/null | tail -1 && " + "df " + Shared.Config.diskMount1 + " --output=pcent 2>/dev/null | tail -1 && " + "df " + Shared.Config.diskMount2 + " --output=pcent 2>/dev/null | tail -1" ]; stdout: StdioCollector { onStreamFinished: { let lines = this.text.trim().split("\n"); if (lines.length >= 1) root.diskRootText = lines[0].trim().replace(/\s+/g, " of "); if (lines.length >= 2) root.diskDataText = lines[1].trim().replace(/\s+/g, " of "); if (lines.length >= 3) root.diskRootVal = parseInt(lines[2]) / 100 || 0; if (lines.length >= 4) root.diskDataVal = parseInt(lines[3]) / 100 || 0; } }} Process { id: updateProc; command: ["bash", "-c", "checkupdates 2>/dev/null; echo \":$?\""]; stdout: StdioCollector { onStreamFinished: { let lines = this.text.trim().split("\n"); let exitMatch = lines[lines.length - 1].match(/:(\d+)$/); let exit = exitMatch ? parseInt(exitMatch[1]) : 1; // checkupdates: exit 0 = updates available, exit 2 = no updates, else error if (exit !== 0 && exit !== 2) return; // error — keep previous state let pkgs = lines.filter(l => l.trim() !== "" && !l.match(/^:/)); let n = pkgs.length; root.updatesPackages = pkgs; if (n === 0) { root.updatesText = ""; root.updatesClass = "uptodate"; root.updatesExpanded = false; } else if (n > 50) { root.updatesText = n.toString(); root.updatesClass = "many"; } else { root.updatesText = n.toString(); root.updatesClass = "pending"; } } }} Process { id: alhpProc; command: ["sh", "-c", "alhp.utils -j 2>/dev/null || echo '{}'"]; stdout: StdioCollector { onStreamFinished: { try { let d = JSON.parse(this.text); let total = d.total || 0; let stale = d.mirror_out_of_date || false; root.alhpPackages = Array.isArray(d.packages) ? d.packages : []; if (stale) { root.alhpText = "stale"; root.alhpClass = "stale"; } else if (total > 0) { root.alhpText = total.toString(); root.alhpClass = "bad"; } else { root.alhpText = ""; root.alhpClass = "good"; root.alhpExpanded = false; } } catch(e) { root.alhpText = "?"; root.alhpClass = "down"; } } }} Process { id: netProc; command: ["sh", "-c", "ip -4 -o addr show | grep -v '" + Shared.Config.netExcludePattern + "' | head -1 | awk '{split($4,a,\"/\"); print $2 \":\" a[1]}'"]; stdout: StdioCollector { onStreamFinished: { let l = this.text.trim(); if (l) { let p = l.split(":"); root.networkIface = p[0] || "--"; root.networkIp = p[1] || "--"; } else { root.networkIface = "Network"; root.networkIp = "Offline"; } } }} Process { id: pavuProc; command: ["pavucontrol"] } Process { id: idleProc; command: ["pgrep", "-x", Shared.Config.idleProcess]; stdout: StdioCollector { onStreamFinished: root.idleActive = this.text.trim().length > 0 } } Process { id: idleKill; command: ["killall", Shared.Config.idleProcess] } Process { id: idleStart; command: ["sh", "-c", "setsid " + Shared.Config.idleProcess + " > /dev/null 2>&1 &"] } Process { id: lockProc; command: [Shared.Config.lockCommand] } Process { id: logoutProc; command: Shared.Config.powerActions[1].command } Process { id: rebootProc; command: Shared.Config.powerActions[2].command } Process { id: offProc; command: Shared.Config.powerActions[3].command } function rerun(proc) { proc.running = false; proc.running = true; } Timer { interval: 5000; running: true; repeat: true; onTriggered: { rerun(cpuProc); rerun(memProc); rerun(tempProc); rerun(gpuProc); rerun(nvmeTempProc); rerun(idleProc); } } Timer { interval: 30000; running: true; repeat: true; onTriggered: { rerun(diskProc); rerun(netProc); } } Timer { interval: 300000; running: true; repeat: true; onTriggered: { rerun(updateProc); rerun(alhpProc); } } // Stagger initial launches to avoid 9 concurrent process spawns Component.onCompleted: { rerun(cpuProc); rerun(memProc); rerun(tempProc); staggerTimer.running = true; } Timer { id: staggerTimer interval: 200 onTriggered: { rerun(gpuProc); rerun(nvmeTempProc); rerun(idleProc); rerun(diskProc); rerun(netProc); rerun(updateProc); rerun(alhpProc); } } }