mirror of
https://git.srhtnin.garden/sarah/SaraSuite.git
synced 2026-10-13 18:18:12 +02:00
Initial commit
This commit is contained in:
commit
bea0f43947
25 files changed
+2327
No files matched your search
+220
@@ -0,0 +1,220 @@
|
||||
"""Program manifests: Programs/<Name>/Manifest.toml, plus optional Templates/
|
||||
and Hooks.py next to it. See README.md for the full manifest format."""
|
||||
|
||||
import importlib.util
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Optional
|
||||
|
||||
import tomlkit
|
||||
|
||||
from Core import (
|
||||
CacheHome, ConfigHome, CurrentDesktop, DataHome, Home, LoadConfig, ManifestsDir, ProgramsDir,
|
||||
SharedConfigDir,
|
||||
)
|
||||
|
||||
# Never worth copying out of a source checkout, whatever Include says.
|
||||
DefaultExclude = [
|
||||
".git", ".venv", "__pycache__", "*.pyc", "build", "dist", ".flet", ".flatpak-builder",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Option:
|
||||
"""A checkbox in the install dialog. Templates, IniEdits and Services
|
||||
that name it are only applied while it's ticked."""
|
||||
|
||||
Id: str
|
||||
Label: str
|
||||
Default: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class Question:
|
||||
"""A value asked for at install time, usable as {Id} in templates and
|
||||
config Set values. Kind "Path" expands ~ and makes it absolute."""
|
||||
|
||||
Id: str
|
||||
Prompt: str
|
||||
Default: str = ""
|
||||
Kind: str = "Text"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateFile:
|
||||
"""A file SaraSuite writes and fully owns (desktop entries, units, launchers).
|
||||
Rewritten on every update; the user isn't expected to edit these."""
|
||||
|
||||
Source: str # relative to the program's Templates/ folder
|
||||
Target: str
|
||||
Option: str = ""
|
||||
Root: bool = False # lives outside $HOME, written through pkexec
|
||||
Executable: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigFile:
|
||||
"""A config the user is expected to edit. Placed once, then merged smartly
|
||||
on updates (see ConfigMerge.py) instead of being overwritten."""
|
||||
|
||||
Source: str # relative to the program's source checkout
|
||||
Target: str
|
||||
Set: dict[str, str] = field(default_factory=dict) # dotted TOML key -> value
|
||||
|
||||
|
||||
@dataclass
|
||||
class IniEdit:
|
||||
"""One key in a file SaraSuite doesn't own (mimeapps.list, portals.conf).
|
||||
The previous value is remembered and put back on removal."""
|
||||
|
||||
File: str
|
||||
Section: str
|
||||
Key: str
|
||||
Value: str
|
||||
Option: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Service:
|
||||
Unit: str # a systemd --user unit, placed by a Template
|
||||
Option: str = "" # enabled + started only while this option is ticked
|
||||
|
||||
|
||||
@dataclass
|
||||
class Leftover:
|
||||
"""Something the program makes by itself while running (caches, logs,
|
||||
its own config dir). Only touched on removal.
|
||||
Kind: "Runtime" or "Config" (only removed when "Remove configs" is ticked).
|
||||
Mode: "Remove" (delete it, directories included) or "IfEmpty" (only
|
||||
remove a directory that's empty, e.g. one that might hold the user's files).
|
||||
Path may contain * globs."""
|
||||
|
||||
Path: str
|
||||
Kind: str = "Runtime"
|
||||
Mode: str = "Remove"
|
||||
Note: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Program:
|
||||
Name: str
|
||||
Description: str
|
||||
Folder: Path # the manifest's own folder
|
||||
Git: str = "" # e.g. "{GitHost}/SaraSortd"
|
||||
Branch: str = "" # empty = the repo's default branch
|
||||
Include: list[str] = field(default_factory=lambda: ["**"])
|
||||
Exclude: list[str] = field(default_factory=list)
|
||||
VersionFile: str = ""
|
||||
VersionPattern: str = ""
|
||||
Requirements: list[str] = field(default_factory=list)
|
||||
RequirementsFile: str = ""
|
||||
Options: list[Option] = field(default_factory=list)
|
||||
Questions: list[Question] = field(default_factory=list)
|
||||
Templates: list[TemplateFile] = field(default_factory=list)
|
||||
Configs: list[ConfigFile] = field(default_factory=list)
|
||||
IniEdits: list[IniEdit] = field(default_factory=list)
|
||||
Services: list[Service] = field(default_factory=list)
|
||||
RestartUnits: list[str] = field(default_factory=list)
|
||||
Leftovers: list[Leftover] = field(default_factory=list)
|
||||
Hooks: Optional[ModuleType] = None
|
||||
|
||||
@property
|
||||
def ProgramDir(self) -> Path:
|
||||
return ProgramsDir / self.Name
|
||||
|
||||
@property
|
||||
def AppDir(self) -> Path:
|
||||
return self.ProgramDir / "App"
|
||||
|
||||
@property
|
||||
def ConfigDir(self) -> Path:
|
||||
return SharedConfigDir / self.Name
|
||||
|
||||
@property
|
||||
def GitUrl(self) -> str:
|
||||
return Render(self.Git, self.Context())
|
||||
|
||||
@property
|
||||
def UsesPython(self) -> bool:
|
||||
return bool(self.Requirements or self.RequirementsFile)
|
||||
|
||||
def OptionEnabled(self, OptionId: str, Options: dict[str, bool]) -> bool:
|
||||
return not OptionId or Options.get(OptionId, False)
|
||||
|
||||
def Context(self, Answers: Optional[dict[str, str]] = None) -> dict[str, str]:
|
||||
"""The {Variable} values templates, targets and Set values can use."""
|
||||
Desktop = CurrentDesktop()
|
||||
Context = {
|
||||
"Name": self.Name,
|
||||
"GitHost": LoadConfig().GitHost.rstrip("/"),
|
||||
"Home": str(Home),
|
||||
"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"),
|
||||
"ConfigDir": str(self.ConfigDir),
|
||||
"DesktopName": Desktop,
|
||||
"Desktop": Desktop.lower(),
|
||||
}
|
||||
Context.update(Answers or {})
|
||||
return Context
|
||||
|
||||
|
||||
def Render(Text: str, Context: dict[str, str]) -> str:
|
||||
"""Replace {Variable}s. Unknown names are left alone, so shell's ${VAR}
|
||||
or a literal {thing} survive untouched."""
|
||||
return re.sub(r"\{(\w+)\}", lambda Match: Context.get(Match[1], Match[0]), Text)
|
||||
|
||||
|
||||
def _LoadHooks(Folder: Path, Name: str) -> Optional[ModuleType]:
|
||||
HooksPath = Folder / "Hooks.py"
|
||||
if not HooksPath.exists():
|
||||
return None
|
||||
Spec = importlib.util.spec_from_file_location(f"SaraSuiteHooks{Name}", HooksPath)
|
||||
Module = importlib.util.module_from_spec(Spec)
|
||||
Spec.loader.exec_module(Module)
|
||||
return Module
|
||||
|
||||
|
||||
def LoadProgram(Folder: Path) -> Program:
|
||||
Data = tomlkit.parse((Folder / "Manifest.toml").read_text()).unwrap()
|
||||
Source = Data.get("Source", {})
|
||||
Version = Data.get("Version", {})
|
||||
Python = Data.get("Python", {})
|
||||
return Program(
|
||||
Name=Data["Name"],
|
||||
Description=Data.get("Description", ""),
|
||||
Folder=Folder,
|
||||
Git=Source["Git"],
|
||||
Branch=Source.get("Branch", ""),
|
||||
Include=Source.get("Include", ["**"]),
|
||||
Exclude=DefaultExclude + Source.get("Exclude", []),
|
||||
VersionFile=Version.get("File", ""),
|
||||
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", [])],
|
||||
Hooks=_LoadHooks(Folder, Data["Name"]),
|
||||
)
|
||||
|
||||
|
||||
def LoadPrograms() -> list[Program]:
|
||||
return [
|
||||
LoadProgram(Folder)
|
||||
for Folder in sorted(ManifestsDir.iterdir())
|
||||
if (Folder / "Manifest.toml").exists()
|
||||
]
|
||||
Reference in new issue
Block a user