stuff
This commit is contained in:
275
views/log_view.py
Normal file
275
views/log_view.py
Normal 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)
|
||||
Reference in New Issue
Block a user