diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index c8bea499f..eeb7ef736 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -43,6 +43,7 @@ Item { readonly property bool usePrimaryColor: widgetSettings.usePrimaryColor !== undefined ? widgetSettings.usePrimaryColor : widgetMetadata.usePrimaryColor readonly property bool useMonospaceFont: widgetSettings.useMonospaceFont !== undefined ? widgetSettings.useMonospaceFont : widgetMetadata.useMonospaceFont readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage + readonly property bool showCpuFreq: (widgetSettings.showCpuFreq !== undefined) ? widgetSettings.showCpuFreq : widgetMetadata.showCpuFreq readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp readonly property bool showGpuTemp: (widgetSettings.showGpuTemp !== undefined) ? widgetSettings.showGpuTemp : widgetMetadata.showGpuTemp readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage @@ -50,6 +51,7 @@ Item { readonly property bool showSwapUsage: (widgetSettings.showSwapUsage !== undefined) ? widgetSettings.showSwapUsage : widgetMetadata.showSwapUsage readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage + readonly property bool showDiskAsFree: (widgetSettings.showDiskAsFree !== undefined) ? widgetSettings.showDiskAsFree : widgetMetadata.showDiskAsFree readonly property bool showLoadAverage: (widgetSettings.showLoadAverage !== undefined) ? widgetSettings.showLoadAverage : widgetMetadata.showLoadAverage readonly property string diskPath: (widgetSettings.diskPath !== undefined) ? widgetSettings.diskPath : widgetMetadata.diskPath readonly property string fontFamily: useMonospaceFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault @@ -75,7 +77,7 @@ Item { let lines = []; // CPU - lines.push(`${I18n.tr("system-monitor.cpu-usage")}: ${Math.round(SystemStatService.cpuUsage)}%`); + lines.push(`${I18n.tr("system-monitor.cpu-usage")}: ${Math.round(SystemStatService.cpuUsage)}% (${SystemStatService.cpuFreq})`); if (SystemStatService.cpuTemp > 0) { lines.push(`${I18n.tr("system-monitor.cpu-temp")}: ${Math.round(SystemStatService.cpuTemp)}°C`); } @@ -107,7 +109,9 @@ Item { if (diskPercent !== undefined) { const usedGb = SystemStatService.diskUsedGb[diskPath] || 0; const sizeGb = SystemStatService.diskSizeGb[diskPath] || 0; - lines.push(`${I18n.tr("system-monitor.disk")}: ${usedGb.toFixed(1)} GB / ${sizeGb.toFixed(1)} GB (${diskPercent}%)`); + const availGb = SystemStatService.diskAvailGb[diskPath] || 0; + lines.push(`${I18n.tr("system-monitor.disk")}: ${usedGb.toFixed(1)}G / ${sizeGb.toFixed(1)}G Used (${diskPercent}%)`); + lines.push(`Available: ${availGb.toFixed(1)}G`); } return lines.join("\n"); @@ -279,6 +283,74 @@ Item { } } + // CPU Frequency Component + Item { + id: cpuFreqContainer + implicitWidth: cpuFreqContent.implicitWidth + implicitHeight: cpuFreqContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showCpuFreq && (!isVertical || compactMode) + + GridLayout { + id: cpuFreqContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "cpu-usage" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: Color.mOnSurface + } + } + + // Text mode + NText { + visible: !compactMode + text: SystemStatService.cpuFreq.replace(" ", "") + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.cpuFreqRatio); + item.statColor = Qt.binding(() => Color.mPrimary); + } + } + } + } + // CPU Temperature Component Item { id: cpuTempContainer @@ -788,7 +860,14 @@ Item { // Text mode NText { visible: !compactMode - text: SystemStatService.diskPercents[diskPath] ? `${SystemStatService.diskPercents[diskPath]}%` : "n/a" + text: { + if (showDiskAsFree && !isVertical) { + let avail = SystemStatService.diskAvailGb[diskPath] || 0; + return avail.toFixed(1) + "G"; + } else { + return SystemStatService.diskPercents[diskPath] ? `${SystemStatService.diskPercents[diskPath]}%` : "n/a"; + } + } family: fontFamily pointSize: barFontSize applyUiScale: false diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml index fbdd1ed29..9e55cfba0 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml @@ -20,6 +20,7 @@ ColumnLayout { property bool valueUsePrimaryColor: widgetData.usePrimaryColor !== undefined ? widgetData.usePrimaryColor : widgetMetadata.usePrimaryColor property bool valueUseMonospaceFont: widgetData.useMonospaceFont !== undefined ? widgetData.useMonospaceFont : widgetMetadata.useMonospaceFont property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage + property bool valueShowCpuFreq: widgetData.showCpuFreq !== undefined ? widgetData.showCpuFreq : widgetMetadata.showCpuFreq property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp property bool valueShowGpuTemp: widgetData.showGpuTemp !== undefined ? widgetData.showGpuTemp : widgetMetadata.showGpuTemp property bool valueShowLoadAverage: widgetData.showLoadAverage !== undefined ? widgetData.showLoadAverage : widgetMetadata.showLoadAverage @@ -28,6 +29,7 @@ ColumnLayout { property bool valueShowSwapUsage: widgetData.showSwapUsage !== undefined ? widgetData.showSwapUsage : widgetMetadata.showSwapUsage property bool valueShowNetworkStats: widgetData.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage + property bool valueShowDiskAsFree: widgetData.showDiskAsFree !== undefined ? widgetData.showDiskAsFree : widgetMetadata.showDiskAsFree property string valueDiskPath: widgetData.diskPath !== undefined ? widgetData.diskPath : widgetMetadata.diskPath function saveSettings() { @@ -36,6 +38,7 @@ ColumnLayout { settings.usePrimaryColor = valueUsePrimaryColor; settings.useMonospaceFont = valueUseMonospaceFont; settings.showCpuUsage = valueShowCpuUsage; + settings.showCpuFreq = valueShowCpuFreq; settings.showCpuTemp = valueShowCpuTemp; settings.showGpuTemp = valueShowGpuTemp; settings.showLoadAverage = valueShowLoadAverage; @@ -44,6 +47,7 @@ ColumnLayout { settings.showSwapUsage = valueShowSwapUsage; settings.showNetworkStats = valueShowNetworkStats; settings.showDiskUsage = valueShowDiskUsage; + settings.showDiskAsFree = valueShowDiskAsFree; settings.diskPath = valueDiskPath; return settings; @@ -96,6 +100,18 @@ ColumnLayout { } } + NToggle { + id: showCpuFreq + Layout.fillWidth: true + label: "Show CPU Frequency" // TODO: use I18n.tr + description: "Display the current CPU clock speed in GHz" // TODO: use I18n.tr + checked: valueShowCpuFreq + onToggled: checked => { + valueShowCpuFreq = checked; + settingsChanged(saveSettings()); + } + } + NToggle { id: showCpuTemp Layout.fillWidth: true @@ -194,6 +210,19 @@ ColumnLayout { } } + NToggle { + id: showDiskAsFree + Layout.fillWidth: true + label: "Show Free Space" // TODO: use I18n.tr + description: "Display available space (GB) instead of percentage" // TODO: use I18n.tr + checked: valueShowDiskAsFree + onToggled: checked => { + valueShowDiskAsFree = checked; + settingsChanged(saveSettings()); + } + visible: valueShowDiskUsage + } + NComboBox { id: diskPathComboBox Layout.fillWidth: true diff --git a/Services/System/SystemStatService.qml b/Services/System/SystemStatService.qml index b9435441b..62f34c9ef 100644 --- a/Services/System/SystemStatService.qml +++ b/Services/System/SystemStatService.qml @@ -20,6 +20,9 @@ Singleton { // Public values property real cpuUsage: 0 property real cpuTemp: 0 + property string cpuFreq: "0.0GHz" + property real cpuFreqRatio: 0 + property real cpuGlobalMaxFreq: 3.5 property real gpuTemp: 0 property bool gpuAvailable: false property string gpuType: "" // "amd", "intel", "nvidia" @@ -31,6 +34,7 @@ Singleton { property var diskPercents: ({}) property var diskUsedGb: ({}) // Used space in GB per mount point property var diskSizeGb: ({}) // Total size in GB per mount point + property var diskAvailGb: ({}) property real rxSpeed: 0 property real txSpeed: 0 property real zfsArcSizeKb: 0 // ZFS ARC cache size in KB @@ -243,7 +247,10 @@ Singleton { restart(); } } - onTriggered: cpuStatFile.reload() + onTriggered: { + cpuStatFile.reload(); + cpuFreqProcess.running = true; + } } // Timer for load average @@ -379,13 +386,13 @@ Singleton { } // -------------------------------------------- - // Process to fetch disk usage (percent, used, size) + // Process to fetch disk usage (percent, used, size, avail) // Uses 'df' aka 'disk free' // "-x efivarfs' skips efivarfs mountpoints, for which the `statfs` syscall may cause system-wide stuttering // --block-size=1 gives us bytes for precise GB calculation Process { id: dfProcess - command: ["df", "--output=target,pcent,used,size", "--block-size=1", "-x", "efivarfs"] + command: ["df", "--output=target,pcent,used,size,avail", "--block-size=1", "-x", "efivarfs"] running: false stdout: StdioCollector { onStreamFinished: { @@ -393,23 +400,27 @@ Singleton { const newPercents = {}; const newUsedGb = {}; const newSizeGb = {}; + const newAvailGb = {}; const bytesPerGb = 1024 * 1024 * 1024; // Start from line 1 (skip header) for (var i = 1; i < lines.length; i++) { const parts = lines[i].trim().split(/\s+/); - if (parts.length >= 4) { + if (parts.length >= 5) { const target = parts[0]; const percent = parseInt(parts[1].replace(/[^0-9]/g, '')) || 0; const usedBytes = parseFloat(parts[2]) || 0; const sizeBytes = parseFloat(parts[3]) || 0; + const availBytes = parseFloat(parts[4]) || 0; newPercents[target] = percent; newUsedGb[target] = usedBytes / bytesPerGb; newSizeGb[target] = sizeBytes / bytesPerGb; + newAvailGb[target] = availBytes / bytesPerGb; } } root.diskPercents = newPercents; root.diskUsedGb = newUsedGb; root.diskSizeGb = newSizeGb; + root.diskAvailGb = newAvailGb; } } } @@ -426,6 +437,55 @@ Singleton { } } + // Process to get avg cpu frquency + Process { + id: cpuFreqProcess + command: ["cat", "/proc/cpuinfo"] + running: false + stdout: StdioCollector { + onStreamFinished: { + let txt = text; + let matches = txt.match(/cpu MHz\s+:\s+([0-9.]+)/g); + if (matches && matches.length > 0) { + let totalFreq = 0.0; + for (let i = 0; i < matches.length; i++) { + totalFreq += parseFloat(matches[i].split(":")[1]); + } + let avgFreq = (totalFreq / matches.length) / 1000.0; + root.cpuFreq = avgFreq.toFixed(1) + " GHz"; + cpuMaxFreqProcess.running = true; + if (avgFreq > root.cpuGlobalMaxFreq) + root.cpuGlobalMaxFreq = avgFreq; + if (root.cpuGlobalMaxFreq > 0) { + root.cpuFreqRatio = Math.min(1.0, avgFreq / root.cpuGlobalMaxFreq); + } + } + } + } + } + + // Process to get maximum CPU frequency limit + // Uses sysfs 'scaling_max_freq' to respect power profiles (e.g. power-profiles-daemon) + // 'sort -nr | head -n1' ensures we get the highest limit across all cores + // '2>/dev/null' ignores errors if cpufreq driver is missing or cores are offline + Process { + id: cpuMaxFreqProcess + command: ["sh", "-c", "cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq 2>/dev/null | sort -nr | head -n1"] + running: false + stdout: StdioCollector { + onStreamFinished: { + let maxKHz = parseInt(text.trim()); + if (!isNaN(maxKHz) && maxKHz > 0) { + let newMaxFreq = maxKHz / 1000000.0; + if (Math.abs(root.cpuGlobalMaxFreq - newMaxFreq) > 0.01) { + Logger.i("SystemStat", `CPU Max Freq changed: ${root.cpuGlobalMaxFreq} -> ${newMaxFreq} GHz`); + root.cpuGlobalMaxFreq = newMaxFreq; + } + } + } + } + } + // -------------------------------------------- // -------------------------------------------- // CPU Temperature diff --git a/Services/UI/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml index af4ab3fc7..84fef7fc9 100644 --- a/Services/UI/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -199,6 +199,7 @@ Singleton { "usePrimaryColor": false, "useMonospaceFont": true, "showCpuUsage": true, + "showCpuFreq": false, "showCpuTemp": true, "showGpuTemp": false, "showLoadAverage": false, @@ -207,6 +208,7 @@ Singleton { "showSwapUsage": false, "showNetworkStats": false, "showDiskUsage": false, + "showDiskAsFree": false, "diskPath": "/" }, "Taskbar": {