diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 00000000..8bb5c48e --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2026-01-29 - CLI UX Patterns +**Learning:** CLI tools often suffer from "duplicate feedback" where a validator logs an error AND the input loop prints a generic error. +**Action:** Silence the generic error if the validator provides specific feedback, or direct the user to the specific errors. diff --git a/main.py b/main.py index 86792da4..3f6bab79 100644 --- a/main.py +++ b/main.py @@ -162,10 +162,11 @@ def sanitize_for_log(text: Any) -> str: def render_progress_bar( current: int, total: int, label: str, prefix: str = "🚀" ) -> None: + """Renders a progress bar to stderr if USE_COLORS is True.""" if not USE_COLORS or total == 0: return - width = 20 + width = 30 progress = min(1.0, current / total) filled = int(width * progress) bar = "█" * filled + "░" * (width - filled) @@ -184,7 +185,7 @@ def countdown_timer(seconds: int, message: str = "Waiting") -> None: time.sleep(seconds) return - width = 15 + width = 30 for remaining in range(seconds, 0, -1): progress = (seconds - remaining + 1) / seconds filled = int(width * progress) @@ -195,27 +196,7 @@ def countdown_timer(seconds: int, message: str = "Waiting") -> None: sys.stderr.flush() time.sleep(1) - sys.stderr.write(f"\r\033[K{Colors.GREEN}✅ {message}: Done!{Colors.ENDC}\n") - sys.stderr.flush() - - -def render_progress_bar( - current: int, total: int, label: str, prefix: str = "🚀" -) -> None: - """Renders a progress bar to stderr if USE_COLORS is True.""" - if not USE_COLORS or total == 0: - return - - width = 15 - 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.write(f"\r\033[K{Colors.GREEN}✅ {message}: Complete!{Colors.ENDC}\n") sys.stderr.flush() @@ -409,9 +390,13 @@ def validate_profile_id(profile_id: str, log_errors: bool = True) -> bool: if not is_valid_profile_id_format(profile_id): if log_errors: if not re.match(r"^[a-zA-Z0-9_-]+$", profile_id): - log.error("Invalid profile ID format (contains unsafe characters)") + log.error( + f"Invalid profile ID {sanitize_for_log(profile_id)}: contains unsafe characters (allowed: A-Z, a-z, 0-9, _, -)" + ) elif len(profile_id) > 64: - log.error("Invalid profile ID length (max 64 chars)") + log.error( + f"Invalid profile ID {sanitize_for_log(profile_id)}: too long (max 64 chars)" + ) return False return True @@ -1249,13 +1234,13 @@ def main(): def validate_profile_input(value: str) -> bool: ids = [extract_profile_id(p) for p in value.split(",") if p.strip()] return bool(ids) and all( - validate_profile_id(pid, log_errors=False) for pid in ids + validate_profile_id(pid, log_errors=True) for pid in ids ) p_input = get_validated_input( f"{Colors.BOLD}Enter Control D Profile ID:{Colors.ENDC} ", validate_profile_input, - "Invalid ID(s) or URL(s). Must be a valid Profile ID or a Control D Profile URL. Comma-separate for multiple.", + "Please enter valid Profile ID(s). See errors above.", ) profile_ids = [ extract_profile_id(p) for p in p_input.split(",") if p.strip() diff --git a/tests/test_ux.py b/tests/test_ux.py index 311b7e91..c626071e 100644 --- a/tests/test_ux.py +++ b/tests/test_ux.py @@ -25,7 +25,7 @@ def test_countdown_timer_visuals(monkeypatch): assert "░" in combined_output assert "█" in combined_output assert "Test" in combined_output - assert "Done!" in combined_output + assert "Complete!" in combined_output # Check for ANSI clear line code assert "\033[K" in combined_output