#!/usr/bin/env python3 import tkinter as tk from tkinter import ttk import pystray from PIL import Image, ImageDraw import threading import sys import queue from models import Customer, Location, Host from data_loader import load_customers class VPNManagerWindow: def __init__(self): self.window = tk.Tk() self.window.title("VPN Manager") self.window.geometry("1200x750") self.window.minsize(1000, 600) self.window.protocol("WM_DELETE_WINDOW", self.hide_window) # Command queue for thread communication self.command_queue = queue.Queue() # Configure modern theme colors self.bg_color = "#1a1d29" # Deep dark blue self.fg_color = "#e8eaf6" # Soft white self.entry_bg = "#2d3142" # Dark gray-blue self.button_bg = "#252836" # Card background self.hover_bg = "#3a3f5c" # Hover state self.accent_color = "#5e72e4" # Primary accent (blue) self.success_color = "#2dce89" # Green for connected self.danger_color = "#f5365c" # Red for disconnected self.warning_color = "#fb6340" # Orange for warnings self.window.configure(bg=self.bg_color) # Load customer data using dataclasses self.customers = load_customers() self.filtered_customers = self.customers.copy() self.icon = None self.setup_ui() self.setup_tray() self.hide_window() def setup_ui(self): # Header with gradient effect (simulated with frame) header_frame = tk.Frame(self.window, bg=self.bg_color) header_frame.pack(fill=tk.X, padx=20, pady=(20, 10)) # Title with icon title_frame = tk.Frame(header_frame, bg=self.bg_color) title_frame.pack(side=tk.TOP) # Shield icon next to title icon_label = tk.Label( title_frame, text="🛡️", font=("Segoe UI Emoji", 24), bg=self.bg_color, fg=self.accent_color ) icon_label.pack(side=tk.LEFT, padx=(0, 10)) title_label = tk.Label( title_frame, text="VPN Connection Manager", font=("Segoe UI", 20, "bold"), bg=self.bg_color, fg=self.fg_color ) title_label.pack(side=tk.LEFT) # Search bar with modern styling search_container = tk.Frame(header_frame, bg=self.bg_color) search_container.pack(fill=tk.X, pady=(20, 10)) search_frame = tk.Frame(search_container, bg=self.entry_bg, highlightbackground=self.accent_color, highlightthickness=1) search_frame.pack(fill=tk.X) search_label = tk.Label( search_frame, text="🔍", font=("Segoe UI Emoji", 14), bg=self.entry_bg, fg=self.accent_color ) search_label.pack(side=tk.LEFT, padx=(10, 5)) self.search_var = tk.StringVar() self.search_var.trace("w", self.filter_customers) search_entry = tk.Entry( search_frame, textvariable=self.search_var, font=("Segoe UI", 12), bg=self.entry_bg, fg=self.fg_color, insertbackground=self.accent_color, relief=tk.FLAT, bd=0 ) search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), pady=10) # Main container for two columns self.columns_container = tk.Frame(self.window, bg=self.bg_color) self.columns_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # Left column - Active customers left_column = tk.Frame(self.columns_container, bg=self.bg_color) left_column.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) # Active customers header active_header = tk.Frame(left_column, bg=self.bg_color) active_header.pack(fill=tk.X, pady=(0, 10)) active_title = tk.Label( active_header, text="✅ Active Customers", font=("Segoe UI", 14, "bold"), bg=self.bg_color, fg=self.success_color ) active_title.pack(side=tk.LEFT) # Active customers scrollable area active_canvas = tk.Canvas( left_column, bg=self.bg_color, highlightthickness=0) active_scrollbar = ttk.Scrollbar( left_column, orient="vertical", command=active_canvas.yview) self.active_frame = tk.Frame(active_canvas, bg=self.bg_color) self.active_frame.bind( "", lambda e: active_canvas.configure( scrollregion=active_canvas.bbox("all")) ) active_canvas.create_window( (0, 0), window=self.active_frame, anchor="nw") active_canvas.configure(yscrollcommand=active_scrollbar.set) active_canvas.pack(side="left", fill="both", expand=True) active_scrollbar.pack(side="right", fill="y") # Separator separator = tk.Frame(self.columns_container, bg="#3a3f5c", width=2) separator.pack(side=tk.LEFT, fill=tk.Y, padx=5) # Right column - Inactive customers right_column = tk.Frame(self.columns_container, bg=self.bg_color) right_column.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0)) # Inactive customers header inactive_header = tk.Frame(right_column, bg=self.bg_color) inactive_header.pack(fill=tk.X, pady=(0, 10)) inactive_title = tk.Label( inactive_header, text="💤 Inactive Customers", font=("Segoe UI", 14, "bold"), bg=self.bg_color, fg="#8892b0" ) inactive_title.pack(side=tk.LEFT) # Inactive customers scrollable area inactive_canvas = tk.Canvas( right_column, bg=self.bg_color, highlightthickness=0) inactive_scrollbar = ttk.Scrollbar( right_column, orient="vertical", command=inactive_canvas.yview) self.inactive_frame = tk.Frame(inactive_canvas, bg=self.bg_color) self.inactive_frame.bind( "", lambda e: inactive_canvas.configure( scrollregion=inactive_canvas.bbox("all")) ) inactive_canvas.create_window( (0, 0), window=self.inactive_frame, anchor="nw") inactive_canvas.configure(yscrollcommand=inactive_scrollbar.set) inactive_canvas.pack(side="left", fill="both", expand=True) inactive_scrollbar.pack(side="right", fill="y") self.active_customer_frames = [] self.inactive_customer_frames = [] self.render_customers() def setup_tray(self): # Create tray icon in a separate thread def run_tray(): image = self.create_tray_icon() menu = pystray.Menu( pystray.MenuItem("Open VPN Manager", self.show_window_from_tray, default=True), pystray.MenuItem("Quit", self.quit_app) ) # Set both menu and default click action self.icon = pystray.Icon("VPNTray", image, menu=menu) # Add left-click handler def on_clicked(icon, item): print("Tray icon clicked!") self.show_window_from_tray() self.icon.default_menu_item = menu.items[0] print("Starting system tray icon...") self.icon.run() tray_thread = threading.Thread(target=run_tray, daemon=True) tray_thread.start() def render_customers(self): # Clear existing customer frames for frame in self.active_customer_frames: frame.destroy() self.active_customer_frames.clear() for frame in self.inactive_customer_frames: frame.destroy() self.inactive_customer_frames.clear() # 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 has at least one active location - show only active locations customer_data = Customer( name=customer.name, locations=active_locations ) customers_with_active.append(customer_data) if inactive_locations: # Customer has at least one inactive location - show only inactive locations customer_data = Customer( name=customer.name, locations=inactive_locations ) customers_with_inactive.append(customer_data) # Render customers with active locations on the left for customer in customers_with_active: self.create_customer_with_active_locations(customer) # Render customers with inactive locations on the right for customer in customers_with_inactive: self.create_customer_without_active_locations(customer) def create_customer_with_active_locations(self, customer): # Customer container card customer_frame = tk.Frame( self.active_frame, bg=self.button_bg, relief=tk.FLAT, highlightbackground="#3a3f5c", highlightthickness=1 ) customer_frame.pack(fill=tk.X, pady=8, padx=5) self.active_customer_frames.append(customer_frame) # Customer header customer_header = tk.Frame(customer_frame, bg=self.button_bg) customer_header.pack(fill=tk.X, padx=15, pady=(10, 5)) customer_label = tk.Label( customer_header, text=f"🏢 {customer.name}", font=("Segoe UI", 14, "bold"), bg=self.button_bg, fg=self.accent_color ) customer_label.pack(anchor="w") # Render each active location for location in customer.locations: self.create_active_location_card( location, customer_frame, customer.name) def create_active_location_card(self, location, parent_frame, customer_name): # Location card within customer container card_frame = tk.Frame( parent_frame, bg="#2a2e3f", relief=tk.FLAT, highlightbackground="#3a3f5c", highlightthickness=1 ) card_frame.pack(fill=tk.X, pady=5, padx=(20, 10)) # Location header header = tk.Frame(card_frame, bg="#2a2e3f") header.pack(fill=tk.X, padx=10, pady=8) # Location name and VPN type info_frame = tk.Frame(header, bg="#2a2e3f") info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True) # Location name location_label = tk.Label( info_frame, text=f"📍 {location.name}", font=("Segoe UI", 12, "bold"), bg="#2a2e3f", fg=self.fg_color, anchor="w" ) location_label.pack(anchor="w") # VPN type with icon vpn_icons = { "OpenVPN": "🔒", "WireGuard": "⚡", "IPSec": "🛡️" } vpn_icon = vpn_icons.get(location.vpn_type, "🔑") type_label = tk.Label( info_frame, text=f"{vpn_icon} {location.vpn_type} VPN", font=("Segoe UI", 10), bg="#2a2e3f", fg="#8892b0", anchor="w" ) type_label.pack(anchor="w") # Connection controls controls_frame = tk.Frame(header, bg="#2a2e3f") controls_frame.pack(side=tk.RIGHT) # Status indicator with modern colors status_color = self.success_color if location.connected else self.danger_color status_text = "● Connected" if location.connected else "○ Disconnected" status_frame = tk.Frame(controls_frame, bg="#2a2e3f") status_frame.pack(side=tk.LEFT, padx=(0, 10)) status_label = tk.Label( status_frame, text=status_text, font=("Segoe UI", 10, "bold"), bg="#2a2e3f", fg=status_color ) status_label.pack() # Connect/Disconnect button with modern styling btn_text = "Disconnect" if location.connected else "Connect" btn_bg = self.danger_color if location.connected else self.accent_color connect_btn = tk.Button( controls_frame, text=btn_text, font=("Segoe UI", 10, "bold"), bg=btn_bg, fg="white", activebackground=self.hover_bg, activeforeground="white", relief=tk.FLAT, padx=15, pady=5, cursor="hand2", command=lambda l=location: self.toggle_connection(l) ) connect_btn.pack(side=tk.LEFT, padx=3) # Route button with modern styling route_btn = tk.Button( controls_frame, text="Routes", font=("Segoe UI", 10), bg=self.hover_bg, fg=self.fg_color, activebackground=self.accent_color, activeforeground="white", relief=tk.FLAT, padx=15, pady=5, cursor="hand2", command=lambda l=location: self.set_route(l) ) route_btn.pack(side=tk.LEFT, padx=3) # Deactivate button deactivate_btn = tk.Button( controls_frame, text="Deactivate", font=("Segoe UI", 10), bg=self.warning_color, fg="white", activebackground=self.hover_bg, activeforeground="white", relief=tk.FLAT, padx=15, pady=5, cursor="hand2", command=lambda l=location: self.deactivate_location( l, customer_name) ) deactivate_btn.pack(side=tk.LEFT, padx=3) # Hosts/Services section if location.hosts: hosts_frame = tk.Frame(card_frame, bg="#2a2e3f") hosts_frame.pack(fill=tk.X, padx=20, pady=(0, 10)) # Services header with separator separator = tk.Frame(hosts_frame, bg="#3a3f5c", height=1) separator.pack(fill=tk.X, pady=(0, 8)) hosts_label = tk.Label( hosts_frame, text="💼 Available Services", font=("Segoe UI", 10, "bold"), bg="#2a2e3f", fg=self.accent_color, anchor="w" ) hosts_label.pack(anchor="w", pady=(0, 8)) for host in location.hosts: # Modern service item with rounded appearance host_item = tk.Frame( hosts_frame, bg="#1a1d29", highlightbackground="#3a3f5c", highlightthickness=1) host_item.pack(fill=tk.X, pady=3, padx=2) # Host type icon with improved icons type_icon = { "SSH": "💻", "Web": "🌐", "SMB": "📂", "PostgreSQL": "🗃️", "Redis": "🗂️" }.get(host.type, "📡") # Left side with icon and text info_frame = tk.Frame(host_item, bg="#1a1d29") info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=10, pady=6) icon_label = tk.Label( info_frame, text=type_icon, font=("Segoe UI Emoji", 12), bg="#1a1d29" ) icon_label.pack(side=tk.LEFT, padx=(0, 8)) # Service details details_frame = tk.Frame(info_frame, bg="#1a1d29") details_frame.pack(side=tk.LEFT, fill=tk.X, expand=True) name_label = tk.Label( details_frame, text=host.name, font=("Segoe UI", 9, "bold"), bg="#1a1d29", fg=self.fg_color, anchor="w" ) name_label.pack(anchor="w") addr_label = tk.Label( details_frame, text=host.address, font=("Consolas", 8), bg="#1a1d29", fg="#8892b0", anchor="w" ) addr_label.pack(anchor="w") # Quick access button with better styling if host.type in ["SSH", "Web"]: access_btn = tk.Button( host_item, text="Launch", font=("Segoe UI", 9, "bold"), bg=self.accent_color, fg="white", activebackground=self.hover_bg, activeforeground="white", relief=tk.FLAT, padx=12, pady=4, cursor="hand2", command=lambda h=host: self.open_service(h) ) access_btn.pack(side=tk.RIGHT, padx=8, pady=4) def create_customer_without_active_locations(self, customer): # Customer container card customer_frame = tk.Frame( self.inactive_frame, bg=self.button_bg, relief=tk.FLAT, highlightbackground="#3a3f5c", highlightthickness=1 ) customer_frame.pack(fill=tk.X, pady=8, padx=5) self.inactive_customer_frames.append(customer_frame) # Customer header customer_header = tk.Frame(customer_frame, bg=self.button_bg) customer_header.pack(fill=tk.X, padx=15, pady=(10, 5)) customer_label = tk.Label( customer_header, text=f"🏢 {customer.name}", font=("Segoe UI", 14, "bold"), bg=self.button_bg, fg="#8892b0" ) customer_label.pack(anchor="w") # Render all locations for this customer for location in customer.locations: self.create_inactive_location_card( location, customer_frame, customer.name) def create_inactive_location_card(self, location, parent_frame, customer_name): # Location card within customer container card_frame = tk.Frame( parent_frame, bg="#2a2e3f", relief=tk.FLAT, highlightbackground="#3a3f5c", highlightthickness=1 ) card_frame.pack(fill=tk.X, pady=5, padx=(20, 10)) # Location info container info_container = tk.Frame(card_frame, bg="#2a2e3f") info_container.pack(fill=tk.X, padx=12, pady=10) # Location and VPN type info_frame = tk.Frame(info_container, bg="#2a2e3f") info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True) # Location name location_label = tk.Label( info_frame, text=f"📍 {location.name}", font=("Segoe UI", 12, "bold"), bg="#2a2e3f", fg=self.fg_color, anchor="w" ) location_label.pack(anchor="w") # VPN type with icon vpn_icons = { "OpenVPN": "🔒", "WireGuard": "⚡", "IPSec": "🛡️" } vpn_icon = vpn_icons.get(location.vpn_type, "🔑") type_label = tk.Label( info_frame, text=f"{vpn_icon} {location.vpn_type} VPN", font=("Segoe UI", 10), bg="#2a2e3f", fg="#8892b0", anchor="w" ) type_label.pack(anchor="w", pady=(2, 0)) # Service count summary service_count = len(location.hosts) services_label = tk.Label( info_frame, text=f"📊 {service_count} services available", font=("Segoe UI", 9), bg="#2a2e3f", fg="#6c757d", anchor="w" ) services_label.pack(anchor="w", pady=(4, 0)) # Set Active button activate_btn = tk.Button( info_container, text="Set Active", font=("Segoe UI", 10, "bold"), bg=self.accent_color, fg="white", activebackground=self.hover_bg, activeforeground="white", relief=tk.FLAT, padx=20, pady=8, cursor="hand2", command=lambda l=location: self.set_location_active( l, customer_name) ) activate_btn.pack(side=tk.RIGHT) def set_location_active(self, location, customer_name): # Set this location as active # Find the location in the original data structure and update it 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): # Deactivate this location 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, *args): search_term = self.search_var.get().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) else: # Check if search term matches any location or host 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: # Create a filtered customer with only 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): # Mock toggle connection for 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, icon=None, item=None): # Put command in queue to be processed by main thread print("Adding show_window to queue...") self.command_queue.put("show_window") def show_window(self): print("Showing window...") self.window.deiconify() self.window.lift() self.window.focus_force() print("Window should be visible now") def hide_window(self): self.window.withdraw() def quit_app(self, icon=None, item=None): if self.icon: self.icon.stop() self.window.quit() sys.exit(0) def create_tray_icon(self): # Create a simple VPN icon width = 64 height = 64 image = Image.new('RGBA', (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(image) # Draw a shield shape for VPN icon shield_points = [ (32, 10), # Top center (50, 20), # Top right (50, 35), # Right side (32, 54), # Bottom point (14, 35), # Left side (14, 20), # Top left (32, 10) # Close shape ] draw.polygon(shield_points, fill=( 66, 135, 245), outline=(255, 255, 255)) # Draw a lock symbol draw.ellipse([25, 23, 39, 32], fill=(255, 255, 255)) draw.rectangle([27, 28, 37, 38], fill=(255, 255, 255)) return image def process_queue(self): # Process commands from the queue try: while True: command = self.command_queue.get_nowait() print(f"Processing command: {command}") if command == "show_window": self.show_window() except queue.Empty: pass # Schedule next check self.window.after(100, self.process_queue) def run(self): # Start processing queue self.process_queue() # Start the tkinter main loop in the main thread self.window.mainloop() def main(): app = VPNManagerWindow() app.run() if __name__ == "__main__": main()