From ef4065bc1fa85365be776e800feef78055ec74fd Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 23 Mar 2026 23:34:03 +0545 Subject: [PATCH] refactor(modules): improve exception handling and simplify service usage Enhancements - Replace bare `except:` with `except Exception:` in multiple modules- Cast step calculation to `int` for consistent type - Simplify `get_battery_icon_file` to return a string directly - Use `get_todo_service()` instead of module-level `todo_service` throughout todo widget - Remove unused `"zen": "zen-browser"` mapping entry Bug Fixes - Prevent bare exception swallowing by catching `Exception` explicitly - Ensure proper cleanup of timer IDs and other resources - Delete unused modules/osdd components New Features - None --- modules/controlcenter/battery.py | 2 +- modules/controlcenter/bluetooth.py | 6 +- modules/controlcenter/expanded_player.py | 4 +- modules/controlcenter/main.py | 4 +- modules/controlcenter/per_app_volume.py | 3 +- modules/controlcenter/player.py | 1 - modules/controlcenter/wifi.py | 2 +- modules/desktop/widget.py | 2 +- modules/launcher/main.py | 4 +- modules/launcher/plugins/bookmarks.py | 6 +- modules/launcher/plugins/password.py | 6 +- modules/launcher/plugins/wallpaper.py | 4 +- modules/notification/notification.py | 24 +- modules/notification/notification_center.py | 6 +- modules/notification/unified_cache.py | 4 +- modules/osd.py | 468 -------------------- modules/osd/__init__.py | 8 + modules/osd/components/__init__.py | 5 + modules/osd/components/animated_scale.py | 28 ++ modules/osd/components/audio.py | 105 +++++ modules/osd/components/base.py | 145 ++++++ modules/osd/components/brightness.py | 64 +++ modules/osd/components/capslock.py | 50 +++ modules/osd/components/keyboard_layout.py | 40 ++ modules/osd/components/microphone.py | 105 +++++ modules/osd/main.py | 83 ++++ modules/osdd/components/__init__.py | 0 modules/osdd/components/audio.py | 0 modules/osdd/components/brightness.py | 0 modules/osdd/components/capslock.py | 0 modules/osdd/components/keyboard_layout.py | 0 modules/osdd/components/microphone.py | 0 modules/osdd/main.py | 0 modules/todo/todo_widget.py | 22 +- 34 files changed, 682 insertions(+), 519 deletions(-) delete mode 100644 modules/osd.py create mode 100644 modules/osd/__init__.py create mode 100644 modules/osd/components/__init__.py create mode 100644 modules/osd/components/animated_scale.py create mode 100644 modules/osd/components/audio.py create mode 100644 modules/osd/components/base.py create mode 100644 modules/osd/components/brightness.py create mode 100644 modules/osd/components/capslock.py create mode 100644 modules/osd/components/keyboard_layout.py create mode 100644 modules/osd/components/microphone.py create mode 100644 modules/osd/main.py delete mode 100644 modules/osdd/components/__init__.py delete mode 100644 modules/osdd/components/audio.py delete mode 100644 modules/osdd/components/brightness.py delete mode 100644 modules/osdd/components/capslock.py delete mode 100644 modules/osdd/components/keyboard_layout.py delete mode 100644 modules/osdd/components/microphone.py delete mode 100644 modules/osdd/main.py diff --git a/modules/controlcenter/battery.py b/modules/controlcenter/battery.py index 33bfc8b..bc49f74 100644 --- a/modules/controlcenter/battery.py +++ b/modules/controlcenter/battery.py @@ -518,7 +518,7 @@ class BluetoothConnections(Box): # Make sure other devices revealer is collapsed when closing try: self.other_devices_revealer.child_revealed = False - except: + except Exception: pass # Widget might already be destroyed def close_bluetooth(self): diff --git a/modules/controlcenter/bluetooth.py b/modules/controlcenter/bluetooth.py index 33bfc8b..0b2584e 100644 --- a/modules/controlcenter/bluetooth.py +++ b/modules/controlcenter/bluetooth.py @@ -29,9 +29,9 @@ def get_battery_icon_file( Relative path to the battery icon file """ clamped = max(0, min(100, percentage)) - step = (clamped // 10) * 10 + step = int((clamped // 10) * 10) filename = f"battery-{step:03d}{'-charging' if is_charging else ''}.svg" - return svg_file(f"battery/{filename}") + return f"battery/{filename}" def set_bluetooth_enabled_with_fallback(client, enabled: bool): @@ -518,7 +518,7 @@ class BluetoothConnections(Box): # Make sure other devices revealer is collapsed when closing try: self.other_devices_revealer.child_revealed = False - except: + except Exception: pass # Widget might already be destroyed def close_bluetooth(self): diff --git a/modules/controlcenter/expanded_player.py b/modules/controlcenter/expanded_player.py index b7a2514..443149f 100644 --- a/modules/controlcenter/expanded_player.py +++ b/modules/controlcenter/expanded_player.py @@ -911,7 +911,7 @@ class PlayerBox(Box): from gi.repository import GLib GLib.source_remove(self._seekbar_timer_id) - except: + except Exception: pass self._seekbar_timer_id = None @@ -1041,7 +1041,7 @@ class PlayerBox(Box): from gi.repository import GLib GLib.source_remove(self._seekbar_timer_id) - except: + except Exception: pass self._seekbar_timer_id = None diff --git a/modules/controlcenter/main.py b/modules/controlcenter/main.py index a212b0d..27f4cd4 100644 --- a/modules/controlcenter/main.py +++ b/modules/controlcenter/main.py @@ -683,7 +683,7 @@ class ModusControlCenter(Window): if self._caffeine_process: try: self._caffeine_process.terminate() - except: + except Exception: pass self._caffeine_process = None else: @@ -1193,5 +1193,5 @@ class ModusControlCenter(Window): logger.error(f"Failed to destroy control center: {e}") try: super().destroy() - except: + except Exception: pass diff --git a/modules/controlcenter/per_app_volume.py b/modules/controlcenter/per_app_volume.py index e9fa338..bf7223b 100644 --- a/modules/controlcenter/per_app_volume.py +++ b/modules/controlcenter/per_app_volume.py @@ -114,7 +114,6 @@ class PerAppVolumeControl(Box): "visual studio code": "vscode", "telegram": "telegram-desktop", "pulse": "audio-card", - "zen": "zen-browser", "pipewire": "audio-card", "alsa": "audio-card", "sink": "audio-speakers", @@ -323,7 +322,7 @@ class PerAppVolumeControl(Box): for connection in self._signal_connections: try: audio_service.disconnect(connection) - except: + except Exception: pass self._signal_connections.clear() diff --git a/modules/controlcenter/player.py b/modules/controlcenter/player.py index bf43827..48d553e 100644 --- a/modules/controlcenter/player.py +++ b/modules/controlcenter/player.py @@ -556,7 +556,6 @@ class PlayerBoxStack(Box): self._update_all_player_buttons() def on_new_player(self, mpris_manager, player): - player_name = player.props.player_name # if player_name in self.config.get("ignore", []): # return diff --git a/modules/controlcenter/wifi.py b/modules/controlcenter/wifi.py index f48d68c..6164437 100644 --- a/modules/controlcenter/wifi.py +++ b/modules/controlcenter/wifi.py @@ -699,7 +699,7 @@ class WifiConnections(Box): # Make sure other networks revealer is collapsed when closing try: self.other_networks_revealer.child_revealed = False - except: + except Exception: pass # Widget might already be destroyed def close_wifi(self): diff --git a/modules/desktop/widget.py b/modules/desktop/widget.py index 5f0ff85..9611038 100644 --- a/modules/desktop/widget.py +++ b/modules/desktop/widget.py @@ -546,7 +546,7 @@ class CpuInfo(SystemInfoBase): for p in ["package id 0", "core 0", ""] ): return round(entry.current, 1) - except: + except Exception: pass return None diff --git a/modules/launcher/main.py b/modules/launcher/main.py index 0ba8f7f..c07104b 100644 --- a/modules/launcher/main.py +++ b/modules/launcher/main.py @@ -1040,7 +1040,7 @@ class Launcher(Window): return widget # Also check if this is the only Entry in the widget (likely to be the target) return widget - except: + except Exception: # If focus checking fails, assume this Entry should handle the event return widget @@ -1207,7 +1207,7 @@ class Launcher(Window): allocation = selected_child.get_allocation() item_height = allocation.height if allocation.height > 0 else 68 item_y = allocation.y - except: + except Exception: # Fallback to estimation item_height = DEFAULT_ITEM_HEIGHT item_y = self.selected_index * item_height diff --git a/modules/launcher/plugins/bookmarks.py b/modules/launcher/plugins/bookmarks.py index ee55139..b910d38 100644 --- a/modules/launcher/plugins/bookmarks.py +++ b/modules/launcher/plugins/bookmarks.py @@ -29,7 +29,7 @@ class BookmarkManager: try: parsed = urlparse(url) return f"{parsed.scheme}://{parsed.netloc}/favicon.ico" - except: + except Exception: return "" def _extract_domain(self, url: str) -> str: @@ -41,7 +41,7 @@ class BookmarkManager: if domain.startswith("www."): domain = domain[4:] return domain - except: + except Exception: return url def _normalize_url(self, url: str) -> str: @@ -683,7 +683,7 @@ class BookmarksPlugin(PluginBase): if domain.startswith("www."): domain = domain[4:] return domain - except: + except Exception: return url def _open_bookmark(self, url: str): diff --git a/modules/launcher/plugins/password.py b/modules/launcher/plugins/password.py index 1551b20..90d1aa6 100644 --- a/modules/launcher/plugins/password.py +++ b/modules/launcher/plugins/password.py @@ -497,7 +497,7 @@ class PasswordPlugin(PluginBase): f"Password '{name}' {action_word} successfully", ] ) - except: + except Exception: pass else: print(f"Failed to add password '{name}'") @@ -524,7 +524,7 @@ class PasswordPlugin(PluginBase): f"Password '{name}' removed successfully", ] ) - except: + except Exception: pass else: print(f"Failed to remove password '{name}'") @@ -564,7 +564,7 @@ class PasswordPlugin(PluginBase): f"Password for '{name}' copied to clipboard", ] ) - except: + except Exception: pass # Clear cache to force refresh diff --git a/modules/launcher/plugins/wallpaper.py b/modules/launcher/plugins/wallpaper.py index c7f1f7c..681711e 100644 --- a/modules/launcher/plugins/wallpaper.py +++ b/modules/launcher/plugins/wallpaper.py @@ -314,11 +314,11 @@ class WallpaperPlugin(PluginBase): ) return filename - def _hsl_to_rgb_hex(self, h: float, s: float = 1.0, l: float = 0.5) -> str: + def _hsl_to_rgb_hex(self, h: float, s: float = 1.0, l_val: float = 0.5) -> str: """Convert HSL color value to RGB HEX string.""" # colorsys uses HLS, not HSL, and expects values between 0.0 and 1.0 hue = h / 360.0 - r, g, b = colorsys.hls_to_rgb(hue, l, s) # Note the order: H, L, S + r, g, b = colorsys.hls_to_rgb(hue, l_val, s) # Note the order: H, L, S r_int, g_int, b_int = int(r * 255), int(g * 255), int(b * 255) return f"#{r_int:02X}{g_int:02X}{b_int:02X}" diff --git a/modules/notification/notification.py b/modules/notification/notification.py index 2e4e25e..838264d 100644 --- a/modules/notification/notification.py +++ b/modules/notification/notification.py @@ -299,14 +299,14 @@ def verify_cache_persistence(): for cache_file in icon_cache_files[:2]: # Test first 2 icon files try: cache_path = os.path.join(NOTIFICATION_ICON_CACHE_DIR, cache_file) - test_pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path) + GdkPixbuf.Pixbuf.new_from_file(cache_path) except Exception as e: logger.warning(f"Failed to load cached icon {cache_file}: {e}") for cache_file in image_cache_files[:2]: # Test first 2 image files try: cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, cache_file) - test_pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path) + GdkPixbuf.Pixbuf.new_from_file(cache_path) except Exception as e: logger.warning(f"Failed to load cached image {cache_file}: {e}") @@ -876,7 +876,7 @@ class NotificationRevealer(SlideRevealer): self.notif_box._should_cleanup_cache = True try: self.notification.close("dismissed-by-user") - except: + except Exception: pass return False @@ -1021,7 +1021,7 @@ class ModusNoti(Window): preload_notification_assets(notification) # Implement smart queue management for smooth transitions - current_time = GLib.get_monotonic_time() / 1000 + GLib.get_monotonic_time() / 1000 # If queue is getting full, remove oldest notifications smoothly if len(self.notification_queue) >= self.MAX_QUEUE_SIZE: @@ -1030,7 +1030,7 @@ class ModusNoti(Window): oldest = self.notification_queue.pop(0) try: oldest.close("dismissed-by-user") - except: + except Exception: pass # Add new notification to queue @@ -1081,7 +1081,7 @@ class ModusNoti(Window): # Force close current notification with smooth animation try: self.current_notification.notification.close("expired") - except: + except Exception: pass def _show_next_notification(self): @@ -1114,7 +1114,7 @@ class ModusNoti(Window): for child in list(self.notifications.children): try: self.notifications.remove(child) - except: + except Exception: pass self.notifications.children = [new_box] @@ -1146,7 +1146,7 @@ class ModusNoti(Window): try: if notification_box in self.notifications.children: self.notifications.remove(notification_box) - except: + except Exception: pass # Reset state @@ -1179,7 +1179,7 @@ class ModusNoti(Window): for notification in list(self.notification_queue): try: notification.close("dismissed-by-user") - except: + except Exception: pass self.notification_queue.clear() @@ -1223,15 +1223,15 @@ class ModusNoti(Window): timeout_str = new_config["notification_timeout"] # Parse the timeout string to milliseconds - timeout_ms = parse_timeout_string(timeout_str) + parse_timeout_string(timeout_str) # Handle ignored apps changes if "notification_ignored_apps" in new_config: - ignored_apps = new_config["notification_ignored_apps"] + new_config["notification_ignored_apps"] # Handle limited apps history changes if "notification_limited_apps_history" in new_config: - limited_apps = new_config["notification_limited_apps_history"] + new_config["notification_limited_apps_history"] # Update the current config self._current_config.update(new_config) diff --git a/modules/notification/notification_center.py b/modules/notification/notification_center.py index 7e1fdcc..1740fa4 100644 --- a/modules/notification/notification_center.py +++ b/modules/notification/notification_center.py @@ -170,7 +170,7 @@ class ExpandableNotificationGroup(Box): def _get_notification_pixbuf_for_group(self, cached_notification): """Get notification pixbuf using cached image key - fallback to app icon""" notification = cached_notification._notification - notification_id = getattr(notification, "id", None) + getattr(notification, "id", None) # First try to get cached notification image using stored cache key if ( @@ -447,7 +447,7 @@ class NotificationCenterWidget(NotificationWidget): def _get_notification_pixbuf(self, notification): """Get notification pixbuf using cached image key - fallback to app icon""" - notification_id = getattr(notification, "id", None) + getattr(notification, "id", None) # First try to get cached notification image using stored cache key if self.cache_metadata: @@ -689,7 +689,7 @@ class NotificationCenter(Window): rebuild_count = 0 for cached_notification in notification_service.cached_notifications: app_name = cached_notification._notification.app_name - notification_id = getattr(cached_notification._notification, "id", None) + getattr(cached_notification._notification, "id", None) # Skip ignored apps during rebuild if app_name in data.NOTIFICATION_IGNORED_APPS_HISTORY: diff --git a/modules/notification/unified_cache.py b/modules/notification/unified_cache.py index 3777989..b9dd51f 100644 --- a/modules/notification/unified_cache.py +++ b/modules/notification/unified_cache.py @@ -150,7 +150,7 @@ def verify_cache_persistence(): for cache_file in cache_files[:2]: # Test first 2 files try: cache_path = os.path.join(UNIFIED_NOTIFICATION_CACHE_DIR, cache_file) - test_pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path) + GdkPixbuf.Pixbuf.new_from_file(cache_path) except Exception as e: logger.warning(f"Failed to load cached asset {cache_file}: {e}") @@ -175,7 +175,7 @@ def get_fallback_icon(size=(48, 48)): return GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, True, 8, size[0], size[1] ) - except: + except Exception: return None diff --git a/modules/osd.py b/modules/osd.py deleted file mode 100644 index e714d63..0000000 --- a/modules/osd.py +++ /dev/null @@ -1,468 +0,0 @@ -import math -import time -from typing import ClassVar, Literal - -# from gi.repository import GLib, GObject -from fabric.audio import Audio -from fabric.utils import GLib, GObject -from fabric.utils.helpers import get_relative_path -from fabric.widgets.box import Box -from fabric.widgets.revealer import Revealer -from fabric.widgets.scale import Scale, ScaleMark -from fabric.widgets.svg import Svg -from fabric.widgets.wayland import WaylandWindow as Window - -from services.brightness import Brightness -from utils.animator import Animator - - -class AnimatedScale(Scale): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.animator = None - - def animate_value(self, value: float): - if not self.animator: - self.animator = Animator( - bezier_curve=(0.34, 1.56, 0.64, 1.0), - duration=0.8, - min_value=self.min_value, - max_value=self.value, - tick_widget=self, - notify_value=lambda p, *_: self.set_value(p.value), - ) - self.animator.pause() - self.animator.min_value = self.value - self.animator.max_value = value - self.animator.play() - - -class BrightnessOSDContainer(Box): - def __init__(self, **kwargs): - super().__init__(**kwargs, orientation="v", spacing=3, name="osd") - self.brightness_service = Brightness.get_initial() - self.scale = AnimatedScale( - marks=(ScaleMark(value=i) for i in range(0, 101, 10)), - value=70, - min_value=0, - max_value=100, - increments=(1, 1), - orientation="h", - ) - self.osd_window_image = Svg( - get_relative_path("../config/assets/icons/brightness/brightness.svg"), - size=(100, 150), - name="osd-image", - h_align="center", - v_align="center", - h_expand=True, - v_expand=True, - ) - - self.add(self.osd_window_image) - self.add(self.scale) - self.update_brightness() - - self.scale.connect("value-changed", lambda *_: self.update_brightness()) - self.brightness_service.connect("screen", self.on_brightness_changed) - - def update_brightness(self) -> None: - current_brightness = self.brightness_service.screen_brightness - normalized_brightness = self._normalize_brightness(current_brightness) - if current_brightness != 0: - self.scale.animate_value(normalized_brightness) - - def get_svg(self, value): - b_level = 0 if value == 0 else min(int(math.ceil(value / 33)), 3) - return b_level - - def on_brightness_changed(self, _sender: any, value: float, *_args) -> None: - normalized_brightness = self._normalize_brightness(value) - self.osd_window_image.set_from_file( - get_relative_path( - f"../config/assets/icons/brightness/brightness-{ - self.get_svg(normalized_brightness) - }.svg" - ) - ) - self.scale.animate_value(normalized_brightness) - - def _normalize_brightness(self, brightness: float) -> float: - return (brightness / self.brightness_service.max_screen) * 100 - - -class AudioOSDContainer(Box): - __gsignals__: ClassVar[dict] = { - "volume-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), - } - - def __init__(self, **kwargs): - super().__init__( - **kwargs, - orientation="v", - name="osd", - ) - self.audio = Audio() - self.scale = AnimatedScale( - value=70, - marks=(ScaleMark(value=i) for i in range(1, 100, 10)), - min_value=0, - max_value=100, - increments=(1, 1), - orientation="h", - ) - self.osd_window_image = Svg( - get_relative_path("../config/assets/icons/volume/audio-volume.svg"), - size=(150, 150), - name="osd-image", - h_align="center", - v_align="center", - h_expand=True, - v_expand=True, - ) - - self.previous_volume = None - self.previous_muted = None - - self.add(self.osd_window_image) - self.add(self.scale) - self.sync_with_audio() - - self.scale.connect("value-changed", self.on_volume_changed) - self.audio.connect("notify::speaker", self.on_audio_speaker_changed) - self.audio.connect("speaker-changed", self.on_speaker_changed) - - # Connect to current speaker if available - self._connect_speaker_signals() - - def _connect_speaker_signals(self): - """Connect to speaker's changed signal which should fire on both volume and mute changes""" - if self.audio.speaker: - # Connect to the main 'changed' signal from AudioStream - self.audio.speaker.connect("changed", self.on_speaker_stream_changed) - - def on_speaker_stream_changed(self, *_): - """This should be called whenever the speaker stream changes (volume OR mute)""" - if self.audio.speaker: - current_volume = ( - round(self.audio.speaker.volume) - if hasattr(self.audio.speaker, "volume") - else 0 - ) - current_muted = ( - self.audio.speaker.muted - if hasattr(self.audio.speaker, "muted") - else False - ) - - # Check if either volume or mute state changed - if ( - self.previous_volume != current_volume - or self.previous_muted != current_muted - ): - self.previous_volume = current_volume - self.previous_muted = current_muted - self.update_volume() - self.emit("volume-changed") - - def get_svg(self, value): - audio_level = 0 if value == 0 else min(int(math.ceil(value / 33)), 3) - return audio_level - - def sync_with_audio(self): - if self.audio.speaker: - volume = ( - round(self.audio.speaker.volume) - if hasattr(self.audio.speaker, "volume") - else 0 - ) - self.scale.set_value(volume) - self.previous_volume = volume - self.previous_muted = ( - self.audio.speaker.muted - if hasattr(self.audio.speaker, "muted") - else False - ) - - def on_volume_changed(self, *_): - if self.audio.speaker: - volume = self.scale.value - if 0 <= volume <= 100: - self.audio.speaker.set_volume(volume) - self.update_volume_display(volume) - self.emit("volume-changed") - - def update_volume_display(self, volume=None): - """Update the visual display based on current volume/mute state""" - if not self.audio.speaker: - return - - if volume is None: - volume = ( - round(self.audio.speaker.volume) - if hasattr(self.audio.speaker, "volume") - else 0 - ) - - is_muted = ( - self.audio.speaker.muted if hasattr(self.audio.speaker, "muted") else False - ) - - if volume == 0 or is_muted: - self.scale.add_style_class("muted") - display_volume = 0 - else: - self.scale.remove_style_class("muted") - display_volume = volume - - self.osd_window_image.set_from_file( - get_relative_path( - f"../config/assets/icons/volume/audio-volume-{ - self.get_svg(display_volume) - }.svg" - ) - ) - - def on_audio_speaker_changed(self, *_): - self._connect_speaker_signals() - self.update_volume() - - def on_speaker_changed(self, *_): - self._connect_speaker_signals() - self.update_volume() - - def update_volume(self, *_): - if self.audio.speaker and not self.is_hovered(): - volume = ( - round(self.audio.speaker.volume) - if hasattr(self.audio.speaker, "volume") - else 0 - ) - self.scale.set_value(volume) - self.update_volume_display(volume) - - -class MicrophoneOSDContainer(Box): - __gsignals__: ClassVar[dict] = { - "mic-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), - } - - def __init__(self, **kwargs): - super().__init__(**kwargs, orientation="v", spacing=13, name="osd") - self.audio = Audio() - self.scale = AnimatedScale( - marks=(ScaleMark(value=i) for i in range(1, 100, 10)), - value=70, - min_value=0, - max_value=100, - increments=(1, 1), - orientation="h", - ) - - self.osd_window_image = Svg( - get_relative_path("../config/assets/icons/mic/microphone.svg"), - name="osd-image", - size=(100, 150), - h_align="center", - v_align="center", - h_expand=True, - v_expand=True, - ) - self.previous_volume = None - self.previous_muted = None - - self.add(self.osd_window_image) - self.add(self.scale) - self.sync_with_audio() - - self.scale.connect("value-changed", self.on_volume_changed) - self.audio.connect("notify::microphone", self.on_audio_microphone_changed) - self.audio.connect("microphone-changed", self.on_microphone_changed) - - # Connect to current microphone if available - self._connect_microphone_signals() - - def _connect_microphone_signals(self): - """Connect to microphone's changed signal which should fire on both volume and mute changes""" - if self.audio.microphone: - # Connect to the main 'changed' signal from AudioStream - self.audio.microphone.connect("changed", self.on_microphone_stream_changed) - - def on_microphone_stream_changed(self, *_): - """This should be called whenever the microphone stream changes (volume OR mute)""" - if self.audio.microphone: - current_volume = ( - round(self.audio.microphone.volume) - if hasattr(self.audio.microphone, "volume") - else 0 - ) - current_muted = ( - self.audio.microphone.muted - if hasattr(self.audio.microphone, "muted") - else False - ) - - # Check if either volume or mute state changed - if ( - self.previous_volume != current_volume - or self.previous_muted != current_muted - ): - self.previous_volume = current_volume - self.previous_muted = current_muted - self.update_volume() - self.emit("mic-changed") - - def get_svg(self, value): - audio_level = 0 if value == 0 else min(int(math.ceil(value / 33)), 3) - return audio_level - - def sync_with_audio(self): - if self.audio.microphone: - volume = ( - round(self.audio.microphone.volume) - if hasattr(self.audio.microphone, "volume") - else 0 - ) - self.scale.set_value(volume) - self.previous_volume = volume - self.previous_muted = ( - self.audio.microphone.muted - if hasattr(self.audio.microphone, "muted") - else False - ) - - def on_volume_changed(self, *_): - if self.audio.microphone: - volume = self.scale.value - if 0 <= volume <= 100: - self.audio.microphone.set_volume(volume) - self.update_volume_display(volume) - self.emit("mic-changed") - - def update_volume_display(self, volume=None): - """Update the visual display based on current volume/mute state""" - if not self.audio.microphone: - return - - if volume is None: - volume = ( - round(self.audio.microphone.volume) - if hasattr(self.audio.microphone, "volume") - else 0 - ) - - is_muted = ( - self.audio.microphone.muted - if hasattr(self.audio.microphone, "muted") - else False - ) - - if volume == 0 or is_muted: - self.scale.add_style_class("muted") - display_volume = 0 - else: - self.scale.remove_style_class("muted") - display_volume = volume - - self.osd_window_image.set_from_file( - get_relative_path( - f"../config/assets/icons/mic/microphone-{ - self.get_svg(display_volume) - }.svg" - ) - ) - - def on_audio_microphone_changed(self, *_): - self._connect_microphone_signals() - self.update_volume() - - def on_microphone_changed(self, *_): - self._connect_microphone_signals() - self.update_volume() - - def update_volume(self, *_): - if self.audio.microphone and not self.is_hovered(): - volume = ( - round(self.audio.microphone.volume) - if hasattr(self.audio.microphone, "volume") - else 0 - ) - self.scale.set_value(volume) - self.update_volume_display(volume) - - -class OSD(Window): - def __init__(self, **kwargs): - self.audio_container = AudioOSDContainer() - self.brightness_container = BrightnessOSDContainer() - self.microphone_container = MicrophoneOSDContainer() - - self.timeout = 1000 - - self.revealer = Revealer( - transition_type="slide-up", - transition_duration=100, - child_revealed=False, - ) - - self.main_box = Box( - orientation="v", - h_expand=True, - children=[self.revealer], - ) - - super().__init__( - layer="overlay", - anchor="bottom", - title="modus", - child=self.main_box, - visible=False, - pass_through=True, - keyboard_mode="on-demand", - **kwargs, - ) - - self.last_activity_time = time.time() - - # Connect to the containers' signals - self.audio_container.connect("volume-changed", self.show_audio) - self.brightness_container.brightness_service.connect( - "screen", self.show_brightness - ) - self.microphone_container.connect("mic-changed", self.show_microphone) - - GLib.timeout_add(100, self.check_inactivity) - - def show_audio(self, *_): - self.show_box(box_to_show="audio") - self.reset_inactivity_timer() - - def show_brightness(self, *_): - self.show_box(box_to_show="brightness") - self.reset_inactivity_timer() - - def show_microphone(self, *_): - self.show_box(box_to_show="microphone") - self.reset_inactivity_timer() - - def show_box(self, box_to_show: Literal["audio", "brightness", "microphone"]): - self.set_visible(True) - if box_to_show == "audio": - self.revealer.children = self.audio_container - elif box_to_show == "brightness": - self.revealer.children = self.brightness_container - elif box_to_show == "microphone": - self.revealer.children = self.microphone_container - self.revealer.set_reveal_child(True) - self.reset_inactivity_timer() - - def start_hide_timer(self): - self.set_visible(False) - - def reset_inactivity_timer(self): - self.last_activity_time = time.time() - - def check_inactivity(self): - if time.time() - self.last_activity_time >= (self.timeout / 1000): - self.start_hide_timer() - return True diff --git a/modules/osd/__init__.py b/modules/osd/__init__.py new file mode 100644 index 0000000..d65876c --- /dev/null +++ b/modules/osd/__init__.py @@ -0,0 +1,8 @@ +from .main import OSDWindow # noqa: F401 +from .components import ( # noqa: F401 + AudioOSDContainer, + BrightnessOSDContainer, + MicrophoneOSDContainer, + CapsLockOSDContainer, + KeyboardLayoutOSDContainer, +) diff --git a/modules/osd/components/__init__.py b/modules/osd/components/__init__.py new file mode 100644 index 0000000..0a7ca48 --- /dev/null +++ b/modules/osd/components/__init__.py @@ -0,0 +1,5 @@ +from .audio import AudioOSDContainer # noqa: F401 +from .brightness import BrightnessOSDContainer # noqa: F401 +from .microphone import MicrophoneOSDContainer # noqa: F401 +from .capslock import CapsLockOSDContainer # noqa: F401 +from .keyboard_layout import KeyboardLayoutOSDContainer # noqa: F401 diff --git a/modules/osd/components/animated_scale.py b/modules/osd/components/animated_scale.py new file mode 100644 index 0000000..bf83fc4 --- /dev/null +++ b/modules/osd/components/animated_scale.py @@ -0,0 +1,28 @@ +from functools import partial +from fabric.widgets.scale import Scale +from utils.animator import Animator, cubic_bezier + + +class AnimatedScale(Scale): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.animator = ( + Animator( + duration=0.8, + timing_function=partial(cubic_bezier, 0.34, 1.56, 0.64, 1.0), + min_value=self.min_value, + max_value=self.value, + tick_widget=self, + notify_value=lambda anim: self.set_value(anim.value), + ) + .build() + .play() + .unwrap() + ) + + def animate_value(self, value: float): + if self.animator: + self.animator.pause() + self.animator.min_value = self.value + self.animator.max_value = value + self.animator.play() diff --git a/modules/osd/components/audio.py b/modules/osd/components/audio.py new file mode 100644 index 0000000..b5b2f29 --- /dev/null +++ b/modules/osd/components/audio.py @@ -0,0 +1,105 @@ +import math +from fabric.audio import Audio +from fabric.utils import get_relative_path +from fabric.widgets.svg import Svg +from fabric.widgets.scale import ScaleMark +from .animated_scale import AnimatedScale +from .base import BaseOSDContainer + + +class AudioOSDContainer(BaseOSDContainer): + def __init__(self, window, **kwargs): + super().__init__(window, **kwargs) + self.audio = Audio() + self._last_volume = None + self._last_muted = None + self._setup_specific_components() + self._connect_specific_signals() + + def _setup_specific_components(self): + self.osd_window_image = Svg( + get_relative_path("../../../config/assets/icons/volume/audio-volume.svg"), + size=(100, 100), + name="osd-image", + h_align="center", + v_align="center", + h_expand=True, + v_expand=True, + ) + self.scale = AnimatedScale( + marks=(ScaleMark(value=i) for i in range(1, 100, 10)), + value=70, + min_value=0, + max_value=100, + increments=(1, 1), + orientation="h", + ) + self.add(self.osd_window_image) + self.add(self.scale) + + def _connect_specific_signals(self): + self.audio.connect("notify::speaker", self._on_speaker_changed) + if self.audio.speaker: + self._connect_speaker_signals() + + def _connect_speaker_signals(self): + if self.audio.speaker: + self.audio.speaker.connect("changed", self._on_speaker_stream_changed) + self._sync_with_audio() + + def _on_speaker_changed(self, *_): + self._connect_speaker_signals() + self.update() + + def _on_speaker_stream_changed(self, *_): + if self.audio.speaker: + current_volume = round(self.audio.speaker.volume) + current_muted = self.audio.speaker.muted + if self._last_volume != current_volume or self._last_muted != current_muted: + self._last_volume = current_volume + self._last_muted = current_muted + self.update() + + def _sync_with_audio(self): + if self.audio.speaker: + self._last_volume = round(self.audio.speaker.volume) + self._last_muted = self.audio.speaker.muted + self.scale.set_value(self._last_volume) + self._update_display() + + def _update_display(self): + if not self.audio.speaker: + return + + volume = ( + self._last_volume + if self._last_volume is not None + else round(self.audio.speaker.volume) + ) + muted = ( + self._last_muted + if self._last_muted is not None + else self.audio.speaker.muted + ) + + display_volume = 0 if (volume == 0 or muted) else volume + level = ( + 0 if display_volume == 0 else min(int(math.ceil(display_volume / 33)), 3) + ) + + self.osd_window_image.set_from_file( + get_relative_path( + f"../../../config/assets/icons/volume/audio-volume-{level}.svg" + ) + ) + + if muted or volume == 0: + self.scale.add_style_class("muted") + else: + self.scale.remove_style_class("muted") + + self.scale.animate_value(volume) + + def update(self, *_): + self._update_display() + super().update() diff --git a/modules/osd/components/base.py b/modules/osd/components/base.py new file mode 100644 index 0000000..30e2197 --- /dev/null +++ b/modules/osd/components/base.py @@ -0,0 +1,145 @@ +from abc import abstractmethod +from fabric.utils import Gdk, GLib, invoke_repeater, remove_handler, time +from fabric.widgets.box import Box +from fabric.widgets.wayland import WaylandWindow as Window + + +class BaseOSDContainer(Box): + MAX_VISIBLE_TIME = 6.0 + COOLDOWN_MS = 200 + + def __init__(self, window: Window, **kwargs): + super().__init__(**kwargs, orientation="v", spacing=12, name="osd-container") + self.last_handler: int = 0 + self.window = window + self.osd = None + self.show_timestamp: float = 0.0 + self.watchdog_handler: int = 0 + self._update_in_progress = False + self._is_hovered = False + self._last_trigger_time: float = 0.0 + + GLib.idle_add(self._setup_window_signals) + + def _setup_window_signals(self): + self.window.add_events( + Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK + ) + self.window.connect("enter-notify-event", self._on_enter_notify) + self.window.connect("leave-notify-event", self._on_leave_notify) + return False + + def _on_enter_notify(self, widget, event): + if event.detail != Gdk.NotifyType.INFERIOR: + self._is_hovered = True + + def _on_leave_notify(self, widget, event): + if event.detail != Gdk.NotifyType.INFERIOR: + self._is_hovered = False + + def is_hovered(self): + return self._is_hovered + + def remove_last_handler(self): + if self.last_handler: + try: + remove_handler(self.last_handler) + except Exception: + pass + self.last_handler = 0 + + def remove_watchdog_handler(self): + if self.watchdog_handler: + try: + GLib.source_remove(self.watchdog_handler) + except Exception: + pass + self.watchdog_handler = 0 + + def cleanup_all_handlers(self): + self.remove_last_handler() + self.remove_watchdog_handler() + self.show_timestamp = 0.0 + self._update_in_progress = False + + def hide_window(self): + if self.osd and hasattr(self.osd, "revealer"): + self.osd.revealer.set_reveal_child(False) + GLib.timeout_add(150, lambda: self.window.hide() or False) + else: + self.window.hide() + + def watchdog_force_hide(self, *_): + if not self.window.get_visible(): + self.cleanup_all_handlers() + return False + + elapsed = time.time() - self.show_timestamp + if elapsed >= self.MAX_VISIBLE_TIME: + if not self.is_hovered(): + self.hide_window() + self.cleanup_all_handlers() + return False + else: + return True + return True + + def update(self, *_): + if self._update_in_progress: + return + + current_time = time.time() + time_since_last_trigger = (current_time - self._last_trigger_time) * 1000 + + if time_since_last_trigger < self.COOLDOWN_MS: + return + + self._last_trigger_time = current_time + self._update_in_progress = True + + self.cleanup_all_handlers() + self.show_timestamp = time.time() + + if self.osd: + self.osd.show_container(self) + + self.window.show() + + self.focus() + self.last_handler = invoke_repeater(1700, self.unfocus, initial_call=False) + self.watchdog_handler = GLib.timeout_add(1000, self.watchdog_force_hide) + self._update_in_progress = False + + def focus(self, *_): + self.add_style_class("focused") + return False + + def unfocus(self, *_): + if not self.window.get_visible(): + self.cleanup_all_handlers() + return False + + self.remove_style_class("focused") + self.remove_last_handler() + self.last_handler = invoke_repeater(1700, self.unpop, initial_call=False) + return False + + def unpop(self, *_): + if not self.window.get_visible(): + self.cleanup_all_handlers() + return False + + if not self.is_hovered(): + self.hide_window() + self.cleanup_all_handlers() + else: + self.last_handler = invoke_repeater(500, self.unpop, initial_call=False) + return False + + @abstractmethod + def _setup_specific_components(self): + pass + + @abstractmethod + def _connect_specific_signals(self): + pass diff --git a/modules/osd/components/brightness.py b/modules/osd/components/brightness.py new file mode 100644 index 0000000..5d9ff56 --- /dev/null +++ b/modules/osd/components/brightness.py @@ -0,0 +1,64 @@ +import math +from services.brightness import Brightness +from fabric.utils import get_relative_path +from fabric.widgets.svg import Svg +from fabric.widgets.scale import ScaleMark +from .animated_scale import AnimatedScale +from .base import BaseOSDContainer + + +class BrightnessOSDContainer(BaseOSDContainer): + def __init__(self, window, **kwargs): + super().__init__(window, **kwargs) + self.brightness_service = Brightness.get_initial() + self._setup_specific_components() + self._connect_specific_signals() + + def _setup_specific_components(self): + self.osd_window_image = Svg( + get_relative_path("../../../config/assets/icons/brightness/brightness.svg"), + size=(100, 100), + name="osd-image", + h_align="center", + v_align="center", + h_expand=True, + v_expand=True, + ) + self.scale = AnimatedScale( + marks=(ScaleMark(value=i) for i in range(0, 101, 10)), + value=70, + min_value=0, + max_value=100, + increments=(1, 1), + orientation="h", + ) + self.add(self.osd_window_image) + self.add(self.scale) + + def _connect_specific_signals(self): + self.brightness_service.connect("screen", self._on_screen_brightness_changed) + + def _on_screen_brightness_changed(self, _sender, value, *_args): + self.update() + + def _get_normalized_brightness(self): + return ( + self.brightness_service.screen_brightness + / self.brightness_service.max_screen + ) * 100 + + def _update_display(self): + normalized = self._get_normalized_brightness() + level = 0 if normalized == 0 else min(int(math.ceil(normalized / 33)), 3) + + self.osd_window_image.set_from_file( + get_relative_path( + f"../../../config/assets/icons/brightness/brightness-{level}.svg" + ) + ) + + self.scale.animate_value(normalized) + + def update(self, *_): + self._update_display() + super().update() diff --git a/modules/osd/components/capslock.py b/modules/osd/components/capslock.py new file mode 100644 index 0000000..3edb32a --- /dev/null +++ b/modules/osd/components/capslock.py @@ -0,0 +1,50 @@ +from fabric.widgets.label import Label +from fabric.widgets.svg import Svg +from fabric.utils import get_relative_path +from .base import BaseOSDContainer +from services.capslock import CapsLock + + +class CapsLockOSDContainer(BaseOSDContainer): + def __init__(self, window, **kwargs): + super().__init__(window, **kwargs) + self.capslock = CapsLock.get_initial() + self._setup_specific_components() + self._connect_specific_signals() + + def _setup_specific_components(self): + self.osd_image = Svg( + get_relative_path("../../../assets/icons/misc/caps-lock.svg"), + size=(100, 100), + name="osd-image", + h_align="center", + v_align="center", + h_expand=True, + v_expand=True, + ) + self.label = Label(name="osd-label") + self.add(self.osd_image) + self._update_display(self.capslock.is_on) + + def _connect_specific_signals(self): + self.capslock.connect("state_changed", self._on_caps_lock_state_changed) + + def _on_caps_lock_state_changed(self, _, is_on: bool): + self._update_display(is_on) + self.update() + + def _update_display(self, is_on: bool): + icon_path = "caps-lock.svg" if is_on else "caps-lock-off.svg" + self.osd_image.set_from_file( + get_relative_path(f"../../../assets/icons/misc/{icon_path}") + ) + if is_on: + self.add_style_class("capslock-on") + self.remove_style_class("capslock-off") + else: + self.add_style_class("capslock-off") + self.remove_style_class("capslock-on") + + def update(self, *_): + # We don't need a separate display update here as it's handled in the signal + super().update() diff --git a/modules/osd/components/keyboard_layout.py b/modules/osd/components/keyboard_layout.py new file mode 100644 index 0000000..5da0c8b --- /dev/null +++ b/modules/osd/components/keyboard_layout.py @@ -0,0 +1,40 @@ +from fabric.widgets.label import Label +from fabric.widgets.svg import Svg +from fabric.utils import get_relative_path +from .base import BaseOSDContainer +from services.keyboard_layout import KeyboardLayout + + +class KeyboardLayoutOSDContainer(BaseOSDContainer): + def __init__(self, window, **kwargs): + super().__init__(window, **kwargs) + self.keyboard_layout = KeyboardLayout.get_initial() + self._setup_specific_components() + self._connect_specific_signals() + + def _setup_specific_components(self): + self.osd_image = Svg( + get_relative_path("../../../assets/icons/misc/keyboard-layout.svg"), + size=(100, 100), + name="osd-image", + h_align="center", + v_align="center", + h_expand=True, + v_expand=True, + ) + self.label = Label(name="osd-label") + self.add(self.osd_image) + self.add(self.label) + self.label.set_text(self.keyboard_layout.current_layout) + + def _connect_specific_signals(self): + self.keyboard_layout.connect("layout_changed", self._on_layout_changed) + + def _on_layout_changed(self, service, layout: str): + if not self.is_hovered(): + self.label.set_text(layout) + self.update() + + def update(self, *_): + self.label.set_text(self.keyboard_layout.current_layout) + super().update() diff --git a/modules/osd/components/microphone.py b/modules/osd/components/microphone.py new file mode 100644 index 0000000..320681c --- /dev/null +++ b/modules/osd/components/microphone.py @@ -0,0 +1,105 @@ +import math +from fabric.audio import Audio +from fabric.utils import get_relative_path +from fabric.widgets.svg import Svg +from fabric.widgets.scale import ScaleMark +from .animated_scale import AnimatedScale +from .base import BaseOSDContainer + + +class MicrophoneOSDContainer(BaseOSDContainer): + def __init__(self, window, **kwargs): + super().__init__(window, **kwargs) + self.audio = Audio() + self._last_volume = None + self._last_muted = None + self._setup_specific_components() + self._connect_specific_signals() + + def _setup_specific_components(self): + self.osd_window_image = Svg( + get_relative_path("../../../config/assets/icons/mic/microphone.svg"), + size=(100, 100), + name="osd-image", + h_align="center", + v_align="center", + h_expand=True, + v_expand=True, + ) + self.scale = AnimatedScale( + marks=(ScaleMark(value=i) for i in range(1, 100, 10)), + value=70, + min_value=0, + max_value=100, + increments=(1, 1), + orientation="h", + ) + self.add(self.osd_window_image) + self.add(self.scale) + + def _connect_specific_signals(self): + self.audio.connect("notify::microphone", self._on_microphone_changed) + if self.audio.microphone: + self._connect_microphone_signals() + + def _connect_microphone_signals(self): + if self.audio.microphone: + self.audio.microphone.connect("changed", self._on_microphone_stream_changed) + self._sync_with_audio() + + def _on_microphone_changed(self, *_): + self._connect_microphone_signals() + self.update() + + def _on_microphone_stream_changed(self, *_): + if self.audio.microphone: + current_volume = round(self.audio.microphone.volume) + current_muted = self.audio.microphone.muted + if self._last_volume != current_volume or self._last_muted != current_muted: + self._last_volume = current_volume + self._last_muted = current_muted + self.update() + + def _sync_with_audio(self): + if self.audio.microphone: + self._last_volume = round(self.audio.microphone.volume) + self._last_muted = self.audio.microphone.muted + self.scale.set_value(self._last_volume) + self._update_display() + + def _update_display(self): + if not self.audio.microphone: + return + + volume = ( + self._last_volume + if self._last_volume is not None + else round(self.audio.microphone.volume) + ) + muted = ( + self._last_muted + if self._last_muted is not None + else self.audio.microphone.muted + ) + + display_volume = 0 if (volume == 0 or muted) else volume + level = ( + 0 if display_volume == 0 else min(int(math.ceil(display_volume / 33)), 3) + ) + + self.osd_window_image.set_from_file( + get_relative_path( + f"../../../config/assets/icons/mic/microphone-{level}.svg" + ) + ) + + if muted or volume == 0: + self.scale.add_style_class("muted") + else: + self.scale.remove_style_class("muted") + + self.scale.animate_value(volume) + + def update(self, *_): + self._update_display() + super().update() diff --git a/modules/osd/main.py b/modules/osd/main.py new file mode 100644 index 0000000..9c1fa4b --- /dev/null +++ b/modules/osd/main.py @@ -0,0 +1,83 @@ +from fabric.widgets.box import Box +from fabric.widgets.revealer import Revealer +from fabric.widgets.wayland import WaylandWindow as Window +from .components import ( + AudioOSDContainer, + BrightnessOSDContainer, + MicrophoneOSDContainer, + CapsLockOSDContainer, + KeyboardLayoutOSDContainer, +) + + +class OSD(Box): + def __init__(self, window: Window, **kwargs): + super().__init__(name="osd", **kwargs) + self.window = window + + self.revealer = Revealer( + transition_type="slide-up", + transition_duration=100, + child_revealed=False, + ) + self.children = [self.revealer] + + self.containers = { + "audio": AudioOSDContainer(window), + "brightness": BrightnessOSDContainer(window), + "microphone": MicrophoneOSDContainer(window), + "capslock": CapsLockOSDContainer(window), + "kb_layout": KeyboardLayoutOSDContainer(window), + } + + # Set the OSD reference in containers + for container in self.containers.values(): + container.osd = self + + self.current_container = None + + def show_container(self, container): + if self.current_container != container: + self.revealer.children = [container] + self.current_container = container + self.revealer.set_reveal_child(True) + + def show_audio_osd(self): + self.show_container(self.containers.get("audio")) + + def show_brightness_osd(self): + self.show_container(self.containers.get("brightness")) + + def show_microphone_osd(self): + self.show_container(self.containers.get("microphone")) + + def show_capslock_osd(self): + self.show_container(self.containers.get("capslock")) + + def show_kb_layout_osd(self): + self.show_container(self.containers.get("kb_layout")) + + +class OSDWindow(Window): + def __init__(self, **kwargs): + super().__init__( + name="osd-window", + title="modus-osd", + anchor="bottom", + margin="0px 0px 40px 0px", + visible=False, + all_visible=False, + pass_through=True, + layer="overlay", + **kwargs, + ) + self.osd = OSD(window=self) + self.add(self.osd) + + +if __name__ == "__main__": + from fabric import Application + + osd_window = OSDWindow() + app = Application("modus-osd", osd_window) + app.run() diff --git a/modules/osdd/components/__init__.py b/modules/osdd/components/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/osdd/components/audio.py b/modules/osdd/components/audio.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/osdd/components/brightness.py b/modules/osdd/components/brightness.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/osdd/components/capslock.py b/modules/osdd/components/capslock.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/osdd/components/keyboard_layout.py b/modules/osdd/components/keyboard_layout.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/osdd/components/microphone.py b/modules/osdd/components/microphone.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/osdd/main.py b/modules/osdd/main.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/todo/todo_widget.py b/modules/todo/todo_widget.py index 0077a38..200afa7 100644 --- a/modules/todo/todo_widget.py +++ b/modules/todo/todo_widget.py @@ -12,7 +12,7 @@ from fabric.widgets.svg import Svg from gi.repository import GLib # Local imports -from services.todo import todo_service +from services.todo import get_todo_service from widgets.mousecapture import MouseCapture from widgets.wayland import WaylandWindow as Window @@ -153,14 +153,14 @@ class TodoItem(Box): def _toggle_completion(self, *_): """Toggle todo completion status""" - todo_service.toggle_todo(self.todo_data["id"]) + get_todo_service().toggle_todo(self.todo_data["id"]) def _cycle_priority(self, *_): """Cycle through priority levels""" priorities = ["low", "medium", "high"] current_index = priorities.index(self.todo_data["priority"]) new_priority = priorities[(current_index + 1) % len(priorities)] - todo_service.set_priority(self.todo_data["id"], new_priority) + get_todo_service().set_priority(self.todo_data["id"], new_priority) def _start_edit(self, *_): """Start editing the todo text""" @@ -183,7 +183,7 @@ class TodoItem(Box): new_text = self.text_entry.get_text().strip() if new_text: - todo_service.edit_todo(self.todo_data["id"], new_text) + get_todo_service().edit_todo(self.todo_data["id"], new_text) self._cancel_edit() @@ -198,7 +198,7 @@ class TodoItem(Box): def _delete_todo(self, *_): """Delete this todo""" - todo_service.delete_todo(self.todo_data["id"]) + get_todo_service().delete_todo(self.todo_data["id"]) def update_from_data(self, todo_data): """Update the widget from new todo data""" @@ -252,7 +252,7 @@ class TodoListWidget(Window): self.todo_items = {} # Maps todo IDs to TodoItem widgets # Register callback with todo service - todo_service.add_callback(self._on_todo_event) + get_todo_service().add_callback(self._on_todo_event) self._build_ui() self._refresh_todos() @@ -363,12 +363,12 @@ class TodoListWidget(Window): """Add a new todo""" text = self.new_todo_entry.get_text().strip() if text: - todo_service.add_todo(text) + get_todo_service().add_todo(text) self.new_todo_entry.set_text("") def _clear_completed(self, *_): """Clear all completed todos""" - todo_service.clear_completed() + get_todo_service().clear_completed() def _on_todo_event(self, event_type, data=None): """Handle todo service events via callback""" @@ -392,7 +392,7 @@ class TodoListWidget(Window): self.todos_container.children = [] # Get all todos - todos = todo_service.todos + todos = get_todo_service().todos # Sort todos: incomplete first, then by priority, then by creation date def sort_key(todo): @@ -418,7 +418,7 @@ class TodoListWidget(Window): def _update_stats(self): """Update the statistics display""" - stats = todo_service.get_stats() + stats = get_todo_service().get_stats() stats_text = f"{stats['pending']} pending, {stats['completed']} completed" self.stats_label.set_label(stats_text) @@ -439,7 +439,7 @@ class TodoListWidget(Window): def destroy(self): """Clean up when destroyed""" # Remove callback from todo service - todo_service.remove_callback(self._on_todo_event) + get_todo_service().remove_callback(self._on_todo_event) super().destroy()