From 3314546a212396181eb57079df07f9b50789fb89 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:43:42 +0000 Subject: [PATCH 1/2] feat(ux): enhance CLI visuals with cleaner progress bars and Unicode summary table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated progress bar empty character from '░' to '·' for a cleaner look. - Implemented `print_summary_table` to render a polished Unicode box-drawing table when colors are enabled, falling back to ASCII otherwise. - Extracted table rendering logic into a reusable function for better organization. - Updated `tests/test_ux.py` to assert the new progress bar character. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- main.py | 117 ++++++++++++++++++----------------------------- tests/test_ux.py | 2 +- 2 files changed, 45 insertions(+), 74 deletions(-) diff --git a/main.py b/main.py index 1f4f047..c24efce 100644 --- a/main.py +++ b/main.py @@ -332,7 +332,7 @@ def countdown_timer(seconds: int, message: str = "Waiting") -> None: for remaining in range(seconds, 0, -1): progress = (seconds - remaining + 1) / seconds filled = int(width * progress) - bar = "█" * filled + "░" * (width - filled) + bar = "█" * filled + "·" * (width - filled) sys.stderr.write( f"\r{Colors.CYAN}⏳ {message}: [{bar}] {remaining}s...{Colors.ENDC}" ) @@ -354,7 +354,7 @@ def render_progress_bar( progress = min(1.0, current / total) filled = int(width * progress) - bar = "█" * filled + "░" * (width - filled) + bar = "█" * filled + "·" * (width - filled) percent = int(progress * 100) # Use \033[K to clear line residue @@ -1873,6 +1873,47 @@ def _fetch_if_valid(url: str): # --------------------------------------------------------------------------- # # 5. Entry-point # --------------------------------------------------------------------------- # +def print_summary_table( + sync_results: List[Dict[str, Any]], success_count: int, total: int, dry_run: bool +) -> None: + # 1. Setup Data + max_p = max((len(r["profile"]) for r in sync_results), default=25) + w = [max(25, max_p), 10, 12, 10, 15] + + t_f, t_r, t_d = sum(r["folders"] for r in sync_results), sum(r["rules"] for r in sync_results), sum(r["duration"] for r in sync_results) + all_ok = success_count == total + t_status = ("✅ Ready" if dry_run else "✅ All Good") if all_ok else "❌ Errors" + t_col = Colors.GREEN if all_ok else Colors.FAIL + + # 2. Render + if not USE_COLORS: + # Simple ASCII Fallback + header = f"{'Profile ID':<{w[0]}} | {'Folders':>{w[1]}} | {'Rules':>{w[2]}} | {'Duration':>{w[3]}} | {'Status':<{w[4]}}" + sep = "-" * len(header) + print(f"\n{('DRY RUN' if dry_run else 'SYNC') + ' SUMMARY':^{len(header)}}\n{sep}\n{header}\n{sep}") + for r in sync_results: + print(f"{r['profile']:<{w[0]}} | {r['folders']:>{w[1]}} | {r['rules']:>{w[2]},} | {r['duration']:>{w[3]-1}.1f}s | {r['status_label']:<{w[4]}}") + print(f"{sep}\n{'TOTAL':<{w[0]}} | {t_f:>{w[1]}} | {t_r:>{w[2]},} | {t_d:>{w[3]-1}.1f}s | {t_status:<{w[4]}}\n{sep}\n") + return + + # Unicode Table + def line(l, m, r): return f"{Colors.BOLD}{l}{m.join('─' * (x+2) for x in w)}{r}{Colors.ENDC}" + def row(c): return f"{Colors.BOLD}│{Colors.ENDC} {c[0]:<{w[0]}} {Colors.BOLD}│{Colors.ENDC} {c[1]:>{w[1]}} {Colors.BOLD}│{Colors.ENDC} {c[2]:>{w[2]}} {Colors.BOLD}│{Colors.ENDC} {c[3]:>{w[3]}} {Colors.BOLD}│{Colors.ENDC} {c[4]:<{w[4]}} {Colors.BOLD}│{Colors.ENDC}" + + print(f"\n{line('┌', '─', '┐')}") + title = f"{'DRY RUN' if dry_run else 'SYNC'} SUMMARY" + print(f"{Colors.BOLD}│{Colors.CYAN if dry_run else Colors.HEADER}{title:^{sum(w) + 14}}{Colors.ENDC}{Colors.BOLD}│{Colors.ENDC}") + print(f"{line('├', '┬', '┤')}\n{row([f'{Colors.HEADER}Profile ID{Colors.ENDC}', f'{Colors.HEADER}Folders{Colors.ENDC}', f'{Colors.HEADER}Rules{Colors.ENDC}', f'{Colors.HEADER}Duration{Colors.ENDC}', f'{Colors.HEADER}Status{Colors.ENDC}'])}") + print(line("├", "┼", "┤")) + + for r in sync_results: + sc = Colors.GREEN if r["success"] else Colors.FAIL + print(row([r["profile"], str(r["folders"]), f"{r['rules']:,}", f"{r['duration']:.1f}s", f"{sc}{r['status_label']}{Colors.ENDC}"])) + + print(f"{line('├', '┼', '┤')}\n{row(['TOTAL', str(t_f), f'{t_r:,}', f'{t_d:.1f}s', f'{t_col}{t_status}{Colors.ENDC}'])}") + print(f"{line('└', '┴', '┘')}\n") + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Control D folder sync") parser.add_argument( @@ -2047,78 +2088,8 @@ def validate_profile_input(value: str) -> bool: log.info("Plan written to %s", args.plan_json) # Print Summary Table - # Determine the width for the Profile ID column (min 25) - max_profile_len = max((len(r["profile"]) for r in sync_results), default=25) - profile_col_width = max(25, max_profile_len) - - # Calculate total width for the table - # Profile ID + " | " + Folders + " | " + Rules + " | " + Duration + " | " + Status - # Widths: profile_col_width + 3 + 10 + 3 + 10 + 3 + 10 + 3 + 15 = profile_col_width + 57 - table_width = profile_col_width + 57 - - title_text = "DRY RUN SUMMARY" if args.dry_run else "SYNC SUMMARY" - title_color = Colors.CYAN if args.dry_run else Colors.HEADER - - print("\n" + "=" * table_width) - print(f"{title_color}{title_text:^{table_width}}{Colors.ENDC}") - print("=" * table_width) - - # Header - print( - f"{Colors.BOLD}" - f"{'Profile ID':<{profile_col_width}} | {'Folders':>10} | {'Rules':>10} | {'Duration':>10} | {'Status':<15}" - f"{Colors.ENDC}" - ) - print("-" * table_width) - - # Rows - total_folders = 0 - total_rules = 0 - total_duration = 0.0 - - for res in sync_results: - # Use boolean success field for color logic - status_color = Colors.GREEN if res["success"] else Colors.FAIL - - print( - f"{res['profile']:<{profile_col_width}} | " - f"{res['folders']:>10} | " - f"{res['rules']:>10,} | " - f"{res['duration']:>9.1f}s | " - f"{status_color}{res['status_label']:<15}{Colors.ENDC}" - ) - total_folders += res["folders"] - total_rules += res["rules"] - total_duration += res["duration"] - - print("-" * table_width) - - # Total Row total = len(profile_ids or ["dry-run-placeholder"]) - all_success = success_count == total - - if args.dry_run: - if all_success: - total_status_text = "✅ Ready" - else: - total_status_text = "❌ Errors" - else: - if all_success: - total_status_text = "✅ All Good" - else: - total_status_text = "❌ Errors" - - total_status_color = Colors.GREEN if all_success else Colors.FAIL - - print( - f"{Colors.BOLD}" - f"{'TOTAL':<{profile_col_width}} | " - f"{total_folders:>10} | " - f"{total_rules:>10,} | " - f"{total_duration:>9.1f}s | " - f"{total_status_color}{total_status_text:<15}{Colors.ENDC}" - ) - print("=" * table_width + "\n") + print_summary_table(sync_results, success_count, total, args.dry_run) # Display cache statistics if any cache activity occurred if _cache_stats["hits"] + _cache_stats["misses"] + _cache_stats["validations"] > 0: diff --git a/tests/test_ux.py b/tests/test_ux.py index ad593ee..ecd973a 100644 --- a/tests/test_ux.py +++ b/tests/test_ux.py @@ -22,7 +22,7 @@ def test_countdown_timer_visuals(monkeypatch): combined_output = "".join(writes) # Check for progress bar chars - assert "░" in combined_output + assert "·" in combined_output assert "█" in combined_output assert "Test" in combined_output assert "Done!" in combined_output From 6099aedf112b6728ed860676e6f4c0759bfa183a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:44:40 +0000 Subject: [PATCH 2/2] feat(ux): enhance CLI visuals and fix CI test failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated progress bar empty character from '░' to '·' for a cleaner look. - Implemented `print_summary_table` to render a polished Unicode box-drawing table when colors are enabled, falling back to ASCII otherwise. - Extracted table rendering logic into a reusable function. - Updated `tests/test_ux.py` to assert the new progress bar character. - Fixed `TypeError` in `tests/test_content_type.py` caused by duplicate method definition in merged code. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- tests/test_content_type.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_content_type.py b/tests/test_content_type.py index a814aec..c2824b2 100644 --- a/tests/test_content_type.py +++ b/tests/test_content_type.py @@ -67,10 +67,6 @@ def test_reject_text_html(self, mock_stream): self.assertIn("Invalid Content-Type", str(cm.exception)) @patch('main._gh.stream') - def test_reject_xml(self, mock_stream): - """Test that application/xml is rejected.""" - mock_response = MagicMock() - mock_response.status_code = 200 def test_reject_xml(self, mock_stream): """Test that application/xml is rejected.""" mock_response = MagicMock()