Skip to main content

max / ripgrow

sets: append_set and append_diagnostic_set take Load, Reps, Rpe The write API now refuses to compile against unvalidated primitives. The TUI's log and diagnostic screens parse text into newtypes at the input boundary, which is where user-facing errors belong anyway. Constructor tests consolidate the "rejects out-of-range" checks that used to duplicate on every DB entry point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-18 18:28 UTC
Commit: 942a801ab5bd1466782fe8e03ad3d69a07fd2b55
Parent: 61bb1a8
5 files changed, +70 insertions, -80 deletions
@@ -246,8 +246,8 @@ impl Db {
246 246 #[cfg(test)]
247 247 mod tests {
248 248 use super::*;
249 - use crate::values::LoadUnit;
250 249 use crate::templates::ResistanceType;
250 + use crate::values::{Load, LoadUnit, Reps, Rpe};
251 251
252 252 fn ex(id: i64, name: &str, tags: Vec<i64>) -> Exercise {
253 253 Exercise {
@@ -414,7 +414,7 @@ mod tests {
414 414 .create_exercise("b", ResistanceType::Freeweight, "kg", 2.5, &[])
415 415 .unwrap();
416 416 let d = NaiveDate::from_ymd_opt(2026, 7, 10).unwrap();
417 - db.append_set(a, d, 100.0, 5, 3).unwrap();
417 + db.append_set(a, d, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
418 418 let map = db
419 419 .days_since_last_map(NaiveDate::from_ymd_opt(2026, 7, 15).unwrap())
420 420 .unwrap();
@@ -357,8 +357,8 @@ impl Db {
357 357 #[cfg(test)]
358 358 mod db_tests {
359 359 use super::*;
360 - use crate::values::LoadUnit;
361 360 use crate::templates::ResistanceType;
361 + use crate::values::{Load, LoadUnit, Reps, Rpe};
362 362 use chrono::NaiveDate;
363 363
364 364 fn setup() -> (Db, i64) {
@@ -384,7 +384,7 @@ mod db_tests {
384 384 let (db, id) = setup();
385 385 let d = NaiveDate::from_ymd_opt(2026, 7, 10).unwrap();
386 386 // Diagnostic finished at 100. Working weight = 0.9 * 100 = 90.
387 - db.append_diagnostic_set(id, d, 100.0, 5, 4).unwrap();
387 + db.append_diagnostic_set(id, d, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
388 388 let PrescriptionResult::Prescribed(pres) = db.compute_prescription(id).unwrap() else {
389 389 panic!("expected prescription");
390 390 };
@@ -399,8 +399,8 @@ mod db_tests {
399 399 let (db, id) = setup();
400 400 let d1 = NaiveDate::from_ymd_opt(2026, 7, 10).unwrap();
401 401 let d2 = NaiveDate::from_ymd_opt(2026, 7, 12).unwrap();
402 - db.append_diagnostic_set(id, d1, 100.0, 5, 4).unwrap();
403 - db.append_set(id, d2, 60.0, 5, 3).unwrap();
402 + db.append_diagnostic_set(id, d1, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
403 + db.append_set(id, d2, Load::new(60.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
404 404 let PrescriptionResult::Prescribed(pres) = db.compute_prescription(id).unwrap() else {
405 405 panic!("expected prescription");
406 406 };
@@ -413,10 +413,10 @@ mod db_tests {
413 413 let (db, id) = setup();
414 414 let day = |n| NaiveDate::from_ymd_opt(2026, 7, n).unwrap();
415 415 for _ in 0..3 {
416 - db.append_set(id, day(1), 100.0, 5, 3).unwrap();
416 + db.append_set(id, day(1), Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
417 417 }
418 418 for _ in 0..3 {
419 - db.append_set(id, day(2), 102.5, 5, 4).unwrap();
419 + db.append_set(id, day(2), Load::new(102.5).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
420 420 }
421 421 let PrescriptionResult::Prescribed(pres) = db.compute_prescription(id).unwrap() else {
422 422 panic!("expected prescription");
@@ -433,16 +433,16 @@ mod db_tests {
433 433 fn compute_prescription_updates_cache_across_calls() {
434 434 let (db, id) = setup();
435 435 let day = |n| NaiveDate::from_ymd_opt(2026, 7, n).unwrap();
436 - db.append_set(id, day(1), 100.0, 5, 3).unwrap();
436 + db.append_set(id, day(1), Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
437 437 db.compute_prescription(id).unwrap();
438 438 assert_eq!(
439 439 db.read_progression_state(id).unwrap(),
440 440 Some(State::Progressing)
441 441 );
442 442 // Add a miss to drop to probation.
443 - db.append_set(id, day(2), 100.0, 5, 3).unwrap();
444 - db.append_set(id, day(2), 100.0, 5, 3).unwrap();
445 - db.append_set(id, day(2), 100.0, 4, 3).unwrap();
443 + db.append_set(id, day(2), Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
444 + db.append_set(id, day(2), Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
445 + db.append_set(id, day(2), Load::new(100.0).unwrap(), Reps::new(4).unwrap(), Rpe::new(3).unwrap()).unwrap();
446 446 db.compute_prescription(id).unwrap();
447 447 assert_eq!(
448 448 db.read_progression_state(id).unwrap(),
@@ -81,22 +81,14 @@ impl Db {
81 81 /// Append a normal (non-diagnostic) working set. `set_index` is
82 82 /// auto-assigned as one past the current max for this (date, exercise).
83 83 /// Returns the new row id.
84 - ///
85 - /// Primitive inputs are validated through the newtype constructors
86 - /// before any SQL runs. A follow-up commit will lift the signature to
87 - /// take `Load`/`Reps`/`Rpe` directly so parsing lives at the TUI
88 - /// boundary.
89 84 pub fn append_set(
90 85 &self,
91 86 exercise_id: i64,
92 87 date: NaiveDate,
93 - load: f64,
94 - reps: i32,
95 - rpe: i32,
88 + load: Load,
89 + reps: Reps,
90 + rpe: Rpe,
96 91 ) -> Result<i64, Error> {
97 - let load = Load::new(load)?;
98 - let reps = Reps::new(reps)?;
99 - let rpe = Rpe::new(rpe)?;
100 92 self.append_set_inner(exercise_id, date, load, reps, rpe, false)
101 93 }
102 94
@@ -107,13 +99,10 @@ impl Db {
107 99 &self,
108 100 exercise_id: i64,
109 101 date: NaiveDate,
110 - load: f64,
111 - reps: i32,
112 - rpe: i32,
102 + load: Load,
103 + reps: Reps,
104 + rpe: Rpe,
113 105 ) -> Result<i64, Error> {
114 - let load = Load::new(load)?;
115 - let reps = Reps::new(reps)?;
116 - let rpe = Rpe::new(rpe)?;
117 106 self.append_set_inner(exercise_id, date, load, reps, rpe, true)
118 107 }
119 108
@@ -280,9 +269,9 @@ mod tests {
280 269 fn append_and_list_round_trip() {
281 270 let (db, ex) = setup();
282 271 let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
283 - db.append_set(ex, date, 100.0, 5, 3).unwrap();
284 - db.append_set(ex, date, 100.0, 5, 3).unwrap();
285 - db.append_set(ex, date, 100.0, 5, 4).unwrap();
272 + db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
273 + db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
274 + db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
286 275 let list = db.list_sets_for_session(ex, date).unwrap();
287 276 assert_eq!(list.len(), 3);
288 277 assert_eq!(list[0].set_index, 1);
@@ -291,18 +280,16 @@ mod tests {
291 280 }
292 281
293 282 #[test]
294 - fn append_rejects_rpe_out_of_range() {
295 - let (db, ex) = setup();
296 - let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
297 - assert!(db.append_set(ex, date, 100.0, 5, 0).is_err());
298 - assert!(db.append_set(ex, date, 100.0, 5, 6).is_err());
299 - }
300 -
301 - #[test]
302 - fn append_rejects_negative_reps() {
303 - let (db, ex) = setup();
304 - let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
305 - assert!(db.append_set(ex, date, 100.0, -1, 3).is_err());
283 + fn newtype_constructors_reject_out_of_range_values() {
284 + // Validation now lives on the value constructors; append_set can
285 + // never be called with a bad Rpe or Reps because you can't
286 + // construct them. These tests confirm the constructors themselves
287 + // still guard.
288 + let _ = setup();
289 + assert!(Rpe::new(0).is_err());
290 + assert!(Rpe::new(6).is_err());
291 + assert!(Reps::new(-1).is_err());
292 + assert!(Load::new(-1.0).is_err());
306 293 }
307 294
308 295 #[test]
@@ -313,9 +300,9 @@ mod tests {
313 300 .unwrap();
314 301 let d1 = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
315 302 let d2 = NaiveDate::from_ymd_opt(2026, 7, 19).unwrap();
316 - db.append_set(ex1, d1, 100.0, 5, 3).unwrap();
317 - db.append_set(ex1, d2, 105.0, 5, 3).unwrap();
318 - db.append_set(ex2, d1, 80.0, 5, 3).unwrap();
303 + db.append_set(ex1, d1, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
304 + db.append_set(ex1, d2, Load::new(105.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
305 + db.append_set(ex2, d1, Load::new(80.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
319 306 assert_eq!(db.list_sets_for_session(ex1, d1).unwrap().len(), 1);
320 307 assert_eq!(db.list_sets_for_session(ex1, d2).unwrap().len(), 1);
321 308 assert_eq!(db.list_sets_for_session(ex2, d1).unwrap().len(), 1);
@@ -326,9 +313,9 @@ mod tests {
326 313 let (db, ex) = setup();
327 314 let d1 = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
328 315 let d2 = NaiveDate::from_ymd_opt(2026, 7, 3).unwrap();
329 - db.append_set(ex, d1, 100.0, 5, 3).unwrap();
330 - db.append_set(ex, d1, 105.0, 5, 4).unwrap();
331 - db.append_set(ex, d2, 100.0, 5, 3).unwrap();
316 + db.append_set(ex, d1, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
317 + db.append_set(ex, d1, Load::new(105.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
318 + db.append_set(ex, d2, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
332 319 let series = db.top_loads_over_time(ex).unwrap();
333 320 assert_eq!(series, vec![(d1, 105.0), (d2, 100.0)]);
334 321 }
@@ -346,9 +333,9 @@ mod tests {
346 333 // 100 x 5 @ RPE 5 -> e1RM 116.67
347 334 // 90 x 8 @ RPE 3 -> e1RM 90 * (1 + 10/30) = 120.0
348 335 // 120 x 3 @ RPE 5 -> e1RM 132.0 <- winner
349 - db.append_set(ex, date, 100.0, 5, 5).unwrap();
350 - db.append_set(ex, date, 90.0, 8, 3).unwrap();
351 - db.append_set(ex, date, 120.0, 3, 5).unwrap();
336 + db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(5).unwrap()).unwrap();
337 + db.append_set(ex, date, Load::new(90.0).unwrap(), Reps::new(8).unwrap(), Rpe::new(3).unwrap()).unwrap();
338 + db.append_set(ex, date, Load::new(120.0).unwrap(), Reps::new(3).unwrap(), Rpe::new(5).unwrap()).unwrap();
352 339 let e = db.best_e1rm(ex).unwrap().unwrap();
353 340 assert!((e - 132.0).abs() < 1e-6);
354 341 }
@@ -357,7 +344,7 @@ mod tests {
357 344 fn best_e1rm_ignores_warmup_rpe_1_sets() {
358 345 let (db, ex) = setup();
359 346 let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
360 - db.append_set(ex, date, 200.0, 10, 1).unwrap();
347 + db.append_set(ex, date, Load::new(200.0).unwrap(), Reps::new(10).unwrap(), Rpe::new(1).unwrap()).unwrap();
361 348 assert_eq!(db.best_e1rm(ex).unwrap(), None);
362 349 }
363 350
@@ -366,9 +353,9 @@ mod tests {
366 353 let (db, ex) = setup();
367 354 let d1 = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
368 355 let d2 = NaiveDate::from_ymd_opt(2026, 7, 3).unwrap();
369 - db.append_diagnostic_set(ex, d1, 60.0, 5, 3).unwrap();
370 - db.append_diagnostic_set(ex, d1, 80.0, 5, 4).unwrap();
371 - db.append_set(ex, d2, 72.0, 5, 3).unwrap();
356 + db.append_diagnostic_set(ex, d1, Load::new(60.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
357 + db.append_diagnostic_set(ex, d1, Load::new(80.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
358 + db.append_set(ex, d2, Load::new(72.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
372 359 let sessions = db.list_sessions_for_exercise(ex).unwrap();
373 360 assert_eq!(sessions.len(), 1, "diagnostic day should not appear");
374 361 assert_eq!(sessions[0][0].session_date, d2);
@@ -380,8 +367,8 @@ mod tests {
380 367 let (db, ex) = setup();
381 368 let d1 = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
382 369 let d2 = NaiveDate::from_ymd_opt(2026, 7, 3).unwrap();
383 - db.append_diagnostic_set(ex, d1, 120.0, 5, 4).unwrap();
384 - db.append_set(ex, d2, 80.0, 5, 3).unwrap();
370 + db.append_diagnostic_set(ex, d1, Load::new(120.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
371 + db.append_set(ex, d2, Load::new(80.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
385 372 let series = db.top_loads_over_time(ex).unwrap();
386 373 assert_eq!(series, vec![(d2, 80.0)]);
387 374 }
@@ -390,7 +377,7 @@ mod tests {
390 377 fn diagnostic_sets_still_count_toward_e1rm() {
391 378 let (db, ex) = setup();
392 379 let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
393 - db.append_diagnostic_set(ex, date, 120.0, 3, 4).unwrap();
380 + db.append_diagnostic_set(ex, date, Load::new(120.0).unwrap(), Reps::new(3).unwrap(), Rpe::new(4).unwrap()).unwrap();
394 381 assert!(db.best_e1rm(ex).unwrap().is_some());
395 382 }
396 383
@@ -399,10 +386,10 @@ mod tests {
399 386 let (db, ex) = setup();
400 387 let d1 = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
401 388 let d2 = NaiveDate::from_ymd_opt(2026, 7, 5).unwrap();
402 - db.append_diagnostic_set(ex, d1, 70.0, 5, 3).unwrap();
403 - db.append_diagnostic_set(ex, d1, 80.0, 5, 4).unwrap();
404 - db.append_diagnostic_set(ex, d2, 60.0, 5, 3).unwrap();
405 - db.append_diagnostic_set(ex, d2, 90.0, 5, 4).unwrap();
389 + db.append_diagnostic_set(ex, d1, Load::new(70.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
390 + db.append_diagnostic_set(ex, d1, Load::new(80.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
391 + db.append_diagnostic_set(ex, d2, Load::new(60.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
392 + db.append_diagnostic_set(ex, d2, Load::new(90.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
406 393 // Most recent diagnostic (d2) had a top of 90.
407 394 assert_eq!(db.diagnostic_seed(ex).unwrap(), Some(90.0));
408 395 }
@@ -411,7 +398,7 @@ mod tests {
411 398 fn diagnostic_seed_none_when_no_diagnostic_history() {
412 399 let (db, ex) = setup();
413 400 let date = NaiveDate::from_ymd_opt(2026, 7, 5).unwrap();
414 - db.append_set(ex, date, 100.0, 5, 3).unwrap();
401 + db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
415 402 assert_eq!(db.diagnostic_seed(ex).unwrap(), None);
416 403 }
417 404
@@ -419,8 +406,8 @@ mod tests {
419 406 fn delete_removes_and_gap_is_fine() {
420 407 let (db, ex) = setup();
421 408 let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
422 - let a = db.append_set(ex, date, 100.0, 5, 3).unwrap();
423 - db.append_set(ex, date, 100.0, 5, 3).unwrap();
409 + let a = db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
410 + db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
424 411 db.delete_set(a).unwrap();
425 412 let list = db.list_sets_for_session(ex, date).unwrap();
426 413 assert_eq!(list.len(), 1);
@@ -392,13 +392,16 @@ fn commit_diagnostic_set(
392 392 pending: &mut PendingRow,
393 393 committed: &mut Vec<DiagnosticSet>,
394 394 ) -> Result<(), ripgrow_core::Error> {
395 - let reps: i32 = parse_field("reps", &pending.reps)?;
396 - let rpe: i32 = parse_field("rpe", &pending.rpe)?;
397 - db.append_diagnostic_set(exercise.id, date, prescribed_load, reps, rpe)?;
395 + let reps_raw: i32 = parse_field("reps", &pending.reps)?;
396 + let rpe_raw: i32 = parse_field("rpe", &pending.rpe)?;
397 + let load = ripgrow_core::Load::new(prescribed_load)?;
398 + let reps = ripgrow_core::Reps::new(reps_raw)?;
399 + let rpe = ripgrow_core::Rpe::new(rpe_raw)?;
400 + db.append_diagnostic_set(exercise.id, date, load, reps, rpe)?;
398 401 committed.push(DiagnosticSet {
399 402 load: prescribed_load,
400 - reps,
401 - rpe,
403 + reps: reps_raw,
404 + rpe: rpe_raw,
402 405 });
403 406 *pending = PendingRow::default();
404 407 let _ = target_reps; // reserved for future variable-target variants
@@ -597,7 +600,7 @@ fn subsequence_match(haystack: &str, needle: &str) -> bool {
597 600 mod tests {
598 601 use super::*;
599 602 use crossterm::event::KeyEvent;
600 - use ripgrow_core::{ResistanceType, LoadUnit};
603 + use ripgrow_core::{Load, LoadUnit, Reps, ResistanceType, Rpe};
601 604
602 605 fn key(c: KeyCode) -> KeyEvent {
603 606 KeyEvent::new(c, KeyModifiers::empty())
@@ -736,7 +739,7 @@ mod tests {
736 739 fn seed_prefills_from_prior_diagnostic() {
737 740 let (db, sq) = setup();
738 741 let d = chrono::NaiveDate::from_ymd_opt(2026, 7, 10).unwrap();
739 - db.append_diagnostic_set(sq, d, 85.0, 5, 4).unwrap();
742 + db.append_diagnostic_set(sq, d, Load::new(85.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
740 743 let mut screen = DiagnosticScreen::load(&db).unwrap();
741 744 for c in "squat".chars() {
742 745 screen.on_key(&db, key(KeyCode::Char(c)));
@@ -271,9 +271,9 @@ fn commit_pending(
271 271 g: &mut GridState,
272 272 date: NaiveDate,
273 273 ) -> Result<(), ripgrow_core::Error> {
274 - let load: f64 = parse_field("load", &g.pending.load)?;
275 - let reps: i32 = parse_field("reps", &g.pending.reps)?;
276 - let rpe: i32 = parse_field("rpe", &g.pending.rpe)?;
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 277 db.append_set(g.exercise.id, date, load, reps, rpe)?;
278 278 g.pending = PendingRow::default();
279 279 g.focus = GridField::Load;
@@ -381,7 +381,7 @@ fn subsequence_match(haystack: &str, needle: &str) -> bool {
381 381 mod tests {
382 382 use super::*;
383 383 use crossterm::event::KeyEvent;
384 - use ripgrow_core::{ResistanceType, LoadUnit};
384 + use ripgrow_core::{Load, LoadUnit, Reps, ResistanceType, Rpe};
385 385
386 386 fn key(c: KeyCode) -> KeyEvent {
387 387 KeyEvent::new(c, KeyModifiers::empty())
@@ -458,8 +458,8 @@ mod tests {
458 458 fn ctrl_backspace_removes_last_committed_set() {
459 459 let (db, sq) = setup();
460 460 let today = chrono::Local::now().date_naive();
461 - db.append_set(sq, today, 100.0, 5, 3).unwrap();
462 - db.append_set(sq, today, 100.0, 5, 3).unwrap();
461 + db.append_set(sq, today, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
462 + db.append_set(sq, today, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
463 463 let mut screen = LogScreen::load(&db).unwrap();
464 464 for c in "squat".chars() {
465 465 screen.on_key(&db, key(KeyCode::Char(c)));