From d009b8d5c8abd1da0b0470012eee3c79fe0fd78e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 15:07:32 -0400 Subject: [PATCH] Using a bash script for SystemStats instead of ZigStat --- Bin/sysmon.sh | 49 ----- Bin/system-stats.sh | 192 ++++++++++++++++++ ...notifications.sh => test-notifications.sh} | 0 Modules/SidePanel/Cards/SystemMonitorCard.qml | 13 +- Services/SysInfo.qml | 47 ----- Services/SystemStats.qml | 37 ++++ Widgets/NCircleStat.qml | 2 +- Widgets/NSystemMonitor.qml | 54 ----- 8 files changed, 234 insertions(+), 160 deletions(-) delete mode 100755 Bin/sysmon.sh create mode 100755 Bin/system-stats.sh rename Bin/{test_notifications.sh => test-notifications.sh} (100%) delete mode 100644 Services/SysInfo.qml create mode 100644 Services/SystemStats.qml delete mode 100644 Widgets/NSystemMonitor.qml diff --git a/Bin/sysmon.sh b/Bin/sysmon.sh deleted file mode 100755 index 0d69904..0000000 --- a/Bin/sysmon.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# A script to display CPU temperature, CPU usage, memory usage, and disk usage without the 'sensors' package. - -echo "--- System Metrics ---" - -# Get CPU Temperature in Celsius from a kernel file. -# This method is more common on modern systems but may vary. -# It reads the temperature from the first available core. -# Function to get CPU temperature - - # Check for the common thermal zone path - if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then - temp_file="/sys/class/thermal/thermal_zone0/temp" - # Check for a different thermal zone path (e.g., some older systems) - elif [ -f "/sys/class/hwmon/hwmon0/temp1_input" ]; then - temp_file="/sys/class/hwmon/hwmon0/temp1_input" - else - echo "Error: Could not find a CPU temperature file." - exit 1 - fi - - # Read the raw temperature value - raw_temp=$(cat "$temp_file") - - # The value is usually in millidegrees Celsius, so we divide by 1000. - temp_celsius=$((raw_temp / 1000)) - - echo "CPU Temperature: ${temp_celsius}°C" - - -# Get CPU Usage -# 'top' is a standard utility for this. It gives a real-time view of system processes. -cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') -echo "CPU Usage: ${cpu_usage}%" - -# Get Memory Usage -# 'free' provides information about memory usage. -mem_total=$(free | grep Mem | awk '{print $2}') -mem_used=$(free | grep Mem | awk '{print $3}') -mem_usage=$((100 * mem_used / mem_total)) -echo "Memory Usage: ${mem_usage}%" - -# Get Disk Usage -# 'df' reports file system disk space usage. We check the root directory. -disk_usage=$(df -h / | grep / | awk '{print $5}' | sed 's/%//g') -echo "Disk Usage: ${disk_usage}%" - -echo "----------------------" \ No newline at end of file diff --git a/Bin/system-stats.sh b/Bin/system-stats.sh new file mode 100755 index 0000000..7bd1170 --- /dev/null +++ b/Bin/system-stats.sh @@ -0,0 +1,192 @@ +#!/bin/bash + +# A Bash script to monitor system stats and output them in JSON format. +# This script is a conversion of ZigStat + +# --- Configuration --- +# Default sleep duration in seconds. Can be overridden by the first argument. +SLEEP_DURATION=3 + +# --- Argument Parsing --- +# Check if a command-line argument is provided for the sleep duration. +if [[ -n "$1" ]]; then + # Basic validation to ensure the argument is a number (integer or float). + if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + SLEEP_DURATION=$1 + else + # Output to stderr if the format is invalid. + echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2 + fi +fi + +# --- Global Cache Variables --- +# These variables will store the discovered CPU temperature sensor path and type +# to avoid searching for it on every loop iteration. +TEMP_SENSOR_PATH="" +TEMP_SENSOR_TYPE="" + +# --- Data Collection Functions --- + +# +# Gets memory usage in GB and as a percentage. +# +get_memory_info() { + awk ' + /MemTotal/ {total=$2} + /MemAvailable/ {available=$2} + END { + if (total > 0) { + usage_kb = total - available + usage_gb = usage_kb / 1000000 + usage_percent = (usage_kb / total) * 100 + # MODIFIED: Round the memory percentage to the nearest integer. + printf "%.1f %.0f\n", usage_gb, usage_percent + } else { + # Fallback if /proc/meminfo is unreadable or empty. + print "0.0 0.0" + } + } + ' /proc/meminfo +} + +# +# Gets the usage percentage of the root filesystem ("/"). +# +get_disk_usage() { + # df gets disk usage. --output=pcent shows only the percentage for the root path. + # tail -1 gets the data line, and tr removes the '%' sign and whitespace. + df --output=pcent / | tail -1 | tr -d ' %' +} + +# +# Calculates current CPU usage over a short interval. +# +get_cpu_usage() { + # Read all 10 CPU time fields to prevent errors on newer kernels. + read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat + + # Calculate previous total and idle times. + local prev_total_idle=$((prev_idle + prev_iowait)) + local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice)) + + # Wait for a short period. + sleep 0.05 + + # Read all 10 CPU time fields again for the second measurement. + read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat + + # Calculate new total and idle times. + local total_idle=$((idle + iowait)) + local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice)) + + # Add a check to prevent division by zero if total hasn't changed. + if (( total <= prev_total )); then + echo "0.0" + return + fi + + # Calculate the difference over the interval. + local diff_total=$((total - prev_total)) + local diff_idle=$((total_idle - prev_total_idle)) + + # Use awk for floating-point calculation and print the percentage. + awk -v total="$diff_total" -v idle="$diff_idle" ' + BEGIN { + if (total > 0) { + # Formula: 100 * (Total - Idle) / Total + usage = 100 * (total - idle) / total + # MODIFIED: Changed format from "%.2f" back to "%.1f" for one decimal place. + printf "%.1f\n", usage + } else { + # MODIFIED: Changed output back to "0.0" to match the precision. + print "0.0" + } + }' +} + +# +# Finds and returns the CPU temperature in degrees Celsius. +# Caches the sensor path for efficiency. +# +get_cpu_temp() { + # If the sensor path hasn't been found yet, search for it. + if [[ -z "$TEMP_SENSOR_PATH" ]]; then + for dir in /sys/class/hwmon/hwmon*; do + # Check if the 'name' file exists and read it. + if [[ -f "$dir/name" ]]; then + local name + name=$(<"$dir/name") + # Check for supported sensor types (matches Zig code). + if [[ "$name" == "coretemp" || "$name" == "k10temp" ]]; then + TEMP_SENSOR_PATH=$dir + TEMP_SENSOR_TYPE=$name + break # Found it, no need to keep searching. + fi + fi + done + fi + + # If after searching no sensor was found, return 0. + if [[ -z "$TEMP_SENSOR_PATH" ]]; then + echo 0 + return + fi + + # --- Get temp based on sensor type --- + if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then + # For Intel 'coretemp', average all core temperatures. + # find gets all temp inputs, cat reads them, and awk calculates the average. + # The value is in millidegrees Celsius, so we divide by 1000. + find "$TEMP_SENSOR_PATH" -type f -name 'temp*_input' -print0 | xargs -0 cat | awk ' + { total += $1; count++ } + END { + if (count > 0) print int(total / count / 1000); + else print 0; + }' + + elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then + # For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature. + local tctl_input="" + for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do + if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then + # The input file has the same name but with '_input' instead of '_label'. + tctl_input="${label_file%_label}_input" + break + fi + done + + if [[ -f "$tctl_input" ]]; then + # Read the temperature and convert from millidegrees to degrees. + echo "$(( $(<"$tctl_input") / 1000 ))" + else + echo 0 # Fallback + fi + else + echo 0 # Should not happen if cache logic is correct. + fi +} + + +# --- Main Loop --- +# This loop runs indefinitely, gathering and printing stats. +while true; do + # Call the functions to gather all the data. + # 'read' is used to capture the two output values from get_memory_info. + read -r mem_gb mem_per <<< "$(get_memory_info)" + + # Command substitution captures the single output from the other functions. + disk_per=$(get_disk_usage) + cpu_usage=$(get_cpu_usage) + cpu_temp=$(get_cpu_temp) + + # Use printf to format the final JSON output string, matching the Zig program. + printf '{"mem":"%s", "cpu": "%s", "cputemp": "%s", "memper": "%s", "diskper": "%s"}\n' \ + "$mem_gb" \ + "$cpu_usage" \ + "$cpu_temp" \ + "$mem_per" \ + "$disk_per" + + # Wait for the specified duration before the next update. + sleep "$SLEEP_DURATION" +done \ No newline at end of file diff --git a/Bin/test_notifications.sh b/Bin/test-notifications.sh similarity index 100% rename from Bin/test_notifications.sh rename to Bin/test-notifications.sh diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index f66c405..221a2ca 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -28,13 +28,8 @@ NBox { height: Style.marginTiny * scaling } - NSystemMonitor { - id: sysMon - intervalSeconds: 1 - } - NCircleStat { - value: sysMon.cpuUsage || SysInfo.cpuUsage + value: SystemStats.cpuUsage icon: "speed" flat: true contentScale: 0.8 @@ -42,7 +37,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: sysMon.cpuTemp || SysInfo.cpuTemp + value: SystemStats.cpuTemp suffix: "°C" icon: "device_thermostat" flat: true @@ -51,7 +46,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: sysMon.memoryUsagePer || SysInfo.memoryUsagePer + value: SystemStats.memoryUsagePer icon: "memory" flat: true contentScale: 0.8 @@ -59,7 +54,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: sysMon.diskUsage || SysInfo.diskUsage + value: SystemStats.diskUsage icon: "data_usage" flat: true contentScale: 0.8 diff --git a/Services/SysInfo.qml b/Services/SysInfo.qml deleted file mode 100644 index 39bef22..0000000 --- a/Services/SysInfo.qml +++ /dev/null @@ -1,47 +0,0 @@ -pragma Singleton - -import QtQuick -import Qt.labs.folderlistmodel -import Quickshell -import Quickshell.Io - -Singleton { - id: manager //TBC - - property string updateInterval: "2s" - property string cpuUsageStr: "" - property string cpuTempStr: "" - property string memoryUsageStr: "" - property string memoryUsagePerStr: "" - property real cpuUsage: 0 - property real memoryUsage: 0 - property real cpuTemp: 0 - property real diskUsage: 0 - property real memoryUsagePer: 0 - property string diskUsageStr: "" - - Process { - id: zigstatProcess - running: true - command: [Quickshell.shellDir + "/Programs/zigstat", updateInterval] - stdout: SplitParser { - onRead: function (line) { - try { - const data = JSON.parse(line) - cpuUsage = +data.cpu - cpuTemp = +data.cputemp - memoryUsage = +data.mem - memoryUsagePer = +data.memper - diskUsage = +data.diskper - cpuUsageStr = data.cpu + "%" - cpuTempStr = data.cputemp + "°C" - memoryUsageStr = data.mem + "G" - memoryUsagePerStr = data.memper + "%" - diskUsageStr = data.diskper + "%" - } catch (e) { - console.error("Failed to parse zigstat output:", e) - } - } - } - } -} diff --git a/Services/SystemStats.qml b/Services/SystemStats.qml new file mode 100644 index 0000000..add1e38 --- /dev/null +++ b/Services/SystemStats.qml @@ -0,0 +1,37 @@ +pragma Singleton + +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + // Public values + property real cpuUsage: 0 + property real cpuTemp: 0 + property real memoryUsagePer: 0 + property real diskUsage: 0 + + // Background process emitting one JSON line per sample + Process { + id: reader + running: true + command: ["sh", "-c", Quickshell.shellDir + "/Bin/system-stats.sh"] + stdout: SplitParser { + onRead: function (line) { + try { + const data = JSON.parse(line) + root.cpuUsage = data.cpu + root.cpuTemp = data.cputemp + root.memoryUsagePer = data.memper + root.diskUsage = data.diskper + } catch (e) { + + // ignore malformed lines + } + } + } + } +} diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index a0a740f..15995cf 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -76,7 +76,7 @@ Rectangle { Text { id: valueLabel anchors.centerIn: parent - text: `${Math.round(root.value)}${root.suffix}` + text: `${root.value}${root.suffix}` font.pointSize: Style.fontSizeMedium * scaling * contentScale color: Colors.textPrimary horizontalAlignment: Text.AlignHCenter diff --git a/Widgets/NSystemMonitor.qml b/Widgets/NSystemMonitor.qml deleted file mode 100644 index bb28716..0000000 --- a/Widgets/NSystemMonitor.qml +++ /dev/null @@ -1,54 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Io - -// Lightweight system monitor using standard Linux interfaces. -// Provides cpu usage %, cpu temperature (°C), and memory usage %. -// No external helpers; uses /proc and /sys via a shell loop. -Item { - id: root - - // Public values - property real cpuUsage: 0 - property real cpuTemp: 0 - property real memoryUsagePer: 0 - property real diskUsage: 0 - - // Interval in seconds between updates - property int intervalSeconds: 1 - - // Background process emitting one JSON line per sample - Process { - id: reader - running: true - command: ["sh", "-c", // Outputs: {"cpu":,"memper":,"cputemp":} - "interval=" + intervalSeconds + "; " + "while true; do " + // First /proc/stat snapshot - "read _ u1 n1 s1 id1 iw1 ir1 si1 st1 gs1 < /proc/stat; " - + "t1=$((u1+n1+s1+id1+iw1+ir1+si1+st1)); i1=$((id1+iw1)); " + "sleep $interval; " + // Second /proc/stat snapshot - "read _ u2 n2 s2 id2 iw2 ir2 si2 st2 gs2 < /proc/stat; " + "t2=$((u2+n2+s2+id2+iw2+ir2+si2+st2)); i2=$((id2+iw2)); " - + "dt=$((t2 - t1)); di=$((i2 - i1)); " + "cpu=$(( (100*(dt - di)) / (dt>0?dt:1) )); " + // Memory percent via /proc/meminfo (kB) - "mt=$(awk '/MemTotal/ {print $2}' /proc/meminfo); " + "ma=$(awk '/MemAvailable/ {print $2}' /proc/meminfo); " - + "mm=$((mt - ma)); mp=$(( (100*mm) / (mt>0?mt:1) )); " + // Temperature: scan hwmon and thermal zones, choose max; convert m°C → °C - "ct=0; " + "for f in /sys/class/hwmon/hwmon*/temp*_input /sys/class/thermal/thermal_zone*/temp; do " - + "[ -r \"$f\" ] || continue; v=$(cat \"$f\" 2>/dev/null); " + "[ -z \"$v\" ] && continue; " - + "if [ \"$v\" -gt 1000 ] 2>/dev/null; then v=$((v/1000)); fi; " + "[ \"$v\" -gt \"$ct\" ] 2>/dev/null && ct=$v; " - + "done; " + // Disk usage percent for root filesystem - "dp=$(df -P / 2>/dev/null | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'); " + "[ -z \"$dp\" ] && dp=0; " + // Emit JSON line - "echo \"{\\\"cpu\\\":$cpu,\\\"memper\\\":$mp,\\\"cputemp\\\":$ct,\\\"diskper\\\":$dp}\"; " + "done"] - - stdout: SplitParser { - onRead: function (line) { - try { - const data = JSON.parse(line) - root.cpuUsage = +data.cpu - root.cpuTemp = +data.cputemp - root.memoryUsagePer = +data.memper - root.diskUsage = +data.diskper - } catch (e) { - - // ignore malformed lines - } - } - } - } -}