From 5b7400c7c013aca54895ffb0792286815ce34d4b Mon Sep 17 00:00:00 2001 From: SpectrixDev Date: Tue, 17 Jun 2025 20:23:06 +0100 Subject: [PATCH 1/3] Add toggleable system temperature monitoring widget --- config/data.py | 2 + config/settings_gui.py | 3 +- modules/bar.py | 6 +- modules/metrics.py | 212 +++++++++++++++++++++++++++++++++++------ styles/metrics.css | 6 ++ 5 files changed, 200 insertions(+), 29 deletions(-) diff --git a/config/data.py b/config/data.py index 29ee6119..2a95b722 100644 --- a/config/data.py +++ b/config/data.py @@ -89,6 +89,7 @@ def load_config(): 'weather': config.get('bar_weather_visible', True), 'battery': config.get('bar_battery_visible', True), 'metrics': config.get('bar_metrics_visible', True), + 'temperatures': config.get('bar_temperatures_visible', False), 'language': config.get('bar_language_visible', True), 'date_time': config.get('bar_date_time_visible', True), 'button_power': config.get('bar_button_power_visible', True), @@ -128,6 +129,7 @@ def load_config(): 'weather': True, 'battery': True, 'metrics': True, + 'temperatures': False, 'language': True, 'date_time': True, 'button_power': True, diff --git a/config/settings_gui.py b/config/settings_gui.py index 2c5cc703..ecf515ae 100644 --- a/config/settings_gui.py +++ b/config/settings_gui.py @@ -26,6 +26,7 @@ from .settings_utils import backup_and_replace, bind_vars, start_config + class HyprConfGUI(Window): def __init__(self, show_lock_checkbox: bool, show_idle_checkbox: bool, **kwargs): super().__init__( @@ -416,7 +417,7 @@ def create_appearance_tab(self): 'button_apps': "App Launcher Button", 'systray': "System Tray", 'control': "Control Panel", 'network': "Network Applet", 'button_tools': "Toolbox Button", 'button_overview': "Overview Button", 'ws_container': "Workspaces", 'weather': "Weather Widget", 'battery': "Battery Indicator", - 'metrics': "System Metrics", 'language': "Language Indicator", 'date_time': "Date & Time", + 'metrics': "System Metrics", 'temperatures': "System Temps", 'language': "Language Indicator", 'date_time': "Date & Time", 'button_power': "Power Button", } diff --git a/modules/bar.py b/modules/bar.py index 78272ad5..1e273537 100644 --- a/modules/bar.py +++ b/modules/bar.py @@ -17,7 +17,7 @@ import modules.icons as icons from modules.controls import ControlSmall from modules.dock import Dock -from modules.metrics import Battery, MetricsSmall, NetworkApplet +from modules.metrics import Battery, MetricsSmall, NetworkApplet, TemperaturesBar from modules.systemtray import SystemTray from modules.weather import Weather from widgets.wayland import WaylandWindow as Window @@ -219,12 +219,14 @@ def __init__(self, **kwargs): self.button_overview.connect("leave_notify_event", self.on_button_leave) self.control = ControlSmall() + self.temperatures_bar = TemperaturesBar() self.metrics = MetricsSmall() self.battery = Battery() self.apply_component_props() self.rev_right = [ + self.temperatures_bar, self.metrics, self.control, ] @@ -441,6 +443,7 @@ def apply_component_props(self): 'weather': self.weather, 'battery': self.battery, 'metrics': self.metrics, + 'temperatures': self.temperatures_bar, 'language': self.language, 'date_time': self.date_time, 'button_power': self.button_power, @@ -462,6 +465,7 @@ def toggle_component_visibility(self, component_name): 'weather': self.weather, 'battery': self.battery, 'metrics': self.metrics, + 'temperatures': self.temperatures_bar, 'language': self.language, 'date_time': self.date_time, 'button_power': self.button_power, diff --git a/modules/metrics.py b/modules/metrics.py index 5d0aa4c7..8146b755 100644 --- a/modules/metrics.py +++ b/modules/metrics.py @@ -1,7 +1,10 @@ import json import logging +import os +import glob import subprocess import time +import shutil import psutil from fabric.core.fabricator import Fabricator @@ -33,6 +36,8 @@ def __init__(self): self.cpu = 0.0 self.mem = 0.0 self.disk = [] + self.cpu_temp = None + self.gpu_temp = None self.upower = UPowerManager() self.display_device = self.upower.get_display_device() @@ -49,6 +54,12 @@ def _update(self): self.mem = psutil.virtual_memory().percent self.disk = [psutil.disk_usage(path).percent for path in data.BAR_METRICS_DISKS] + # Fetch CPU temperature using multiple providers + self.cpu_temp = self._get_cpu_temperature() + + # Fetch GPU temperature using multiple providers + self.gpu_temp = self._get_gpu_temperature() + if not self._gpu_update_running: self._start_gpu_update_async() @@ -64,6 +75,80 @@ def _update(self): return True + def _get_cpu_temperature(self): + """Attempt to get CPU temperature from multiple sources.""" + # Provider 1: psutil sensors_temperatures + try: + temps = psutil.sensors_temperatures() + cpu_temp = None + for key in temps: + if key.lower().startswith("coretemp") or key.lower().startswith("k10temp") or key.lower().startswith("cpu"): + entries = temps[key] + if entries: + cpu_temp = entries[0].current + break + if cpu_temp is not None: + return int(cpu_temp) + except Exception: + pass + + # Provider 2: Read from /sys/class/thermal + try: + thermal_paths = glob.glob("/sys/class/thermal/thermal_zone*/temp") + for path in thermal_paths: + with open(path, 'r') as f: + temp = int(f.read().strip()) / 1000 # Temp is in millidegrees Celsius + if 0 < temp < 150: # Basic sanity check + return int(temp) + except Exception: + pass + + # If no providers succeed, return None + return None + + def _get_gpu_temperature(self): + """Attempt to get GPU temperature from multiple sources.""" + # Provider 1: AMD GPU via sysfs + try: + hwmon_paths = glob.glob("/sys/class/drm/card*/device/hwmon/hwmon*/temp*_input") + amd_temps = [] + for path in hwmon_paths: + name_file = os.path.join(os.path.dirname(path), "name") + if os.path.exists(name_file): + with open(name_file, 'r') as f_name: + if 'amdgpu' not in f_name.read().lower(): + continue + try: + with open(path, 'r') as f: + temp = int(f.read().strip()) / 1000 + amd_temps.append(temp) + except Exception: + continue + if amd_temps: + max_temp = max(amd_temps) + if 0 < max_temp < 150: + return int(max_temp) + except Exception: + pass + + # Provider 2: NVIDIA via nvidia-smi + try: + if shutil.which("nvidia-smi") is not None: + result = subprocess.check_output( + ["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"], + text=True, timeout=2 + ) + lines = result.strip().splitlines() + if lines: + nvidia_temp = int(lines[0]) + if 0 < nvidia_temp < 150: + return nvidia_temp + except Exception: + pass + + # If no providers succeed, return None + return None + def _start_gpu_update_async(self): """Starts a new GLib thread to run nvtop in the background.""" self._gpu_update_running = True @@ -119,7 +204,14 @@ def _process_gpu_output(self, output, error_message): return False def get_metrics(self): - return (self.cpu, self.mem, self.disk, self.gpu) + return { + "cpu": self.cpu, + "mem": self.mem, + "disk": self.disk, + "gpu": self.gpu, + "cpu_temp": self.cpu_temp, + "gpu_temp": self.gpu_temp, + } def get_battery(self): return (self.bat_percent, self.bat_charging, self.bat_time) @@ -146,8 +238,73 @@ def get_gpu_info(self): shared_provider = MetricsProvider() +class TemperatureIndicator(Box): + def __init__(self, id, icon): + super().__init__( + name=f"{id}-temp-indicator", + orientation="h", + spacing=4, + visible=True, + all_visible=True, + ) + self.icon = Label( + name=f"{id}-temp-icon", + markup=icon, + use_markup=True, + ) + self.bar = Scale( + name=f"{id}-temp-bar", + value=0, + orientation="h", + h_align="fill", + h_expand=True, + style_classes=[id, "temp-bar"], + ) + self.temp_label = Label( + name=f"{id}-temp-label", + label="--°C", + use_markup=True, + ) + self.add(self.icon) + self.add(self.bar) + self.add(self.temp_label) + self.bar.set_sensitive(False) + + def set_temp(self, temp): + if temp is not None: + value = max(0, min(100, temp)) / 100.0 + self.bar.value = value + self.temp_label.set_label(f"{temp}°C") + else: + self.bar.value = 0 + self.temp_label.set_label("--°C") + +class TemperaturesBar(Box): + def __init__(self): + super().__init__( + name="temps-bar", + orientation="h", + spacing=8, + visible=True, + all_visible=True, + style_classes=["temps-bar-container"], + ) + self.cpu = TemperatureIndicator("cpu", icons.cpu) + self.gpu = TemperatureIndicator("gpu", icons.gpu) + self.add(self.cpu) + self.add(self.gpu) + GLib.timeout_add_seconds(1, self.update_temps) + + def update_temps(self): + metrics = shared_provider.get_metrics() + self.cpu.set_temp(metrics["cpu_temp"]) + self.gpu.set_temp(metrics["gpu_temp"]) + return True + + class SingularMetric: def __init__(self, id, name, icon): + # Usage bar (vertical) self.usage = Scale( name=f"{id}-usage", value=0.25, @@ -157,15 +314,18 @@ def __init__(self, id, name, icon): v_expand=True, ) + # Icon label (icon only) self.label = Label( name=f"{id}-label", markup=icon, + use_markup=True, ) + # Outer box for this metric self.box = Box( name=f"{id}-box", orientation='v', - spacing=8, + spacing=2, children=[ self.usage, self.label, @@ -217,20 +377,18 @@ def __init__(self, **kwargs): GLib.timeout_add_seconds(1, self.update_status) def update_status(self): - cpu, mem, disks, gpus = shared_provider.get_metrics() + metrics = shared_provider.get_metrics() if self.cpu: - self.cpu.usage.value = cpu / 100.0 + self.cpu.usage.value = metrics["cpu"] / 100.0 if self.ram: - self.ram.usage.value = mem / 100.0 + self.ram.usage.value = metrics["mem"] / 100.0 for i, disk in enumerate(self.disk): - - if i < len(disks): - disk.usage.value = disks[i] / 100.0 + if i < len(metrics["disk"]): + disk.usage.value = metrics["disk"][i] / 100.0 for i, gpu in enumerate(self.gpu): - - if i < len(gpus): - gpu.usage.value = gpus[i] / 100.0 + if i < len(metrics["gpu"]): + gpu.usage.value = metrics["gpu"][i] / 100.0 return True class SingularMetricSmall: @@ -238,7 +396,8 @@ def __init__(self, id, name, icon): self.name_markup = name self.icon_markup = icon - self.icon = Label(name="metrics-icon", markup=icon) + # Icon only, no temp + self.icon = Label(name="metrics-icon", markup=icon, use_markup=True) self.circle = CircularProgressBar( name="metrics-circle", value=0, @@ -266,6 +425,7 @@ def __init__(self, id, name, icon): children=[self.circle, self.revealer], ) + def markup(self): return f"{self.icon_markup} {self.name_markup}" if not data.VERTICAL else f"{self.icon_markup} {self.name_markup}: {self.level.get_label()}" @@ -357,24 +517,22 @@ def hide_revealer(self): return False def update_metrics(self): - cpu, mem, disks, gpus = shared_provider.get_metrics() + metrics = shared_provider.get_metrics() if self.cpu: - self.cpu.circle.set_value(cpu / 100.0) - self.cpu.level.set_label(self._format_percentage(int(cpu))) + self.cpu.circle.set_value(metrics["cpu"] / 100.0) + self.cpu.level.set_label(self._format_percentage(int(metrics["cpu"]))) if self.ram: - self.ram.circle.set_value(mem / 100.0) - self.ram.level.set_label(self._format_percentage(int(mem))) + self.ram.circle.set_value(metrics["mem"] / 100.0) + self.ram.level.set_label(self._format_percentage(int(metrics["mem"]))) for i, disk in enumerate(self.disk): - - if i < len(disks): - disk.circle.set_value(disks[i] / 100.0) - disk.level.set_label(self._format_percentage(int(disks[i]))) + if i < len(metrics["disk"]): + disk.circle.set_value(metrics["disk"][i] / 100.0) + disk.level.set_label(self._format_percentage(int(metrics["disk"][i]))) for i, gpu in enumerate(self.gpu): - - if i < len(gpus): - gpu.circle.set_value(gpus[i] / 100.0) - gpu.level.set_label(self._format_percentage(int(gpus[i]))) + if i < len(metrics["gpu"]): + gpu.circle.set_value(metrics["gpu"][i] / 100.0) + gpu.level.set_label(self._format_percentage(int(metrics["gpu"][i]))) tooltip_metrics = [] if self.disk: tooltip_metrics.extend(self.disk) @@ -473,8 +631,8 @@ def hide_revealer(self): self.hide_timer = None return False - def update_battery(self, sender, battery_data): - value, charging, time = battery_data + def update_battery(self, sender, value): + value, charging, time = value if value == 0: self.set_visible(False) else: diff --git a/styles/metrics.css b/styles/metrics.css index ddc0fd51..a95c8fd5 100644 --- a/styles/metrics.css +++ b/styles/metrics.css @@ -101,3 +101,9 @@ #upload-icon-label.urgent { color: var(--shadow); } + +#temps-bar { + background-color: var(--surface); + padding-left: 6px; + padding-right: 6px; +} From ac5e87ef5c17fd5538117fcac3a3bbea4a15effd Mon Sep 17 00:00:00 2001 From: SpectrixDev Date: Sun, 20 Jul 2025 13:11:32 +0100 Subject: [PATCH 2/3] Add temperature polling interval configuration and update metrics refresh rate --- config/data.py | 1 + config/settings_gui.py | 2 +- modules/metrics.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/data.py b/config/data.py index 2fe1aaea..206efa1f 100644 --- a/config/data.py +++ b/config/data.py @@ -99,6 +99,7 @@ def load_config(): BAR_METRICS_DISKS = config.get('bar_metrics_disks', ["/"]) METRICS_VISIBLE = config.get('metrics_visible', {'cpu': True, 'ram': True, 'disk': True, 'gpu': True}) METRICS_SMALL_VISIBLE = config.get('metrics_small_visible', {'cpu': True, 'ram': True, 'disk': True, 'gpu': True}) + TEMPERATURE_POLL_INTERVAL = config.get('temperature_poll_interval', 1) else: WALLPAPERS_DIR = WALLPAPERS_DIR_DEFAULT BAR_POSITION = "Top" diff --git a/config/settings_gui.py b/config/settings_gui.py index 06e52129..dd0d220a 100644 --- a/config/settings_gui.py +++ b/config/settings_gui.py @@ -580,7 +580,7 @@ def create_appearance_tab(self): "weather": "Weather Widget", "battery": "Battery Indicator", "metrics": "System Metrics", - 'temperatures': "System Temps" + 'temperatures': "System Temps", "language": "Language Indicator", "date_time": "Date & Time", "button_power": "Power Button", diff --git a/modules/metrics.py b/modules/metrics.py index 8146b755..65874fc6 100644 --- a/modules/metrics.py +++ b/modules/metrics.py @@ -47,7 +47,7 @@ def __init__(self): self._gpu_update_running = False - GLib.timeout_add_seconds(1, self._update) + GLib.timeout_add_seconds(data.TEMPERATURE_POLL_INTERVAL, self._update) def _update(self): self.cpu = psutil.cpu_percent(interval=0) From 2ff2217bd4f31641586d09f20141d429010792bf Mon Sep 17 00:00:00 2001 From: SpectrixDev Date: Mon, 8 Sep 2025 20:33:23 +0100 Subject: [PATCH 3/3] Bold font etc for temp and gpu widget --- styles/metrics.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/styles/metrics.css b/styles/metrics.css index a95c8fd5..dd6de101 100644 --- a/styles/metrics.css +++ b/styles/metrics.css @@ -107,3 +107,15 @@ padding-left: 6px; padding-right: 6px; } + +#cpu-temp-icon, +#gpu-temp-icon { + font-size: 16px; + font-family: 'tabler-icons'; + font-weight: bold; +} + +#cpu-temp-label, +#gpu-temp-label { + font-weight: bold; +}