"""Windows stand-ins for what SaraSuite uses on Linux: Linux Windows .desktop entry Start Menu shortcut (.lnk) systemd --user service shortcut in the Startup folder, started and stopped through Programs/ServiceRun.py (Services.py) system python3 the py launcher / python.exe, else winget installs it fc-list the Fonts registry key Only imported on Windows (pylnk3, winreg). """ import os import re import shutil import subprocess from pathlib import Path from typing import Callable, Optional from Core import DataHome LogFn = Callable[[str], None] # Child processes of a windowed program would each flash a console window. NoWindow = getattr(subprocess, "CREATE_NO_WINDOW", 0) PythonVersion = "3.14" # what winget installs when there's no Python yet _WingetId = f"Python.Python.{PythonVersion}" # --- Shortcuts and icons --------------------------------------------------------------- def MakeIcon(Png: Path, Ico: Path) -> Optional[Path]: """A .ico from one of the Sara icons (32x32 pixel art stored at 10x). Each size is a whole multiple of the 32x32 grid, so every art pixel stays square and crisp (same reasoning as the cards' 128px icons).""" try: from PIL import Image Source = Image.open(Png).convert("RGBA") Grid = Source.resize((32, 32), Image.NEAREST) Sizes = [(Size, Size) for Size in (256, 128, 64, 32, 16)] Ico.parent.mkdir(parents=True, exist_ok=True) Grid.resize((256, 256), Image.NEAREST).save(Ico, format="ICO", sizes=Sizes) return Ico except Exception: return None def CreateShortcut( Link: Path, Target: Path, Arguments: str = "", WorkingDir: Optional[Path] = None, Icon: Optional[Path] = None, Description: str = "", ) -> None: import pylnk3 Link.parent.mkdir(parents=True, exist_ok=True) pylnk3.for_file( str(Target), str(Link), arguments=Arguments or None, description=Description or None, icon_file=str(Icon) if Icon else None, icon_index=0, work_dir=str(WorkingDir) if WorkingDir else None, ) # --- Python ------------------------------------------------------------------------------- def _Usable(Candidate: str) -> bool: # The "python.exe" in WindowsApps is only a stub that opens the Microsoft Store. return bool(Candidate) and "WindowsApps" not in Candidate and Path(Candidate).is_file() def FindPython() -> Optional[str]: """A real Python 3 to build program venvs from.""" Launcher = shutil.which("py") if Launcher: try: Result = subprocess.run( [Launcher, "-3", "-c", "import sys; print(sys.executable)"], capture_output=True, text=True, timeout=30, creationflags=NoWindow, ) if _Usable(Result.stdout.strip()): return Result.stdout.strip() except (OSError, subprocess.SubprocessError): pass for Name in ("python3", "python"): Found = shutil.which(Name) or "" if _Usable(Found): return Found # Per-user installs from python.org or winget, newest first. Installed = sorted((DataHome / "Programs" / "Python").glob("Python3*/python.exe"), reverse=True) return str(Installed[0]) if Installed else None def InstallPython(Log: LogFn) -> Optional[str]: """Install the official Python for the current user with winget (built into Windows 10/11; no admin needed with --scope user).""" Winget = shutil.which("winget") if Winget is None: Log("winget isn't available. Install Python from https://www.python.org/downloads/ and try again.") return None Log(f"Python not found; installing Python {PythonVersion} with winget (just for you, no admin)") Command = [ Winget, "install", "--id", _WingetId, "--exact", "--scope", "user", "--silent", "--accept-package-agreements", "--accept-source-agreements", ] Log(f"$ {' '.join(Command)}") Process = subprocess.Popen(Command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, creationflags=NoWindow, errors="replace") for Line in Process.stdout: if Line.strip(): Log(f" {Line.rstrip()}") Process.wait() return FindPython() # --- Fonts ----------------------------------------------------------------------------------- def FontFamilies() -> set[str]: """Installed font families, from the system and per-user Fonts keys ("Arial Bold (TrueType)" -> "Arial").""" import winreg Families: set[str] = set() Key = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" for Root in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER): try: with winreg.OpenKey(Root, Key) as Fonts: Index = 0 while True: try: Name = winreg.EnumValue(Fonts, Index)[0] except OSError: break Index += 1 Name = re.sub(r"\s*\(.*\)$", "", Name) Name = re.sub(r"\s+(Bold|Italic|Light|Semibold|SemiBold|Black|Regular|Medium|Thin)\b.*$", "", Name) if Name and "&" not in Name: Families.add(Name.strip()) except OSError: continue return Families def OpenPath(Target: Path) -> None: os.startfile(str(Target)) # type: ignore[attr-defined] # Windows only