# RepoSync/utility/logger.py """ Configures and provides access to the application-wide logging system. This module sets up logging to the console, a file, and a thread-safe Tkinter widget. It is designed to be configured once at application startup. """ import logging import tkinter as tk from logging.handlers import RotatingFileHandler from queue import Queue, Empty as QueueEmpty from tkinter.scrolledtext import ScrolledText from typing import Optional, Dict, Any # A global instance to hold the handler, managed by setup/shutdown functions. _tkinter_handler_instance: Optional["TkinterTextHandler"] = None class TkinterTextHandler(logging.Handler): """ A logging handler that directs records to a Tkinter Text widget. It uses a thread-safe queue to avoid blocking the GUI and updates the widget periodically via the Tkinter event loop (`root.after`). """ def __init__( self, text_widget: tk.Text, root_tk_instance: tk.Tk, level_colors: Dict[int, str], queue_poll_interval_ms: int ): super().__init__() self.text_widget = text_widget self.root_tk_instance = root_tk_instance self.log_queue = Queue() self.level_colors = level_colors self.queue_poll_interval_ms = queue_poll_interval_ms self._after_id: Optional[str] = None self._is_active = True # Ensure widgets are valid before proceeding if not (self.text_widget and self.text_widget.winfo_exists()): print("Warning: TkinterTextHandler initialized with invalid text_widget.") self._is_active = False return # Configure color tags on the text widget for level, color in self.level_colors.items(): tag_name = logging.getLevelName(level) try: self.text_widget.tag_config(tag_name, foreground=color) except tk.TclError as e: print(f"Warning: Could not configure tag '{tag_name}': {e}") # Start the queue processing loop self._process_log_queue() def emit(self, record: logging.LogRecord): """Adds a log record to the queue to be processed by the GUI thread.""" if self._is_active and self.root_tk_instance.winfo_exists(): msg = self.format(record) self.log_queue.put_nowait((record.levelname, msg)) def _process_log_queue(self): """ Processes messages from the log queue and updates the text widget. Schedules itself to run again. """ if not self._is_active or not self.root_tk_instance.winfo_exists(): self.close() return try: # Process all available messages in the queue while True: level_name, msg = self.log_queue.get_nowait() self.text_widget.configure(state=tk.NORMAL) self.text_widget.insert(tk.END, msg + "\n", (level_name,)) self.text_widget.see(tk.END) self.text_widget.configure(state=tk.DISABLED) self.log_queue.task_done() except QueueEmpty: # Queue is empty, do nothing pass except Exception as e: # Handle unexpected errors during widget update print(f"Error updating log widget: {e}") self.close() # Stop processing on error return # Schedule the next check self._after_id = self.root_tk_instance.after( self.queue_poll_interval_ms, self._process_log_queue ) def close(self): """Stops the queue processor and cleans up resources.""" self._is_active = False if self._after_id and self.root_tk_instance.winfo_exists(): self.root_tk_instance.after_cancel(self._after_id) self._after_id = None super().close() def setup_logging( gui_log_widget: Optional[tk.Text] = None, root_tk_instance: Optional[tk.Tk] = None, config: Optional[Dict[str, Any]] = None, ): """ Sets up application-wide logging based on a configuration dictionary. Handles partial configurations by merging them with a set of defaults. """ global _tkinter_handler_instance # 1. Define a complete default configuration default_config = { "level": logging.INFO, "format": "%(asctime)s [%(levelname)-8s] (%(name)s) %(message)s", "date_format": "%H:%M:%S", "enable_console": True, "enable_file": False, "file_path": "RepoSync.log", "file_max_bytes": 5 * 1024 * 1024, "file_backup_count": 3, "colors": { logging.DEBUG: "gray", logging.INFO: "black", logging.WARNING: "orange", logging.ERROR: "red", logging.CRITICAL: "purple", }, "queue_poll_interval_ms": 100, } # 2. If a user config is provided, update the defaults with it if config: default_config.update(config) # From now on, use the merged 'default_config' which is guaranteed to be complete final_config = default_config formatter = logging.Formatter(final_config["format"], datefmt=final_config["date_format"]) root_logger = logging.getLogger() root_logger.setLevel(final_config["level"]) # Clear any existing handlers to avoid duplicates for handler in root_logger.handlers[:]: handler.close() root_logger.removeHandler(handler) _tkinter_handler_instance = None # Reset global instance # --- Console Handler --- if final_config["enable_console"]: console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) root_logger.addHandler(console_handler) # --- File Handler (Rotating) --- if final_config["enable_file"]: file_handler = RotatingFileHandler( final_config["file_path"], maxBytes=final_config["file_max_bytes"], backupCount=final_config["file_backup_count"], encoding='utf-8', ) file_handler.setFormatter(formatter) root_logger.addHandler(file_handler) # --- GUI Handler --- if gui_log_widget and root_tk_instance: if gui_log_widget.winfo_exists() and root_tk_instance.winfo_exists(): _tkinter_handler_instance = TkinterTextHandler( text_widget=gui_log_widget, root_tk_instance=root_tk_instance, level_colors=final_config["colors"], queue_poll_interval_ms=final_config["queue_poll_interval_ms"], ) _tkinter_handler_instance.setFormatter(formatter) root_logger.addHandler(_tkinter_handler_instance) else: print("Warning: Tkinter root or widget does not exist. GUI logger not created.") def shutdown_logging(): """Closes and removes all logging handlers gracefully.""" logging.shutdown()