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()