660 lines
23 KiB
Python
660 lines
23 KiB
Python
#!/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('<span font="24">🛡️</span>')
|
|
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() |