From 8bbe211dd48d611fa786a9991d19157e9ad43a61 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 11:22:09 +0200 Subject: [PATCH 1/2] fix onClosed recursion stack overflow --- modules/NotifItem.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/NotifItem.qml b/modules/NotifItem.qml index d3e96b9..fc5d552 100644 --- a/modules/NotifItem.qml +++ b/modules/NotifItem.qml @@ -46,7 +46,8 @@ QtObject { readonly property Connections _notifConn: Connections { target: root.notification function onClosed() { - M.NotifService.dismiss(root.id); + if (root.state !== "dismissed") + M.NotifService.dismiss(root.id); } } From 88a0886681321a22dc6e4c568a219c6d5e7b3242 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 11:24:28 +0200 Subject: [PATCH 2/2] temperature: per-device breakdown in panel, device filter config option --- README.md | 1 + modules/Modules.qml | 3 +- modules/SystemStats.qml | 5 ++- modules/Temperature.qml | 63 ++++++++++++++++++++++++++++++- nix/hm-module.nix | 8 ++++ stats-daemon/src/temp.rs | 80 +++++++++++++++++++++++++++++++++------- 6 files changed, 144 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index cab99da..e938289 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ programs.nova-shell.modules = { weather.interval = 3600000; # refresh interval in ms (default 1h) temperature.warm = 55; # °C threshold for warm color temperature.hot = 75; # °C threshold for hot color + temperature.device = "x86_pkg_temp"; # pin to a specific thermal zone (default: system max) battery.warning = 30; # % for warning notification battery.critical = 10; # % for critical blink + notification cpu.interval = 2000; # polling interval in ms diff --git a/modules/Modules.qml b/modules/Modules.qml index 4a0053e..c7a4b6d 100644 --- a/modules/Modules.qml +++ b/modules/Modules.qml @@ -56,7 +56,8 @@ QtObject { property var temperature: ({ enable: true, warm: 80, - hot: 90 + hot: 90, + device: "" }) property var gpu: ({ enable: true diff --git a/modules/SystemStats.qml b/modules/SystemStats.qml index a668be4..7345727 100644 --- a/modules/SystemStats.qml +++ b/modules/SystemStats.qml @@ -32,7 +32,8 @@ QtObject { // ── Temperature ────────────────────────────────────────────────────── property int tempCelsius: 0 - property var tempHistory: [] // 150 samples @ 4s each ≈ 10 min + property var tempHistory: [] // 150 samples @ 4s each ≈ 10 min + property var tempDevices: [] // [{name, celsius}] sorted hottest-first // ── GPU ────────────────────────────────────────────────────────────── property bool gpuAvailable: false @@ -95,6 +96,8 @@ QtObject { root.tempCelsius = ev.celsius; const th = root.tempHistory.concat([ev.celsius]); root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th; + if (ev.devices) + root.tempDevices = ev.devices; } else if (ev.type === "gpu") { root.gpuAvailable = true; root.gpuVendor = ev.vendor; diff --git a/modules/Temperature.qml b/modules/Temperature.qml index 0106632..d7aa47c 100644 --- a/modules/Temperature.qml +++ b/modules/Temperature.qml @@ -9,7 +9,17 @@ M.BarSection { readonly property int _warm: M.Modules.temperature.warm || 80 readonly property int _hot: M.Modules.temperature.hot || 90 - readonly property int _temp: M.SystemStats.tempCelsius + readonly property string _deviceFilter: M.Modules.temperature.device || "" + + // If a device filter is set, use that device's temp; otherwise fall back to system max + readonly property int _temp: { + if (_deviceFilter !== "") { + const dev = M.SystemStats.tempDevices.find(d => d.name === _deviceFilter); + if (dev) + return dev.celsius; + } + return M.SystemStats.tempCelsius; + } property color _stateColor: _temp > _hot ? M.Theme.base08 : _temp > _warm ? M.Theme.base0A : root.accentColor Behavior on _stateColor { @@ -257,6 +267,57 @@ M.BarSection { } } + // Per-device breakdown + Rectangle { + width: parent.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + visible: M.SystemStats.tempDevices.length > 0 + } + + Repeater { + model: M.SystemStats.tempDevices + delegate: Item { + required property var modelData + width: hoverPanel.contentWidth + height: 22 + + readonly property bool _isActive: root._deviceFilter === modelData.name + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + color: _isActive ? Qt.rgba(root.accentColor.r, root.accentColor.g, root.accentColor.b, 0.12) : "transparent" + radius: 3 + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + color: _isActive ? root.accentColor : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - 80 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.celsius + "\u00B0C" + color: root._tempColor(modelData.celsius) + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + font.bold: _isActive + } + } + } + Item { width: 1 height: 4 diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 7911c9b..440b902 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -141,6 +141,14 @@ in default = 90; description = "Temperature threshold for hot state (°C)."; }; + device = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Thermal zone device name to use for the primary temperature reading + (e.g. "x86_pkg_temp", "acpitz"). Leave empty to use the system maximum. + ''; + }; }; battery = moduleOpt "battery" { warning = lib.mkOption { diff --git a/stats-daemon/src/temp.rs b/stats-daemon/src/temp.rs index 54beb3c..2c93ef9 100644 --- a/stats-daemon/src/temp.rs +++ b/stats-daemon/src/temp.rs @@ -1,25 +1,79 @@ +use std::collections::HashMap; use std::fs; use std::io::Write; -pub fn read_temp_celsius() -> Option { - let mut max: Option = None; +#[derive(Debug)] +pub struct ThermalDevice { + pub name: String, + pub celsius: i32, +} + +pub fn read_thermal_devices() -> Vec { + let mut by_name: HashMap = HashMap::new(); + for i in 0.. { - let path = format!("/sys/class/thermal/thermal_zone{i}/temp"); - match fs::read_to_string(&path) { - Ok(s) => { - if let Ok(millic) = s.trim().parse::() { - let c = millic / 1000; - max = Some(max.map_or(c, |m: i32| m.max(c))); - } - } + let temp_path = format!("/sys/class/thermal/thermal_zone{i}/temp"); + let type_path = format!("/sys/class/thermal/thermal_zone{i}/type"); + + let temp_str = match fs::read_to_string(&temp_path) { + Ok(s) => s, Err(_) => break, + }; + + let millic: i32 = match temp_str.trim().parse() { + Ok(v) => v, + Err(_) => continue, + }; + let celsius = millic / 1000; + + let name = fs::read_to_string(&type_path) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|_| format!("zone{i}")); + + // Keep the highest temp seen for each device type + let entry = by_name.entry(name).or_insert(celsius); + if celsius > *entry { + *entry = celsius; } } - max + + let mut devices: Vec = by_name + .into_iter() + .map(|(name, celsius)| ThermalDevice { name, celsius }) + .collect(); + + // Sort descending by temp so the hottest shows first + devices.sort_by(|a, b| b.celsius.cmp(&a.celsius)); + devices } pub fn emit_temp(out: &mut impl Write) { - if let Some(c) = read_temp_celsius() { - let _ = writeln!(out, "{{\"type\":\"temp\",\"celsius\":{c}}}"); + let devices = read_thermal_devices(); + if devices.is_empty() { + return; } + + let max = devices.iter().map(|d| d.celsius).max().unwrap_or(0); + + let devices_json: Vec = devices + .iter() + .map(|d| { + format!( + "{{\"name\":{},\"celsius\":{}}}", + json_str(&d.name), + d.celsius + ) + }) + .collect(); + + let _ = writeln!( + out, + "{{\"type\":\"temp\",\"celsius\":{max},\"devices\":[{}]}}", + devices_json.join(",") + ); +} + +fn json_str(s: &str) -> String { + let escaped = s.replace('\\', "\\\\").replace('"', "\\\""); + format!("\"{escaped}\"") }