Skip to content
Open
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
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2025-01-26 - [Silent Waits in CI]
**Learning:** Long silent waits in CLI tools (especially in CI/non-interactive mode) cause user anxiety about hung processes.
**Action:** Always provide periodic heartbeat logs (e.g. every 10s) for long operations in non-interactive environments.

## 2025-02-14 - [ASCII Fallback for Tables]
**Learning:** Using Unicode box drawing characters enhances the CLI experience, but a robust ASCII fallback is crucial for CI environments and piped outputs.
**Action:** Always implement a fallback mechanism (like checking `sys.stderr.isatty()`) when using rich text or Unicode symbols.
119 changes: 90 additions & 29 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import logging
import os
import platform
import random
import re
import shutil
import socket
Expand Down Expand Up @@ -71,6 +72,15 @@
UNDERLINE = ""


class Box:
"""Box drawing characters for pretty tables."""

if USE_COLORS:
H, V, TL, TR, BL, BR, T, B, L, R, X = "─", "│", "┌", "┐", "└", "┘", "┬", "┴", "├", "┤", "┼"
else:
H, V, TL, TR, BL, BR, T, B, L, R, X = "-", "|", "+", "+", "+", "+", "+", "+", "+", "+", "+"

Comment on lines +75 to +82
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The new table rendering functionality (Box class and card-style summary table) lacks test coverage. Given that this project has comprehensive test coverage across 16 test files, the new visual features should have corresponding tests to verify that box-drawing characters are used correctly in interactive mode and fall back to ASCII characters in non-interactive mode. Consider adding tests similar to test_countdown_timer_visuals that verify the Box class characters appear in the table output when USE_COLORS is True and ASCII fallbacks appear when USE_COLORS is False.

Copilot uses AI. Check for mistakes.

class ColoredFormatter(logging.Formatter):
"""Custom formatter to add colors to log levels."""

Expand Down Expand Up @@ -332,7 +342,7 @@
for remaining in range(seconds, 0, -1):
progress = (seconds - remaining + 1) / seconds
filled = int(width * progress)
bar = "█" * filled + "" * (width - filled)
bar = "█" * filled + "·" * (width - filled)
sys.stderr.write(
f"\r{Colors.CYAN}⏳ {message}: [{bar}] {remaining}s...{Colors.ENDC}"
)
Expand All @@ -354,7 +364,7 @@

progress = min(1.0, current / total)
filled = int(width * progress)
bar = "█" * filled + "" * (width - filled)
bar = "█" * filled + "·" * (width - filled)
percent = int(progress * 100)

# Use \033[K to clear line residue
Expand Down Expand Up @@ -2051,25 +2061,56 @@
max_profile_len = max((len(r["profile"]) for r in sync_results), default=25)
profile_col_width = max(25, max_profile_len)

# Calculate total width for the table
# Profile ID + " | " + Folders + " | " + Rules + " | " + Duration + " | " + Status
# Widths: profile_col_width + 3 + 10 + 3 + 10 + 3 + 10 + 3 + 15 = profile_col_width + 57
table_width = profile_col_width + 57

title_text = "DRY RUN SUMMARY" if args.dry_run else "SYNC SUMMARY"
# Column widths
w_profile = profile_col_width
w_folders = 10
w_rules = 12
w_duration = 10
w_status = 15

def make_col_separator(left, mid, right, horiz):
parts = [
horiz * (w_profile + 2),
horiz * (w_folders + 2),
horiz * (w_rules + 2),
horiz * (w_duration + 2),
horiz * (w_status + 2),
]
return left + mid.join(parts) + right

# Calculate table width using a dummy separator
dummy_sep = make_col_separator(Box.TL, Box.T, Box.TR, Box.H)
table_width = len(dummy_sep)

title_text = " DRY RUN SUMMARY " if args.dry_run else " SYNC SUMMARY "
title_color = Colors.CYAN if args.dry_run else Colors.HEADER

print("\n" + "=" * table_width)
print(f"{title_color}{title_text:^{table_width}}{Colors.ENDC}")
print("=" * table_width)
# Top Border (Single Cell for Title)
print("\n" + Box.TL + Box.H * (table_width - 2) + Box.TR)

# Header
# Title Row
visible_title = title_text.strip()
inner_width = table_width - 2
pad_left = (inner_width - len(visible_title)) // 2
pad_right = inner_width - len(visible_title) - pad_left
print(
f"{Colors.BOLD}"
f"{'Profile ID':<{profile_col_width}} | {'Folders':>10} | {'Rules':>10} | {'Duration':>10} | {'Status':<15}"
f"{Colors.ENDC}"
f"{Box.V}{' ' * pad_left}{title_color}{visible_title}{Colors.ENDC}{' ' * pad_right}{Box.V}"
)
print("-" * table_width)

