diff --git a/config/hypr/modus.lua b/config/hypr/modus.lua index b245154..d049b74 100644 --- a/config/hypr/modus.lua +++ b/config/hypr/modus.lua @@ -21,22 +21,34 @@ end) -- Reload Modus hl.bind("ALT + SHIFT + R", hl.dsp.exec_cmd("killall modus; cd " .. modus .. " && uwsm app -- uv run start")) --- Fabric Spotlight Binds +-- Fabric Binds for key, method in pairs({ - ["SUPER + D"] = "spotlight.toggle()", -- Spotlight - ["SUPER + E"] = "spotlight.toggle('em')", -- Emoji - ["SUPER + V"] = "spotlight.toggle('clip')", -- Clipboard - ["SUPER + W"] = "spotlight.toggle('wall')", -- Wallpaper ["SUPER + SHIFT + Y"] = "app.set_css()", -- Reload CSS ["ALT + TAB"] = "switcher.show_switcher()", -- Application Switcher ["SUPER + Z"] = "screencapture.toggle()", -- ScreenCapture ["SUPER + S"] = "screencapture.toggle(ss='region')", -- Screenshot Region ["ALT + SPACE"] = "switch_keyboard_layout()", -- KB_Layout Switcher - ["ALT + SHIFT + W"] = "spotlight.toggle('wr', external=True)", -- Random Wallpaper }) do hl.bind(key, hl.dsp.exec_cmd(fabricSend .. ' "' .. method .. '"')) end +-- Spotlight Binds (spawns the process; it exits when closed to free memory. +-- A second press while open forwards a toggle to the running instance.) +local spotlightCmd = "cd " .. modus .. " && uv run python start.py spotlight" +for key, args in pairs({ + ["SUPER + D"] = "", -- Spotlight + ["SUPER + E"] = "em", -- Emoji + ["SUPER + V"] = "clip", -- Clipboard + ["SUPER + W"] = "wall", -- Wallpaper + ["ALT + SHIFT + W"] = "--external wr", -- Random Wallpaper +}) do + local cmd = spotlightCmd + if args ~= "" then + cmd = cmd .. " " .. args + end + hl.bind(key, hl.dsp.exec_cmd(cmd)) +end + -- Layer Rules hl.layer_rule({ match = { diff --git a/src/globalmenu/dbusmenu.py b/src/globalmenu/dbusmenu.py index ed97a0a..35ff255 100644 --- a/src/globalmenu/dbusmenu.py +++ b/src/globalmenu/dbusmenu.py @@ -116,7 +116,10 @@ class DBusMenuClient: 3000, None, ) - except Exception: + except Exception as e: + logger.warning( + f"[dbusmenu] return _get_bus().call_sync( self.service_name, self.obje... failed: {e}" + ) return None def get_layout(self, force_refresh=False): @@ -231,11 +234,15 @@ class DBusMenuClient: if item.has_submenu and not item.children: try: self.about_to_show(item.id) - except Exception: - pass + except Exception as e: + logger.warning( + f"[dbusmenu] self.about_to_show(item.id) failed: {e}" + ) - except Exception: - pass + except Exception as e: + logger.warning( + f"[dbusmenu] if node.n_children() != 3: return item failed: {e}" + ) return item @@ -247,7 +254,10 @@ class DBusMenuClient: GLib.VariantType("(b)"), ) return res.get_child_value(0).get_boolean() if res else False - except Exception: + except Exception as e: + logger.warning( + f"[dbusmenu] res = self._call( 'AboutToShow', GLib.Variant('(i)', (ite... failed: {e}" + ) return False def click_item(self, item_id, pid=0): @@ -257,8 +267,10 @@ class DBusMenuClient: GLib.Variant("(isvu)", (item_id, "clicked", GLib.Variant("s", ""), 0)), ) self.invalidate() - except Exception: - pass + except Exception as e: + logger.warning( + f"[dbusmenu] self._call( 'Event', GLib.Variant('(isvu)', (item_id, 'cl... failed: {e}" + ) def invalidate(self): self._cache_valid = False @@ -339,8 +351,8 @@ class DBusMenuClient: for sid in self._signal_ids: try: bus.signal_unsubscribe(sid) - except Exception: - pass + except Exception as e: + logger.warning(f"[dbusmenu] bus.signal_unsubscribe(sid) failed: {e}") self._signal_ids.clear() def update_layout( diff --git a/src/globalmenu/detection.py b/src/globalmenu/detection.py index 1b526e0..9ad3d8b 100644 --- a/src/globalmenu/detection.py +++ b/src/globalmenu/detection.py @@ -57,7 +57,10 @@ def _parse_dynamic(path: Path) -> tuple[set[str], list[Path]]: return set(), [] is_64 = ident[4] == 2 data = path.read_bytes() - except OSError: + except OSError as e: + logger.warning( + f"[detection] with open(path, 'rb') as f: ident = f.read(20) failed: {e}" + ) return set(), [] try: @@ -69,7 +72,10 @@ def _parse_dynamic(path: Path) -> tuple[set[str], list[Path]]: e_phoff = int.from_bytes(data[0x1C:0x20], "little") e_phentsize = int.from_bytes(data[0x2A:0x2C], "little") e_phnum = int.from_bytes(data[0x2C:0x2E], "little") - except (IndexError, ValueError): + except (IndexError, ValueError) as e: + logger.warning( + f"[detection] if is_64: e_phoff = int.from_bytes(data[0x20:0x28], 'litt... failed: {e}" + ) return set(), [] PT_DYNAMIC = 2 @@ -175,7 +181,10 @@ def _transitive_needed(root: Path) -> set[str]: def _load_cache() -> dict: try: return json.loads(_CACHE_FILE.read_text()) - except (OSError, json.JSONDecodeError): + except (OSError, json.JSONDecodeError) as e: + logger.warning( + f"[detection] return json.loads(_CACHE_FILE.read_text()) failed: {e}" + ) return {} @@ -211,7 +220,8 @@ def executable_gtk_class(executable: str | None) -> str | None: try: mtime = path.stat().st_mtime_ns - except OSError: + except OSError as e: + logger.warning(f"[detection] mtime = path.stat().st_mtime_ns failed: {e}") return None cache = _load_cache() diff --git a/src/globalmenu/environment.py b/src/globalmenu/environment.py index 28273d0..09d666a 100644 --- a/src/globalmenu/environment.py +++ b/src/globalmenu/environment.py @@ -65,8 +65,8 @@ def _remove_global_ld_preload(): out.append(ln) rc.write_text("".join(out)) logger.info(f"[GlobalMenu] Removed stale LD_PRELOAD block from {rc}") - except OSError: - pass + except OSError as e: + logger.warning(f"[environment] if not rc.is_file(): continue failed: {e}") # Strip the LD_PRELOAD line from environment.d/appmenu.conf. env_path = home / ".config" / "environment.d" / "appmenu.conf" @@ -78,8 +78,10 @@ def _remove_global_ld_preload(): if not ln.strip().startswith("LD_PRELOAD=") ] env_path.write_text("".join(kept)) - except OSError: - pass + except OSError as e: + logger.warning( + f"[environment] if env_path.is_file(): kept = [ ln for ln in env_path.rea... failed: {e}" + ) # Clear it from the activation environment for future launches. exec_shell_command("dbus-update-activation-environment --systemd LD_PRELOAD=") @@ -151,8 +153,10 @@ def setup_global_menu_environment(): with open(env_path, "w") as f: for k, v in ENV_VARS.items(): f.write(f"{k}={v}\n") - except OSError: - pass + except OSError as e: + logger.warning( + f"[environment] env_dir.mkdir(parents=True, exist_ok=True) failed: {e}" + ) settings_path = Path.home() / ".config" / "gtk-3.0" / "settings.ini" try: @@ -165,8 +169,10 @@ def setup_global_menu_environment(): if content and not content.endswith("\n"): f.write("\n") f.write("gtk-shell-shows-menubar=1\n") - except OSError: - pass + except OSError as e: + logger.warning( + f"[environment] settings_path.parent.mkdir(parents=True, exist_ok=True) failed: {e}" + ) pam_path = Path.home() / ".pam_environment" try: @@ -179,8 +185,8 @@ def setup_global_menu_environment(): if not any(ln.startswith(f"{k} ") for ln in lines): with open(pam_path, "a") as f: f.write(line) - except OSError: - pass + except OSError as e: + logger.warning(f"[environment] existing = '' failed: {e}") logger.info("[GlobalMenu] GTK environment injected") except Exception as e: diff --git a/src/globalmenu/gtkmenu.py b/src/globalmenu/gtkmenu.py index f892f47..41cd3b6 100644 --- a/src/globalmenu/gtkmenu.py +++ b/src/globalmenu/gtkmenu.py @@ -17,7 +17,8 @@ def atspi_activate_menu_item(pid: int, label: str) -> bool: gi.require_version("Atspi", "2.0") from gi.repository import Atspi - except (ImportError, ValueError): + except (ImportError, ValueError) as e: + logger.warning(f"[gtkmenu] import gi failed: {e}") return False try: @@ -28,7 +29,8 @@ def atspi_activate_menu_item(pid: int, label: str) -> bool: continue try: app_pid = app.get_process_id() - except Exception: + except Exception as e: + logger.warning(f"[gtkmenu] app_pid = app.get_process_id() failed: {e}") continue if app_pid != pid: continue @@ -43,8 +45,10 @@ def atspi_activate_menu_item(pid: int, label: str) -> bool: result = _find_menubar(obj.get_child_at_index(j), depth + 1) if result: return result - except Exception: - pass + except Exception as e: + logger.warning( + f"[gtkmenu] if obj.get_role_name() == 'menu bar': return obj failed: {e}" + ) return None def _find_frame(obj, depth=0): @@ -57,8 +61,10 @@ def atspi_activate_menu_item(pid: int, label: str) -> bool: result = _find_frame(obj.get_child_at_index(j), depth + 1) if result: return result - except Exception: - pass + except Exception as e: + logger.warning( + f"[gtkmenu] if obj.get_role_name() == 'frame': return obj failed: {e}" + ) return None frame = _find_frame(app) @@ -75,7 +81,10 @@ def atspi_activate_menu_item(pid: int, label: str) -> bool: continue try: item_name = (item.get_name() or "").lower().strip() - except Exception: + except Exception as e: + logger.warning( + f"[gtkmenu] item_name = (item.get_name() or '').lower().strip() failed: {e}" + ) continue if item_name == label_lower or label_lower in item_name: try: @@ -384,7 +393,10 @@ class GtkMenuClient: if pid and item_label: atspi_activate_menu_item(pid, item_label) return - except Exception: + except Exception as e: + logger.warning( + f"[gtkmenu] if ( use_change_state and action_info and action_info.sta... failed: {e}" + ) continue except Exception as e: logger.error(f"[GtkMenuClient] click failed: {e}") @@ -398,8 +410,10 @@ class GtkMenuClient: if action_target is not None: try: return GLib.Variant("av", [action_target]) - except Exception: - pass + except Exception as e: + logger.warning( + f"[gtkmenu] return GLib.Variant('av', [action_target]) failed: {e}" + ) if action_info and action_info.parameter_type: param_type_str = action_info.parameter_type if param_type_str and param_type_str != "()": @@ -407,8 +421,10 @@ class GtkMenuClient: param_type = GLib.VariantType(param_type_str) default_val = GLib.Variant.new_tuple(GLib.Variant(param_type, None)) return GLib.Variant("av", [default_val]) - except Exception: - pass + except Exception as e: + logger.warning( + f"[gtkmenu] param_type = GLib.VariantType(param_type_str) failed: {e}" + ) return GLib.Variant("av", []) @@ -443,7 +459,10 @@ class ActionMenuClient: 3000, None, ) - except Exception: + except Exception as e: + logger.warning( + f"[gtkmenu] return _get_bus().call_sync( self.service_name, path or s... failed: {e}" + ) return None def _refresh_action_cache(self): @@ -723,8 +742,10 @@ class ActionMenuClient: if action_target is not None: try: return GLib.Variant("av", [action_target]) - except Exception: - pass + except Exception as e: + logger.warning( + f"[gtkmenu] return GLib.Variant('av', [action_target]) failed: {e}" + ) if action_info and action_info.parameter_type: param_type_str = action_info.parameter_type if param_type_str and param_type_str != "()": @@ -732,6 +753,8 @@ class ActionMenuClient: param_type = GLib.VariantType(param_type_str) default_val = GLib.Variant.new_tuple(GLib.Variant(param_type, None)) return GLib.Variant("av", [default_val]) - except Exception: - pass + except Exception as e: + logger.warning( + f"[gtkmenu] param_type = GLib.VariantType(param_type_str) failed: {e}" + ) return GLib.Variant("av", []) diff --git a/src/globalmenu/service.py b/src/globalmenu/service.py index 024bd4d..cd9f429 100644 --- a/src/globalmenu/service.py +++ b/src/globalmenu/service.py @@ -86,7 +86,10 @@ class AppServiceInfo: def _resolve_executable(pid: int) -> str: try: return os.path.realpath(f"/proc/{pid}/exe") - except Exception: + except Exception as e: + logger.warning( + f"[service] return os.path.realpath(f'/proc/(pid)/exe') failed: {e}" + ) return "" @@ -106,7 +109,10 @@ def _dbus_introspect(bus, service: str, path: str, timeout: int) -> str: None, ) return res.get_child_value(0).get_string() if res else "" - except Exception: + except Exception as e: + logger.warning( + f"[service] res = bus.call_sync( service, path, 'org.freedesktop.DBus... failed: {e}" + ) return "" @@ -124,7 +130,10 @@ def _dbus_get_pid(bus, service: str, timeout: int) -> int: None, ) return res.get_child_value(0).get_uint32() if res else 0 - except Exception: + except Exception as e: + logger.warning( + f"[service] res = bus.call_sync( 'org.freedesktop.DBus', '/org/freede... failed: {e}" + ) return 0 @@ -222,7 +231,10 @@ class GlobalMenuService(Service): ): try: name, old_owner, new_owner = parameters.unpack() - except Exception: + except Exception as e: + logger.warning( + f"[service] name, old_owner, new_owner = parameters.unpack() failed: {e}" + ) return if not name or name.startswith(":") or name == "org.freedesktop.DBus": @@ -265,8 +277,10 @@ class GlobalMenuService(Service): if isinstance(entry.importer, DBusMenuClient): try: entry.importer.disconnect_signals() - except Exception: - pass + except Exception as e: + logger.warning( + f"[service] entry.importer.disconnect_signals() failed: {e}" + ) self._introspection_cache.invalidate(service_name) @@ -385,8 +399,10 @@ class GlobalMenuService(Service): if out: data = json.loads(out) target_pid = data.get("pid", 0) - except Exception: - pass + except Exception as e: + logger.warning( + f"[service] out = exec_shell_command('hyprctl activewindow -j') failed: {e}" + ) with self._state_lock: same_class = wm_class == self._current_wm_class @@ -672,7 +688,10 @@ class GlobalMenuService(Service): if not res: return None names = res.get_child_value(0).unpack() - except Exception: + except Exception as e: + logger.warning( + f"[service] res = bus.call_sync( 'org.freedesktop.DBus', '/org/freede... failed: {e}" + ) return None services = [ @@ -745,8 +764,10 @@ class GlobalMenuService(Service): gsvc, gpath, pid, wm_class, test_client ) return test_client - except Exception: - pass + except Exception as e: + logger.warning( + f"[service] test_client = GtkMenuClient(gsvc, gpath) failed: {e}" + ) action_candidates = self._build_action_candidates( svc, _safe_cls, _safe_cap @@ -773,7 +794,10 @@ class GlobalMenuService(Service): logger.info( f"[GlobalMenuService] No menu found for {svc} wm={wm_class}" ) - except Exception: + except Exception as e: + logger.warning( + f"[service] pid = _dbus_get_pid(bus, svc, _DBUS_TIMEOUT_PID) failed: {e}" + ) continue return None @@ -942,8 +966,10 @@ class GlobalMenuService(Service): _node_count, ) ) - except Exception: - pass + except Exception as e: + logger.warning( + f"[service] xml = self._introspection_cache.get( service, path, _get_... failed: {e}" + ) return results def _is_same_app(self, pid1: int, pid2: int) -> bool: @@ -961,21 +987,28 @@ class GlobalMenuService(Service): comm2 = f.read().strip() if comm1 and comm2 and comm1 == comm2: return True - except OSError: - pass + except OSError as e: + logger.warning( + f"[service] with open(f'/proc/(pid1)/comm') as f: comm1 = f.read().st... failed: {e}" + ) def _ppid(p: int) -> int: try: with open(f"/proc/{p}/stat") as f: parts = f.read().split() return int(parts[3]) if len(parts) > 4 else 0 - except Exception: + except Exception as e: + logger.warning( + f"[service] with open(f'/proc/(p)/stat') as f: parts = f.read().split... failed: {e}" + ) return 0 if _ppid(pid1) == pid2 or _ppid(pid2) == pid1: return True - except (OSError, PermissionError, FileNotFoundError): - pass + except (OSError, PermissionError, FileNotFoundError) as e: + logger.warning( + f"[service] exe1 = os.path.realpath(f'/proc/(pid1)/exe') failed: {e}" + ) return False def _extract_menu( diff --git a/src/main.py b/src/main.py index 4d85773..9934001 100644 --- a/src/main.py +++ b/src/main.py @@ -94,8 +94,8 @@ def main(): for m in css_monitors: try: m.cancel() - except Exception: - pass + except Exception as e: + logger.warning(f"[main] m.cancel() failed: {e}") atexit.register(cleanup_css_monitors) diff --git a/src/services/bluetooth.py b/src/services/bluetooth.py index 9d5a424..d86f101 100644 --- a/src/services/bluetooth.py +++ b/src/services/bluetooth.py @@ -135,9 +135,11 @@ class BluetoothDevice(Service): try: self._battery_proxy = _make_proxy(bus, object_path, BLUEZ_BATTERY_IFACE) - except Exception: + except Exception as e: # Battery1 is optional — not all devices expose it - pass + logger.warning( + f"[bluetooth] self._battery_proxy = _make_proxy(bus, object_path, BLUEZ... failed: {e}" + ) # Subscribe to Device1 PropertiesChanged self._prop_sub_id = bus.signal_subscribe( @@ -190,7 +192,10 @@ class BluetoothDevice(Service): try: v = self._battery_proxy.get_cached_property(name) return v.unpack() if v else None - except Exception: + except Exception as e: + logger.warning( + f"[bluetooth] v = self._battery_proxy.get_cached_property(name) failed: {e}" + ) return None def _on_properties_changed( @@ -466,8 +471,8 @@ class BluetoothAdapter(Service): if handler_id is not None: try: device.disconnect(handler_id) - except Exception: - pass + except Exception as e: + logger.warning(f"[bluetooth] device.disconnect(handler_id) failed: {e}") self.emit("device-removed", addr) if device.connected: diff --git a/src/services/brightness.py b/src/services/brightness.py index a7c5a2d..7825320 100644 --- a/src/services/brightness.py +++ b/src/services/brightness.py @@ -140,8 +140,10 @@ class Brightness(Service): self._brightness_path = base_path / "brightness" self._max_brightness_path = base_path / "max_brightness" return self._screen_device - except Exception: - pass + except Exception as e: + logger.warning( + f"[brightness] devices = os.listdir('/sys/class/backlight') failed: {e}" + ) return "" def _detect_ddcutil_bus(self): @@ -152,7 +154,10 @@ class Brightness(Service): match = re.search(r"I2C bus:\s*/dev/i2c-(\d+)", process.stdout) return int(match.group(1)) if match else -1 return -1 - except Exception: + except Exception as e: + logger.warning( + f"[brightness] process = run_command(['ddcutil', 'detect'], timeout=2) failed: {e}" + ) return -1 def _read_max_brightness(self): @@ -185,7 +190,10 @@ class Brightness(Service): try: if self._max_brightness_path and self._max_brightness_path.exists(): return int(self._max_brightness_path.read_text().strip()) - except Exception: + except Exception as e: + logger.warning( + f"[brightness] if self._max_brightness_path and self._max_brightness_pat... failed: {e}" + ) return None return None diff --git a/src/services/capslock.py b/src/services/capslock.py index da7f08e..90fb6af 100644 --- a/src/services/capslock.py +++ b/src/services/capslock.py @@ -64,7 +64,10 @@ class CapsLock(Service): if brightness_path.exists(): try: return bool(int(brightness_path.read_text().strip())) - except (ValueError, OSError): + except (ValueError, OSError) as e: + logger.warning( + f"[capslock] return bool(int(brightness_path.read_text().strip())) failed: {e}" + ) return False return False diff --git a/src/services/custom_notification.py b/src/services/custom_notification.py index b90496c..f88d250 100644 --- a/src/services/custom_notification.py +++ b/src/services/custom_notification.py @@ -94,10 +94,14 @@ class CachedNotification(Service): self.image_file ) return self._cached_image_pixbuf - except Exception: - pass - except Exception: - pass + except Exception as e: + logger.warning( + f"[custom_notification] self._cached_image_pixbuf = GdkPixbuf.Pixbuf.new_from_fil... failed: {e}" + ) + except Exception as e: + logger.warning( + f"[custom_notification] if self.image_pixmap: self._cached_image_pixbuf = self.im... failed: {e}" + ) self._cached_image_pixbuf = None return None @@ -136,12 +140,16 @@ class CachedNotification(Service): notification_image_cache_key = ( get_notification_image_cache_key(self.id, image_pixbuf) ) - except Exception: + except Exception as e: # If temp file is gone or any other error, just mark as None - pass - except Exception: + logger.warning( + f"[custom_notification] image_pixbuf = getattr(self._notification, 'image_pixbuf'... failed: {e}" + ) + except Exception as e: # If any error occurs during cache key generation, skip it - pass + logger.warning( + f"[custom_notification] if hasattr(self, 'cache_metadata') and self.cache_metadat... failed: {e}" + ) return { "cached-id": self.cache_id, diff --git a/src/services/keyboard_layout.py b/src/services/keyboard_layout.py index 5e6c5e0..c0221d7 100644 --- a/src/services/keyboard_layout.py +++ b/src/services/keyboard_layout.py @@ -1,17 +1,19 @@ -from pathlib import Path +import json -import tomlkit from fabric.core.service import Property, Service, Signal +from fabric.hyprland.service import Hyprland from fabric.utils import Gio, GLib, logger -import shared.data as data -from services.config import on_config_change +from services.config import get_config, on_config_change HYPRCTL_BIN = "hyprctl" +DEFAULT_LAYOUTS = ["us", "np"] class KeyboardLayout(Service): - """Service to manage keyboard layout switching and monitoring via Hyprland.""" + """In-memory service that treats Hyprland as the source of truth for the + active keyboard layout. All runtime state lives in memory; Hyprland is + queried/synced via hyprctl and kept in sync through its event socket.""" _instance = None @@ -28,7 +30,7 @@ class KeyboardLayout(Service): @Signal def layout_changed(self, layout: str) -> None: - """Signal emitted when keyboard layout changes.""" + """Signal emitted when the active keyboard layout changes.""" def __init__(self, **kwargs): if getattr(self, "_initialized", False): @@ -36,143 +38,198 @@ class KeyboardLayout(Service): super().__init__(**kwargs) self._initialized = True - self.layout_state_file = Path(data.CACHE_DIR) / "kb_layout.toml" - self.layout_file = Path(data.CACHE_DIR) / "kb_layout_current.txt" - self._last_layout = None - self.layouts = [] - self.current_index = 0 - self._reload_pending = False + self._layouts: list[str] = [] + self._current_index: int = 0 + self._current_layout: str | None = None + self._sync_pending: bool = False self._init_layout_config() - self._start_file_monitor() - + self._connect_hyprland_events() on_config_change(self._on_config_change) - def _init_layout_config(self): - config_layouts = data.load_config().get("keyboard_layouts", ["us", "np"]) - state = self._read_state() + # ── hyprctl wrapper ──────────────────────────────────────────────────────── - if state: - self.layouts = state.get("layouts", config_layouts) - self.current_index = state.get("current_index", 0) - else: - self.layouts = config_layouts - self.current_index = 0 - self._save_state() + def _hyprctl(self, *args, on_reply=None): + """Execute a hyprctl command via Gio.Subprocess. + + Every hyprctl invocation goes through here. The call is asynchronous and + never blocks the UI. ``on_reply`` (optional) receives the stripped stdout + string, or ``None`` on failure. + """ + argv = [HYPRCTL_BIN, *[str(arg) for arg in args]] + try: + proc = Gio.Subprocess.new( + argv, + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_SILENCE, + ) + except GLib.Error as error: + logger.error(f"[KeyboardLayout] Failed to run {argv}: {error}") + if on_reply is not None: + on_reply(None) + return + + def on_done(process: Gio.Subprocess, task: Gio.Task): + try: + _, stdout, _ = process.communicate_utf8_finish(task) + except GLib.Error: + stdout = None + if on_reply is not None: + on_reply(stdout.strip() if stdout else None) + + proc.communicate_utf8_async(None, None, on_done) + + # ── initialization ───────────────────────────────────────────────────────── + + def _init_layout_config(self): + self._layouts = self._load_layouts_from_config() + + if not self._layouts: + logger.warning("[KeyboardLayout] No keyboard layouts configured.") + return self._apply_layouts_to_hyprland() - self._apply_current_layout_index() + self._sync_from_hyprland() - def _read_state(self): - if not self.layout_state_file.exists(): - return None - try: - with open(self.layout_state_file) as f: - data = tomlkit.load(f) - return dict(data) if data else None - except Exception as e: - logger.error(f"[KeyboardLayout] Failed to read state: {e}") - return None + if self._current_layout is None: + self._set_current(0, emit=False) - def _save_state(self): - try: - self.layout_state_file.parent.mkdir(parents=True, exist_ok=True) - doc = tomlkit.document() - arr = tomlkit.array() - for layout in self.layouts: - arr.append(layout) - doc["layouts"] = arr - doc["current_index"] = self.current_index - with open(self.layout_state_file, "w") as f: - tomlkit.dump(doc, f) - except Exception as e: - logger.error(f"[KeyboardLayout] Failed to save state: {e}") + def _load_layouts_from_config(self) -> list[str]: + layouts = get_config("keyboard_layouts", DEFAULT_LAYOUTS) + if not isinstance(layouts, list) or not layouts: + return list(DEFAULT_LAYOUTS) + return [str(layout).strip() for layout in layouts if str(layout).strip()] - def _on_config_change(self, new_config, old_config): - new_layouts = new_config.get("keyboard_layouts") - if new_layouts and new_layouts != self.layouts: - self.layouts = new_layouts - self._save_state() - self._apply_layouts_to_hyprland() - logger.info(f"[KeyboardLayout] Layouts updated from config: {self.layouts}") + # ── Hyprland synchronization ─────────────────────────────────────────────── - def _start_file_monitor(self): - if not self.layout_state_file.exists(): + def _sync_from_hyprland(self): + """Query Hyprland for the current layout and update in-memory state. + + Prefers the ``active_layout_index`` reported by ``hyprctl -j devices``, + falling back to index 0 when the information is unavailable. + """ + if self._sync_pending: return + self._sync_pending = True + + def on_reply(stdout: str | None): + self._sync_pending = False + if not stdout: + return + try: + devices = json.loads(stdout) + except json.JSONDecodeError as error: + logger.error(f"[KeyboardLayout] Failed to parse devices: {error}") + return + + keyboard = self._pick_keyboard(devices.get("keyboards", [])) + if keyboard is None: + return + + index = keyboard.get("active_layout_index", 0) + self._set_current(index, emit=True) + + self._hyprctl("-j", "devices", on_reply=on_reply) + + @staticmethod + def _pick_keyboard(keyboards: list[dict]) -> dict | None: + if not keyboards: + return None + for kb in keyboards: + if kb.get("main"): + return kb + return keyboards[0] + + def _connect_hyprland_events(self): try: - gio_file = Gio.File.new_for_path(str(self.layout_state_file)) - self._file_monitor = gio_file.monitor_file(Gio.FileMonitorFlags.NONE, None) - self._file_monitor.connect("changed", self._on_file_changed) - except Exception as e: - logger.error(f"[KeyboardLayout] Failed to start file monitor: {e}") + Hyprland().connect("event::activelayout", self._on_hyprland_layout_event) + except Exception as error: + logger.warning( + f"[KeyboardLayout] Could not subscribe to Hyprland layout " + f"events (external changes won't be tracked): {error}" + ) - def _on_file_changed(self, monitor, file, *args): - if not self._reload_pending: - self._reload_pending = True - GLib.timeout_add(50, self._reload_layout) + def _on_hyprland_layout_event(self, _hyprland, _event): + self._sync_from_hyprland() - def _reload_layout(self): - self._reload_pending = False - new_layout = self._read_layout() - if new_layout != self._last_layout: - self._last_layout = new_layout - self.emit("layout_changed", new_layout) - return False + # ── state management ─────────────────────────────────────────────────────── - def _read_layout(self): - state = self._read_state() - if state: - layouts = state.get("layouts", []) - current_index = state.get("current_index", 0) - if layouts and 0 <= current_index < len(layouts): - return layouts[current_index] - return "us" + def _set_current(self, index: int, emit: bool) -> bool: + if not self._layouts: + return False - def _write_layout(self, layout: str): - try: - self.layout_file.parent.mkdir(parents=True, exist_ok=True) - self.layout_file.write_text(layout, encoding="utf-8") - except Exception as e: - logger.error(f"[KeyboardLayout] Failed writing layout state file: {e}") + index = max(0, min(index, len(self._layouts) - 1)) + layout = self._layouts[index] + + if index == self._current_index and layout == self._current_layout: + return False + + self._current_index = index + self._current_layout = layout + self.notify("current_layout", "current_index", "layouts") + + if emit: + self.emit("layout_changed", layout) + return True + + # ── Hyprland writes ──────────────────────────────────────────────────────── def _apply_layouts_to_hyprland(self): - if not self.layouts: + if not self._layouts: return - layouts_str = ",".join(self.layouts) + layouts_str = ",".join(self._layouts) lua_eval_cmd = f"hl.config({{ input = {{ kb_layout = '{layouts_str}' }} }})" - Gio.Subprocess.new( - [HYPRCTL_BIN, "eval", lua_eval_cmd], - Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE, - ) + self._hyprctl("eval", lua_eval_cmd) - def _apply_current_layout_index(self): - Gio.Subprocess.new( - [HYPRCTL_BIN, "switchxkblayout", "all", str(self.current_index)], - Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE, - ) + # ── public API ───────────────────────────────────────────────────────────── - def switch_to_next(self): - if not self.layouts: + def switch_to_next(self) -> bool: + if not self._layouts: logger.warning("[KeyboardLayout] No layouts configured, cannot cycle.") return False - self.current_index = (self.current_index + 1) % len(self.layouts) - new_layout = self.layouts[self.current_index] - - self._apply_current_layout_index() - self._save_state() - self._write_layout(new_layout) - - logger.info( - f"[KeyboardLayout] Switched layout successfully to '{new_layout}' " - f"(index {self.current_index})" - ) + next_index = (self._current_index + 1) % len(self._layouts) + self._hyprctl("switchxkblayout", "all", str(next_index)) + self._set_current(next_index, emit=True) return True @staticmethod def switch_keyboard_layout(): KeyboardLayout.get_initial().switch_to_next() - @Property(str, "readable") - def current_layout(self): - return self._read_layout() + # ── config changes ───────────────────────────────────────────────────────── + + def _on_config_change(self, new_config, old_config): + new_layouts = new_config.get("keyboard_layouts") + if not isinstance(new_layouts, list) or not new_layouts: + return + new_layouts = [str(item).strip() for item in new_layouts if str(item).strip()] + if new_layouts == self._layouts: + return + + previous_layout = self._current_layout + self._layouts = new_layouts + self._apply_layouts_to_hyprland() + + preserved_index = ( + self._layouts.index(previous_layout) + if previous_layout in self._layouts + else 0 + ) + self._hyprctl("switchxkblayout", "all", str(preserved_index)) + self._set_current(preserved_index, emit=True) + + logger.info(f"[KeyboardLayout] Layouts updated from config: {self._layouts}") + + # ── properties ───────────────────────────────────────────────────────────── + + @Property(str, "readable", default_value="") + def current_layout(self) -> str: + return self._current_layout or "" + + @Property(list, "readable") + def layouts(self) -> list[str]: + return self._layouts + + @Property(int, "readable", default_value=0) + def current_index(self) -> int: + return self._current_index diff --git a/src/services/modus.py b/src/services/modus.py index 8c80bea..f8adb3a 100644 --- a/src/services/modus.py +++ b/src/services/modus.py @@ -438,7 +438,10 @@ def is_night_light_active() -> bool: try: _night_light_active = bool(find_process_pid("hyprsunset")) return _night_light_active - except Exception: + except Exception as e: + logger.warning( + f"[modus] _night_light_active = bool(find_process_pid('hyprsunset')) failed: {e}" + ) return False @@ -572,7 +575,10 @@ def open_trash(): if find_binary(fm): spawn_detached([fm, trash_path]) return - except Exception: + except Exception as e: + logger.warning( + f"[modus] if find_binary(fm): spawn_detached([fm, trash_path]) return failed: {e}" + ) continue diff --git a/src/services/mpris.py b/src/services/mpris.py index 00d5ead..dabffdc 100644 --- a/src/services/mpris.py +++ b/src/services/mpris.py @@ -81,28 +81,38 @@ class PlayerService(Service): def player_name(self) -> str: try: return self._player.props.player_name or "" - except Exception: + except Exception as e: + logger.warning( + f"[mpris] return self._player.props.player_name or '' failed: {e}" + ) return "" @Property(str, "readable", default_value="") def title(self) -> str: try: return self._player.get_title() or "" - except Exception: + except Exception as e: + logger.warning(f"[mpris] return self._player.get_title() or '' failed: {e}") return "" @Property(object, "readable") def artist(self) -> list: try: return list(self._player.props.metadata["xesam:artist"]) or [] - except Exception: + except Exception as e: + logger.warning( + f"[mpris] return list(self._player.props.metadata['xesam:artist']) ... failed: {e}" + ) return [] @Property(str, "readable", default_value="") def album(self) -> str: try: return self._player.props.metadata["xesam:album"] or "" - except Exception: + except Exception as e: + logger.warning( + f"[mpris] return self._player.props.metadata['xesam:album'] or '' failed: {e}" + ) return "" @Property(str, "readable", default_value="Stopped") @@ -115,21 +125,30 @@ class PlayerService(Service): return "Paused" else: return "Stopped" - except Exception: + except Exception as e: + logger.warning( + f"[mpris] val = int(self._player.props.playback_status) failed: {e}" + ) return "Stopped" @Property(int, "readable", default_value=0) def length(self) -> int: try: return int(self._player.props.metadata["mpris:length"]) - except Exception: + except Exception as e: + logger.warning( + f"[mpris] return int(self._player.props.metadata['mpris:length']) failed: {e}" + ) return 0 @Property(int, "readable", default_value=0) def position(self) -> int: try: return int(self._player.get_position()) - except Exception: + except Exception as e: + logger.warning( + f"[mpris] return int(self._player.get_position()) failed: {e}" + ) return 0 def play_pause(self, *_): @@ -206,7 +225,10 @@ class PlayerService(Service): return False try: metadata = self._player.props.metadata - except Exception: + except Exception as e: + logger.warning( + f"[mpris] metadata = self._player.props.metadata failed: {e}" + ) return False if metadata: # Push full metadata only once to avoid UI churn on every retry. @@ -363,7 +385,8 @@ class PlayerService(Service): def _meta_str(self, metadata, key: str) -> str | None: try: value = metadata[key] - except (KeyError, TypeError, GLib.Error): + except (KeyError, TypeError, GLib.Error) as e: + logger.warning(f"[mpris] value = metadata[key] failed: {e}") return None if isinstance(value, (list, tuple)): return " ".join(str(v) for v in value) if value else None @@ -401,7 +424,8 @@ class PlayerService(Service): return try: art_url = metadata["mpris:artUrl"] - except (KeyError, TypeError): + except (KeyError, TypeError) as e: + logger.warning(f"[mpris] art_url = metadata['mpris:artUrl'] failed: {e}") return if not art_url: return diff --git a/src/services/network.py b/src/services/network.py index fc7ff12..af6c011 100644 --- a/src/services/network.py +++ b/src/services/network.py @@ -45,18 +45,22 @@ class Wifi(Service): """Disconnect all signal handlers so the service can be garbage collected.""" try: self._client.disconnect(self._client_handler) - except Exception: - pass + except Exception as e: + logger.warning( + f"[network] self._client.disconnect(self._client_handler) failed: {e}" + ) for hid in getattr(self, "_device_handlers", ()): try: self._device.disconnect(hid) - except Exception: - pass + except Exception as e: + logger.warning(f"[network] self._device.disconnect(hid) failed: {e}") if getattr(self, "_ap", None) and getattr(self, "_ap_signal", None): try: self._ap.disconnect(self._ap_signal) - except Exception: - pass + except Exception as e: + logger.warning( + f"[network] self._ap.disconnect(self._ap_signal) failed: {e}" + ) def ap_update(self): self.emit("changed") @@ -287,8 +291,8 @@ class Ethernet(Service): for hid in getattr(self, "_handlers", ()): try: self._device.disconnect(hid) - except Exception: - pass + except Exception as e: + logger.warning(f"[network] self._device.disconnect(hid) failed: {e}") class NetworkClient(Service): diff --git a/src/services/nightlight.py b/src/services/nightlight.py new file mode 100644 index 0000000..1e10292 --- /dev/null +++ b/src/services/nightlight.py @@ -0,0 +1,206 @@ +import re + +from fabric.core.service import Property, Service, Signal +from fabric.utils import Gio, GLib, logger + +from services.config import get_config, on_config_change + +_CONFIG_KEY = "night_light_temperature" +_DEFAULT_TEMPERATURE = 4500 +_NEUTRAL_TEMPERATURE = 6000 +_PROFILE_TEMP_RE = re.compile(r"(\d{3,5})") + + +def _coerce_temperature(value, fallback: int = _DEFAULT_TEMPERATURE) -> int: + try: + return int(value) + except (TypeError, ValueError) as e: + logger.warning(f"[nightlight] return int(value) failed: {e}") + return fallback + + +class NightLightService(Service): + """Blue-light filter service backed by a persistent hyprsunset daemon. + + All state changes go through ``hyprctl hyprsunset`` IPC so the daemon is + started at most once and never respawned on every toggle. + """ + + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + @Signal + def night_light_changed(self, active: bool) -> None: + """Emitted when the filter is enabled or disabled.""" + + @Signal + def temperature_changed(self, value: int) -> None: + """Emitted when the color temperature changes.""" + + def __init__(self, **kwargs): + if getattr(self, "_initialized", False): + return + self._initialized = True + super().__init__(**kwargs) + self._active = False + self._temperature = _coerce_temperature(get_config(_CONFIG_KEY)) + self._process: Gio.Subprocess | None = None + self._daemon_running = False + on_config_change(self._on_config_change) + self._sync_state() + + @Property(bool, "readable", default_value=False) + def active(self) -> bool: + return self._active + + @Property(int, "readable", default_value=_DEFAULT_TEMPERATURE) + def temperature(self) -> int: + return self._temperature + + def is_active(self) -> bool: + return self._active + + def enable(self): + kelvin = self._temperature or _DEFAULT_TEMPERATURE + if self._daemon_running: + self._hyprctl("temperature", kelvin) + else: + self._start_daemon(["-t", str(kelvin)]) + self._set_temperature(kelvin) + self._set_active(True) + + def disable(self): + if self._daemon_running: + self._hyprctl("identity") + self._set_active(False) + + def toggle(self) -> bool: + if self._active: + self.disable() + else: + self.enable() + return self._active + + def set_temperature(self, kelvin: int): + self._set_temperature(kelvin) + if not self._active: + return + if self._daemon_running: + self._hyprctl("temperature", kelvin) + else: + self._start_daemon(["-t", str(kelvin)]) + + def set_gamma(self, percent: int): + if self._daemon_running: + self._hyprctl("gamma", percent) + else: + self._start_daemon(["-g", str(percent)]) + + def reset(self): + if self._daemon_running: + self._hyprctl("reset") + self._sync_state() + + def _on_config_change(self, new_config, old_config): + if new_config.get(_CONFIG_KEY) == old_config.get(_CONFIG_KEY): + return + self.set_temperature(_coerce_temperature(new_config.get(_CONFIG_KEY))) + + def _set_active(self, value: bool): + if value != self._active: + self._active = value + self.emit("night_light_changed", value) + + def _set_temperature(self, value: int): + if value != self._temperature: + self._temperature = value + self.emit("temperature_changed", value) + + def _start_daemon(self, args: list[str]): + try: + self._process = Gio.Subprocess.new( + ["hyprsunset", *args], + Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE, + ) + except GLib.Error as error: + logger.error(f"[NightLightService] Failed to start hyprsunset: {error}") + self._process = None + return + self._daemon_running = True + self._process.wait_async(None, self._on_daemon_exit) + + def _on_daemon_exit(self, process: Gio.Subprocess, task: Gio.Task): + self._process = None + self._daemon_running = False + self._set_active(False) + + def _hyprctl(self, *args, on_reply=None): + argv = ["hyprctl", "hyprsunset", *[str(arg) for arg in args]] + try: + proc = Gio.Subprocess.new( + argv, + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_SILENCE, + ) + except GLib.Error as error: + logger.error(f"[NightLightService] Failed to run {argv}: {error}") + if on_reply: + on_reply(None) + return + + def on_done(process: Gio.Subprocess, task: Gio.Task): + try: + _, stdout, _ = process.communicate_utf8_finish(task) + except GLib.Error: + stdout = None + if on_reply: + on_reply(stdout.strip() if stdout else None) + + proc.communicate_utf8_async(None, None, on_done) + + def _sync_state(self): + self._hyprctl("profile", on_reply=self._on_profile_reply) + + def _on_profile_reply(self, reply: str | None): + if not self._daemon_alive(reply): + self._daemon_running = False + return + self._daemon_running = True + temperature = self._parse_temperature(reply) + if temperature is not None: + self._apply_synced_temperature(temperature) + else: + self._hyprctl("temperature", on_reply=self._on_temperature_reply) + + def _on_temperature_reply(self, reply: str | None): + if reply is None: + return + temperature = self._parse_temperature(reply) + if temperature is not None: + self._apply_synced_temperature(temperature) + + def _apply_synced_temperature(self, temperature: int): + active = temperature < _NEUTRAL_TEMPERATURE + if active: + self._set_temperature(temperature) + self._set_active(active) + + @staticmethod + def _daemon_alive(reply: str | None) -> bool: + if reply is None: + return False + return "couldn't connect" not in reply.lower() + + @staticmethod + def _parse_temperature(reply: str | None) -> int | None: + if not reply: + return None + match = _PROFILE_TEMP_RE.search(reply) + return int(match.group(1)) if match else None + + +def get_night_light_service() -> NightLightService: + return NightLightService() diff --git a/src/services/numlock.py b/src/services/numlock.py index 7434e9b..f24261d 100644 --- a/src/services/numlock.py +++ b/src/services/numlock.py @@ -64,7 +64,10 @@ class NumLock(Service): if brightness_path.exists(): try: return bool(int(brightness_path.read_text().strip())) - except (ValueError, OSError): + except (ValueError, OSError) as e: + logger.warning( + f"[numlock] return bool(int(brightness_path.read_text().strip())) failed: {e}" + ) return False return False diff --git a/src/services/wallpaper.py b/src/services/wallpaper.py index 33afc2d..1e0893d 100644 --- a/src/services/wallpaper.py +++ b/src/services/wallpaper.py @@ -3,7 +3,7 @@ import mimetypes from fabric.core import Service, Signal from fabric.utils import GdkPixbuf as gdk_pixbuf -from fabric.utils import exec_shell_command_async, os +from fabric.utils import exec_shell_command_async, logger, os from shared.data import ( WALLPAPER_PATH, @@ -44,7 +44,10 @@ def create_thumbnail( ) ) - except Exception: + except Exception as e: + logger.warning( + f"[wallpaper] pixbuf = gdk_pixbuf.Pixbuf.new_from_file_at_scale( image_... failed: {e}" + ) return False diff --git a/src/shared/capture.py b/src/shared/capture.py index acab8f4..0d7e50e 100644 --- a/src/shared/capture.py +++ b/src/shared/capture.py @@ -248,6 +248,7 @@ class HyprlandCaptureBackend(CaptureBackend): try: proc.wait_finish(task) except Exception as error: + logger.warning(f"[capture] proc.wait_finish(task) failed: {error}") on_result(CaptureResult.failure(str(error))) return if expected.exists(): @@ -338,7 +339,10 @@ class HyprlandCaptureBackend(CaptureBackend): def on_done(proc: Gio.Subprocess, task: Gio.Task) -> None: try: _, stdout, _ = proc.communicate_utf8_finish(task) - except Exception: + except Exception as e: + logger.warning( + f"[capture] _, stdout, _ = proc.communicate_utf8_finish(task) failed: {e}" + ) on_geometry(None) return on_geometry(stdout.strip() if stdout else None) @@ -372,7 +376,10 @@ class HyprlandCaptureBackend(CaptureBackend): def on_done(proc: Gio.Subprocess, task: Gio.Task) -> None: try: _, stdout, _ = proc.communicate_utf8_finish(task) - except Exception: + except Exception as e: + logger.warning( + f"[capture] _, stdout, _ = proc.communicate_utf8_finish(task) failed: {e}" + ) on_geometry(None) return on_geometry(stdout.strip() if stdout else None) diff --git a/src/shared/dialogs/about.py b/src/shared/dialogs/about.py index 96426b3..135c005 100644 --- a/src/shared/dialogs/about.py +++ b/src/shared/dialogs/about.py @@ -11,7 +11,10 @@ def read_dmi(field): try: with open(f"/sys/class/dmi/id/{field}") as f: return f.read().strip() - except Exception: + except Exception as e: + logger.warning( + f"[about] with open(f'/sys/class/dmi/id/(field)') as f: return f.re... failed: {e}" + ) return "Unknown" @@ -40,7 +43,10 @@ def get_gpu_name(): return clean(output[0]) return "Unknown GPU" - except Exception: + except Exception as e: + logger.warning( + f"[about] output = ( subprocess.check_output( 'lspci -nn | grep -i ... failed: {e}" + ) return "Unknown GPU" @@ -146,7 +152,10 @@ def get_app_info(wmclass): ): desktop_file = full_path break - except Exception: + except Exception as e: + logger.warning( + f"[about] full_path = os.path.join(path, f) failed: {e}" + ) continue if desktop_file: @@ -199,7 +208,10 @@ def get_app_info(wmclass): "categories": categories, "desktop_file": desktop_file, } - except Exception: + except Exception as e: + logger.warning( + f"[about] with open(desktop_file, encoding='utf-8') as f: content =... failed: {e}" + ) continue # Fallback: try to find executable in PATH diff --git a/src/shared/dialogs/wifi_password_dialog.py b/src/shared/dialogs/wifi_password_dialog.py index 6cf1152..90b4357 100644 --- a/src/shared/dialogs/wifi_password_dialog.py +++ b/src/shared/dialogs/wifi_password_dialog.py @@ -1,4 +1,4 @@ -from fabric.utils import Gdk, GLib +from fabric.utils import Gdk, GLib, logger from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.entry import Entry @@ -273,7 +273,10 @@ class WiFiPasswordDialog(Window): self.password_entry.grab_focus() self.password_entry.select_region(0, -1) return False - except Exception: + except Exception as e: + logger.warning( + f"[wifi_password_dialog] self.password_entry.grab_focus() failed: {e}" + ) return False def destroy_dialog(self): diff --git a/src/shared/window/battery_widget.py b/src/shared/window/battery_widget.py index 02a3571..ccda227 100644 --- a/src/shared/window/battery_widget.py +++ b/src/shared/window/battery_widget.py @@ -1,4 +1,4 @@ -from fabric.utils import GLib +from fabric.utils import GLib, logger from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox @@ -245,8 +245,11 @@ class BatteryControl(Box): self.battery_service.connect( "power_profile_changed", self.on_profile_changed ) - except TypeError: - pass # signal does not exist + except TypeError as e: + logger.warning( + f"[battery_widget] self.battery_service.connect( 'power_profile_changed', se... failed: {e}" + ) + # signal does not exist # Initialize display self.update_battery_info() @@ -393,10 +396,14 @@ class BatteryControl(Box): def destroy(self): try: self.battery_service.disconnect_by_func(self.on_battery_changed) - except Exception: - pass + except Exception as e: + logger.warning( + f"[battery_widget] self.battery_service.disconnect_by_func(self.on_battery_c... failed: {e}" + ) try: self.battery_service.disconnect_by_func(self.on_profile_changed) - except Exception: - pass + except Exception as e: + logger.warning( + f"[battery_widget] self.battery_service.disconnect_by_func(self.on_profile_c... failed: {e}" + ) super().destroy() diff --git a/src/shared/window/popup_win.py b/src/shared/window/popup_win.py index 1e87b94..ca2dbe8 100644 --- a/src/shared/window/popup_win.py +++ b/src/shared/window/popup_win.py @@ -99,12 +99,16 @@ class PopupWindow(WaylandWindow): if self._pointing_widget: try: self._pointing_widget.disconnect_by_func(self.do_handle_size_allocate) - except Exception: - pass + except Exception as e: + logger.warning( + f"[popup_win] self._pointing_widget.disconnect_by_func(self.do_handle_s... failed: {e}" + ) try: self.disconnect_by_func(self.do_handle_size_allocate) - except Exception: - pass + except Exception as e: + logger.warning( + f"[popup_win] self.disconnect_by_func(self.do_handle_size_allocate) failed: {e}" + ) super().destroy() def do_calculate_edges(self): diff --git a/src/utils/constants.py b/src/utils/constants.py index 09ad8d2..d2a48c0 100644 --- a/src/utils/constants.py +++ b/src/utils/constants.py @@ -20,6 +20,7 @@ DEFAULT = { "workspace_indicator": True, "bluetooth": True, "date_time": True, + "night_light_temperature": 4500, "custom_mods": True, "keyboard_layouts": ["us", "np"], "window_switcher": True, diff --git a/src/utils/functions.py b/src/utils/functions.py index e7c6b4c..29d4bc8 100644 --- a/src/utils/functions.py +++ b/src/utils/functions.py @@ -81,19 +81,22 @@ def parse_timeout_string(timeout_str): try: seconds = int(timeout_str[:-1]) return seconds * 1000 - except ValueError: + except ValueError as e: + logger.warning(f"[functions] seconds = int(timeout_str[:-1]) failed: {e}") return 5000 elif timeout_str.endswith("m"): try: minutes = int(timeout_str[:-1]) return minutes * 60 * 1000 - except ValueError: + except ValueError as e: + logger.warning(f"[functions] minutes = int(timeout_str[:-1]) failed: {e}") return 5000 else: try: seconds = int(timeout_str) return seconds * 1000 - except ValueError: + except ValueError as e: + logger.warning(f"[functions] seconds = int(timeout_str) failed: {e}") return 5000 @@ -114,7 +117,10 @@ def copy_text(text: str) -> bool: clipboard.set_text(text, -1) clipboard.store() return True - except Exception: + except Exception as e: + logger.warning( + f"[functions] clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) failed: {e}" + ) return False @@ -125,7 +131,10 @@ def copy_image(image_path: str) -> bool: clipboard.set_image(image) clipboard.store() return True - except Exception: + except Exception as e: + logger.warning( + f"[functions] clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) failed: {e}" + ) return False @@ -150,8 +159,10 @@ def trigger_paste_shortcut(): stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - except FileNotFoundError: - pass + except FileNotFoundError as e: + logger.warning( + f"[functions] subprocess.Popen( ['sh', '-c', 'wl-paste | wtype -'], std... failed: {e}" + ) def get_wifi_icon_for_strength(strength: int) -> str: @@ -199,8 +210,9 @@ def is_special_workspace_id(ws_id) -> bool: workspace_id = int(ws_id) # Special workspaces have negative IDs return workspace_id < 0 - except (ValueError, TypeError): + except (ValueError, TypeError) as e: # If it's a string, check if it starts with "special:" + logger.warning(f"[functions] workspace_id = int(ws_id) failed: {e}") return bool(isinstance(ws_id, str) and ws_id.startswith("special:")) @@ -264,7 +276,10 @@ def is_process_running(process_name: str) -> bool: try: result = subprocess.run(["pidof", process_name], capture_output=True, text=True) return bool(result.stdout.strip()) - except Exception: + except Exception as e: + logger.warning( + f"[functions] result = subprocess.run(['pidof', process_name], capture_... failed: {e}" + ) return False @@ -281,7 +296,10 @@ def find_process_pid(process_name: str, timeout: float | None = None) -> list[st timeout=timeout, ) return [pid for pid in result.stdout.strip().split() if pid] - except Exception: + except Exception as e: + logger.warning( + f"[functions] result = subprocess.run( ['pgrep', '-f', process_name], c... failed: {e}" + ) return [] @@ -290,7 +308,10 @@ def find_binary(name: str) -> str | None: try: result = subprocess.run(["which", name], capture_output=True, text=True) return result.stdout.strip() or None - except Exception: + except Exception as e: + logger.warning( + f"[functions] result = subprocess.run(['which', name], capture_output=T... failed: {e}" + ) return None @@ -307,7 +328,8 @@ def format_duration(seconds: int) -> str: """ try: total = int(seconds) - except (TypeError, ValueError): + except (TypeError, ValueError) as e: + logger.warning(f"[functions] total = int(seconds) failed: {e}") return "N/A" if total <= 0: @@ -358,14 +380,23 @@ def run_command( ) return CommandResult(result.returncode, result.stdout, result.stderr) except subprocess.TimeoutExpired as e: + logger.warning( + f"[functions] result = subprocess.run( args, capture_output=True, text=... failed: {e}" + ) return CommandResult( -1, (e.stdout or b"" if not text else e.stdout or ""), (e.stderr or (b"timeout" if not text else "timeout")), ) except FileNotFoundError as e: + logger.warning( + f"[functions] result = subprocess.run( args, capture_output=True, text=... failed: {e}" + ) return CommandResult(127, "", str(e)) except Exception as e: + logger.warning( + f"[functions] result = subprocess.run( args, capture_output=True, text=... failed: {e}" + ) return CommandResult(1, "", str(e)) diff --git a/src/utils/icon_resolver.py b/src/utils/icon_resolver.py index cd6b521..811b150 100644 --- a/src/utils/icon_resolver.py +++ b/src/utils/icon_resolver.py @@ -142,7 +142,10 @@ class IconResolver: name = line[5:].strip().lower() if any(sid == name for sid in search_ids): return path - except Exception: + except Exception as e: + logger.warning( + f"[icon_resolver] with open(path, errors='ignore') as file: for line in fil... failed: {e}" + ) continue return None diff --git a/src/utils/utils.py b/src/utils/utils.py index 01b2434..8838439 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -106,8 +106,10 @@ class EvdevLEDMonitor: for child in device_path.iterdir(): if child.name.startswith("event"): return Path("/dev/input") / child.name - except Exception: - pass + except Exception as e: + logger.warning( + f"[utils] device_path = self.led_path / 'device' failed: {e}" + ) return None def start(self): @@ -137,7 +139,10 @@ class EvdevLEDMonitor: while True: try: data = os.read(fd, event_size) - except BlockingIOError: + except BlockingIOError as e: + logger.warning( + f"[utils] data = os.read(fd, event_size) failed: {e}" + ) break if len(data) == event_size: @@ -160,6 +165,6 @@ class EvdevLEDMonitor: if self._fd is not None: try: os.close(self._fd) - except OSError: - pass + except OSError as e: + logger.warning(f"[utils] os.close(self._fd) failed: {e}") self._fd = None diff --git a/src/window/controlcenter/bluetooth.py b/src/window/controlcenter/bluetooth.py index c30729e..24d1218 100644 --- a/src/window/controlcenter/bluetooth.py +++ b/src/window/controlcenter/bluetooth.py @@ -535,8 +535,10 @@ class BluetoothConnections(Box): self.other_devices_scrolled.height_animator.disconnect( self._anim_finished_handler ) - except Exception: - pass + except Exception as e: + logger.warning( + f"[bluetooth] self.other_devices_scrolled.height_animator.disconnect( s... failed: {e}" + ) self._anim_finished_handler = None def _refresh_after_animation(self): @@ -558,8 +560,10 @@ class BluetoothConnections(Box): spawn_detached(["blueman-manager"]) if self.parent and hasattr(self.parent, "hide_controlcenter"): self.parent.hide_controlcenter() - except FileNotFoundError: - pass + except FileNotFoundError as e: + logger.warning( + f"[bluetooth] spawn_detached(['blueman-manager']) failed: {e}" + ) except Exception as e: logger.error(f"[Bluetooth] Failed to open bluetooth settings: {e}") diff --git a/src/window/controlcenter/expanded_player.py b/src/window/controlcenter/expanded_player.py index 5cf5bd6..903bf6a 100644 --- a/src/window/controlcenter/expanded_player.py +++ b/src/window/controlcenter/expanded_player.py @@ -528,8 +528,10 @@ class PlayerBox(Box): self.position_label.set_label( self.length_str(self.player.position or 0) ) - except Exception: - pass + except Exception as e: + logger.warning( + f"[expanded_player] metadata = self.player._player.props.metadata failed: {e}" + ) def _poll_initial_metadata(self): if self.exit: @@ -724,8 +726,10 @@ class PlayerBox(Box): if self.player and not self.exit: try: self.player.seek_position(value) - except Exception: - pass + except Exception as e: + logger.warning( + f"[expanded_player] self.player.seek_position(value) failed: {e}" + ) if hasattr(self, "_seek_timeout") and self._seek_timeout: from fabric.utils import GLib @@ -763,8 +767,10 @@ class PlayerBox(Box): self.seek_bar.set_range(0, duration / 1_000_000) self.length_label.set_label(self.length_str(duration)) return True - except Exception: - pass + except Exception as e: + logger.warning( + f"[expanded_player] duration = self._cached_duration or self.player.length or 0 failed: {e}" + ) return False def _on_seek_bar_realized(self, widget): diff --git a/src/window/controlcenter/player.py b/src/window/controlcenter/player.py index 5118c66..eba416f 100644 --- a/src/window/controlcenter/player.py +++ b/src/window/controlcenter/player.py @@ -471,8 +471,10 @@ class PlayerBox(Box): if "xesam:artist" in keys and metadata["xesam:artist"] else "No Artist" ) - except Exception: - pass + except Exception as e: + logger.warning( + f"[player] metadata = self.player._player.props.metadata failed: {e}" + ) def update_buttons(self, player_buttons, show_buttons): if show_buttons and len(player_buttons) > 1: diff --git a/src/window/controlcenter/wifi.py b/src/window/controlcenter/wifi.py index 07f5cd8..26049ef 100644 --- a/src/window/controlcenter/wifi.py +++ b/src/window/controlcenter/wifi.py @@ -417,8 +417,10 @@ class WifiConnections(Box): self.other_networks_scrolled.height_animator.disconnect( self._anim_finished_handler ) - except Exception: - pass + except Exception as e: + logger.warning( + f"[wifi] self.other_networks_scrolled.height_animator.disconnect( ... failed: {e}" + ) self._anim_finished_handler = None def _refresh_after_animation(self): @@ -477,8 +479,10 @@ class WifiConnections(Box): spawn_detached(["nm-connection-editor"]) if self.parent and hasattr(self.parent, "hide_controlcenter"): self.parent.hide_controlcenter() - except FileNotFoundError: - pass + except FileNotFoundError as e: + logger.warning( + f"[wifi] spawn_detached(['nm-connection-editor']) failed: {e}" + ) except Exception as e: logger.error(f"[WiFi] Failed to open network settings: {e}") diff --git a/src/window/desktop/widget.py b/src/window/desktop/widget.py index 7d38526..73aa3d9 100644 --- a/src/window/desktop/widget.py +++ b/src/window/desktop/widget.py @@ -146,7 +146,10 @@ class WeatherService(Service): if isinstance(data, list) and data: try: coords = (float(data[0]["lat"]), float(data[0]["lon"])) - except (ValueError, KeyError): + except (ValueError, KeyError) as e: + logger.warning( + f"[widget] coords = (float(data[0]['lat']), float(data[0]['lon'])) failed: {e}" + ) return None self._coords = coords self._coords_location = city @@ -325,8 +328,10 @@ class Weather(Box): """Disconnect from the weather service (the service is app-lifetime).""" try: self._service.disconnect_by_func(self._on_service_update) - except ValueError: - pass + except ValueError as e: + logger.warning( + f"[widget] self._service.disconnect_by_func(self._on_service_update) failed: {e}" + ) super().destroy() diff --git a/src/window/dock/canvas.py b/src/window/dock/canvas.py index f5a62de..b53a966 100644 --- a/src/window/dock/canvas.py +++ b/src/window/dock/canvas.py @@ -412,7 +412,8 @@ class DockCanvas(Gtk.DrawingArea): def _on_modus_workspace_changed(self, service, value) -> None: try: clients = self._get_clients() - except Exception: + except Exception as e: + logger.warning(f"[canvas] clients = self._get_clients() failed: {e}") return client_map = {c.get("address"): c for c in clients if c.get("address")} for item in self.model.items: @@ -450,8 +451,8 @@ class DockCanvas(Gtk.DrawingArea): f"{app_class}: {title}" if title != app_class else app_class ) break - except Exception: - pass + except Exception as e: + logger.warning(f"[canvas] address = signal.data[0] failed: {e}") self._needs_redraw = True self.queue_draw() @@ -811,7 +812,10 @@ class DockCanvas(Gtk.DrawingArea): try: trash_path = os.path.expanduser("~/.local/share/Trash/files") return os.path.exists(trash_path) and len(os.listdir(trash_path)) > 0 - except Exception: + except Exception as e: + logger.warning( + f"[canvas] trash_path = os.path.expanduser('~/.local/share/Trash/fil... failed: {e}" + ) return False def _handle_trash_click(self) -> None: @@ -860,7 +864,10 @@ class DockCanvas(Gtk.DrawingArea): "command_line": app.command_line if app else app_class, } self.pinned_apps.append(app_data) - except Exception: + except Exception as e: + logger.warning( + f"[canvas] app = self._find_desktop_app(app_class, self._get_desktop... failed: {e}" + ) self.pinned_apps.append(app_class) self._write_pinned_apps() self._rebuild_model() @@ -1040,7 +1047,8 @@ class DockCanvas(Gtk.DrawingArea): elif isinstance(ws, (int, str)): try: return int(ws) - except (ValueError, TypeError): + except (ValueError, TypeError) as e: + logger.warning(f"[canvas] return int(ws) failed: {e}") return None return None @@ -1060,15 +1068,19 @@ class DockCanvas(Gtk.DrawingArea): for handler_id in self._hyprland_event_handlers: try: self._hyprland_connection.disconnect(handler_id) - except Exception: - pass + except Exception as e: + logger.warning( + f"[canvas] self._hyprland_connection.disconnect(handler_id) failed: {e}" + ) self._hyprland_event_handlers.clear() if self._modus_ws_handler is not None: try: get_modus_service().disconnect(self._modus_ws_handler) - except Exception: - pass + except Exception as e: + logger.warning( + f"[canvas] get_modus_service().disconnect(self._modus_ws_handler) failed: {e}" + ) self._modus_ws_handler = None if self.menu: diff --git a/src/window/notification/notification.py b/src/window/notification/notification.py index 718f210..7163d6b 100644 --- a/src/window/notification/notification.py +++ b/src/window/notification/notification.py @@ -201,8 +201,11 @@ def get_notification_image_cache_key(notification_id, image_pixbuf): # Fallback to timestamp for invalid pixbufs return str(int(time.time()))[:8] - except Exception: + except Exception as e: # Ultimate fallback + logger.warning( + f"[notification] if image_pixbuf and hasattr(image_pixbuf, 'get_pixels'): ... failed: {e}" + ) return str(int(time.time()))[:8] @@ -644,8 +647,10 @@ class NotificationWidget(Box): if notif_key: try: pixbuf = get_cached_notification_image(notif_key) - except Exception: - pass + except Exception as e: + logger.warning( + f"[notification] pixbuf = get_cached_notification_image(notif_key) failed: {e}" + ) # 2. Try notification image via image_pixbuf (only if no cached) if not pixbuf: @@ -659,8 +664,10 @@ class NotificationWidget(Box): pixbuf = cached else: pixbuf = img - except Exception: - pass + except Exception as e: + logger.warning( + f"[notification] img = getattr(notification, 'image_pixbuf', None) failed: {e}" + ) # 3. App icon via cache_metadata if not pixbuf: @@ -670,8 +677,10 @@ class NotificationWidget(Box): from window.notification.unified_cache import get_from_cache pixbuf = get_from_cache(app_key, (64, 64)) - except Exception: - pass + except Exception as e: + logger.warning( + f"[notification] from window.notification.unified_cache import get_from_cache failed: {e}" + ) # 4. Direct app icon caching if not pixbuf: @@ -681,8 +690,10 @@ class NotificationWidget(Box): if app_icon: try: pixbuf = cache_notification_icon(app_icon, (64, 64)) - except Exception: - pass + except Exception as e: + logger.warning( + f"[notification] pixbuf = cache_notification_icon(app_icon, (64, 64)) failed: {e}" + ) # 5. Fallback if not pixbuf: @@ -694,7 +705,10 @@ class NotificationWidget(Box): NOTIFICATION_IMAGE_SIZE, GdkPixbuf.InterpType.BILINEAR, ) - except Exception: + except Exception as e: + logger.warning( + f"[notification] return pixbuf.scale_simple( NOTIFICATION_IMAGE_SIZE, NOTI... failed: {e}" + ) return pixbuf def create_action_buttons(self, notification): diff --git a/src/window/notification/notification_center.py b/src/window/notification/notification_center.py index 135531b..1db846c 100644 --- a/src/window/notification/notification_center.py +++ b/src/window/notification/notification_center.py @@ -234,7 +234,10 @@ class ExpandableNotificationGroup(Box): # Always scale to 64x64 so screenshot thumbnails match app icon size try: return pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.BILINEAR) - except Exception: + except Exception as e: + logger.warning( + f"[notification_center] return pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.B... failed: {e}" + ) return pixbuf def create_expanded_state(self): @@ -499,7 +502,10 @@ class NotificationCenterWidget(NotificationWidget): # Always scale to 64x64 so screenshot thumbnails match app icon size try: return pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.BILINEAR) - except Exception: + except Exception as e: + logger.warning( + f"[notification_center] return pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.B... failed: {e}" + ) return pixbuf def create_content(self, notification): diff --git a/src/window/notification/unified_cache.py b/src/window/notification/unified_cache.py index 62b7694..429c441 100644 --- a/src/window/notification/unified_cache.py +++ b/src/window/notification/unified_cache.py @@ -20,8 +20,10 @@ def _get_icon_theme_name(): settings = Gtk.Settings.get_default() if settings: return getattr(settings, "gtk_icon_theme_name", "") or "" - except Exception: - pass + except Exception as e: + logger.warning( + f"[unified_cache] settings = Gtk.Settings.get_default() failed: {e}" + ) # Fallback: read from settings.ini try: settings_path = os.path.expanduser("~/.config/gtk-3.0/settings.ini") @@ -31,8 +33,10 @@ def _get_icon_theme_name(): line = line.strip() if line.startswith("gtk-icon-theme-name="): return line.split("=", 1)[1].strip() - except Exception: - pass + except Exception as e: + logger.warning( + f"[unified_cache] settings_path = os.path.expanduser('~/.config/gtk-3.0/set... failed: {e}" + ) return "" @@ -50,8 +54,11 @@ def get_unified_cache_key(source_data, size=None, app_name=None): pixel_data = source_data.get_pixels() image_hash = hashlib.md5(pixel_data).hexdigest()[:8] return image_hash - except Exception: + except Exception as e: # Fallback to random UUID if pixel data fails + logger.warning( + f"[unified_cache] pixel_data = source_data.get_pixels() failed: {e}" + ) return str(uuid.uuid4())[:8] elif isinstance(source_data, str): # For file paths - create hash-based name @@ -71,8 +78,11 @@ def get_unified_cache_key(source_data, size=None, app_name=None): else: # Fallback to random UUID return str(uuid.uuid4())[:8] - except Exception: + except Exception as e: # Ultimate fallback + logger.warning( + f"[unified_cache] if hasattr(source_data, 'get_pixels'): # For pixbuf data ... failed: {e}" + ) return str(uuid.uuid4())[:8] @@ -216,7 +226,10 @@ def get_fallback_icon(size=(48, 48)): return GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, True, 8, size[0], size[1] ) - except Exception: + except Exception as e: + logger.warning( + f"[unified_cache] return GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, Tr... failed: {e}" + ) return None @@ -230,8 +243,10 @@ def _get_stored_theme(): if os.path.exists(_CURRENT_THEME_FILE): with open(_CURRENT_THEME_FILE) as f: return f.read().strip() - except Exception: - pass + except Exception as e: + logger.warning( + f"[unified_cache] if os.path.exists(_CURRENT_THEME_FILE): with open(_CURREN... failed: {e}" + ) return "" @@ -241,8 +256,10 @@ def _store_theme(name): os.makedirs(os.path.dirname(_CURRENT_THEME_FILE), exist_ok=True) with open(_CURRENT_THEME_FILE, "w") as f: f.write(name) - except Exception: - pass + except Exception as e: + logger.warning( + f"[unified_cache] os.makedirs(os.path.dirname(_CURRENT_THEME_FILE), exist_o... failed: {e}" + ) def cleanup_theme_changed_caches(): @@ -262,8 +279,10 @@ def cleanup_theme_changed_caches(): if os.path.exists(ICON_CACHE_FILE): os.unlink(ICON_CACHE_FILE) logger.info("[Cache] Cleared icon resolver cache") - except Exception: - pass + except Exception as e: + logger.warning( + f"[unified_cache] from utils.icon_resolver import ICON_CACHE_FILE failed: {e}" + ) # Store current theme for next run if current_theme: diff --git a/src/window/panel/components/globalmenu.py b/src/window/panel/components/globalmenu.py index bba9169..7948a46 100644 --- a/src/window/panel/components/globalmenu.py +++ b/src/window/panel/components/globalmenu.py @@ -253,8 +253,10 @@ class GlobalMenuDropdowns: self.global_title_menu_quit.get_child().get_start_children()[0].set_label( f"Quit {value}" ) - except Exception: - pass + except Exception as e: + logger.warning( + f"[globalmenu] self.global_title_menu_about.get_child().get_start_childr... failed: {e}" + ) # Notify the global menu service about the active window change if self._global_menu_svc: @@ -284,15 +286,17 @@ class GlobalMenuDropdowns: if btn.get_parent(): btn.get_parent().remove(btn) btn.destroy() - except Exception: - pass + except Exception as e: + logger.warning( + f"[globalmenu] if btn.get_parent(): btn.get_parent().remove(btn) failed: {e}" + ) # Clean up any previous dynamic dropdowns for dd in self._dynamic_dropdowns: try: dd.destroy() - except Exception: - pass + except Exception as e: + logger.warning(f"[globalmenu] dd.destroy() failed: {e}") self._dynamic_dropdowns.clear() # Filter to visible top-level menu items only @@ -413,8 +417,10 @@ class GlobalMenuDropdowns: if self._global_menu_svc: try: self._global_menu_svc.disconnect_by_func(self._on_menu_changed) - except Exception: - pass + except Exception as e: + logger.warning( + f"[globalmenu] self._global_menu_svc.disconnect_by_func(self._on_menu_ch... failed: {e}" + ) # Destroy dynamic dropdowns for dd in self._dynamic_dropdowns: diff --git a/src/window/panel/notch/indicators.py b/src/window/panel/notch/indicators.py index d3dafbb..1d3fdde 100644 --- a/src/window/panel/notch/indicators.py +++ b/src/window/panel/notch/indicators.py @@ -343,12 +343,16 @@ class MicrophoneIndicator(Box): if old is not None and old is not mic: try: old.disconnect_by_func(self._on_mic_stream_changed) - except Exception: - pass + except Exception as e: + logger.warning( + f"[indicators] old.disconnect_by_func(self._on_mic_stream_changed) failed: {e}" + ) try: mic.disconnect_by_func(self._on_mic_stream_changed) - except Exception: - pass + except Exception as e: + logger.warning( + f"[indicators] mic.disconnect_by_func(self._on_mic_stream_changed) failed: {e}" + ) mic.connect("changed", self._on_mic_stream_changed) self._mic_device = mic self._sync_state() @@ -386,8 +390,10 @@ class MicrophoneIndicator(Box): self._hide_timer_id = 0 try: self._audio.disconnect_by_func(self._on_mic_device_changed) - except Exception: - pass + except Exception as e: + logger.warning( + f"[indicators] self._audio.disconnect_by_func(self._on_mic_device_changed) failed: {e}" + ) old = getattr(self, "_mic_device", None) target = old if old is not None else self._audio.microphone if target is not None: diff --git a/src/window/panel/notch/notch.py b/src/window/panel/notch/notch.py index 6919b2b..ab2eb5a 100644 --- a/src/window/panel/notch/notch.py +++ b/src/window/panel/notch/notch.py @@ -1,4 +1,4 @@ -from fabric.utils import Gdk, GLib, Gtk +from fabric.utils import Gdk, GLib, Gtk, logger from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox from fabric.widgets.shapes import Corner @@ -192,8 +192,10 @@ class Notch(Box): svc = self._mpris.get_player_service(name) if svc is not None: svc.disconnect_by_func(self._on_any_artwork) - except Exception: - pass + except Exception as e: + logger.warning( + f"[notch] svc = self._mpris.get_player_service(name) failed: {e}" + ) self._apply_stack_state() # ── Scroll ──────────────────────────────────────────────────────────────── diff --git a/src/window/panel/notch/player.py b/src/window/panel/notch/player.py index 7596073..466f0a7 100644 --- a/src/window/panel/notch/player.py +++ b/src/window/panel/notch/player.py @@ -2,7 +2,7 @@ import math import os import time -from fabric.utils import GdkPixbuf, GLib, Gtk +from fabric.utils import GdkPixbuf, GLib, Gtk, logger from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox @@ -146,8 +146,10 @@ class NotchPlayer(Box): for sid in self._sigs: try: self._service.disconnect(sid) - except Exception: - pass + except Exception as e: + logger.warning( + f"[player] self._service.disconnect(sid) failed: {e}" + ) self._sigs.clear() def _on_play(self, *_) -> None: @@ -185,7 +187,10 @@ class NotchPlayer(Box): self.artwork_image.set_from_pixbuf(pixbuf) self.fallback_icon.set_visible(False) self.artwork_image.set_visible(True) - except Exception: + except Exception as e: + logger.warning( + f"[player] pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( path, 1... failed: {e}" + ) self._show_fallback() def _update_from_service(self) -> None: diff --git a/src/window/panel/notch/recording.py b/src/window/panel/notch/recording.py index c2e6f58..0b9332a 100644 --- a/src/window/panel/notch/recording.py +++ b/src/window/panel/notch/recording.py @@ -1,4 +1,4 @@ -from fabric.utils import GLib, time +from fabric.utils import GLib, logger, time from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox @@ -73,8 +73,10 @@ class RecordingIndicator(Box): try: screen_capture_service.stop_recording() self.stop_timer() - except Exception: - pass + except Exception as e: + logger.warning( + f"[recording] screen_capture_service.stop_recording() failed: {e}" + ) def destroy(self): self.stop_timer() diff --git a/src/window/screencapture/main.py b/src/window/screencapture/main.py index fd1b551..39c5a56 100644 --- a/src/window/screencapture/main.py +++ b/src/window/screencapture/main.py @@ -1,4 +1,4 @@ -from fabric.utils import Gdk, GLib +from fabric.utils import Gdk, GLib, logger from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.label import Label @@ -252,8 +252,8 @@ class ScreenCaptureWindow(Window): if _handler_id[0] is not None: try: svc.disconnect(_handler_id[0]) - except Exception: - pass + except Exception as e: + logger.warning(f"[main] svc.disconnect(_handler_id[0]) failed: {e}") _handler_id[0] = None # Re-show only on cancel (path is None) if not path: @@ -265,8 +265,10 @@ class ScreenCaptureWindow(Window): if _handler_id[0] is not None: try: screen_capture_service.disconnect(_handler_id[0]) - except Exception: - pass + except Exception as e: + logger.warning( + f"[main] screen_capture_service.disconnect(_handler_id[0]) failed: {e}" + ) _handler_id[0] = None return False @@ -284,8 +286,10 @@ class ScreenCaptureWindow(Window): if _handler_id[0] is not None: try: screen_capture_service.disconnect(_handler_id[0]) - except Exception: - pass + except Exception as e: + logger.warning( + f"[main] screen_capture_service.disconnect(_handler_id[0]) failed: {e}" + ) _handler_id[0] = None GLib.timeout_add(120, self.show) diff --git a/src/window/spotlight/core/loader.py b/src/window/spotlight/core/loader.py index 718f5ce..6b92dcc 100644 --- a/src/window/spotlight/core/loader.py +++ b/src/window/spotlight/core/loader.py @@ -301,7 +301,10 @@ class PluginLoader: """Check if requirements.txt changed since last install.""" try: return req_file.read_text() != marker_file.read_text() - except Exception: + except Exception as e: + logger.warning( + f"[loader] return req_file.read_text() != marker_file.read_text() failed: {e}" + ) return True # ------------------------------------------------------------------ diff --git a/src/window/spotlight/core/search.py b/src/window/spotlight/core/search.py index 9b6fe8f..fb135d0 100644 --- a/src/window/spotlight/core/search.py +++ b/src/window/spotlight/core/search.py @@ -117,8 +117,10 @@ class SearchPipeline: with self._lock: try: self._pending_idle_ids.remove(source_id) - except ValueError: - pass + except ValueError as e: + logger.warning( + f"[search] self._pending_idle_ids.remove(source_id) failed: {e}" + ) callback(results) return False @@ -133,8 +135,10 @@ class SearchPipeline: with self._lock: try: self._pending_idle_ids.remove(source_id) - except ValueError: - pass + except ValueError as e: + logger.warning( + f"[search] self._pending_idle_ids.remove(source_id) failed: {e}" + ) callback([]) return False @@ -178,8 +182,10 @@ class SearchPipeline: with self._lock: try: self._pending_idle_ids.remove(source_id) - except ValueError: - pass + except ValueError as e: + logger.warning( + f"[search] self._pending_idle_ids.remove(source_id) failed: {e}" + ) _on_all_done() return False diff --git a/src/window/spotlight/main.py b/src/window/spotlight/main.py index fb80167..7aea315 100644 --- a/src/window/spotlight/main.py +++ b/src/window/spotlight/main.py @@ -1,7 +1,7 @@ import os from fabric.core import Signal -from fabric.utils import Gdk, GLib +from fabric.utils import Gdk, GLib, logger from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.entry import Entry @@ -265,8 +265,10 @@ class Spotlight(Box): return try: widget.disconnect_by_func(self.on_slot_enter) - except (TypeError, ValueError): - pass + except (TypeError, ValueError) as e: + logger.warning( + f"[main] widget.disconnect_by_func(self.on_slot_enter) failed: {e}" + ) self.viewport.remove(widget) widget.destroy() @@ -544,8 +546,10 @@ class Spotlight(Box): for child in list(self.viewport.children): try: child.disconnect_by_func(self.on_slot_enter) - except (TypeError, ValueError): - pass + except (TypeError, ValueError) as e: + logger.warning( + f"[main] child.disconnect_by_func(self.on_slot_enter) failed: {e}" + ) self.viewport.remove(child) child.destroy() self._result_widgets.clear() diff --git a/src/window/spotlight/plugins/__init__.py b/src/window/spotlight/plugins/__init__.py index 30eec8f..b3c9d6a 100644 --- a/src/window/spotlight/plugins/__init__.py +++ b/src/window/spotlight/plugins/__init__.py @@ -8,6 +8,8 @@ Each plugin module exposes a PLUGIN variable pointing to its class. import importlib import pkgutil +from fabric.utils import logger + __all__ = [] for module_info in pkgutil.iter_modules(__path__): @@ -20,5 +22,7 @@ for module_info in pkgutil.iter_modules(__path__): if plugin_class is not None: globals()[module_name] = plugin_class __all__.append(module_name) - except Exception: - pass + except Exception as e: + logger.warning( + f"[__init__] module = importlib.import_module(f'(__name__).(module_nam... failed: {e}" + ) diff --git a/src/window/spotlight/plugins/application.py b/src/window/spotlight/plugins/application.py index f0e543f..2d4fd03 100644 --- a/src/window/spotlight/plugins/application.py +++ b/src/window/spotlight/plugins/application.py @@ -1,7 +1,7 @@ import time from typing import Any -from fabric.utils import DesktopApp, get_desktop_applications +from fabric.utils import DesktopApp, get_desktop_applications, logger from utils.functions import fuzzy_score from window.spotlight.api import SearchResult, SpotlightPlugin @@ -38,8 +38,10 @@ class ApplicationPlugin(SpotlightPlugin): try: self._desktop_apps = get_desktop_applications() self._apps_loaded_at = now - except Exception: - pass + except Exception as e: + logger.warning( + f"[application] self._desktop_apps = get_desktop_applications() failed: {e}" + ) results: list[SearchResult] = [] for app in self._desktop_apps: diff --git a/src/window/spotlight/plugins/wallpapers.py b/src/window/spotlight/plugins/wallpapers.py index 022a67c..f93a6d4 100644 --- a/src/window/spotlight/plugins/wallpapers.py +++ b/src/window/spotlight/plugins/wallpapers.py @@ -2,6 +2,8 @@ import os import random from typing import Any +from fabric.utils import logger + from services.wallpaper import WallpaperService, create_thumbnail from shared.data import WALLPAPERS_THUMBNAILS_SIZE from utils.functions import fuzzy_filter @@ -71,7 +73,10 @@ class WallpaperPlugin(SpotlightPlugin): image_path, thumbnail_path, WALLPAPERS_THUMBNAILS_SIZE ) thumbs_created += 1 - except Exception: + except Exception as e: + logger.warning( + f"[wallpapers] create_thumbnail( image_path, thumbnail_path, WALLPAPERS_... failed: {e}" + ) continue filename = os.path.basename(image_path) diff --git a/start.py b/start.py index 5e6d571..30720a7 100644 --- a/start.py +++ b/start.py @@ -35,8 +35,32 @@ def run_lock(): lock_main() +def run_spotlight(argv): + from window.spotlight.app import main as spotlight_main + + command = "" + text = "" + external = False + + positional = [] + for arg in argv: + if arg == "--external": + external = True + else: + positional.append(arg) + + if positional: + command = positional[0] + if len(positional) > 1: + text = " ".join(positional[1:]) + + spotlight_main(command=command, text=text, external=external) + + if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "lock": run_lock() + elif len(sys.argv) > 1 and sys.argv[1] == "spotlight": + run_spotlight(sys.argv[2:]) else: run_app()