106 lines
2.8 KiB
Python
Executable File
106 lines
2.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
from functools import cache
|
|
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) -> None:
|
|
"""Move file to Trash using trash CLI."""
|
|
if not file_path.is_file():
|
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
|
|
subprocess.run(
|
|
["trash", str(file_path.resolve())],
|
|
check=True,
|
|
)
|
|
|
|
|
|
@cache
|
|
def make_dir(path: Path) -> None:
|
|
path.mkdir(parents=True, exist_ok=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)
|
|
|
|
if len(edited) != len(originals):
|
|
logging.error("number of edited lines does not match number of original lines")
|
|
return
|
|
|
|
deletables = []
|
|
moveables: list[tuple[Path, Path]] = []
|
|
for source, target in zip(originals, edited):
|
|
if target.startswith("#") or target == "":
|
|
deletables.append((cwd / source).resolve())
|
|
continue
|
|
if source != target:
|
|
moveables.append((cwd / source, cwd / target))
|
|
|
|
for source, target in moveables:
|
|
logging.info(f"moving {source} -> {target}")
|
|
make_dir(target.parent)
|
|
source.rename(target)
|
|
|
|
for f in deletables:
|
|
try:
|
|
logging.info(f"trashing {f}")
|
|
move_to_trash(f)
|
|
except Exception as e:
|
|
logging.error(f"failed to trash {f}: {e}")
|
|
|
|
|
|
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()
|