Files
VPNTray/views/log_view.py
2025-09-07 23:33:55 +02:00

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)