Added .sv_* files that are files for directories. So you can now edit

directories as files.
This commit is contained in:
sarah committed 2026-09-21 17:19:16 +02:00
1 parent 70a390f7cf
commit 6083e00504
9 files changed
+176 -18

No files matched your search

+76 -3
View File
@@ -38,6 +38,64 @@ def ClassifyFile(EntryPath: Path) -> str:
return FileTypeMap.get(EntryPath.suffix.lower(), "unknown")
NoteFilePrefix = ".sv_"
DefaultNoteExtension = ".md"
def IsNoteFile(EntryName: str) -> bool:
"""A directory's own "file" - editable/viewable like any other file, but
hidden from normal listings behind the ShowSvFiles setting.
"""
return EntryName.startswith(NoteFilePrefix)
def FindDirectoryNote(DirectoryPath: Path) -> Path | None:
"""Find a directory's note file, whatever extension it currently has."""
Prefix = f"{NoteFilePrefix}{DirectoryPath.name}."
for Candidate in DirectoryPath.iterdir():
if Candidate.is_file() and Candidate.name.startswith(Prefix):
return Candidate
return None
def EnsureDirectoryNote(DirectoryPath: Path) -> Path:
"""Find a directory's note file, creating one (defaulting to markdown)
if it doesn't have one yet - e.g. a directory that predates this feature.
"""
Existing = FindDirectoryNote(DirectoryPath)
if Existing is not None:
return Existing
NotePath = DirectoryPath / f"{NoteFilePrefix}{DirectoryPath.name}{DefaultNoteExtension}"
NotePath.touch()
return NotePath
def GetDirectoryNoteInfo(DirectoryPath: Path) -> dict[str, Any] | None:
"""Read-only lookup for listings - never creates a note."""
NotePath = FindDirectoryNote(DirectoryPath)
if NotePath is None:
return None
return {"Name": NotePath.name, "FileType": ClassifyFile(NotePath)}
def RenameDirectoryNote(DirectoryPath: Path, OldDirectoryName: str) -> None:
"""After a directory is renamed/moved, keep its note file's name (which
embeds the directory's own name) in sync, preserving its extension.
"""
OldPrefix = f"{NoteFilePrefix}{OldDirectoryName}."
for Candidate in DirectoryPath.iterdir():
if Candidate.is_file() and Candidate.name.startswith(OldPrefix):
NewName = f"{NoteFilePrefix}{DirectoryPath.name}{Candidate.suffix}"
Candidate.rename(DirectoryPath / NewName)
return
def ListRoots() -> list[dict[str, Any]]:
"""Return the configured roots as a flat list for the frontend."""
return [{"Name": Name} for Name in GetRoots()]
@@ -52,6 +110,7 @@ def CreateEntry(RootName: str, RelativePath: str, IsDirectory: bool) -> None:
if IsDirectory:
EntryPath.mkdir(parents=False)
EnsureDirectoryNote(EntryPath)
else:
EntryPath.parent.mkdir(parents=True, exist_ok=True)
EntryPath.touch()
@@ -70,6 +129,7 @@ def MoveEntry(RootName: str, SourceRelativePath: str, DestinationRelativePath: s
if DestinationPath.exists():
raise FileExistsError(f"{DestinationPath.name} already exists.")
OldName = SourcePath.name
DestinationPath.parent.mkdir(parents=True, exist_ok=True)
try:
@@ -78,6 +138,9 @@ def MoveEntry(RootName: str, SourceRelativePath: str, DestinationRelativePath: s
# Cross-device or other rename failures — fall back to a copy+delete.
shutil.move(str(SourcePath), str(DestinationPath))
if DestinationPath.is_dir():
RenameDirectoryNote(DestinationPath, OldName)
def DeleteEntry(RootName: str, RelativePath: str) -> None:
"""Delete a file, or an entire directory tree."""
@@ -92,11 +155,17 @@ def DeleteEntry(RootName: str, RelativePath: str) -> None:
EntryPath.unlink()
def ListDirectory(RootName: str, RelativePath: str, ShowHidden: bool = False) -> list[dict[str, Any]]:
def ListDirectory(
RootName: str, RelativePath: str, ShowHidden: bool = False, ShowSvFiles: bool = False
) -> list[dict[str, Any]]:
"""List the immediate contents of a directory.
SaraTag's internal files are always hidden, regardless of ShowHidden.
Dotfiles/dotfolders are hidden unless ShowHidden is True.
Note files (.sv_*) are hidden unless ShowSvFiles is True - independently
of ShowHidden, since they're an implementation detail rather than a
normal dotfile. Every directory's Note metadata is still attached
regardless of ShowSvFiles, so clicking the directory can open it.
Other dotfiles/dotfolders are hidden unless ShowHidden is True.
"""
DirectoryPath = ResolvePath(RootName, RelativePath)
@@ -114,7 +183,10 @@ def ListDirectory(RootName: str, RelativePath: str, ShowHidden: bool = False) ->
if IsInternalPath(EntryPath):
continue
if not ShowHidden and EntryPath.name.startswith("."):
if IsNoteFile(EntryPath.name):
if not ShowSvFiles:
continue
elif not ShowHidden and EntryPath.name.startswith("."):
continue
IsDirectory = EntryPath.is_dir()
@@ -124,6 +196,7 @@ def ListDirectory(RootName: str, RelativePath: str, ShowHidden: bool = False) ->
"IsDirectory": IsDirectory,
"FileType": None if IsDirectory else ClassifyFile(EntryPath),
"Size": None if IsDirectory else EntryPath.stat().st_size,
"Note": GetDirectoryNoteInfo(EntryPath) if IsDirectory else None,
}
)