void-focus 0.3.0-alpha.5

A feature-rich terminal focus timer with task tracking
Documentation
use super::*;
use crate::model::TimerMode;

impl App {
    pub(crate) fn maybe_complete_task_estimate(&mut self, task_id: Option<u64>) {
        let Some(id) = task_id else {
            return;
        };
        let estimated = self
            .data
            .tasks
            .iter()
            .find(|t| t.id == id)
            .map(|t| (t.title.clone(), t.actual_minutes, t.estimated_minutes));
        let Some((title, actual, estimate)) = estimated else {
            return;
        };
        if actual < estimate {
            return;
        }
        match self.data.estimate_complete {
            EstimateCompleteBehavior::Nudge => {
                self.set_status(
                    format!("Estimate reached for \"{title}\" — mark done?"),
                    false,
                );
            }
            EstimateCompleteBehavior::AutoDone => {
                self.persist_data(|db, data| storage::mark_task_done(db, data, id));
                if self.active_task == Some(id) {
                    self.active_task = None;
                    self.data.active_task_id = None;
                    self.persist(|db| db.persist_active_task(None));
                }
                self.bump_data();
                self.set_status(format!("\"{title}\" auto-completed (estimate met)."), false);
                self.check_queue_empty();
            }
            EstimateCompleteBehavior::None => {}
        }
    }

    pub fn end_session(&mut self) {
        if self.timer.state == TimerState::Running {
            self.pause_timer();
        }
        let today = storage::today_focus_minutes(&self.data);
        let goal = self.data.daily_goal_minutes;
        let queue_note = if self.queue_empty() {
            "all tasks done"
        } else {
            "tasks remain"
        };
        self.set_status(
            format!(
                "Session ended — today {}/{} min · goal streak {} days · {queue_note}",
                today, goal, self.data.goal_streak_days
            ),
            false,
        );
    }

    pub fn on_tick(&mut self) {
        let _ = storage::ensure_today_reset(&self.db, &mut self.data);
        if self.data.auto_pause_idle_minutes > 0
            && self.timer.state == TimerState::Running
            && self.last_activity.elapsed()
                > Duration::from_secs(self.data.auto_pause_idle_minutes as u64 * 60)
        {
            self.pause_timer();
            self.set_status("Auto-paused — terminal idle.", false);
        }
        if self.data.warn_one_minute
            && self.timer.is_one_minute_warning()
            && !self.end_warning_shown
        {
            self.end_warning_shown = true;
            if self.data.sound_enabled {
                sound::play_pause();
            }
            self.set_status("1 minute remaining!", false);
        }
        if !self.timer.is_one_minute_warning() {
            self.end_warning_shown = false;
        }
        let just_finished = self.timer.tick();
        if just_finished {
            self.on_timer_finished(false);
        }
        if self.status.is_some()
            && !self.status_error
            && self.last_status_set.elapsed() > Duration::from_secs(4)
        {
            self.status = None;
        }
    }

    pub(crate) fn on_timer_finished(&mut self, skipped: bool) {
        let mode = self.timer.mode;
        if mode == TimerMode::Focus {
            let mins = self.elapsed_minutes(skipped);
            let task_id = self.active_task;
            let meta = self.timer.session_meta();
            self.persist_data(|db, data| {
                storage::record_focus_session_with_meta(db, data, mins, task_id, mode, meta)
            });
            self.maybe_complete_task_estimate(task_id);
            if self.data.sound_enabled {
                sound::play_finish();
            }
            if self.data.notify_on_finish {
                let msg = if skipped {
                    format!("Logged {} min (skipped early)", mins)
                } else {
                    format!("+{} min logged — time for a break", mins)
                };
                let kind = if skipped {
                    sound::NotifyKind::SessionSkipped
                } else {
                    sound::NotifyKind::FocusComplete
                };
                sound::notify_typed(kind, "Void · Focus complete", &msg);
            }
            self.set_status(
                format!(
                    "Focus {}: +{} min",
                    if skipped { "skipped" } else { "complete" },
                    mins
                ),
                false,
            );
            self.maybe_advance_task();
            self.bump_data();
            if !skipped {
                self.persist_timer_state();
            }
            self.timer.reset_session_pauses();
            self.advance_to_break();
        } else if mode == TimerMode::Custom {
            let mins = self.elapsed_minutes(skipped);
            let task_id = self.active_task;
            let meta = self.timer.session_meta();
            self.persist_data(|db, data| {
                storage::record_focus_session_with_meta(db, data, mins, task_id, mode, meta)
            });
            if self.data.sound_enabled {
                sound::play_finish();
            }
            self.set_status(format!("Custom session complete: +{} min", mins), false);
            self.bump_data();
            self.timer.configure(TimerMode::Focus);
            self.timer.reset_session_pauses();
            self.persist_timer_state();
        } else {
            let break_mins = self.elapsed_minutes(false);
            self.persist_data(|db, data| storage::record_break_session(db, data, mode, break_mins));
            if self.data.sound_enabled {
                sound::play_finish();
            }
            if self.data.notify_on_finish {
                sound::notify_typed(
                    sound::NotifyKind::BreakComplete,
                    "Void · Break over",
                    "Break finished — ready to focus again",
                );
            }
            self.set_status("Break finished. Ready for focus.", false);
            self.bump_data();
            self.advance_to_focus();
        }
    }

