import json import os import sys from fabric.core.service import Property, Service, Signal from fabric.hyprland.service import Hyprland from fabric.utils import exec_shell_command_async, logger from services.custom_notification import CachedNotifications from services.inhibit import get_inhibit_service from utils.app_name_resolver import app_name_resolver from utils.functions import ( find_binary, find_process_pid, spawn_detached, ) from utils.functions import ( kill_process as _kill_process, ) def __getattr__(name): mod = sys.modules[__name__] if name == "notification_service": _inst = CachedNotifications() setattr(mod, name, _inst) return _inst if name == "modus_service": return get_modus_service() if name == "audio_service": try: from fabric.audio import Audio _inst = Audio() except Exception as e: logger.error(f"[Main] Failed to create AudioService: {e}") _inst = None setattr(mod, name, _inst) return _inst if name == "screenshot_service": from services.screenshot import Screenshot _inst = Screenshot() setattr(mod, name, _inst) return _inst if name == "screen_recorder_service": from services.screenrecorder import ScreenRecorder _inst = ScreenRecorder() setattr(mod, name, _inst) return _inst raise AttributeError(f"module {__name__!r} has no attribute {name!r}") def get_notification_service(): import sys return sys.modules[__name__].notification_service class ModusService(Service): @Signal def bluetooth_changed(self, new_bluetooth: str) -> None: ... @Signal def volume_changed(self, new_volume: int) -> None: ... @Signal def wlan_changed(self, new_wlan: str) -> None: ... @Signal def battery_changed(self, new_battery: str) -> None: ... @Signal def dont_disturb_changed(self, value: bool) -> None: ... @Signal def current_active_app_name_changed(self, value: str) -> None: ... @Signal def current_active_wm_class_changed(self, value: str) -> None: ... @Signal def current_workspace_changed(self, value: str) -> None: ... @Signal def music_changed(self, value: str) -> None: ... @Signal def current_dropdown_changed(self, value: str) -> None: ... @Signal def dropdowns_hide_changed(self, value: bool) -> None: ... @Signal def dock_width_changed(self, value: int) -> None: ... @Signal def dock_height_changed(self, value: int) -> None: ... @Signal def dock_hidden_changed(self, value: bool) -> None: ... @Signal def show_notificationcenter_changed(self, value: bool) -> None: ... @Signal def fullscreen_changed(self, value: bool) -> None: ... @Signal def notification_count_changed(self, value: int) -> None: ... @Property(str, flags="read-write") def current_active_app_name(self) -> str: return self._current_active_app_name @Property(str, flags="read-write") def current_active_wm_class(self) -> str: return self._current_active_wm_class @Property(str, flags="read-write") def current_workspace(self) -> str: return self._current_workspace @Property(str, flags="read-write") def bluetooth(self) -> str: return self._bluetooth @Property(str, flags="read-write") def wlan(self) -> str: return self._wlan @Property(str, flags="read-write") def battery(self) -> str: return self._battery @Property(int, flags="read-write") def volume(self) -> int: return self._volume @Property(str, flags="read-write") def dock_apps(self) -> str: return self._dock_apps @Property(bool, flags="read-write", default_value=False) def dont_disturb(self) -> bool: return self._dont_disturb @Property(str, flags="read-write") def music(self) -> str: return self._music @Property(str, flags="read-write") def current_dropdown(self) -> str: return self._current_dropdown @Property(bool, flags="read-write", default_value=False) def dropdowns_hide(self) -> bool: return self._dropdowns_hide @Property(int, flags="read-write") def dock_width(self) -> int: return self._dock_width @Property(int, flags="read-write") def dock_height(self) -> int: return self._dock_height @Property(bool, flags="read-write", default_value=False) def dock_hidden(self) -> bool: return self._dock_hidden @Property(bool, flags="read-write", default_value=False) def show_notificationcenter(self) -> bool: return self._show_notificationcenter @Property(bool, flags="read-write", default_value=False) def fullscreen(self) -> bool: return self._fullscreen @fullscreen.setter def fullscreen(self, value: bool): if value != self._fullscreen: self._fullscreen = value self.fullscreen_changed(value) @current_active_app_name.setter def current_active_app_name(self, value: str): if value != self._current_active_app_name: self._current_active_app_name = value self.current_active_app_name_changed(value) @current_active_wm_class.setter def current_active_wm_class(self, value: str): if value != self._current_active_wm_class: self._current_active_wm_class = value self.current_active_wm_class_changed(value) @current_workspace.setter def current_workspace(self, value: str): if value != self._current_workspace: self._current_workspace = value self.current_workspace_changed(value) @volume.setter def volume(self, value: int): if value != self._volume: self._volume = value self.volume_changed(value) @wlan.setter def wlan(self, value: str): if value != self._wlan: self._wlan = value self.wlan_changed(value) @battery.setter def battery(self, value: str): if value != self._battery: self._battery = value self.battery_changed(value) @bluetooth.setter def bluetooth(self, value: str): if value != self._bluetooth: self._bluetooth = value self.bluetooth_changed(value) @dont_disturb.setter def dont_disturb(self, value: bool): if value != self._dont_disturb: self._dont_disturb = value self.dont_disturb_changed(value) @music.setter def music(self, value: str): if value != self._music: self._music = value self.music_changed(value) @current_dropdown.setter def current_dropdown(self, value: str): if value != self._current_dropdown: self._current_dropdown = value self.current_dropdown_changed(value) @dropdowns_hide.setter def dropdowns_hide(self, value: bool): if value != self._dropdowns_hide: self._dropdowns_hide = value self.dropdowns_hide_changed(value) @dock_width.setter def dock_width(self, value: int): if value != self._dock_width: self._dock_width = value self.dock_width_changed(value) @dock_height.setter def dock_height(self, value: int): if value != self._dock_height: self._dock_height = value self.dock_height_changed(value) @dock_hidden.setter def dock_hidden(self, value: bool): if value != self._dock_hidden: self._dock_hidden = value self.dock_hidden_changed(value) @show_notificationcenter.setter def show_notificationcenter(self, value: bool): if value != self._show_notificationcenter: self._show_notificationcenter = value self.show_notificationcenter_changed(value) def __init__(self): super().__init__() self._volume = 0 self._wlan = "" self._battery = "" self._bluetooth = "" self._dock_apps = "" self._dont_disturb = False self._current_active_app_name = "Modus" self._current_active_wm_class = "" self._current_workspace = "1" self._music = "" self._current_dropdown = None self._dropdowns_hide = False self._dock_hidden = False self._show_notificationcenter = False self._dock_width = 0 self._dock_height = 0 self._fullscreen = False @property def notification_count(self) -> int: return get_notification_service().count _modus_instance: ModusService | None = None _hyprland_connection: Hyprland | None = None def get_modus_service() -> ModusService: global _modus_instance if _modus_instance is None: try: _modus_instance = ModusService() _setup_workspace_monitoring() _setup_active_window_monitoring() except Exception as e: logger.error("[ModusService] Failed to create instance:", e) raise return _modus_instance def _setup_workspace_monitoring(): global _hyprland_connection service = get_modus_service() try: _hyprland_connection = Hyprland() service._hyprland_connection = _hyprland_connection workspace_data = _hyprland_connection.send_command("j/activeworkspace").reply active_workspace = json.loads(workspace_data.decode("utf-8"))["name"] service._current_workspace = str(active_workspace) _hyprland_connection.connect("event::workspace", _on_workspace_changed) _hyprland_connection.connect("event::fullscreen", _on_fullscreen_changed) except Exception as e: logger.error(f"[ModusService] Failed to setup workspace monitoring: {e}") service._current_workspace = "1" def _setup_active_window_monitoring(): global _hyprland_connection get_modus_service() try: if not _hyprland_connection: return _update_active_window() except Exception as e: logger.error(f"[ModusService] Failed to setup active window monitoring: {e}") def _update_active_window(): global _hyprland_connection service = get_modus_service() try: if not _hyprland_connection: return window_data = _hyprland_connection.send_command("j/activewindow").reply if not window_data: service.current_active_app_name = "Modus" return window_info = json.loads(window_data.decode("utf-8")) wmclass = window_info.get("class", "") title = window_info.get("title", "") if not title and not wmclass: service.current_active_app_name = "Modus" return name = app_name_resolver.format_app_name(title, wmclass) service.current_active_wm_class = wmclass service.current_active_app_name = name except Exception as e: logger.error(f"[ModusService] Error updating active window: {e}") service.current_active_app_name = "Modus" def _on_workspace_changed(obj, signal): service = get_modus_service() try: workspace_name = json.loads(signal.data[0]) service.current_workspace = str(workspace_name) except Exception as e: logger.error(f"[ModusService] Error processing workspace change: {e}") def _on_fullscreen_changed(obj, event): service = get_modus_service() try: data = event.data[0] if event.data else "" mode = data.split(":")[-1] if ":" in data else "0" service.fullscreen = mode in ("1", "2") except Exception as e: logger.error(f"[ModusService] Error processing fullscreen change: {e}") def toggle_dnd(): ns = get_notification_service() ns.toggle_dnd() get_modus_service().dont_disturb_changed(ns.dont_disturb) # --- Night light orchestration --- _night_light_active = False def toggle_night_light() -> bool: global _night_light_active try: if _night_light_active: _kill_process("hyprsunset") _night_light_active = False else: spawn_detached(["hyprsunset", "-t", "4500"]) _night_light_active = True return True except Exception as e: logger.error(f"[Modus] Failed to toggle night light: {e}") return False def is_night_light_active() -> bool: global _night_light_active if _night_light_active: return True try: _night_light_active = bool(find_process_pid("hyprsunset")) return _night_light_active except Exception as e: logger.warning( f"[modus] _night_light_active = bool(find_process_pid('hyprsunset')) failed: {e}" ) return False # --- Caffeine orchestration --- def is_caffeine_active() -> bool: return get_inhibit_service().active def toggle_caffeine() -> bool: return get_inhibit_service().toggle() # --- Hyprland window management API --- def _ensure_hyprland(): """Ensure hyprland_connection is initialized.""" global _hyprland_connection if _hyprland_connection is None: get_modus_service() return _hyprland_connection def _hyprctl_send(cmd: str) -> bytes | None: conn = _ensure_hyprland() if not conn: return None try: reply = conn.send_command(cmd).reply return reply except Exception as e: logger.error(f"[Modus] hyprctl send error ({cmd}): {e}") return None def _hyprctl_json(cmd: str) -> list | dict | None: reply = _hyprctl_send(cmd) if not reply: return None try: return json.loads(reply.decode("utf-8")) except Exception as e: logger.error(f"[Modus] hyprctl JSON parse error ({cmd}): {e}") return None def get_clients() -> list: data = _hyprctl_json("j/clients") return data if isinstance(data, list) else [] def get_active_window() -> dict: data = _hyprctl_json("j/activewindow") return data if isinstance(data, dict) else {} def get_monitors() -> list: data = _hyprctl_json("j/monitors") return data if isinstance(data, list) else [] def get_focused_monitor() -> dict | None: monitors = get_monitors() for monitor in monitors: if monitor.get("focused"): return monitor return monitors[0] if monitors else None def get_monitor_names() -> list[str]: return [name for m in get_monitors() if (name := m.get("name"))] def get_active_monitor_name(fallback: str = "eDP-1") -> str: monitor = get_focused_monitor() if monitor and (name := monitor.get("name")): return name return fallback def get_active_workspace_id() -> int: data = _hyprctl_json("j/activeworkspace") if isinstance(data, dict): return data.get("id", -1) return -1 def get_monitor_for_workspace(workspace_id: int | None = None) -> dict | None: if workspace_id is None: workspace_id = get_active_workspace_id() monitors = get_monitors() for monitor in monitors: if monitor.get("activeWorkspace", {}).get("id") == workspace_id: return monitor return monitors[0] if monitors else None def focus_window(address: str): exec_shell_command_async( f"hyprctl dispatch 'hl.dsp.focus({{ window = \"address:{address}\" }})'" ) def close_window(address: str): exec_shell_command_async( f"hyprctl dispatch 'hl.dsp.window.close({{ window = \"address:{address}\" }})'" ) def launch_app(command_line: str): from window.globalmenu.launch import launch_command launch_command(command_line) def open_trash(): trash_path = os.path.expanduser("~/.local/share/Trash/files") if not os.path.exists(trash_path): return for fm in ["nautilus", "dolphin", "thunar", "nemo", "caja", "pcmanfm"]: try: if find_binary(fm): spawn_detached([fm, trash_path]) return except Exception as e: logger.warning( f"[modus] if find_binary(fm): spawn_detached([fm, trash_path]) return failed: {e}" ) continue # Occlusion def check_occlusion(occlusion_region, workspace=None) -> bool: if workspace is None: workspace = get_active_workspace_id() if isinstance(occlusion_region, tuple) and len(occlusion_region) == 2: side, size = occlusion_region if isinstance(side, str): screen_width, screen_height = 1920, 1080 monitor = get_monitor_for_workspace(workspace) if monitor: screen_width = monitor.get("width", screen_width) screen_height = monitor.get("height", screen_height) if side.lower() == "bottom": occlusion_region = (0, screen_height - size, screen_width, size) elif side.lower() == "top": occlusion_region = (0, 0, screen_width, size) elif side.lower() == "left": occlusion_region = (0, 0, size, screen_height) elif side.lower() == "right": occlusion_region = (screen_width - size, 0, size, screen_height) if not isinstance(occlusion_region, tuple) or len(occlusion_region) != 4: return False clients = get_clients() occ_x, occ_y, occ_width, occ_height = occlusion_region occ_x2 = occ_x + occ_width occ_y2 = occ_y + occ_height for client in clients: if not client.get("mapped", False): continue client_workspace = client.get("workspace", {}) if client_workspace.get("id") != workspace: continue position = client.get("at") size = client.get("size") if not position or not size: continue x, y = position width, height = size win_x1, win_y1 = x, y win_x2, win_y2 = x + width, y + height if not ( win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2 ): return True return False