From 7ceb22f1e12e3a040874a43b5e1177db83be15ed Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Thu, 12 Mar 2026 18:00:45 +0100 Subject: Add templates, per-set tracking, and float weight support - Rename tables with workout_ prefix, add workout_templates and workout_template_exercises tables - Add bw_relative flag to exercises for body-weight-relative display - Store reps, weight, and rest_time as per-set comma-separated TEXT in session exercises (rest_time is optional/nullable) - Support float weight with one decimal place - Add template CRUD and template-to-session logging flow --- ui.py | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) (limited to 'ui.py') diff --git a/ui.py b/ui.py index 2d63c06..ec4c1ee 100644 --- a/ui.py +++ b/ui.py @@ -1,6 +1,11 @@ +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") @@ -23,10 +28,13 @@ def prompt_int( 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.") @@ -45,10 +53,44 @@ def prompt_int( return n -def prompt_str(prompt: str, required: bool = True) -> str | None: +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.") @@ -56,6 +98,98 @@ def prompt_str(prompt: str, required: bool = True) -> str | None: 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") @@ -72,6 +206,10 @@ def prompt_datetime(prompt: str, default_now: bool = True) -> str: 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" -- cgit v1.2.3