initial Commit
This commit is contained in:
141
backend/services/apps.py
Normal file
141
backend/services/apps.py
Normal file
@@ -0,0 +1,141 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user