This commit is contained in:
2025-09-07 23:33:55 +02:00
parent d918f1e497
commit fbacfde9f2
33 changed files with 2626 additions and 1236 deletions

275
views/log_view.py Normal file
View File

@@ -0,0 +1,275 @@
"""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)