From d4a60ce6ec17b25751e9ec6979021c2033c42ea6 Mon Sep 17 00:00:00 2001 From: Pila Date: Tue, 13 Jan 2026 22:05:19 +0100 Subject: [PATCH 01/16] Sensors pages, first draft --- usr/lib/linuxmint/mintreport/app.py | 13 +- usr/lib/linuxmint/mintreport/sensors.py | 176 +++++++++++++++++++ usr/share/linuxmint/mintreport/mintreport.ui | 34 +++- 3 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 usr/lib/linuxmint/mintreport/sensors.py diff --git a/usr/lib/linuxmint/mintreport/app.py b/usr/lib/linuxmint/mintreport/app.py index c2c8838..c6cce36 100755 --- a/usr/lib/linuxmint/mintreport/app.py +++ b/usr/lib/linuxmint/mintreport/app.py @@ -25,6 +25,8 @@ from pci import PCIListWidget from usb import USBListWidget from gpu import GPUListWidget +from sensors import SensorsListWidget + setproctitle.setproctitle("mintreport") _ = xapp.util.l10n("mintreport") @@ -68,6 +70,7 @@ def on_command_line(self, app, command_line): group.add_argument("--bios", action="store_const", dest="page", const="bios") group.add_argument("--pci", action="store_const", dest="page", const="pci") group.add_argument("--gpu", action="store_const", dest="page", const="gpu") + group.add_argument("--sensors", action="store_const", dest="page", const="sensors") argv = command_line.get_arguments()[1:] args, _ = parser.parse_known_args(argv) if args.page is None: @@ -405,7 +408,6 @@ def sort_by_date(model, a, b, *args): self.builder.get_object("button_sysinfo_copy").connect("clicked", self.copy_inxi_info) self.builder.get_object("button_sysinfo_upload").connect("clicked", self.upload_inxi_info) - # USB page self.usb_widget = USBListWidget() self.builder.get_object("box_usb_widget").pack_start(self.usb_widget, True, True, 0) @@ -414,6 +416,10 @@ def sort_by_date(model, a, b, *args): self.pci_widget = PCIListWidget() self.builder.get_object("box_pci_widget").pack_start(self.pci_widget, True, True, 0) + # Sensors page + self.sensors_widget = SensorsListWidget() + self.builder.get_object("box_sensors_widget").pack_start(self.sensors_widget, True, True, 0) + # BIOS page self.bios_widget = BIOSListWidget() self.builder.get_object("box_bios_widget").add(self.bios_widget) @@ -427,6 +433,7 @@ def sort_by_date(model, a, b, *args): self.load_pci() self.load_bios() self.load_gpu() + self.load_sensors() def show_page(self, page_name): page_name = f"page_{page_name}" @@ -585,6 +592,10 @@ def load_usb(self): def load_pci(self): self.pci_widget.load() + @xt.run_async + def load_sensors(self): + self.sensors_widget.load() + @xt.run_async def load_bios(self): self.bios_widget.load() diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py new file mode 100644 index 0000000..468f6cf --- /dev/null +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -0,0 +1,176 @@ +import os +import gi +import xapp.util + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib, Pango + +_ = xapp.util.l10n("mintreport") + +SYS_HWMON = "/sys/class/hwmon" + +( + COL_NAME, + COL_VALUE, + COL_UNIT, + COL_SENSITIVE, + COL_ICON, +) = range(5) + + +class SensorsListWidget(Gtk.ScrolledWindow): + ICONS = { + "root": "xsi-cpu-symbolic", + "temp": "xsi-temperature-symbolic", + "fan": "xsi-cpu-symbolic", + "in": "xsi-cpu-symbolic", + "power": "xsi-cpu-symbolic", + "other": "xsi-cpu-symbolic", + } + + def __init__(self): + super().__init__() + + # TreeStore with icon, name, value, unit + self.treestore = Gtk.TreeStore(str, str, str, bool, str) + self.treeview = Gtk.TreeView(model=self.treestore) + self.treeview.set_enable_tree_lines(True) + self.treeview.set_headers_clickable(True) + + # First column: tree + icon + name + renderer = Gtk.CellRendererPixbuf() + column = Gtk.TreeViewColumn("", renderer, icon_name=COL_ICON) + column.set_expand(False) + self.treeview.append_column(column) + + renderer = Gtk.CellRendererText() + renderer.set_property("ellipsize", Pango.EllipsizeMode.END) + column = Gtk.TreeViewColumn(_("Sensor"), renderer, text=COL_NAME) + column.set_expand(True) + self.treeview.append_column(column) + + # Value column + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(_("Value"), renderer, text=COL_VALUE) + column.set_expand(False) + self.treeview.append_column(column) + + # Unit column + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(_("Unit"), renderer, text=COL_UNIT) + column.set_expand(False) + self.treeview.append_column(column) + + self.add(self.treeview) + self.set_shadow_type(Gtk.ShadowType.IN) + + self.hwmon_parents = {} + self.sensor_rows = {} + + # ------------------------------------------------------------------ + def load(self): + self.build_tree() + self.refresh_values() + GLib.timeout_add_seconds(1, self.refresh_values) + + # ------------------------------------------------------------------ + def build_tree(self): + self.treestore.clear() + self.hwmon_parents.clear() + self.sensor_rows.clear() + + if not os.path.isdir(SYS_HWMON): + return + + for hwmon in sorted(os.listdir(SYS_HWMON)): + hwmon_path = os.path.join(SYS_HWMON, hwmon) + device_path = os.path.join(hwmon_path, "device") + + # Determine base path for sensors + base_path = None + if os.path.isdir(device_path): + # Use device/ only if it contains *_input files + inputs = [f for f in os.listdir(device_path) if f.endswith("_input")] + if inputs: + base_path = device_path + if base_path is None: + base_path = hwmon_path + + # Root name + name_file = os.path.join(base_path, "name") + name = self._read_file(name_file) + name = name.strip() if name else hwmon + + parent = self.treestore.append( + None, [name, "", "", False, self.ICONS["root"]] + ) + self.hwmon_parents[hwmon_path] = parent + + # Process all *_input files in base_path + for fname in sorted(os.listdir(base_path)): + if not fname.endswith("_input"): + continue + + fpath = os.path.join(base_path, fname) + raw = self._read_file(fpath) + if raw is None: + continue + + # Label + label_file = fpath.replace("_input", "_label") + label = self._read_file(label_file) + label = label.strip() if label else fname.replace("_input", "") + + unit, display, stype = self.format_sensor(fname, raw) + + itr = self.treestore.append( + parent, + [label, display, unit, True, self.ICONS.get(stype, self.ICONS["other"])], + ) + self.sensor_rows[fpath] = itr + + self.treeview.expand_all() + + # ------------------------------------------------------------------ + def refresh_values(self): + for fpath, itr in self.sensor_rows.items(): + raw = self._read_file(fpath) + if raw is None: + continue + + fname = os.path.basename(fpath) + _, display, _ = self.format_sensor(fname, raw) + self.treestore.set_value(itr, COL_VALUE, display) + + return True + + # ------------------------------------------------------------------ + def format_sensor(self, filename, raw): + raw = raw.strip() + stype = "other" + + if filename.startswith("temp"): + stype = "temp" + return "°C", f"{int(raw)/1000:.1f}", stype + + if filename.startswith("fan"): + stype = "fan" + return "RPM", raw, stype + + if filename.startswith("in"): + stype = "in" + return "V", f"{int(raw)/1000:.3f}", stype + + if filename.startswith("power"): + stype = "power" + return "W", f"{int(raw)/1_000_000:.2f}", stype + + return "", raw, stype + + # ------------------------------------------------------------------ + def _read_file(self, path): + try: + with open(path, "r") as f: + return f.read() + except Exception: + return None diff --git a/usr/share/linuxmint/mintreport/mintreport.ui b/usr/share/linuxmint/mintreport/mintreport.ui index cfa03e3..0f55f92 100644 --- a/usr/share/linuxmint/mintreport/mintreport.ui +++ b/usr/share/linuxmint/mintreport/mintreport.ui @@ -306,6 +306,34 @@ 3 + + + True + False + vertical + + + True + False + vertical + + + + + + True + True + 0 + + + + + pense_sensors + Sensors + xsi-temperature-symbolic + 4 + + True @@ -357,7 +385,7 @@ page_bios BIOS xsi-cpu-symbolic - 4 + 5 @@ -590,7 +618,7 @@ page_reports System Reports mintreport-symbolic - 5 + 6 @@ -869,7 +897,7 @@ page_crashes Crash Reports xsi-computer-fail-symbolic - 6 + 7 From d116f6d452479ea2eab1e55d799ad14781be8a20 Mon Sep 17 00:00:00 2001 From: Pila Date: Wed, 14 Jan 2026 22:12:24 +0100 Subject: [PATCH 02/16] Code cleanup, add other sensor types --- usr/lib/linuxmint/mintreport/sensors.py | 118 ++++++++++++------------ 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 468f6cf..00824ae 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -9,45 +9,64 @@ SYS_HWMON = "/sys/class/hwmon" -( - COL_NAME, - COL_VALUE, - COL_UNIT, - COL_SENSITIVE, - COL_ICON, -) = range(5) +COL_NAME, COL_VALUE, COL_UNIT, COL_SENSITIVE, COL_ICON_NAME = range(5) +def format_sensor(filename, raw): + raw = raw.strip() + + if filename.startswith("temp"): + return f"{int(raw)/1000:.1f}", _("°C"), "xsi-temperature-symbolic" + + if filename.startswith("fan"): + return raw, _("RPM"), "xsi-cpu-symbolic" + + if filename.startswith("pwm"): + return f"{int(raw)*100/255:.0f}", _("%"), "xsi-cpu-symbolic" + + if filename.startswith("in"): + return f"{int(raw)/1000:.3f}", _("V"), "xsi-cpu-symbolic" + + if filename.startswith("curr"): + return f"{int(raw)/1000:.3f}", _("A"), "xsi-cpu-symbolic" + + if filename.startswith("power"): + return f"{int(raw)/1_000_000:.2f}", _("W"), "xsi-cpu-symbolic" + + if filename.startswith("energy"): + return f"{int(raw)/1_000_000:.2f}", _("J"), "xsi-cpu-symbolic" + + return raw, "", "xsi-cpu-symbolic" class SensorsListWidget(Gtk.ScrolledWindow): - ICONS = { - "root": "xsi-cpu-symbolic", - "temp": "xsi-temperature-symbolic", - "fan": "xsi-cpu-symbolic", - "in": "xsi-cpu-symbolic", - "power": "xsi-cpu-symbolic", - "other": "xsi-cpu-symbolic", - } def __init__(self): super().__init__() - # TreeStore with icon, name, value, unit self.treestore = Gtk.TreeStore(str, str, str, bool, str) + self.treeview = Gtk.TreeView(model=self.treestore) self.treeview.set_enable_tree_lines(True) + self.treeview.set_property("expand", True) self.treeview.set_headers_clickable(True) - # First column: tree + icon + name - renderer = Gtk.CellRendererPixbuf() - column = Gtk.TreeViewColumn("", renderer, icon_name=COL_ICON) - column.set_expand(False) + # --- Columns --- + # Name column with device icon + icon_renderer = Gtk.CellRendererPixbuf() + icon_renderer.set_property("xpad", 2) + icon_renderer.set_property("ypad", 2) + text_renderer = Gtk.CellRendererText() + text_renderer.set_property("ypad", 6) + column = Gtk.TreeViewColumn(_("Name")) + column.pack_start(icon_renderer, False) + column.pack_start(text_renderer, True) + column.add_attribute(icon_renderer, "icon-name", COL_ICON_NAME) + column.add_attribute(text_renderer, "text", COL_NAME) + column.add_attribute(text_renderer, "sensitive", COL_SENSITIVE) + text_renderer.set_property("ellipsize", Pango.EllipsizeMode.END) + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) self.treeview.append_column(column) - - renderer = Gtk.CellRendererText() - renderer.set_property("ellipsize", Pango.EllipsizeMode.END) - column = Gtk.TreeViewColumn(_("Sensor"), renderer, text=COL_NAME) column.set_expand(True) - self.treeview.append_column(column) + column.set_resizable(True) # Value column renderer = Gtk.CellRendererText() @@ -64,19 +83,15 @@ def __init__(self): self.add(self.treeview) self.set_shadow_type(Gtk.ShadowType.IN) - self.hwmon_parents = {} self.sensor_rows = {} - # ------------------------------------------------------------------ def load(self): self.build_tree() self.refresh_values() GLib.timeout_add_seconds(1, self.refresh_values) - # ------------------------------------------------------------------ def build_tree(self): self.treestore.clear() - self.hwmon_parents.clear() self.sensor_rows.clear() if not os.path.isdir(SYS_HWMON): @@ -102,9 +117,10 @@ def build_tree(self): name = name.strip() if name else hwmon parent = self.treestore.append( - None, [name, "", "", False, self.ICONS["root"]] + None, [name, "", "", True, "xsi-cpu-symbolic"] ) - self.hwmon_parents[hwmon_path] = parent + + device_without_sensors = True; # Process all *_input files in base_path for fname in sorted(os.listdir(base_path)): @@ -116,22 +132,26 @@ def build_tree(self): if raw is None: continue + value, unit, icon_name = format_sensor(fname, raw) + # Label label_file = fpath.replace("_input", "_label") label = self._read_file(label_file) label = label.strip() if label else fname.replace("_input", "") - unit, display, stype = self.format_sensor(fname, raw) - itr = self.treestore.append( parent, - [label, display, unit, True, self.ICONS.get(stype, self.ICONS["other"])], + [label, value, unit, True, icon_name], ) self.sensor_rows[fpath] = itr + device_without_sensors = False; + + if (device_without_sensors): + self.treestore.set_value(parent, COL_SENSITIVE, False) + self.treeview.expand_all() - # ------------------------------------------------------------------ def refresh_values(self): for fpath, itr in self.sensor_rows.items(): raw = self._read_file(fpath) @@ -139,35 +159,11 @@ def refresh_values(self): continue fname = os.path.basename(fpath) - _, display, _ = self.format_sensor(fname, raw) - self.treestore.set_value(itr, COL_VALUE, display) + value, _, _ = format_sensor(fname, raw) + self.treestore.set_value(itr, COL_VALUE, value) return True - # ------------------------------------------------------------------ - def format_sensor(self, filename, raw): - raw = raw.strip() - stype = "other" - - if filename.startswith("temp"): - stype = "temp" - return "°C", f"{int(raw)/1000:.1f}", stype - - if filename.startswith("fan"): - stype = "fan" - return "RPM", raw, stype - - if filename.startswith("in"): - stype = "in" - return "V", f"{int(raw)/1000:.3f}", stype - - if filename.startswith("power"): - stype = "power" - return "W", f"{int(raw)/1_000_000:.2f}", stype - - return "", raw, stype - - # ------------------------------------------------------------------ def _read_file(self, path): try: with open(path, "r") as f: From 40924f7e123d2160dc8a2062bcfacca087682a9c Mon Sep 17 00:00:00 2001 From: Pilatomic Date: Sat, 17 Jan 2026 22:25:27 +0100 Subject: [PATCH 03/16] Add freq value, fine tune decimal display --- usr/lib/linuxmint/mintreport/sensors.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 00824ae..b0ceab0 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -30,10 +30,13 @@ def format_sensor(filename, raw): return f"{int(raw)/1000:.3f}", _("A"), "xsi-cpu-symbolic" if filename.startswith("power"): - return f"{int(raw)/1_000_000:.2f}", _("W"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000:.1f}", _("W"), "xsi-cpu-symbolic" + + if filename.startswith("freq"): + return f"{int(raw)/1_000_000_000:.3f}", _("GHz"), "xsi-cpu-symbolic" if filename.startswith("energy"): - return f"{int(raw)/1_000_000:.2f}", _("J"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000:.3f}", _("J"), "xsi-cpu-symbolic" return raw, "", "xsi-cpu-symbolic" From fbaeda0537189e754b9d9c2771962600a6359a24 Mon Sep 17 00:00:00 2001 From: Pilatomic Date: Sat, 17 Jan 2026 22:38:43 +0100 Subject: [PATCH 04/16] Use cog icon as placeholder for measures other than temperature --- usr/lib/linuxmint/mintreport/sensors.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index b0ceab0..1743125 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -18,25 +18,25 @@ def format_sensor(filename, raw): return f"{int(raw)/1000:.1f}", _("°C"), "xsi-temperature-symbolic" if filename.startswith("fan"): - return raw, _("RPM"), "xsi-cpu-symbolic" + return raw, _("RPM"), "xsi-cog-symbolic" if filename.startswith("pwm"): - return f"{int(raw)*100/255:.0f}", _("%"), "xsi-cpu-symbolic" + return f"{int(raw)*100/255:.0f}", _("%"), "xsi-cog-symbolic" if filename.startswith("in"): - return f"{int(raw)/1000:.3f}", _("V"), "xsi-cpu-symbolic" + return f"{int(raw)/1000:.3f}", _("V"), "xsi-cog-symbolic" if filename.startswith("curr"): - return f"{int(raw)/1000:.3f}", _("A"), "xsi-cpu-symbolic" + return f"{int(raw)/1000:.3f}", _("A"), "xsi-cog-symbolic" if filename.startswith("power"): - return f"{int(raw)/1_000_000:.1f}", _("W"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000:.1f}", _("W"), "xsi-cog-symbolic" if filename.startswith("freq"): - return f"{int(raw)/1_000_000_000:.3f}", _("GHz"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000_000:.3f}", _("GHz"), "xsi-cog-symbolic" if filename.startswith("energy"): - return f"{int(raw)/1_000_000:.3f}", _("J"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000:.3f}", _("J"), "xsi-cog-symbolic" return raw, "", "xsi-cpu-symbolic" @@ -108,6 +108,7 @@ def build_tree(self): base_path = None if os.path.isdir(device_path): # Use device/ only if it contains *_input files + # This is required as some modules put sensors files in the device folder (apple-smc for example) inputs = [f for f in os.listdir(device_path) if f.endswith("_input")] if inputs: base_path = device_path From 112056aee7e0790372701febec9528c2838bbc52 Mon Sep 17 00:00:00 2001 From: Pilatomic Date: Sat, 17 Jan 2026 22:41:18 +0100 Subject: [PATCH 05/16] Remove translations for non-translatable units --- usr/lib/linuxmint/mintreport/sensors.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 1743125..cf87517 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -15,28 +15,28 @@ def format_sensor(filename, raw): raw = raw.strip() if filename.startswith("temp"): - return f"{int(raw)/1000:.1f}", _("°C"), "xsi-temperature-symbolic" + return f"{int(raw)/1000:.1f}", "°C", "xsi-temperature-symbolic" if filename.startswith("fan"): return raw, _("RPM"), "xsi-cog-symbolic" if filename.startswith("pwm"): - return f"{int(raw)*100/255:.0f}", _("%"), "xsi-cog-symbolic" + return f"{int(raw)*100/255:.0f}", "%", "xsi-cog-symbolic" if filename.startswith("in"): - return f"{int(raw)/1000:.3f}", _("V"), "xsi-cog-symbolic" + return f"{int(raw)/1000:.3f}", "V", "xsi-cog-symbolic" if filename.startswith("curr"): - return f"{int(raw)/1000:.3f}", _("A"), "xsi-cog-symbolic" + return f"{int(raw)/1000:.3f}", "A", "xsi-cog-symbolic" if filename.startswith("power"): - return f"{int(raw)/1_000_000:.1f}", _("W"), "xsi-cog-symbolic" + return f"{int(raw)/1_000_000:.1f}", "W", "xsi-cog-symbolic" if filename.startswith("freq"): - return f"{int(raw)/1_000_000_000:.3f}", _("GHz"), "xsi-cog-symbolic" + return f"{int(raw)/1_000_000_000:.3f}", "GHz", "xsi-cog-symbolic" if filename.startswith("energy"): - return f"{int(raw)/1_000_000:.3f}", _("J"), "xsi-cog-symbolic" + return f"{int(raw)/1_000_000:.3f}", "J", "xsi-cog-symbolic" return raw, "", "xsi-cpu-symbolic" From 773d3a15f254d9c525640d3ebac92fb0fb439aa7 Mon Sep 17 00:00:00 2001 From: Pila Date: Sun, 18 Jan 2026 15:57:27 +0100 Subject: [PATCH 06/16] Reduce CPU usage, only update row if value changed, and use freeze_notify / thaw_notify --- usr/lib/linuxmint/mintreport/sensors.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index cf87517..eea3f70 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -151,12 +151,13 @@ def build_tree(self): device_without_sensors = False; - if (device_without_sensors): + if device_without_sensors: self.treestore.set_value(parent, COL_SENSITIVE, False) self.treeview.expand_all() def refresh_values(self): + self.treestore.freeze_notify() for fpath, itr in self.sensor_rows.items(): raw = self._read_file(fpath) if raw is None: @@ -164,8 +165,9 @@ def refresh_values(self): fname = os.path.basename(fpath) value, _, _ = format_sensor(fname, raw) - self.treestore.set_value(itr, COL_VALUE, value) - + if value != self.treestore.get_value(itr, COL_VALUE): + self.treestore.set_value(itr, COL_VALUE, value) + self.treestore.thaw_notify() return True def _read_file(self, path): From 8d5a5c526457249c675bd20b5f3f17bda7efef92 Mon Sep 17 00:00:00 2001 From: Pila Date: Sun, 18 Jan 2026 21:38:21 +0100 Subject: [PATCH 07/16] Stop refresh when not visible --- usr/lib/linuxmint/mintreport/sensors.py | 28 ++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index eea3f70..c6d2d6f 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -88,10 +88,32 @@ def __init__(self): self.sensor_rows = {} + self.timeout_id = None + self.refresh_interval = 1 # seconds + + self.connect("map", self._on_map) + self.connect("unmap", self._on_unmap) + def load(self): - self.build_tree() - self.refresh_values() - GLib.timeout_add_seconds(1, self.refresh_values) + # do nothing, we do everything in the _on_map() function + pass + + def _on_map(self, *arg): + if self.timeout_id is None: + # Refresh existing tree or build it is does not exist yet + if self.sensor_rows: + self.refresh_values() + else: + self.build_tree() + + self.timeout_id = GLib.timeout_add_seconds( + self.refresh_interval, self.refresh_values + ) + + def _on_unmap(self, *arg): + if self.timeout_id is not None: + GLib.source_remove(self.timeout_id) + self.timeout_id = None def build_tree(self): self.treestore.clear() From 37eca668042c6ba64cc99893c37b6d9595995cc8 Mon Sep 17 00:00:00 2001 From: Pila Date: Mon, 19 Jan 2026 22:29:52 +0100 Subject: [PATCH 08/16] Move all sensors specifications to a dictionnary. Sort bny natural order (for ease to use & it fixes core order in coretemp) --- usr/lib/linuxmint/mintreport/sensors.py | 162 ++++++++++++++++++------ 1 file changed, 124 insertions(+), 38 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index c6d2d6f..15ff076 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -1,6 +1,8 @@ import os import gi import xapp.util +import re +from enum import IntEnum gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Pango @@ -11,34 +13,99 @@ COL_NAME, COL_VALUE, COL_UNIT, COL_SENSITIVE, COL_ICON_NAME = range(5) -def format_sensor(filename, raw): - raw = raw.strip() - - if filename.startswith("temp"): - return f"{int(raw)/1000:.1f}", "°C", "xsi-temperature-symbolic" - - if filename.startswith("fan"): - return raw, _("RPM"), "xsi-cog-symbolic" - - if filename.startswith("pwm"): - return f"{int(raw)*100/255:.0f}", "%", "xsi-cog-symbolic" - - if filename.startswith("in"): - return f"{int(raw)/1000:.3f}", "V", "xsi-cog-symbolic" - - if filename.startswith("curr"): - return f"{int(raw)/1000:.3f}", "A", "xsi-cog-symbolic" - - if filename.startswith("power"): - return f"{int(raw)/1_000_000:.1f}", "W", "xsi-cog-symbolic" - - if filename.startswith("freq"): - return f"{int(raw)/1_000_000_000:.3f}", "GHz", "xsi-cog-symbolic" - - if filename.startswith("energy"): - return f"{int(raw)/1_000_000:.3f}", "J", "xsi-cog-symbolic" - - return raw, "", "xsi-cpu-symbolic" +class SensorType(IntEnum): + TEMP = 0 + FAN = 1 + PWM = 2 + FREQ = 3 + VOLTAGE = 4 + CURRENT = 5 + POWER = 6 + ENERGY = 7 + OTHER = 99 + + +SENSOR_SPECS = { + SensorType.TEMP: { + "prefix":"temp", + "format":lambda raw: f"{int(raw)/1000:.1f}", + "unit":"°C", + "icon":"xsi-temperature-symbolic" + }, + SensorType.FAN: { + "prefix":"fan", + "format":lambda raw: raw.strip(), + "unit":_("RPM"), + "icon":"xsi-cog-symbolic" + }, + SensorType.PWM: { + "prefix":"pwm", + "format":lambda raw: f"{int(raw)*100/255:.0f}", + "unit":"%", + "icon":"xsi-cog-symbolic" + }, + SensorType.FREQ: { + "prefix":"freq", + "format":lambda raw: f"{int(raw)/1_000_000_000:.3f}", + "unit":"GHz", + "icon":"xsi-cog-symbolic" + }, + SensorType.VOLTAGE: { + "prefix":"in", + "format":lambda raw: f"{int(raw)/1000:.3f}", + "unit":"V", + "icon":"xsi-cog-symbolic" + }, + SensorType.CURRENT: { + "prefix":"curr", + "format":lambda raw: f"{int(raw)/1000:.3f}", + "unit":"A", + "icon":"xsi-cog-symbolic" + }, + SensorType.POWER: { + "prefix":"power", + "format":lambda raw: f"{int(raw)/1_000_000:.1f}", + "unit":"W", + "icon":"xsi-cog-symbolic" + }, + SensorType.ENERGY: { + "prefix":"energy", + "format":lambda raw: f"{int(raw)/1_000_000:.3f}", + "unit":"J", + "icon":"xsi-cog-symbolic" + }, + SensorType.OTHER: { + "prefix":"", + "format":lambda raw: raw.strip(), + "unit":"", + "icon":"xsi-cog-symbolic" + }, +} + +def sensor_spec_from_filename(filename): + for stype, spec in SENSOR_SPECS.items(): + prefix = spec["prefix"] + if prefix and filename.startswith(prefix): + return stype, spec + return SensorType.OTHER, SENSOR_SPECS[SensorType.OTHER] + +# Helper funcs to sort sensors in correct numerical order (ex in10 after in9) +def natural_key(label): + # Split around any digit sequence + parts = re.split(r'(\d+)', label) + key = [] + for part in parts: + if part.isdigit(): + key.append(int(part)) + else: + key.append(part.lower()) + return key + +def sort_sensors(sensors): + # Natural sort within each sensor type + sensors.sort(key=lambda s: natural_key(s["label"])) + # Group by sensor type + sensors.sort(key=lambda s: s["type"]) class SensorsListWidget(Gtk.ScrolledWindow): @@ -122,7 +189,7 @@ def build_tree(self): if not os.path.isdir(SYS_HWMON): return - for hwmon in sorted(os.listdir(SYS_HWMON)): + for hwmon in os.listdir(SYS_HWMON): hwmon_path = os.path.join(SYS_HWMON, hwmon) device_path = os.path.join(hwmon_path, "device") @@ -146,10 +213,11 @@ def build_tree(self): None, [name, "", "", True, "xsi-cpu-symbolic"] ) - device_without_sensors = True; + device_without_sensors = True # Process all *_input files in base_path - for fname in sorted(os.listdir(base_path)): + sensors = [] + for fname in os.listdir(base_path): if not fname.endswith("_input"): continue @@ -158,20 +226,35 @@ def build_tree(self): if raw is None: continue - value, unit, icon_name = format_sensor(fname, raw) + stype, spec = sensor_spec_from_filename(fname) + value = spec["format"](raw) # Label label_file = fpath.replace("_input", "_label") label = self._read_file(label_file) label = label.strip() if label else fname.replace("_input", "") + sensors.append({ + "label": label, + "path": fpath, + "value": value, + "unit": spec["unit"], + "icon": spec["icon"], + "type": stype, + }) + + sort_sensors(sensors) + + # Add sorted sensors to treestore + for s in sensors: itr = self.treestore.append( parent, - [label, value, unit, True, icon_name], + [s["label"], s["value"], s["unit"], True, s["icon"]], ) - self.sensor_rows[fpath] = itr - device_without_sensors = False; + # Store TreeStore itr and sensor type by path for refresh + self.sensor_rows[s["path"]] = (itr, s["type"]) + device_without_sensors = False if device_without_sensors: self.treestore.set_value(parent, COL_SENSITIVE, False) @@ -180,15 +263,18 @@ def build_tree(self): def refresh_values(self): self.treestore.freeze_notify() - for fpath, itr in self.sensor_rows.items(): + + for fpath, (itr, stype) in self.sensor_rows.items(): raw = self._read_file(fpath) if raw is None: continue - fname = os.path.basename(fpath) - value, _, _ = format_sensor(fname, raw) + spec = SENSOR_SPECS[stype] + value = spec["format"](raw) + if value != self.treestore.get_value(itr, COL_VALUE): self.treestore.set_value(itr, COL_VALUE, value) + self.treestore.thaw_notify() return True From 9d79dba5dfbb4330b0b717e569d401a7d8e1cd66 Mon Sep 17 00:00:00 2001 From: Pila Date: Tue, 20 Jan 2026 17:46:19 +0100 Subject: [PATCH 09/16] Also sort devices --- usr/lib/linuxmint/mintreport/sensors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 15ff076..0e7079c 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -189,7 +189,8 @@ def build_tree(self): if not os.path.isdir(SYS_HWMON): return - for hwmon in os.listdir(SYS_HWMON): + # sort hwmon folders by natural order, as the listdir order is random + for hwmon in sorted(os.listdir(SYS_HWMON), key=natural_key): hwmon_path = os.path.join(SYS_HWMON, hwmon) device_path = os.path.join(hwmon_path, "device") From b852a5ba015acec34df26a1d412482d46687a1dc Mon Sep 17 00:00:00 2001 From: Pila Date: Wed, 21 Jan 2026 19:37:53 +0100 Subject: [PATCH 10/16] Implement suffix to properly match pwm file name. Remove SensorType.OTHER, we implement all useful sensor types. Group POWER sensors close to FREQ --- usr/lib/linuxmint/mintreport/sensors.py | 45 +++++++++++++------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 0e7079c..0aa6671 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -18,76 +18,78 @@ class SensorType(IntEnum): FAN = 1 PWM = 2 FREQ = 3 - VOLTAGE = 4 - CURRENT = 5 - POWER = 6 + POWER = 4 + VOLTAGE = 5 + CURRENT = 6 ENERGY = 7 - OTHER = 99 SENSOR_SPECS = { SensorType.TEMP: { "prefix":"temp", + "suffix":"_input", "format":lambda raw: f"{int(raw)/1000:.1f}", "unit":"°C", "icon":"xsi-temperature-symbolic" }, SensorType.FAN: { "prefix":"fan", + "suffix":"_input", "format":lambda raw: raw.strip(), "unit":_("RPM"), "icon":"xsi-cog-symbolic" }, SensorType.PWM: { "prefix":"pwm", + "suffix":"", # no _input suffix for pwm "format":lambda raw: f"{int(raw)*100/255:.0f}", "unit":"%", "icon":"xsi-cog-symbolic" }, SensorType.FREQ: { "prefix":"freq", + "suffix":"_input", "format":lambda raw: f"{int(raw)/1_000_000_000:.3f}", "unit":"GHz", "icon":"xsi-cog-symbolic" }, SensorType.VOLTAGE: { "prefix":"in", + "suffix":"_input", "format":lambda raw: f"{int(raw)/1000:.3f}", "unit":"V", "icon":"xsi-cog-symbolic" }, SensorType.CURRENT: { "prefix":"curr", + "suffix":"_input", "format":lambda raw: f"{int(raw)/1000:.3f}", "unit":"A", "icon":"xsi-cog-symbolic" }, SensorType.POWER: { "prefix":"power", + "suffix":"_input", "format":lambda raw: f"{int(raw)/1_000_000:.1f}", "unit":"W", "icon":"xsi-cog-symbolic" }, SensorType.ENERGY: { "prefix":"energy", + "suffix":"_input", "format":lambda raw: f"{int(raw)/1_000_000:.3f}", "unit":"J", "icon":"xsi-cog-symbolic" - }, - SensorType.OTHER: { - "prefix":"", - "format":lambda raw: raw.strip(), - "unit":"", - "icon":"xsi-cog-symbolic" - }, + } } def sensor_spec_from_filename(filename): for stype, spec in SENSOR_SPECS.items(): prefix = spec["prefix"] - if prefix and filename.startswith(prefix): + suffix = spec["suffix"] + if filename.startswith(prefix) and filename.endswith(suffix): return stype, spec - return SensorType.OTHER, SENSOR_SPECS[SensorType.OTHER] + return None, None # Helper funcs to sort sensors in correct numerical order (ex in10 after in9) def natural_key(label): @@ -219,26 +221,25 @@ def build_tree(self): # Process all *_input files in base_path sensors = [] for fname in os.listdir(base_path): - if not fname.endswith("_input"): - continue + stype, spec = sensor_spec_from_filename(fname) + if spec is None: + continue # that's not a sensor fpath = os.path.join(base_path, fname) raw = self._read_file(fpath) if raw is None: - continue - - stype, spec = sensor_spec_from_filename(fname) - value = spec["format"](raw) + continue # unable to read sensor -> skip # Label - label_file = fpath.replace("_input", "_label") - label = self._read_file(label_file) + labelname = fname.replace(spec["suffix"], "_label") + labelpath = os.path.join(base_path, labelname) + label = self._read_file(labelpath) label = label.strip() if label else fname.replace("_input", "") sensors.append({ "label": label, "path": fpath, - "value": value, + "value": spec["format"](raw), "unit": spec["unit"], "icon": spec["icon"], "type": stype, From 54dd74164d568b765650ae4acde159d0f831cf18 Mon Sep 17 00:00:00 2001 From: Clement Lefebvre Date: Wed, 25 Feb 2026 14:34:38 +0000 Subject: [PATCH 11/16] Use new XSI icons --- usr/lib/linuxmint/mintreport/sensors.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 0aa6671..59618b9 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -37,49 +37,49 @@ class SensorType(IntEnum): "suffix":"_input", "format":lambda raw: raw.strip(), "unit":_("RPM"), - "icon":"xsi-cog-symbolic" + "icon":"xsi-fan-symbolic" }, SensorType.PWM: { "prefix":"pwm", "suffix":"", # no _input suffix for pwm "format":lambda raw: f"{int(raw)*100/255:.0f}", "unit":"%", - "icon":"xsi-cog-symbolic" + "icon":"xsi-fan-symbolic" }, SensorType.FREQ: { "prefix":"freq", "suffix":"_input", "format":lambda raw: f"{int(raw)/1_000_000_000:.3f}", "unit":"GHz", - "icon":"xsi-cog-symbolic" + "icon":"xsi-physics-wavelength-symbolic" }, SensorType.VOLTAGE: { "prefix":"in", "suffix":"_input", "format":lambda raw: f"{int(raw)/1000:.3f}", "unit":"V", - "icon":"xsi-cog-symbolic" + "icon":"xsi-physics-volts-symbolic" }, SensorType.CURRENT: { "prefix":"curr", "suffix":"_input", "format":lambda raw: f"{int(raw)/1000:.3f}", "unit":"A", - "icon":"xsi-cog-symbolic" + "icon":"xsi-physics-wave-symbolic" }, SensorType.POWER: { "prefix":"power", "suffix":"_input", "format":lambda raw: f"{int(raw)/1_000_000:.1f}", "unit":"W", - "icon":"xsi-cog-symbolic" + "icon":"xsi-physics-watts-symbolic" }, SensorType.ENERGY: { "prefix":"energy", "suffix":"_input", "format":lambda raw: f"{int(raw)/1_000_000:.3f}", "unit":"J", - "icon":"xsi-cog-symbolic" + "icon":"xsi-power-symbolic" } } From 9a36f1b9fe1b298c580606412601642bf8d13a41 Mon Sep 17 00:00:00 2001 From: Clement Lefebvre Date: Wed, 25 Feb 2026 14:46:01 +0000 Subject: [PATCH 12/16] Switch to a card view --- usr/lib/linuxmint/mintreport/sensors.py | 124 +++++++++--------------- 1 file changed, 48 insertions(+), 76 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 59618b9..43f120c 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -1,18 +1,17 @@ import os import gi import xapp.util +import xapp.SettingsWidgets as Xs import re from enum import IntEnum gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Pango +from gi.repository import Gtk, GLib _ = xapp.util.l10n("mintreport") SYS_HWMON = "/sys/class/hwmon" -COL_NAME, COL_VALUE, COL_UNIT, COL_SENSITIVE, COL_ICON_NAME = range(5) - class SensorType(IntEnum): TEMP = 0 FAN = 1 @@ -41,7 +40,7 @@ class SensorType(IntEnum): }, SensorType.PWM: { "prefix":"pwm", - "suffix":"", # no _input suffix for pwm + "suffix":"", # no _input suffix for pwm "format":lambda raw: f"{int(raw)*100/255:.0f}", "unit":"%", "icon":"xsi-fan-symbolic" @@ -113,49 +112,17 @@ class SensorsListWidget(Gtk.ScrolledWindow): def __init__(self): super().__init__() + self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + + self.page = Xs.SettingsPage() + self.page.set_spacing(24) + self.page.set_margin_left(24) + self.page.set_margin_right(24) + self.page.set_margin_top(12) + self.page.set_margin_bottom(12) + self.add(self.page) - self.treestore = Gtk.TreeStore(str, str, str, bool, str) - - self.treeview = Gtk.TreeView(model=self.treestore) - self.treeview.set_enable_tree_lines(True) - self.treeview.set_property("expand", True) - self.treeview.set_headers_clickable(True) - - # --- Columns --- - # Name column with device icon - icon_renderer = Gtk.CellRendererPixbuf() - icon_renderer.set_property("xpad", 2) - icon_renderer.set_property("ypad", 2) - text_renderer = Gtk.CellRendererText() - text_renderer.set_property("ypad", 6) - column = Gtk.TreeViewColumn(_("Name")) - column.pack_start(icon_renderer, False) - column.pack_start(text_renderer, True) - column.add_attribute(icon_renderer, "icon-name", COL_ICON_NAME) - column.add_attribute(text_renderer, "text", COL_NAME) - column.add_attribute(text_renderer, "sensitive", COL_SENSITIVE) - text_renderer.set_property("ellipsize", Pango.EllipsizeMode.END) - column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) - self.treeview.append_column(column) - column.set_expand(True) - column.set_resizable(True) - - # Value column - renderer = Gtk.CellRendererText() - column = Gtk.TreeViewColumn(_("Value"), renderer, text=COL_VALUE) - column.set_expand(False) - self.treeview.append_column(column) - - # Unit column - renderer = Gtk.CellRendererText() - column = Gtk.TreeViewColumn(_("Unit"), renderer, text=COL_UNIT) - column.set_expand(False) - self.treeview.append_column(column) - - self.add(self.treeview) - self.set_shadow_type(Gtk.ShadowType.IN) - - self.sensor_rows = {} + self.sensor_rows = {} # fpath -> (value_label, stype) self.timeout_id = None self.refresh_interval = 1 # seconds @@ -167,25 +134,26 @@ def load(self): # do nothing, we do everything in the _on_map() function pass - def _on_map(self, *arg): + def _on_map(self, *_): if self.timeout_id is None: - # Refresh existing tree or build it is does not exist yet + # Refresh existing cards or build them if they don't exist yet if self.sensor_rows: self.refresh_values() else: - self.build_tree() + self.build_cards() self.timeout_id = GLib.timeout_add_seconds( self.refresh_interval, self.refresh_values ) - def _on_unmap(self, *arg): + def _on_unmap(self, *_): if self.timeout_id is not None: GLib.source_remove(self.timeout_id) self.timeout_id = None - def build_tree(self): - self.treestore.clear() + def build_cards(self): + for child in self.page.get_children(): + self.page.remove(child) self.sensor_rows.clear() if not os.path.isdir(SYS_HWMON): @@ -212,13 +180,7 @@ def build_tree(self): name = self._read_file(name_file) name = name.strip() if name else hwmon - parent = self.treestore.append( - None, [name, "", "", True, "xsi-cpu-symbolic"] - ) - - device_without_sensors = True - - # Process all *_input files in base_path + # Process all sensor files in base_path sensors = [] for fname in os.listdir(base_path): stype, spec = sensor_spec_from_filename(fname) @@ -245,28 +207,39 @@ def build_tree(self): "type": stype, }) + if not sensors: + continue + sort_sensors(sensors) - # Add sorted sensors to treestore + section = self.page.add_section(name) + for s in sensors: - itr = self.treestore.append( - parent, - [s["label"], s["value"], s["unit"], True, s["icon"]], - ) + row = Xs.SettingsWidget() + row.set_spacing(8) - # Store TreeStore itr and sensor type by path for refresh - self.sensor_rows[s["path"]] = (itr, s["type"]) - device_without_sensors = False + icon = Gtk.Image.new_from_icon_name(s["icon"], Gtk.IconSize.MENU) + icon.set_pixel_size(16) + row.pack_start(icon, False, False, 0) - if device_without_sensors: - self.treestore.set_value(parent, COL_SENSITIVE, False) + name_label = Gtk.Label(label=s["label"]) + name_label.set_xalign(0) + row.pack_start(name_label, True, True, 0) - self.treeview.expand_all() + value_label = Gtk.Label(label=s["value"]) + value_label.get_style_context().add_class("dim-label") + unit_label = Gtk.Label(label=s["unit"]) + unit_label.get_style_context().add_class("dim-label") + row.pack_end(unit_label, False, False, 0) + row.pack_end(value_label, False, False, 0) - def refresh_values(self): - self.treestore.freeze_notify() + section.add_row(row) + self.sensor_rows[s["path"]] = (value_label, s["type"]) - for fpath, (itr, stype) in self.sensor_rows.items(): + self.page.show_all() + + def refresh_values(self): + for fpath, (value_label, stype) in self.sensor_rows.items(): raw = self._read_file(fpath) if raw is None: continue @@ -274,10 +247,9 @@ def refresh_values(self): spec = SENSOR_SPECS[stype] value = spec["format"](raw) - if value != self.treestore.get_value(itr, COL_VALUE): - self.treestore.set_value(itr, COL_VALUE, value) + if value != value_label.get_text(): + value_label.set_text(value) - self.treestore.thaw_notify() return True def _read_file(self, path): From 3fe3ec82a7df3f877090ef3a8c549b745fb77093 Mon Sep 17 00:00:00 2001 From: Clement Lefebvre Date: Wed, 25 Feb 2026 17:53:46 +0000 Subject: [PATCH 13/16] Format the values nicely --- usr/lib/linuxmint/mintreport/sensors.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 43f120c..1dea0d3 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -48,21 +48,21 @@ class SensorType(IntEnum): SensorType.FREQ: { "prefix":"freq", "suffix":"_input", - "format":lambda raw: f"{int(raw)/1_000_000_000:.3f}", + "format":lambda raw: f"{int(raw)/1_000_000_000:.2f}", "unit":"GHz", "icon":"xsi-physics-wavelength-symbolic" }, SensorType.VOLTAGE: { "prefix":"in", "suffix":"_input", - "format":lambda raw: f"{int(raw)/1000:.3f}", + "format":lambda raw: f"{int(raw)/1000:.2f}", "unit":"V", "icon":"xsi-physics-volts-symbolic" }, SensorType.CURRENT: { "prefix":"curr", "suffix":"_input", - "format":lambda raw: f"{int(raw)/1000:.3f}", + "format":lambda raw: f"{int(raw)/1000:.2f}", "unit":"A", "icon":"xsi-physics-wave-symbolic" }, @@ -76,7 +76,7 @@ class SensorType(IntEnum): SensorType.ENERGY: { "prefix":"energy", "suffix":"_input", - "format":lambda raw: f"{int(raw)/1_000_000:.3f}", + "format":lambda raw: f"{int(raw)/1_000_000:.1f}", "unit":"J", "icon":"xsi-power-symbolic" } @@ -123,6 +123,8 @@ def __init__(self): self.add(self.page) self.sensor_rows = {} # fpath -> (value_label, stype) + self.value_size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + self.unit_size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) self.timeout_id = None self.refresh_interval = 1 # seconds @@ -228,8 +230,12 @@ def build_cards(self): value_label = Gtk.Label(label=s["value"]) value_label.get_style_context().add_class("dim-label") + value_label.set_xalign(1.0) unit_label = Gtk.Label(label=s["unit"]) unit_label.get_style_context().add_class("dim-label") + unit_label.set_xalign(0.0) + self.value_size_group.add_widget(value_label) + self.unit_size_group.add_widget(unit_label) row.pack_end(unit_label, False, False, 0) row.pack_end(value_label, False, False, 0) From b20bcaf0ed0c3bd688056f68cbcb75b68055f56d Mon Sep 17 00:00:00 2001 From: Clement Lefebvre Date: Wed, 25 Feb 2026 18:13:01 +0000 Subject: [PATCH 14/16] Don't let multiple chips have the same name --- usr/lib/linuxmint/mintreport/sensors.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 1dea0d3..b50ae27 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -161,7 +161,8 @@ def build_cards(self): if not os.path.isdir(SYS_HWMON): return - # sort hwmon folders by natural order, as the listdir order is random + # First pass: collect all chips with their sensors + chips = [] for hwmon in sorted(os.listdir(SYS_HWMON), key=natural_key): hwmon_path = os.path.join(SYS_HWMON, hwmon) device_path = os.path.join(hwmon_path, "device") @@ -213,6 +214,17 @@ def build_cards(self): continue sort_sensors(sensors) + chips.append((hwmon, name, sensors)) + + # Disambiguate chips that share the same name (e.g. two RAM sticks: spd5118) + name_counts = {} + for _, name, _ in chips: + name_counts[name] = name_counts.get(name, 0) + 1 + + # Second pass: build cards + for hwmon, name, sensors in chips: + if name_counts[name] > 1: + name = f"{name} ({hwmon})" section = self.page.add_section(name) From 94a334fb6aeab664806ddd22db945651cd2d08c4 Mon Sep 17 00:00:00 2001 From: Pila Date: Wed, 25 Feb 2026 19:44:23 +0100 Subject: [PATCH 15/16] Fix PWM --- usr/lib/linuxmint/mintreport/sensors.py | 39 ++++++++++--------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index b50ae27..b0a8709 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -25,57 +25,49 @@ class SensorType(IntEnum): SENSOR_SPECS = { SensorType.TEMP: { - "prefix":"temp", - "suffix":"_input", + "pattern":re.compile(r"^(temp\d+)_input$"), "format":lambda raw: f"{int(raw)/1000:.1f}", "unit":"°C", "icon":"xsi-temperature-symbolic" }, SensorType.FAN: { - "prefix":"fan", - "suffix":"_input", + "pattern":re.compile(r"^(fan\d+)_input$"), "format":lambda raw: raw.strip(), "unit":_("RPM"), "icon":"xsi-fan-symbolic" }, SensorType.PWM: { - "prefix":"pwm", - "suffix":"", # no _input suffix for pwm + "pattern":re.compile(r"^(pwm\d+)$"), # no _input suffix for pwm type "format":lambda raw: f"{int(raw)*100/255:.0f}", "unit":"%", "icon":"xsi-fan-symbolic" }, SensorType.FREQ: { - "prefix":"freq", - "suffix":"_input", + "pattern":re.compile(r"^(freq\d+)_input$"), "format":lambda raw: f"{int(raw)/1_000_000_000:.2f}", "unit":"GHz", "icon":"xsi-physics-wavelength-symbolic" }, SensorType.VOLTAGE: { - "prefix":"in", - "suffix":"_input", + "pattern":re.compile(r"^(in\d+)_input$"), "format":lambda raw: f"{int(raw)/1000:.2f}", "unit":"V", "icon":"xsi-physics-volts-symbolic" }, SensorType.CURRENT: { - "prefix":"curr", - "suffix":"_input", + "pattern":re.compile(r"^(curr\d+)_input$"), "format":lambda raw: f"{int(raw)/1000:.2f}", "unit":"A", "icon":"xsi-physics-wave-symbolic" }, SensorType.POWER: { - "prefix":"power", - "suffix":"_input", + "pattern":re.compile(r"^(power\d+)_input$"), "format":lambda raw: f"{int(raw)/1_000_000:.1f}", "unit":"W", "icon":"xsi-physics-watts-symbolic" }, SensorType.ENERGY: { - "prefix":"energy", - "suffix":"_input", + "pattern":re.compile(r"^(energy\d+)_input$"), "format":lambda raw: f"{int(raw)/1_000_000:.1f}", "unit":"J", "icon":"xsi-power-symbolic" @@ -84,11 +76,11 @@ class SensorType(IntEnum): def sensor_spec_from_filename(filename): for stype, spec in SENSOR_SPECS.items(): - prefix = spec["prefix"] - suffix = spec["suffix"] - if filename.startswith(prefix) and filename.endswith(suffix): - return stype, spec - return None, None + m = spec["pattern"].match(filename) + if m: + base = m.group(1) + return stype, spec, base + return None, None, None # Helper funcs to sort sensors in correct numerical order (ex in10 after in9) def natural_key(label): @@ -186,7 +178,7 @@ def build_cards(self): # Process all sensor files in base_path sensors = [] for fname in os.listdir(base_path): - stype, spec = sensor_spec_from_filename(fname) + stype, spec, base_name = sensor_spec_from_filename(fname) if spec is None: continue # that's not a sensor @@ -196,8 +188,7 @@ def build_cards(self): continue # unable to read sensor -> skip # Label - labelname = fname.replace(spec["suffix"], "_label") - labelpath = os.path.join(base_path, labelname) + labelpath = os.path.join(base_path, f"{base_name}_label") label = self._read_file(labelpath) label = label.strip() if label else fname.replace("_input", "") From b340a143ad89a3a04357a68f2b1bb033e701069a Mon Sep 17 00:00:00 2001 From: Pila Date: Thu, 26 Feb 2026 11:13:08 +0100 Subject: [PATCH 16/16] Use correct icon for current measurement --- usr/lib/linuxmint/mintreport/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index b0a8709..a7bb16c 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -58,7 +58,7 @@ class SensorType(IntEnum): "pattern":re.compile(r"^(curr\d+)_input$"), "format":lambda raw: f"{int(raw)/1000:.2f}", "unit":"A", - "icon":"xsi-physics-wave-symbolic" + "icon":"xsi-physics-amps-symbolic" }, SensorType.POWER: { "pattern":re.compile(r"^(power\d+)_input$"),