diff --git a/main.py b/main.py index f7158a0..e1e516c 100644 --- a/main.py +++ b/main.py @@ -313,6 +313,156 @@ def print_plan_details(plan_entry: Dict[str, Any]) -> None: print("") +def print_summary_table(results: List[Dict[str, Any]], dry_run: bool) -> None: + """Prints a nicely formatted summary table.""" + # Determine the width for the Profile ID column (min 25) + max_profile_len = max((len(r["profile"]) for r in results), default=25) + profile_col_width = max(25, max_profile_len) + + # Calculate widths + col_widths = { + "profile": profile_col_width, + "folders": 10, + "rules": 10, + "duration": 10, + "status": 15, + } + + if USE_COLORS: + # Unicode Box Drawing + chars = { + "tl": "┌", "tm": "┬", "tr": "┐", + "bl": "└", "bm": "┴", "br": "┘", + "ml": "├", "mm": "┼", "mr": "┤", + "v": "│", "h": "─", + } + else: + # ASCII Fallback + chars = { + "tl": "+", "tm": "+", "tr": "+", + "bl": "+", "bm": "+", "br": "+", + "ml": "+", "mm": "+", "mr": "+", + "v": "|", "h": "-", + } + + def _print_separator(left, mid, right): + segments = [chars["h"] * (width + 2) for width in col_widths.values()] + print(f"{chars[left]}{chars[mid].join(segments)}{chars[right]}") + + def _print_row(profile, folders, rules, duration, status, is_header=False): + v = chars["v"] + + # 1. Pad raw strings first (so padding is calculated on visible chars) + p_val = f"{profile:<{col_widths['profile']}}" + f_val = f"{folders:>{col_widths['folders']}}" + r_val = f"{rules:>{col_widths['rules']}}" + d_val = f"{duration:>{col_widths['duration']}}" + s_val = f"{status:<{col_widths['status']}}" + + # 2. Wrap in color codes if needed + if is_header and USE_COLORS: + p_val = f"{Colors.BOLD}{p_val}{Colors.ENDC}" + f_val = f"{Colors.BOLD}{f_val}{Colors.ENDC}" + r_val = f"{Colors.BOLD}{r_val}{Colors.ENDC}" + d_val = f"{Colors.BOLD}{d_val}{Colors.ENDC}" + s_val = f"{Colors.BOLD}{s_val}{Colors.ENDC}" + + print( + f"{v} {p_val} {v} {f_val} {v} {r_val} {v} {d_val} {v} {s_val} {v}" + ) + + title_text = "DRY RUN SUMMARY" if dry_run else "SYNC SUMMARY" + title_color = Colors.CYAN if dry_run else Colors.HEADER + + total_width = ( + 1 + (col_widths["profile"] + 2) + 1 + + (col_widths["folders"] + 2) + 1 + + (col_widths["rules"] + 2) + 1 + + (col_widths["duration"] + 2) + 1 + + (col_widths["status"] + 2) + 1 + ) + + print("\n" + (f"{title_color}{title_text:^{total_width}}{Colors.ENDC}" if USE_COLORS else f"{title_text:^{total_width}}")) + + _print_separator("tl", "tm", "tr") + # Header row - pad manually then print + _print_row("Profile ID", "Folders", "Rules", "Duration", "Status", is_header=True) + _print_separator("ml", "mm", "mr") + + total_folders = 0 + total_rules = 0 + total_duration = 0.0 + success_count = 0 + + for res in results: + # Profile + p_val = f"{res['profile']:<{col_widths['profile']}}" + + # Folders + f_val = f"{res['folders']:>{col_widths['folders']}}" + + # Rules + r_val = f"{res['rules']:>{col_widths['rules']},}" + + # Duration + d_val = f"{res['duration']:>{col_widths['duration']-1}.1f}s" + + # Status + status_label = res["status_label"] + s_val_raw = f"{status_label:<{col_widths['status']}}" + if USE_COLORS: + status_color = Colors.GREEN if res["success"] else Colors.FAIL + s_val = f"{status_color}{s_val_raw}{Colors.ENDC}" + else: + s_val = s_val_raw + + # Delegate the actual row printing to the shared helper to avoid + # duplicating table border/spacing logic here. + _print_row(p_val, f_val, r_val, d_val, s_val) + + total_folders += res["folders"] + total_rules += res["rules"] + total_duration += res["duration"] + if res["success"]: + success_count += 1 + + _print_separator("ml", "mm", "mr") + + # Total Row + total = len(results) + all_success = success_count == total + + if dry_run: + total_status_text = "✅ Ready" if all_success else "❌ Errors" + else: + total_status_text = "✅ All Good" if all_success else "❌ Errors" + + p_val = f"{'TOTAL':<{col_widths['profile']}}" + if USE_COLORS: + p_val = f"{Colors.BOLD}{p_val}{Colors.ENDC}" + + f_val = f"{total_folders:>{col_widths['folders']}}" + r_val = f"{total_rules:>{col_widths['rules']},}" + d_val = f"{total_duration:>{col_widths['duration']-1}.1f}s" + + s_val_raw = f"{total_status_text:<{col_widths['status']}}" + if USE_COLORS: + status_color = Colors.GREEN if all_success else Colors.FAIL + s_val = f"{status_color}{s_val_raw}{Colors.ENDC}" + else: + s_val = s_val_raw + + print( + f"{chars['v']} {p_val} " + f"{chars['v']} {f_val} " + f"{chars['v']} {r_val} " + f"{chars['v']} {d_val} " + f"{chars['v']} {s_val} {chars['v']}" + ) + + _print_separator("bl", "bm", "br") + + def _get_progress_bar_width() -> int: """Calculate dynamic progress bar width based on terminal size.