Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
'--name=WoWSync',
'--onedir' if sys.platform == 'darwin' else '--onefile',
'--noconsole',
'--icon=icons/icon.png',
'--icon=icons/icon.icns' if sys.platform == 'darwin' else ('--icon=icons/icon.ico' if sys.platform == 'win32' else '--icon=icons/icon.png'),
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build script has a complex ternary expression that's difficult to read and maintain. Consider breaking this into multiple lines or using if-elif-else logic for better readability. For example:

if sys.platform == 'darwin':
    icon_arg = '--icon=icons/icon.icns'
elif sys.platform == 'win32':
    icon_arg = '--icon=icons/icon.ico'
else:
    icon_arg = '--icon=icons/icon.png'

Then use icon_arg in the list.

Copilot uses AI. Check for mistakes.
f'--add-data=icons{";" if sys.platform == "win32" else ":"}icons',
'--hidden-import=tkinter',
'--hidden-import=sv_ttk',
Expand Down
97 changes: 87 additions & 10 deletions icons/generate_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

from PIL import Image, ImageEnhance
from pathlib import Path
try:
from icnsutil import IcnsFile
ICNS_AVAILABLE = True
except ImportError:
ICNS_AVAILABLE = False


def make_transparent(image, *, background=(255, 255, 255), alpha_power=1.0, alpha_floor=6):
Expand Down Expand Up @@ -88,17 +93,19 @@ def main():
print(f" ✓ Saved {original_processed_path}")

# Generate icons at different sizes
sizes = {
"icon.png": 256,
"icon_256.png": 256,
"icon_128.png": 128,
"icon_64.png": 64,
"icon_48.png": 48,
"icon_32.png": 32,
"icon_16.png": 16,
}
sizes = [
("icon.png", 256),
("icon_256.png", 256),
("icon_128.png", 128),
("icon_64.png", 64),
("icon_48.png", 48),
("icon_32.png", 32),
("icon_16.png", 16),
]

for filename, size in sizes.items():
# Process all icon sizes and save as PNG files
icon_sizes_for_ico = {}
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data structure change from dictionary to list is good for maintaining order, but the variable name icon_sizes_for_ico is misleading. This dictionary is used to collect resized images for both ICO and ICNS file generation, not just ICO. Consider renaming it to icon_sizes_cache or resized_icons for better clarity.

Copilot uses AI. Check for mistakes.
for filename, size in sizes:
output_path = icons_dir / filename
print(f"Generating {filename} ({size}x{size})...")

Expand All @@ -108,6 +115,76 @@ def main():
# Save as PNG with transparency
resized.save(output_path, "PNG", optimize=True)
print(f" ✓ Saved {output_path}")

# Collect images for ICO file (avoid duplicates)
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "avoid duplicates" but the logic actually ensures only one image per size is stored, using the last one processed. If the sizes list has duplicates (like two entries with size 256), the second will overwrite the first. Consider either removing duplicates from the sizes list earlier, or clarifying the comment to explain this overwrite behavior.

Suggested change
# Collect images for ICO file (avoid duplicates)
# Track unique icon sizes for the ICO file (skip duplicate sizes in the sizes list)

Copilot uses AI. Check for mistakes.
if size not in icon_sizes_for_ico:
icon_sizes_for_ico[size] = resized

# Generate ICO file for Windows (contains multiple resolutions)
ico_path = icons_dir / "icon.ico"
print(f"\nGenerating icon.ico with multiple resolutions...")
# Sort sizes for consistent ordering in the ICO file
sorted_sizes = sorted(icon_sizes_for_ico.keys())

# For ICO files, we need to save with the sizes parameter
# which tells Pillow to create an ICO with multiple embedded images
if sorted_sizes:
# Use the original transparent image and let Pillow resize to each size
transparent_img.save(ico_path, format='ICO',
sizes=[(s, s) for s in sorted_sizes])
print(f" ✓ Saved {ico_path} with {len(sorted_sizes)} resolutions: {sorted_sizes}")

# Generate ICNS file for macOS (contains multiple resolutions)
if ICNS_AVAILABLE:
icns_path = icons_dir / "icon.icns"
print(f"\nGenerating icon.icns with multiple resolutions...")

# Create ICNS file
icns = IcnsFile()

