diff --git a/assets/vk.ico b/assets/vk.ico new file mode 100644 index 0000000..6925d0b Binary files /dev/null and b/assets/vk.ico differ diff --git a/assets/vk.png b/assets/vk.png new file mode 100644 index 0000000..a4c6580 Binary files /dev/null and b/assets/vk.png differ diff --git a/main.py b/main.py index bcfc042..757b811 100644 --- a/main.py +++ b/main.py @@ -110,8 +110,10 @@ log.info("[FROZEN] Environment configured for frozen app") +from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QApplication from voxkit.config import STARTUP_SCRIPT +from voxkit.config.app_config import get_app_icon_path from voxkit.gui import VoxKitGUI from voxkit.gui.workers.startup import execute_mfa_provisioning, execute_startup_script @@ -135,6 +137,7 @@ def main(): app = QApplication(sys.argv) app.setStyle("Fusion") + app.setWindowIcon(QIcon(str(get_app_icon_path()))) # Execute startup script on first launch (before GUI initialization) log.info("Running startup script") diff --git a/scripts/build.py b/scripts/build.py index 70811ed..4dc7930 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -106,11 +106,11 @@ def build(args): # This is important because PyInstaller resolves paths relative to CWD project_root = Path(__file__).parent.parent original_cwd = Path.cwd() - + if original_cwd != project_root: print(f"[INFO] Changing working directory to project root: {project_root}") os.chdir(project_root) - + opts = [] # Basic options @@ -136,6 +136,17 @@ def build(args): opts.append(f'--specpath={args.specpath}') if args.icon: opts.append(f'--icon={args.icon}') + else: + # Default the executable's embedded icon, so packaged builds do + # not ship with PyInstaller's generic one. This is separate from + # the runtime QIcon: --icon is what Explorer, the Start menu and + # a pinned shortcut read off the .exe itself, before the app has + # run at all. Windows needs .ico; macOS needs .icns, which we do + # not ship, and Linux ignores the flag entirely. + default_icon = project_root / "assets" / "vk.ico" + if sys.platform == "win32" and default_icon.exists(): + print(f"[INFO] Using default executable icon: {default_icon}") + opts.append(f'--icon={default_icon}') # Hidden imports default_hidden = [ @@ -184,6 +195,14 @@ def build(args): print("[INFO] Adding vendor folder to build assets") opts.append(f'--add-data={vendor_dir}{sep}vendor') + # Add assets folder if it exists (branding images used for the in-app + # window icon and first-launch splash logo, resolved at runtime via + # voxkit.config.app_config.get_assets_root()) + assets_dir = project_root / "assets" + if assets_dir.exists() and assets_dir.is_dir(): + print("[INFO] Adding assets folder to build assets") + opts.append(f'--add-data={assets_dir}{sep}assets') + for ad in args.add_data: if sep in ad: opts.append(f'--add-data={ad}') @@ -217,7 +236,7 @@ def build(args): print(f" Run with: ./dist/{args.name}/{args.name}") else: print(f"\n⚠️ Expected build output not found at {app_path}") - + # Restore original working directory if original_cwd != project_root: os.chdir(original_cwd) diff --git a/src/voxkit/config/app_config.py b/src/voxkit/config/app_config.py index c6376a9..1455e3b 100644 --- a/src/voxkit/config/app_config.py +++ b/src/voxkit/config/app_config.py @@ -36,6 +36,36 @@ def get_config_root() -> Path: return Path(__file__).parent.parent.parent.parent / "config" +def get_assets_root() -> Path: + """Get the path to the assets root directory. + + Returns the correct assets path whether running from source or as a + PyInstaller bundle. + + Returns: + Path to the assets directory + """ + if getattr(sys, "_MEIPASS", None): + return Path(getattr(sys, "_MEIPASS")) / "assets" + else: + return Path(__file__).parent.parent.parent.parent / "assets" + + +def get_app_icon_path() -> Path: + """Get the best available runtime icon file for QIcon/QPixmap use. + + Prefers the Windows .ico (multi-resolution, sharper in the taskbar/title + bar) when on Windows, falling back to the PNG everywhere else. + + Returns: + Path to the icon file + """ + root = get_assets_root() + if sys.platform == "win32" and (root / "vk.ico").exists(): + return root / "vk.ico" + return root / "vk.png" + + def get_active_profile() -> str: """Get the active configuration profile name. diff --git a/src/voxkit/gui/__init__.py b/src/voxkit/gui/__init__.py index 0878851..1758972 100644 --- a/src/voxkit/gui/__init__.py +++ b/src/voxkit/gui/__init__.py @@ -411,7 +411,10 @@ def open_feedback(self): webbrowser.open(mailto_url) def init_ui(self): + from voxkit.config.app_config import get_app_icon_path + self.setWindowTitle(self.app_config.app_name) + self.setWindowIcon(QIcon(str(get_app_icon_path()))) self.setMinimumSize(1200, 800) # Set application-wide stylesheet