diff --git a/src/services/config.py b/src/services/config.py index 653b189..74f8d90 100644 --- a/src/services/config.py +++ b/src/services/config.py @@ -40,6 +40,7 @@ class ConfigService: self.RELOAD_DELAY_MS: int = 100 self._load_config() + self._last_notified_config = self._config.copy() self._setup_monitors() def get(self, key: str, default: Any = None) -> Any: @@ -51,6 +52,24 @@ class ConfigService: def has_changed(self, key: str, old_config: Dict[str, Any]) -> bool: return self._config.get(key) != old_config.get(key) + def set(self, key: str, value: Any) -> None: + """Set a config value (local state only).""" + self._config[key] = value + + def save(self) -> bool: + """Persist the current config state to disk and trigger reloads.""" + try: + os.makedirs(os.path.dirname(self._config_file), exist_ok=True) + with open(self._config_file, "w") as f: + json.dump(self._config, f, indent=4) + + # Manually trigger a reload to notify listeners immediately + GLib.idle_add(self._reload_config) + return True + except Exception as e: + logger.error(f"[ConfigService] Failed to save config: {e}") + return False + def register_reload_callback( self, callback: Callable[[Dict[str, Any], Dict[str, Any]], None] ) -> None: @@ -97,7 +116,7 @@ class ConfigService: self._config = {} def _setup_monitors(self) -> None: - files_to_watch = [self._config_file] + files_to_watch = [self._config_file, os.path.dirname(self._config_file)] for file_path in files_to_watch: if not os.path.exists(file_path): continue @@ -110,26 +129,32 @@ class ConfigService: logger.error(f"[ConfigService] Failed to monitor {file_path}: {e}") def _on_file_changed(self, monitor, file, other_file, event_type, file_path: str): - if ( - event_type == Gio.FileMonitorEvent.CHANGES_DONE_HINT - and not self._reload_pending - ): + # Trigger on various change events to be more robust across editors/OSs + valid_events = [ + Gio.FileMonitorEvent.CHANGES_DONE_HINT, + Gio.FileMonitorEvent.CHANGED, + Gio.FileMonitorEvent.CREATED, + Gio.FileMonitorEvent.DELETED, + ] + if event_type in valid_events and not self._reload_pending: self._reload_pending = True GLib.timeout_add(self.RELOAD_DELAY_MS, self._reload_config) def _reload_config(self) -> bool: try: - old_config = self._config.copy() + self._reload_pending = False + old_config = self._last_notified_config.copy() self._load_config() + # Always notify listeners on reload request to be safe for callback in list(self._reload_callbacks): try: callback(self._config, old_config) except Exception as e: logger.error(f"[ConfigService] Callback failed: {e}") - self._reload_pending = False - return True + self._last_notified_config = self._config.copy() + return False except Exception as e: logger.error(f"[ConfigService] Failed to reload configuration: {e}") self._reload_pending = False @@ -164,8 +189,8 @@ def get_config(key: str, default: Any = None) -> Any: return start_config_service().get(key, default) -def get_all_config() -> Dict[str, Any]: - """Get all config values from the singleton service.""" +def get_config_all() -> Dict[str, Any]: + """Get the entire current configuration state.""" return start_config_service().get_all() diff --git a/src/services/custom_notification.py b/src/services/custom_notification.py index c75e89c..c7cf3c3 100644 --- a/src/services/custom_notification.py +++ b/src/services/custom_notification.py @@ -11,6 +11,7 @@ from fabric.notifications import ( from fabric.utils import GdkPixbuf, logger, os, time import shared.data as data +from services.config import config, on_config_change NOTIFICATION_CACHE_FILE = f"{data.CACHE_DIR}/notification_history.json" @@ -222,11 +223,43 @@ class CachedNotifications(Notifications): self.load_cached_notifications() - # Connect to the notification_added signal to cache new notifications - # Note: self here refers to the CachedNotifications service, which inherits from Notifications - # So we connect to our own notification_added signal super().notification_added.connect(self.on_notification_added) + # Listen for config changes to prune history if needed + on_config_change(self._on_config_changed) + + def _on_config_changed(self, new_config, old_config): + # If ignored apps changed, remove them from history + if config().has_changed("notification_ignored_apps", old_config): + ignored_apps = new_config.get("notification_ignored_apps", []) + ids_to_remove = [ + cid + for cid, cnotif in self._cached_notifications.items() + if cnotif.app_name in ignored_apps + ] + for cid in ids_to_remove: + self.remove_cached_notification(cid) + if ids_to_remove: + logger.debug( + f"Hot-reload: Removed {len(ids_to_remove)} notifications from ignored apps" + ) + + # If limited apps changed, prune duplicates + if config().has_changed("notification_limited_apps_history", old_config): + limited_apps = new_config.get("notification_limited_apps_history", []) + for app in limited_apps: + app_notifs = [ + (cid, cnotif.timestamp) + for cid, cnotif in self._cached_notifications.items() + if cnotif.app_name == app + ] + if len(app_notifs) > 1: + # Sort by timestamp descending and keep only the first (latest) + app_notifs.sort(key=lambda x: x[1], reverse=True) + for cid, _ in app_notifs[1:]: + self.remove_cached_notification(cid) + logger.debug(f"Hot-reload: Pruned history for limited app {app}") + def load_cached_notifications(self) -> dict[int, CachedNotification]: """Load cached notifications from a JSON file (deserialization).""" try: @@ -309,7 +342,8 @@ class CachedNotifications(Notifications): ) # Check if this app should be ignored for history (don't cache) - if notification.app_name in data.NOTIFICATION_IGNORED_APPS_HISTORY: + ignored_apps = config().get("notification_ignored_apps", []) + if notification.app_name in ignored_apps: # Don't cache notifications from ignored apps, but still allow popup display logger.debug( f"Ignoring notification from {notification.app_name} (in ignore list)" @@ -347,6 +381,20 @@ class CachedNotifications(Notifications): f"Caching new notification: ID={notification.id}, App={notification.app_name}, Summary={notification.summary[:50]}..." ) + # Handle limited history apps - remove previous notifications from same app + limited_apps = config().get("notification_limited_apps_history", []) + if notification.app_name in limited_apps: + ids_to_remove = [ + cid + for cid, cnotif in self._cached_notifications.items() + if cnotif.app_name == notification.app_name + ] + for cid in ids_to_remove: + self.remove_cached_notification(cid) + logger.debug( + f"Pruned {len(ids_to_remove)} old notifications for limited app {notification.app_name}" + ) + # GUARANTEED STORAGE: Always create and store notification to history first cache_id = self._next_cache_id self._next_cache_id += 1