diff --git a/config/data.py b/config/data.py index 8f770b4..971a7d7 100644 --- a/config/data.py +++ b/config/data.py @@ -83,29 +83,28 @@ def load_config(): NOTIF_POS = config.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) BAR_COMPONENTS_VISIBILITY = { - "button_apps": config.get("bar_button_apps_visible", True), - "systray": config.get("bar_systray_visible", True), - "control": config.get("bar_control_visible", True), - "network": config.get("bar_network_visible", True), - "button_tools": config.get("bar_button_tools_visible", True), - "sysprofiles": config.get("bar_sysprofiles_visible", True), - "button_overview": config.get("bar_button_overview_visible", True), - "ws_container": config.get("bar_ws_container_visible", True), - "weather": config.get("bar_weather_visible", True), - "battery": config.get("bar_battery_visible", True), - "metrics": config.get("bar_metrics_visible", True), - "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), + 'button_apps': config.get('bar_button_apps_visible', True), + 'systray': config.get('bar_systray_visible', True), + 'control': config.get('bar_control_visible', True), + 'network': config.get('bar_network_visible', True), + 'button_tools': config.get('bar_button_tools_visible', True), + 'button_overview': config.get('bar_button_overview_visible', True), + 'ws_container': config.get('bar_ws_container_visible', True), + '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), + 'sysprofiles': config.get("bar_sysprofiles_visible", True), + '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), } + + 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) - 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} - ) else: WALLPAPERS_DIR = WALLPAPERS_DIR_DEFAULT BAR_POSITION = "Left" @@ -126,23 +125,25 @@ def load_config(): PANEL_POSITION = PANEL_POSITION_DEFAULT NOTIF_POS = NOTIF_POS_DEFAULT + # Merged default BAR_COMPONENTS_VISIBILITY dictionary BAR_COMPONENTS_VISIBILITY = { - "button_apps": True, - "systray": True, - "control": True, - "network": True, - "button_tools": True, - "button_overview": True, - "ws_container": True, - "weather": True, - "battery": True, - "metrics": True, - "language": True, - "date_time": True, - "button_power": True, - "sysprofiles": True, + 'button_apps': True, + 'systray': True, + 'control': True, + 'network': True, + 'button_tools': True, + 'button_overview': True, + 'ws_container': True, + 'weather': True, + 'battery': True, + 'metrics': True, + 'temperatures': False, + 'sysprofiles': True, + 'language': True, + 'date_time': True, + 'button_power': True, } BAR_METRICS_DISKS = ["/"] METRICS_VISIBLE = {"cpu": True, "ram": True, "disk": True, "gpu": True} - METRICS_SMALL_VISIBLE = {"cpu": True, "ram": True, "disk": True, "gpu": True} + METRICS_SMALL_VISIBLE = {"cpu": True, "ram": True, "disk": True, "gpu": True} \ No newline at end of file diff --git a/config/settings_gui.py b/config/settings_gui.py index 5b37962..74a1d65 100644 --- a/config/settings_gui.py +++ b/config/settings_gui.py @@ -32,6 +32,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__( @@ -580,6 +581,7 @@ def create_appearance_tab(self): "weather": "Weather Widget", "battery": "Battery Indicator", "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 ff7e9e5..d3094f3 100644 --- a/modules/bar.py +++ b/modules/bar.py @@ -18,7 +18,8 @@ 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.systemprofiles import Systemprofiles from modules.systemtray import SystemTray from modules.weather import Weather @@ -173,9 +174,9 @@ def __init__(self, **kwargs): self.button_tools.connect("leave_notify_event", self.on_button_leave) self.systray = SystemTray() - self.weather = Weather() self.sysprofiles = Systemprofiles() + self.temperatures_bar = TemperaturesBar() self.network = NetworkApplet() @@ -240,8 +241,8 @@ def __init__(self, **kwargs): self.battery = Battery() self.apply_component_props() - self.rev_right = [ + self.temperatures_bar, self.metrics, self.control, ] @@ -486,21 +487,23 @@ def __init__(self, **kwargs): self.chinese_numbers() def apply_component_props(self): + # Merged components dictionary components = { - "button_apps": self.button_apps, - "systray": self.systray, - "control": self.control, - "network": self.network, - "button_tools": self.button_tools, - "button_overview": self.button_overview, - "ws_container": self.ws_container, - "weather": self.weather, - "battery": self.battery, - "metrics": self.metrics, - "language": self.language, - "date_time": self.date_time, - "button_power": self.button_power, - "sysprofiles": self.sysprofiles, + 'button_apps': self.button_apps, + 'systray': self.systray, + 'control': self.control, + 'network': self.network, + 'button_tools': self.button_tools, + 'button_overview': self.button_overview, + 'ws_container': self.ws_container, + '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, + 'sysprofiles': self.sysprofiles, } for component_name, widget in components.items(): @@ -508,21 +511,23 @@ def apply_component_props(self): widget.set_visible(self.component_visibility[component_name]) def toggle_component_visibility(self, component_name): + # Merged components dictionary components = { - "button_apps": self.button_apps, - "systray": self.systray, - "control": self.control, - "network": self.network, - "button_tools": self.button_tools, - "button_overview": self.button_overview, - "ws_container": self.ws_container, - "weather": self.weather, - "battery": self.battery, - "metrics": self.metrics, - "language": self.language, - "date_time": self.date_time, - "button_power": self.button_power, - "sysprofiles": self.sysprofiles, + 'button_apps': self.button_apps, + 'systray': self.systray, + 'control': self.control, + 'network': self.network, + 'button_tools': self.button_tools, + 'button_overview': self.button_overview, + 'ws_container': self.ws_container, + '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, + 'sysprofiles': self.sysprofiles, } if component_name in components and component_name in self.component_visibility: @@ -607,4 +612,4 @@ def chinese_numbers(self): if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS: self.workspaces_num.add_style_class("chinese") else: - self.workspaces_num.remove_style_class("chinese") + self.workspaces_num.remove_style_class("chinese") \ No newline at end of file diff --git a/modules/metrics.py b/modules/metrics.py index 3a39952..8aaa7ec 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() @@ -42,13 +47,19 @@ 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) 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 @@ -126,7 +211,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) @@ -153,8 +245,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, @@ -164,15 +321,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, @@ -224,20 +384,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: @@ -245,7 +403,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, @@ -273,6 +432,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()}" @@ -364,24 +524,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) @@ -480,8 +638,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 ddc0fd5..a95c8fd 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; +}