Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 12 additions & 27 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,11 @@
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The width 30 is now used here and in countdown_timer on line 188. To improve maintainability and avoid magic numbers, consider defining this as a constant in the constants section (e.g., PROGRESS_BAR_WIDTH = 30) and using it in both places.

progress = min(1.0, current / total)
filled = int(width * progress)
bar = "β–ˆ" * filled + "β–‘" * (width - filled)
Expand All @@ -184,7 +185,7 @@
time.sleep(seconds)
return

width = 15
width = 30
for remaining in range(seconds, 0, -1):
progress = (seconds - remaining + 1) / seconds
filled = int(width * progress)
Expand All @@ -195,27 +196,7 @@
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()


Expand Down Expand Up @@ -409,9 +390,13 @@
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(

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions
f"Invalid profile ID {sanitize_for_log(profile_id)}: contains unsafe characters (allowed: A-Z, a-z, 0-9, _, -)"

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (131/100) Warning

Line too long (131/100)

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (131/100) Warning

Line too long (131/100)
)
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

Expand Down Expand Up @@ -850,7 +835,7 @@
def push_rules(
profile_id: str,
folder_name: str,
folder_id: str,

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions
do: int,
status: int,
hostnames: List[str],
Expand Down Expand Up @@ -1092,7 +1077,7 @@
grp = folder_data["group"]
name = grp["group"].strip()

if "rule_groups" in folder_data:

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions
# Multi-action format
total_rules = sum(
len(rg.get("rules", [])) for rg in folder_data["rule_groups"]
Expand Down Expand Up @@ -1219,7 +1204,7 @@
"--profiles", help="Comma-separated list of profile IDs", default=None
)
parser.add_argument(
"--folder-url", action="append", help="Folder JSON URL(s)", default=None

Check warning

Code scanning / Prospector (reported by Codacy)

Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning

Use lazy % formatting in logging functions (logging-fstring-interpolation)
)
parser.add_argument("--dry-run", action="store_true", help="Plan only")
parser.add_argument(
Expand Down Expand Up @@ -1249,13 +1234,13 @@
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(
Comment on lines 1234 to 1236
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When multiple profile IDs are entered, the all() function will short-circuit on the first invalid ID, meaning users will only see the validation error for the first invalid ID, not all of them. For better UX, consider validating all IDs before returning, e.g., using a list comprehension to collect all validation results: results = [validate_profile_id(pid, log_errors=True) for pid in ids] then return bool(ids) and all(results).

Copilot uses AI. Check for mistakes.
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()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ux.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
assert "β–‘" in combined_output
assert "β–ˆ" in combined_output
assert "Test" in combined_output
assert "Done!" in combined_output
assert "Complete!" in combined_output

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# Check for ANSI clear line code
assert "\033[K" in combined_output
Expand Down
Loading