diff --git a/config/mods.toml b/config/mods.toml index ebff9cd..8b96f9d 100644 --- a/config/mods.toml +++ b/config/mods.toml @@ -23,5 +23,5 @@ on-clicked = "python3 -m utils.google_lens_cli" # # [[Mods.terminal.options]] # label = "Terminal but scary" -# on-clicked = "ghostty" +# on-clicked = "zen-browser" # divider = true diff --git a/src/main.py b/src/main.py index 81e917e..8862360 100644 --- a/src/main.py +++ b/src/main.py @@ -32,6 +32,7 @@ from shared.data import APP_NAME, load_config from utils.functions import set_process_name from window.desktop.widget import Deskwidgets from window.dock import Dock +from window.lock import LockScreenWrapper from window.notification.notification import ModusNoti from window.osd.main import OSDWindow from window.panel.main import Panel @@ -54,6 +55,20 @@ def main(): set_process_name(APP_NAME) load_config() + + from services.config import config + + _debug = config().get("debug", False) + + if not _debug: + logger.disable("modus_plugin_builtin_emoji") + else: + logger.enable("fabric") + logger.enable("services") + logger.enable("window") + logger.enable("utils") + logger.enable("window.globalmenu") + switcher = ApplicationSwitcher() panel = Panel() modusnoti = ModusNoti() @@ -113,6 +128,7 @@ def main(): __main__.dock = dock __main__.osd = osd __main__.screencapture = screencapture + __main__.lock_screen = LockScreenWrapper() __main__.switch_keyboard_layout = KeyboardLayout.switch_keyboard_layout app.run() diff --git a/src/services/brightness.py b/src/services/brightness.py index 7825320..8e44658 100644 --- a/src/services/brightness.py +++ b/src/services/brightness.py @@ -93,7 +93,8 @@ class Brightness(Service): return True except Exception as e: logger.error(f"Error checking brightness file: {e}") - return True + self._poll_timer_id = None + return False def _detect_backend(self, backend): """Detect appropriate backend for brightness control.""" diff --git a/src/services/config.py b/src/services/config.py index da37bf1..354af65 100644 --- a/src/services/config.py +++ b/src/services/config.py @@ -59,6 +59,15 @@ class ConfigService: """Set a config value (local state only).""" self._config[key] = value + def save(self) -> None: + """Persist current in-memory config to disk.""" + try: + doc = self._dict_to_toml(self._config) + with open(self._config_file, "w") as f: + toml_dump(doc, f) + except Exception as e: + logger.error(f"[ConfigService] Failed to save config: {e}") + def register_reload_callback( self, callback: Callable[[Dict[str, Any], Dict[str, Any]], None] ) -> None: diff --git a/src/services/mpris.py b/src/services/mpris.py index dabffdc..fa74641 100644 --- a/src/services/mpris.py +++ b/src/services/mpris.py @@ -327,12 +327,14 @@ class PlayerService(Service): if metadata is None: try: metadata = self._player.props.metadata - except Exception: + except Exception as e: + logger.debug(f"[mpris] Failed to read metadata: {e}") metadata = None if metadata is not None: try: dur = metadata["mpris:length"] / 1_000_000 - except Exception: + except Exception as e: + logger.debug(f"[mpris] Failed to read duration: {e}") dur = 0 # Browser players (YouTube in Firefox/Chromium) populate # `mpris:artUrl` lazily and don't reliably emit the `metadata` diff --git a/src/services/network.py b/src/services/network.py index af6c011..54e3377 100644 --- a/src/services/network.py +++ b/src/services/network.py @@ -1,3 +1,4 @@ +import shlex from types import SimpleNamespace from typing import List, Literal @@ -105,7 +106,8 @@ class Wifi(Service): try: self._device.request_scan_async(None, _on_scan_finish) - except Exception: + except Exception as e: + logger.warning(f"[network] request_scan_async failed: {e}") self._scanning = False def notifier(self, name: str, *args): @@ -401,26 +403,33 @@ class NetworkClient(Service): callback(False, "No wifi device") return + if not bssid: + if callback: + callback(False, "No bssid") + return + device = self.wifi_device._device handler_id: list[int] = [] + _fired = [False] def _on_state_changed(_dev, new_state, _old_state, reason): - if not callback: + if _fired[0]: return state = NM.DeviceState(new_state) if state == NM.DeviceState.ACTIVATED: + _fired[0] = True _cleanup() - callback(True, "Connected") - elif state in (NM.DeviceState.FAILED, NM.DeviceState.DISCONNECTED): + if callback: + callback(True, "Connected") + elif state == NM.DeviceState.FAILED: + _fired[0] = True _cleanup() reason_str = NM.DeviceStateReason(reason).value_nick - if "secret" in reason_str or "auth" in reason_str: - callback(False, "Wrong password") - else: - callback(False, f"Failed ({reason_str})") - elif state == NM.DeviceState.NEED_AUTH: - _cleanup() - callback(False, "Wrong password") + if callback: + if "secret" in reason_str or "auth" in reason_str: + callback(False, "Wrong password") + else: + callback(False, f"Failed ({reason_str})") def _cleanup(): if handler_id: @@ -433,7 +442,7 @@ class NetworkClient(Service): handler_id.append(device.connect("state-changed", _on_state_changed)) exec_shell_command_async( - f"nmcli device wifi connect {bssid}", + f"nmcli device wifi connect {shlex.quote(bssid)}", lambda *args: logger.debug(f"connect result: {args}"), ) @@ -442,15 +451,24 @@ class NetworkClient(Service): callback(False, "No wifi device") return + if not bssid: + callback(False, "No bssid") + return + device = self.wifi_device._device handler_id: list[int] = [] + _fired = [False] def _on_state_changed(_dev, new_state, _old_state, reason): + if _fired[0]: + return state = NM.DeviceState(new_state) if state == NM.DeviceState.ACTIVATED: + _fired[0] = True _cleanup() callback(True, "Connected") elif state in (NM.DeviceState.FAILED, NM.DeviceState.DISCONNECTED): + _fired[0] = True _cleanup() reason_str = NM.DeviceStateReason(reason).value_nick if "secret" in reason_str or "auth" in reason_str: @@ -458,6 +476,7 @@ class NetworkClient(Service): else: callback(False, f"Failed ({reason_str})") elif state == NM.DeviceState.NEED_AUTH: + _fired[0] = True _cleanup() callback(False, "Wrong password") @@ -471,6 +490,6 @@ class NetworkClient(Service): handler_id.append(device.connect("state-changed", _on_state_changed)) exec_shell_command_async( - f"nmcli device wifi connect {bssid} password {password!r}", + f"nmcli device wifi connect {shlex.quote(bssid)} password {shlex.quote(password)}", lambda *args: logger.debug(f"connect_with_password result: {args}"), ) diff --git a/src/services/screenrecorder.py b/src/services/screenrecorder.py index 4f8ad21..8e33bb9 100644 --- a/src/services/screenrecorder.py +++ b/src/services/screenrecorder.py @@ -5,6 +5,7 @@ Recording state is held entirely in memory as Fabric properties -- no temporary files are used. """ +import shlex import time from datetime import datetime from pathlib import Path @@ -261,9 +262,11 @@ class ScreenRecorder(Service): def _handle_action(self, action: str, path: str) -> None: match action: case "files": - exec_shell_command_async(f"xdg-open {self.recordings_dir}") + exec_shell_command_async( + f"xdg-open {shlex.quote(str(self.recordings_dir))}" + ) case "view": - exec_shell_command_async(f"xdg-open {path}") + exec_shell_command_async(f"xdg-open {shlex.quote(path)}") def _notify_cancelled(self) -> None: self._notifier.notify( diff --git a/src/services/screenshot.py b/src/services/screenshot.py index a480e31..d43fb31 100644 --- a/src/services/screenshot.py +++ b/src/services/screenshot.py @@ -6,6 +6,7 @@ into a concrete request, hands it to a :class:`CaptureBackend`, then raises notifications and signals based on the result. """ +import shlex from datetime import datetime from pathlib import Path @@ -157,11 +158,13 @@ class Screenshot(Service): def _handle_action(self, action: str, path: str) -> None: match action: case "files": - exec_shell_command_async(f"xdg-open {self.screenshots_dir}") + exec_shell_command_async( + f"xdg-open {shlex.quote(str(self.screenshots_dir))}" + ) case "view": - exec_shell_command_async(f"xdg-open {path}") + exec_shell_command_async(f"xdg-open {shlex.quote(path)}") case "edit": - exec_shell_command_async(f"satty -f {path}") + exec_shell_command_async(f"satty -f {shlex.quote(path)}") def _notify_cancelled(self) -> None: self._notifier.notify( diff --git a/src/services/wallpaper.py b/src/services/wallpaper.py index e7653c8..605273c 100644 --- a/src/services/wallpaper.py +++ b/src/services/wallpaper.py @@ -1,18 +1,18 @@ import hashlib import mimetypes +import shlex from fabric.core import Service, Signal from fabric.utils import GdkPixbuf as gdk_pixbuf from fabric.utils import exec_shell_command_async, logger, os +from services.config import on_config_change from shared.data import ( - WALLPAPER_PATH, - WALLPAPER_THUMBS_PATH, WALLPAPERS_THUMBNAILS_SIZE, + _get_wallpaper_path, ) -os.makedirs(WALLPAPER_PATH, exist_ok=True) -os.makedirs(WALLPAPER_THUMBS_PATH, exist_ok=True) +os.makedirs(_get_wallpaper_path(), exist_ok=True) def get_thumbnail_filename(image_path: str) -> str: @@ -64,14 +64,23 @@ class WallpaperService(Service): def __init__(self, **kwargs): super().__init__(**kwargs) self._file_cache: list[str] | None = None + on_config_change(self._on_config_change) + + def _on_config_change(self, new_config, old_config): + if new_config.get("wallpapers_dir") != old_config.get("wallpapers_dir"): + self._file_cache = None + path = _get_wallpaper_path() + os.makedirs(path, exist_ok=True) + os.makedirs(f"{path}/.thumbnails", exist_ok=True) def get_all_wallpapers(self, use_cache: bool = True) -> list[str]: if use_cache and self._file_cache is not None: return self._file_cache wallpapers: list[str] = [] + wallpaper_path = _get_wallpaper_path() - with os.scandir(WALLPAPER_PATH) as entries: + with os.scandir(wallpaper_path) as entries: for entry in entries: if not entry.is_file(): continue @@ -87,8 +96,9 @@ class WallpaperService(Service): self._file_cache = None def get_thumbnail_path(self, image_path: str) -> str: + thumbs_dir = f"{_get_wallpaper_path()}/.thumbnails" return os.path.join( - WALLPAPER_THUMBS_PATH, + thumbs_dir, get_thumbnail_filename(image_path), ) @@ -97,7 +107,7 @@ class WallpaperService(Service): def set_wallpaper(self, image_path: str) -> None: exec_shell_command_async( - f'awww img "{image_path}" ' + f"awww img {shlex.quote(image_path)} " "--transition-type none " "--transition-duration 0 " "--transition-fps 60" @@ -112,7 +122,9 @@ class WallpaperService(Service): os.symlink(os.path.abspath(image_path), link_path) - exec_shell_command_async(f'matugen image "{image_path}" --source-color-index 0') + exec_shell_command_async( + f"matugen image {shlex.quote(image_path)} --source-color-index 0" + ) self.colors_generated(image_path) diff --git a/src/shared/data.py b/src/shared/data.py index b215483..f7ef3e2 100644 --- a/src/shared/data.py +++ b/src/shared/data.py @@ -20,13 +20,11 @@ def _get_wallpaper_path() -> str: custom = get_config("wallpapers_dir") if custom: return os.path.expanduser(str(custom)) - except Exception: - pass + except Exception as e: + logger.warning(f"[data] Failed to load wallpapers_dir from config: {e}") return f"{HOME_DIR}/Pictures/Wallpapers" -WALLPAPER_PATH = _get_wallpaper_path() -WALLPAPER_THUMBS_PATH = f"{WALLPAPER_PATH}/.thumbnails" WALLPAPERS_THUMBNAILS_SIZE = 200 WALLPAPERS_DIR_DEFAULT = get_relative_path("../assets/wallpapers_example/") diff --git a/src/shared/dialogs/wifi_password_dialog.py b/src/shared/dialogs/wifi_password_dialog.py index 90b4357..231bc5b 100644 --- a/src/shared/dialogs/wifi_password_dialog.py +++ b/src/shared/dialogs/wifi_password_dialog.py @@ -226,8 +226,12 @@ class WiFiPasswordDialog(Window): def _connection_timeout(self): if self.is_connecting: self.is_connecting = False - self.join_button.set_sensitive(True) - self.show_error("Connection timeout. Please try again.") + self.connection_timeout_id = None + try: + self.join_button.set_sensitive(True) + self.show_error("Connection timeout. Please try again.") + except Exception: + pass return False def show_dialog(self): diff --git a/src/shared/window/applet_window.py b/src/shared/window/applet_window.py index 422a76c..1583e8e 100644 --- a/src/shared/window/applet_window.py +++ b/src/shared/window/applet_window.py @@ -102,4 +102,5 @@ class AppletWindow(PopupWindow): self.dismiss_layer.destroy() if self._hide_timeout_id is not None: GLib.source_remove(self._hide_timeout_id) + self._hide_timeout_id = None super().destroy() diff --git a/src/utils/constants.py b/src/utils/constants.py index a9521ad..4b887de 100644 --- a/src/utils/constants.py +++ b/src/utils/constants.py @@ -1,4 +1,5 @@ DEFAULT = { + "debug": False, "dock_enabled": True, "dock_auto_hide": True, "dock_always_occluded": False, diff --git a/src/window/controlcenter/main.py b/src/window/controlcenter/main.py index ef9ffa2..d9f2129 100644 --- a/src/window/controlcenter/main.py +++ b/src/window/controlcenter/main.py @@ -93,6 +93,7 @@ class ModusControlCenter(AppletWindow): # Initialize network service for WiFi toggle - lazy load self.network_service = None self.wifi_service = None + self._network_ready_fired = False # Initialize flight mode and caffeine states - lazy load self.caffeine_mode = False @@ -905,9 +906,19 @@ class ModusControlCenter(AppletWindow): logger.warning(f"Failed to toggle bluetooth: {e}") def on_network_ready(self, *_): + if self._network_ready_fired: + return + self._network_ready_fired = True self.wifi_service = self.network_service.wifi_device if self.wifi_service: - self.wifi_service.connect("notify::wireless-enabled", self.update_wifi_icon) + self._signal_connections.append( + ( + self.wifi_service, + self.wifi_service.connect( + "notify::wireless-enabled", self.update_wifi_icon + ), + ) + ) def update_wifi_icon(self, *_): try: diff --git a/src/window/controlcenter/player.py b/src/window/controlcenter/player.py index 39a1d1b..08c28ee 100644 --- a/src/window/controlcenter/player.py +++ b/src/window/controlcenter/player.py @@ -574,9 +574,7 @@ class PlayerBox(Box): current_status = str(self.player.playback_status).lower() self._last_notified_status = current_status self.play_pause_icon.dynamic_file( - "player/play.svg" - if current_status == "paused" - else "player/pause.svg" + "player/play.svg" if current_status == "paused" else "player/pause.svg" ) self._on_metadata(self.player, self.player._player.props.metadata) self.set_image() diff --git a/src/window/controlcenter/wifi.py b/src/window/controlcenter/wifi.py index 28e9bb7..fcd6ee5 100644 --- a/src/window/controlcenter/wifi.py +++ b/src/window/controlcenter/wifi.py @@ -275,6 +275,7 @@ class WifiConnections(Box): self._update_in_progress = False # Prevent concurrent updates self._destroyed = False # Track if widget is destroyed self._signal_ids = [] # Track all service signal IDs for cleanup + self._network_ready_fired = False # Guard against duplicate device-ready self._signal_ids.append( ( @@ -437,6 +438,9 @@ class WifiConnections(Box): def on_network_ready(self, *_): """Called when network service is ready""" + if self._network_ready_fired: + return + self._network_ready_fired = True self.wifi_service = self.network_service.wifi_device if self.wifi_service: self.toggle_button.set_active(self.wifi_service.enabled) diff --git a/src/window/desktop/widget.py b/src/window/desktop/widget.py index 93732db..46f8051 100644 --- a/src/window/desktop/widget.py +++ b/src/window/desktop/widget.py @@ -15,6 +15,7 @@ from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay from fabric.widgets.wayland import WaylandWindow as Window +from services.config import on_config_change from shared.data import load_config from utils.gtk_utils import svg_file from window.desktop.constants import ( @@ -93,6 +94,16 @@ class WeatherService(Service): # connecting (see Weather.__init__) to avoid a race with subscription. GLib.timeout_add_seconds(WEATHER_UPDATE_INTERVAL, self._refresh) + on_config_change(self._on_config_change) + + def _on_config_change(self, new_config, old_config): + if new_config.get("weather_location") != old_config.get("weather_location"): + self._weather = None + self._location = None + self._coords = None + self._coords_location = None + self._executor.submit(self._fetch) + # -- scheduling ------------------------------------------------------- def refresh(self) -> None: diff --git a/src/window/dock/canvas.py b/src/window/dock/canvas.py index 24358d0..60993d6 100644 --- a/src/window/dock/canvas.py +++ b/src/window/dock/canvas.py @@ -132,7 +132,8 @@ class DockCanvas(Gtk.DrawingArea): if not self._desktop_apps: try: self._desktop_apps = get_desktop_applications(include_hidden=False) - except Exception: + except Exception as e: + logger.warning(f"[dock] Failed to load desktop applications: {e}") self._desktop_apps = [] return self._desktop_apps diff --git a/src/window/globalmenu/dbusmenu.py b/src/window/globalmenu/dbusmenu.py index 35ff255..65f4c60 100644 --- a/src/window/globalmenu/dbusmenu.py +++ b/src/window/globalmenu/dbusmenu.py @@ -123,10 +123,9 @@ class DBusMenuClient: return None def get_layout(self, force_refresh=False): - if self._cache and self._cache_valid and not force_refresh: - return self._cache - with self._lock: + if self._cache and self._cache_valid and not force_refresh: + return self._cache if self._fetching: return self._cache or [] self._fetching = True @@ -261,6 +260,10 @@ class DBusMenuClient: return False def click_item(self, item_id, pid=0): + logger.debug( + f"[dbusmenu] click_item: item_id={item_id}, " + f"service={self.service_name}, path={self.object_path}" + ) try: self._call( "Event", @@ -269,7 +272,7 @@ class DBusMenuClient: self.invalidate() except Exception as e: logger.warning( - f"[dbusmenu] self._call( 'Event', GLib.Variant('(isvu)', (item_id, 'cl... failed: {e}" + f"[dbusmenu] click_item: Event call failed for item_id={item_id}: {e}" ) def invalidate(self): @@ -369,9 +372,10 @@ class DBusMenuClient: layout = layout.get_variant() parsed = self._parse_dbusmenu(layout).children if parent_id == 0: - self._hash_cache = self._hash(parsed) - self._cache = parsed - self._cache_valid = True + with self._lock: + self._hash_cache = self._hash(parsed) + self._cache = parsed + self._cache_valid = True return parsed except Exception as e: logger.error(f"[Menu] update_layout error: {e}") @@ -403,5 +407,6 @@ class DBusMenuClient: result.append(item) return result - self._cache = walk(self._cache) - self._hash_cache = None + with self._lock: + self._cache = walk(self._cache) + self._hash_cache = None diff --git a/src/window/globalmenu/service.py b/src/window/globalmenu/service.py index b0b6b7d..ef6c7c0 100644 --- a/src/window/globalmenu/service.py +++ b/src/window/globalmenu/service.py @@ -372,9 +372,26 @@ class GlobalMenuService(Service): """Return the registry entry and PID for the currently active window.""" with self._state_lock: pid = self._current_pid + if pid == 0: + logger.warning( + "[GlobalMenuService] _resolve_current_entry: _current_pid is 0" + ) + return None, 0 with self._registry_lock: svc = self._pid_to_service.get(pid) entry = self._service_registry.get(svc) if svc else None + if not svc: + logger.warning( + f"[GlobalMenuService] _resolve_current_entry: no service for pid {pid}" + ) + elif not entry: + logger.warning( + f"[GlobalMenuService] _resolve_current_entry: no entry for service '{svc}' (pid {pid})" + ) + elif not entry.importer: + logger.warning( + f"[GlobalMenuService] _resolve_current_entry: importer is None for '{svc}' (pid {pid})" + ) return entry, pid def _register_new_service( @@ -510,7 +527,7 @@ class GlobalMenuService(Service): entry = self._service_registry.get(svc_name) if not entry or not entry.importer: return - entry.revision = revision + entry.revision = revision subtree = entry.importer.update_layout(parent_id, revision) if subtree is None: return @@ -1066,8 +1083,15 @@ class GlobalMenuService(Service): def click_item(self, item_id: int) -> bool: entry, pid = self._resolve_current_entry() if entry and entry.importer: + logger.debug( + f"[GlobalMenuService] click_item: item_id={item_id}, " + f"service={entry.service_name}, importer={type(entry.importer).__name__}" + ) entry.importer.click_item(item_id, pid=pid) return True + logger.warning( + f"[GlobalMenuService] click_item FAILED: item_id={item_id}, entry={entry}, pid={pid}" + ) return False def about_to_show(self, item_id: int) -> bool: diff --git a/src/window/lock.py b/src/window/lock.py index 18bbe4b..81474de 100644 --- a/src/window/lock.py +++ b/src/window/lock.py @@ -2,7 +2,14 @@ import getpass import pam from fabric import Application -from fabric.utils import Gdk, GLib, get_relative_path, logger, os +from fabric.utils import ( + Gdk, + GLib, + exec_shell_command_async, + get_relative_path, + logger, + os, +) from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox from fabric.widgets.datetime import DateTime @@ -243,7 +250,16 @@ class LockScreen(Window): return self.lock.unlock_and_destroy() self.destroy() - GLib.idle_add(app.quit) # schedules quit after unlock + _app = globals().get("app") + if _app is not None: + GLib.idle_add(_app.quit) + + +class LockScreenWrapper: + """Thin wrapper exposing lock.screen() for fabric-cli exec.""" + + def lock(self): + exec_shell_command_async(f"python3 {get_relative_path('../../start.py')} lock") def initialize(): diff --git a/src/window/osd/components/base.py b/src/window/osd/components/base.py index aef08fe..e674e97 100644 --- a/src/window/osd/components/base.py +++ b/src/window/osd/components/base.py @@ -16,6 +16,7 @@ class BaseOSDContainer(Box): self.osd = None self.show_timestamp: float = 0.0 self.watchdog_handler: int = 0 + self._hide_timer_id: int | None = None self._update_in_progress = False self._is_hovered = False self._last_trigger_time: float = 0.0 @@ -31,6 +32,7 @@ class BaseOSDContainer(Box): return False def destroy(self): + self._cancel_hide_timer() self.cleanup_all_handlers() super().destroy() @@ -67,13 +69,27 @@ class BaseOSDContainer(Box): self.show_timestamp = 0.0 self._update_in_progress = False + def _cancel_hide_timer(self): + if self._hide_timer_id is not None: + try: + GLib.source_remove(self._hide_timer_id) + except Exception: + pass + self._hide_timer_id = None + def hide_window(self): + self._cancel_hide_timer() 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) + self._hide_timer_id = GLib.timeout_add(150, self._on_hide_timer_done) else: self.window.hide() + def _on_hide_timer_done(self): + self._hide_timer_id = None + self.window.hide() + return False + def watchdog_force_hide(self, *_): if not self.window.get_visible(): self.cleanup_all_handlers() diff --git a/src/window/panel/components/globalmenu.py b/src/window/panel/components/globalmenu.py index 8bdc637..da51101 100644 --- a/src/window/panel/components/globalmenu.py +++ b/src/window/panel/components/globalmenu.py @@ -153,7 +153,11 @@ class SystemDropdown(ModusDropdown): dropdown_option("Restart", "", "systemctl reboot"), dropdown_option("Shut Down", "", "shutdown now"), dropdown_divider("---------------------"), - dropdown_option("Lock Screen", "󰘳 L", "hyprlock"), + dropdown_option( + "Lock Screen", + "󰘳 L", + 'fabric-cli exec modus "lock_screen.lock()"', + ), ], **kwargs, ) diff --git a/src/window/settings/main.py b/src/window/settings/main.py index 59433aa..eeb7757 100644 --- a/src/window/settings/main.py +++ b/src/window/settings/main.py @@ -334,6 +334,11 @@ class SettingsWindow(Gtk.Window): return SettingsPage( "General Settings", [ + SettingsRow( + "Debug Mode", + SettingsSwitch("debug"), + "Enable verbose logging for development", + ), SettingsRow( "Weather Location", SettingsEntry("weather_location"), diff --git a/src/window/spotlight/core/search.py b/src/window/spotlight/core/search.py index fb135d0..46c74c2 100644 --- a/src/window/spotlight/core/search.py +++ b/src/window/spotlight/core/search.py @@ -78,7 +78,10 @@ class SearchPipeline: t.join(timeout=0.1) self._pending_threads.clear() for source_id in self._pending_idle_ids: - GLib.source_remove(source_id) + try: + GLib.source_remove(source_id) + except Exception: + pass self._pending_idle_ids.clear() def _route(self, query: str) -> tuple[PluginEntry | None, str, str]: diff --git a/src/window/spotlight/plugins/power_menu.py b/src/window/spotlight/plugins/power_menu.py index fe959cd..8ce84e7 100644 --- a/src/window/spotlight/plugins/power_menu.py +++ b/src/window/spotlight/plugins/power_menu.py @@ -53,7 +53,7 @@ class PowerMenuPlugin(SpotlightPlugin): "id": "lock", "name": "Lock Screen", "icon": "system-lock-screen", - "command": "hyprlock", + "command": 'fabric-cli exec modus "lock_screen.lock()"', "description": "Lock the screen", }, ]