Initial commit

This commit is contained in:
sarah committed 2026-09-24 22:04:04 +02:00
commit bea0f43947
25 files changed
+2327

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
Name = "SaraFem"
Description = "Desktop file manager that understands SaraTag Books, and can stand in as the system file picker."
# The portal reads its descriptors and preferences once at startup.
RestartUnits = ["xdg-desktop-portal.service"]
[Source]
Git = "{GitHost}/SaraFem"
Include = ["*.py", "requirements.txt", "assets/**"]
Exclude = ["Packaging"]
[Python]
RequirementsFile = "requirements.txt"
[[Options]]
Id = "FilePicker"
Label = "Use SaraFem as the system file picker (asks for your password: the portal file lives in /usr/share)"
Default = true
[[Options]]
Id = "DefaultFileManager"
Label = "Make SaraFem the default file manager"
Default = false
[[Templates]]
Source = "Launch.sh"
Target = "{AppDir}/Packaging/Launch.sh" # where PortalService.py looks for it
Executable = true
[[Templates]]
Source = "SaraFem.desktop"
Target = "{ApplicationsDir}/SaraFem.desktop"
[[Templates]]
Source = "PortalService.service"
Target = "{DBusServicesDir}/org.freedesktop.impl.portal.desktop.SaraFem.service"
Option = "FilePicker"
[[Templates]]
Source = "SaraFem.portal"
Target = "/usr/share/xdg-desktop-portal/portals/SaraFem.portal"
Option = "FilePicker"
Root = true
[[IniEdits]]
File = "{ConfigHome}/xdg-desktop-portal/{Desktop}-portals.conf"
Section = "preferred"
Key = "org.freedesktop.impl.portal.FileChooser"
Value = "SaraFem;{Desktop};gtk;"
Option = "FilePicker"
[[IniEdits]]
File = "{ConfigHome}/mimeapps.list"
Section = "Default Applications"
Key = "inode/directory"
Value = "SaraFem.desktop"
Option = "DefaultFileManager"
[[Leftovers]]
Path = "{CacheHome}/SaraFem"
Kind = "Runtime"
Note = "thumbnail cache"
[[Leftovers]]
Path = "{ConfigHome}/SaraFem"
Kind = "Config"
Note = "old pre-SaraSuite config location"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Written by SaraSuite. Runs from the app folder so Flet finds assets/.
cd "{AppDir}" || exit 1
exec "{Python}" Main.py "$@"
@@ -0,0 +1,3 @@
[D-BUS Service]
Name=org.freedesktop.impl.portal.desktop.SaraFem
Exec={AppDir}/Packaging/Launch.sh --portal-service
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=SaraFem
Comment=A file manager for the SaraSuite
Exec={AppDir}/Packaging/Launch.sh %f
Icon={AppDir}/assets/Icon.png
Terminal=false
Categories=System;FileManager;
MimeType=inode/directory;
StartupNotify=true
@@ -0,0 +1,4 @@
[portal]
DBusName=org.freedesktop.impl.portal.desktop.SaraFem
Interfaces=org.freedesktop.impl.portal.FileChooser;
UseIn={DesktopName}
+91
View File
@@ -0,0 +1,91 @@
"""Works out what SaraSortd made on its own while running, by reading
GlobalConf.toml the same way SaraSortd.py does (see its Parse/GetConf/Init).
It never removes the user's files: the input/output/failed folders are only
removed if they're empty once SaraSortd's own files are gone.
"""
import re
from pathlib import Path
import tomlkit
from Manifests import Leftover
def _Find(Conf: dict, Key: str):
"""SaraSortd's GetConf: a top-level key, or one inside a table/array of tables."""
if Key in Conf:
return Conf[Key]
for Value in Conf.values():
if isinstance(Value, dict) and Key in Value:
return Value[Key]
if isinstance(Value, list):
for Item in Value:
if isinstance(Item, dict) and Key in Item:
return Item[Key]
return None
def _Resolve(Text: str, Conf: dict) -> str:
"""SaraSortd's %Var% substitution (single pass, like its Parse)."""
VarCall = Conf.get("Variables", {}).get("VarCall", "%%")
Pattern = f"{re.escape(VarCall[0])}(.*?){re.escape(VarCall[1])}"
def Replace(Match: re.Match) -> str:
Value = _Find(Conf, Match[1]) if Match[1] else None
return str(Value) if Value is not None else Match[0]
return re.sub(Pattern, Replace, Text)
def _DirFileName(Conf: dict, NameKey: str, DirPath: Path) -> str:
"""e.g. ".@Config" for a folder named Input -> ".InputConfig.toml"."""
Parent = Conf.get("Variables", {}).get("Parent", "@")
Name = Conf.get("Names", {}).get(NameKey, "")
return f"{Name.replace(Parent, DirPath.name)}.toml" if Name else ""
def Leftovers(Context: dict) -> list[Leftover]:
ConfPath = Path(Context["ConfigDir"]) / "GlobalConf.toml"
if not ConfPath.exists():
return []
try:
Conf = tomlkit.parse(ConfPath.read_text()).unwrap()
except tomlkit.exceptions.TOMLKitError:
return []
Paths = Conf.get("DirectoryPaths", {})
Found: list[Leftover] = []
Folders = []
for Key in ("InputDir", "OutputDir"):
Entries = Paths.get(Key, [])
for Entry in Entries if isinstance(Entries, list) else [Entries]:
Folders.append(Path(_Resolve(str(Entry), Conf)))
for Folder in Folders:
for NameKey in ("InDirConfName", "OutDirConfName", "DirConfName"):
FileName = _DirFileName(Conf, NameKey, Folder)
if FileName:
Found.append(Leftover(str(Folder / FileName), "Config", Note="this folder's SaraSortd rules"))
HistoryName = _DirFileName(Conf, "FileHistoryName", Folder)
if HistoryName:
Found.append(Leftover(str(Folder / HistoryName), "Runtime", Note="sorting history"))
Found.append(Leftover(str(Folder), "Runtime", "IfEmpty", "only if empty"))
LogDir = Paths.get("LogDir")
if LogDir:
LogDir = _Resolve(str(LogDir), Conf)
Found.append(Leftover(f"{LogDir}/*.log", "Runtime", Note="log"))
Found.append(Leftover(LogDir, "Runtime", "IfEmpty", "only if empty"))
FailedDir = Paths.get("FailedDir")
if FailedDir:
Found.append(Leftover(_Resolve(str(FailedDir), Conf), "Runtime", "IfEmpty", "only if empty, it holds files that failed to sort"))
RootDir = Paths.get("RootDir")
if RootDir and RootDir != "Unset":
Found.append(Leftover(_Resolve(str(RootDir), Conf), "Runtime", "IfEmpty", "only if empty"))
return Found
+51
View File
@@ -0,0 +1,51 @@
Name = "SaraSortd"
Description = "Sorts files from input folders into output folders by the rules in its configs. Runs in the background as a systemd user service."
[Version]
File = "SaraSortd.py"
Pattern = '# Version = "([^"]+)"'
[Source]
Git = "{GitHost}/SaraSortd"
Include = ["SaraSortd.py", "LICENSE", "README.md"]
[Python]
Requirements = ["toml"]
[[Questions]]
Id = "RootDir"
Prompt = "Folder SaraSortd sorts in (RootDir)"
Default = "~/Sorting"
Kind = "Path"
[[Options]]
Id = "Autostart"
Label = "Start SaraSortd now and on every login"
Default = true
# SaraSortd reads ./GlobalConf.toml and copies ./InputDirConf.toml and
# ./OutputDirConf.toml into its folders, so all three live together in
# ConfigDir and the service runs from there.
[[Configs]]
Source = "GlobalConf.toml"
Target = "{ConfigDir}/GlobalConf.toml"
Set = { "DirectoryPaths.RootDir" = "{RootDir}" }
[[Configs]]
Source = "InputDirConf.toml"
Target = "{ConfigDir}/InputDirConf.toml"
[[Configs]]
Source = "OutputDirConf.toml"
Target = "{ConfigDir}/OutputDirConf.toml"
[[Templates]]
Source = "SaraSortd.service"
Target = "{SystemdUserDir}/SaraSortd.service"
[[Services]]
Unit = "SaraSortd.service"
Option = "Autostart"
# Everything SaraSortd makes inside the sorting folders is worked out from
# GlobalConf.toml by Hooks.py.
@@ -0,0 +1,15 @@
# Written by SaraSuite. Changes here are overwritten on update:
# edit the manifest template in SaraSuite/Programs/SaraSortd/ instead.
[Unit]
Description=SaraSortd file sorting daemon
[Service]
Type=simple
WorkingDirectory={ConfigDir}
ExecStart={Python} {AppDir}/SaraSortd.py
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target