"""Logger integration wrapper for ControlPanel GUI. Tries to load the external `python-tkinter-logger` module from `externals/python-tkinter-logger`. If available, uses its `TkinterLogger` implementation. Otherwise falls back to a minimal thread-safe Tkinter text handler so the UI still receives logs. API: - init_logger(root, **opts) -> object - add_widget(text_widget, level_colors=None, max_lines=1000) -> bool - shutdown() -> None - get_logger(name) -> logging.Logger """ from __future__ import annotations import logging import importlib import os import sys from typing import Optional, Dict import tkinter as tk _logger_system = None _simple_handler = None def _try_import_external() -> Optional[object]: """Attempts to import the external TkinterLogger module. Returns the imported module or None if import fails. """ try: # externals path (repo_root/externals/python-tkinter-logger) base = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) ext_path = os.path.join(base, "externals", "python-tkinter-logger") if os.path.isdir(ext_path) and ext_path not in sys.path: sys.path.insert(0, ext_path) module = importlib.import_module("tkinter_logger") return module except Exception: return None def init_logger(root: Optional[tk.Tk] = None, **opts) -> Optional[object]: """Initializes the logger system. If the external `TkinterLogger` is present it will be used; otherwise a minimal fallback is configured that writes to a `tk.Text` widget when `add_widget` is called. """ global _logger_system if _logger_system is not None: return _logger_system ext = _try_import_external() if ext is not None and hasattr(ext, "TkinterLogger"): Tk = getattr(ext, "TkinterLogger") try: instance = Tk(root) instance.setup(**{k: v for k, v in opts.items()}) _logger_system = instance return _logger_system except Exception: # Fall through to fallback pass # Fallback: basic configuration (console + handlers will be added when widget added) logging.basicConfig(level=opts.get("root_level", logging.INFO)) _logger_system = None return None def add_widget(text_widget: tk.Text, level_colors: Optional[Dict[int, str]] = None, max_lines: int = 1000) -> bool: """Adds a tkinter text widget as a log sink. If the external logger was loaded, delegates to its `add_tkinter_handler`. Otherwise attaches a simple handler to the root logger that writes into `text_widget` using `after` to remain thread-safe. """ global _logger_system, _simple_handler if _logger_system is not None: try: _logger_system.add_tkinter_handler(text_widget, level_colors=level_colors, max_lines=max_lines) return True except Exception: pass # Minimal fallback handler class _SimpleTkHandler(logging.Handler): def __init__(self, widget: tk.Text, max_lines: int = 1000): super().__init__() self.widget = widget self.max_lines = max_lines def emit(self, record: logging.LogRecord) -> None: try: msg = self.format(record) def _append(): try: if not self.widget.winfo_exists(): return self.widget.config(state=tk.NORMAL) self.widget.insert(tk.END, msg + "\n") # Trim lines total_lines = int(self.widget.index("end-1c").split(".")[0]) if total_lines > self.max_lines: delete_to = total_lines - self.max_lines self.widget.delete("1.0", f"{delete_to}.0") self.widget.see(tk.END) self.widget.config(state=tk.DISABLED) except Exception: pass # Schedule append on the Tk event loop try: self.widget.after(0, _append) except Exception: # If scheduling fails, try direct append (best effort) _append() except Exception: self.handleError(record) # Attach handler try: handler = _SimpleTkHandler(text_widget, max_lines=max_lines) formatter = logging.Formatter(fmt=opts.get("log_format", "%(asctime)s [%(levelname)s] %(name)s: %(message)s"), datefmt=opts.get("date_format", "%Y-%m-%d %H:%M:%S")) handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.addHandler(handler) _simple_handler = handler return True except Exception: return False def shutdown() -> None: """Shuts down the logger system (if external) and removes fallback handler.""" global _logger_system, _simple_handler try: if _logger_system is not None: try: _logger_system.shutdown() except Exception: pass _logger_system = None if _simple_handler is not None: try: logging.getLogger().removeHandler(_simple_handler) except Exception: pass _simple_handler = None finally: return def get_logger(name: str) -> logging.Logger: return logging.getLogger(name)