# Separator between Title and Headers (introduces columns)
print(make_col_separator(Box.L, Box.T, Box.R, Box.H))

# Header Row
print(
f"{Box.V} {Colors.BOLD}{'Profile ID':<{w_profile}}{Colors.ENDC} "
f"{Box.V} {Colors.BOLD}{'Folders':>{w_folders}}{Colors.ENDC} "
f"{Box.V} {Colors.BOLD}{'Rules':>{w_rules}}{Colors.ENDC} "
f"{Box.V} {Colors.BOLD}{'Duration':>{w_duration}}{Colors.ENDC} "
f"{Box.V} {Colors.BOLD}{'Status':<{w_status}}{Colors.ENDC} {Box.V}"
)

# Separator between Header and Body
print(make_col_separator(Box.L, Box.X, Box.R, Box.H))

# Rows
total_folders = 0
Expand All @@ -2080,18 +2121,23 @@
# Use boolean success field for color logic
status_color = Colors.GREEN if res["success"] else Colors.FAIL

s_folders = f"{res['folders']:,}"
s_rules = f"{res['rules']:,}"
s_duration = f"{res['duration']:.1f}s"

print(
f"{res['profile']:<{profile_col_width}} | "
f"{res['folders']:>10} | "
f"{res['rules']:>10,} | "
f"{res['duration']:>9.1f}s | "
f"{status_color}{res['status_label']:<15}{Colors.ENDC}"
f"{Box.V} {res['profile']:<{w_profile}} "
f"{Box.V} {s_folders:>{w_folders}} "
f"{Box.V} {s_rules:>{w_rules}} "
f"{Box.V} {s_duration:>{w_duration}} "
f"{Box.V} {status_color}{res['status_label']:<{w_status}}{Colors.ENDC} {Box.V}"
)
total_folders += res["folders"]
total_rules += res["rules"]
total_duration += res["duration"]

print("-" * table_width)
# Separator between Body and Total
print(make_col_separator(Box.L, Box.X, Box.R, Box.H))

# Total Row
total = len(profile_ids or ["dry-run-placeholder"])
Expand All @@ -2110,15 +2156,30 @@

total_status_color = Colors.GREEN if all_success else Colors.FAIL

s_total_folders = f"{total_folders:,}"
s_total_rules = f"{total_rules:,}"
s_total_duration = f"{total_duration:.1f}s"

print(
f"{Colors.BOLD}"
f"{'TOTAL':<{profile_col_width}} | "
f"{total_folders:>10} | "
f"{total_rules:>10,} | "
f"{total_duration:>9.1f}s | "
f"{total_status_color}{total_status_text:<15}{Colors.ENDC}"
f"{Box.V} {Colors.BOLD}{'TOTAL':<{w_profile}}{Colors.ENDC} "
f"{Box.V} {s_total_folders:>{w_folders}} "
f"{Box.V} {s_total_rules:>{w_rules}} "
f"{Box.V} {s_total_duration:>{w_duration}} "
f"{Box.V} {total_status_color}{total_status_text:<{w_status}}{Colors.ENDC} {Box.V}"
)
print("=" * table_width + "\n")
# Bottom Border
print(make_col_separator(Box.BL, Box.B, Box.BR, Box.H))

# Success Delight
if all_success and USE_COLORS and not args.dry_run:
success_msgs = [
"✨ All synced!",
"🚀 Ready for liftoff!",
"🎨 Beautifully done!",
"💎 Smooth operation!",
"🌈 Perfect harmony!",
]
print(f"\n{Colors.GREEN}{random.choice(success_msgs)}{Colors.ENDC}")

Check notice

Code scanning / Bandit

Standard pseudo-random generators are not suitable for security/cryptographic purposes. Note

Standard pseudo-random generators are not suitable for security/cryptographic purposes.

# Display cache statistics if any cache activity occurred
if _cache_stats["hits"] + _cache_stats["misses"] + _cache_stats["validations"] > 0:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_ux.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
combined_output = "".join(writes)

# Check for progress bar chars
assert "░" in combined_output
# We changed the empty character from '░' to '·' in the progress bar
assert "·" 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.
assert "█" in combined_output
assert "Test" in combined_output
assert "Done!" in combined_output
Expand Down
Loading