Windows support in SaraSuite

This commit is contained in:
sarah committed 2026-10-09 13:59:08 +02:00
1 parent 3998dfde74
commit 92033f3bf6
9 files changed
+627 -75

No files matched your search

+86 -20
View File
@@ -1,5 +1,10 @@
"""Program manifests: Programs/<Name>/Manifest.toml, plus optional Templates/
and Hooks.py next to it. See README.md for the full manifest format."""
and Hooks.py next to it. See README.md for the full manifest format.
A manifest's Platforms (default ["Linux"]) says where the program runs; each
SaraSuite only lists the programs for its own system. Any list item can also
carry Platform = "Linux" or "Windows" to only apply there (e.g. desktop
entries on Linux, Start Menu shortcuts on Windows); others are dropped at load."""
import importlib.util
import re
@@ -11,8 +16,8 @@ from typing import Optional
import tomlkit
from Core import (
CacheHome, ConfigHome, CurrentDesktop, DataHome, Home, LoadConfig, ProgramsDir,
SharedConfigDir,
CacheHome, ConfigHome, CurrentDesktop, DataHome, Home, IsWindows, LoadConfig, Platform,
ProgramsDir, SharedConfigDir, StartMenuDir, StartupDir,
)
# Never worth copying out of a source checkout, whatever Include says.
@@ -78,8 +83,33 @@ class IniEdit:
@dataclass
class Service:
Unit: str # a systemd --user unit, placed by a Template
"""A background service. Linux: a systemd --user Unit (placed by a
Template). Windows: Script (relative to the app folder) run windowless by
ServiceRun.py in WorkingDir, at login through a Startup-folder shortcut."""
Unit: str = ""
Option: str = "" # enabled + started only while this option is ticked
Name: str = "" # Windows: the service's name (PID file, Startup shortcut)
Script: str = ""
WorkingDir: str = ""
@property
def Id(self) -> str:
return self.Unit or self.Name
@dataclass
class Shortcut:
"""Windows: a Start Menu shortcut running Script (relative to the app
folder) with the program's pythonw.exe, from the app folder, with Icon
(a PNG in the app folder, turned into an .ico)."""
Name: str
Script: str
Arguments: str = ""
Icon: str = ""
Description: str = ""
Option: str = ""
@dataclass
@@ -119,6 +149,8 @@ class Program:
Services: list[Service] = field(default_factory=list)
RestartUnits: list[str] = field(default_factory=list)
Leftovers: list[Leftover] = field(default_factory=list)
Shortcuts: list[Shortcut] = field(default_factory=list)
Platforms: list[str] = field(default_factory=lambda: ["Linux"])
Hooks: Optional[ModuleType] = None
@property
@@ -132,9 +164,13 @@ class Program:
@property
def IconPath(self) -> Optional[Path]:
"""The installed copy's own icon if it's there (always current; the
Icon= line of the program's desktop entry template says where), otherwise
the copy bundled next to the manifest (Programs/<Name>/Icon.png), so the
card has an icon before installing too."""
Icon= line of the program's desktop entry template, or its Windows
Shortcut's Icon, says where), otherwise the copy bundled next to the
manifest (Programs/<Name>/Icon.png), so the card has an icon before
installing too."""
for Link in self.Shortcuts:
if Link.Icon and (self.AppDir / Link.Icon).is_file():
return self.AppDir / Link.Icon
for Template in self.Templates:
if Template.Target.endswith(".desktop"):
for Line in (self.Folder / "Templates" / Template.Source).read_text().splitlines():
@@ -153,6 +189,16 @@ class Program:
def GitUrl(self) -> str:
return Render(self.Git, self.Context())
@property
def Python(self) -> Path:
Venv = self.ProgramDir / ".venv"
return Venv / "Scripts" / "python.exe" if IsWindows else Venv / "bin" / "python"
@property
def PythonW(self) -> Path:
"""Windows: the venv's console-less python, for GUI programs and services."""
return self.Python.with_name("pythonw.exe") if IsWindows else self.Python
@property
def UsesPython(self) -> bool:
return bool(self.Requirements or self.RequirementsFile)
@@ -170,16 +216,22 @@ class Program:
"ConfigHome": str(ConfigHome),
"DataHome": str(DataHome),
"CacheHome": str(CacheHome),
"SystemdUserDir": str(ConfigHome / "systemd" / "user"),
"ApplicationsDir": str(DataHome / "applications"),
"DBusServicesDir": str(DataHome / "dbus-1" / "services"),
"ProgramDir": str(self.ProgramDir),
"AppDir": str(self.AppDir),
"Python": str(self.ProgramDir / ".venv" / "bin" / "python"),
"Python": str(self.Python),
"PythonW": str(self.PythonW),
"ConfigDir": str(self.ConfigDir),
"DesktopName": Desktop,
"Desktop": Desktop.lower(),
}
if IsWindows:
Context.update(StartMenuDir=str(StartMenuDir), StartupDir=str(StartupDir))
else:
Context.update(
SystemdUserDir=str(ConfigHome / "systemd" / "user"),
ApplicationsDir=str(DataHome / "applications"),
DBusServicesDir=str(DataHome / "dbus-1" / "services"),
)
Context.update(Answers or {})
return Context
@@ -200,8 +252,18 @@ def _LoadHooks(Folder: Path, Name: str) -> Optional[ModuleType]:
return Module
def _ForPlatform(Items: list[dict]) -> list[dict]:
"""Drop the items meant for the other system (their Platform key)."""
Kept = []
for Item in Items:
Item = dict(Item)
if Item.pop("Platform", Platform) == Platform:
Kept.append(Item)
return Kept
def LoadProgram(Folder: Path) -> Program:
Data = tomlkit.parse((Folder / "Manifest.toml").read_text()).unwrap()
Data = tomlkit.parse((Folder / "Manifest.toml").read_text(encoding="utf-8")).unwrap()
Source = Data.get("Source", {})
Version = Data.get("Version", {})
Python = Data.get("Python", {})
@@ -218,14 +280,16 @@ def LoadProgram(Folder: Path) -> Program:
VersionPattern=Version.get("Pattern", ""),
Requirements=Python.get("Requirements", []),
RequirementsFile=Python.get("RequirementsFile", ""),
Options=[Option(**Item) for Item in Data.get("Options", [])],
Questions=[Question(**Item) for Item in Data.get("Questions", [])],
Templates=[TemplateFile(**Item) for Item in Data.get("Templates", [])],
Configs=[ConfigFile(**Item) for Item in Data.get("Configs", [])],
IniEdits=[IniEdit(**Item) for Item in Data.get("IniEdits", [])],
Services=[Service(**Item) for Item in Data.get("Services", [])],
RestartUnits=Data.get("RestartUnits", []),
Leftovers=[Leftover(**Item) for Item in Data.get("Leftovers", [])],
Options=[Option(**Item) for Item in _ForPlatform(Data.get("Options", []))],
Questions=[Question(**Item) for Item in _ForPlatform(Data.get("Questions", []))],
Templates=[TemplateFile(**Item) for Item in _ForPlatform(Data.get("Templates", []))],
Configs=[ConfigFile(**Item) for Item in _ForPlatform(Data.get("Configs", []))],
IniEdits=[IniEdit(**Item) for Item in _ForPlatform(Data.get("IniEdits", []))],
Services=[Service(**Item) for Item in _ForPlatform(Data.get("Services", []))],
RestartUnits=[] if IsWindows else Data.get("RestartUnits", []),
Leftovers=[Leftover(**Item) for Item in _ForPlatform(Data.get("Leftovers", []))],
Shortcuts=[Shortcut(**Item) for Item in _ForPlatform(Data.get("Shortcuts", []))],
Platforms=Data.get("Platforms", ["Linux"]),
Hooks=_LoadHooks(Folder, Data["Name"]),
)
@@ -246,6 +310,8 @@ def LoadPrograms() -> list[Program]:
Loaded = _LoadFrom(ManifestsDir())
except Exception:
Loaded = _LoadFrom(BundledManifestsDir)
# Only the programs that run on this system (the .exe lists Windows ones).
Loaded = [Current for Current in Loaded if Platform in Current.Platforms]
Names = {Current.Name for Current in Loaded}
Ordered = []
for Current in Loaded: