-
Notifications
You must be signed in to change notification settings - Fork 1
🎨 Palette: Polish CLI progress bar & fix cursor visibility #165
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,3 @@ | ||
| ## 2024-05-21 - CLI Cursor Hygiene | ||
| **Learning:** Hiding the terminal cursor (`\033[?25l`) during progress bar updates eliminates flickering and looks more professional. | ||
| **Action:** Always use `atexit` to register a cleanup function that restores the cursor (`\033[?25h`) to prevent leaving the user's terminal in a broken state if the script crashes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| """ | ||
|
|
||
| import argparse | ||
| import atexit | ||
| import concurrent.futures | ||
| import getpass | ||
| import ipaddress | ||
|
|
@@ -56,6 +57,8 @@ | |
| ENDC = "\033[0m" | ||
| BOLD = "\033[1m" | ||
| UNDERLINE = "\033[4m" | ||
| CURSOR_HIDE = "\033[?25l" | ||
| CURSOR_SHOW = "\033[?25h" | ||
| else: | ||
| HEADER = "" | ||
| BLUE = "" | ||
|
|
@@ -66,6 +69,8 @@ | |
| ENDC = "" | ||
| BOLD = "" | ||
| UNDERLINE = "" | ||
| CURSOR_HIDE = "" | ||
| CURSOR_SHOW = "" | ||
|
|
||
|
|
||
| class ColoredFormatter(logging.Formatter): | ||
|
|
@@ -102,6 +107,16 @@ | |
| logging.getLogger("httpx").setLevel(logging.WARNING) | ||
|
|
||
|
|
||
| def restore_cursor(): | ||
| """Ensure cursor is visible when the script exits.""" | ||
| if USE_COLORS: | ||
| sys.stderr.write(Colors.CURSOR_SHOW) | ||
| sys.stderr.flush() | ||
|
|
||
|
|
||
| atexit.register(restore_cursor) | ||
|
Comment on lines
+110
to
+117
|
||
|
|
||
|
|
||
| def check_env_permissions(env_path: str = ".env") -> None: | ||
| """ | ||
| Check .env file permissions and warn if readable by others. | ||
|
|
@@ -159,25 +174,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: | ||
|
|
@@ -190,7 +186,7 @@ | |
| filled = int(width * progress) | ||
| bar = "█" * filled + "░" * (width - filled) | ||
| sys.stderr.write( | ||
| f"\r{Colors.CYAN}⏳ {message}: [{bar}] {remaining}s...{Colors.ENDC}" | ||
| f"\r{Colors.CURSOR_HIDE}{Colors.CYAN}⏳ {message}: [{bar}] {remaining}s...{Colors.ENDC}" | ||
| ) | ||
| sys.stderr.flush() | ||
| time.sleep(1) | ||
|
Comment on lines
+189
to
192
|
||
|
|
@@ -206,15 +202,15 @@ | |
| if not USE_COLORS or total == 0: | ||
| return | ||
|
|
||
| width = 15 | ||
| width = 30 | ||
| 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}" | ||
| f"\r{Colors.CURSOR_HIDE}\033[K{Colors.CYAN}{prefix} {label}: [{bar}] {percent}% ({current}/{total}){Colors.ENDC}" | ||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (121/100) Warning
Line too long (121/100)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Line too long (121/100) Warning
Line too long (121/100)
|
||
| ) | ||
| sys.stderr.flush() | ||
|
|
||
|
Comment on lines
+213
to
216
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make the
atexithandler more robust, consider wrapping thewriteoperation in atry...exceptblock. In the edge case wheresys.stderris closed or otherwise unavailable when this handler runs,sys.stderr.write()would raise an exception, leading to a traceback on exit. This change will prevent that and ensure a cleaner exit.