pragma Singleton import QtQuick import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io import qs.Commons Singleton { id: root // Public values property real cpuUsage: 0 property real cpuTemp: 0 property real gpuTemp: 0 property real memGb: 0 property real memPercent: 0 property real diskPercent: 0 property real rxSpeed: 0 property real txSpeed: 0 // Configuration property int sleepDuration: 3000 // Internal state for CPU calculation property var prevCpuStats: null // Internal state for network speed calculation // Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered // since the computer started, so their value will easily overlfow a 32bit int. property real prevRxBytes: 0 property real prevTxBytes: 0 property real prevTime: 0 // Cpu temperature is the most complex readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"] property string cpuTempSensorName: "" property string cpuTempHwmonPath: "" // Gpu temperature (simple hwmon read if available) readonly property var supportedTempGpuSensorNames: ["amdgpu", "nvidia", "radeon"] property string gpuTempSensorName: "" property string gpuTempHwmonPath: "" property bool gpuIsDedicated: false property string _gpuPendingAmdPath: "" // For Intel coretemp averaging of all cores/sensors property var intelTempValues: [] property int intelTempFilesChecked: 0 property int intelTempMaxFiles: 20 // Will test up to temp20_input // -------------------------------------------- Component.onCompleted: { Logger.log("SystemStat", "Service started with interval:", root.sleepDuration, "ms") // Kickoff the cpu name detection for temperature cpuTempNameReader.checkNext() } // -------------------------------------------- // Timer for periodic updates Timer { id: updateTimer interval: root.sleepDuration repeat: true running: true triggeredOnStart: true onTriggered: { // Trigger all direct system files reads memInfoFile.reload() cpuStatFile.reload() netDevFile.reload() // Run df (disk free) one time dfProcess.running = true updateCpuTemperature() updateGpuTemperature() } } // -------------------------------------------- // FileView components for reading system files FileView { id: memInfoFile path: "/proc/meminfo" onLoaded: parseMemoryInfo(text()) } FileView { id: cpuStatFile path: "/proc/stat" onLoaded: calculateCpuUsage(text()) } FileView { id: netDevFile path: "/proc/net/dev" onLoaded: calculateNetworkSpeed(text()) } // -------------------------------------------- // Process to fetch disk usage in percent // Uses 'df' aka 'disk free' Process { id: dfProcess command: ["df", "--output=pcent", "/"] running: false stdout: StdioCollector { onStreamFinished: { const lines = text.trim().split('\n') if (lines.length >= 2) { const percent = lines[1].replace(/[^0-9]/g, '') root.diskPercent = parseInt(percent) || 0 } } } } // -------------------------------------------- // -------------------------------------------- // CPU Temperature // It's more complex. // ---- // #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower" FileView { id: cpuTempNameReader property int currentIndex: 0 printErrors: false function checkNext() { if (currentIndex >= 16) { // Check up to hwmon10 Logger.warn("No supported temperature sensor found") return } //Logger.log("SystemStat", "---- Probing: hwmon", currentIndex) cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name` cpuTempNameReader.reload() } onLoaded: { const name = text().trim() if (root.supportedTempCpuSensorNames.includes(name)) { root.cpuTempSensorName = name root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}` Logger.log("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`) } else { currentIndex++ Qt.callLater(() => { // Qt.callLater is mandatory checkNext() }) } } onLoadFailed: function (error) { currentIndex++ Qt.callLater(() => { // Qt.callLater is mandatory checkNext() }) } } // ---- // #2 - Read sensor value FileView { id: cpuTempReader printErrors: false onLoaded: { const data = text().trim() if (root.cpuTempSensorName === "coretemp") { // For Intel, collect all temperature values const temp = parseInt(data) / 1000.0 //console.log(temp, cpuTempReader.path) root.intelTempValues.push(temp) Qt.callLater(() => { // Qt.callLater is mandatory checkNextIntelTemp() }) } else { // For AMD sensors (k10temp and zenpower), directly set the temperature root.cpuTemp = Math.round(parseInt(data) / 1000.0) } } onLoadFailed: function (error) { Qt.callLater(() => { // Qt.callLater is mandatory checkNextIntelTemp() }) } } // -------------------------------------------- // -------------------------------------------- // ---- GPU temperature detection (hwmon) FileView { id: gpuTempNameReader property int currentIndex: 0 printErrors: false function checkNext() { if (currentIndex >= 16) { // Check up to hwmon10 Logger.warn("SystemStat", "No supported GPU temperature sensor found") return } gpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name` gpuTempNameReader.reload() } Component.onCompleted: checkNext() onLoaded: { const name = text().trim() if (root.supportedTempGpuSensorNames.includes(name)) { const hwPath = `/sys/class/hwmon/hwmon${currentIndex}` if (name === "nvidia") { // Treat NVIDIA as dedicated by default root.gpuTempSensorName = name root.gpuTempHwmonPath = hwPath root.gpuIsDedicated = true Logger.log("SystemStat", `Selected NVIDIA GPU thermal sensor at ${root.gpuTempHwmonPath}`) } else if (name === "amdgpu") { // Probe VRAM to distinguish dGPU vs iGPU root._gpuPendingAmdPath = hwPath vramReader.requestCheck(hwPath) } else if (!root.gpuTempHwmonPath) { // Fallback to first supported sensor (e.g., radeon) root.gpuTempSensorName = name root.gpuTempHwmonPath = hwPath Logger.log("SystemStat", `Selected GPU thermal sensor at ${root.gpuTempHwmonPath}`) } } else { currentIndex++ Qt.callLater(() => { checkNext() }) } } onLoadFailed: function (error) { currentIndex++ Qt.callLater(() => { checkNext() }) } } // Reader to detect AMD dGPU by checking VRAM presence FileView { id: vramReader property string targetHwmonPath: "" function requestCheck(hwPath) { targetHwmonPath = hwPath vramReader.path = `${hwPath}/device/mem_info_vram_total` vramReader.reload() } printErrors: false onLoaded: { const val = parseInt(text().trim()) // If VRAM present (>0), prefer this as dGPU if (!isNaN(val) && val > 0) { root.gpuTempSensorName = "amdgpu" root.gpuTempHwmonPath = targetHwmonPath root.gpuIsDedicated = true Logger.log("SystemStat", `Selected AMD dGPU (VRAM=${Math.round(val / (1024 * 1024 * 1024))}GB) at ${root.gpuTempHwmonPath}`) } else if (!root.gpuTempHwmonPath) { // Use as fallback iGPU if nothing selected yet root.gpuTempSensorName = "amdgpu" root.gpuTempHwmonPath = targetHwmonPath root.gpuIsDedicated = false Logger.log("SystemStat", `Selected AMD GPU (no VRAM) at ${root.gpuTempHwmonPath}`) } // Continue scanning other hwmon entries gpuTempNameReader.currentIndex++ Qt.callLater(() => { gpuTempNameReader.checkNext() }) } onLoadFailed: function (error) { // If failed to read VRAM, consider as fallback if none selected if (!root.gpuTempHwmonPath) { root.gpuTempSensorName = "amdgpu" root.gpuTempHwmonPath = targetHwmonPath } gpuTempNameReader.currentIndex++ Qt.callLater(() => { gpuTempNameReader.checkNext() }) } } // ------------------------------------------------------- // ------------------------------------------------------- // Parse memory info from /proc/meminfo function parseMemoryInfo(text) { if (!text) return const lines = text.split('\n') let memTotal = 0 let memAvailable = 0 for (const line of lines) { if (line.startsWith('MemTotal:')) { memTotal = parseInt(line.split(/\s+/)[1]) || 0 } else if (line.startsWith('MemAvailable:')) { memAvailable = parseInt(line.split(/\s+/)[1]) || 0 } } if (memTotal > 0) { const usageKb = memTotal - memAvailable root.memGb = (usageKb / 1000000).toFixed(1) root.memPercent = Math.round((usageKb / memTotal) * 100) } } // ------------------------------------------------------- // Calculate CPU usage from /proc/stat function calculateCpuUsage(text) { if (!text) return const lines = text.split('\n') const cpuLine = lines[0] // First line is total CPU if (!cpuLine.startsWith('cpu ')) return const parts = cpuLine.split(/\s+/) const stats = { "user": parseInt(parts[1]) || 0, "nice": parseInt(parts[2]) || 0, "system": parseInt(parts[3]) || 0, "idle": parseInt(parts[4]) || 0, "iowait": parseInt(parts[5]) || 0, "irq": parseInt(parts[6]) || 0, "softirq": parseInt(parts[7]) || 0, "steal": parseInt(parts[8]) || 0, "guest": parseInt(parts[9]) || 0, "guestNice": parseInt(parts[10]) || 0 } const totalIdle = stats.idle + stats.iowait const total = Object.values(stats).reduce((sum, val) => sum + val, 0) if (root.prevCpuStats) { const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => sum + val, 0) const diffTotal = total - prevTotal const diffIdle = totalIdle - prevTotalIdle if (diffTotal > 0) { root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1) } } root.prevCpuStats = stats } // ------------------------------------------------------- // Calculate RX and TX speed from /proc/net/dev // Average speed of all interfaces excepted 'lo' function calculateNetworkSpeed(text) { if (!text) { return } const currentTime = Date.now() / 1000 const lines = text.split('\n') let totalRx = 0 let totalTx = 0 for (var i = 2; i < lines.length; i++) { const line = lines[i].trim() if (!line) { continue } const colonIndex = line.indexOf(':') if (colonIndex === -1) { continue } const iface = line.substring(0, colonIndex).trim() if (iface === 'lo') { continue } const statsLine = line.substring(colonIndex + 1).trim() const stats = statsLine.split(/\s+/) const rxBytes = parseInt(stats[0], 10) || 0 const txBytes = parseInt(stats[8], 10) || 0 totalRx += rxBytes totalTx += txBytes } // Compute only if we have a previous run to compare to. if (root.prevTime > 0) { const timeDiff = currentTime - root.prevTime // Avoid division by zero if time hasn't passed. if (timeDiff > 0) { let rxDiff = totalRx - root.prevRxBytes let txDiff = totalTx - root.prevTxBytes // Handle counter resets (e.g., WiFi reconnect), which would cause a negative value. if (rxDiff < 0) { rxDiff = 0 } if (txDiff < 0) { txDiff = 0 } root.rxSpeed = Math.round(rxDiff / timeDiff) // Speed in Bytes/s root.txSpeed = Math.round(txDiff / timeDiff) } } root.prevRxBytes = totalRx root.prevTxBytes = totalTx root.prevTime = currentTime } // ------------------------------------------------------- // Helper function to format network speeds function formatSpeed(bytesPerSecond) { if (bytesPerSecond < 1024 * 1024) { return (bytesPerSecond / 1024).toFixed(1) + "KB/s" } else if (bytesPerSecond < 1024 * 1024 * 1024) { return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s" } else { return (bytesPerSecond / (1024 * 1024 * 1024)).toFixed(1) + "GB/s" } } // ------------------------------------------------------- // Function to start fetching and computing the cpu temperature function updateCpuTemperature() { // For AMD sensors (k10temp and zenpower), only use Tctl sensor // temp1_input corresponds to Tctl (Temperature Control) on these sensors if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") { cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input` cpuTempReader.reload() } // For Intel coretemp, start averaging all available sensors/cores else if (root.cpuTempSensorName === "coretemp") { root.intelTempValues = [] root.intelTempFilesChecked = 0 checkNextIntelTemp() } } // ------------------------------------------------------- // Function to start/refresh the GPU temperature function updateGpuTemperature() { if (!root.gpuTempHwmonPath) return gpuTempReader.path = `${root.gpuTempHwmonPath}/temp1_input` gpuTempReader.reload() } FileView { id: gpuTempReader printErrors: false onLoaded: { const data = parseInt(text().trim()) if (!isNaN(data)) { root.gpuTemp = Math.round(data / 1000.0) } } } // ------------------------------------------------------- // Function to check next Intel temperature sensor function checkNextIntelTemp() { if (root.intelTempFilesChecked >= root.intelTempMaxFiles) { // Calculate average of all found temperatures if (root.intelTempValues.length > 0) { let sum = 0 for (var i = 0; i < root.intelTempValues.length; i++) { sum += root.intelTempValues[i] } root.cpuTemp = Math.round(sum / root.intelTempValues.length) //Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`) } else { Logger.warn("SystemStat", "No temperature sensors found for coretemp") root.cpuTemp = 0 } return } // Check next temperature file root.intelTempFilesChecked++ cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input` cpuTempReader.reload() } }