diff --git a/.Jules/palette.md b/.Jules/palette.md index 13f6528..4e317ea 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -1,3 +1,7 @@ ## 2024-05-22 - Helpful CLI Prompts **Learning:** Even in CLI tools, users often get stuck on authentication steps (Tokens/IDs). Providing direct URLs or location hints in the prompt text significantly reduces friction compared to forcing users to consult external docs. **Action:** When prompting for credentials in CLI tools, always include a "Where to find this" hint or direct URL. + +## 2024-05-23 - CLI Progress Bars +**Learning:** Using clear-line ANSI codes (`\033[K`) is significantly more robust than space-padding for overwriting CLI lines, especially when line lengths vary between updates. Visual progress bars (e.g., `[██░░]`) provide better psychological feedback for waiting periods than simple countdowns. +**Action:** Use `\033[K` for dynamic CLI updates and favor visual bars for waits > 5 seconds. diff --git a/main.py b/main.py index b6bbb8b..9f324ef 100644 --- a/main.py +++ b/main.py @@ -117,12 +117,16 @@ def countdown_timer(seconds: int, message: str = "Waiting") -> None: time.sleep(seconds) return + width = 15 for remaining in range(seconds, 0, -1): - sys.stderr.write(f"\r{Colors.CYAN}⏳ {message}: {remaining}s...{Colors.ENDC}") + progress = (seconds - remaining + 1) / seconds + filled = int(width * progress) + bar = "█" * filled + "░" * (width - filled) + sys.stderr.write(f"\r{Colors.CYAN}⏳ {message}: [{bar}] {remaining}s...{Colors.ENDC}") sys.stderr.flush() time.sleep(1) - sys.stderr.write(f"\r{Colors.GREEN}✅ {message}: Done! {Colors.ENDC}\n") + sys.stderr.write(f"\r\033[K{Colors.GREEN}✅ {message}: Done!{Colors.ENDC}\n") sys.stderr.flush() diff --git a/tests/test_ux.py b/tests/test_ux.py new file mode 100644 index 0000000..311b7e9 --- /dev/null +++ b/tests/test_ux.py @@ -0,0 +1,46 @@ + +import sys +from unittest.mock import MagicMock +import main + +def test_countdown_timer_visuals(monkeypatch): + """Verify that countdown_timer writes a progress bar to stderr.""" + # Force colors on + monkeypatch.setattr(main, "USE_COLORS", True) + + # Mock stderr + mock_stderr = MagicMock() + monkeypatch.setattr(sys, "stderr", mock_stderr) + + # Mock time.sleep to run instantly + monkeypatch.setattr(main.time, "sleep", MagicMock()) + + main.countdown_timer(3, "Test") + + # Check calls + writes = [args[0] for args, _ in mock_stderr.write.call_args_list] + combined_output = "".join(writes) + + # Check for progress bar chars + assert "░" in combined_output + assert "█" in combined_output + assert "Test" in combined_output + assert "Done!" in combined_output + + # Check for ANSI clear line code + assert "\033[K" in combined_output + +def test_countdown_timer_no_colors(monkeypatch): + """Verify that countdown_timer sleeps without writing to stderr if NO_COLOR.""" + monkeypatch.setattr(main, "USE_COLORS", False) + mock_stderr = MagicMock() + monkeypatch.setattr(sys, "stderr", mock_stderr) + mock_sleep = MagicMock() + monkeypatch.setattr(main.time, "sleep", mock_sleep) + + main.countdown_timer(3, "Test") + + # Should not write to stderr + mock_stderr.write.assert_not_called() + # Should call sleep exactly once with full seconds + mock_sleep.assert_called_once_with(3)