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

View File

@@ -1,4 +1,5 @@
from .active_view import ActiveView
from .inactive_view import InactiveView
from .log_view import LogView, LogLevel
__all__ = ['ActiveView', 'InactiveView']
__all__ = ['ActiveView', 'InactiveView', 'LogView', 'LogLevel']

View File

@@ -1,62 +1,65 @@
from widgets import ActiveCustomerCard
from gi.repository import Gtk
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from widgets import ActiveCustomerCard
class ActiveView:
"""View for displaying active customer locations."""
def __init__(self, callbacks):
self.callbacks = callbacks
self.widget = self._create_widget()
def _create_widget(self):
"""Create the main container for active locations."""
# Main container
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
# Scrolled window for content
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scrolled.set_shadow_type(Gtk.ShadowType.NONE)
vbox.pack_start(scrolled, True, True, 0)
# Content box
self.content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
self.content_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, spacing=12)
scrolled.add(self.content_box)
return vbox
def update(self, customers):
"""Update the view with new customer data.
Args:
customers: List of Customer objects with active locations to display
"""
# Clear existing content
for child in self.content_box.get_children():
child.destroy()
if customers:
# Add customer cards
for customer in customers:
customer_card = ActiveCustomerCard(customer, self.callbacks)
self.content_box.pack_start(customer_card.widget, False, False, 0)
self.content_box.pack_start(
customer_card.widget, False, False, 0)
else:
# Show empty state message
no_active_label = Gtk.Label()
no_active_label.set_markup("<span alpha='50%'>No active locations</span>")
no_active_label.set_markup(
"<span alpha='50%'>No active locations</span>")
no_active_label.set_margin_top(20)
self.content_box.pack_start(no_active_label, False, False, 0)
self.content_box.show_all()
def set_visible(self, visible):
"""Set visibility of the entire view."""
self.widget.set_visible(visible)
def clear(self):
"""Clear all content from the view."""
for child in self.content_box.get_children():
child.destroy()
child.destroy()

View File

@@ -1,52 +1,54 @@
from widgets import InactiveCustomerCard
from gi.repository import Gtk
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from widgets import InactiveCustomerCard
class InactiveView:
"""View for displaying inactive customer locations (search results)."""
def __init__(self, callbacks):
self.callbacks = callbacks
self.widget = self._create_widget()
self.current_search = ""
def _create_widget(self):
"""Create the main container for inactive/search results."""
# Main container
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
# Scrolled window for content
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scrolled.set_shadow_type(Gtk.ShadowType.NONE)
vbox.pack_start(scrolled, True, True, 0)
# Content box
self.content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
self.content_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, spacing=12)
scrolled.add(self.content_box)
return vbox
def update(self, customers, search_term=""):
"""Update the view with search results.
Args:
customers: List of Customer objects with inactive locations to display
search_term: The current search term
"""
self.current_search = search_term
# Clear existing content
for child in self.content_box.get_children():
child.destroy()
if customers:
# Add customer cards
for customer in customers:
customer_card = InactiveCustomerCard(customer, self.callbacks)
self.content_box.pack_start(customer_card.widget, False, False, 0)
self.content_box.pack_start(
customer_card.widget, False, False, 0)
else:
# Show no results message
if search_term:
@@ -56,14 +58,14 @@ class InactiveView:
)
no_results_label.set_margin_top(20)
self.content_box.pack_start(no_results_label, False, False, 0)
self.content_box.show_all()
def set_visible(self, visible):
"""Set visibility of the entire view."""
self.widget.set_visible(visible)
def clear(self):
"""Clear all content from the view."""
for child in self.content_box.get_children():
child.destroy()
child.destroy()

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)