"""macOS stand-ins for what SaraSuite uses on Linux: Linux macOS .desktop entry a small app bundle in ~/Applications (.app) systemd --user service a LaunchAgent (~/Library/LaunchAgents) that starts it at login, through Programs/ServiceRun.py (Services.py) system python3 a standalone Python installed by uv into SaraSuite's own folder (no admin, no Homebrew, no Xcode tools) fc-list the font files' own names xdg-open open Only used on macOS, but nothing in here needs macOS to import. """ import plistlib import shlex import shutil import subprocess import tarfile import tempfile import urllib.request from pathlib import Path from typing import Callable, Optional from Core import CacheHome, ChildEnvironment, DataDir, Home LogFn = Callable[[str], None] PythonVersion = "3.14" # what uv installs # Apple Silicon only. uv's own release file, and so the Python it installs. UvTarget = "aarch64-apple-darwin" UvUrl = f"https://github.com/astral-sh/uv/releases/latest/download/uv-{UvTarget}.tar.gz" ToolsDir = DataDir / "Tools" # uv itself PythonsDir = DataDir / "Python" # the Pythons it installs (program venvs are built from these) ApplicationsDir = Home / "Applications" LaunchAgentsDir = Home / "Library" / "LaunchAgents" LogsDir = Home / "Library" / "Logs" # --- Python ------------------------------------------------------------------------------- def _UvEnvironment() -> dict[str, str]: Environment = ChildEnvironment() Environment.update( UV_PYTHON_INSTALL_DIR=str(PythonsDir), UV_PYTHON_PREFERENCE="only-managed", # never the Xcode or Homebrew one UV_CACHE_DIR=str(CacheHome / "SaraSuite" / "uv"), UV_NO_MODIFY_PATH="1", ) return Environment def _Uv() -> Path: return ToolsDir / "uv" def FindPython() -> Optional[str]: """The Python uv installed for SaraSuite earlier, if any.""" if not _Uv().is_file(): return None try: Result = subprocess.run( [str(_Uv()), "python", "find", PythonVersion], capture_output=True, text=True, timeout=60, env=_UvEnvironment(), ) except (OSError, subprocess.SubprocessError): return None Found = Result.stdout.strip() return Found if Result.returncode == 0 and Found and Path(Found).is_file() else None def _Stream(Command: list[str], Log: LogFn) -> int: Log(f"$ {' '.join(Command)}") Process = subprocess.Popen(Command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors="replace", env=_UvEnvironment()) for Line in Process.stdout: if Line.strip(): Log(f" {Line.rstrip()}") return Process.wait() def _InstallUv(Log: LogFn) -> bool: Log(f"Downloading uv ({UvUrl})") try: with urllib.request.urlopen(UvUrl, timeout=120) as Response, tempfile.TemporaryDirectory() as Temporary: Archive = Path(Temporary) / "uv.tar.gz" Archive.write_bytes(Response.read()) with tarfile.open(Archive) as Tar: Member = next((Item for Item in Tar.getmembers() if Item.isfile() and Path(Item.name).name == "uv"), None) if Member is None: Log("The uv download has no uv in it.") return False Tar.extract(Member, Temporary, filter="data") ToolsDir.mkdir(parents=True, exist_ok=True) shutil.move(Path(Temporary) / Member.name, _Uv()) _Uv().chmod(0o755) return True except (OSError, tarfile.TarError) as Error: Log(f"Couldn't download uv: {Error}") return False def InstallPython(Log: LogFn) -> Optional[str]: """Download uv, and with it a standalone Python, into SaraSuite's own folder. Nothing is installed system-wide and no password is asked.""" Log(f"Python not found; installing Python {PythonVersion} with uv (just for SaraSuite, no admin)") if not _Uv().is_file() and not _InstallUv(Log): return None # --no-bin: no python3.14 command in ~/.local/bin; only SaraSuite uses this one. if _Stream([str(_Uv()), "python", "install", "--no-bin", PythonVersion], Log) != 0: return None return FindPython() # --- App bundles and icons ------------------------------------------------------------------- def MakeIcns(Png: Path, Icns: Path) -> Optional[Path]: """An .icns from one of the Sara icons (32x32 pixel art stored at 10x), each size a whole multiple of the 32x32 grid (nearest-neighbour), so the art stays crisp. Needs Pillow; None without it.""" try: from PIL import Image Grid = Image.open(Png).convert("RGBA").resize((32, 32), Image.NEAREST) Sizes = [Grid.resize((Size, Size), Image.NEAREST) for Size in (16, 32, 64, 128, 256, 512, 1024)] Icns.parent.mkdir(parents=True, exist_ok=True) Sizes[-1].save(Icns, format="ICNS", append_images=Sizes[:-1]) return Icns except Exception: return None def CreateAppBundle( Bundle: Path, Name: str, Identifier: str, Command: list[str], WorkingDir: Path, IconPng: Optional[Path] = None, Description: str = "", ) -> None: """~/Applications/.app: Launchpad, Spotlight and the Finder list it like any app. Its executable is a shell script that runs Command from WorkingDir. Written fresh each time (the old bundle is replaced).""" if Bundle.exists(): shutil.rmtree(Bundle) Contents = Bundle / "Contents" (Contents / "MacOS").mkdir(parents=True) (Contents / "Resources").mkdir() Executable = Contents / "MacOS" / Name Executable.write_text( "#!/bin/sh\n" "# Written by SaraSuite; replaced on every update.\n" f"cd {shlex.quote(str(WorkingDir))} || exit 1\n" f'exec {" ".join(shlex.quote(Part) for Part in Command)} "$@"\n' ) Executable.chmod(0o755) Info = { "CFBundleName": Name, "CFBundleDisplayName": Name, "CFBundleIdentifier": Identifier, "CFBundleExecutable": Name, "CFBundlePackageType": "APPL", "CFBundleInfoDictionaryVersion": "6.0", "CFBundleShortVersionString": "1.0", "LSMinimumSystemVersion": "11.0", "NSHighResolutionCapable": True, "NSHumanReadableCopyright": Description, } if IconPng is not None and MakeIcns(IconPng, Contents / "Resources" / f"{Name}.icns") is not None: Info["CFBundleIconFile"] = f"{Name}.icns" with (Contents / "Info.plist").open("wb") as File: plistlib.dump(Info, File) # The Finder and Launchpad cache bundles; tell them this one changed. Register = Path("/System/Library/Frameworks/CoreServices.framework/Frameworks/" "LaunchServices.framework/Support/lsregister") if Register.is_file(): subprocess.run([str(Register), "-f", str(Bundle)], capture_output=True, timeout=30) # --- Services ---------------------------------------------------------------------------------- def LaunchAgentPath(Name: str) -> Path: return LaunchAgentsDir / f"srhtnin.{Name}.plist" def LaunchAgent(Name: str, Command: list[str], WorkingDir: Path) -> bytes: """The LaunchAgent that starts a service at login (macOS's equivalent of the Startup folder / an XDG autostart entry). Only that: ServiceRun.py keeps it running and logs it, like on the other systems, so SaraSuite and SaraSort can start and stop it the same way everywhere.""" return plistlib.dumps({ "Label": f"srhtnin.{Name}", "ProgramArguments": Command, "WorkingDirectory": str(WorkingDir), "RunAtLoad": True, "ProcessType": "Background", # Only what fails before ServiceRun.py has its own log (e.g. a missing venv). "StandardErrorPath": str(LogsDir / f"{Name}.launchd.log"), }) # --- Fonts and files ---------------------------------------------------------------------------- def _FontFamilies(FilePath: Path) -> set[str]: """Family names in one font file's name table (every font of a .ttc). The typographic family (name ID 16) where there is one, else ID 1.""" import struct Families: set[str] = set() try: Data = FilePath.read_bytes() except OSError: return Families Offsets = [0] if Data[:4] == b"ttcf": Count = struct.unpack_from(">I", Data, 8)[0] Offsets = list(struct.unpack_from(f">{Count}I", Data, 12)) for Offset in Offsets: try: for Index in range(struct.unpack_from(">H", Data, Offset + 4)[0]): Tag, _Sum, Start, _Length = struct.unpack_from(">4sIII", Data, Offset + 12 + Index * 16) if Tag != b"name": continue _Format, Count, Strings = struct.unpack_from(">HHH", Data, Start) Found: dict[int, str] = {} for Record in range(Count): PlatformId, _Encoding, Language, NameId, Length, At = struct.unpack_from( ">6H", Data, Start + 6 + Record * 12) if NameId not in (1, 16) or NameId in Found: continue Raw = Data[Start + Strings + At:Start + Strings + At + Length] if PlatformId in (0, 3): Found[NameId] = Raw.decode("utf-16-be", "replace") elif PlatformId == 1 and Language == 0: Found[NameId] = Raw.decode("mac-roman", "replace") Name = (Found.get(16) or Found.get(1) or "").strip() if Name and not Name.startswith("."): # ".SF NS" and co. are system-internal Families.add(Name) except struct.error: continue return Families def FontFamilies() -> set[str]: """Installed font families: the system's, the shared and your own Fonts folders.""" Families: set[str] = set() for Folder in (Path("/System/Library/Fonts"), Path("/Library/Fonts"), Home / "Library" / "Fonts"): if Folder.is_dir(): for FilePath in Folder.rglob("*"): if FilePath.suffix.lower() in (".ttf", ".otf", ".ttc"): Families |= _FontFamilies(FilePath) return Families def OpenPath(Target: Path) -> None: subprocess.Popen(["open", str(Target)], env=ChildEnvironment(), start_new_session=True)