import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from models import ServiceType, HostType from utils import IconLoader def escape_markup(text: str) -> str: """Escape special characters for Pango markup.""" return text.replace('&', '&').replace('<', '<').replace('>', '>') class HostItem: def __init__(self, host, location, open_service_callback): self.host = host self.location = location self.open_service_callback = open_service_callback self.widget = self._create_widget() def _create_widget(self): # Clean horizontal layout without borders host_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) # Host header host_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) host_box.pack_start(host_header, False, False, 0) # Host icon - custom or fallback to Material Icons icon_widget = IconLoader.get_host_icon_widget(self.host, size=24) icon_container = Gtk.Box() icon_container.set_size_request(32, 24) # Fixed size icon_container.set_center_widget(icon_widget) host_header.pack_start(icon_container, False, False, 0) # Host details - compact single line details_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1) host_header.pack_start(details_vbox, True, True, 0) # Host name with IP inline name_label = Gtk.Label() ip_display = self.host.get_ip_display() escaped_host_name = escape_markup(self.host.name) escaped_host_type = escape_markup(self.host.host_type.value) escaped_ip_display = escape_markup(ip_display) name_label.set_markup(f"{escaped_host_name} ({escaped_host_type}) - {escaped_ip_display}") name_label.set_halign(Gtk.Align.START) if len(self.host.ip_addresses) > 1: name_label.set_tooltip_text(f"All IPs: {', '.join(self.host.get_all_ips())}") details_vbox.pack_start(name_label, False, False, 0) # Services section - compact button row if self.host.services: services_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) services_box.set_margin_start(16) # Indent services services_box.set_margin_top(4) host_box.pack_start(services_box, False, False, 0) for service in self.host.services: if service.service_type in [ServiceType.WEB_GUI, ServiceType.SSH, ServiceType.RDP]: # Only show launchable services # Check if service is reachable is_reachable = self.location.is_service_reachable(self.host, service) is_external = self.location.get_external_url_for_service(self.host, service) is not None service_btn = Gtk.Button(label=service.service_type.value) # Apply color-based styling if is_reachable: # Green styling for accessible services service_btn.get_style_context().add_class("suggested-action") service_btn.set_name("service-btn-accessible") if is_external and not self.location.connected: external_url = self.location.get_external_url_for_service(self.host, service) service_btn.set_tooltip_text(f"Open {service.name}\nExternal: {external_url}") else: service_btn.set_tooltip_text(f"Open {service.name}") else: # Red styling for inaccessible services service_btn.get_style_context().add_class("destructive-action") service_btn.set_name("service-btn-inaccessible") service_btn.set_tooltip_text(f"{service.name} - Not reachable (VPN disconnected)") # Enable/disable based on reachability service_btn.set_sensitive(is_reachable) # Connect handler only if reachable if is_reachable: service_btn.connect("clicked", lambda btn, s=service: self._on_service_clicked(s)) services_box.pack_start(service_btn, False, False, 0) # Sub-hosts (VMs) section if self.host.sub_hosts: subhost_label = Gtk.Label() subhost_label.set_markup(f"Virtual Machines ({len(self.host.sub_hosts)})") subhost_label.set_halign(Gtk.Align.START) subhost_label.set_margin_top(8) subhost_label.set_margin_start(16) host_box.pack_start(subhost_label, False, False, 0) subhosts_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) subhosts_box.set_margin_start(32) # Double indent for VMs host_box.pack_start(subhosts_box, False, False, 0) for subhost in self.host.sub_hosts: subhost_item = HostItem(subhost, self.location, self.open_service_callback) subhosts_box.pack_start(subhost_item.widget, False, False, 0) return host_box def _on_service_clicked(self, service): self.open_service_callback(service)