44from PyQt6 .QtWidgets import (QApplication , QMainWindow , QLabel , QVBoxLayout ,
55 QWidget , QSystemTrayIcon , QMenu , QPushButton ,
66 QMessageBox , QListWidget , QListWidgetItem , QHBoxLayout )
7+ from PyQt6 .QtSvg import QSvgRenderer
78from PyQt6 .QtGui import QAction , QIcon , QPainter , QColor
89from PyQt6 .QtCore import QSize , QTimer , Qt
910
1011# Configuration
1112WG_DIR = "/etc/wireguard"
1213
14+ # --- HELPER FUNCTION ---
15+ def resource_path (relative_path ):
16+ """ Get absolute path to resource, works for dev and for PyInstaller """
17+ try :
18+ base_path = sys ._MEIPASS
19+ except Exception :
20+ base_path = os .path .abspath ("." )
21+
22+ return os .path .join (base_path , relative_path )
23+
1324class MainWindow (QMainWindow ):
1425 def __init__ (self ):
1526 super ().__init__ ()
1627
28+ # Initial Icon Load
29+ self .setWindowIcon (QIcon (resource_path ("icon.svg" )))
30+
1731 self .setWindowTitle ("WireTray" )
1832 self .setMinimumSize (QSize (400 , 450 ))
19- self .configs = [] # List of config names (e.g., ['wg0', 'wg1'])
33+ self .configs = []
2034
2135 # --- GUI Setup ---
2236 central_widget = QWidget ()
@@ -58,14 +72,12 @@ def __init__(self):
5872
5973 # --- System Tray Setup ---
6074 self .tray_icon = QSystemTrayIcon (self )
61- self .tray_icon .setIcon (self .create_status_icon (False ))
6275
63- # We build the menu dynamically in update_tray_menu
76+ # Initialize menu
6477 self .tray_menu = QMenu ()
6578 self .tray_icon .setContextMenu (self .tray_menu )
6679 self .tray_icon .show ()
6780
68- # Handle clicking the icon itself (Left click)
6981 self .tray_icon .activated .connect (self .on_tray_icon_click )
7082
7183 # --- Timer for Status Updates ---
@@ -84,8 +96,6 @@ def scan_configs(self):
8496
8597 try :
8698 if not os .path .exists (WG_DIR ):
87- # We can't use MessageBox here easily if it runs on startup loop,
88- # so we show error in list
8999 item = QListWidgetItem ("Error: /etc/wireguard not found" )
90100 self .list_widget .addItem (item )
91101 return
@@ -98,10 +108,8 @@ def scan_configs(self):
98108
99109 self .configs .sort ()
100110
101- # Populate List Widget
102111 for config in self .configs :
103112 item = QListWidgetItem (config )
104- # FIX: Store the "clean" name in hidden data so we don't break it when changing text
105113 item .setData (Qt .ItemDataRole .UserRole , config )
106114 self .list_widget .addItem (item )
107115
@@ -115,10 +123,8 @@ def update_tray_menu(self):
115123 """Rebuilds the tray menu with available configs."""
116124 self .tray_menu .clear ()
117125
118- # Add Actions for each config
119126 for config in self .configs :
120127 action = QAction (config , self )
121- # We use a lambda with default arg to capture the specific config string
122128 action .triggered .connect (lambda checked , c = config : self .toggle_vpn (c ))
123129 self .tray_menu .addAction (action )
124130
@@ -136,17 +142,12 @@ def is_interface_active(self, interface):
136142 return os .path .exists (f"/sys/class/net/{ interface } " )
137143
138144 def update_status (self ):
139- """Checks status of all configs and updates UI."""
140145 any_active = False
141146
142- # 1. Update List Widget
143147 for i in range (self .list_widget .count ()):
144148 item = self .list_widget .item (i )
145-
146- # FIX: Retrieve the clean name from hidden data
147149 name = item .data (Qt .ItemDataRole .UserRole )
148150
149- # Skip items that aren't configs (like error messages)
150151 if not name :
151152 continue
152153
@@ -155,21 +156,16 @@ def update_status(self):
155156 any_active = True
156157 item .setIcon (QIcon .fromTheme ("network-transmit-receive" ))
157158 item .setText (f"{ name } (Active)" )
158- item .setForeground (QColor ("#4CAF50" )) # Green text
159+ item .setForeground (QColor ("#4CAF50" ))
159160 else :
160161 item .setIcon (QIcon .fromTheme ("network-offline" ))
161162 item .setText (name )
162- # Reset color based on theme (light/dark mode safe-ish)
163- item .setForeground (QColor (0 ,0 ,0 ) if self .palette (). windowText (). color (). lightness () < 128 else QColor (255 ,255 ,255 ))
163+ # Use theme-aware text color for list items
164+ item .setForeground (QColor (0 ,0 ,0 ) if self .get_is_light_theme () else QColor (255 ,255 ,255 ))
164165
165- # 2. Update Tray Menu Text/Icons
166166 actions = self .tray_menu .actions ()
167167 for action in actions :
168- # We check if the action text starts with a known config name
169- # This is a bit loose, but works since we rebuild menu on scan
170- # A cleaner way would be to store data in actions too, but this works.
171168 current_text = action .text ()
172- # Strip existing status to find the name
173169 clean_name = current_text .replace (" [Active]" , "" )
174170
175171 if clean_name in self .configs :
@@ -179,42 +175,53 @@ def update_status(self):
179175 else :
180176 action .setText (f"{ clean_name } " )
181177
182- # 3. Update Main Tray Icon
183178 self .tray_icon .setIcon (self .create_status_icon (any_active ))
184179 self .setWindowIcon (self .create_status_icon (any_active ))
185180
181+ def get_is_light_theme (self ):
182+ return self .palette ().color (self .palette ().ColorRole .WindowText ).lightness () < 128
183+
186184 def create_status_icon (self , active ):
187- base_icon = QIcon .fromTheme ("network-vpn" )
185+
186+ icon_path = resource_path ("icon.svg" )
187+ base_icon = QIcon (icon_path )
188188 if base_icon .isNull ():
189- base_icon = QIcon .fromTheme ("network-wired " )
189+ base_icon = QIcon .fromTheme ("network-vpn " )
190190
191191 pixmap = base_icon .pixmap (64 , 64 )
192+
192193 painter = QPainter (pixmap )
193194 painter .setRenderHint (QPainter .RenderHint .Antialiasing )
194195
195- # Dot color
196- color = QColor ("#4CAF50" ) if active else QColor ("#F44336" )
196+ is_light_mode = self .get_is_light_theme ()
197+
198+ icon_color = QColor ("black" ) if is_light_mode else QColor ("white" )
199+
200+ painter .setCompositionMode (QPainter .CompositionMode .CompositionMode_SourceIn )
201+ painter .fillRect (pixmap .rect (), icon_color )
202+
203+ painter .setCompositionMode (QPainter .CompositionMode .CompositionMode_SourceOver )
197204
198- painter .setBrush (color )
205+ dot_color = QColor ("#4CAF50" ) if active else QColor ("#F44336" )
206+ painter .setBrush (dot_color )
199207 painter .setPen (Qt .PenStyle .NoPen )
200208
201209 dot_size = 24
202210 x = 64 - dot_size
203211 y = 64 - dot_size
204212 painter .drawEllipse (x , y , dot_size , dot_size )
213+
205214 painter .end ()
206215 return QIcon (pixmap )
207216
208217 def on_list_double_click (self , item ):
209- # FIX: Use data instead of text splitting
210218 name = item .data (Qt .ItemDataRole .UserRole )
211219 if name and name in self .configs :
212220 self .toggle_vpn (name )
213221
214222 def toggle_selected (self ):
215223 current_item = self .list_widget .currentItem ()
216224 if current_item :
217- # FIX: Use data instead of text splitting
218225 name = current_item .data (Qt .ItemDataRole .UserRole )
219226 if name and name in self .configs :
220227 self .toggle_vpn (name )
@@ -227,14 +234,12 @@ def toggle_vpn(self, interface):
227234
228235 try :
229236 QApplication .setOverrideCursor (Qt .CursorShape .WaitCursor )
230-
231237 result = subprocess .run (cmd_list , capture_output = True , text = True )
232238
233239 if result .returncode != 0 :
234240 QApplication .restoreOverrideCursor ()
235241 QMessageBox .critical (self , "Error" , f"Failed to toggle { interface } :\n { result .stderr } " )
236242 else :
237- # Force immediate update
238243 self .update_status ()
239244
240245 except Exception as e :
0 commit comments