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 /egometrics.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 'egometrics.py')
| -rw-r--r-- | egometrics.py | 472 |
1 files changed, 402 insertions, 70 deletions
diff --git a/egometrics.py b/egometrics.py index eda2faa..d882a59 100644 --- a/egometrics.py +++ b/egometrics.py @@ -7,25 +7,33 @@ import ui def main_menu(conn: sqlite3.Connection) -> None: while True: - print("\n=== EgoMetrics ===\n") + ui.clear_screen() + print("=== EgoMetrics ===\n") print("1. Log Workout") - print("2. View Sessions") - print("3. Manage Exercises") - print("4. Quit") + print("2. View Workout Sessions") + print("3. Manage Workout Exercises") + print("4. Manage Workout Templates") + print("5. Quit") choice = input("\n> ").strip() if choice == "1": log_workout(conn) elif choice == "2": - view_sessions(conn) + view_workout_sessions(conn) elif choice == "3": - manage_exercises(conn) + manage_workout_exercises(conn) elif choice == "4": + manage_workout_templates(conn) + elif choice == "5": break -def manage_exercises(conn: sqlite3.Connection) -> None: +# --- Workout Exercises --- + + +def manage_workout_exercises(conn: sqlite3.Connection) -> None: while True: - ui.print_header("Manage Exercises") + ui.clear_screen() + ui.print_header("Manage Workout Exercises") print("1. List Exercises") print("2. Add Exercise") print("3. Edit Exercise") @@ -33,91 +41,173 @@ def manage_exercises(conn: sqlite3.Connection) -> None: print("5. Back") choice = input("\n> ").strip() if choice == "1": - list_exercises(conn) + list_workout_exercises(conn) elif choice == "2": - add_exercise(conn) + add_workout_exercise(conn) elif choice == "3": - edit_exercise(conn) + edit_workout_exercise(conn) elif choice == "4": - delete_exercise(conn) + delete_workout_exercise(conn) elif choice == "5": break -def list_exercises(conn: sqlite3.Connection) -> None: - exercises = models.list_exercises(conn) - ui.print_header("All Exercises") +def list_workout_exercises(conn: sqlite3.Connection, pause: bool = True) -> None: + exercises = models.list_workout_exercises(conn) + ui.print_header("All Workout Exercises") ui.print_table( - ["ID", "Name", "Note"], - [[str(e["id"]), e["name"], e["note"] or ""] for e in exercises], + ["ID", "Name", "BW?", "Note"], + [ + [ + str(e["id"]), + e["name"], + "Yes" if e["bw_relative"] else "No", + e["note"] or "", + ] + for e in exercises + ], ) + if pause: + ui.pause() -def add_exercise(conn: sqlite3.Connection) -> None: +def add_workout_exercise(conn: sqlite3.Connection) -> None: name = ui.prompt_str("Name: ") assert name is not None + bw_relative = ui.confirm("Is weight relative to body weight?") note = ui.prompt_str("Note (optional): ", required=False) try: - models.add_exercise(conn, name, note) + models.add_workout_exercise(conn, name, bw_relative, note) print(f'Exercise "{name}" added.') except sqlite3.IntegrityError: print(f'Exercise "{name}" already exists.') + ui.pause() -def edit_exercise(conn: sqlite3.Connection) -> None: - list_exercises(conn) +def edit_workout_exercise(conn: sqlite3.Connection) -> None: + list_workout_exercises(conn, pause=False) eid = ui.prompt_int("\nExercise ID to edit: ") assert eid is not None - ex = models.get_exercise(conn, eid) + ex = models.get_workout_exercise(conn, eid) if not ex: print("Exercise not found.") return print(f'Editing "{ex["name"]}" (leave blank to keep current value)') name = ui.prompt_str(f"Name [{ex['name']}]: ", required=False) + bw_cur = "Yes" if ex["bw_relative"] else "No" + print(f"BW-relative [{bw_cur}]: ", end="") + bw_input = input().strip().lower() + if bw_input in ("y", "yes"): + bw_relative: bool | None = True + elif bw_input in ("n", "no"): + bw_relative = False + else: + bw_relative = None # keep current note = ui.prompt_str(f"Note [{ex['note'] or ''}]: ", required=False) try: - models.update_exercise(conn, eid, name, note) + models.update_workout_exercise(conn, eid, name, bw_relative, note) print("Exercise updated.") except sqlite3.IntegrityError: print(f'An exercise named "{name}" already exists.') + ui.pause() -def delete_exercise(conn: sqlite3.Connection) -> None: - list_exercises(conn) +def delete_workout_exercise(conn: sqlite3.Connection) -> None: + list_workout_exercises(conn, pause=False) eid = ui.prompt_int("\nExercise ID to delete: ") assert eid is not None - ex = models.get_exercise(conn, eid) + ex = models.get_workout_exercise(conn, eid) if not ex: print("Exercise not found.") return if not ui.confirm(f'Delete "{ex["name"]}"?'): return try: - models.delete_exercise(conn, eid) + models.delete_workout_exercise(conn, eid) print("Exercise deleted.") except sqlite3.IntegrityError: - print(f'Cannot delete "{ex["name"]}" — it is used in existing sessions.') + print( + f'Cannot delete "{ex["name"]}" — it is used in existing sessions or templates.' + ) + ui.pause() + + +# --- Workout Sessions --- def log_workout(conn: sqlite3.Connection) -> None: - exercises = models.list_exercises(conn) + exercises = models.list_workout_exercises(conn) if not exercises: - print("No exercises defined. Add some first via Manage Exercises.") + print("No exercises defined. Add some first via Manage Workout Exercises.") return # Session metadata + ui.clear_screen() ui.print_header("Log Workout") date_time = ui.prompt_datetime("Date/Time") session_note = ui.prompt_str("Session note (optional): ", required=False) + # Check if user wants to start from a template + entries: list[dict] = [] + templates = models.list_workout_templates(conn) + if templates: + print("\nStart from a template?") + for i, t in enumerate(templates, 1): + print(f" {i}. {t['name']}") + print(" 0. No template (blank session)") + choice = input("\n> ").strip() + try: + idx = int(choice) + if 1 <= idx <= len(templates): + # Load template exercises as defaults + template_id = templates[idx - 1]["id"] + _, tmpl_entries = models.get_workout_template_detail(conn, template_id) + print(f'\nUsing template "{templates[idx - 1]["name"]}"') + print("Press Enter to accept defaults, or type new values.\n") + for te in tmpl_entries: + print(f" -- {te['exercise_name']} --") + sets = ui.prompt_int( + f" Sets [{te['sets'] or ''}]: ", + min_val=1, + default=te["sets"], + ) + assert sets is not None + reps, weight, rest_time = ui.prompt_sets_detail( + sets, + default_reps=te["reps"], + default_rest=te["rest_time"], + bw_relative=bool(te["bw_relative"]), + ) + lsrpe = ui.prompt_int(" Last Set RPE: ", min_val=1, max_val=10) + note = ui.prompt_str( + f" Note [{te['note'] or ''}]: ", + required=False, + default=te["note"], + ) + entries.append( + { + "exercise_id": te["exercise_id"], + "exercise_name": te["exercise_name"], + "bw_relative": bool(te["bw_relative"]), + "sets": sets, + "reps": reps, + "weight": weight, + "rest_time": rest_time, + "lsrpe": lsrpe, + "note": note, + } + ) + print(f' "{te["exercise_name"]}" added to session.') + except (ValueError, IndexError): + pass # Invalid input or 0 — proceed without template + # Build exercise list — collect everything in memory before saving - entries = [] while True: print("\nAvailable exercises:") for e in exercises: print(f" {e['id']}. {e['name']}") print() - choice = input("Select exercise ID ('n' = new exercise, 'd' = done): ").strip() + choice = input("Select exercise ID ('d' = done): ").strip() if choice.lower() == "d": if not entries: @@ -125,49 +215,34 @@ def log_workout(conn: sqlite3.Connection) -> None: return break - # Inline exercise creation - elif choice.lower() == "n": - name = ui.prompt_str("Exercise name: ") - assert name is not None - note = ui.prompt_str("Note (optional): ", required=False) - try: - models.add_exercise(conn, name, note) - exercises = models.list_exercises(conn) - print(f'Exercise "{name}" added.') - except sqlite3.IntegrityError: - print(f'Exercise "{name}" already exists.') - continue - # Select existing exercise and prompt for set details try: eid = int(choice) except ValueError: print("Invalid input.") continue - ex = models.get_exercise(conn, eid) + ex = models.get_workout_exercise(conn, eid) if not ex: print("Exercise not found.") continue print(f"\n -- {ex['name']} --") sets = ui.prompt_int(" Sets: ", min_val=1) - reps = ui.prompt_int(" Reps: ", min_val=1) - rpe = ui.prompt_int(" RPE (optional): ", required=False, min_val=1, max_val=10) - rest_time = ui.prompt_int( - " Rest time in seconds (optional): ", required=False, min_val=0 - ) - lsrpe = ui.prompt_int( - " Last Set RPE (optional): ", required=False, min_val=1, max_val=10 + assert sets is not None + reps, weight, rest_time = ui.prompt_sets_detail( + sets, bw_relative=bool(ex["bw_relative"]) ) + lsrpe = ui.prompt_int(" Last Set RPE: ", min_val=1, max_val=10) note = ui.prompt_str(" Note (optional): ", required=False) entries.append( { "exercise_id": eid, "exercise_name": ex["name"], + "bw_relative": bool(ex["bw_relative"]), "sets": sets, "reps": reps, - "rpe": rpe, + "weight": weight, "rest_time": rest_time, "lsrpe": lsrpe, "note": note, @@ -176,21 +251,22 @@ def log_workout(conn: sqlite3.Connection) -> None: print(f' "{ex["name"]}" added to session.') # Show summary and confirm before writing to DB + ui.clear_screen() ui.print_header("Session Summary") print(f"Date: {date_time}") if session_note: print(f"Note: {session_note}") ui.print_table( - ["#", "Exercise", "Sets", "Reps", "RPE", "Rest", "LSRPE", "Note"], + ["#", "Exercise", "Sets", "Reps", "Weight", "Rest", "LSRPE", "Note"], [ [ str(i), e["exercise_name"], str(e["sets"]), - str(e["reps"]), - str(e["rpe"] or ""), - f"{e['rest_time']}s" if e["rest_time"] else "", - str(e["lsrpe"] or ""), + str(e["reps"]).replace(",", "/"), + ui.format_weight(e["weight"], e["bw_relative"]), + ui.format_rest_time(e["rest_time"]), + str(e["lsrpe"]), e["note"] or "", ] for i, e in enumerate(entries, 1) @@ -198,20 +274,22 @@ def log_workout(conn: sqlite3.Connection) -> None: ) if ui.confirm("\nSave this session?"): - models.save_session(conn, date_time, session_note, entries) + models.save_workout_session(conn, date_time, session_note, entries) print("Session saved!") else: print("Session discarded.") + ui.pause() -def view_sessions(conn: sqlite3.Connection) -> None: +def view_workout_sessions(conn: sqlite3.Connection) -> None: while True: + ui.clear_screen() # List all sessions with exercise name preview - sessions = models.list_sessions(conn) + sessions = models.list_workout_sessions(conn) if not sessions: print("\nNo sessions recorded yet.") return - ui.print_header("Past Sessions") + ui.print_header("Past Workout Sessions") ui.print_table( ["#", "Date", "Exercises", "Note"], [ @@ -234,23 +312,24 @@ def view_sessions(conn: sqlite3.Connection) -> None: continue # Show full session detail with all exercise entries + ui.clear_screen() session_id = sessions[idx]["id"] - session, entries = models.get_session_detail(conn, session_id) + session, entries = models.get_workout_session_detail(conn, session_id) assert session is not None ui.print_header(f"Session: {session['date_time']}") if session["note"]: print(f"Note: {session['note']}\n") ui.print_table( - ["#", "Exercise", "Sets", "Reps", "RPE", "Rest", "LSRPE", "Note"], + ["#", "Exercise", "Sets", "Reps", "Weight", "Rest", "LSRPE", "Note"], [ [ str(e["position"]), e["exercise_name"], str(e["sets"]), - str(e["reps"]), - str(e["rpe"] or ""), - f"{e['rest_time']}s" if e["rest_time"] else "", - str(e["lsrpe"] or ""), + str(e["reps"]).replace(",", "/"), + ui.format_weight(str(e["weight"]), bool(e["bw_relative"])), + ui.format_rest_time(e["rest_time"]), + str(e["lsrpe"]), e["note"] or "", ] for e in entries @@ -259,8 +338,261 @@ def view_sessions(conn: sqlite3.Connection) -> None: action = input("\nActions: (b)ack, (d)elete session\n> ").strip().lower() if action == "d": if ui.confirm("Delete this session?"): - models.delete_session(conn, session_id) + models.delete_workout_session(conn, session_id) print("Session deleted.") + ui.pause() + + +# --- Workout Templates --- + + +def manage_workout_templates(conn: sqlite3.Connection) -> None: + while True: + ui.clear_screen() + ui.print_header("Manage Workout Templates") + print("1. List Templates") + print("2. Create Template") + print("3. View/Edit Template") + print("4. Delete Template") + print("5. Back") + choice = input("\n> ").strip() + if choice == "1": + list_workout_templates(conn) + elif choice == "2": + create_workout_template(conn) + elif choice == "3": + edit_workout_template(conn) + elif choice == "4": + delete_workout_template(conn) + elif choice == "5": + break + + +def list_workout_templates(conn: sqlite3.Connection, pause: bool = True) -> None: + templates = models.list_workout_templates(conn) + ui.print_header("All Workout Templates") + ui.print_table( + ["ID", "Name", "Exercises"], + [[str(t["id"]), t["name"], t["exercises"] or ""] for t in templates], + ) + if pause: + ui.pause() + + +def create_workout_template(conn: sqlite3.Connection) -> None: + name = ui.prompt_str("Template name: ") + assert name is not None + try: + template_id = models.create_workout_template(conn, name) + print(f'Template "{name}" created.') + except sqlite3.IntegrityError: + print(f'Template "{name}" already exists.') + ui.pause() + return + # Immediately enter exercise editor + print("Now add exercises to the template.") + ui.pause() + edit_template_exercises(conn, template_id) + + +def edit_workout_template(conn: sqlite3.Connection) -> None: + templates = models.list_workout_templates(conn) + if not templates: + print("\nNo templates defined.") + return + list_workout_templates(conn, pause=False) + tid = ui.prompt_int("\nTemplate ID to edit: ") + assert tid is not None + template = models.get_workout_template(conn, tid) + if not template: + print("Template not found.") + return + edit_template_exercises(conn, tid) + + +def edit_template_exercises(conn: sqlite3.Connection, template_id: int) -> None: + """Interactive editor for a template's exercise list.""" + while True: + ui.clear_screen() + template, entries = models.get_workout_template_detail(conn, template_id) + assert template is not None + ui.print_header(f'Template: {template["name"]}') + + # Build in-memory list from current DB state + exercises_data: list[dict] = [ + { + "exercise_id": e["exercise_id"], + "exercise_name": e["exercise_name"], + "bw_relative": bool(e["bw_relative"]), + "sets": e["sets"], + "reps": e["reps"], + "lsrpe": e["lsrpe"], + "rest_time": e["rest_time"], + "note": e["note"], + } + for e in entries + ] + + if exercises_data: + ui.print_table( + ["#", "Exercise", "Sets", "Reps", "LSRPE", "Rest", "Note"], + [ + [ + str(i), + e["exercise_name"], + str(e["sets"]), + str(e["reps"]), + str(e["lsrpe"]), + f"{e['rest_time']}s", + e["note"] or "", + ] + for i, e in enumerate(exercises_data, 1) + ], + ) + else: + print(" (no exercises)") + + print( + "\nActions: (a)dd exercise, (r)emove, (m)ove, (e)dit defaults, (n)ame, (b)ack" + ) + action = input("> ").strip().lower() + + if action == "b": + break + + elif action == "n": + new_name = ui.prompt_str("New template name: ") + assert new_name is not None + try: + models.update_workout_template_name(conn, template_id, new_name) + print(f'Template renamed to "{new_name}".') + except sqlite3.IntegrityError: + print(f'Template "{new_name}" already exists.') + ui.pause() + + elif action == "a": + available = models.list_workout_exercises(conn) + if not available: + print("No exercises defined. Create some first.") + continue + print("\nAvailable exercises:") + for e in available: + print(f" {e['id']}. {e['name']}") + eid = ui.prompt_int("\nExercise ID to add: ") + assert eid is not None + ex = models.get_workout_exercise(conn, eid) + if not ex: + print("Exercise not found.") + continue + print(f"\n -- {ex['name']} (defaults) --") + sets = ui.prompt_int(" Sets: ", min_val=1) + reps = ui.prompt_int(" Reps: ", min_val=1) + lsrpe = ui.prompt_int(" LSRPE: ", min_val=1, max_val=10) + rest_time = ui.prompt_int(" Rest time in seconds: ", min_val=0) + note = ui.prompt_str(" Note (optional): ", required=False) + exercises_data.append( + { + "exercise_id": eid, + "sets": sets, + "reps": reps, + "lsrpe": lsrpe, + "rest_time": rest_time, + "note": note, + } + ) + models.save_workout_template_exercises(conn, template_id, exercises_data) + print(f'"{ex["name"]}" added to template.') + ui.pause() + + elif action == "r": + if not exercises_data: + print("No exercises to remove.") + continue + pos = ui.prompt_int("Position # to remove: ", min_val=1) + assert pos is not None + if pos > len(exercises_data): + print("Invalid position.") + continue + removed = exercises_data.pop(pos - 1) + models.save_workout_template_exercises(conn, template_id, exercises_data) + print(f'Removed "{removed["exercise_name"]}".') + ui.pause() + + elif action == "m": + if len(exercises_data) < 2: + print("Need at least 2 exercises to move.") + continue + from_pos = ui.prompt_int( + "Move from position #: ", min_val=1, max_val=len(exercises_data) + ) + to_pos = ui.prompt_int( + "Move to position #: ", min_val=1, max_val=len(exercises_data) + ) + assert from_pos is not None and to_pos is not None + item = exercises_data.pop(from_pos - 1) + exercises_data.insert(to_pos - 1, item) + models.save_workout_template_exercises(conn, template_id, exercises_data) + print("Exercise moved.") + ui.pause() + + elif action == "e": + if not exercises_data: + print("No exercises to edit.") + continue + pos = ui.prompt_int("Position # to edit: ", min_val=1) + assert pos is not None + if pos > len(exercises_data): + print("Invalid position.") + continue + entry = exercises_data[pos - 1] + print(f'\n -- {entry["exercise_name"]} (edit defaults) --') + entry["sets"] = ui.prompt_int( + f" Sets [{entry['sets'] or ''}]: ", min_val=1, default=entry["sets"] + ) + entry["reps"] = ui.prompt_int( + f" Reps [{entry['reps'] or ''}]: ", min_val=1, default=entry["reps"] + ) + entry["lsrpe"] = ui.prompt_int( + f" LSRPE [{entry['lsrpe']}]: ", + min_val=1, + max_val=10, + default=entry["lsrpe"], + ) + entry["rest_time"] = ui.prompt_int( + f" Rest time in seconds [{entry['rest_time']}]: ", + min_val=0, + default=entry["rest_time"], + ) + entry["note"] = ui.prompt_str( + f" Note [{entry['note'] or ''}]: ", + required=False, + default=entry["note"], + ) + models.save_workout_template_exercises(conn, template_id, exercises_data) + print("Defaults updated.") + ui.pause() + + +def delete_workout_template(conn: sqlite3.Connection) -> None: + templates = models.list_workout_templates(conn) + if not templates: + print("\nNo templates defined.") + return + list_workout_templates(conn, pause=False) + tid = ui.prompt_int("\nTemplate ID to delete: ") + assert tid is not None + template = models.get_workout_template(conn, tid) + if not template: + print("Template not found.") + return + if not ui.confirm(f'Delete template "{template["name"]}"?'): + return + models.delete_workout_template(conn, tid) + print("Template deleted.") + ui.pause() + + +# --- Entry Point --- def main() -> None: |
