diff --git a/main.py b/main.py index 86792da4..7ababd63 100644 --- a/main.py +++ b/main.py @@ -159,25 +159,6 @@ 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: @@ -1033,6 +1014,40 @@ def _process_single_folder( return folder_success +def print_dry_run_plan(plan_entry: Dict[str, Any]) -> None: + profile = sanitize_for_log(plan_entry["profile"]) + folders = plan_entry["folders"] + + if USE_COLORS: + print( + f"\n{Colors.HEADER}šŸ“ Dry Run Plan for Profile: {Colors.BOLD}{profile}{Colors.ENDC}" + ) + print( + f"{Colors.CYAN} The following folders will be created/replaced:{Colors.ENDC}" + ) + else: + print(f"\nšŸ“ Dry Run Plan for Profile: {profile}") + print(" The following folders will be created/replaced:") + + if not folders: + print( + f" {Colors.WARNING}(No folders found to sync){Colors.ENDC}" + if USE_COLORS + else " (No folders found to sync)" + ) + + for folder in folders: + safe_name = sanitize_for_log(folder["name"]) + safe_rules = sanitize_for_log(folder["rules"]) + if USE_COLORS: + print( + f" • {Colors.BOLD}{safe_name}{Colors.ENDC}: {safe_rules} rules" + ) + else: + print(f" - {safe_name}: {safe_rules} rules") + print("") + + # --------------------------------------------------------------------------- # # 4. Main workflow # --------------------------------------------------------------------------- # @@ -1129,6 +1144,7 @@ def _fetch_if_valid(url: str): plan_accumulator.append(plan_entry) if dry_run: + print_dry_run_plan(plan_entry) log.info("Dry-run complete: no API calls were made.") return True diff --git a/tests/test_palette_ux.py b/tests/test_palette_ux.py new file mode 100644 index 00000000..16987fa6 --- /dev/null +++ b/tests/test_palette_ux.py @@ -0,0 +1,89 @@ +import sys +from unittest.mock import MagicMock +import main + +def test_print_dry_run_plan_with_colors(monkeypatch): + """Verify print_dry_run_plan output with colors enabled.""" + # Force colors on + monkeypatch.setattr(main, "USE_COLORS", True) + + # Since Colors class is evaluated at import time, we need to manually set color codes + # if the module was imported in a non-TTY environment + for attr, code in { + "HEADER": "\033[95m", + "BLUE": "\033[94m", + "CYAN": "\033[96m", + "GREEN": "\033[92m", + "WARNING": "\033[93m", + "FAIL": "\033[91m", + "ENDC": "\033[0m", + "BOLD": "\033[1m", + "UNDERLINE": "\033[4m", + }.items(): + monkeypatch.setattr(main.Colors, attr, code) + + # Mock stdout to capture print output + mock_stdout = MagicMock() + monkeypatch.setattr(sys, "stdout", mock_stdout) + + plan = { + "profile": "test_profile", + "folders": [ + {"name": "Test Folder 1", "rules": 10}, + {"name": "Test Folder 2", "rules": 20}, + ] + } + + main.print_dry_run_plan(plan) + + # Aggregate all writes + # print() typically calls write(string) and write('\n') + combined_output = "".join([str(args[0]) for args, _ in mock_stdout.write.call_args_list]) + + assert "šŸ“ Dry Run Plan for Profile:" in combined_output + assert "test_profile" in combined_output + assert "Test Folder 1" in combined_output + assert "10 rules" in combined_output + # ANSI codes should be present (main.Colors.HEADER starts with \033[95m) + assert "\033[" in combined_output + +def test_print_dry_run_plan_no_colors(monkeypatch): + """Verify print_dry_run_plan output without colors.""" + monkeypatch.setattr(main, "USE_COLORS", False) + + mock_stdout = MagicMock() + monkeypatch.setattr(sys, "stdout", mock_stdout) + + plan = { + "profile": "test_profile", + "folders": [ + {"name": "Test Folder 1", "rules": 10}, + ] + } + + main.print_dry_run_plan(plan) + + combined_output = "".join([str(args[0]) for args, _ in mock_stdout.write.call_args_list]) + + assert "šŸ“ Dry Run Plan for Profile:" in combined_output + assert "test_profile" in combined_output + assert "Test Folder 1" in combined_output + assert "10 rules" in combined_output + # No ANSI codes + assert "\033[" not in combined_output + +def test_print_dry_run_plan_empty_folders(monkeypatch): + """Verify output when no folders are present.""" + monkeypatch.setattr(main, "USE_COLORS", False) + mock_stdout = MagicMock() + monkeypatch.setattr(sys, "stdout", mock_stdout) + + plan = { + "profile": "test_profile", + "folders": [] + } + + main.print_dry_run_plan(plan) + + combined_output = "".join([str(args[0]) for args, _ in mock_stdout.write.call_args_list]) + assert "(No folders found to sync)" in combined_output