aboutsummaryrefslogtreecommitdiffstats
path: root/egometrics.py
diff options
context:
space:
mode:
Diffstat (limited to 'egometrics.py')
-rw-r--r--egometrics.py472
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: