init
This commit is contained in:
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
79
CLAUDE.md
Normal file
79
CLAUDE.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
### Running the Application
|
||||||
|
- `uv run python main.py` - Start the VPN manager GUI application
|
||||||
|
- `uv run python main.py &` - Start the application in background for testing
|
||||||
|
- The application runs as a system tray application and can be minimized to the tray
|
||||||
|
|
||||||
|
### Managing Running Instances
|
||||||
|
- `ps aux | grep "python main.py" | grep -v grep` - Check for running instances
|
||||||
|
- `pkill -f "uv run python main.py"` - Kill running instances (recommended)
|
||||||
|
- Note: `KillBash` only kills the shell, not the spawned uv process - use `pkill` instead
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
- This project uses `uv` for Python package management
|
||||||
|
- `uv sync` - Install dependencies from pyproject.toml
|
||||||
|
- Python 3.13+ required
|
||||||
|
|
||||||
|
## Code Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
**main.py** - Main GUI application entry point
|
||||||
|
- `VPNManagerWindow` class: Primary tkinter-based GUI application
|
||||||
|
- Implements a two-column layout: active customers (left) vs inactive customers (right)
|
||||||
|
- Features system tray integration using `pystray`
|
||||||
|
- Uses a modern dark theme with predefined color scheme
|
||||||
|
- Includes search functionality across customers, locations, and hosts
|
||||||
|
|
||||||
|
**models.py** - Data model definitions using dataclasses
|
||||||
|
- `Host`: Individual services/endpoints (SSH, Web, SMB, etc.)
|
||||||
|
- `Location`: Customer locations (headquarters, branch offices) with VPN configurations
|
||||||
|
- `Customer`: Top-level entities containing multiple locations
|
||||||
|
- Each model includes helper methods for common operations
|
||||||
|
|
||||||
|
**data_loader.py** - Data management layer
|
||||||
|
- `load_customers()`: Currently returns mock data, designed to be replaceable
|
||||||
|
- `save_customers()`: Placeholder for future persistence
|
||||||
|
- Isolates data loading logic from UI components
|
||||||
|
|
||||||
|
### Key Architecture Patterns
|
||||||
|
|
||||||
|
**Hierarchical Data Structure**: Customer → Location → Host
|
||||||
|
- Customers can have multiple locations (e.g., headquarters, branches)
|
||||||
|
- Each location has its own VPN configuration and connection state
|
||||||
|
- Locations contain multiple hosts/services that become accessible when VPN is connected
|
||||||
|
|
||||||
|
**Active/Inactive Location Management**:
|
||||||
|
- Locations (not customers) are activated/deactivated individually
|
||||||
|
- Left column shows customers with at least one active location (displaying only their active locations)
|
||||||
|
- Right column shows customers with at least one inactive location (displaying only their inactive locations)
|
||||||
|
- UI automatically reorganizes based on location activation state
|
||||||
|
|
||||||
|
**Mock Implementation**:
|
||||||
|
- Currently a UI mockup with no actual VPN functionality
|
||||||
|
- All VPN operations (connect, disconnect, routes) are placeholder methods
|
||||||
|
- Button actions update UI state but don't perform real network operations
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
1. `data_loader.load_customers()` provides initial customer data
|
||||||
|
2. Main window loads and filters data based on search terms
|
||||||
|
3. UI renders two-column layout based on location active states
|
||||||
|
4. User interactions update dataclass attributes directly
|
||||||
|
5. UI re-renders to reflect state changes
|
||||||
|
|
||||||
|
### UI Layout Structure
|
||||||
|
- Header: Search bar and application title
|
||||||
|
- Two-column main area with independent scrolling
|
||||||
|
- Left column shows full location details with hosts for active locations
|
||||||
|
- Right column shows summary cards with activation buttons for inactive locations
|
||||||
|
- System tray integration for minimize-to-tray behavior
|
||||||
|
|
||||||
|
### Future Extensibility
|
||||||
|
- Replace `load_customers()` with real data source (database, config files, API)
|
||||||
|
- Implement actual VPN connection logic in placeholder methods
|
||||||
|
- Add persistence through `save_customers()` implementation
|
||||||
147
data_loader.py
Normal file
147
data_loader.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
from typing import List
|
||||||
|
from models import Customer, Location, Host
|
||||||
|
|
||||||
|
|
||||||
|
def load_customers() -> List[Customer]:
|
||||||
|
"""Load customer data. For now, returns mock data but can be replaced later."""
|
||||||
|
|
||||||
|
# Create hosts for each location
|
||||||
|
alpha_hq_hosts = [
|
||||||
|
Host(name="Web Server", address="10.10.1.5", type="SSH"),
|
||||||
|
Host(name="Admin Panel", address="https://10.10.1.10:8443", type="Web"),
|
||||||
|
Host(name="Database", address="10.10.1.20", type="SSH"),
|
||||||
|
]
|
||||||
|
|
||||||
|
alpha_branch_hosts = [
|
||||||
|
Host(name="File Server", address="10.10.2.5", type="SMB"),
|
||||||
|
Host(name="Backup Server", address="10.10.2.10", type="SSH"),
|
||||||
|
]
|
||||||
|
|
||||||
|
beta_main_hosts = [
|
||||||
|
Host(name="Main Portal", address="https://portal.beta.local", type="Web"),
|
||||||
|
Host(name="File Server", address="192.168.50.10", type="SMB"),
|
||||||
|
]
|
||||||
|
|
||||||
|
gamma_dc_hosts = [
|
||||||
|
Host(name="Application Server", address="172.16.0.100", type="SSH"),
|
||||||
|
Host(name="Monitoring Dashboard", address="https://monitor.gamma.net", type="Web"),
|
||||||
|
Host(name="Backup Server", address="172.16.0.150", type="SSH"),
|
||||||
|
]
|
||||||
|
|
||||||
|
gamma_dr_hosts = [
|
||||||
|
Host(name="DR Server", address="172.16.1.100", type="SSH"),
|
||||||
|
Host(name="Backup Storage", address="172.16.1.150", type="SMB"),
|
||||||
|
]
|
||||||
|
|
||||||
|
delta_dev_hosts = [
|
||||||
|
Host(name="Development Server", address="10.20.30.40", type="SSH"),
|
||||||
|
Host(name="CI/CD Pipeline", address="https://jenkins.delta.local:8080", type="Web"),
|
||||||
|
]
|
||||||
|
|
||||||
|
epsilon_prod_hosts = [
|
||||||
|
Host(name="Production API", address="https://api.epsilon.com", type="Web"),
|
||||||
|
Host(name="Database Cluster", address="10.5.0.50", type="PostgreSQL"),
|
||||||
|
Host(name="Redis Cache", address="10.5.0.60", type="Redis"),
|
||||||
|
]
|
||||||
|
|
||||||
|
epsilon_staging_hosts = [
|
||||||
|
Host(name="Staging API", address="https://staging-api.epsilon.com", type="Web"),
|
||||||
|
Host(name="Test Database", address="10.5.1.50", type="PostgreSQL"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create locations
|
||||||
|
alpha_hq = Location(
|
||||||
|
name="Headquarters",
|
||||||
|
vpn_type="OpenVPN",
|
||||||
|
connected=False,
|
||||||
|
active=True,
|
||||||
|
hosts=alpha_hq_hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
alpha_branch = Location(
|
||||||
|
name="Branch Office",
|
||||||
|
vpn_type="WireGuard",
|
||||||
|
connected=False,
|
||||||
|
active=False,
|
||||||
|
hosts=alpha_branch_hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
beta_main = Location(
|
||||||
|
name="Main Office",
|
||||||
|
vpn_type="WireGuard",
|
||||||
|
connected=True,
|
||||||
|
active=True,
|
||||||
|
hosts=beta_main_hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
gamma_dc = Location(
|
||||||
|
name="Data Center",
|
||||||
|
vpn_type="IPSec",
|
||||||
|
connected=False,
|
||||||
|
active=False,
|
||||||
|
hosts=gamma_dc_hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
gamma_dr = Location(
|
||||||
|
name="DR Site",
|
||||||
|
vpn_type="OpenVPN",
|
||||||
|
connected=False,
|
||||||
|
active=False,
|
||||||
|
hosts=gamma_dr_hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
delta_dev = Location(
|
||||||
|
name="Development Lab",
|
||||||
|
vpn_type="OpenVPN",
|
||||||
|
connected=False,
|
||||||
|
active=False,
|
||||||
|
hosts=delta_dev_hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
epsilon_prod = Location(
|
||||||
|
name="Production",
|
||||||
|
vpn_type="WireGuard",
|
||||||
|
connected=False,
|
||||||
|
active=False,
|
||||||
|
hosts=epsilon_prod_hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
epsilon_staging = Location(
|
||||||
|
name="Staging",
|
||||||
|
vpn_type="OpenVPN",
|
||||||
|
connected=False,
|
||||||
|
active=False,
|
||||||
|
hosts=epsilon_staging_hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create customers
|
||||||
|
customers = [
|
||||||
|
Customer(
|
||||||
|
name="Customer Alpha Corp",
|
||||||
|
locations=[alpha_hq, alpha_branch]
|
||||||
|
),
|
||||||
|
Customer(
|
||||||
|
name="Beta Industries",
|
||||||
|
locations=[beta_main]
|
||||||
|
),
|
||||||
|
Customer(
|
||||||
|
name="Gamma Solutions",
|
||||||
|
locations=[gamma_dc, gamma_dr]
|
||||||
|
),
|
||||||
|
Customer(
|
||||||
|
name="Delta Tech",
|
||||||
|
locations=[delta_dev]
|
||||||
|
),
|
||||||
|
Customer(
|
||||||
|
name="Epsilon Systems",
|
||||||
|
locations=[epsilon_prod, epsilon_staging]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return customers
|
||||||
|
|
||||||
|
|
||||||
|
def save_customers(customers: List[Customer]) -> None:
|
||||||
|
"""Save customer data. Placeholder for future implementation."""
|
||||||
|
# TODO: Implement saving to file/database
|
||||||
|
pass
|
||||||
753
main.py
Normal file
753
main.py
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
#!/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(
|
||||||
|
"<Configure>",
|
||||||
|
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(
|
||||||
|
"<Configure>",
|
||||||
|
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()
|
||||||
54
models.py
Normal file
54
models.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Host:
|
||||||
|
"""Represents a host/service within a location."""
|
||||||
|
name: str
|
||||||
|
address: str
|
||||||
|
type: str # e.g., "SSH", "Web", "SMB", "PostgreSQL", "Redis"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Location:
|
||||||
|
"""Represents a location within a customer (e.g., headquarters, branch office)."""
|
||||||
|
name: str
|
||||||
|
vpn_type: str # e.g., "OpenVPN", "WireGuard", "IPSec"
|
||||||
|
connected: bool = False
|
||||||
|
active: bool = False
|
||||||
|
hosts: List[Host] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.hosts is None:
|
||||||
|
self.hosts = []
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Customer:
|
||||||
|
"""Represents a customer with multiple locations."""
|
||||||
|
name: str
|
||||||
|
locations: List[Location] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.locations is None:
|
||||||
|
self.locations = []
|
||||||
|
|
||||||
|
def get_active_locations(self) -> List[Location]:
|
||||||
|
"""Get all active locations for this customer."""
|
||||||
|
return [loc for loc in self.locations if loc.active]
|
||||||
|
|
||||||
|
def get_inactive_locations(self) -> List[Location]:
|
||||||
|
"""Get all inactive locations for this customer."""
|
||||||
|
return [loc for loc in self.locations if not loc.active]
|
||||||
|
|
||||||
|
def has_active_locations(self) -> bool:
|
||||||
|
"""Check if customer has any active locations."""
|
||||||
|
return any(loc.active for loc in self.locations)
|
||||||
|
|
||||||
|
def get_location_by_name(self, location_name: str) -> Optional[Location]:
|
||||||
|
"""Get a location by its name."""
|
||||||
|
for location in self.locations:
|
||||||
|
if location.name == location_name:
|
||||||
|
return location
|
||||||
|
return None
|
||||||
10
pyproject.toml
Normal file
10
pyproject.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[project]
|
||||||
|
name = "vpntray"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"pystray>=0.19.0",
|
||||||
|
"pillow>=10.0.0",
|
||||||
|
]
|
||||||
151
uv.lock
generated
Normal file
151
uv.lock
generated
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "11.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-core"
|
||||||
|
version = "11.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/e9/0b85c81e2b441267bca707b5d89f56c2f02578ef8f3eafddf0e0c0b8848c/pyobjc_core-11.1.tar.gz", hash = "sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe", size = 974602, upload-time = "2025-06-14T20:56:34.189Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/24/12e4e2dae5f85fd0c0b696404ed3374ea6ca398e7db886d4f1322eb30799/pyobjc_core-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:18986f83998fbd5d3f56d8a8428b2f3e0754fd15cef3ef786ca0d29619024f2c", size = 676431, upload-time = "2025-06-14T20:44:49.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/79/031492497624de4c728f1857181b06ce8c56444db4d49418fa459cba217c/pyobjc_core-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8849e78cfe6595c4911fbba29683decfb0bf57a350aed8a43316976ba6f659d2", size = 719330, upload-time = "2025-06-14T20:44:51.621Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/7d/6169f16a0c7ec15b9381f8bf33872baf912de2ef68d96c798ca4c6ee641f/pyobjc_core-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cb9ed17a8d84a312a6e8b665dd22393d48336ea1d8277e7ad20c19a38edf731", size = 667203, upload-time = "2025-06-14T20:44:53.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/0f/f5ab2b0e57430a3bec9a62b6153c0e79c05a30d77b564efdb9f9446eeac5/pyobjc_core-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:f2455683e807f8541f0d83fbba0f5d9a46128ab0d5cc83ea208f0bec759b7f96", size = 708807, upload-time = "2025-06-14T20:44:54.851Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-framework-cocoa"
|
||||||
|
version = "11.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyobjc-core" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/7a866d24bc026f79239b74d05e2cf3088b03263da66d53d1b4cf5207f5ae/pyobjc_framework_cocoa-11.1.tar.gz", hash = "sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038", size = 5565335, upload-time = "2025-06-14T20:56:59.683Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/0b/a01477cde2a040f97e226f3e15e5ffd1268fcb6d1d664885a95ba592eca9/pyobjc_framework_cocoa-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54e93e1d9b0fc41c032582a6f0834befe1d418d73893968f3f450281b11603da", size = 389049, upload-time = "2025-06-14T20:46:53.757Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/e6/64cf2661f6ab7c124d0486ec6d1d01a9bb2838a0d2a46006457d8c5e6845/pyobjc_framework_cocoa-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd5245ee1997d93e78b72703be1289d75d88ff6490af94462b564892e9266350", size = 393110, upload-time = "2025-06-14T20:46:54.894Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/87/01e35c5a3c5bbdc93d5925366421e10835fcd7b23347b6c267df1b16d0b3/pyobjc_framework_cocoa-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:aede53a1afc5433e1e7d66568cc52acceeb171b0a6005407a42e8e82580b4fc0", size = 392644, upload-time = "2025-06-14T20:46:56.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/7c/54afe9ffee547c41e1161691e72067a37ed27466ac71c089bfdcd07ca70d/pyobjc_framework_cocoa-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:1b5de4e1757bb65689d6dc1f8d8717de9ec8587eb0c4831c134f13aba29f9b71", size = 396742, upload-time = "2025-06-14T20:46:57.64Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-framework-quartz"
|
||||||
|
version = "11.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyobjc-core" },
|
||||||
|
{ name = "pyobjc-framework-cocoa" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/ac/6308fec6c9ffeda9942fef72724f4094c6df4933560f512e63eac37ebd30/pyobjc_framework_quartz-11.1.tar.gz", hash = "sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75", size = 3953275, upload-time = "2025-06-14T20:58:17.924Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/27/4f4fc0e6a0652318c2844608dd7c41e49ba6006ee5fb60c7ae417c338357/pyobjc_framework_quartz-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43a1138280571bbf44df27a7eef519184b5c4183a588598ebaaeb887b9e73e76", size = 216816, upload-time = "2025-06-14T20:53:37.358Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/8a/1d15e42496bef31246f7401aad1ebf0f9e11566ce0de41c18431715aafbc/pyobjc_framework_quartz-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b23d81c30c564adf6336e00b357f355b35aad10075dd7e837cfd52a9912863e5", size = 221941, upload-time = "2025-06-14T20:53:38.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/a8/a3f84d06e567efc12c104799c7fd015f9bea272a75f799eda8b79e8163c6/pyobjc_framework_quartz-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:07cbda78b4a8fcf3a2d96e047a2ff01f44e3e1820f46f0f4b3b6d77ff6ece07c", size = 221312, upload-time = "2025-06-14T20:53:39.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/ef/8c08d4f255bb3efe8806609d1f0b1ddd29684ab0f9ffb5e26d3ad7957b29/pyobjc_framework_quartz-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:39d02a3df4b5e3eee1e0da0fb150259476910d2a9aa638ab94153c24317a9561", size = 226353, upload-time = "2025-06-14T20:53:40.655Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pystray"
|
||||||
|
version = "0.19.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" },
|
||||||
|
{ name = "python-xlib", marker = "sys_platform == 'linux'" },
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/64/927a4b9024196a4799eba0180e0ca31568426f258a4a5c90f87a97f51d28/pystray-0.19.5-py2.py3-none-any.whl", hash = "sha256:a0c2229d02cf87207297c22d86ffc57c86c227517b038c0d3c59df79295ac617", size = 49068, upload-time = "2023-09-17T13:44:26.872Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-xlib"
|
||||||
|
version = "0.33"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vpntray"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pystray" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "pillow", specifier = ">=10.0.0" },
|
||||||
|
{ name = "pystray", specifier = ">=0.19.0" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user