from __future__ import annotations import os import subprocess from pathlib import Path from typing import Any from backend.database import Database def _start_menu_dirs() -> list[Path]: paths = [ Path(os.environ.get("ProgramData", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs", Path(os.environ.get("APPDATA", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs", ] return [path for path in paths if path.exists()] class AppDiscovery: def __init__(self, db: Database): self.db = db def discover(self) -> list[dict[str, Any]]: apps: list[dict[str, Any]] = [] apps.extend(self._start_menu_apps()) apps.extend(self._registry_apps()) apps.extend(self.db.manual_apps()) return self._dedupe(apps) def launch(self, config: dict[str, Any]) -> None: path = config.get("path") args = config.get("args") or "" if not path: raise ValueError("App launch action requires a path.") if path.lower().endswith((".lnk", ".url", ".bat", ".cmd")): os.startfile(path) # type: ignore[attr-defined] return if Path(path).suffix.lower() == ".exe": subprocess.Popen([path, *self._split_args(args)], close_fds=True) return os.startfile(path) # type: ignore[attr-defined] def _start_menu_apps(self) -> list[dict[str, Any]]: apps: list[dict[str, Any]] = [] for root in _start_menu_dirs(): for shortcut in root.rglob("*.lnk"): apps.append( { "id": f"shortcut:{shortcut}", "name": shortcut.stem, "path": str(shortcut), "args": None, "source": "start_menu", } ) return apps def _registry_apps(self) -> list[dict[str, Any]]: apps: list[dict[str, Any]] = [] try: import winreg except ImportError: return apps roots = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"), (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), ] for hive, key_path in roots: try: key = winreg.OpenKey(hive, key_path) except OSError: continue with key: for index in range(winreg.QueryInfoKey(key)[0]): try: sub_name = winreg.EnumKey(key, index) sub_key = winreg.OpenKey(key, sub_name) except OSError: continue with sub_key: name = self._reg_value(sub_key, "DisplayName") if not name: continue path = self._best_registry_path(sub_key) if not path: continue apps.append( { "id": f"registry:{sub_name}", "name": name, "path": path, "args": None, "source": "registry", } ) return apps def _best_registry_path(self, key: Any) -> str | None: candidates = [ self._reg_value(key, "InstallLocation"), self._reg_value(key, "DisplayIcon"), ] for candidate in candidates: if not candidate: continue cleaned = candidate.strip('"').split(",")[0] path = Path(cleaned) if path.is_file() and path.suffix.lower() == ".exe": return str(path) if path.is_dir(): exes = sorted(path.glob("*.exe")) if exes: return str(exes[0]) return None def _reg_value(self, key: Any, name: str) -> str | None: try: value, _ = __import__("winreg").QueryValueEx(key, name) except OSError: return None return str(value) if value else None def _dedupe(self, apps: list[dict[str, Any]]) -> list[dict[str, Any]]: seen: set[tuple[str, str]] = set() result: list[dict[str, Any]] = [] for app in sorted(apps, key=lambda item: item["name"].lower()): key = (app["name"].lower(), str(app["path"]).lower()) if key in seen: continue seen.add(key) result.append(app) return result def _split_args(self, args: str) -> list[str]: if not args: return [] import shlex return shlex.split(args, posix=False)