mirror of
https://git.srhtnin.garden/sarah/SaraFem.git
synced 2026-10-13 18:18:11 +02:00
175 lines
6.5 KiB
Python
175 lines
6.5 KiB
Python
"""Read-only SaraSortd integration (Features/Major: "Shows what types of files
|
|
go to somewhere else, or where types of files come from"). Reads SaraSortd's
|
|
own GlobalConf.toml plus each relevant directory's [email protected] sidecar file
|
|
directly as TOML — never imports or runs SaraSortd itself, and never writes to
|
|
any of its files.
|
|
|
|
Only replicates the one substitution InputDir/OutputDir/ValidInputDirs entries
|
|
actually use in practice: %Name% tokens resolved against [DirectoryPaths], and
|
|
the Parent placeholder ("@" by default) swapped for a directory's own name.
|
|
SaraSortd's fuller Parse() also handles NextNum/Year/OrgFileName/etc, but those
|
|
only matter for file *renaming*, not this read-only "what goes where" view."""
|
|
|
|
import fnmatch
|
|
import re
|
|
import tomllib
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from Core import AppConfig
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SortRule:
|
|
"""One line of what GetSortRules found: Pattern goes Direction ("->" for
|
|
outgoing, "<-" for incoming) Other (the other directory, display-
|
|
formatted). Structured rather than a pre-formatted string so Browser.py
|
|
can group rules sharing the same (Direction, Other) into one row —
|
|
Bugs/Major: "SaraSortd integration header is way too long... condense
|
|
outputs and inputs into > then v button things"."""
|
|
|
|
Pattern: str
|
|
Direction: str
|
|
Other: str
|
|
|
|
DefaultParentChar = "@"
|
|
DefaultDirConfName = ".@Config"
|
|
|
|
|
|
def _LoadToml(TomlPath: Path) -> Optional[dict]:
|
|
try:
|
|
return tomllib.loads(TomlPath.read_text())
|
|
except (OSError, tomllib.TOMLDecodeError):
|
|
return None
|
|
|
|
|
|
def _ResolveRootDirTokens(Value: str, DirectoryPaths: dict) -> str:
|
|
def Replace(Match: re.Match) -> str:
|
|
Name = Match.group(1)
|
|
Found = DirectoryPaths.get(Name) if Name else None
|
|
return Found if isinstance(Found, str) else Match.group(0)
|
|
|
|
return re.sub(r"%(.*?)%", Replace, Value)
|
|
|
|
|
|
def _DirConfFileName(NamePattern: str, ParentChar: str, DirName: str) -> str:
|
|
return f"{NamePattern.replace(ParentChar, DirName)}.toml"
|
|
|
|
|
|
def _ValidInputDirMatches(Pattern: str, ParentChar: str, OutputDirName: str, InputPath: Path) -> bool:
|
|
Resolved = Pattern.replace(ParentChar, OutputDirName)
|
|
return fnmatch.fnmatchcase(str(InputPath), Resolved)
|
|
|
|
|
|
def _DisplayPath(DirPath: Path) -> str:
|
|
try:
|
|
return f"~/{DirPath.relative_to(Path.home())}"
|
|
except ValueError:
|
|
return str(DirPath)
|
|
|
|
|
|
def GetSortRules(CurrentPath: Path, Config: AppConfig) -> list[SortRule]:
|
|
"""Returns SortRules such as Pattern="*.png", Direction="->",
|
|
Other="~/Pictures" (CurrentPath is a configured SaraSortd input
|
|
directory) and/or Direction="<-" ones (CurrentPath is a configured
|
|
output directory). Empty list if the integration is off, unconfigured
|
|
(including SaraSortd's own un-set-up RootDir = "Unset"), or nothing
|
|
matches this directory. Never raises — a malformed or foreign-format
|
|
SaraSortd config degrades to no info rather than breaking the browser."""
|
|
if not Config.SaraSortd.Enabled or not Config.SaraSortd.GlobalConfPath:
|
|
return []
|
|
|
|
try:
|
|
return _GetSortRulesUnsafe(CurrentPath, Config)
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def _GetSortRulesUnsafe(CurrentPath: Path, Config: AppConfig) -> list[SortRule]:
|
|
GlobalConfPath = Path(Config.SaraSortd.GlobalConfPath).expanduser()
|
|
GlobalConf = _LoadToml(GlobalConfPath)
|
|
if not GlobalConf:
|
|
return []
|
|
|
|
DirectoryPaths = GlobalConf.get("DirectoryPaths")
|
|
if not isinstance(DirectoryPaths, dict):
|
|
return []
|
|
|
|
RootDir = DirectoryPaths.get("RootDir")
|
|
if not isinstance(RootDir, str) or not RootDir or RootDir == "Unset":
|
|
return []
|
|
|
|
Names = GlobalConf.get("Names") or {}
|
|
ParentChar = (GlobalConf.get("Variables") or {}).get("Parent", DefaultParentChar)
|
|
OutDirConfName = Names.get("OutDirConfName", DefaultDirConfName)
|
|
|
|
def ResolvedDirs(Key: str) -> list[Path]:
|
|
Raw = DirectoryPaths.get(Key, [])
|
|
if not isinstance(Raw, list):
|
|
return []
|
|
return [
|
|
Path(_ResolveRootDirTokens(Entry, DirectoryPaths)).expanduser()
|
|
for Entry in Raw if isinstance(Entry, str)
|
|
]
|
|
|
|
InputDirs = ResolvedDirs("InputDir")
|
|
OutputDirs = ResolvedDirs("OutputDir")
|
|
|
|
CurrentResolved = CurrentPath.expanduser().resolve()
|
|
|
|
IsInputDir = any(D.exists() and D.resolve() == CurrentResolved for D in InputDirs)
|
|
IsOutputDir = any(D.exists() and D.resolve() == CurrentResolved for D in OutputDirs)
|
|
|
|
Results: list[SortRule] = []
|
|
|
|
if IsInputDir:
|
|
for OutputDir in OutputDirs:
|
|
if not OutputDir.is_dir():
|
|
continue
|
|
OutputDirName = OutputDir.name
|
|
OutConf = _LoadToml(OutputDir / _DirConfFileName(OutDirConfName, ParentChar, OutputDirName))
|
|
if not OutConf:
|
|
continue
|
|
ValidInputDirs = OutConf.get("ValidInputDirs", ["*"])
|
|
if not isinstance(ValidInputDirs, list):
|
|
continue
|
|
if not any(
|
|
_ValidInputDirMatches(V, ParentChar, OutputDirName, CurrentResolved)
|
|
for V in ValidInputDirs if isinstance(V, str)
|
|
):
|
|
continue
|
|
for FileRule in OutConf.get("Files") or []:
|
|
if isinstance(FileRule, dict):
|
|
Results.append(SortRule(FileRule.get("Pattern", "*"), "->", _DisplayPath(OutputDir)))
|
|
|
|
if IsOutputDir:
|
|
OutputDirName = CurrentResolved.name
|
|
OutConf = _LoadToml(CurrentResolved / _DirConfFileName(OutDirConfName, ParentChar, OutputDirName))
|
|
if OutConf:
|
|
ValidInputDirs = OutConf.get("ValidInputDirs", ["*"])
|
|
Patterns = [
|
|
FileRule.get("Pattern", "*")
|
|
for FileRule in (OutConf.get("Files") or [])
|
|
if isinstance(FileRule, dict)
|
|
]
|
|
if isinstance(ValidInputDirs, list) and Patterns:
|
|
for InputDir in InputDirs:
|
|
if not InputDir.is_dir():
|
|
continue
|
|
if not any(
|
|
_ValidInputDirMatches(V, ParentChar, OutputDirName, InputDir.resolve())
|
|
for V in ValidInputDirs if isinstance(V, str)
|
|
):
|
|
continue
|
|
for Pattern in Patterns:
|
|
Results.append(SortRule(Pattern, "<-", _DisplayPath(InputDir)))
|
|
|
|
Seen: set[SortRule] = set()
|
|
Deduped = []
|
|
for Rule in Results:
|
|
if Rule not in Seen:
|
|
Seen.add(Rule)
|
|
Deduped.append(Rule)
|
|
return Deduped
|