refactor: clean up service implementations and remove dead code
This commit is contained in:
22 files changed
+2067
-756
No files matched your search
@@ -1,10 +1,10 @@
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from fabric.core.service import Property, Service, Signal
|
||||
from fabric.utils import GLib, exec_shell_command_async, logger, os, re
|
||||
|
||||
from utils.functions import find_binary, run_command
|
||||
|
||||
|
||||
class Brightness(Service):
|
||||
"""Service for controlling screen brightness using ddcutil or brightnessctl backends.
|
||||
@@ -103,7 +103,7 @@ class Brightness(Service):
|
||||
logger.info(f"Using forced backend: {backend}")
|
||||
return backend
|
||||
|
||||
if shutil.which("brightnessctl"):
|
||||
if find_binary("brightnessctl"):
|
||||
device = self._get_screen_device()
|
||||
if device:
|
||||
logger.info(f"Using brightnessctl backend with device: {device}")
|
||||
@@ -113,7 +113,7 @@ class Brightness(Service):
|
||||
"brightnessctl is available but no backlight devices found in /sys/class/backlight/"
|
||||
)
|
||||
|
||||
if shutil.which("ddcutil"):
|
||||
if find_binary("ddcutil"):
|
||||
bus = self._detect_ddcutil_bus()
|
||||
if bus != -1:
|
||||
self.ddcutil_bus = bus
|
||||
@@ -139,9 +139,7 @@ class Brightness(Service):
|
||||
def _detect_ddcutil_bus(self):
|
||||
"""Detect I2C bus number for ddcutil."""
|
||||
try:
|
||||
process = subprocess.run(
|
||||
["ddcutil", "detect"], text=True, capture_output=True, timeout=2
|
||||
)
|
||||
process = run_command(["ddcutil", "detect"], timeout=2)
|
||||
if process.returncode == 0:
|
||||
match = re.search(r"I2C bus:\s*/dev/i2c-(\d+)", process.stdout)
|
||||
return int(match.group(1)) if match else -1
|
||||
@@ -154,7 +152,7 @@ class Brightness(Service):
|
||||
if self.backend:
|
||||
if self.backend == "ddcutil":
|
||||
try:
|
||||
process = subprocess.run(
|
||||
process = run_command(
|
||||
[
|
||||
"ddcutil",
|
||||
"--bus",
|
||||
@@ -163,8 +161,6 @@ class Brightness(Service):
|
||||
"getvcp",
|
||||
"10",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class CachedNotification(Service):
|
||||
# Store cache metadata for cleanup
|
||||
self.cache_metadata = data.get("cache_metadata", {})
|
||||
self.timestamp = data.get("timestamp", int(time.time()))
|
||||
self._pixbuf_cache = {}
|
||||
|
||||
return self
|
||||
|
||||
@@ -82,18 +83,23 @@ class CachedNotification(Service):
|
||||
|
||||
@Property(object, "readable")
|
||||
def image_pixbuf(self) -> GdkPixbuf.Pixbuf | None:
|
||||
if hasattr(self, "_cached_image_pixbuf"):
|
||||
return self._cached_image_pixbuf
|
||||
try:
|
||||
if self.image_pixmap:
|
||||
return self.image_pixmap.as_pixbuf()
|
||||
self._cached_image_pixbuf = self.image_pixmap.as_pixbuf()
|
||||
return self._cached_image_pixbuf
|
||||
if self.image_file and os.path.exists(self.image_file):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file(self.image_file)
|
||||
self._cached_image_pixbuf = GdkPixbuf.Pixbuf.new_from_file(
|
||||
self.image_file
|
||||
)
|
||||
return self._cached_image_pixbuf
|
||||
except Exception:
|
||||
# If file can't be loaded, return None
|
||||
pass
|
||||
except Exception:
|
||||
# If any error occurs (including temp file gone), return None safely
|
||||
pass
|
||||
self._cached_image_pixbuf = None
|
||||
return None
|
||||
|
||||
@Property(dict, "readable")
|
||||
@@ -169,6 +175,12 @@ class CachedNotification(Service):
|
||||
self._cache_id = cache_id
|
||||
self.cache_metadata = {}
|
||||
self.timestamp = int(time.time())
|
||||
self._pixbuf_cache: dict[str, GdkPixbuf.Pixbuf | None] = {}
|
||||
|
||||
def get_or_cache_pixbuf(self, cache_key: str, loader) -> GdkPixbuf.Pixbuf | None:
|
||||
if cache_key not in self._pixbuf_cache:
|
||||
self._pixbuf_cache[cache_key] = loader()
|
||||
return self._pixbuf_cache[cache_key]
|
||||
|
||||
def remove_from_cache(self):
|
||||
self.removed_from_cache.emit()
|
||||
@@ -280,8 +292,8 @@ class CachedNotifications(Notifications):
|
||||
|
||||
handler_id = cached_notification.connect(
|
||||
"removed-from-cache",
|
||||
lambda *args: self.remove_cached_notification(
|
||||
notification_id=cache_id
|
||||
lambda *args, cid=cache_id: self.remove_cached_notification(
|
||||
notification_id=cid
|
||||
),
|
||||
)
|
||||
self._signal_handlers[cache_id] = handler_id
|
||||
@@ -435,7 +447,9 @@ class CachedNotifications(Notifications):
|
||||
# IMMEDIATELY store to history before attempting any caching operations
|
||||
handler_id = cached_notification.connect(
|
||||
"removed-from-cache",
|
||||
lambda *args: self.remove_cached_notification(notification_id=cache_id),
|
||||
lambda *args, cid=cache_id: self.remove_cached_notification(
|
||||
notification_id=cid
|
||||
),
|
||||
)
|
||||
self._signal_handlers[cache_id] = handler_id
|
||||
self._cached_notifications[cache_id] = cached_notification
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Global menu environment setup — GTK_MODULES, UBUNTU_MENUPROXY, settings.ini, Flatpak."""
|
||||
|
||||
from fabric.utils import exec_shell_command, logger, os
|
||||
|
||||
|
||||
ENV_VARS = {
|
||||
"GTK_MODULES": "appmenu-gtk-module",
|
||||
"UBUNTU_MENUPROXY": "1",
|
||||
}
|
||||
|
||||
|
||||
def setup_global_menu_environment():
|
||||
"""Write env config files so GTK apps register with AppMenu at next login.
|
||||
|
||||
Does NOT set env via hyprctl keyword env — that would cause Fabric's own
|
||||
GTK windows to load appmenu-gtk-module and trigger Gdk-CRITICAL assertions.
|
||||
Uses session-level config files that take effect at login.
|
||||
"""
|
||||
try:
|
||||
# exec_shell_command("hyprctl keyword env GTK_MODULES,appmenu-gtk-module")
|
||||
# exec_shell_command("hyprctl keyword env UBUNTU_MENUPROXY,1")
|
||||
exec_shell_command(
|
||||
"dbus-update-activation-environment --systemd GTK_MODULES=appmenu-gtk-module UBUNTU_MENUPROXY=1"
|
||||
)
|
||||
exec_shell_command(
|
||||
"flatpak override --user --talk-name=com.canonical.AppMenu.Registrar"
|
||||
)
|
||||
|
||||
env_dir = os.path.expanduser("~/.config/environment.d")
|
||||
try:
|
||||
os.makedirs(env_dir, exist_ok=True)
|
||||
env_path = os.path.join(env_dir, "appmenu.conf")
|
||||
with open(env_path, "w") as f:
|
||||
for k, v in ENV_VARS.items():
|
||||
f.write(f"{k}={v}\n")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
settings_path = os.path.expanduser("~/.config/gtk-3.0/settings.ini")
|
||||
try:
|
||||
if not os.path.exists(os.path.dirname(settings_path)):
|
||||
os.makedirs(os.path.dirname(settings_path), exist_ok=True)
|
||||
content = ""
|
||||
if os.path.exists(settings_path):
|
||||
with open(settings_path, "r") as f:
|
||||
content = f.read()
|
||||
if "gtk-shell-shows-menubar" not in content:
|
||||
with open(settings_path, "a") as f:
|
||||
if content and not content.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("gtk-shell-shows-menubar=1\n")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
pam_path = os.path.expanduser("~/.pam_environment")
|
||||
try:
|
||||
existing = ""
|
||||
if os.path.exists(pam_path):
|
||||
with open(pam_path, "r") as f:
|
||||
existing = f.read()
|
||||
lines = existing.splitlines(True)
|
||||
for k, v in ENV_VARS.items():
|
||||
line = f"{k} DEFAULT={v}\n"
|
||||
if not any(ln.startswith(f"{k} ") for ln in lines):
|
||||
with open(pam_path, "a") as f:
|
||||
f.write(line)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
logger.info("[GlobalMenu] GTK environment injected")
|
||||
except Exception as e:
|
||||
logger.warning(f"[GlobalMenu] Environment setup failed: {e}")
|
||||
@@ -7,21 +7,20 @@ and other visual effects for better performance during gaming.
|
||||
Uses a marker file in /tmp to track state.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from utils.functions import read_json_file, write_json_file
|
||||
from utils.functions import read_json_file, run_command, write_json_file
|
||||
|
||||
STATE_FILE = "/tmp/hyprland_gamemode"
|
||||
|
||||
|
||||
def run_hyprctl(command: str) -> str:
|
||||
"""Run a hyprctl command and return the output (raises on error)."""
|
||||
result = subprocess.run(
|
||||
["hyprctl"] + command.split(), capture_output=True, text=True, check=True
|
||||
)
|
||||
result = run_command(["hyprctl"] + command.split(), timeout=5)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"hyprctl failed: {result.stderr}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
|
||||
+664
-364
File diff suppressed because it is too large.
Load diff
+386
-114
@@ -1,12 +1,54 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from fabric.core.service import Property, Service, Signal
|
||||
from fabric.hyprland.service import Hyprland
|
||||
from fabric.utils import logger
|
||||
|
||||
from services.custom_notification import CachedNotifications
|
||||
from utils.functions import (
|
||||
find_binary,
|
||||
find_process_pid,
|
||||
kill_process as _kill_process,
|
||||
run_command,
|
||||
spawn_detached,
|
||||
)
|
||||
|
||||
notification_service = CachedNotifications()
|
||||
|
||||
def __getattr__(name):
|
||||
mod = sys.modules[__name__]
|
||||
if name == "notification_service":
|
||||
from services.custom_notification import CachedNotifications
|
||||
|
||||
_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 == "screen_capture_service":
|
||||
from services.screencapture import ScreenCapture
|
||||
|
||||
_inst = ScreenCapture()
|
||||
setattr(mod, name, _inst)
|
||||
return _inst
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
def get_notification_service():
|
||||
from services.custom_notification import CachedNotifications
|
||||
|
||||
return CachedNotifications()
|
||||
|
||||
|
||||
class ModusService(Service):
|
||||
@@ -146,7 +188,7 @@ class ModusService(Service):
|
||||
@volume.setter
|
||||
def volume(self, value: int):
|
||||
if value != self._volume:
|
||||
self._name = value
|
||||
self._volume = value
|
||||
self.volume_changed(value)
|
||||
|
||||
@wlan.setter
|
||||
@@ -223,7 +265,6 @@ class ModusService(Service):
|
||||
|
||||
def sc(self, signal_name: str, callback: callable, def_value="..."):
|
||||
self.connect(signal_name, callback)
|
||||
# Return current property value instead of default
|
||||
if signal_name == "bluetooth-changed":
|
||||
return self.bluetooth if self.bluetooth else "Off"
|
||||
elif signal_name == "wlan-changed":
|
||||
@@ -243,7 +284,7 @@ class ModusService(Service):
|
||||
self._bluetooth = ""
|
||||
self._dock_apps = ""
|
||||
self._dont_disturb = False
|
||||
self._current_active_app_name = "Finder" # Changed from "Hyprland" to "Finder"
|
||||
self._current_active_app_name = "Finder"
|
||||
self._current_active_wm_class = ""
|
||||
self._current_workspace = "1"
|
||||
self._music = ""
|
||||
@@ -251,119 +292,350 @@ class ModusService(Service):
|
||||
self._dropdowns_hide = False
|
||||
self._dock_hidden = False
|
||||
self._show_notificationcenter = False
|
||||
|
||||
self._dock_width = 0
|
||||
self._dock_height = 0
|
||||
|
||||
# Initialize Hyprland connection for workspace and window monitoring
|
||||
self._setup_workspace_monitoring()
|
||||
self._setup_active_window_monitoring()
|
||||
|
||||
def _setup_workspace_monitoring(self):
|
||||
"""Setup Hyprland connection and workspace monitoring"""
|
||||
try:
|
||||
self._hyprland_connection = Hyprland()
|
||||
|
||||
# Get initial workspace
|
||||
workspace_data = self._hyprland_connection.send_command(
|
||||
"j/activeworkspace"
|
||||
).reply
|
||||
active_workspace = json.loads(workspace_data.decode("utf-8"))["name"]
|
||||
self._current_workspace = str(active_workspace)
|
||||
|
||||
# Connect to workspace change events
|
||||
self._hyprland_connection.connect(
|
||||
"event::workspace", self._on_workspace_changed
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ModusService] Failed to setup workspace monitoring: {e}")
|
||||
self._current_workspace = "1"
|
||||
|
||||
def _setup_active_window_monitoring(self):
|
||||
"""Setup active window monitoring"""
|
||||
try:
|
||||
if (
|
||||
not hasattr(self, "_hyprland_connection")
|
||||
or not self._hyprland_connection
|
||||
):
|
||||
return
|
||||
|
||||
self._update_active_window()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[ModusService] Failed to setup active window monitoring: {e}"
|
||||
)
|
||||
|
||||
def _update_active_window(self):
|
||||
try:
|
||||
if (
|
||||
not hasattr(self, "_hyprland_connection")
|
||||
or not self._hyprland_connection
|
||||
):
|
||||
return
|
||||
|
||||
window_data = self._hyprland_connection.send_command("j/activewindow").reply
|
||||
if not window_data:
|
||||
self.current_active_app_name = "Finder"
|
||||
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:
|
||||
self.current_active_app_name = "Finder"
|
||||
return
|
||||
|
||||
# Delegate name resolution to app_name_resolver (lazy import to avoid circular)
|
||||
from utils.app_name_resolver import app_name_resolver
|
||||
|
||||
name = app_name_resolver.format_app_name(title, wmclass)
|
||||
|
||||
# Set wm_class FIRST so that signal handlers reading it get the new value
|
||||
self.current_active_wm_class = wmclass
|
||||
self.current_active_app_name = name
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ModusService] Error updating active window: {e}")
|
||||
self.current_active_app_name = "Finder"
|
||||
|
||||
def _on_workspace_changed(self, obj, signal):
|
||||
try:
|
||||
workspace_name = json.loads(signal.data[0])
|
||||
self.current_workspace = str(workspace_name)
|
||||
except Exception as e:
|
||||
logger.error(f"[ModusService] Error processing workspace change: {e}")
|
||||
|
||||
def remove_notification(self, id: int):
|
||||
notification_service.remove_cached_notification(id)
|
||||
self.notification_count_changed(self.notification_count)
|
||||
|
||||
def clear_all_notifications(self):
|
||||
notification_service.clear_all_cached_notifications()
|
||||
self.notification_count_changed(self.notification_count)
|
||||
|
||||
def get_cached_notifications(self):
|
||||
return notification_service.cached_notifications
|
||||
|
||||
def get_deserialized_with_ids(self):
|
||||
return [
|
||||
(notif._notification, notif.cache_id)
|
||||
for notif in notification_service.cached_notifications
|
||||
]
|
||||
|
||||
@property
|
||||
def notification_count(self) -> int:
|
||||
return notification_service.count
|
||||
|
||||
def toggle_dnd(self):
|
||||
notification_service.toggle_dnd()
|
||||
self.dont_disturb_changed(notification_service.dont_disturb)
|
||||
return get_notification_service().count
|
||||
|
||||
|
||||
global modus_service
|
||||
try:
|
||||
modus_service = ModusService()
|
||||
except Exception as e:
|
||||
logger.error("[Main] Failed to create ModusShellService:", e)
|
||||
_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)
|
||||
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 = "Finder"
|
||||
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 = "Finder"
|
||||
return
|
||||
from utils.app_name_resolver import app_name_resolver
|
||||
|
||||
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 = "Finder"
|
||||
|
||||
|
||||
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 remove_notification(id: int):
|
||||
get_notification_service().remove_cached_notification(id)
|
||||
get_modus_service().notification_count_changed(get_notification_service().count)
|
||||
|
||||
|
||||
def clear_all_notifications():
|
||||
get_notification_service().clear_all_cached_notifications()
|
||||
get_modus_service().notification_count_changed(get_notification_service().count)
|
||||
|
||||
|
||||
def get_cached_notifications():
|
||||
return get_notification_service().cached_notifications
|
||||
|
||||
|
||||
def get_deserialized_with_ids():
|
||||
ns = get_notification_service()
|
||||
return [(notif._notification, notif.cache_id) for notif in ns.cached_notifications]
|
||||
|
||||
|
||||
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 set_night_light_temperature(temperature: int) -> bool:
|
||||
global _night_light_active
|
||||
try:
|
||||
_kill_process("hyprsunset")
|
||||
spawn_detached(["hyprsunset", "-t", str(temperature)])
|
||||
_night_light_active = True
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[Modus] Failed to set night light temperature: {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:
|
||||
return False
|
||||
|
||||
|
||||
# --- Caffeine orchestration ---
|
||||
_caffeine_process: subprocess.Popen | None = None
|
||||
|
||||
|
||||
def is_caffeine_active() -> bool:
|
||||
if _caffeine_process is not None:
|
||||
return True
|
||||
try:
|
||||
return bool(find_process_pid("modus-inhibit", timeout=0.5))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def toggle_caffeine() -> bool:
|
||||
global _caffeine_process
|
||||
try:
|
||||
if is_caffeine_active():
|
||||
from fabric.utils import get_relative_path
|
||||
|
||||
inhibit_script = get_relative_path("utils/inhibit.py")
|
||||
run_command(["python3", inhibit_script, "off"])
|
||||
if _caffeine_process:
|
||||
_caffeine_process.terminate()
|
||||
_caffeine_process = None
|
||||
return False
|
||||
else:
|
||||
from fabric.utils import get_relative_path
|
||||
|
||||
inhibit_script = get_relative_path("utils/inhibit.py")
|
||||
_caffeine_process = spawn_detached(["python3", inhibit_script, "on"])
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[Modus] Failed to toggle caffeine: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# --- 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_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()
|
||||
for monitor in get_monitors():
|
||||
if monitor.get("activeWorkspace", {}).get("id") == workspace_id:
|
||||
return monitor
|
||||
monitors = get_monitors()
|
||||
return monitors[0] if monitors else None
|
||||
|
||||
|
||||
def get_screen_dimensions() -> tuple[int, int]:
|
||||
monitor = get_monitor_for_workspace()
|
||||
if monitor:
|
||||
return monitor.get("width", 1920), monitor.get("height", 1080)
|
||||
return 1920, 1080
|
||||
|
||||
|
||||
def focus_window(address: str):
|
||||
conn = _ensure_hyprland()
|
||||
if conn:
|
||||
conn.send_command(f"dispatch focuswindow address:{address}")
|
||||
|
||||
|
||||
def close_window(address: str):
|
||||
conn = _ensure_hyprland()
|
||||
if conn:
|
||||
conn.send_command(f"dispatch closewindow address:{address}")
|
||||
|
||||
|
||||
def launch_app(command_line: str):
|
||||
if not command_line:
|
||||
return
|
||||
import re
|
||||
|
||||
cleaned = re.sub(r"%\w+", "", command_line).strip()
|
||||
from fabric.utils import exec_shell_command_async
|
||||
|
||||
exec_shell_command_async(
|
||||
f"hyprctl dispatch 'hl.dsp.exec_cmd([[uwsm app -- {cleaned}]])'"
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
continue
|
||||
|
||||
|
||||
# --- Occlusion (uses Hyprland API above) ---
|
||||
|
||||
|
||||
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 = get_screen_dimensions()
|
||||
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
|
||||
@@ -367,4 +367,9 @@ class ScreenCapture(Service):
|
||||
return is_app_running("wf-recorder") or is_app_running("gpu-screen-recorder")
|
||||
|
||||
|
||||
screen_capture_service = ScreenCapture()
|
||||
def __getattr__(name):
|
||||
if name == "screen_capture_service":
|
||||
import sys
|
||||
|
||||
return sys.modules["services.modus"].screen_capture_service
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
Reference in new issue
Block a user