import sqlite3 from db import get_connection, init_db import models import ui def main_menu(conn: sqlite3.Connection) -> None: while True: ui.clear_screen() print("=== EgoMetrics ===\n") print("1. Log Workout") 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_workout_sessions(conn) elif choice == "3": manage_workout_exercises(conn) elif choice == "4": manage_workout_templates(conn) elif choice == "5": break # --- Workout Exercises --- def manage_workout_exercises(conn: sqlite3.Connection) -> None: while True: ui.clear_screen() ui.print_header("Manage Workout Exercises") print("1. List Exercises") print("2. Add Exercise") print("3. Edit Exercise") print("4. Delete Exercise") print("5. Back") choice = input("\n> ").strip() if choice == "1": list_workout_exercises(conn) elif choice == "2": add_workout_exercise(conn) elif choice == "3": edit_workout_exercise(conn) elif choice == "4": delete_workout_exercise(conn) elif choice == "5": break 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", "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_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_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_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_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_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_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_workout_exercise(conn, eid) if not ex: print("Exercise not found.") return if not ui.confirm(f'Delete "{ex["name"]}"?'): return try: models.delete_workout_exercise(conn, eid) print("Exercise deleted.") except sqlite3.IntegrityError: 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_workout_exercises(conn) if not 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 while True: print("\nAvailable exercises:") for e in exercises: print(f" {e['id']}. {e['name']}") print() choice = input("Select exercise ID ('d' = done): ").strip() if choice.lower() == "d": if not entries: print("No exercises added. Aborting.") return break # Select existing exercise and prompt for set details try: eid = int(choice) except ValueError: print("Invalid input.") continue 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) 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, "weight": weight, "rest_time": rest_time, "lsrpe": lsrpe, "note": note, } ) 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", "Weight", "Rest", "LSRPE", "Note"], [ [ str(i), e["exercise_name"], str(e["sets"]), 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) ], ) if ui.confirm("\nSave this session?"): models.save_workout_session(conn, date_time, session_note, entries) print("Session saved!") else: print("Session discarded.") ui.pause() def view_workout_sessions(conn: sqlite3.Connection) -> None: while True: ui.clear_screen() # List all sessions with exercise name preview sessions = models.list_workout_sessions(conn) if not sessions: print("\nNo sessions recorded yet.") return ui.print_header("Past Workout Sessions") ui.print_table( ["#", "Date", "Exercises", "Note"], [ [str(i), s["date_time"], s["exercises"] or "", s["note"] or ""] for i, s in enumerate(sessions, 1) ], ) # Select a session to view details choice = input("\nSelect # for details ('b' = back): ").strip() if choice.lower() == "b": break try: idx = int(choice) - 1 if idx < 0 or idx >= len(sessions): print("Invalid selection.") continue except ValueError: print("Invalid input.") continue # Show full session detail with all exercise entries ui.clear_screen() session_id = sessions[idx]["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", "Weight", "Rest", "LSRPE", "Note"], [ [ str(e["position"]), e["exercise_name"], str(e["sets"]), 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 ], ) action = input("\nActions: (b)ack, (d)elete session\n> ").strip().lower() if action == "d": if ui.confirm("Delete this session?"): 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: init_db() conn = get_connection() try: main_menu(conn) except KeyboardInterrupt: print("\nGoodbye!") finally: conn.close() if __name__ == "__main__": main()