Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lockscreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
79 changes: 68 additions & 11 deletions services/style.py
Original file line number Diff line number Diff line change
@@ -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 "<file>" 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):
Expand All @@ -12,30 +48,51 @@ 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:
return self._style_changed

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}")
28 changes: 23 additions & 5 deletions services/themes.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand Down