"""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 .@Config.toml 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