-
Notifications
You must be signed in to change notification settings - Fork 1
🎨 Palette: Improve CLI UX with graceful exit and cleaner help #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # Palette's Journal - Critical UX/Accessibility Learnings | ||
|
|
||
| ## 2024-05-22 - Initial Setup | ||
| **Learning:** This journal was created to track UX learnings. | ||
| **Action:** Will document impactful UX discoveries here. | ||
|
|
||
| ## 2024-05-22 - CLI UX: Graceful Exits | ||
| **Learning:** Users often use Ctrl+C to exit interactive prompts. Showing a Python traceback is hostile UX. | ||
| **Action:** Always wrap interactive CLI entry points in `try...except KeyboardInterrupt` to show a clean "Cancelled" message. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -159,25 +159,6 @@ | |||||
| 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: | ||||||
|
|
@@ -1214,7 +1195,10 @@ | |||||
| # 5. Entry-point | ||||||
| # --------------------------------------------------------------------------- # | ||||||
| def parse_args() -> argparse.Namespace: | ||||||
| parser = argparse.ArgumentParser(description="Control D folder sync") | ||||||
| parser = argparse.ArgumentParser( | ||||||
| description="Control D folder sync", | ||||||
| epilog="Example: python main.py --dry-run --profiles 12345abc", | ||||||
| ) | ||||||
| parser.add_argument( | ||||||
| "--profiles", help="Comma-separated list of profile IDs", default=None | ||||||
| ) | ||||||
|
|
@@ -1239,42 +1223,48 @@ | |||||
| folder_urls = args.folder_url if args.folder_url else DEFAULT_FOLDER_URLS | ||||||
|
|
||||||
| # Interactive prompts for missing config | ||||||
| if not args.dry_run and sys.stdin.isatty(): | ||||||
| if not profile_ids: | ||||||
| print(f"{Colors.CYAN}ℹ Profile ID is missing.{Colors.ENDC}") | ||||||
| print( | ||||||
| f"{Colors.CYAN} You can find this in the URL of your profile in the Control D Dashboard (or just paste the URL).{Colors.ENDC}" | ||||||
| ) | ||||||
| try: | ||||||
| if not args.dry_run and sys.stdin.isatty(): | ||||||
| if not profile_ids: | ||||||
| print(f"{Colors.CYAN}ℹ Profile ID is missing.{Colors.ENDC}") | ||||||
| print( | ||||||
| f"{Colors.CYAN} You can find this in the URL of your profile in the Control D Dashboard (or just paste the URL).{Colors.ENDC}" | ||||||
| ) | ||||||
|
|
||||||
| 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 | ||||||
| ) | ||||||
|
|
||||||
| 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 | ||||||
| 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.", | ||||||
| ) | ||||||
| profile_ids = [ | ||||||
| extract_profile_id(p) for p in p_input.split(",") if p.strip() | ||||||
| ] | ||||||
|
|
||||||
| 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.", | ||||||
| ) | ||||||
| profile_ids = [ | ||||||
| extract_profile_id(p) for p in p_input.split(",") if p.strip() | ||||||
| ] | ||||||
|
|
||||||
| if not TOKEN: | ||||||
| print(f"{Colors.CYAN}ℹ API Token is missing.{Colors.ENDC}") | ||||||
| print( | ||||||
| f"{Colors.CYAN} You can generate one at: https://controld.com/account/manage-account{Colors.ENDC}" | ||||||
| ) | ||||||
| if not TOKEN: | ||||||
Check warningCode scanning / Pylint (reported by Codacy) Missing function docstring Warning
Missing function docstring
|
||||||
| print(f"{Colors.CYAN}ℹ API Token is missing.{Colors.ENDC}") | ||||||
| print( | ||||||
| f"{Colors.CYAN} You can generate one at: https://controld.com/account/manage-account{Colors.ENDC}" | ||||||
| ) | ||||||
|
|
||||||
| t_input = get_validated_input( | ||||||
| f"{Colors.BOLD}Enter Control D API Token:{Colors.ENDC} ", | ||||||
| lambda x: len(x) > 8, | ||||||
| "Token seems too short. Please check your API token.", | ||||||
| is_password=True, | ||||||
| ) | ||||||
| TOKEN = t_input | ||||||
| t_input = get_validated_input( | ||||||
| f"{Colors.BOLD}Enter Control D API Token:{Colors.ENDC} ", | ||||||
| lambda x: len(x) > 8, | ||||||
| "Token seems too short. Please check your API token.", | ||||||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (127/100) Warning
Line too long (127/100)
|
||||||
| is_password=True, | ||||||
| ) | ||||||
| TOKEN = t_input | ||||||
| except KeyboardInterrupt: | ||||||
| sys.stderr.write( | ||||||
| f"\n{Colors.WARNING}⚠️ Sync cancelled by user.{Colors.ENDC}\n" | ||||||
| ) | ||||||
| exit(130) | ||||||
Check warningCode scanning / Pylintpython3 (reported by Codacy) Consider using sys.exit() Warning
Consider using sys.exit()
Check warningCode scanning / Prospector (reported by Codacy) Consider using sys.exit() (consider-using-sys-exit) Warning
Consider using sys.exit() (consider-using-sys-exit)
Comment on lines
+1226
to
+1266
|
||||||
| exit(130) | |
| sys.exit(130) |
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (115/100) Warning
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (143/100) Warning