Fixed Potential memory leak

This commit is contained in:
tr1x_em committed 2025-08-07 23:00:16 +05:30
1 parent d286998d91
commit 643d39b38b
6 files changed
+343 -252

No files matched your search

+5 -5
View File
@@ -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)
+64 -34
View File
@@ -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()
+58 -7
View File
@@ -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."""
+185 -195
View File
@@ -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)
+22 -1
View File
@@ -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)
+9 -10
View File
@@ -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;