diff --git a/lockscreen.py b/lockscreen.py index 04e1cfb..23235a4 100644 --- a/lockscreen.py +++ b/lockscreen.py @@ -558,9 +558,11 @@ def lock(): ) return None - from services.paths import resolve_style_file + from services.style import compile_resolved_stylesheet app = Application("lock") - app.set_stylesheet_from_file(resolve_style_file("style.css")) + resolved_css = compile_resolved_stylesheet() + if resolved_css: + app.set_stylesheet_from_string(resolved_css, compile=True) manager = LockManager() app.run() diff --git a/services/style.py b/services/style.py index 8ae60ce..df996dc 100644 --- a/services/style.py +++ b/services/style.py @@ -1,9 +1,45 @@ import os +import re from fabric.core.service import Service, Property from fabric.utils import monitor_file from gi.repository import GLib +from loguru import logger from plugin_loader import apply_plugin_css -from services.paths import get_user_config_dir, resolve_style_file +from services.paths import get_user_config_dir, get_repo_dir, resolve_style_file + + +def compile_resolved_stylesheet() -> str | None: + """ + Loads style.css and resolves every @import "" with resolve_style_file(fname), + ensuring user overrides in ~/.config/agility-shell/style/ (such as Matugen-generated + colors.css, user borders.css, user fonts.css) take precedence over static repo defaults. + Also appends custom.css if present in the user style directory. + """ + target_style = resolve_style_file("style.css") + if not os.path.isfile(target_style): + return None + + try: + with open(target_style, "r") as f: + content = f.read() + + def _replace_import(match: re.Match) -> str: + fname = match.group(1).strip() + resolved = resolve_style_file(fname) + if os.path.isfile(resolved): + return f'@import "{resolved}";' + return match.group(0) + + resolved_css = re.sub(r'@import\s+(?:url\()?["\']?([^"\')]+)["\']?\)?\s*;', _replace_import, content) + + user_custom = os.path.join(get_user_config_dir(), "style", "custom.css") + if os.path.isfile(user_custom): + resolved_css += f'\n@import "{user_custom}";\n' + + return resolved_css + except Exception as e: + logger.error(f"[StyleService] Error resolving stylesheet: {e}") + return None class StyleService(Service): @@ -12,11 +48,30 @@ def __init__(self, app, **kwargs): super().__init__(**kwargs) self.app = app self._style_changed = False + self._reload_timer_id: int | None = None style_dir = os.path.join(get_user_config_dir(), "style") os.makedirs(style_dir, exist_ok=True) self.style_monitor = monitor_file(style_dir) - self.style_monitor.connect("changed", lambda *_: self.reload()) + self.style_monitor.connect("changed", self._on_style_file_changed) + + repo_style_dir = os.path.join(get_repo_dir(), "style") + if os.path.isdir(repo_style_dir) and os.path.abspath(repo_style_dir) != os.path.abspath(style_dir): + try: + self.repo_style_monitor = monitor_file(repo_style_dir) + self.repo_style_monitor.connect("changed", self._on_style_file_changed) + except Exception as e: + logger.debug(f"[StyleService] Repo style monitor not attached: {e}") + + def _on_style_file_changed(self, *_): + if self._reload_timer_id is not None: + GLib.source_remove(self._reload_timer_id) + self._reload_timer_id = GLib.timeout_add(100, self._debounced_reload) + + def _debounced_reload(self): + self._reload_timer_id = None + self.reload() + return GLib.SOURCE_REMOVE @Property(bool, default_value=False) def style_changed(self) -> bool: @@ -24,18 +79,20 @@ def style_changed(self) -> bool: def reload(self, *_): try: - target_style = resolve_style_file("style.css") - - if os.path.isfile(target_style): - self.app.set_stylesheet_from_file( - file_path=target_style, + resolved_css = compile_resolved_stylesheet() + if resolved_css: + target_style = resolve_style_file("style.css") + base_dir = os.path.dirname(target_style) if os.path.isfile(target_style) else "." + self.app.set_stylesheet_from_string( + style_string=resolved_css, + compile=True, + base_path=base_dir, ) - + GLib.timeout_add(100, apply_plugin_css, self.app) self._style_changed = not self._style_changed - self.notify("style-changed") - + except Exception as e: - print(f"[StyleService] Error reloading styles: {e}") + logger.error(f"[StyleService] Error reloading styles: {e}") diff --git a/services/themes.py b/services/themes.py index 1a5241c..0e7bd09 100644 --- a/services/themes.py +++ b/services/themes.py @@ -1,6 +1,7 @@ import json import os import subprocess +import threading from fabric.core.service import Service, Signal, Property from gi.repository import GLib @@ -10,7 +11,7 @@ from .wallpaper import WallpaperService from .templates import template_service, MATUGEN_CONFIG_CACHE -from services.paths import get_cache_path, get_theme_dirs +from services.paths import get_cache_path, get_theme_dirs, get_user_config_dir CACHE_THEME_PATH = get_cache_path("theme.json") @@ -104,6 +105,9 @@ def __init__(self, **kwargs): self._connect_wallpaper_service() self._load_current_theme() + user_colors = os.path.join(get_user_config_dir(), "style", "colors.css") + if not os.path.isfile(user_colors) or os.path.getsize(user_colors) == 0: + self.apply() logger.info("[ThemeService] initialised") def _connect_wallpaper_service(self) -> None: @@ -218,7 +222,7 @@ def _on_wallpaper_changed(self, _service, _path: str) -> None: if getattr(self, "_matugen_timer_id", None) is not None: GLib.source_remove(self._matugen_timer_id) self._matugen_timer_id = None - self._matugen_timer_id = GLib.timeout_add(300, self._apply_debounced) + self._matugen_timer_id = GLib.timeout_add(50, self._apply_debounced) def _apply_debounced(self) -> bool: self._matugen_timer_id = None @@ -322,8 +326,8 @@ def _apply(self) -> bool: if active_name == WALLPAPER_THEME: wp_path = ( self._wallpaper_service.wallpaper_path - if self._wallpaper_service - else "" + if (self._wallpaper_service and self._wallpaper_service.wallpaper_path) + else getattr(user_options.wallpaper, "path", "") ) if not wp_path or not os.path.isfile(wp_path): logger.warning("[ThemeService] no wallpaper set, cannot apply Matugen mode") @@ -359,7 +363,21 @@ def _apply(self) -> bool: cmd += ["-c", MATUGEN_CONFIG_CACHE] logger.info(f"[ThemeService] running: {' '.join(cmd)}") - subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + def _run_matugen(): + try: + res = subprocess.run(cmd, capture_output=True, text=True) + if res.returncode != 0: + logger.error(f"[ThemeService] matugen failed (code {res.returncode}): {res.stderr}") + else: + logger.info(f"[ThemeService] matugen generated theme successfully") + from . import singletons + if hasattr(singletons, "style_service") and singletons.style_service: + GLib.idle_add(singletons.style_service.reload) + except Exception as e: + logger.error(f"[ThemeService] unexpected error running matugen: {e}") + + threading.Thread(target=_run_matugen, daemon=True).start() logger.info(f"[ThemeService] launched matugen: mode={mode}, theme={active_name}") except FileNotFoundError: