Files
playground/multi_rename.py
T
2024-12-24 20:49:41 +03:00

97 lines
2.4 KiB
Python

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