#!/usr/bin/env python3 import gi gi.require_version('Gtk', '3.0') gi.require_version('AppIndicator3', '0.1') from gi.repository import Gtk, Gdk, GLib, Gio, AppIndicator3 import threading import sys from models import Customer, Location, Host from data_loader import load_customers 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.hide_window) # Set up CSS for dark theme self.setup_css() # Create UI self.setup_ui() self.setup_system_tray() # Start hidden self.window.hide() def setup_css(self): css_provider = Gtk.CssProvider() css = """ window { background-color: #1a1d29; color: #e8eaf6; } .header { background: linear-gradient(135deg, #1a1d29 0%, #252836 100%); padding: 20px; } .title { font-size: 20px; font-weight: bold; color: #e8eaf6; } .search-entry { background-color: #2d3142; color: #e8eaf6; border: 1px solid #5e72e4; border-radius: 8px; padding: 10px; margin: 10px 0; } .customer-card { background-color: #252836; border: 1px solid #3a3f5c; border-radius: 8px; margin: 8px 5px; padding: 15px; } .location-card { background-color: #2a2e3f; border: 1px solid #3a3f5c; border-radius: 6px; margin: 5px 20px 5px 20px; padding: 10px; } .host-item { background-color: #1a1d29; border: 1px solid #3a3f5c; border-radius: 4px; margin: 3px 2px; padding: 6px 10px; } .active-title { color: #2dce89; font-weight: bold; font-size: 14px; } .inactive-title { color: #8892b0; font-weight: bold; font-size: 14px; } .customer-name { color: #5e72e4; font-weight: bold; font-size: 14px; } .inactive-customer-name { color: #8892b0; font-weight: bold; font-size: 14px; } .location-name { color: #e8eaf6; font-weight: bold; font-size: 12px; } .vpn-type { color: #8892b0; font-size: 10px; } .connected-status { color: #2dce89; font-weight: bold; font-size: 10px; } .disconnected-status { color: #f5365c; font-weight: bold; font-size: 10px; } .connect-button { background-color: #5e72e4; color: white; border: none; border-radius: 4px; padding: 5px 15px; font-weight: bold; } .connect-button:hover { background-color: #3a3f5c; } .disconnect-button { background-color: #f5365c; color: white; border: none; border-radius: 4px; padding: 5px 15px; font-weight: bold; } .disconnect-button:hover { background-color: #3a3f5c; } .routes-button, .deactivate-button, .activate-button { background-color: #3a3f5c; color: #e8eaf6; border: none; border-radius: 4px; padding: 5px 15px; margin: 0 3px; } .deactivate-button { background-color: #fb6340; color: white; } .activate-button { background-color: #5e72e4; color: white; font-weight: bold; padding: 8px 20px; } .launch-button { background-color: #5e72e4; color: white; border: none; border-radius: 4px; padding: 4px 12px; font-weight: bold; font-size: 9px; } .host-name { color: #e8eaf6; font-weight: bold; font-size: 9px; } .host-address { color: #8892b0; font-size: 8px; font-family: monospace; } .services-header { color: #5e72e4; font-weight: bold; font-size: 10px; } .service-count { color: #6c757d; font-size: 9px; } """ 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): # Main container main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) self.window.add(main_vbox) # Header header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) header_box.get_style_context().add_class("header") main_vbox.pack_start(header_box, False, False, 0) # Title with icon title_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) title_box.set_halign(Gtk.Align.CENTER) header_box.pack_start(title_box, False, False, 10) icon_label = Gtk.Label(label="🛡️") icon_label.set_markup('🛡️') title_box.pack_start(icon_label, False, False, 0) title_label = Gtk.Label(label="VPN Connection Manager") title_label.get_style_context().add_class("title") title_box.pack_start(title_label, False, False, 0) # Search bar search_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) search_box.set_margin_start(20) search_box.set_margin_end(20) header_box.pack_start(search_box, False, False, 0) search_icon = Gtk.Label(label="🔍") search_box.pack_start(search_icon, False, False, 10) self.search_entry = Gtk.Entry() self.search_entry.set_placeholder_text("Search customers, locations, or hosts...") self.search_entry.get_style_context().add_class("search-entry") self.search_entry.connect("changed", self.filter_customers) search_box.pack_start(self.search_entry, True, True, 0) # Main content area with two columns columns_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10, homogeneous=True) columns_box.set_margin_start(10) columns_box.set_margin_end(10) columns_box.set_margin_bottom(10) main_vbox.pack_start(columns_box, True, True, 0) # Left column - Active customers left_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) columns_box.pack_start(left_vbox, True, True, 0) active_header = Gtk.Label(label="✅ Active Customers") active_header.get_style_context().add_class("active-title") active_header.set_halign(Gtk.Align.START) left_vbox.pack_start(active_header, False, False, 0) # Active customers scrolled window active_scrolled = Gtk.ScrolledWindow() active_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) left_vbox.pack_start(active_scrolled, True, True, 0) self.active_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) active_scrolled.add(self.active_box) # Right column - Inactive customers right_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) columns_box.pack_start(right_vbox, True, True, 0) inactive_header = Gtk.Label(label="💤 Inactive Customers") inactive_header.get_style_context().add_class("inactive-title") inactive_header.set_halign(Gtk.Align.START) right_vbox.pack_start(inactive_header, False, False, 0) # Inactive customers scrolled window inactive_scrolled = Gtk.ScrolledWindow() inactive_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) right_vbox.pack_start(inactive_scrolled, True, True, 0) self.inactive_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) inactive_scrolled.add(self.inactive_box) # Render initial data self.render_customers() def setup_system_tray(self): self.indicator = AppIndicator3.Indicator.new( "vpn-manager", "network-vpn", AppIndicator3.IndicatorCategory.APPLICATION_STATUS ) self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) # Create menu menu = Gtk.Menu() # Open item open_item = Gtk.MenuItem(label="Open VPN Manager") open_item.connect("activate", self.show_window_from_tray) menu.append(open_item) # Separator menu.append(Gtk.SeparatorMenuItem()) # Quit item quit_item = Gtk.MenuItem(label="Quit") quit_item.connect("activate", self.quit_app) menu.append(quit_item) menu.show_all() self.indicator.set_menu(menu) 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: customer_data = Customer( name=customer.name, locations=active_locations ) customers_with_active.append(customer_data) if inactive_locations: customer_data = Customer( name=customer.name, locations=inactive_locations ) customers_with_inactive.append(customer_data) # Render active customers for customer in customers_with_active: self.create_customer_with_active_locations(customer) # Render inactive customers for customer in customers_with_inactive: self.create_customer_without_active_locations(customer) self.window.show_all() def create_customer_with_active_locations(self, customer): # Customer card customer_frame = Gtk.Frame() customer_frame.get_style_context().add_class("customer-card") self.active_box.pack_start(customer_frame, False, False, 0) customer_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) customer_frame.add(customer_vbox) # Customer header customer_label = Gtk.Label(label=f"🏢 {customer.name}") customer_label.get_style_context().add_class("customer-name") customer_label.set_halign(Gtk.Align.START) customer_vbox.pack_start(customer_label, False, False, 0) # Render each location for location in customer.locations: self.create_active_location_card(location, customer_vbox, customer.name) def create_active_location_card(self, location, parent_box, customer_name): # Location card location_frame = Gtk.Frame() location_frame.get_style_context().add_class("location-card") parent_box.pack_start(location_frame, False, False, 0) location_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) location_frame.add(location_vbox) # Location header with controls header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) location_vbox.pack_start(header_box, False, False, 0) # Location info info_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) header_box.pack_start(info_vbox, True, True, 0) location_label = Gtk.Label(label=f"📍 {location.name}") location_label.get_style_context().add_class("location-name") location_label.set_halign(Gtk.Align.START) info_vbox.pack_start(location_label, False, False, 0) # VPN type vpn_icons = { "OpenVPN": "🔒", "WireGuard": "⚡", "IPSec": "🛡️" } vpn_icon = vpn_icons.get(location.vpn_type, "🔑") type_label = Gtk.Label(label=f"{vpn_icon} {location.vpn_type} VPN") type_label.get_style_context().add_class("vpn-type") type_label.set_halign(Gtk.Align.START) info_vbox.pack_start(type_label, False, False, 0) # Controls controls_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) header_box.pack_end(controls_box, False, False, 0) # Status status_text = "● Connected" if location.connected else "○ Disconnected" status_label = Gtk.Label(label=status_text) if location.connected: status_label.get_style_context().add_class("connected-status") else: status_label.get_style_context().add_class("disconnected-status") controls_box.pack_start(status_label, False, False, 0) # Connect/Disconnect button btn_text = "Disconnect" if location.connected else "Connect" connect_btn = Gtk.Button(label=btn_text) if location.connected: connect_btn.get_style_context().add_class("disconnect-button") else: connect_btn.get_style_context().add_class("connect-button") connect_btn.connect("clicked", lambda btn, l=location: self.toggle_connection(l)) controls_box.pack_start(connect_btn, False, False, 0) # Routes button routes_btn = Gtk.Button(label="Routes") routes_btn.get_style_context().add_class("routes-button") routes_btn.connect("clicked", lambda btn, l=location: self.set_route(l)) controls_box.pack_start(routes_btn, False, False, 0) # Deactivate button deactivate_btn = Gtk.Button(label="Deactivate") deactivate_btn.get_style_context().add_class("deactivate-button") deactivate_btn.connect("clicked", lambda btn, l=location: self.deactivate_location(l, customer_name)) controls_box.pack_start(deactivate_btn, False, False, 0) # Hosts section if location.hosts: # Separator separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) location_vbox.pack_start(separator, False, False, 5) services_label = Gtk.Label(label="💼 Available Services") services_label.get_style_context().add_class("services-header") services_label.set_halign(Gtk.Align.START) location_vbox.pack_start(services_label, False, False, 0) for host in location.hosts: self.create_host_item(host, location_vbox) def create_host_item(self, host, parent_box): host_frame = Gtk.Frame() host_frame.get_style_context().add_class("host-item") parent_box.pack_start(host_frame, False, False, 0) host_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) host_frame.add(host_box) # Icon type_icons = { "SSH": "💻", "Web": "🌐", "SMB": "📂", "PostgreSQL": "🗃️", "Redis": "🗂️" } icon = type_icons.get(host.type, "📡") icon_label = Gtk.Label(label=icon) host_box.pack_start(icon_label, False, False, 0) # Host details details_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1) host_box.pack_start(details_vbox, True, True, 0) name_label = Gtk.Label(label=host.name) name_label.get_style_context().add_class("host-name") name_label.set_halign(Gtk.Align.START) details_vbox.pack_start(name_label, False, False, 0) addr_label = Gtk.Label(label=host.address) addr_label.get_style_context().add_class("host-address") addr_label.set_halign(Gtk.Align.START) details_vbox.pack_start(addr_label, False, False, 0) # Launch button for SSH and Web services if host.type in ["SSH", "Web"]: launch_btn = Gtk.Button(label="Launch") launch_btn.get_style_context().add_class("launch-button") launch_btn.connect("clicked", lambda btn, h=host: self.open_service(h)) host_box.pack_end(launch_btn, False, False, 0) def create_customer_without_active_locations(self, customer): # Customer card customer_frame = Gtk.Frame() customer_frame.get_style_context().add_class("customer-card") self.inactive_box.pack_start(customer_frame, False, False, 0) customer_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) customer_frame.add(customer_vbox) # Customer header customer_label = Gtk.Label(label=f"🏢 {customer.name}") customer_label.get_style_context().add_class("inactive-customer-name") customer_label.set_halign(Gtk.Align.START) customer_vbox.pack_start(customer_label, False, False, 0) # Render each location for location in customer.locations: self.create_inactive_location_card(location, customer_vbox, customer.name) def create_inactive_location_card(self, location, parent_box, customer_name): # Location card location_frame = Gtk.Frame() location_frame.get_style_context().add_class("location-card") parent_box.pack_start(location_frame, False, False, 0) location_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) location_frame.add(location_hbox) # Location info info_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) location_hbox.pack_start(info_vbox, True, True, 0) location_label = Gtk.Label(label=f"📍 {location.name}") location_label.get_style_context().add_class("location-name") location_label.set_halign(Gtk.Align.START) info_vbox.pack_start(location_label, False, False, 0) # VPN type vpn_icons = { "OpenVPN": "🔒", "WireGuard": "⚡", "IPSec": "🛡️" } vpn_icon = vpn_icons.get(location.vpn_type, "🔑") type_label = Gtk.Label(label=f"{vpn_icon} {location.vpn_type} VPN") type_label.get_style_context().add_class("vpn-type") type_label.set_halign(Gtk.Align.START) info_vbox.pack_start(type_label, False, False, 0) # Service count service_count = len(location.hosts) count_label = Gtk.Label(label=f"📊 {service_count} services available") count_label.get_style_context().add_class("service-count") count_label.set_halign(Gtk.Align.START) info_vbox.pack_start(count_label, False, False, 0) # Activate button activate_btn = Gtk.Button(label="Set Active") activate_btn.get_style_context().add_class("activate-button") activate_btn.connect("clicked", lambda btn, l=location: self.set_location_active(l, customer_name)) location_hbox.pack_end(activate_btn, False, False, 0) 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 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: if search_term in customer.name.lower(): self.filtered_customers.append(customer) else: matching_locations = [] for location in customer.locations: if (search_term in location.name.lower() or search_term in location.vpn_type.lower() or any(search_term in h.name.lower() or search_term in h.address.lower() for h in location.hosts)): matching_locations.append(location) if matching_locations: filtered_customer = Customer( name=customer.name, locations=matching_locations ) self.filtered_customers.append(filtered_customer) 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}") self.render_customers() def set_route(self, location): print(f"Mock: Setting route for {location.name}") def open_service(self, host): print(f"Mock: Opening {host.type} service: {host.name} at {host.address}") def show_window_from_tray(self, widget=None): self.window.present() self.window.show_all() def hide_window(self, widget, event): self.window.hide() return True def quit_app(self, widget=None): 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()