160 lines
5.5 KiB
Python
160 lines
5.5 KiB
Python
"""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)
|