mirror of
https://git.srhtnin.garden/sarah/SaraSuite.git
synced 2026-10-13 18:18:12 +02:00
Windows support in SaraSuite
This commit is contained in:
1 parent
3998dfde74
commit
92033f3bf6
9 files changed
+627
-75
No files matched your search
+86
-20
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user