Flow edit mode: edit arrow rules, delete directories

Each rule on an arrow can be edited (pattern, NewFileName, case-sensitive,
overwrite); a rule shared with other inputs is split so only this arrow
changes. Connecting an input with a pattern an output already has no longer
renames it for the other inputs. Boxes get a delete button that removes the
directory from SaraSortd (GlobalConf, other outputs' rules, its config and
history files) but never the folder itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
sarahandClaude Opus 5.5 committed 2026-10-07 22:50:11 +02:00
1 parent 60da6dd9ab
commit 918e1d68cf
3 files changed
+232 -37

No files matched your search

+118 -7
View File
@@ -427,14 +427,21 @@ class Setup:
OutDoc["Files"] = AoT([])
Files = OutDoc["Files"]
Defaults = self.TemplateFileDefaults()
Desired = {"NewFileName": Defaults["NewFileName"], **(EntryOverrides or {})}
Splits: list[str] = [] # patterns this input already gets, but named differently now
for Pattern in Patterns:
Existing = next((Entry for Entry in Files if Entry.get("Pattern") == Pattern), None)
if Existing is not None:
Own = Existing.get("ValidInputDirs")
if Own is not None and not self._EntryAccepts(Existing.unwrap(), OutputPath, InputPath):
Own.append(self.Shorthand(InputPath))
for Key, Value in (EntryOverrides or {}).items():
Existing[Key] = Value
Current = next((Entry for Entry in Files if Entry.get("Pattern") == Pattern
and self._EntryAccepts(Entry.unwrap(), OutputPath, InputPath)), None)
if Current is not None:
if EntryOverrides and any(Current.get(Key) != Value for Key, Value in EntryOverrides.items()):
Splits.append(Pattern)
continue
# An entry other inputs use with the same pattern and naming can take this input too.
Reusable = next((Entry for Entry in Files if Entry.get("Pattern") == Pattern
and isinstance(Entry.get("ValidInputDirs"), list)
and all(Entry.get(Key) == Value for Key, Value in Desired.items())), None)
if Reusable is not None:
Reusable["ValidInputDirs"].append(self.Shorthand(InputPath))
continue
Entry = tomlkit.table()
Entry["Pattern"] = Pattern
@@ -446,6 +453,11 @@ class Setup:
Entry["ValidInputDirs"] = [self.Shorthand(InputPath)]
Files.append(Entry)
WriteToml(self.ConfFile(OutputPath, "Out"), OutDoc)
for Pattern in Splits:
Entries = (self.LoadDirConf(OutputPath, "Out") or {}).get("Files", [])
Index = next(Position for Position, Entry in enumerate(Entries) if Entry.get("Pattern") == Pattern
and self._EntryAccepts(Entry, OutputPath, InputPath))
self.EditRule(InputPath, OutputPath, Index, dict(EntryOverrides))
# The input side: it has to let these files through at all.
InDoc, IsNew = self.EnsureDirConf(InputPath, "In")
@@ -511,6 +523,105 @@ class Setup:
Messages.append(Message)
return Messages
def RuleInputs(self, OutputPath: str, Index: int) -> list[str]:
"""The inputs the [[Files]] entry at Index takes files from."""
OutConf = self.LoadDirConf(OutputPath, "Out") or {}
Entries = OutConf.get("Files", [])
if Index >= len(Entries):
return []
return [Input for Input in self.ConnectedInputs(OutputPath) if self._EntryAccepts(Entries[Index], OutputPath, Input)]
def EditRule(self, InputPath: str, OutputPath: str, Index: int, Changes: dict) -> Optional[str]:
"""Changes (Pattern, NewFileName, CaseSensitive, Overwrite) the [[Files]]
entry at Index for the arrow InputPath -> OutputPath. An entry other
inputs use too is split: this arrow gets its own changed copy just
before it (so it matches first), the others keep the original.
Returns a note when it couldn't be split."""
ConfPath = self.ConfFile(OutputPath, "Out")
Document = ReadToml(ConfPath)
Files = Document.get("Files")
if Files is None or Index >= len(Files):
return "That rule no longer exists."
Entry = Files[Index]
Others = [Input for Input in self.RuleInputs(OutputPath, Index) if Input != InputPath]
Message = None
if Others:
Own = Entry.get("ValidInputDirs")
Remaining = None
if Own is None:
Remaining = [self.Shorthand(Other) for Other in Others]
else:
Parent = os.path.basename(OutputPath)
Remaining = [Valid for Valid in Own if self.Parse(str(Valid), Parent=Parent) != InputPath]
Test = dict(Entry.unwrap(), ValidInputDirs=Remaining)
if self._EntryAccepts(Test, OutputPath, InputPath):
Remaining = None # a wildcard still matches this input: can't split it off
Message = (f"{Entry.get('Pattern')} reaches this input through a wildcard, so the change "
f"applies to {', '.join(os.path.basename(Other) for Other in Others)} too.")
if Remaining is not None:
Copy = tomlkit.table()
for Key, Value in Entry.unwrap().items():
Copy[Key] = Value
for Key, Value in Changes.items():
Copy[Key] = Value
Copy["ValidInputDirs"] = [self.Shorthand(InputPath)]
Entry["ValidInputDirs"] = Remaining
Files.insert(Index, Copy)
WriteToml(ConfPath, Document)
return None
for Key, Value in Changes.items():
Entry[Key] = Value
WriteToml(ConfPath, Document)
return Message
def DeleteDirectory(self, PathText: str) -> bool:
"""Takes a directory out of SaraSortd completely: out of InputDir and
OutputDir, out of other outputs' ValidInputDirs (entries left with no
input are removed), and its config and history files are deleted.
The directory and its files stay. True if GlobalConf changed."""
for OutputPath in dict.fromkeys(self.Outputs):
if OutputPath == PathText:
continue
ConfPath = self.ConfFile(OutputPath, "Out")
try:
Document = ReadToml(ConfPath)
except (OSError, tomlkit.exceptions.TOMLKitError):
continue
Changed = False
DirValid = Document.get("ValidInputDirs")
if isinstance(DirValid, list):
Parent = Document.get("ParentDir")
for Position in reversed(range(len(DirValid))):
if self.Parse(str(DirValid[Position]), Parent=Parent) == PathText:
del DirValid[Position]
Changed = True
Files = Document.get("Files")
Parent = os.path.basename(OutputPath)
for Index in reversed(range(len(Files) if Files is not None else 0)):
Own = Files[Index].get("ValidInputDirs")
if not isinstance(Own, list):
continue
Before = len(Own)
for Position in reversed(range(Before)):
if self.Parse(str(Own[Position]), Parent=Parent) == PathText:
del Own[Position]
if len(Own) != Before:
Changed = True
if len(Own) == 0:
del Files[Index]
if Changed:
WriteToml(ConfPath, Document)
GlobalChanged = self.RemoveDir(PathText)
Parent = os.path.basename(PathText)
Leftovers = {self.ConfFile(PathText, "In"), self.ConfFile(PathText, "Out")}
HistoryName = self.Names.get("FileHistoryName")
if HistoryName:
Leftovers.add(Path(PathText) / f"{self.Parse(str(HistoryName), Parent=Parent)}.toml")
for FilePath in Leftovers:
FilePath.unlink(missing_ok=True)
return GlobalChanged
# What would happen to a file
def Simulate(self, InputPath: str, FileName: str) -> SimulationResult: