#!/usr/bin/env python3 import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GLib import sys import threading import pystray from PIL import Image, ImageDraw from models import Customer from data_loader import load_customers from widgets import ActiveCustomerCard, InactiveCustomerCard class VPNManagerWindow: def __init__(self): self.customers = load_customers() self.filtered_customers = self.customers.copy() # Create main window self.window = Gtk.Window() self.window.set_title("VPN Manager") self.window.set_default_size(1200, 750) self.window.connect("delete-event", self.quit_app_from_close) self.window.connect("window-state-event", self.on_window_state_event) # Set up minimal CSS for GNOME-style cards self.setup_css() # Create UI self.setup_ui() self.setup_system_tray() # Start hidden self.window.hide() def setup_css(self): """Minimal CSS for GNOME-style cards""" css_provider = Gtk.CssProvider() css = """ .card { background: @theme_base_color; border-radius: 8px; border: 1px solid @borders; box-shadow: 0 1px 3px rgba(0,0,0,0.1); padding: 16px; margin: 6px; } """ css_provider.load_from_data(css.encode()) # Apply CSS to default screen screen = Gdk.Screen.get_default() style_context = Gtk.StyleContext() style_context.add_provider_for_screen( screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) def setup_ui(self): # Use HeaderBar for native GNOME look header_bar = Gtk.HeaderBar() header_bar.set_show_close_button(True) header_bar.set_title("VPN Manager") header_bar.set_subtitle("Connection Manager") self.window.set_titlebar(header_bar) # Main container with proper spacing main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) main_vbox.set_margin_start(12) main_vbox.set_margin_end(12) main_vbox.set_margin_top(12) main_vbox.set_margin_bottom(12) self.window.add(main_vbox) # Search bar with SearchEntry self.search_entry = Gtk.SearchEntry() self.search_entry.set_placeholder_text("Search customers, locations, or hosts...") self.search_entry.connect("search-changed", self.filter_customers) main_vbox.pack_start(self.search_entry, False, False, 0) # Clean two-column layout like GNOME Control Center columns_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=24) main_vbox.pack_start(columns_box, True, True, 0) # Left column - Active customers left_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) columns_box.pack_start(left_vbox, True, True, 0) # Simple label header active_label = Gtk.Label() active_label.set_markup("Active Customers") active_label.set_halign(Gtk.Align.START) left_vbox.pack_start(active_label, False, False, 0) # Clean scrolled window without borders active_scrolled = Gtk.ScrolledWindow() active_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) active_scrolled.set_shadow_type(Gtk.ShadowType.NONE) left_vbox.pack_start(active_scrolled, True, True, 0) self.active_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) active_scrolled.add(self.active_box) # Right column - Inactive customers right_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) columns_box.pack_start(right_vbox, True, True, 0) # Simple label header inactive_label = Gtk.Label() inactive_label.set_markup("Inactive Customers") inactive_label.set_halign(Gtk.Align.START) right_vbox.pack_start(inactive_label, False, False, 0) # Clean scrolled window without borders inactive_scrolled = Gtk.ScrolledWindow() inactive_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) inactive_scrolled.set_shadow_type(Gtk.ShadowType.NONE) right_vbox.pack_start(inactive_scrolled, True, True, 0) self.inactive_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) inactive_scrolled.add(self.inactive_box) # Render initial data self.render_customers() def setup_system_tray(self): # Create a simple icon for the system tray def create_icon(): # Create a simple network icon width = height = 64 image = Image.new('RGBA', (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(image) # Draw a simple network/VPN icon # Outer circle draw.ellipse([8, 8, 56, 56], outline=(50, 150, 50), width=4) # Inner dot draw.ellipse([26, 26, 38, 38], fill=(50, 150, 50)) # Connection lines draw.line([32, 16, 32, 24], fill=(50, 150, 50), width=3) draw.line([32, 40, 32, 48], fill=(50, 150, 50), width=3) draw.line([16, 32, 24, 32], fill=(50, 150, 50), width=3) draw.line([40, 32, 48, 32], fill=(50, 150, 50), width=3) return image # Simple approach: Create tray icon with direct action and minimal menu self.tray_icon = pystray.Icon( "VPN Manager", create_icon(), "VPN Manager - Double-click to open" ) # Set direct click action self.tray_icon.default_action = self.show_window_from_tray # Also provide a right-click menu menu = pystray.Menu( pystray.MenuItem("Open VPN Manager", self.show_window_from_tray, default=True), pystray.MenuItem("Quit", self.quit_app) ) self.tray_icon.menu = menu # Start tray icon in separate thread threading.Thread(target=self.tray_icon.run, daemon=True).start() def get_callbacks(self): """Return callback functions for widget interactions""" return { 'toggle_connection': self.toggle_connection, 'set_location_active': self.set_location_active, 'deactivate_location': self.deactivate_location, 'open_service': self.open_service, 'open_customer_service': self.open_customer_service } def render_customers(self): # Clear existing content for child in self.active_box.get_children(): child.destroy() for child in self.inactive_box.get_children(): child.destroy() # Separate customers with active and inactive locations customers_with_active = [] customers_with_inactive = [] for customer in self.filtered_customers: active_locations = customer.get_active_locations() inactive_locations = customer.get_inactive_locations() if active_locations: from models import Customer customer_data = Customer(name=customer.name) customer_data.services = customer.services customer_data.locations = active_locations customers_with_active.append(customer_data) if inactive_locations: from models import Customer customer_data = Customer(name=customer.name) customer_data.services = customer.services customer_data.locations = inactive_locations customers_with_inactive.append(customer_data) # Get callbacks for widgets callbacks = self.get_callbacks() # Render active customers using widget classes for customer in customers_with_active: customer_card = ActiveCustomerCard(customer, callbacks) self.active_box.pack_start(customer_card.widget, False, False, 0) # Render inactive customers using widget classes for customer in customers_with_inactive: customer_card = InactiveCustomerCard(customer, callbacks) self.inactive_box.pack_start(customer_card.widget, False, False, 0) self.window.show_all() def set_location_active(self, location, customer_name): for customer in self.customers: if customer.name == customer_name: target_location = customer.get_location_by_name(location.name) if target_location: target_location.active = True print(f"Mock: Setting {customer.name} - {target_location.name} as active") break self.render_customers() def deactivate_location(self, location, customer_name): for customer in self.customers: if customer.name == customer_name: target_location = customer.get_location_by_name(location.name) if target_location: target_location.active = False target_location.connected = False # Disconnect when deactivating print(f"Mock: Deactivating {customer.name} - {target_location.name}") break self.render_customers() def filter_customers(self, entry): search_term = entry.get_text().lower() if search_term: self.filtered_customers = [] for customer in self.customers: # Check if search term matches customer name if search_term in customer.name.lower(): self.filtered_customers.append(customer) continue # Check customer services if any(search_term in service.name.lower() or search_term in service.url.lower() or search_term in service.service_type.lower() for service in customer.services): self.filtered_customers.append(customer) continue # Check locations and their hosts for location in customer.locations: # Check location name if search_term in location.name.lower(): self.filtered_customers.append(customer) break # Check hosts and their services in this location def search_hosts(hosts): for host in hosts: if (search_term in host.name.lower() or search_term in host.ip_address.lower() or search_term in host.host_type.value.lower() or search_term in host.description.lower()): return True # Check host services if any(search_term in service.name.lower() or search_term in str(service.port).lower() or search_term in service.service_type.value.lower() for service in host.services): return True # Check sub-hosts recursively if search_hosts(host.sub_hosts): return True return False if search_hosts(location.hosts): self.filtered_customers.append(customer) break else: self.filtered_customers = self.customers.copy() self.render_customers() def toggle_connection(self, location): location.connected = not location.connected status = "connected to" if location.connected else "disconnected from" print(f"Mock: {status} {location.name} via {location.vpn_type.value}") self.render_customers() def open_service(self, service): # Get the host IP from context - this would need to be passed properly in a real implementation print(f"Mock: Opening {service.service_type.value} service: {service.name} on port {service.port}") def open_customer_service(self, customer_service): print(f"Mock: Opening customer service: {customer_service.name} at {customer_service.url}") def show_window_from_tray(self, _icon=None, _item=None): # Use GLib.idle_add to safely call GTK functions from the tray thread GLib.idle_add(self._show_window_safe) def _show_window_safe(self): """Safely show window in main GTK thread""" self.window.deiconify() self.window.present() self.window.show_all() return False # Don't repeat the idle call def on_window_state_event(self, _widget, event): """Handle window state changes - hide to tray when minimized""" if event.new_window_state & Gdk.WindowState.ICONIFIED: self.window.hide() return False def quit_app_from_close(self, _widget=None, _event=None): """Quit app when close button is pressed""" self.quit_app() return False def quit_app(self, _widget=None): # Stop the tray icon if hasattr(self, 'tray_icon'): self.tray_icon.stop() Gtk.main_quit() sys.exit(0) def run(self): self.window.show_all() Gtk.main() def main(): app = VPNManagerWindow() app.run() if __name__ == "__main__": main()