#!/usr/bin/env python3 """Render rsync's per-file output as a single updating status line. Fed the paths rsync reports with --out-format='%n', one per line. Prints one line that rewrites itself, showing how far through the transfer is and which album is currently going across, rather than either scrolling fifty thousand filenames past or -- as rsync does while it builds its file list -- saying nothing at all for several minutes. Falls back to periodic plain lines when stderr is not a terminal, so a log does not fill up with carriage returns. """ import argparse import os import shutil import sys import time def album_of(path): """Return "Artist / Album" for a mirror-relative path.""" parts = [part for part in path.strip("/").split("/") if part] if len(parts) >= 3: return f"{parts[0]} / {parts[1]}" if len(parts) == 2: return parts[0] return "" def fit(text, width): """Trim to the terminal, from the left: the album matters more than the artist.""" if width <= 1 or len(text) <= width: return text return "…" + text[-(width - 1) :] def render(done, total, label, width): if total > 0: share = min(100, done * 100 // total) head = f"[{done:>6,} / {total:<6,} {share:>3}%] " else: head = f"[{done:>6,} files] " return head + fit(label, max(0, width - len(head))) def main(argv=None, stream=None, out=None): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--total", type=int, default=0, help="files expected") parser.add_argument("--interval", type=float, default=0.1, help="seconds between redraws") args = parser.parse_args(argv) stream = stream or sys.stdin out = out or sys.stderr interactive = out.isatty() width = shutil.get_terminal_size((100, 24)).columns - 1 done = 0 last_drawn = 0.0 label = "" for line in stream: path = line.rstrip("\n") # rsync reports directories too, with a trailing slash. They are not # files and counting them would put the percentage past a hundred. if not path or path.endswith("/"): continue done += 1 label = album_of(path) or os.path.basename(path) now = time.monotonic() if interactive: if now - last_drawn >= args.interval: out.write("\r\033[2K" + render(done, args.total, label, width)) out.flush() last_drawn = now elif now - last_drawn >= 30: out.write(render(done, args.total, label, width) + "\n") out.flush() last_drawn = now if interactive: out.write("\r\033[2K") out.write(render(done, args.total, label, width).rstrip() + "\n") out.flush() return 0 if __name__ == "__main__": sys.exit(main())