From 643d39b38b219bae43c7d145852e195766b958c7 Mon Sep 17 00:00:00 2001 From: tr1x_em Date: Thu, 7 Aug 2025 23:00:16 +0530 Subject: [PATCH] Fixed Potential memory leak --- main.py | 10 +- modules/controlcenter/main.py | 98 +++++--- modules/controlcenter/player.py | 65 +++++- modules/widget.py | 380 ++++++++++++++++---------------- services/mpris.py | 23 +- styles/controlcenter.css | 19 +- 6 files changed, 343 insertions(+), 252 deletions(-) diff --git a/main.py b/main.py index d4ab529..695a958 100644 --- a/main.py +++ b/main.py @@ -36,11 +36,11 @@ for log in [ "fabric.bluetooth.service", "services.network", "utils.wayland", - "modules.notification.notification", - "modules.notification.notification_center", - "modules.controlcenter.player", - "services.brightness", - "modules.controlcenter.expanded_player", + # "modules.notification.notification", + # "modules.notification.notification_center", + # "modules.controlcenter.player", + # "services.brightness", + # "modules.controlcenter.expanded_player", ]: logger.disable(log) diff --git a/modules/controlcenter/main.py b/modules/controlcenter/main.py index 7360b01..81b0405 100644 --- a/modules/controlcenter/main.py +++ b/modules/controlcenter/main.py @@ -6,6 +6,7 @@ from fabric.utils.helpers import ( ) from fabric.widgets.box import Box from fabric.widgets.button import Button +from loguru import logger from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label from fabric.widgets.scale import Scale @@ -50,11 +51,12 @@ class ModusControlCenter(Window): self._per_app_volume_initialized = False self._expanded_player_initialized = False - # Store references for cleanup + # Store references for cleanup - initialize all as None self._signal_connections = [] self._music_widget_content = None self._per_app_volume_widget = None self._expanded_player_widget = None + self._mpris_manager = None # Shared MPRIS manager instance self.add_keybinding("Escape", self.hide_controlcenter) @@ -358,24 +360,59 @@ class ModusControlCenter(Window): self._cleanup_when_hidden() def _cleanup_when_hidden(self): - """Clean up resources when widget is hidden""" - # Clean up music widget content if it exists - if self._music_widget_content and not self._music_initialized: - # Remove from the parent container - current_children = list(self.music_widget.children) - if self._music_widget_content in current_children: - current_children.remove(self._music_widget_content) - self.music_widget.children = current_children - self._music_widget_content = None + """Aggressively clean up resources when widget is hidden to reduce memory usage""" + try: + # Clean up music widget content if it exists + if self._music_widget_content: + # Remove from the parent container + current_children = list(self.music_widget.children) + if self._music_widget_content in current_children: + current_children.remove(self._music_widget_content) + self.music_widget.children = current_children - # Reset lazy loading flags to allow re-initialization when shown again - # Keep initialized flags to avoid recreating heavy components unnecessarily + # Trigger periodic cleanup before destroying + if hasattr(self._music_widget_content, "_periodic_cleanup"): + self._music_widget_content._periodic_cleanup() + + # Properly destroy the music widget content + try: + self._music_widget_content.destroy() + except Exception as e: + logger.warning(f"Failed to destroy music widget content: {e}") + self._music_widget_content = None + + # Clean up shared MPRIS manager when hidden to free memory + if self._mpris_manager: + try: + self._mpris_manager.destroy() + except Exception as e: + logger.warning( + f"Failed to destroy MPRIS manager during cleanup: {e}" + ) + self._mpris_manager = None + + # Reset initialization flags to force recreation next time + self._music_initialized = False + + # Force garbage collection + import gc + + gc.collect() + + logger.debug("Control center aggressive cleanup completed") + + except Exception as e: + logger.warning(f"Control center cleanup failed: {e}") def _ensure_music_widget(self): - """Lazy load music widget content""" + """Lazy load music widget content - reuse MPRIS manager""" if not self._music_initialized: + # Create shared MPRIS manager if it doesn't exist + if self._mpris_manager is None: + self._mpris_manager = MprisPlayerManager() + self._music_widget_content = PlayerBoxStack( - MprisPlayerManager(), control_center=self + self._mpris_manager, control_center=self ) # Add to the music widget's children list current_children = list(self.music_widget.children) @@ -422,22 +459,7 @@ class ModusControlCenter(Window): h_expand=True, name="control-center-widgets", children=[ - Box( - orientation="horizontal", - name="top-widget", - h_expand=True, - children=[ - Box( - orientation="vertical", - name="wb-widget", - style_classes="menu", - spacing=5, - children=[ - self.wifi_man, - ], - ), - ], - ), + self.wifi_man, ], ) self.wifi_center_box = CenterBox(start_children=[self.wifi_widgets]) @@ -710,10 +732,10 @@ class ModusControlCenter(Window): pass # Clean up heavy components - if self._wifi_man: - self._wifi_man.destroy() - if self._bluetooth_man: - self._bluetooth_man.destroy() + if hasattr(self, "wifi_man") and self.wifi_man: + self.wifi_man.destroy() + if hasattr(self, "bluetooth_man") and self.bluetooth_man: + self.bluetooth_man.destroy() if self._music_widget_content: self._music_widget_content.destroy() if self._per_app_volume_widget: @@ -721,4 +743,12 @@ class ModusControlCenter(Window): if self._expanded_player_widget: self._expanded_player_widget.destroy() + # Clean up shared MPRIS manager + if self._mpris_manager: + try: + self._mpris_manager.destroy() + except Exception as e: + logger.warning(f"Failed to destroy MPRIS manager: {e}") + self._mpris_manager = None + super().destroy() diff --git a/modules/controlcenter/player.py b/modules/controlcenter/player.py index f4c797f..f65ac8a 100644 --- a/modules/controlcenter/player.py +++ b/modules/controlcenter/player.py @@ -28,7 +28,7 @@ CACHE_DIR = f"{data.CACHE_DIR}/media" def cleanup_old_cache_files(): - """Clean up old artwork cache files (older than 1 day).""" + """Clean up old artwork cache files (older than 1 day) and limit total cache size.""" try: if not os.path.exists(CACHE_DIR): return @@ -37,16 +37,46 @@ def cleanup_old_cache_files(): current_time = time.time() one_day_ago = current_time - (24 * 60 * 60) # 24 hours + cache_files = [] + # Collect all cache files with their modification times for filename in os.listdir(CACHE_DIR): filepath = os.path.join(CACHE_DIR, filename) try: if os.path.isfile(filepath): file_mtime = os.path.getmtime(filepath) - if file_mtime < one_day_ago: - os.unlink(filepath) + file_size = os.path.getsize(filepath) + cache_files.append((filepath, file_mtime, file_size)) except Exception: pass # Ignore individual file errors + + # Remove files older than 1 day + total_size = 0 + recent_files = [] + for filepath, file_mtime, file_size in cache_files: + if file_mtime < one_day_ago: + try: + os.unlink(filepath) + except Exception: + pass + else: + recent_files.append((filepath, file_mtime, file_size)) + total_size += file_size + + # If cache is still too large (>50MB), remove oldest files + MAX_CACHE_SIZE = 50 * 1024 * 1024 # 50MB + if total_size > MAX_CACHE_SIZE: + # Sort by modification time (oldest first) + recent_files.sort(key=lambda x: x[1]) + for filepath, _, file_size in recent_files: + if total_size <= MAX_CACHE_SIZE: + break + try: + os.unlink(filepath) + total_size -= file_size + except Exception: + pass + except Exception: pass # Ignore all errors in cleanup @@ -133,20 +163,41 @@ class PlayerBoxStack(Box): super().destroy() def _periodic_cleanup(self): - """Light cleanup for reuse - clean internal state but preserve widget""" + """Enhanced cleanup for reuse - clean internal state and free memory""" try: + # Destroy all player boxes properly to free their resources + current_children = list(self.player_stack.get_children()) + for child in current_children: + if hasattr(child, "destroy") and child != self.no_media_box: + try: + child.destroy() + except Exception as e: + logger.warning(f"Failed to destroy player child: {e}") + # Reset to no media state self.player_stack.children = [self.no_media_box] - # Clear player buttons but don't destroy them + # Clear player buttons + for button in self.player_buttons: + try: + button.destroy() + except Exception: + pass self.player_buttons.clear() # Reset stack position self.current_stack_pos = 0 - logger.debug("PlayerBoxStack periodic cleanup completed") + # Clean up old cache files more aggressively + cleanup_old_cache_files() + + # Force garbage collection + import gc + gc.collect() + + logger.debug("PlayerBoxStack enhanced cleanup completed") except Exception as e: - logger.warning(f"PlayerBoxStack periodic cleanup failed: {e}") + logger.warning(f"PlayerBoxStack enhanced cleanup failed: {e}") def _create_no_media_box(self): """Create a placeholder box for when no media is playing.""" diff --git a/modules/widget.py b/modules/widget.py index 74b7726..b98fb61 100644 --- a/modules/widget.py +++ b/modules/widget.py @@ -9,7 +9,7 @@ import calendar from concurrent.futures import ThreadPoolExecutor from typing import Optional, Tuple, List, Dict, Any -# Fabric imports +# Fabric imports from fabric.widgets.box import Box from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay @@ -24,7 +24,7 @@ from config.data import load_config # Module-level constants WEATHER_UPDATE_INTERVAL = 600 # 10 minutes -WEATHER_CACHE_TIMEOUT = 1800 # 30 minutes +WEATHER_CACHE_TIMEOUT = 1800 # 30 minutes SYSTEM_UPDATE_INTERVAL = 1000 # 1 second CALENDAR_UPDATE_INTERVAL = 86400000 # 24 hours LOCATION_CACHE_TIMEOUT = 604800 # 7 days (extended from 24h) @@ -35,54 +35,48 @@ executor = ThreadPoolExecutor(max_workers=4) # Weather condition to CSS class mapping (iOS-style gradients) WEATHER_GRADIENT_MAP = { # Clear/Sunny conditions - bright blue to lighter blue - 0: "weather-clear", # Clear sky - 1: "weather-mostly-clear", # Mainly clear - + 0: "weather-clear", # Clear sky + 1: "weather-mostly-clear", # Mainly clear # Cloudy conditions - grey gradients - 2: "weather-partly-cloudy", # Partly cloudy - 3: "weather-overcast", # Overcast - + 2: "weather-partly-cloudy", # Partly cloudy + 3: "weather-overcast", # Overcast # Fog conditions - muted grey/blue - 45: "weather-fog", # Fog - 48: "weather-fog", # Depositing rime fog - + 45: "weather-fog", # Fog + 48: "weather-fog", # Depositing rime fog # Light rain/drizzle - blue-grey gradients - 51: "weather-light-rain", # Light drizzle - 53: "weather-rain", # Moderate drizzle - 55: "weather-rain", # Dense drizzle - 61: "weather-light-rain", # Slight rain - 80: "weather-light-rain", # Slight rain showers - + 51: "weather-light-rain", # Light drizzle + 53: "weather-rain", # Moderate drizzle + 55: "weather-rain", # Dense drizzle + 61: "weather-light-rain", # Slight rain + 80: "weather-light-rain", # Slight rain showers # Heavy rain - darker blue-grey - 63: "weather-heavy-rain", # Moderate rain - 65: "weather-heavy-rain", # Heavy rain - 81: "weather-heavy-rain", # Moderate rain showers - 82: "weather-storm", # Violent rain showers - + 63: "weather-heavy-rain", # Moderate rain + 65: "weather-heavy-rain", # Heavy rain + 81: "weather-heavy-rain", # Moderate rain showers + 82: "weather-storm", # Violent rain showers # Snow conditions - blue-white gradients - 56: "weather-snow", # Light freezing drizzle - 57: "weather-snow", # Dense freezing drizzle - 66: "weather-snow", # Light freezing rain - 67: "weather-snow", # Heavy freezing rain - 71: "weather-snow", # Slight snow fall - 73: "weather-heavy-snow", # Moderate snow fall - 75: "weather-heavy-snow", # Heavy snow fall - 77: "weather-snow", # Snow grains - 85: "weather-snow", # Slight snow showers - 86: "weather-heavy-snow", # Heavy snow showers - + 56: "weather-snow", # Light freezing drizzle + 57: "weather-snow", # Dense freezing drizzle + 66: "weather-snow", # Light freezing rain + 67: "weather-snow", # Heavy freezing rain + 71: "weather-snow", # Slight snow fall + 73: "weather-heavy-snow", # Moderate snow fall + 75: "weather-heavy-snow", # Heavy snow fall + 77: "weather-snow", # Snow grains + 85: "weather-snow", # Slight snow showers + 86: "weather-heavy-snow", # Heavy snow showers # Storm conditions - dark dramatic gradients - 95: "weather-storm", # Thunderstorm - 96: "weather-storm", # Thunderstorm with slight hail - 99: "weather-storm", # Thunderstorm with heavy hail + 95: "weather-storm", # Thunderstorm + 96: "weather-storm", # Thunderstorm with slight hail + 99: "weather-storm", # Thunderstorm with heavy hail } # Weather condition to emoji mapping WEATHER_EMOJI_MAP = { - 0: "☀️", # Clear sky - 1: "🌤️", # Mainly clear - 2: "⛅", # Partly cloudy - 3: "☁️", # Overcast + 0: "☀️", # Clear sky + 1: "🌤️", # Mainly clear + 2: "⛅", # Partly cloudy + 3: "☁️", # Overcast 45: "🌫️", # Fog 48: "🌫️", # Depositing rime fog 51: "🌦️", # Light drizzle @@ -96,35 +90,35 @@ WEATHER_EMOJI_MAP = { 66: "🌨️", # Light freezing rain 67: "🌨️", # Heavy freezing rain 71: "🌨️", # Slight snow fall - 73: "❄️", # Moderate snow fall - 75: "❄️", # Heavy snow fall + 73: "❄️", # Moderate snow fall + 75: "❄️", # Heavy snow fall 77: "🌨️", # Snow grains 80: "🌦️", # Slight rain showers 81: "🌧️", # Moderate rain showers - 82: "⛈️", # Violent rain showers + 82: "⛈️", # Violent rain showers 85: "🌨️", # Slight snow showers - 86: "❄️", # Heavy snow showers - 95: "⛈️", # Thunderstorm - 96: "⛈️", # Thunderstorm with slight hail - 99: "⛈️", # Thunderstorm with heavy hail + 86: "❄️", # Heavy snow showers + 95: "⛈️", # Thunderstorm + 96: "⛈️", # Thunderstorm with slight hail + 99: "⛈️", # Thunderstorm with heavy hail } # Weather condition descriptions WEATHER_DESC_MAP = { 0: "Clear sky", - 1: "Mainly clear", + 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", - 53: "Moderate drizzle", + 53: "Moderate drizzle", 55: "Dense drizzle", 56: "Light freezing drizzle", 57: "Dense freezing drizzle", 61: "Slight rain", 63: "Moderate rain", - 65: "Heavy rain", + 65: "Heavy rain", 66: "Light freezing rain", 67: "Heavy freezing rain", 71: "Slight snow", @@ -134,7 +128,7 @@ WEATHER_DESC_MAP = { 80: "Light rain showers", 81: "Moderate rain showers", 82: "Violent rain showers", - 85: "Slight snow showers", + 85: "Slight snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm with hail", @@ -143,9 +137,9 @@ WEATHER_DESC_MAP = { # Location APIs in order of preference (fastest first) LOCATION_APIS = [ - "https://ipapi.co/json/", # Fastest, 200ms average - "http://ip-api.com/json/", # Fast fallback, 150ms average - "https://ipinfo.io/json" # Original fallback + "https://ipapi.co/json/", # Fastest, 200ms average + "http://ip-api.com/json/", # Fast fallback, 150ms average + "https://ipinfo.io/json", # Original fallback ] # Global cache for weather data @@ -167,7 +161,7 @@ def get_location() -> str: except requests.RequestException as e: print(f"Location API {api_url} failed: {e}") continue - + print("All location APIs failed") return "" @@ -176,28 +170,30 @@ def get_coordinates(city: str) -> Optional[Tuple[float, float]]: """Get coordinates for a city using Nominatim geocoding API.""" cache_key = city.lower() current_time = time.time() - + # Check cache first (cache for 7 days) if cache_key in _location_cache: lat, lon, timestamp = _location_cache[cache_key] if current_time - timestamp < LOCATION_CACHE_TIMEOUT: return lat, lon - + try: encoded_city = urllib.parse.quote(city) url = f"https://nominatim.openstreetmap.org/search?q={encoded_city}&format=json&limit=1" - response = requests.get(url, timeout=3, headers={'User-Agent': 'Modus-Desktop/1.0'}) - + response = requests.get( + url, timeout=3, headers={"User-Agent": "Modus-Desktop/1.0"} + ) + if response.status_code == 200: data = response.json() if data: - lat = float(data[0]['lat']) - lon = float(data[0]['lon']) + lat = float(data[0]["lat"]) + lon = float(data[0]["lon"]) _location_cache[cache_key] = (lat, lon, current_time) return lat, lon except (requests.RequestException, ValueError, KeyError) as e: print(f"Error geocoding {city}: {e}") - + return None @@ -212,37 +208,37 @@ def get_weather_data(lat: float, lon: float) -> Optional[Dict[str, Any]]: f"&timezone=auto" f"&forecast_days=1" ) - + response = requests.get(url, timeout=3) if response.status_code == 200: return response.json() except requests.RequestException as e: print(f"Error fetching weather data: {e}") - + return None def format_weather_data(weather_data: Dict[str, Any], city: str) -> List[str]: """Format weather data into the expected format.""" try: - current = weather_data['current_weather'] - daily = weather_data['daily'] - + current = weather_data["current_weather"] + daily = weather_data["daily"] + # Get weather code and map to emoji and description - weather_code = current['weathercode'] + weather_code = current["weathercode"] emoji = WEATHER_EMOJI_MAP.get(weather_code, "🌤️") condition = WEATHER_DESC_MAP.get(weather_code, "Unknown") gradient_class = WEATHER_GRADIENT_MAP.get(weather_code, "weather-clear") - + # Temperature temp = f"{round(current['temperature'])}°" - + # Daily min/max temperatures max_temp = f"{round(daily['temperature_2m_max'][0])}°" min_temp = f"{round(daily['temperature_2m_min'][0])}°" - + return [emoji, temp, condition, city, max_temp, min_temp, gradient_class] - + except (KeyError, IndexError, TypeError) as e: print(f"Error formatting weather data: {e}") return None @@ -250,34 +246,34 @@ def format_weather_data(weather_data: Dict[str, Any], city: str) -> List[str]: def get_weather(callback): """Fetch weather data asynchronously using Open-Meteo API.""" - + def fetch_weather(): # Get location location = get_location() if not location: return GLib.idle_add(callback, None) - + # Check cache first cache_key = location.lower() current_time = time.time() - + if cache_key in _weather_cache: cached_data, timestamp = _weather_cache[cache_key] if current_time - timestamp < WEATHER_CACHE_TIMEOUT: return GLib.idle_add(callback, cached_data) - + # Get coordinates for the location coords = get_coordinates(location) if not coords: return GLib.idle_add(callback, None) - + lat, lon = coords - + # Fetch weather data weather_data = get_weather_data(lat, lon) if not weather_data: return GLib.idle_add(callback, None) - + # Format data formatted_data = format_weather_data(weather_data, location) if formatted_data: @@ -286,16 +282,17 @@ def get_weather(callback): GLib.idle_add(callback, formatted_data) else: GLib.idle_add(callback, None) - + executor.submit(fetch_weather) def update_weather(widget): """Update weather widget with new data.""" + def fetch_and_update(): get_weather(lambda weather_info: update_widget(widget, weather_info)) return True - + GLib.timeout_add_seconds(WEATHER_UPDATE_INTERVAL, fetch_and_update) fetch_and_update() @@ -309,7 +306,7 @@ def update_widget(widget, weather_info): class Weather(Box): """Weather widget displaying current conditions and forecast.""" - + def __init__(self, parent, **kwargs): super().__init__( name="weather-widget", @@ -323,46 +320,33 @@ class Weather(Box): self.parent = parent self.weatherinfo = None - + # Create labels with better organization self._create_labels() self._layout_labels() - + # Start weather updates update_weather(self) def _create_labels(self): """Create all weather labels.""" self.city = Label( - name="city", - label="Loading...", - justification="right", - h_align="start" - ) - self.temperature = Label( - name="temperature", - label="--°", - h_align="start" - ) - self.condition_em = Label( - name="condition-emoji", - label="🌤️", - h_align="start" - ) - self.condition = Label( - name="condition", - label="Loading...", - h_align="start" - ) - self.feels_like = Label( - name="feels-like", - label="H:-- L:--", - h_align="start" + name="city", label="Loading...", justification="right", h_align="start" ) + self.temperature = Label(name="temperature", label="--°", h_align="start") + self.condition_em = Label(name="condition-emoji", label="🌤️", h_align="start") + self.condition = Label(name="condition", label="Loading...", h_align="start") + self.feels_like = Label(name="feels-like", label="H:-- L:--", h_align="start") def _layout_labels(self): """Add labels to the widget in proper order.""" - labels = [self.city, self.temperature, self.condition_em, self.condition, self.feels_like] + labels = [ + self.city, + self.temperature, + self.condition_em, + self.condition, + self.feels_like, + ] for label in labels: self.add(label) @@ -371,7 +355,9 @@ class Weather(Box): if not weather_info or len(weather_info) != 7: return - emoji, temp, condition, location, maxtemp, mintemp, gradient_class = weather_info + emoji, temp, condition, location, maxtemp, mintemp, gradient_class = ( + weather_info + ) maxmin = f"H:{maxtemp} L:{mintemp}" # Batch update labels for better performance @@ -380,19 +366,19 @@ class Weather(Box): (self.temperature, temp), (self.condition_em, emoji), (self.condition, condition), - (self.feels_like, maxmin) + (self.feels_like, maxmin), ] - + for label, text in label_updates: label.set_label(text) - + # Apply gradient background based on weather condition self.parent.set_visible(True) class WeatherContainer(Box): """Container for weather widget.""" - + def __init__(self, **kwargs): super().__init__( orientation="v", @@ -409,7 +395,7 @@ class WeatherContainer(Box): class Date(Box): """Date widget displaying day, month, and date.""" - + def __init__(self, **kwargs): super().__init__( name="date-widget", @@ -424,13 +410,15 @@ class Date(Box): # Create date components self.top = Box(orientation="h", name="date-top", h_expand=True) - + # Use consistent interval for all date components date_interval = 10000 # 10 seconds self.dateone = DateTime(formatters=["%a"], interval=date_interval, name="day") - self.datetwo = DateTime(formatters=["%b"], interval=date_interval, name="month") - self.datethree = DateTime(formatters=["%-d"], interval=date_interval, name="date") - + self.datetwo = DateTime(formatters=["%b"], interval=date_interval, name="month") + self.datethree = DateTime( + formatters=["%-d"], interval=date_interval, name="date" + ) + # Layout components self.top.add(self.dateone) self.top.add(self.datetwo) @@ -440,7 +428,7 @@ class Date(Box): class DateContainer(Box): """Container for date widget.""" - + def __init__(self, **kwargs): super().__init__( orientation="v", @@ -456,7 +444,7 @@ class DateContainer(Box): class Calendar(Box): """Calendar widget displaying current month.""" - + def __init__(self, **kwargs): super().__init__( name="calendar-widget", @@ -468,12 +456,12 @@ class Calendar(Box): # Cache current date for efficiency self._update_current_date() - + # Create calendar components self._create_header() self._create_days_header() self._create_calendar_grid() - + # Layout components self.add(self.month_label) self.add(self.days_header) @@ -501,17 +489,18 @@ class Calendar(Box): def _create_days_header(self): """Create day abbreviations header.""" self.days_header = Box( - name="calendar-days-header", - orientation="h", - h_expand=True, - spacing=2 + name="calendar-days-header", orientation="h", h_expand=True, spacing=2 ) day_names = ["S", "M", "T", "W", "T", "F", "S"] for i, day_name in enumerate(day_names): is_weekend = i in (0, 6) # Sunday or Saturday day_label = Label( - name="calendar-day-header-weekend" if is_weekend else "calendar-day-header", + name=( + "calendar-day-header-weekend" + if is_weekend + else "calendar-day-header" + ), label=day_name, h_align="center", h_expand=True, @@ -526,10 +515,12 @@ class Calendar(Box): def update_calendar_if_needed(self) -> bool: """Check if date changed and update calendar if needed.""" now = datetime.datetime.now() - if (now.month != self.current_month or - now.year != self.current_year or - now.day != self.current_day): - + if ( + now.month != self.current_month + or now.year != self.current_year + or now.day != self.current_day + ): + self._update_current_date() self.update_calendar() return True @@ -562,9 +553,11 @@ class Calendar(Box): ) else: # Regular day - is_today = (day == self.current_day and - self.current_month == current_date.month and - self.current_year == current_date.year) + is_today = ( + day == self.current_day + and self.current_month == current_date.month + and self.current_year == current_date.year + ) is_weekend = day_index in (0, 6) # Sunday or Saturday if is_today: @@ -575,10 +568,7 @@ class Calendar(Box): name = "calendar-day" day_label = Label( - name=name, - label=str(day), - h_align="center", - h_expand=True + name=name, label=str(day), h_align="center", h_expand=True ) week_box.add(day_label) @@ -587,7 +577,7 @@ class Calendar(Box): class CalendarContainer(Box): """Container for calendar widget.""" - + def __init__(self, **kwargs): super().__init__( orientation="v", @@ -603,7 +593,7 @@ class CalendarContainer(Box): class SystemInfoBase(Box): """Base class for system information widgets.""" - + @staticmethod def create_progress_bar(name: str = "progress-bar", size: int = 80, **kwargs): """Create a standardized circular progress bar.""" @@ -620,7 +610,7 @@ class SystemInfoBase(Box): def __init__(self, name: str, **kwargs): super().__init__( layer="bottom", - title="sysinfo", + title="sysinfo", name=name, visible=True, size=(170, 170), @@ -633,11 +623,9 @@ class SystemInfoBase(Box): # Create progress bar and labels self.progress = self.create_progress_bar(name="progress") self.main_label = Label( - label="0%\nLoading", - justification="center", - name="progress-label" + label="0%\nLoading", justification="center", name="progress-label" ) - + # Create info container self.info_container = Box( name="info-container", @@ -645,7 +633,7 @@ class SystemInfoBase(Box): spacing=2, h_align="center", ) - + # Create main layout self.progress_container = Box( name="progress-bar-container", @@ -656,13 +644,15 @@ class SystemInfoBase(Box): h_align="center", v_align="center", children=[ - Box(children=[ - Overlay( - child=self.progress, - tooltip_text="", - overlays=self.main_label, - ) - ]), + Box( + children=[ + Overlay( + child=self.progress, + tooltip_text="", + overlays=self.main_label, + ) + ] + ), Box( h_align="center", justification="centre", @@ -671,33 +661,35 @@ class SystemInfoBase(Box): ), ], ) - + self.add(self.progress_container) - + # Don't start updates here - let subclasses call start_updates() when ready def start_updates(self): """Start the update timer - call this after subclass initialization is complete.""" invoke_repeater(SYSTEM_UPDATE_INTERVAL, self.update) - def create_info_line(self, indicator_name: str, info_text: str, value_text: str) -> Box: + def create_info_line( + self, indicator_name: str, info_text: str, value_text: str + ) -> Box: """Create an information line with indicator, label, and value.""" indicator = Label(label="■", name=indicator_name) info_label = Label(label=info_text, name="info-text") value_label = Label(label=value_text, name="info-value") - + line = Box( orientation="h", spacing=4, h_align="start", children=[indicator, info_label, value_label], ) - + # Store references for easy updates line.indicator = indicator - line.info_label = info_label + line.info_label = info_label line.value_label = value_label - + return line def update(self) -> bool: @@ -707,22 +699,18 @@ class SystemInfoBase(Box): class RamInfo(SystemInfoBase): """RAM usage information widget.""" - + def __init__(self, **kwargs): super().__init__("info-box-widget", **kwargs) - + # Create info lines and store references - self.used_line = self.create_info_line( - "used-color-indicator", "Used", "0.0GB" - ) - self.free_line = self.create_info_line( - "free-color-indicator", "Free", "0.0GB" - ) - + self.used_line = self.create_info_line("used-color-indicator", "Used", "0.0GB") + self.free_line = self.create_info_line("free-color-indicator", "Free", "0.0GB") + # Add to info container self.info_container.add(self.used_line) self.info_container.add(self.free_line) - + # Now that everything is set up, start updates self.start_updates() @@ -730,37 +718,37 @@ class RamInfo(SystemInfoBase): """Update RAM information.""" try: mem = psutil.virtual_memory() - + # Update main label self.main_label.set_label(f" {round(mem.percent):<2} %\nRAM") - + # Calculate values used_gb = mem.used / (1024**3) free_gb = mem.available / (1024**3) - + # Update info labels using stored references self.used_line.value_label.set_label(f"{round(used_gb, 1)}GB") self.free_line.value_label.set_label(f"{round(free_gb, 1)}GB") - + # Update progress bar (use GLib.idle_add for thread safety) GLib.idle_add(self.progress.set_value, mem.percent) - + except Exception as e: print(f"Error updating RAM info: {e}") - + return True class CpuInfo(SystemInfoBase): """CPU usage and temperature information widget.""" - + def __init__(self, **kwargs): super().__init__("info-box-widget", **kwargs) - + # Create temperature info components self.temp_info = Label(label="Temp", name="info-text") self.temp_value = Label(label="0°C", name="info-value") - + # Create temperature info line (no indicator) self.temp_line = Box( orientation="h", @@ -768,10 +756,10 @@ class CpuInfo(SystemInfoBase): h_align="start", children=[self.temp_info, self.temp_value], ) - + # Add to info container self.info_container.add(self.temp_line) - + # Now that everything is set up, start updates self.start_updates() @@ -785,16 +773,18 @@ class CpuInfo(SystemInfoBase): # Search for CPU temperature sensors cpu_sensor_names = ["coretemp", "k10temp", "cpu"] cpu_label_patterns = ["package id 0", "core 0", ""] - + for name, entries in temps.items(): if any(sensor in name.lower() for sensor in cpu_sensor_names): for entry in entries: entry_label = (entry.label or "").lower() - if any(pattern in entry_label for pattern in cpu_label_patterns): + if any( + pattern in entry_label for pattern in cpu_label_patterns + ): return round(entry.current, 1) except Exception as e: print(f"Error reading CPU temperature: {e}") - + return None def update(self) -> bool: @@ -802,27 +792,27 @@ class CpuInfo(SystemInfoBase): try: # Get CPU usage cpu = psutil.cpu_percent() - + # Update main label self.main_label.set_label(f" {round(cpu):<2} %\nCPU") - + # Update temperature using stored reference temp = self.get_cpu_temp() temp_text = f"{temp}°C" if temp is not None else "N/A" self.temp_value.set_label(temp_text) - + # Update progress bar (use GLib.idle_add for thread safety) GLib.idle_add(self.progress.set_value, cpu) - + except Exception as e: print(f"Error updating CPU info: {e}") - + return True class Deskwidgets(Window): """Desktop widgets manager - handles all desktop widgets.""" - + config = load_config() def __init__(self, **kwargs): @@ -836,11 +826,11 @@ class Deskwidgets(Window): anchor="top left", **kwargs, ) - + # Create separate independent windows as attributes self.top_left = Window( anchor="top left", - title="modus-widgets-topleft", + title="modus-widgets-topleft", orientation="h", layer="bottom", visible=False, # Start hidden until content ready @@ -857,7 +847,7 @@ class Deskwidgets(Window): self.bottom_left = Window( anchor="bottom right", title="modus-widgets-bottomright", - orientation="h", + orientation="h", layer="bottom", visible=False, # Start hidden until content ready child=Box( @@ -868,7 +858,7 @@ class Deskwidgets(Window): ], ), ) - + # Show widgets after initialization is complete self.top_left.set_visible(True) self.bottom_left.set_visible(True) diff --git a/services/mpris.py b/services/mpris.py index b52a807..539a727 100644 --- a/services/mpris.py +++ b/services/mpris.py @@ -252,16 +252,37 @@ class MprisPlayerManager(Service): **kwargs, ): self._manager = Playerctl.PlayerManager.new() - bulk_connect( + self._signal_connections = [] + + # Track signal connections for cleanup + connections = bulk_connect( self._manager, { "name-appeared": self.on_name_appeared, "name-vanished": self.on_name_vanished, }, ) + # Store as (object, handler_id) tuples + for handler_id in connections: + self._signal_connections.append((self._manager, handler_id)) + self.add_players() super().__init__(**kwargs) + def destroy(self): + """Clean up resources when the manager is destroyed.""" + # Disconnect all signal connections + for obj, handler_id in self._signal_connections: + try: + obj.disconnect(handler_id) + except Exception as e: + logger.warning(f"Failed to disconnect manager signal: {e}") + self._signal_connections.clear() + + # Clean up the manager + if hasattr(self, '_manager'): + del self._manager + def on_name_appeared(self, manager, player_name: Playerctl.PlayerName): logger.info(f"[MprisPlayer] {player_name.name} appeared") new_player = Playerctl.Player.new_from_name(player_name) diff --git a/styles/controlcenter.css b/styles/controlcenter.css index 2f49768..49a0cbe 100644 --- a/styles/controlcenter.css +++ b/styles/controlcenter.css @@ -216,14 +216,14 @@ #toggle-button { min-width: 40px; min-height: 20px; - background-color: #888; + background-color: #bababa; border-radius: 15px; padding: 2px; transition: background-color 0.3s ease; } #toggle-button slider { - background-color: var(--primary); + background-color: #fff; border-radius: 50%; min-width: 16px; min-height: 8px; @@ -234,14 +234,13 @@ background-color: #4487f6; } -#toggle-button:checked slider { - background-color: #bababa; -} - -#toggle-button:checked image { - color: var(--shadow); - opacity: 0; -} +/* #toggle-button:checked slider { */ +/* background-color: #fff; */ +/* } */ +/**/ +/* #toggle-button:checked image { */ +/* opacity: 0; */ +/* } */ .menu { margin: 5px;