spinner animat i comptador de clones en curs

This commit is contained in:
2026-05-18 11:47:06 +02:00
parent f49512b72b
commit f35ce3c5dd
+45 -19
View File
@@ -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 ---------------------------------------------------------------