diff --git a/.Jules/palette.md b/.Jules/palette.md index 532c04f4..eea3a5d4 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -9,3 +9,7 @@ ## 2024-05-24 - Fail Fast & Friendly **Learning:** In CLI tools involving APIs, cascade failures (hundreds of "Failed to X") caused by basic auth issues (401/403) are overwhelming and confusing. A dedicated "Pre-flight Check" that validates credentials *before* attempting the main workload allows for specific, actionable error messages (e.g. "Check your token at [URL]") instead of generic HTTP errors. **Action:** Implement a `check_api_access()` step at the start of any CLI workflow to validate permissions and provide human-readable guidance on failure. + +## 2024-05-25 - Consistent Progress Feedback +**Learning:** Inconsistent progress indicators (some text-based, some bars) degrade the perceived polish of a CLI tool. Centralizing progress logic into a single helper ensures consistent visuals (bars vs counters) and reliable terminal handling (clearing lines properly) across different operations. +**Action:** Implement a reusable `render_progress_bar` function that handles ANSI codes and math safety, rather than re-implementing loops for each task. diff --git a/main.py b/main.py index c7810580..373230f6 100644 --- a/main.py +++ b/main.py @@ -110,6 +110,21 @@ def sanitize_for_log(text: Any) -> str: return safe +def render_progress_bar(current: int, total: int, label: str, prefix: str = "🚀") -> None: + if not USE_COLORS or total == 0: + return + + width = 20 + progress = min(1.0, current / total) + filled = int(width * progress) + bar = "█" * filled + "░" * (width - filled) + percent = int(progress * 100) + + # Use \033[K to clear line residue + sys.stderr.write(f"\r\033[K{Colors.CYAN}{prefix} {label}: [{bar}] {percent}% ({current}/{total}){Colors.ENDC}") + sys.stderr.flush() + + def countdown_timer(seconds: int, message: str = "Waiting") -> None: """Shows a countdown timer if strictly in a TTY, otherwise just sleeps.""" if not USE_COLORS: @@ -481,16 +496,10 @@ def warm_up_cache(urls: Sequence[str]) -> None: with concurrent.futures.ThreadPoolExecutor() as executor: futures = {executor.submit(_gh_get, url): url for url in urls_to_fetch} - if USE_COLORS: - sys.stderr.write(f"\r{Colors.CYAN}⏳ Warming up cache: 0/{total}...{Colors.ENDC}") - sys.stderr.flush() + render_progress_bar(0, total, "Warming up cache", prefix="⏳") for future in concurrent.futures.as_completed(futures): completed += 1 - if USE_COLORS: - sys.stderr.write(f"\r{Colors.CYAN}⏳ Warming up cache: {completed}/{total}...{Colors.ENDC}") - sys.stderr.flush() - try: future.result() except Exception as e: @@ -501,13 +510,10 @@ def warm_up_cache(urls: Sequence[str]) -> None: log.warning(f"Failed to pre-fetch {sanitize_for_log(futures[future])}: {e}") - if USE_COLORS: - # Restore progress - sys.stderr.write(f"\r{Colors.CYAN}⏳ Warming up cache: {completed}/{total}...{Colors.ENDC}") - sys.stderr.flush() + render_progress_bar(completed, total, "Warming up cache", prefix="⏳") if USE_COLORS: - sys.stderr.write(f"\r{Colors.GREEN}✅ Warming up cache: {total}/{total} Done! {Colors.ENDC}\n") + sys.stderr.write(f"\r\033[K{Colors.GREEN}✅ Warming up cache: Done!{Colors.ENDC}\n") sys.stderr.flush() def delete_folder(client: httpx.Client, profile_id: str, name: str, folder_id: str) -> bool: @@ -670,13 +676,11 @@ def process_batch(batch_idx: int, batch_data: List[str]) -> Optional[List[str]]: successful_batches += 1 existing_rules.update(result) - if USE_COLORS: - sys.stderr.write(f"\r{Colors.CYAN}🚀 Folder {sanitize_for_log(folder_name)}: Pushing batch {successful_batches}/{total_batches}...{Colors.ENDC}") - sys.stderr.flush() + render_progress_bar(successful_batches, total_batches, f"Folder {sanitize_for_log(folder_name)}") if successful_batches == total_batches: if USE_COLORS: - sys.stderr.write(f"\r{Colors.GREEN}✅ Folder {sanitize_for_log(folder_name)}: Finished ({len(filtered_hostnames)} rules) {Colors.ENDC}\n") + sys.stderr.write(f"\r\033[K{Colors.GREEN}✅ Folder {sanitize_for_log(folder_name)}: Finished ({len(filtered_hostnames)} rules){Colors.ENDC}\n") sys.stderr.flush() else: log.info("Folder %s – finished (%d new rules added)", sanitize_for_log(folder_name), len(filtered_hostnames))