Skip to main content

very_normal/
warmup.rs

1//! A guided one-minute warm-up before the first drill of the day: a few slow
2//! breaths to settle in, then gentle hand and wrist stretches.
3
4use 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    /// The step you're on and the seconds left in it, or `None` when done.
59    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    /// During the breathing step: whether to breathe in or out right now.
75    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    /// Jumps to the start of the next step.
89    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}