diff --git a/repoman.py b/repoman.py index f3df88a..a4f8808 100644 --- a/repoman.py +++ b/repoman.py @@ -9,6 +9,8 @@ import os import shutil import subprocess import sys +import threading +import time import tomllib import urllib.error import urllib.parse @@ -23,7 +25,7 @@ from rich.live import Live from rich.table import Table from rich.text import Text -__version__ = "1.0.3" +__version__ = "1.0.4" console = Console() @@ -258,6 +260,13 @@ def build_entries(remote_repos: list[RemoteRepo], local_index: dict[str, Path]) # --- render ----------------------------------------------------------------- +SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + + +def spinner_frame() -> str: + return SPINNER_FRAMES[int(time.monotonic() * 10) % len(SPINNER_FRAMES)] + + def compute_page(cursor: int, total: int) -> tuple[int, int, int, int, int]: """Calcula (start, end, page_idx, total_pages, page_size) per a la finestra visible.""" h = max(10, console.size.height) @@ -294,7 +303,7 @@ def render(entries: list[RepoEntry], cursor: int, base: Path, owner: str, status # Estado if e.cloning: - state = Text("⟳ clonando…", style="bold blue") + state = Text(f"{spinner_frame()} clonant…", style="bold blue") elif e.cloned_ok is True: state = Text("✓ clonado", style="bold green") elif e.cloned_ok is False: @@ -364,27 +373,44 @@ def clone_one(entry: RepoEntry, base: Path, cfg: Config) -> tuple[RepoEntry, boo def run_clone_queue(entries: list[RepoEntry], base: Path, cfg: Config, live: Live, cursor: int, owner: str) -> int: pending = [e for e in entries if e.selected and e.local_path is None and e.remote is not None] if not pending: - live.update(render(entries, cursor, base, owner, "Nada marcado para clonar.")) + live.update(render(entries, cursor, base, owner, "Res marcat per clonar."), refresh=True) return 0 for e in pending: e.cloning = True e.selected = False - live.update(render(entries, cursor, base, owner, f"Clonando {len(pending)} repo(s)…")) - ok = 0 - max_workers = min(4, len(pending)) - with ThreadPoolExecutor(max_workers=max_workers) as ex: - futures = {ex.submit(clone_one, e, base, cfg): e for e in pending} - for fut in as_completed(futures): - entry, success, msg = fut.result() - entry.cloning = False - entry.cloned_ok = success - entry.error = "" if success else msg - if success: - entry.local_path = base / entry.name - ok += 1 - live.update(render(entries, cursor, base, owner, f"Clonando… {ok}/{len(pending)} listos")) - live.update(render(entries, cursor, base, owner, f"Hecho: {ok}/{len(pending)} clonados.")) - return ok + total = len(pending) + + stop = threading.Event() + state = {"ok": 0, "done": 0} + + def animate() -> None: + while not stop.is_set(): + in_progress = total - state["done"] + msg = f"Clonant {in_progress} en curs · {state['done']}/{total} acabats" + live.update(render(entries, cursor, base, owner, msg), refresh=True) + stop.wait(0.1) + + anim = threading.Thread(target=animate, daemon=True) + anim.start() + try: + max_workers = min(4, total) + with ThreadPoolExecutor(max_workers=max_workers) as ex: + futures = {ex.submit(clone_one, e, base, cfg): e for e in pending} + for fut in as_completed(futures): + entry, success, msg = fut.result() + entry.cloning = False + entry.cloned_ok = success + entry.error = "" if success else msg + if success: + entry.local_path = base / entry.name + state["ok"] += 1 + state["done"] += 1 + finally: + stop.set() + anim.join() + + live.update(render(entries, cursor, base, owner, f"Fet: {state['ok']}/{total} clonats."), refresh=True) + return state["ok"] # --- TUI loop ---------------------------------------------------------------