    pub(crate) fn advance_to_break(&mut self) {
        let long_break = self.timer.completed_focus_sessions > 0
            && self
                .timer
                .completed_focus_sessions
                .is_multiple_of(self.timer.config.long_break_every);
        let next = if long_break {
            TimerMode::LongBreak
        } else {
            TimerMode::ShortBreak
        };
        self.timer.configure(next);
        self.persist_timer_state();
        if self.data.auto_start_breaks {
            self.timer.start();
            if self.data.sound_enabled {
                sound::play_start();
            }
            self.set_status(format!("{} started.", next.label()), false);
        }
    }

    pub(crate) fn advance_to_focus(&mut self) {
        self.timer.configure(TimerMode::Focus);
        self.persist_timer_state();
        if self.queue_empty() && self.data.empty_queue_behavior == EmptyQueueBehavior::PauseTimer {
            self.set_status("All tasks done — timer waiting. [E] end session", false);
            return;
        }
        self.auto_pick_task_if_needed();
        if self.data.auto_start_focus {
            self.timer.start();
            if self.data.sound_enabled {
                sound::play_start();
            }
            self.set_status("Focus started.", false);
        }
    }

    pub fn toggle_timer(&mut self) {
        if self.timer.state == TimerState::Running {
            self.pause_timer();
        } else {
            self.start_timer();
        }
    }

    pub fn start_timer(&mut self) {
        if self.timer.state == TimerState::Running {
            return;
        }
        if self.timer.state == TimerState::Finished {
            self.timer.reset();
        }
        if self.timer.mode == TimerMode::Focus {
            self.auto_pick_task_if_needed();
        }
        self.timer.start();
        self.end_warning_shown = false;
        if self.data.sound_enabled {
            sound::play_start();
        }
        self.set_status("Timer started.", false);
    }

    pub fn pause_timer(&mut self) {
        if self.timer.state != TimerState::Running {
            return;
        }
        let elapsed = self.timer.current_elapsed_seconds();
        self.timer.pause();
        if self.data.sound_enabled {
            sound::play_pause();
        }
        let active_minutes = (elapsed / 60).max(1);
        self.set_status(
            format!(
                "Paused at {} ({} min in).",
                self.timer.format_remaining(),
                active_minutes
            ),
            false,
        );
    }

    pub fn reset_timer(&mut self) {
        self.timer.reset();
        self.set_status("Timer reset.", false);
    }

    pub fn cycle_mode(&mut self) {
        if self.timer.state == TimerState::Running || self.timer.state == TimerState::Paused {
            self.set_status("Stop the timer before changing mode.", true);
            return;
        }
        let next = match self.timer.mode {
            TimerMode::Focus => TimerMode::ShortBreak,
            TimerMode::ShortBreak => TimerMode::LongBreak,
            TimerMode::LongBreak => TimerMode::Custom,
            TimerMode::Custom => TimerMode::Focus,
        };
        self.timer.configure(next);
        self.set_status(format!("Mode: {}", next.label()), false);
    }

    pub fn cycle_timer_preset(&mut self) {
        if self.timer.state == TimerState::Running || self.timer.state == TimerState::Paused {
            self.set_status("Stop the timer before switching preset.", true);
            return;
        }
        if let Some(preset) = storage::cycle_timer_preset(&mut self.data) {
            self.timer
                .sync_config(TimerConfig::from_app_data(&self.data));
            if let Err(e) = self.db.persist_timer_settings(&self.data) {
                self.set_status(format!("Save error: {e}"), true);
            }
            self.persist_setting(
                "active_preset",
                self.data.active_preset.clone().unwrap_or_default(),
            );
            self.set_status(format!("Preset: {}", preset.name), false);
        }
    }

    pub fn adjust_minutes(&mut self, delta: i32) {
        if self.timer.state == TimerState::Running || self.timer.state == TimerState::Paused {
            self.set_status("Stop the timer before adjusting duration.", true);
            return;
        }
        match self.timer.mode {
            TimerMode::Focus => {
                let cur = self.timer.config.focus_minutes as i32 + delta;
                let v = cur.clamp(1, 240) as u32;
                self.timer.set_focus_minutes(v);
                self.data.focus_minutes = v;
            }
            TimerMode::ShortBreak => {
                let cur = self.timer.config.short_break_minutes as i32 + delta;
                let v = cur.clamp(1, 60) as u32;
                self.timer.config.short_break_minutes = v;
                self.data.short_break_minutes = v;
                self.timer.total_seconds = self.timer.duration_seconds();
            }
            TimerMode::LongBreak => {
                let cur = self.timer.config.long_break_minutes as i32 + delta;
                let v = cur.clamp(1, 120) as u32;
                self.timer.config.long_break_minutes = v;
                self.data.long_break_minutes = v;
                self.timer.total_seconds = self.timer.duration_seconds();
            }
            TimerMode::Custom => {
                let cur = self.timer.custom_minutes as i32 + delta;
                let v = cur.clamp(1, 240) as u32;
                self.timer.set_custom_minutes(v);
            }
        }
        if let Err(e) = self.db.persist_timer_settings(&self.data) {
            self.set_status(format!("Save error: {e}"), true);
        }
        self.set_status(
            format!(
                "{} length: {} min",
                self.timer.mode.label(),
                self.timer.duration_seconds() / 60
            ),
            false,
        );
    }
}