| 4 |
4 |
|
//! subsequence match; Enter selects the highlighted one. **Grid** shows
|
| 5 |
5 |
|
//! the sets already logged for (date, exercise) plus a pending row being
|
| 6 |
6 |
|
//! typed. Enter on the last column commits the pending row and opens a
|
| 7 |
|
- |
//! fresh one, so the flow is:
|
|
7 |
+ |
//! fresh one.
|
| 8 |
8 |
|
//!
|
| 9 |
|
- |
//! 100 [tab] 5 [tab] 3 [enter] → row saved, cursor back at load.
|
|
9 |
+ |
//! Grid shape depends on the exercise's effort kind:
|
|
10 |
+ |
//!
|
|
11 |
+ |
//! - **Reps** `load [tab] reps [tab] rpe [enter]` → row saved.
|
|
12 |
+ |
//! - **Timed** `duration [tab] rpe [enter]`. Duration accepts `N`
|
|
13 |
+ |
//! (seconds) or `M:SS` (minutes:seconds).
|
|
14 |
+ |
//! - **Distance** `distance [tab] duration [tab] rpe [enter]`. Distance
|
|
15 |
+ |
//! is meters.
|
| 10 |
16 |
|
//!
|
| 11 |
17 |
|
//! Session date defaults to today; `[`/`]` step by a day.
|
| 12 |
18 |
|
|
| 13 |
|
- |
use chrono::{Duration, Local, NaiveDate};
|
|
19 |
+ |
use chrono::{Duration as ChronoDuration, Local, NaiveDate};
|
| 14 |
20 |
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
| 15 |
21 |
|
use ratatui::Frame;
|
| 16 |
22 |
|
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
| 17 |
23 |
|
use ratatui::style::{Modifier, Style};
|
| 18 |
24 |
|
use ratatui::text::{Line, Span};
|
| 19 |
25 |
|
use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
|
| 20 |
|
- |
use ripgrow_core::{Db, Exercise, Kind, RepsKind, RepsPayload, RepsSet};
|
|
26 |
+ |
use ripgrow_core::{
|
|
27 |
+ |
Db, Distance, DistanceKind, DistancePayload, DistanceSet, Duration, EffortKind, Exercise,
|
|
28 |
+ |
Kind, Load, Reps, RepsKind, RepsPayload, RepsSet, Rpe, TimedKind, TimedPayload, TimedSet,
|
|
29 |
+ |
};
|
| 21 |
30 |
|
|
| 22 |
31 |
|
pub struct LogScreen {
|
| 23 |
32 |
|
date: NaiveDate,
|
| 34 |
43 |
|
|
| 35 |
44 |
|
struct GridState {
|
| 36 |
45 |
|
exercise: Exercise,
|
|
46 |
+ |
kind: KindGrid,
|
|
47 |
+ |
}
|
|
48 |
+ |
|
|
49 |
+ |
enum KindGrid {
|
|
50 |
+ |
Reps(RepsGrid),
|
|
51 |
+ |
Timed(TimedGrid),
|
|
52 |
+ |
Distance(DistanceGrid),
|
|
53 |
+ |
}
|
|
54 |
+ |
|
|
55 |
+ |
struct RepsGrid {
|
| 37 |
56 |
|
committed: Vec<RepsSet>,
|
| 38 |
|
- |
pending: PendingRow,
|
| 39 |
|
- |
focus: GridField,
|
|
57 |
+ |
pending: RepsPending,
|
|
58 |
+ |
focus: RepsField,
|
| 40 |
59 |
|
}
|
| 41 |
60 |
|
|
| 42 |
61 |
|
#[derive(Default)]
|
| 43 |
|
- |
struct PendingRow {
|
|
62 |
+ |
struct RepsPending {
|
| 44 |
63 |
|
load: String,
|
| 45 |
64 |
|
reps: String,
|
| 46 |
65 |
|
rpe: String,
|
| 47 |
66 |
|
}
|
| 48 |
67 |
|
|
| 49 |
68 |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 50 |
|
- |
enum GridField {
|
|
69 |
+ |
enum RepsField {
|
| 51 |
70 |
|
Load,
|
| 52 |
71 |
|
Reps,
|
| 53 |
72 |
|
Rpe,
|
| 54 |
73 |
|
}
|
| 55 |
74 |
|
|
|
75 |
+ |
struct TimedGrid {
|
|
76 |
+ |
committed: Vec<TimedSet>,
|
|
77 |
+ |
pending: TimedPending,
|
|
78 |
+ |
focus: TimedField,
|
|
79 |
+ |
}
|
|
80 |
+ |
|
|
81 |
+ |
#[derive(Default)]
|
|
82 |
+ |
struct TimedPending {
|
|
83 |
+ |
duration: String,
|
|
84 |
+ |
rpe: String,
|
|
85 |
+ |
}
|
|
86 |
+ |
|
|
87 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
88 |
+ |
enum TimedField {
|
|
89 |
+ |
Duration,
|
|
90 |
+ |
Rpe,
|
|
91 |
+ |
}
|
|
92 |
+ |
|
|
93 |
+ |
struct DistanceGrid {
|
|
94 |
+ |
committed: Vec<DistanceSet>,
|
|
95 |
+ |
pending: DistancePending,
|
|
96 |
+ |
focus: DistanceField,
|
|
97 |
+ |
}
|
|
98 |
+ |
|
|
99 |
+ |
#[derive(Default)]
|
|
100 |
+ |
struct DistancePending {
|
|
101 |
+ |
distance: String,
|
|
102 |
+ |
duration: String,
|
|
103 |
+ |
rpe: String,
|
|
104 |
+ |
}
|
|
105 |
+ |
|
|
106 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
107 |
+ |
enum DistanceField {
|
|
108 |
+ |
Distance,
|
|
109 |
+ |
Duration,
|
|
110 |
+ |
Rpe,
|
|
111 |
+ |
}
|
|
112 |
+ |
|
| 56 |
113 |
|
impl LogScreen {
|
| 57 |
114 |
|
pub fn load(db: &Db) -> Result<Self, ripgrow_core::Error> {
|
| 58 |
115 |
|
let exercises = db.list_exercises()?;
|
| 113 |
170 |
|
KeyCode::Down => self.move_pick(1),
|
| 114 |
171 |
|
KeyCode::Up => self.move_pick(-1),
|
| 115 |
172 |
|
KeyCode::Char('[') if key.modifiers == KeyModifiers::NONE => {
|
| 116 |
|
- |
self.date -= Duration::days(1);
|
|
173 |
+ |
self.date -= ChronoDuration::days(1);
|
| 117 |
174 |
|
}
|
| 118 |
175 |
|
KeyCode::Char(']') if key.modifiers == KeyModifiers::NONE => {
|
| 119 |
|
- |
self.date += Duration::days(1);
|
|
176 |
+ |
self.date += ChronoDuration::days(1);
|
| 120 |
177 |
|
}
|
| 121 |
178 |
|
KeyCode::Backspace => {
|
| 122 |
179 |
|
self.picker.query.pop();
|
| 141 |
198 |
|
}
|
| 142 |
199 |
|
|
| 143 |
200 |
|
fn enter_grid(&mut self, exercise: Exercise) {
|
| 144 |
|
- |
// Committed sets are loaded fresh from the DB by the caller when
|
| 145 |
|
- |
// needed; construct the grid state with an empty vec here and let
|
| 146 |
|
- |
// refresh_grid() populate it.
|
| 147 |
|
- |
self.grid = Some(GridState {
|
| 148 |
|
- |
exercise,
|
| 149 |
|
- |
committed: Vec::new(),
|
| 150 |
|
- |
pending: PendingRow::default(),
|
| 151 |
|
- |
focus: GridField::Load,
|
| 152 |
|
- |
});
|
|
201 |
+ |
let kind = match exercise.effort_kind {
|
|
202 |
+ |
EffortKind::Reps => KindGrid::Reps(RepsGrid {
|
|
203 |
+ |
committed: Vec::new(),
|
|
204 |
+ |
pending: RepsPending::default(),
|
|
205 |
+ |
focus: RepsField::Load,
|
|
206 |
+ |
}),
|
|
207 |
+ |
EffortKind::Timed => KindGrid::Timed(TimedGrid {
|
|
208 |
+ |
committed: Vec::new(),
|
|
209 |
+ |
pending: TimedPending::default(),
|
|
210 |
+ |
focus: TimedField::Duration,
|
|
211 |
+ |
}),
|
|
212 |
+ |
EffortKind::Distance => KindGrid::Distance(DistanceGrid {
|
|
213 |
+ |
committed: Vec::new(),
|
|
214 |
+ |
pending: DistancePending::default(),
|
|
215 |
+ |
focus: DistanceField::Distance,
|
|
216 |
+ |
}),
|
|
217 |
+ |
};
|
|
218 |
+ |
self.grid = Some(GridState { exercise, kind });
|
| 153 |
219 |
|
}
|
| 154 |
220 |
|
|
| 155 |
221 |
|
fn refresh_grid(&mut self, db: &Db) {
|
| 156 |
|
- |
if let Some(g) = self.grid.as_mut()
|
| 157 |
|
- |
&& let Ok(list) = RepsKind::list_sets_for_session(db, g.exercise.id, self.date)
|
| 158 |
|
- |
{
|
| 159 |
|
- |
g.committed = list;
|
|
222 |
+ |
let Some(g) = self.grid.as_mut() else { return };
|
|
223 |
+ |
match &mut g.kind {
|
|
224 |
+ |
KindGrid::Reps(r) => {
|
|
225 |
+ |
if let Ok(list) = RepsKind::list_sets_for_session(db, g.exercise.id, self.date) {
|
|
226 |
+ |
r.committed = list;
|
|
227 |
+ |
}
|
|
228 |
+ |
}
|
|
229 |
+ |
KindGrid::Timed(t) => {
|
|
230 |
+ |
if let Ok(list) = TimedKind::list_sets_for_session(db, g.exercise.id, self.date) {
|
|
231 |
+ |
t.committed = list;
|
|
232 |
+ |
}
|
|
233 |
+ |
}
|
|
234 |
+ |
KindGrid::Distance(d) => {
|
|
235 |
+ |
if let Ok(list) =
|
|
236 |
+ |
DistanceKind::list_sets_for_session(db, g.exercise.id, self.date)
|
|
237 |
+ |
{
|
|
238 |
+ |
d.committed = list;
|
|
239 |
+ |
}
|
|
240 |
+ |
}
|
| 160 |
241 |
|
}
|
| 161 |
242 |
|
}
|
| 162 |
243 |
|
|
| 170 |
251 |
|
return;
|
| 171 |
252 |
|
}
|
| 172 |
253 |
|
(KeyCode::Tab, _) => {
|
| 173 |
|
- |
g.focus = match g.focus {
|
| 174 |
|
- |
GridField::Load => GridField::Reps,
|
| 175 |
|
- |
GridField::Reps => GridField::Rpe,
|
| 176 |
|
- |
GridField::Rpe => GridField::Load,
|
| 177 |
|
- |
};
|
|
254 |
+ |
advance_focus(&mut g.kind);
|
| 178 |
255 |
|
return;
|
| 179 |
256 |
|
}
|
| 180 |
257 |
|
(KeyCode::BackTab, _) => {
|
| 181 |
|
- |
g.focus = match g.focus {
|
| 182 |
|
- |
GridField::Load => GridField::Rpe,
|
| 183 |
|
- |
GridField::Reps => GridField::Load,
|
| 184 |
|
- |
GridField::Rpe => GridField::Reps,
|
| 185 |
|
- |
};
|
|
258 |
+ |
retreat_focus(&mut g.kind);
|
| 186 |
259 |
|
return;
|
| 187 |
260 |
|
}
|
| 188 |
261 |
|
(KeyCode::Enter, _) => {
|
| 195 |
268 |
|
return;
|
| 196 |
269 |
|
}
|
| 197 |
270 |
|
(KeyCode::Backspace, KeyModifiers::CONTROL) => {
|
| 198 |
|
- |
if let Some(last) = g.committed.last().cloned() {
|
| 199 |
|
- |
match RepsKind::delete(db, last.id) {
|
| 200 |
|
- |
Ok(()) => {
|
| 201 |
|
- |
self.status = "last set deleted".to_string();
|
| 202 |
|
- |
self.refresh_grid(db);
|
| 203 |
|
- |
}
|
| 204 |
|
- |
Err(e) => self.status = format!("delete failed: {e}"),
|
|
271 |
+ |
match delete_last(db, g) {
|
|
272 |
+ |
Ok(true) => {
|
|
273 |
+ |
self.status = "last set deleted".to_string();
|
|
274 |
+ |
self.refresh_grid(db);
|
| 205 |
275 |
|
}
|
|
276 |
+ |
Ok(false) => {}
|
|
277 |
+ |
Err(e) => self.status = format!("delete failed: {e}"),
|
| 206 |
278 |
|
}
|
| 207 |
279 |
|
return;
|
| 208 |
280 |
|
}
|
| 209 |
281 |
|
_ => {}
|
| 210 |
282 |
|
}
|
| 211 |
283 |
|
|
| 212 |
|
- |
let target = match g.focus {
|
| 213 |
|
- |
GridField::Load => &mut g.pending.load,
|
| 214 |
|
- |
GridField::Reps => &mut g.pending.reps,
|
| 215 |
|
- |
GridField::Rpe => &mut g.pending.rpe,
|
| 216 |
|
- |
};
|
|
284 |
+ |
let target = focused_field_mut(&mut g.kind);
|
| 217 |
285 |
|
match key.code {
|
| 218 |
286 |
|
KeyCode::Backspace => {
|
| 219 |
287 |
|
target.pop();
|
| 248 |
316 |
|
|
| 249 |
317 |
|
let hint = match &self.grid {
|
| 250 |
318 |
|
None => "type to filter enter pick [/] date esc clear",
|
| 251 |
|
- |
Some(_) => {
|
| 252 |
|
- |
"tab move enter save ctrl-backspace remove last esc back to picker"
|
| 253 |
|
- |
}
|
|
319 |
+ |
Some(_) => "tab move enter save ctrl-backspace remove last esc back to picker",
|
| 254 |
320 |
|
};
|
| 255 |
321 |
|
frame.render_widget(
|
| 256 |
322 |
|
Paragraph::new(Line::from(vec![
|
| 266 |
332 |
|
}
|
| 267 |
333 |
|
}
|
| 268 |
334 |
|
|
|
335 |
+ |
fn advance_focus(kind: &mut KindGrid) {
|
|
336 |
+ |
match kind {
|
|
337 |
+ |
KindGrid::Reps(r) => {
|
|
338 |
+ |
r.focus = match r.focus {
|
|
339 |
+ |
RepsField::Load => RepsField::Reps,
|
|
340 |
+ |
RepsField::Reps => RepsField::Rpe,
|
|
341 |
+ |
RepsField::Rpe => RepsField::Load,
|
|
342 |
+ |
}
|
|
343 |
+ |
}
|
|
344 |
+ |
KindGrid::Timed(t) => {
|
|
345 |
+ |
t.focus = match t.focus {
|
|
346 |
+ |
TimedField::Duration => TimedField::Rpe,
|
|
347 |
+ |
TimedField::Rpe => TimedField::Duration,
|
|
348 |
+ |
}
|
|
349 |
+ |
}
|
|
350 |
+ |
KindGrid::Distance(d) => {
|
|
351 |
+ |
d.focus = match d.focus {
|
|
352 |
+ |
DistanceField::Distance => DistanceField::Duration,
|
|
353 |
+ |
DistanceField::Duration => DistanceField::Rpe,
|
|
354 |
+ |
DistanceField::Rpe => DistanceField::Distance,
|
|
355 |
+ |
}
|
|
356 |
+ |
}
|
|
357 |
+ |
}
|
|
358 |
+ |
}
|
|
359 |
+ |
|
|
360 |
+ |
fn retreat_focus(kind: &mut KindGrid) {
|
|
361 |
+ |
match kind {
|
|
362 |
+ |
KindGrid::Reps(r) => {
|
|
363 |
+ |
r.focus = match r.focus {
|
|
364 |
+ |
RepsField::Load => RepsField::Rpe,
|
|
365 |
+ |
RepsField::Reps => RepsField::Load,
|
|
366 |
+ |
RepsField::Rpe => RepsField::Reps,
|
|
367 |
+ |
}
|
|
368 |
+ |
}
|
|
369 |
+ |
KindGrid::Timed(t) => {
|
|
370 |
+ |
t.focus = match t.focus {
|
|
371 |
+ |
TimedField::Duration => TimedField::Rpe,
|
|
372 |
+ |
TimedField::Rpe => TimedField::Duration,
|
|
373 |
+ |
}
|
|
374 |
+ |
}
|
|
375 |
+ |
KindGrid::Distance(d) => {
|
|
376 |
+ |
d.focus = match d.focus {
|
|
377 |
+ |
DistanceField::Distance => DistanceField::Rpe,
|
|
378 |
+ |
DistanceField::Duration => DistanceField::Distance,
|
|
379 |
+ |
DistanceField::Rpe => DistanceField::Duration,
|
|
380 |
+ |
}
|
|
381 |
+ |
}
|
|
382 |
+ |
}
|
|
383 |
+ |
}
|
|
384 |
+ |
|
|
385 |
+ |
fn focused_field_mut(kind: &mut KindGrid) -> &mut String {
|
|
386 |
+ |
match kind {
|
|
387 |
+ |
KindGrid::Reps(r) => match r.focus {
|
|
388 |
+ |
RepsField::Load => &mut r.pending.load,
|
|
389 |
+ |
RepsField::Reps => &mut r.pending.reps,
|
|
390 |
+ |
RepsField::Rpe => &mut r.pending.rpe,
|
|
391 |
+ |
},
|
|
392 |
+ |
KindGrid::Timed(t) => match t.focus {
|
|
393 |
+ |
TimedField::Duration => &mut t.pending.duration,
|
|
394 |
+ |
TimedField::Rpe => &mut t.pending.rpe,
|
|
395 |
+ |
},
|
|
396 |
+ |
KindGrid::Distance(d) => match d.focus {
|
|
397 |
+ |
DistanceField::Distance => &mut d.pending.distance,
|
|
398 |
+ |
DistanceField::Duration => &mut d.pending.duration,
|
|
399 |
+ |
DistanceField::Rpe => &mut d.pending.rpe,
|
|
400 |
+ |
},
|
|
401 |
+ |
}
|
|
402 |
+ |
}
|
|
403 |
+ |
|
| 269 |
404 |
|
fn commit_pending(
|
| 270 |
405 |
|
db: &Db,
|
| 271 |
406 |
|
g: &mut GridState,
|
| 272 |
407 |
|
date: NaiveDate,
|
| 273 |
408 |
|
) -> Result<(), ripgrow_core::Error> {
|
| 274 |
|
- |
let load = ripgrow_core::Load::new(parse_field("load", &g.pending.load)?)?;
|
| 275 |
|
- |
let reps = ripgrow_core::Reps::new(parse_field("reps", &g.pending.reps)?)?;
|
| 276 |
|
- |
let rpe = ripgrow_core::Rpe::new(parse_field("rpe", &g.pending.rpe)?)?;
|
| 277 |
|
- |
RepsKind::append(
|
| 278 |
|
- |
db,
|
| 279 |
|
- |
g.exercise.id,
|
| 280 |
|
- |
date,
|
| 281 |
|
- |
RepsPayload::new(load, reps),
|
| 282 |
|
- |
rpe,
|
| 283 |
|
- |
false,
|
| 284 |
|
- |
)?;
|
| 285 |
|
- |
g.pending = PendingRow::default();
|
| 286 |
|
- |
g.focus = GridField::Load;
|
|
409 |
+ |
match &mut g.kind {
|
|
410 |
+ |
KindGrid::Reps(r) => {
|
|
411 |
+ |
let load = Load::new(parse_field("load", &r.pending.load)?)?;
|
|
412 |
+ |
let reps = Reps::new(parse_field("reps", &r.pending.reps)?)?;
|
|
413 |
+ |
let rpe = Rpe::new(parse_field("rpe", &r.pending.rpe)?)?;
|
|
414 |
+ |
RepsKind::append(
|
|
415 |
+ |
db,
|
|
416 |
+ |
g.exercise.id,
|
|
417 |
+ |
date,
|
|
418 |
+ |
RepsPayload::new(load, reps),
|
|
419 |
+ |
rpe,
|
|
420 |
+ |
false,
|
|
421 |
+ |
)?;
|
|
422 |
+ |
r.pending = RepsPending::default();
|
|
423 |
+ |
r.focus = RepsField::Load;
|
|
424 |
+ |
}
|
|
425 |
+ |
KindGrid::Timed(t) => {
|
|
426 |
+ |
let duration = Duration::from_seconds(parse_duration(&t.pending.duration)?)?;
|
|
427 |
+ |
let rpe = Rpe::new(parse_field("rpe", &t.pending.rpe)?)?;
|
|
428 |
+ |
TimedKind::append(
|
|
429 |
+ |
db,
|
|
430 |
+ |
g.exercise.id,
|
|
431 |
+ |
date,
|
|
432 |
+ |
TimedPayload::new(duration),
|
|
433 |
+ |
rpe,
|
|
434 |
+ |
false,
|
|
435 |
+ |
)?;
|
|
436 |
+ |
t.pending = TimedPending::default();
|
|
437 |
+ |
t.focus = TimedField::Duration;
|
|
438 |
+ |
}
|
|
439 |
+ |
KindGrid::Distance(d) => {
|
|
440 |
+ |
let distance = Distance::from_meters(parse_field("distance", &d.pending.distance)?)?;
|
|
441 |
+ |
let duration = Duration::from_seconds(parse_duration(&d.pending.duration)?)?;
|
|
442 |
+ |
let rpe = Rpe::new(parse_field("rpe", &d.pending.rpe)?)?;
|
|
443 |
+ |
DistanceKind::append(
|
|
444 |
+ |
db,
|
|
445 |
+ |
g.exercise.id,
|
|
446 |
+ |
date,
|
|
447 |
+ |
DistancePayload::new(distance, duration),
|
|
448 |
+ |
rpe,
|
|
449 |
+ |
false,
|
|
450 |
+ |
)?;
|
|
451 |
+ |
d.pending = DistancePending::default();
|
|
452 |
+ |
d.focus = DistanceField::Distance;
|
|
453 |
+ |
}
|
|
454 |
+ |
}
|
| 287 |
455 |
|
Ok(())
|
| 288 |
456 |
|
}
|
| 289 |
457 |
|
|
|
458 |
+ |
/// Delete the most recently committed set for the current grid. Returns
|
|
459 |
+ |
/// `Ok(true)` when a row was actually removed, `Ok(false)` when there
|
|
460 |
+ |
/// was nothing to delete.
|
|
461 |
+ |
fn delete_last(db: &Db, g: &GridState) -> Result<bool, ripgrow_core::Error> {
|
|
462 |
+ |
match &g.kind {
|
|
463 |
+ |
KindGrid::Reps(r) => {
|
|
464 |
+ |
let Some(last) = r.committed.last() else { return Ok(false) };
|
|
465 |
+ |
RepsKind::delete(db, last.id)?;
|
|
466 |
+ |
Ok(true)
|
|
467 |
+ |
}
|
|
468 |
+ |
KindGrid::Timed(t) => {
|
|
469 |
+ |
let Some(last) = t.committed.last() else { return Ok(false) };
|
|
470 |
+ |
TimedKind::delete(db, last.id)?;
|
|
471 |
+ |
Ok(true)
|
|
472 |
+ |
}
|
|
473 |
+ |
KindGrid::Distance(d) => {
|
|
474 |
+ |
let Some(last) = d.committed.last() else { return Ok(false) };
|
|
475 |
+ |
DistanceKind::delete(db, last.id)?;
|
|
476 |
+ |
Ok(true)
|
|
477 |
+ |
}
|
|
478 |
+ |
}
|
|
479 |
+ |
}
|
|
480 |
+ |
|
| 290 |
481 |
|
fn parse_field<T: std::str::FromStr>(
|
| 291 |
482 |
|
field: &'static str,
|
| 292 |
483 |
|
raw: &str,
|
| 299 |
490 |
|
})
|
| 300 |
491 |
|
}
|
| 301 |
492 |
|
|
|
493 |
+ |
/// Parse a duration string into seconds. Accepts `N` (seconds) or `M:SS`
|
|
494 |
+ |
/// (minutes:seconds). `1:5` is 65s, `1:05` is 65s — the seconds field is
|
|
495 |
+ |
/// numeric, not lexical.
|
|
496 |
+ |
fn parse_duration(raw: &str) -> Result<i32, ripgrow_core::Error> {
|
|
497 |
+ |
let s = raw.trim();
|
|
498 |
+ |
let err = || ripgrow_core::Error::ParseField {
|
|
499 |
+ |
field: "duration",
|
|
500 |
+ |
value: raw.to_string(),
|
|
501 |
+ |
};
|
|
502 |
+ |
if let Some((m, sec)) = s.split_once(':') {
|
|
503 |
+ |
let m: i32 = m.parse().map_err(|_| err())?;
|
|
504 |
+ |
let sec: i32 = sec.parse().map_err(|_| err())?;
|
|
505 |
+ |
if m < 0 || sec < 0 {
|
|
506 |
+ |
return Err(err());
|
|
507 |
+ |
}
|
|
508 |
+ |
Ok(m * 60 + sec)
|
|
509 |
+ |
} else {
|
|
510 |
+ |
let n: i32 = s.parse().map_err(|_| err())?;
|
|
511 |
+ |
if n < 0 {
|
|
512 |
+ |
return Err(err());
|
|
513 |
+ |
}
|
|
514 |
+ |
Ok(n)
|
|
515 |
+ |
}
|
|
516 |
+ |
}
|
|
517 |
+ |
|
| 302 |
518 |
|
fn render_picker(
|
| 303 |
519 |
|
frame: &mut Frame,
|
| 304 |
520 |
|
area: Rect,
|
| 334 |
550 |
|
let inner = block.inner(area);
|
| 335 |
551 |
|
frame.render_widget(block, area);
|
| 336 |
552 |
|
|
|
553 |
+ |
let lines = match &g.kind {
|
|
554 |
+ |
KindGrid::Reps(r) => render_reps_lines(&g.exercise, r),
|
|
555 |
+ |
KindGrid::Timed(t) => render_timed_lines(t),
|
|
556 |
+ |
KindGrid::Distance(d) => render_distance_lines(d),
|
|
557 |
+ |
};
|
|
558 |
+ |
frame.render_widget(Paragraph::new(lines), inner);
|
|
559 |
+ |
}
|
|
560 |
+ |
|
|
561 |
+ |
fn render_reps_lines<'a>(exercise: &'a Exercise, r: &'a RepsGrid) -> Vec<Line<'a>> {
|
| 337 |
562 |
|
let header = format!(
|
| 338 |
563 |
|
" # {:>8} {:>4} {:>3}",
|
| 339 |
|
- |
format!("load ({})", g.exercise.load_unit),
|
|
564 |
+ |
format!("load ({})", exercise.load_unit),
|
| 340 |
565 |
|
"reps",
|
| 341 |
566 |
|
"rpe"
|
| 342 |
567 |
|
);
|
| 344 |
569 |
|
header,
|
| 345 |
570 |
|
Style::default().add_modifier(Modifier::BOLD),
|
| 346 |
571 |
|
))];
|
| 347 |
|
- |
|
| 348 |
|
- |
for s in &g.committed {
|
|
572 |
+ |
for s in &r.committed {
|
| 349 |
573 |
|
lines.push(Line::from(format!(
|
| 350 |
574 |
|
" {:<3} {:>8} {:>4} {:>3}",
|
| 351 |
575 |
|
s.set_index, s.load, s.reps, s.rpe
|
| 352 |
576 |
|
)));
|
| 353 |
577 |
|
}
|
| 354 |
|
- |
|
| 355 |
|
- |
let load_span = focused_span(&g.pending.load, g.focus == GridField::Load, 8);
|
| 356 |
|
- |
let reps_span = focused_span(&g.pending.reps, g.focus == GridField::Reps, 4);
|
| 357 |
|
- |
let rpe_span = focused_span(&g.pending.rpe, g.focus == GridField::Rpe, 3);
|
| 358 |
|
- |
let next_idx = g.committed.last().map(|s| s.set_index + 1).unwrap_or(1);
|
|
578 |
+ |
let load = focused_span(&r.pending.load, r.focus == RepsField::Load, 8);
|
|
579 |
+ |
let reps = focused_span(&r.pending.reps, r.focus == RepsField::Reps, 4);
|
|
580 |
+ |
let rpe = focused_span(&r.pending.rpe, r.focus == RepsField::Rpe, 3);
|
|
581 |
+ |
let next_idx = r.committed.last().map(|s| s.set_index + 1).unwrap_or(1);
|
| 359 |
582 |
|
lines.push(Line::from(vec![
|
| 360 |
583 |
|
Span::raw(format!(" {:<3} ", next_idx)),
|
| 361 |
|
- |
load_span,
|
|
584 |
+ |
load,
|
| 362 |
585 |
|
Span::raw(" "),
|
| 363 |
|
- |
reps_span,
|
|
586 |
+ |
reps,
|
| 364 |
587 |
|
Span::raw(" "),
|
| 365 |
|
- |
rpe_span,
|
|
588 |
+ |
rpe,
|
| 366 |
589 |
|
]));
|
|
590 |
+ |
lines
|
|
591 |
+ |
}
|
| 367 |
592 |
|
|
| 368 |
|
- |
frame.render_widget(Paragraph::new(lines), inner);
|
|
593 |
+ |
fn render_timed_lines(t: &TimedGrid) -> Vec<Line<'_>> {
|
|
594 |
+ |
let header = format!(" # {:>10} {:>3}", "duration", "rpe");
|
|
595 |
+ |
let mut lines: Vec<Line> = vec![Line::from(Span::styled(
|
|
596 |
+ |
header,
|
|
597 |
+ |
Style::default().add_modifier(Modifier::BOLD),
|
|
598 |
+ |
))];
|