# macOS ICNS standard sizes - use the naming convention icnsutil expects
# Format: icon_<size>x<size>.png and icon_<size>x<size>@2x.png for retina
# Note: 64x64 is covered by 32x32@2x
icns_mappings = [
(16, 32), # 16x16 and 16x16@2x (32x32)
(32, 64), # 32x32 and 32x32@2x (64x64)
(128, 256), # 128x128 and 128x128@2x (256x256)
(256, 512), # 256x256 and 256x256@2x (512x512)
(512, 1024), # 512x512 and 512x512@2x (1024x1024)
]

for normal_size, retina_size in icns_mappings:
# Add normal resolution
if normal_size in icon_sizes_for_ico:
img = icon_sizes_for_ico[normal_size]
else:
img = transparent_img.resize((normal_size, normal_size), Image.Resampling.LANCZOS)

temp_png = icons_dir / f"icon_{normal_size}x{normal_size}.png"
img.save(temp_png, "PNG")
icns.add_media(file=str(temp_png))
temp_png.unlink()

# Add retina resolution
if retina_size in icon_sizes_for_ico:
retina_img = icon_sizes_for_ico[retina_size]
elif retina_size <= 1024:
retina_img = transparent_img.resize((retina_size, retina_size), Image.Resampling.LANCZOS)
else:
# For 1024, just use the processed image
retina_img = transparent_img.resize((1024, 1024), Image.Resampling.LANCZOS)

temp_retina_png = icons_dir / f"icon_{normal_size}x{normal_size}@2x.png"
retina_img.save(temp_retina_png, "PNG")
icns.add_media(file=str(temp_retina_png))
Comment on lines +162 to +179
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temporary PNG files created for ICNS generation are immediately deleted after being added to the ICNS file. However, if an exception occurs between saving the temp file and unlinking it (e.g., in the icns.add_media() call), the temp file will not be cleaned up. Consider using a try-finally block or context manager to ensure cleanup, or use a temporary directory that's automatically cleaned up.

Copilot uses AI. Check for mistakes.
temp_retina_png.unlink()

# Write the ICNS file
icns.write(str(icns_path))
print(f" ✓ Saved {icns_path} with standard macOS resolutions")
else:
print(f"\n⚠ Skipping icon.icns generation (icnsutil not available)")
print(f" Install with: pip install icnsutil")

print("\nAll icons generated successfully!")

Expand Down
Binary file added icons/icon.icns
Binary file not shown.
Binary file added icons/icon.ico
Binary file not shown.
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ psutil==7.2.0
dbus-fast==3.1.2
async-tkinter-loop==0.10.3
# Build tools (optional, for packaging)
pyinstaller==6.17.0; extra == "build"
pyinstaller==6.17.0; extra == "build"
icnsutil==1.1.0; extra == "build"
6 changes: 5 additions & 1 deletion wow_sync/tray/tray_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ def __init__(self, on_show: Optional[Callable] = None,

async def setup(self):
"""Setup the tray icon using macOS NSStatusBar."""
icon_path = ICON_DIR / "icon.png"
# Use ICNS file for macOS (supports multiple resolutions and retina displays)
icon_path = ICON_DIR / "icon.icns"
# Fallback to PNG if ICNS not available
if not icon_path.exists():
icon_path = ICON_DIR / "icon.png"
if not icon_path.exists():
raise RuntimeError(f"Icon not found: {icon_path}")

Expand Down
3 changes: 2 additions & 1 deletion wow_sync/tray/tray_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def __init__(self, on_show: Optional[Callable] = None,

async def setup(self):
"""Setup the tray icon using Windows Shell NotifyIcon."""
icon_path = ICON_DIR / "icon.png"
# Use ICO file for Windows (supports multiple resolutions)
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Use ICO file for Windows (supports multiple resolutions)" is helpful, but it would be even more informative to mention that PNG files cannot be loaded as ICO files using the Win32 API's LoadImage function, which was the root cause of the bug being fixed.

Suggested change
# Use ICO file for Windows (supports multiple resolutions)
# Use ICO file for Windows: LoadImage cannot load PNG files as ICOs,
# and this limitation previously caused a bug when using a PNG icon.

Copilot uses AI. Check for mistakes.
icon_path = ICON_DIR / "icon.ico"
if not icon_path.exists():
raise RuntimeError(f"Icon not found: {icon_path}")

Expand Down