Fixed Potential memory leak
This commit is contained in:
1 parent
d286998d91
commit
643d39b38b
6 files changed
+343
-252
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user