1use std::time::{Duration, Instant};
5
6pub struct Step {
7 pub title: &'static str,
8 pub how: &'static str,
9 pub secs: u64,
10}
11
12pub const STEPS: [Step; 6] = [
13 Step {
14 title: "Breathe",
15 how: "Slow breaths: in for 4, out for 6. Drop your shoulders on every out-breath.",
16 secs: 20,
17 },
18 Step {
19 title: "Fist and fan",
20 how: "Make a loose fist, then spread your fingers wide. Five slow times.",
21 secs: 10,
22 },
23 Step {
24 title: "Prayer stretch",
25 how: "Palms together at your chest. Lower them slowly until your wrists feel a gentle stretch, and hold.",
26 secs: 10,
27 },
28 Step {
29 title: "Wrist circles",
30 how: "Five slow circles each way.",
31 secs: 10,
32 },
33 Step {
34 title: "Thumb touches",
35 how: "Touch your thumb to each fingertip and back again, both hands.",
36 secs: 8,
37 },
38 Step {
39 title: "Shake it out",
40 how: "Let your hands hang loose and shake them out.",
41 secs: 5,
42 },
43];
44
45pub struct Warmup {
46 started: Instant,
47}
48
49impl Warmup {
50 pub fn new(now: Instant) -> Warmup {
51 Warmup { started: now }
52 }
53
54 fn elapsed(&self, now: Instant) -> Duration {
55 now.saturating_duration_since(self.started)
56 }
57
58 pub fn current(&self, now: Instant) -> Option<(usize, u64)> {
60 let mut t = self.elapsed(now).as_secs_f64();
61 for (i, step) in STEPS.iter().enumerate() {
62 if t < step.secs as f64 {
63 return Some((i, (step.secs as f64 - t).ceil() as u64));
64 }
65 t -= step.secs as f64;
66 }
67 None
68 }
69
70 pub fn done(&self, now: Instant) -> bool {
71 self.current(now).is_none()
72 }
73
74 pub fn breath(&self, now: Instant) -> Option<&'static str> {
76 let (step, _) = self.current(now)?;
77 if step != 0 {
78 return None;
79 }
80 let cycle = self.elapsed(now).as_secs_f64() % 10.0;
81 Some(if cycle < 4.0 {
82 "breathe in…"
83 } else {
84 "and out…"
85 })
86 }
87
88 pub fn skip_step(&mut self, now: Instant) {
90 if let Some((_, left)) = self.current(now) {
91 let left = Duration::from_secs(left);
92 self.started = self.started.checked_sub(left).unwrap_or(self.started);
93 }
94 }
95
96 pub fn total() -> u64 {
97 STEPS.iter().map(|s| s.secs).sum()
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn walks_through_the_steps_in_about_a_minute() {
107 let t0 = Instant::now();
108 let w = Warmup::new(t0);
109 assert_eq!(w.current(t0), Some((0, 20)));
110 assert_eq!(w.current(t0 + Duration::from_secs(21)), Some((1, 9)));
111 assert!((55..=70).contains(&Warmup::total()));
112 assert!(w.done(t0 + Duration::from_secs(Warmup::total())));
113 }
114
115 #[test]
116 fn breathes_in_for_four_and_out_for_six() {
117 let t0 = Instant::now();
118 let w = Warmup::new(t0);
119 assert_eq!(w.breath(t0 + Duration::from_secs(2)), Some("breathe in…"));
120 assert_eq!(w.breath(t0 + Duration::from_secs(7)), Some("and out…"));
121 assert_eq!(w.breath(t0 + Duration::from_secs(25)), None);
122 }
123
124 #[test]
125 fn enter_skips_to_the_next_step() {
126 let t0 = Instant::now() + Duration::from_secs(100);
127 let mut w = Warmup::new(t0);
128 w.skip_step(t0 + Duration::from_secs(3));
129 assert_eq!(w.current(t0 + Duration::from_secs(3)).map(|c| c.0), Some(1));
130 }
131}