refactor: clean up service implementations and remove dead code

This commit is contained in:
sankalp committed 2026-07-08 12:24:39 +05:45
1 parent 9a1f512a28
commit 06be28535a
22 files changed
+2067 -756

No files matched your search

+6 -10
View File
@@ -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,
)
+21 -7
View File
@@ -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
+72
View File
@@ -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}")
+4 -5
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+386 -114
View File
@@ -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
+6 -1
View File
@@ -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}")