···11use std::time::{Duration, Instant};
2233-use super::profile::{BreakLevel, Profile};
33+use super::profile::Profile;
4455#[derive(Debug, Clone)]
66pub struct ScheduledBreak {
···1111 pub level_index: usize,
1212}
13131414-/// Tracks elapsed work time and cycle counts to determine the next break.
1515-///
1616-/// Each break level has its own independent elapsed counter so that a
1717-/// short micro-break firing does not reset progress toward longer levels.
1818-/// For example, with levels [1 min micro, 3 min regular]:
1919-/// - level_elapsed[0] resets every time the micro fires
2020-/// - level_elapsed[1] keeps accumulating and fires independently at 3 min
1414+// Tracks elapsed work time and cycle counts to determine the next break.
1515+//
1616+// Each break level has its own independent elapsed counter so that a
1717+// short micro-break firing does not reset progress toward longer levels.
1818+// For example, with levels [1 min micro, 3 min regular]:
1919+// - level_elapsed[0] resets every time the micro fires
2020+// - level_elapsed[1] keeps accumulating and fires independently at 3 min
2121pub struct Scheduler {
2222 profile: Profile,
2323- /// Per-level elapsed work time. Index matches profile.levels.
2323+ // Per-level elapsed work time. Index matches profile.levels.
2424 level_elapsed: Vec<Duration>,
2525- /// How many primary-level (longest interval) cycles have completed.
2525+ // How many primary-level (longest interval) cycles have completed.
2626 primary_cycle_count: u32,
2727- /// When the last break ended (for cycle gap detection).
2727+ // When the last break ended (for cycle gap detection).
2828 last_break_ended: Option<Instant>,
2929}
3030···5050 self.primary_cycle_count = 0;
5151 }
52525353- /// Advance all level timers by `delta`. Returns a break to fire if one is due.
5353+ // Advance all level timers by `delta`. Returns a break to fire if one is due.
5454 pub fn tick(&mut self, delta: Duration) -> Option<ScheduledBreak> {
5555 for elapsed in &mut self.level_elapsed {
5656 *elapsed += delta;
···5858 self.due_break()
5959 }
60606161- /// Force the next scheduled break to fire immediately (tray "skip to next break").
6161+ // Force the next scheduled break to fire immediately (tray "skip to next break").
6262 pub fn skip_to_next_break(&mut self) -> Option<ScheduledBreak> {
6363 if let Some(idx) = self.next_break_level_index() {
6464 self.level_elapsed[idx] = self.profile.levels[idx].work_duration;
···7171 self.due_break()
7272 }
73737474- /// Called when a break finishes. Resets only the fired level's counter;
7575- /// all other levels keep their accumulated time.
7474+ // Called when a break finishes. Resets only the fired level's counter;
7575+ // all other levels keep their accumulated time.
7676 pub fn record_break_completed(&mut self, fired_index: usize, was_long: bool) {
7777 let now = Instant::now();
7878···8989 let primary_idx = self.profile.levels.len().saturating_sub(1);
90909191 if fired_index == primary_idx {
9292- let gap_ok = self.last_break_ended.map_or(true, |t| {
9292+ let gap_ok = self.last_break_ended.is_none_or(|t| {
9393 self.profile
9494 .long_break
9595 .as_ref()
9696- .map_or(true, |lb| now.duration_since(t) <= lb.max_cycle_gap)
9696+ .is_none_or(|lb| now.duration_since(t) <= lb.max_cycle_gap)
9797 });
9898 if gap_ok {
9999 self.primary_cycle_count += 1;
···126126 );
127127 }
128128129129- /// Called when idle is detected (natural break). Optionally resets cycle gap.
129129+ // Called when idle is detected (natural break). Optionally resets cycle gap.
130130 pub fn record_idle_break(&mut self, idle_duration: Duration) {
131131- if let Some(lb) = &self.profile.long_break {
132132- if idle_duration >= lb.max_cycle_gap {
133133- self.primary_cycle_count = 0;
134134- }
131131+ if let Some(lb) = &self.profile.long_break
132132+ && idle_duration >= lb.max_cycle_gap
133133+ {
134134+ self.primary_cycle_count = 0;
135135 }
136136 for e in &mut self.level_elapsed {
137137 *e = Duration::ZERO;
···139139 self.last_break_ended = Some(Instant::now());
140140 }
141141142142- /// Seconds until the next break fires (for tray label).
142142+ // Seconds until the next break fires (for tray label).
143143 pub fn secs_until_next_break(&self) -> u64 {
144144- if let Some(lb) = &self.profile.long_break {
145145- if self.primary_cycle_count >= lb.after_cycles {
146146- let primary_idx = self.profile.levels.len() - 1;
147147- let primary_dur = self.profile.primary_level().work_duration;
148148- return primary_dur
149149- .saturating_sub(self.level_elapsed[primary_idx])
150150- .as_secs();
151151- }
144144+ if let Some(lb) = &self.profile.long_break
145145+ && self.primary_cycle_count >= lb.after_cycles
146146+ {
147147+ let primary_idx = self.profile.levels.len() - 1;
148148+ let primary_dur = self.profile.primary_level().work_duration;
149149+ return primary_dur
150150+ .saturating_sub(self.level_elapsed[primary_idx])
151151+ .as_secs();
152152 }
153153 if let Some(idx) = self.next_break_level_index() {
154154 let due = self.profile.levels[idx].work_duration;
···349349 assert_eq!(s.primary_cycle_count(), 3);
350350 let b = s.tick(Duration::from_secs(180)).unwrap();
351351 assert!(b.is_long_break);
352352+ assert_eq!(b.break_duration, Duration::from_secs(600));
353353+ }
354354+355355+ #[test]
356356+ fn skip_to_next_break_targets_soonest_due_level() {
357357+ let mut s = Scheduler::new(make_profile(vec![(60, 15), (180, 60)], None));
358358+359359+ s.tick(Duration::from_secs(50));
360360+ let b = s.skip_to_next_break().expect("skip should fire a break");
361361+362362+ assert_eq!(b.level_index, 0);
363363+ assert_eq!(b.break_duration, Duration::from_secs(15));
364364+ }
365365+366366+ #[test]
367367+ fn skip_to_next_break_can_force_pending_long_break() {
368368+ let mut s = Scheduler::new(make_profile(vec![(60, 15), (180, 60)], Some((2, 600, 600))));
369369+370370+ for _ in 0..2 {
371371+ tick_and_complete(&mut s, 60);
372372+ tick_and_complete(&mut s, 60);
373373+ tick_and_complete(&mut s, 60);
374374+ }
375375+376376+ let b = s
377377+ .skip_to_next_break()
378378+ .expect("skip should fire the pending long break");
379379+380380+ assert!(b.is_long_break);
381381+ assert_eq!(b.level_index, 1);
352382 assert_eq!(b.break_duration, Duration::from_secs(600));
353383 }
354384}