214 lines
8.1 KiB
Python
214 lines
8.1 KiB
Python
# -*- 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')
|