import os from datetime import datetime def clear_screen() -> None: os.system("cls" if os.name == "nt" else "clear") def print_header(title: str) -> None: print(f"\n--- {title} ---\n") def print_table(headers: list[str], rows: list[list[str]]) -> None: if not rows: print(" (empty)") return all_rows = [headers] + [[str(c) for c in row] for row in rows] widths = [max(len(r[i]) for r in all_rows) for i in range(len(headers))] fmt = " | ".join(f"{{:<{w}}}" for w in widths) print(fmt.format(*headers)) print("-+-".join("-" * w for w in widths)) for row in all_rows[1:]: print(fmt.format(*row)) def prompt_int( prompt: str, required: bool = True, min_val: int | None = None, max_val: int | None = None, default: int | None = None, ) -> int | None: while True: val = input(prompt).strip() if not val: if default is not None: return default if not required: return None print("This field is required.") continue try: n = int(val) except ValueError: print("Please enter a valid integer.") continue if min_val is not None and n < min_val: print(f"Must be at least {min_val}.") continue if max_val is not None and n > max_val: print(f"Must be at most {max_val}.") continue return n def prompt_float( prompt: str, required: bool = True, min_val: float | None = None, max_val: float | None = None, default: float | None = None, ) -> float | None: while True: val = input(prompt).strip() if not val: if default is not None: return default if not required: return None print("This field is required.") continue try: n = float(val) except ValueError: print("Please enter a valid number.") continue if min_val is not None and n < min_val: print(f"Must be at least {min_val}.") continue if max_val is not None and n > max_val: print(f"Must be at most {max_val}.") continue return round(n, 1) def prompt_str( prompt: str, required: bool = True, default: str | None = None ) -> str | None: while True: val = input(prompt).strip() if not val: if default is not None: return default if not required: return None print("This field is required.") continue return val def _fmt_kg(val: str) -> str: """Format a single weight value, dropping '.0' for whole numbers.""" n = float(val) return str(int(n)) if n == int(n) else f"{n:.1f}" def format_weight(weight_str: str, bw_relative: bool) -> str: """Format comma-separated weight values for display.""" values = weight_str.split(",") if bw_relative: parts = [] for v in values: n = float(v) label = _fmt_kg(v) if n >= 0: parts.append(f"BW+{label}") else: parts.append(f"BW{label}") return "/".join(parts) + "kg" return "/".join(_fmt_kg(v) for v in values) + "kg" def prompt_sets_detail( sets: int, default_reps: int | None = None, default_weight: float | None = None, default_rest: int | None = None, bw_relative: bool = False, ) -> tuple[str, str, str]: """Prompt for reps, weight (kg), and rest time on each set, return comma-separated strings.""" weight_label = "Weight relative to BW (kg)" if bw_relative else "Weight kg" weight_min: float | None = None if bw_relative else 0.0 reps_list = [] weight_list = [] rest_list = [] for i in range(1, sets + 1): print(f" Set {i}:") if default_reps is not None: r = prompt_int( f" Reps [{default_reps}]: ", min_val=1, default=default_reps ) else: r = prompt_int(" Reps: ", min_val=1) assert r is not None reps_list.append(str(r)) if default_weight is not None: w = prompt_float( f" {weight_label} [{_fmt_kg(str(default_weight))}]: ", min_val=weight_min, default=default_weight, ) else: w = prompt_float(f" {weight_label}: ", min_val=weight_min) assert w is not None weight_list.append(f"{w:.1f}") if default_rest is not None: rest_val = input(f" Rest seconds [{default_rest}] (- = skip): ").strip() if rest_val == "-": rest_list.append("") elif rest_val == "": rest_list.append(str(default_rest)) else: try: n = int(rest_val) if n < 0: print(" Must be at least 0, using default.") rest_list.append(str(default_rest)) else: rest_list.append(str(n)) except ValueError: print(" Invalid input, using default.") rest_list.append(str(default_rest)) else: rt = prompt_int(" Rest seconds (optional): ", required=False, min_val=0) rest_list.append(str(rt) if rt is not None else "") return ",".join(reps_list), ",".join(weight_list), ",".join(rest_list) def format_rest_time(rest_str: str | None) -> str: """Format comma-separated rest values for display, showing '-' for skipped sets.""" if not rest_str: return "-" parts = [] for v in rest_str.split(","): v = v.strip() if v: parts.append(f"{v}s") else: parts.append("-") return "/".join(parts) def prompt_datetime(prompt: str, default_now: bool = True) -> str: while True: default = datetime.now().strftime("%Y-%m-%d %H") val = input(f"{prompt} [{default}]: ").strip() if not val: if default_now: return default else: try: datetime.strptime(val, "%Y-%m-%d %H") return val except ValueError: print("Invalid format. Use YYYY-MM-DD HH.") continue def pause() -> None: input("\nPress Enter to continue...") def confirm(prompt: str) -> bool: val = input(f"{prompt} [y/N]: ").strip().lower() return val == "y"