fix: resolve wifi crashes, shell injections, race conditions, and add debug mode
- Remove transient NEED_AUTH/DISCONNECTED error paths from wifi connect - Guard double-callback in connect_wifi_bssid/connect_wifi_with_password - Shell-quote user paths in wallpaper, screenshot, screenrecorder services - Fix OSD hide-timer use-after-free and applet_window stale source ID - Fix brightness poll timer leak on read failure - Fix spotlight search stale idle source removal - Lock dbusmenu _cache mutation under _registry_lock - Fix entry.revision written outside _registry_lock scope - Add fabric-cli exec wrapper for lock screen (lock_screen.lock()) - Replace hyprlock with lock screen in power menu and dropdown - Add debug mode toggle in settings (controls log verbosity) - Add diagnostic logging to globalmenu click chain
This commit is contained in:
26 files changed
+224
-57
No files matched your search
+1
-1
@@ -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
|
||||
+16
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`
|
||||
|
||||
+32
-13
@@ -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}"),
|
||||
)
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+2
-4
@@ -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/")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -1,4 +1,5 @@
|
||||
DEFAULT = {
|
||||
"debug": False,
|
||||
"dock_enabled": True,
|
||||
"dock_auto_hide": True,
|
||||
"dock_always_occluded": False,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
+18
-2
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user