diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-12 18:00:45 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-18 14:31:27 +0100 |
| commit | 7ceb22f1e12e3a040874a43b5e1177db83be15ed (patch) | |
| tree | b7418bbe91223bd35f03548249547011e0d99bdf /ui.py | |
| parent | 6e7e00846e658cb79d0c23e18939c59fedba06dd (diff) | |
| download | EgoMetrics-7ceb22f1e12e3a040874a43b5e1177db83be15ed.tar.gz EgoMetrics-7ceb22f1e12e3a040874a43b5e1177db83be15ed.zip | |
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
Diffstat (limited to 'ui.py')
| -rw-r--r-- | ui.py | 140 |
1 files changed, 139 insertions, 1 deletions
@@ -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" |
