# Copyright (C) 2025 James Brotosky, Brandon Wickline # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import logging import logging.config import logging.handlers import os from pathlib import Path import platform from dotenv import load_dotenv, set_key from utils.configmanager import ( get_system_value, load_system_config, load_user_config, ) class TextualNotificationHandler(logging.Handler): """ Custom logging handler that sends ERROR, WARNING, and CRITICAL logs to Textual toast notifications. """ def __init__(self, app): super().__init__() self.app = app def emit(self, record): try: # Only handle ERROR, WARNING, and CRITICAL if record.levelno >= logging.WARNING: # Format the message msg = self.format(record) # Map log levels to Textual severity severity_map = { logging.WARNING: "warning", logging.ERROR: "error", logging.CRITICAL: "error", } severity = severity_map.get(record.levelno, "information") # Send to Textual notification # Use call_from_thread if logging from non-main thread try: self.app.notify(msg, severity=severity, timeout=5) except Exception: # If we're not on the main thread, schedule it try: self.app.call_from_thread( self.app.notify, msg, severity=severity, timeout=5 ) except Exception: # Silently fail to avoid breaking the logging system pass except Exception: # Silently fail to avoid breaking the logging system pass def get_base_directory() -> Path: system = platform.system() home = Path.home() if system == "Windows": return Path(os.getenv("APPDATA", home / "AppData" / "Roaming")) / "Loxide" elif system == "Darwin": return home / "Library" / "Application Support" / "Loxide" else: return home / ".local" / "share" / "Loxide" def configure_logging(log_dir: Path, log_level: str = "INFO"): log_file = log_dir / "Loxide.log" config = { "version": 1, "disable_existing_loggers": False, "formatters": { "detailed": { "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" }, "simple": {"format": "%(levelname)s - %(message)s"}, "toast": {"format": "%(name)s: %(message)s"}, # Simpler format for toasts }, "handlers": { "file": { "class": "logging.handlers.TimedRotatingFileHandler", "filename": str(log_file), "when": "midnight", "interval": 1, "backupCount": 7, "encoding": "utf-8", "level": "DEBUG", "formatter": "detailed", }, # REMOVED console handler - it interferes with Textual TUI }, "root": { "level": "DEBUG", "handlers": ["file"], # Only use file handler, not console }, } # Add Windows Event Log handler if on Windows if platform.system() == "Windows": try: config["handlers"]["eventlog"] = { "class": "logging.handlers.NTEventLogHandler", "appname": "Loxide", # Event log source name "level": "CRITICAL", # Only log critical errors "formatter": "simple", # Use simple format } config["root"]["handlers"].append("eventlog") except Exception as e: logging.warning(f"Could not attach Windows Event Log handler: {e}") # Apply the logging configuration logging.config.dictConfig(config) logging.getLogger().debug("✅ Logging configured.") # Return a function to attach the notification handler once the app is created def attach_notification_handler(app): """Attach the Textual notification handler to the root logger.""" handler = TextualNotificationHandler(app) handler.setLevel(logging.WARNING) # Only WARNING and above formatter = logging.Formatter("%(name)s: %(message)s") handler.setFormatter(formatter) logging.getLogger().addHandler(handler) logging.getLogger().debug("✅ Textual notification handler attached.") return attach_notification_handler def setup(): """ Initialize the application environment: 1. Create directory structure 2. Load system config (immutable, from system_config.json) 3. Load user config (mutable, from user_config.json) 4. Configure logging 5. Set up .env with WORKING_DIR only Returns: attach_notification_handler: Function to attach notification handler to TUI app """ base_dir = get_base_directory() dirs = { "config": base_dir / "config", "cache": base_dir / "cache", "logs": base_dir / "logs", } for name, path in dirs.items(): path.mkdir(parents=True, exist_ok=True) logging.debug(f"{name.capitalize()} directory ensured at: {path}") # Load system config (immutable) load_system_config() # Configure logging with system-defined log level log_level = get_system_value("LOG_LEVEL", str, "INFO") attach_handler = configure_logging(dirs["logs"], log_level) # Load user config (mutable) load_user_config(dirs["config"]) # Set up .env file - ONLY for WORKING_DIR (runtime-configurable value) env_path = base_dir / ".env" if not env_path.exists(): env_path.touch() load_dotenv(dotenv_path=env_path, override=True) # Set up working directory (only dynamic value in .env) working_dir = Path(os.getenv("WORKING_DIR") or (base_dir / "data")) working_dir.mkdir(parents=True, exist_ok=True) set_key(str(env_path), "WORKING_DIR", str(working_dir)) os.environ["WORKING_DIR"] = str(working_dir) logging.debug(f"Working directory set to: {working_dir}") # Create folder structure in working directory folders_structure = { "Approved": [], "Needs_Review": ["Review_First", "Review_Second", "HTML"], "Preflight": ["HTML"], } for folder_name, subfolders in folders_structure.items(): folder_path = working_dir / folder_name folder_path.mkdir(parents=True, exist_ok=True) logging.debug(f"'{folder_name}' folder ensured at: {folder_path}") for subfolder in subfolders: subfolder_path = folder_path / subfolder subfolder_path.mkdir(parents=True, exist_ok=True) logging.debug(f"'{subfolder}' subfolder created at: {subfolder_path}") logging.info("✅ Setup complete") # Return the attach handler function return attach_handler