"""Log view for displaying command output and system logs.""" from enum import Enum from typing import Optional import time from gi.repository import Gtk, GLib, Pango import gi gi.require_version('Gtk', '3.0') class LogLevel(Enum): """Log levels for different types of messages.""" DEBUG = "DEBUG" INFO = "INFO" WARNING = "WARNING" ERROR = "ERROR" COMMAND = "COMMAND" class LogView: """View for displaying logs and command output.""" def __init__(self): self.widget = self._create_widget() self.max_lines = 1000 # Maximum number of log lines to keep self.auto_scroll = True def _create_widget(self): """Create the main log view widget.""" # Main container vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) # Header with controls header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) header_box.set_margin_start(12) header_box.set_margin_end(12) header_box.set_margin_top(8) header_box.set_margin_bottom(8) vbox.pack_start(header_box, False, False, 0) # Log title log_label = Gtk.Label() log_label.set_markup("📋 Command Log") log_label.set_halign(Gtk.Align.START) header_box.pack_start(log_label, False, False, 0) # Spacer spacer = Gtk.Box() header_box.pack_start(spacer, True, True, 0) # Auto-scroll toggle self.autoscroll_switch = Gtk.Switch() self.autoscroll_switch.set_active(True) self.autoscroll_switch.connect( "notify::active", self._on_autoscroll_toggle) header_box.pack_start(self.autoscroll_switch, False, False, 0) autoscroll_label = Gtk.Label() autoscroll_label.set_text("Auto-scroll") autoscroll_label.set_margin_start(4) header_box.pack_start(autoscroll_label, False, False, 0) # Clear button clear_btn = Gtk.Button(label="Clear") clear_btn.connect("clicked", self._on_clear_clicked) header_box.pack_start(clear_btn, False, False, 0) # Separator separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) vbox.pack_start(separator, False, False, 0) # Scrolled window for log content scrolled = Gtk.ScrolledWindow() scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled.set_min_content_height(150) scrolled.set_max_content_height(400) vbox.pack_start(scrolled, True, True, 0) # Text view for log content self.text_view = Gtk.TextView() self.text_view.set_editable(False) self.text_view.set_cursor_visible(False) self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) # Set monospace font font_desc = Pango.FontDescription("monospace 9") self.text_view.modify_font(font_desc) scrolled.add(self.text_view) # Get text buffer and create tags for different log levels self.text_buffer = self.text_view.get_buffer() self._create_text_tags() # Store reference to scrolled window for auto-scrolling self.scrolled_window = scrolled return vbox def _create_text_tags(self): """Create text tags for different log levels.""" # Command tag (bold, blue) command_tag = self.text_buffer.create_tag("command") command_tag.set_property("weight", Pango.Weight.BOLD) command_tag.set_property("foreground", "#0066cc") # Info tag (default) info_tag = self.text_buffer.create_tag("info") # Warning tag (orange) warning_tag = self.text_buffer.create_tag("warning") warning_tag.set_property("foreground", "#ff8800") # Error tag (red, bold) error_tag = self.text_buffer.create_tag("error") error_tag.set_property("foreground", "#cc0000") error_tag.set_property("weight", Pango.Weight.BOLD) # Debug tag (gray) debug_tag = self.text_buffer.create_tag("debug") debug_tag.set_property("foreground", "#666666") # Timestamp tag (small, gray) timestamp_tag = self.text_buffer.create_tag("timestamp") timestamp_tag.set_property("foreground", "#888888") timestamp_tag.set_property("size", 8 * Pango.SCALE) def _on_autoscroll_toggle(self, switch, gparam): """Handle auto-scroll toggle.""" self.auto_scroll = switch.get_active() def _on_clear_clicked(self, button): """Clear the log content.""" self.text_buffer.set_text("") def _auto_scroll_to_bottom(self): """Scroll to bottom if auto-scroll is enabled.""" if not self.auto_scroll: return # Get the end iterator end_iter = self.text_buffer.get_end_iter() # Create a mark at the end mark = self.text_buffer.get_insert() self.text_buffer.place_cursor(end_iter) # Scroll to the mark self.text_view.scroll_mark_onscreen(mark) def _get_timestamp(self) -> str: """Get current timestamp string.""" return time.strftime("%H:%M:%S") def _trim_log_if_needed(self): """Trim log to max_lines if exceeded.""" line_count = self.text_buffer.get_line_count() if line_count <= self.max_lines: return # Calculate how many lines to remove (keep some buffer) lines_to_remove = line_count - (self.max_lines - 100) # Get iterator at start start_iter = self.text_buffer.get_start_iter() # Move to the line we want to keep end_iter = self.text_buffer.get_iter_at_line(lines_to_remove) # Delete the old lines self.text_buffer.delete(start_iter, end_iter) def log_message(self, message: str, level: LogLevel = LogLevel.INFO, command: Optional[str] = None): """Add a log message to the view. Args: message: The message to log level: The log level command: Optional command that generated this message """ # Ensure we're on the main thread GLib.idle_add(self._add_log_message, message, level, command) def _add_log_message(self, message: str, level: LogLevel, command: Optional[str]): """Add log message to buffer (main thread only).""" timestamp = self._get_timestamp() # Get end iterator end_iter = self.text_buffer.get_end_iter() # Add timestamp self.text_buffer.insert_with_tags_by_name( end_iter, f"[{timestamp}] ", "timestamp" ) # Add command if provided if command: end_iter = self.text_buffer.get_end_iter() self.text_buffer.insert_with_tags_by_name( end_iter, f"$ {command}\n", "command" ) # Add the message with appropriate tag end_iter = self.text_buffer.get_end_iter() tag_name = level.value.lower() self.text_buffer.insert_with_tags_by_name( end_iter, f"{message}\n", tag_name ) # Trim log if needed self._trim_log_if_needed() # Auto-scroll to bottom self._auto_scroll_to_bottom() return False # Remove from idle queue def log_command(self, command: str, output: str = "", error: str = "", return_code: int = 0): """Log a command execution with its output. Args: command: The command that was executed output: Standard output from the command error: Standard error from the command return_code: Command return code """ # Log the command self.log_message("", LogLevel.COMMAND, command) # Log output if present if output.strip(): for line in output.strip().split('\n'): self.log_message(line, LogLevel.INFO) # Log error if present if error.strip(): for line in error.strip().split('\n'): self.log_message(f"ERROR: {line}", LogLevel.ERROR) # Log return code if non-zero if return_code != 0: self.log_message( f"Command exited with code: {return_code}", LogLevel.ERROR) elif return_code == 0 and (output.strip() or error.strip()): self.log_message("Command completed successfully", LogLevel.INFO) def log_info(self, message: str): """Log an info message.""" self.log_message(message, LogLevel.INFO) def log_warning(self, message: str): """Log a warning message.""" self.log_message(message, LogLevel.WARNING) def log_error(self, message: str): """Log an error message.""" self.log_message(message, LogLevel.ERROR) def log_debug(self, message: str): """Log a debug message.""" self.log_message(message, LogLevel.DEBUG) def log_success(self, message: str): """Log a success message.""" self.log_message(f"✓ {message}", LogLevel.INFO) def set_visible(self, visible: bool): """Set visibility of the entire view.""" self.widget.set_visible(visible) def clear(self): """Clear all log content.""" self._on_clear_clicked(None)