# -*- coding: utf-8 -*- """ Logger Dock: Real-time system logs display. """ import tkinter as tk from tkinter import ttk, scrolledtext import logging from pymsc.gui.docking.title_panel import TitlePanel class LoggerDock(TitlePanel): """ Logger dockable panel with scrollable text display. Shows real-time logs from the ARTOS system. """ def __init__(self, parent: tk.Widget, existing_widget=None, existing_handler=None, **kwargs): """ Args: parent: Parent widget existing_widget: Optional pre-created ScrolledText widget to use existing_handler: Optional pre-created TextWidgetHandler to use """ self.existing_widget = existing_widget self.existing_handler = existing_handler super().__init__(parent, title="System Logger", closable=False, **kwargs) self.log_handler = existing_handler def populate_content(self): """Create or reuse the scrollable log text widget.""" # Remove outer padding from the content frame so the text widget # can occupy the full dock area without an unwanted gap. try: self.content_frame.configure(padding=0) except Exception: pass if self.existing_widget: # Try to reparent the existing widget. If that doesn't render # correctly (some platforms don't fully support reparenting), # create a new ScrolledText inside our content_frame and copy # the text/tags across, then update the handler to point to it. old_widget = self.existing_widget # Read existing content (if any) and current view try: old_text = old_widget.get('1.0', tk.END) except Exception: old_text = '' # Remove old widget from geometry managers try: old_widget.pack_forget() except Exception: pass # Create a fresh ScrolledText in our content frame self.log_text = scrolledtext.ScrolledText( self.content_frame, wrap=tk.WORD, height=10, font=('Consolas', 9), bg='#ffffff', fg='#000000', state='disabled' ) # Configure tags for different log levels on the new widget self.log_text.tag_config('INFO', foreground='#0066cc') self.log_text.tag_config('WARNING', foreground='#ff8800') self.log_text.tag_config('ERROR', foreground='#cc0000') self.log_text.tag_config('DEBUG', foreground='#666666') # Insert previous content into the new widget try: self.log_text.config(state='normal') if old_text: self.log_text.insert(tk.END, old_text) self.log_text.see(tk.END) self.log_text.config(state='disabled') except Exception: pass # Pack the new widget to fill area self.log_text.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Update or create the handler so logs go to the new widget if self.existing_handler: try: self.existing_handler.text_widget = self.log_text self.log_handler = self.existing_handler except Exception: self.log_handler = TextWidgetHandler(self.log_text) logging.getLogger().addHandler(self.log_handler) else: self.log_handler = TextWidgetHandler(self.log_text) logging.getLogger().addHandler(self.log_handler) # Load previous logs from file to show startup messages self._load_previous_logs() # Log that we've transferred logger = logging.getLogger('ARTOS') logger.info("System Logger dock created - continuing to capture logs") else: # Create new widget (fallback if called without existing widget) self.log_text = scrolledtext.ScrolledText( self.content_frame, wrap=tk.WORD, height=10, font=('Consolas', 9), bg='#ffffff', fg='#000000', state='disabled' # Read-only ) # Pack without extra padding so it fills the dock content fully self.log_text.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Configure tags for different log levels self.log_text.tag_config('INFO', foreground='#0066cc') self.log_text.tag_config('WARNING', foreground='#ff8800') self.log_text.tag_config('ERROR', foreground='#cc0000') self.log_text.tag_config('DEBUG', foreground='#666666') # Create custom log handler that writes to this widget self.log_handler = TextWidgetHandler(self.log_text) self.log_handler.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') self.log_handler.setFormatter(formatter) # Add handler to root logger logging.getLogger().addHandler(self.log_handler) # Add initial message self.append_log("System Logger initialized. Ready to display logs.", 'INFO') def _load_previous_logs(self): """Load and display logs from the log file that were written before GUI started.""" import os log_file = 'logs/artos.log' if not os.path.exists(log_file): return try: with open(log_file, 'r', encoding='utf-8') as f: lines = f.readlines() # Load last 100 lines to avoid overwhelming the widget recent_lines = lines[-100:] if len(lines) > 100 else lines self.log_text.config(state='normal') for line in recent_lines: line = line.rstrip('\n') if not line: continue # Detect log level from line content level = 'INFO' if ' - ERROR - ' in line: level = 'ERROR' elif ' - WARNING - ' in line: level = 'WARNING' elif ' - DEBUG - ' in line: level = 'DEBUG' self.log_text.insert(tk.END, line + '\n', level) self.log_text.see(tk.END) self.log_text.config(state='disabled') except Exception as e: # Silently ignore if we can't read the file pass def append_log(self, message: str, level: str = 'INFO'): """ Append a log message to the text widget. Args: message: Log message text level: Log level (INFO, WARNING, ERROR, DEBUG) """ self.log_text.config(state='normal') self.log_text.insert(tk.END, message + '\n', level) self.log_text.see(tk.END) # Auto-scroll to bottom self.log_text.config(state='disabled') class TextWidgetHandler(logging.Handler): """ Custom logging handler that outputs to a Tkinter Text widget. """ def __init__(self, text_widget): super().__init__() self.text_widget = text_widget def emit(self, record): """Emit a log record to the text widget.""" try: msg = self.format(record) level = record.levelname # Thread-safe update self.text_widget.after(0, self._append_log, msg, level) except Exception: self.handleError(record) def _append_log(self, message, level): """Append message to text widget (must be called from main thread).""" self.text_widget.config(state='normal') self.text_widget.insert(tk.END, message + '\n', level) self.text_widget.see(tk.END) self.text_widget.config(state='disabled')