max / ripgrow
1 file changed,
+49 insertions,
-3 deletions
| @@ -22,7 +22,9 @@ use ratatui::style::{Modifier, Style}; | |||
| 22 | 22 | use ratatui::text::{Line, Span}; | |
| 23 | 23 | use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; | |
| 24 | 24 | use ripgrow_core::diagnostic::{DiagnosticSet, DiagnosticStep, next_step}; | |
| 25 | - | use ripgrow_core::{Db, Estimate, Exercise, Kind, Load, Reps, RepsKind, RepsPayload, RepsSet, Rpe}; | |
| 25 | + | use ripgrow_core::{ | |
| 26 | + | Db, EffortKind, Estimate, Exercise, Kind, Load, Reps, RepsKind, RepsPayload, RepsSet, Rpe, | |
| 27 | + | }; | |
| 26 | 28 | ||
| 27 | 29 | pub struct DiagnosticScreen { | |
| 28 | 30 | date: NaiveDate, | |
| @@ -75,11 +77,32 @@ enum RunField { | |||
| 75 | 77 | ||
| 76 | 78 | impl DiagnosticScreen { | |
| 77 | 79 | pub fn load(db: &Db) -> Result<Self, ripgrow_core::Error> { | |
| 78 | - | let exercises = db.list_exercises()?; | |
| 80 | + | // Diagnostic is reps-only for now: the DiagnosticStep protocol | |
| 81 | + | // and its "escalate on easy sets" heuristic are reps-shaped. | |
| 82 | + | // Timed / distance diagnostics need their own protocol design | |
| 83 | + | // (what does an "easy" plank hold or 5k run look like?) which | |
| 84 | + | // is deferred; for now those exercises are filtered out of the | |
| 85 | + | // picker and the status hints at what was skipped. | |
| 86 | + | let all = db.list_exercises()?; | |
| 87 | + | let skipped = all | |
| 88 | + | .iter() | |
| 89 | + | .filter(|e| e.effort_kind != EffortKind::Reps) | |
| 90 | + | .count(); | |
| 91 | + | let exercises: Vec<Exercise> = all | |
| 92 | + | .into_iter() | |
| 93 | + | .filter(|e| e.effort_kind == EffortKind::Reps) | |
| 94 | + | .collect(); | |
| 79 | 95 | let mut list_state = ListState::default(); | |
| 80 | 96 | if !exercises.is_empty() { | |
| 81 | 97 | list_state.select(Some(0)); | |
| 82 | 98 | } | |
| 99 | + | let status = if skipped > 0 { | |
| 100 | + | format!( | |
| 101 | + | "hiding {skipped} non-reps exercise(s) — diagnostic is reps-only for now" | |
| 102 | + | ) | |
| 103 | + | } else { | |
| 104 | + | String::new() | |
| 105 | + | }; | |
| 83 | 106 | Ok(Self { | |
| 84 | 107 | date: Local::now().date_naive(), | |
| 85 | 108 | exercises, | |
| @@ -88,7 +111,7 @@ impl DiagnosticScreen { | |||
| 88 | 111 | list_state, | |
| 89 | 112 | }, | |
| 90 | 113 | session: None, | |
| 91 | - | status: String::new(), | |
| 114 | + | status, | |
| 92 | 115 | }) | |
| 93 | 116 | } | |
| 94 | 117 | ||
| @@ -632,6 +655,29 @@ mod tests { | |||
| 632 | 655 | } | |
| 633 | 656 | ||
| 634 | 657 | #[test] | |
| 658 | + | fn load_filters_out_non_reps_exercises_and_reports_skip_count() { | |
| 659 | + | let db = Db::open_in_memory().unwrap(); | |
| 660 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 661 | + | db.create_exercise("squat", ResistanceType::Freeweight, LoadUnit::Kg, 2.5, &[]) | |
| 662 | + | .unwrap(); | |
| 663 | + | db.create_exercise("plank", ResistanceType::CardioTime, LoadUnit::Kg, 10.0, &[]) | |
| 664 | + | .unwrap(); | |
| 665 | + | db.create_exercise("5k run", ResistanceType::CardioDistance, LoadUnit::Kg, 0.0, &[]) | |
| 666 | + | .unwrap(); | |
| 667 | + | let screen = DiagnosticScreen::load(&db).unwrap(); | |
| 668 | + | let names: Vec<&str> = screen.exercises.iter().map(|e| e.name.as_str()).collect(); | |
| 669 | + | assert_eq!(names, vec!["squat"]); | |
| 670 | + | assert!(screen.status.contains("hiding 2"), "status: {}", screen.status); | |
| 671 | + | } | |
| 672 | + | ||
| 673 | + | #[test] | |
| 674 | + | fn load_status_is_empty_when_every_exercise_is_reps() { | |
| 675 | + | let (db, _) = setup(); | |
| 676 | + | let screen = DiagnosticScreen::load(&db).unwrap(); | |
| 677 | + | assert!(screen.status.is_empty(), "status: {:?}", screen.status); | |
| 678 | + | } | |
| 679 | + | ||
| 680 | + | #[test] | |
| 635 | 681 | fn picking_exercise_enters_seed_stage() { | |
| 636 | 682 | let (db, _) = setup(); | |
| 637 | 683 | let mut screen = DiagnosticScreen::load(&db).unwrap(); |