Skip to content

Commit bb66a43

Browse files
committed
Icon updated
1 parent 1d45b64 commit bb66a43

File tree

3 files changed

+59
-40
lines changed

3 files changed

+59
-40
lines changed

icon.svg

Lines changed: 4 additions & 0 deletions
Loading

package_app.sh

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ if ! command -v pyinstaller &> /dev/null; then
2626
fi
2727

2828
echo "Building standalone binary with PyInstaller..."
29-
pyinstaller --noconfirm --onefile --windowed --clean --name "$PACKAGE_NAME" "$SOURCE_SCRIPT"
29+
# UPDATED: Added --add-data to bundle the SVG icon inside the binary
30+
pyinstaller --noconfirm --onefile --windowed --clean \
31+
--add-data "icon.svg:." \
32+
--name "$PACKAGE_NAME" "$SOURCE_SCRIPT"
3033

3134
# Verify build succeeded
3235
if [ ! -f "dist/$PACKAGE_NAME" ]; then
@@ -40,14 +43,20 @@ BUILD_DIR="build/${PACKAGE_NAME}_${VERSION}_${ARCH}"
4043
mkdir -p "$BUILD_DIR/DEBIAN"
4144
mkdir -p "$BUILD_DIR/usr/local/bin"
4245
mkdir -p "$BUILD_DIR/usr/share/applications"
46+
# UPDATED: Create pixmaps directory for the system icon
47+
mkdir -p "$BUILD_DIR/usr/share/pixmaps"
4348

4449
echo "Creating directory structure for $MENU_NAME (v$VERSION) [$ARCH]..."
4550

4651
# 1. Copy the Compiled Binary
4752
cp "dist/$PACKAGE_NAME" "$BUILD_DIR/usr/local/bin/$PACKAGE_NAME"
4853
chmod 755 "$BUILD_DIR/usr/local/bin/$PACKAGE_NAME"
4954

50-
# 2. Create the Control file (Metadata)
55+
# 2. UPDATED: Copy the Icon for the System Menu
56+
# We rename it to match the PACKAGE_NAME so the .desktop file finds it easily
57+
cp "icon.svg" "$BUILD_DIR/usr/share/pixmaps/$PACKAGE_NAME.svg"
58+
59+
# 3. Create the Control file (Metadata)
5160
cat <<EOF > "$BUILD_DIR/DEBIAN/control"
5261
Package: $PACKAGE_NAME
5362
Version: $VERSION
@@ -60,7 +69,7 @@ Description: $DESCRIPTION
6069
A system tray application to toggle WireGuard interfaces.
6170
EOF
6271

63-
# 3. Create the Post-Installation Script
72+
# 4. Create the Post-Installation Script
6473
cat <<EOF > "$BUILD_DIR/DEBIAN/postinst"
6574
#!/bin/bash
6675
set -e
@@ -84,7 +93,7 @@ EOF
8493

8594
chmod 755 "$BUILD_DIR/DEBIAN/postinst"
8695

87-
# 4. Create the Post-Removal Script
96+
# 5. Create the Post-Removal Script
8897
cat <<EOF > "$BUILD_DIR/DEBIAN/postrm"
8998
#!/bin/bash
9099
set -e
@@ -99,19 +108,20 @@ EOF
99108

100109
chmod 755 "$BUILD_DIR/DEBIAN/postrm"
101110

102-
# 5. Create the .desktop file
111+
# 6. Create the .desktop file
103112
cat <<EOF > "$BUILD_DIR/usr/share/applications/$PACKAGE_NAME.desktop"
104113
[Desktop Entry]
105114
Name=$MENU_NAME
106115
Comment=$DESCRIPTION
107116
Exec=/usr/local/bin/$PACKAGE_NAME
108-
Icon=network-vpn
117+
# UPDATED: Use the package name (which maps to the file in /usr/share/pixmaps)
118+
Icon=$PACKAGE_NAME
109119
Terminal=false
110120
Type=Application
111121
Categories=Network;Utility;
112122
EOF
113123

114-
# 6. Build the .deb package
124+
# 7. Build the .deb package
115125
DEB_FILE="${PACKAGE_NAME}_${VERSION}_${ARCH}.deb"
116126
dpkg-deb --build "$BUILD_DIR" "$DEB_FILE"
117127
rm -rf build dist "$PACKAGE_NAME.spec"

wiretray.py

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,33 @@
44
from PyQt6.QtWidgets import (QApplication, QMainWindow, QLabel, QVBoxLayout,
55
QWidget, QSystemTrayIcon, QMenu, QPushButton,
66
QMessageBox, QListWidget, QListWidgetItem, QHBoxLayout)
7+
from PyQt6.QtSvg import QSvgRenderer
78
from PyQt6.QtGui import QAction, QIcon, QPainter, QColor
89
from PyQt6.QtCore import QSize, QTimer, Qt
910

1011
# Configuration
1112
WG_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+
1324
class 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

Comments
 (0)