""" core/file_association.py Self-registers LabUI as the handler for .labui files with the OS, so double-clicking one launches the app — Windows (registry) and Linux (XDG). User-scoped, no admin/root rights needed. macOS doesn't need any of this: labui.spec builds a real .app bundle with CFBundleDocumentTypes baked into Info.plist at build time, which is enough for Finder to associate .labui files with it automatically. Only meaningful for a packaged build (sys.frozen, set by PyInstaller) — registering the bare Python interpreter as the handler for "python.exe main.py" isn't useful, so callers should gate the UI action on that. """ from __future__ import annotations import os import sys import subprocess from typing import Optional _PROG_ID = "LabUI.Profile" _EXT = ".labui" _MIME_TYPE = "application/x-labui" def _exe_path() -> str: return sys.executable def is_registered() -> bool: if sys.platform.startswith("win"): return _win_is_registered() if sys.platform.startswith("linux"): return _linux_is_registered() return False def register() -> Optional[str]: """Register .labui with this app. Returns an error string, or None on success.""" try: if sys.platform.startswith("win"): _win_register() elif sys.platform.startswith("linux"): _linux_register() else: return f"File association isn't needed on this platform ({sys.platform})." return None except Exception as e: return str(e) def unregister() -> Optional[str]: """Remove the association. Returns an error string, or None on success.""" try: if sys.platform.startswith("win"): _win_unregister() elif sys.platform.startswith("linux"): _linux_unregister() return None except Exception as e: return str(e) # ── Windows ────────────────────────────────────────────────────────────────── def _win_is_registered() -> bool: import winreg try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{_EXT}") as k: value, _ = winreg.QueryValueEx(k, "") return value == _PROG_ID except FileNotFoundError: return False def _win_register(): import winreg exe = _exe_path() with winreg.CreateKey(winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{_EXT}") as k: winreg.SetValueEx(k, "", 0, winreg.REG_SZ, _PROG_ID) with winreg.CreateKey(winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{_PROG_ID}") as k: winreg.SetValueEx(k, "", 0, winreg.REG_SZ, "LabUI Profile") with winreg.CreateKey(winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{_PROG_ID}\\DefaultIcon") as k: winreg.SetValueEx(k, "", 0, winreg.REG_SZ, f'"{exe}",0') with winreg.CreateKey(winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{_PROG_ID}\\shell\\open\\command") as k: winreg.SetValueEx(k, "", 0, winreg.REG_SZ, f'"{exe}" "%1"') _win_notify_shell() def _win_unregister(): import winreg for path in ( f"Software\\Classes\\{_PROG_ID}\\shell\\open\\command", f"Software\\Classes\\{_PROG_ID}\\shell\\open", f"Software\\Classes\\{_PROG_ID}\\shell", f"Software\\Classes\\{_PROG_ID}\\DefaultIcon", f"Software\\Classes\\{_PROG_ID}", f"Software\\Classes\\{_EXT}", ): try: winreg.DeleteKey(winreg.HKEY_CURRENT_USER, path) except FileNotFoundError: pass _win_notify_shell() def _win_notify_shell(): """Tell Explorer the file-type association changed — otherwise it keeps showing the old icon/handler until logoff or reboot.""" try: import ctypes SHCNE_ASSOCCHANGED = 0x08000000 SHCNF_IDLIST = 0x0000 ctypes.windll.shell32.SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None) except Exception: pass # ── Linux (XDG) ────────────────────────────────────────────────────────────── def _xdg_data_home() -> str: return os.environ.get("XDG_DATA_HOME") or os.path.join(os.path.expanduser("~"), ".local", "share") def _desktop_file_path() -> str: return os.path.join(_xdg_data_home(), "applications", "labui.desktop") def _mime_package_path() -> str: return os.path.join(_xdg_data_home(), "mime", "packages", "labui-labui.xml") def _linux_is_registered() -> bool: return os.path.isfile(_desktop_file_path()) def _linux_register(): exe = _exe_path() desktop_path = _desktop_file_path() os.makedirs(os.path.dirname(desktop_path), exist_ok=True) with open(desktop_path, "w") as f: f.write( "[Desktop Entry]\n" "Type=Application\n" "Name=LabUI\n" f"Exec={exe} %f\n" "Terminal=false\n" f"MimeType={_MIME_TYPE};\n" "NoDisplay=true\n" ) mime_path = _mime_package_path() os.makedirs(os.path.dirname(mime_path), exist_ok=True) with open(mime_path, "w") as f: f.write( '\n' '\n' f' \n' ' LabUI Profile\n' f' \n' ' \n' '\n' ) # Best-effort — desktop-environment dependent, the glob-matched .desktop # file alone is often enough even if these tools/steps are unavailable. _run_best_effort(["update-desktop-database", os.path.join(_xdg_data_home(), "applications")]) _run_best_effort(["update-mime-database", os.path.join(_xdg_data_home(), "mime")]) _run_best_effort(["xdg-mime", "default", "labui.desktop", _MIME_TYPE]) def _linux_unregister(): for path in (_desktop_file_path(), _mime_package_path()): try: os.remove(path) except FileNotFoundError: pass _run_best_effort(["update-desktop-database", os.path.join(_xdg_data_home(), "applications")]) _run_best_effort(["update-mime-database", os.path.join(_xdg_data_home(), "mime")]) def _run_best_effort(cmd: list): try: subprocess.run(cmd, check=False, capture_output=True, timeout=5) except Exception: pass