snapshot
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_editor() -> str:
|
||||
editor = os.getenv("EDITOR")
|
||||
if editor:
|
||||
return editor
|
||||
|
||||
for candidate in ["micro", "nano"]:
|
||||
if shutil.which(candidate):
|
||||
return candidate
|
||||
|
||||
return "nano"
|
||||
|
||||
|
||||
def edit_text(text: str) -> str | None:
|
||||
with tempfile.NamedTemporaryFile(suffix=".tmp", delete=False) as tf:
|
||||
temp_file_path = Path(tf.name)
|
||||
temp_file_path.write_text(text)
|
||||
|
||||
old_modtime = temp_file_path.stat().st_mtime
|
||||
subprocess.call([*find_editor().split(), str(temp_file_path)])
|
||||
new_modtime = temp_file_path.stat().st_mtime
|
||||
if new_modtime == old_modtime:
|
||||
logging.warning("editor exited without saving")
|
||||
return None
|
||||
|
||||
return temp_file_path.read_text()
|
||||
|
||||
|
||||
def move_to_trash(file_path: Path):
|
||||
if not file_path.is_file():
|
||||
raise FileNotFoundError
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"osascript",
|
||||
"-e",
|
||||
f'tell app "Finder" to move POSIX file "{file_path}" to trash',
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def rename_files(files: list[Path], cwd: Path | None = None):
|
||||
if not cwd:
|
||||
cwd = files[0].parent
|
||||
|
||||
originals = [f"{f.relative_to(cwd)}" for f in files]
|
||||
edited_raw = edit_text("\n".join(originals))
|
||||
if not edited_raw:
|
||||
return
|
||||
edited = edited_raw.splitlines(keepends=False)
|
||||
|
||||
deletables = []
|
||||
moveables = []
|
||||
for source, target in zip(originals, edited):
|
||||
if target.startswith("#") or target == "":
|
||||
deletables.append(Path(source))
|
||||
continue
|
||||
if source != target:
|
||||
moveables.append((cwd / source, cwd / target))
|
||||
|
||||
for source, target in moveables:
|
||||
logging.info(f"moving {source} -> {target}")
|
||||
source.rename(target)
|
||||
|
||||
for f in deletables:
|
||||
try:
|
||||
logging.info(f"trashing {f}")
|
||||
move_to_trash(f)
|
||||
except:
|
||||
logging.error(f"failed to trash {f}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
arger = argparse.ArgumentParser()
|
||||
arger.add_argument("filenames", type=Path, nargs="+", help="File paths")
|
||||
arger.add_argument("--cwd", type=Path, help="Current working directory")
|
||||
return arger.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
rename_files(args.filenames, cwd=args.cwd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
main()
|
||||
Reference in New Issue
Block a user