276 lines
9.1 KiB
Python
276 lines
9.1 KiB
Python
"""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("<b>📋 Command Log</b>")
|
|
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)
|