feat(root): implement dynamic todo list refresh and UI enhancements
This change adds a mechanism to refresh the todo list dynamically when the underlying service updates, improving UI responsiveness and user experience. It also updates documentation and removes deprecated files. - New Features: - Added `_refresh_todos` method to rebuild the todo list when `get_todo_service().todos` changes. - Introduced `TodoListCapture` class for mouse capture handling of the todo list window. - Enhancements: - Updated README with corrected image paths, installation tips, and manual installation commands. - Introduced `start.py` to launch the application or lock mode. - Removed deprecated `config.json` and `debug_memory.py`. - Bug Fixes: - Fixed lock screen screenshot path in README.
This commit is contained in:
267 files changed
+24582
-394
No files matched your search
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Centralized configuration service with file watching and dynamic reloads.
|
||||
|
||||
This service encapsulates the logic for loading the application's JSON config,
|
||||
watching for file changes, and notifying registered listeners when the config
|
||||
changes. It is implemented as a singleton and intended to be reused by any
|
||||
module that needs dynamic configuration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from fabric.utils import Gio, GLib, get_relative_path, logger, os
|
||||
|
||||
|
||||
class ConfigService:
|
||||
"""Singleton service handling config state and reload notifications."""
|
||||
|
||||
_instance: Optional["ConfigService"] = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if getattr(self, "_initialized", False):
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
self._config: Dict[str, Any] = {}
|
||||
self._reload_callbacks: List[
|
||||
Callable[[Dict[str, Any], Dict[str, Any]], None]
|
||||
] = []
|
||||
self._config_file: str = get_relative_path("../../config/config.json")
|
||||
self._monitors: List[Gio.FileMonitor] = []
|
||||
self._reload_pending: bool = False
|
||||
self.RELOAD_DELAY_MS: int = 100
|
||||
|
||||
self._load_config()
|
||||
self._setup_monitors()
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
return self._config.get(key, default)
|
||||
|
||||
def get_all(self) -> Dict[str, Any]:
|
||||
return self._config.copy()
|
||||
|
||||
def has_changed(self, key: str, old_config: Dict[str, Any]) -> bool:
|
||||
return self._config.get(key) != old_config.get(key)
|
||||
|
||||
def register_reload_callback(
|
||||
self, callback: Callable[[Dict[str, Any], Dict[str, Any]], None]
|
||||
) -> None:
|
||||
if callback not in self._reload_callbacks:
|
||||
self._reload_callbacks.append(callback)
|
||||
|
||||
def unregister_reload_callback(
|
||||
self, callback: Callable[[Dict[str, Any], Dict[str, Any]], None]
|
||||
) -> None:
|
||||
if callback in self._reload_callbacks:
|
||||
self._reload_callbacks.remove(callback)
|
||||
|
||||
def stop(self) -> None:
|
||||
for monitor in self._monitors:
|
||||
try:
|
||||
monitor.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
self._monitors.clear()
|
||||
|
||||
def _load_config(self) -> None:
|
||||
try:
|
||||
if os.path.exists(self._config_file):
|
||||
with open(self._config_file, "r") as f:
|
||||
self._config = json.load(f)
|
||||
else:
|
||||
try:
|
||||
from utils.constants import DEFAULT
|
||||
|
||||
# Ensure the config directory exists
|
||||
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
|
||||
with open(self._config_file, "w") as f:
|
||||
json.dump(DEFAULT, f, indent=4)
|
||||
|
||||
self._config = DEFAULT.copy()
|
||||
logger.info(
|
||||
f"[ConfigService] Generated default config at {self._config_file}"
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error(f"[ConfigService] Failed to import defaults: {e}")
|
||||
self._config = {}
|
||||
except Exception as e:
|
||||
logger.error(f"[ConfigService] Failed to load config: {e}")
|
||||
self._config = {}
|
||||
|
||||
def _setup_monitors(self) -> None:
|
||||
files_to_watch = [self._config_file]
|
||||
for file_path in files_to_watch:
|
||||
if not os.path.exists(file_path):
|
||||
continue
|
||||
try:
|
||||
gio_file = Gio.File.new_for_path(file_path)
|
||||
monitor = gio_file.monitor_file(Gio.FileMonitorFlags.NONE, None)
|
||||
monitor.connect("changed", self._on_file_changed, file_path)
|
||||
self._monitors.append(monitor)
|
||||
except Exception as e:
|
||||
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
|
||||
):
|
||||
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._load_config()
|
||||
|
||||
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
|
||||
except Exception as e:
|
||||
logger.error(f"[ConfigService] Failed to reload configuration: {e}")
|
||||
self._reload_pending = False
|
||||
return False
|
||||
|
||||
|
||||
_service: Optional[ConfigService] = None
|
||||
|
||||
|
||||
def start_config_service() -> ConfigService:
|
||||
global _service
|
||||
if _service is None:
|
||||
_service = ConfigService()
|
||||
return _service
|
||||
|
||||
|
||||
def stop_config_service() -> None:
|
||||
global _service
|
||||
if _service is not None:
|
||||
_service.stop()
|
||||
_service = None
|
||||
|
||||
|
||||
# Convenience helpers for concise usage
|
||||
def config() -> ConfigService:
|
||||
"""Get the singleton service (concise alias)."""
|
||||
return start_config_service()
|
||||
|
||||
|
||||
def get_config(key: str, default: Any = None) -> Any:
|
||||
"""Get a single config value from the singleton service."""
|
||||
return start_config_service().get(key, default)
|
||||
|
||||
|
||||
def get_all_config() -> Dict[str, Any]:
|
||||
"""Get all config values from the singleton service."""
|
||||
return start_config_service().get_all()
|
||||
|
||||
|
||||
def on_config_change(
|
||||
callback: Callable[[Dict[str, Any], Dict[str, Any]], None],
|
||||
) -> None:
|
||||
"""Register a reload callback on the singleton service."""
|
||||
start_config_service().register_reload_callback(callback)
|
||||
Reference in new issue
Block a user