aboutsummaryrefslogtreecommitdiffstats
path: root/screens
diff options
context:
space:
mode:
Diffstat (limited to 'screens')
-rw-r--r--screens/__init__.py3
-rw-r--r--screens/exercises.py106
-rw-r--r--screens/sessions.py212
-rw-r--r--screens/templates.py250
4 files changed, 571 insertions, 0 deletions
diff --git a/screens/__init__.py b/screens/__init__.py
new file mode 100644
index 0000000..5bd64e6
--- /dev/null
+++ b/screens/__init__.py
@@ -0,0 +1,3 @@
+from screens.exercises import manage_workout_exercises
+from screens.sessions import log_workout, view_workout_sessions
+from screens.templates import manage_workout_templates
diff --git a/screens/exercises.py b/screens/exercises.py
new file mode 100644
index 0000000..983f50f
--- /dev/null
+++ b/screens/exercises.py
@@ -0,0 +1,106 @@
+import sqlite3
+
+import models
+import ui
+
+
+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()
diff --git a/screens/sessions.py b/screens/sessions.py
new file mode 100644
index 0000000..58b2f71
--- /dev/null
+++ b/screens/sessions.py
@@ -0,0 +1,212 @@
+import sqlite3
+
+import models
+import ui
+
+
+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()
diff --git a/screens/templates.py b/screens/templates.py
new file mode 100644
index 0000000..7c8ff60
--- /dev/null
+++ b/screens/templates.py
@@ -0,0 +1,250 @@
+import sqlite3
+
+import models
+import ui
+
+
+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()