tui_breath 0.4.0

Terminal breathing guide built with Rust + Ratatui. 6 patterns, breath hold, workout mode, smooth animations, JSON session tracking.
use chrono::{DateTime, Utc};
use uuid::Uuid;

use super::breathing::BreathingEngine;
use super::hold::{best_hold_seconds, BreathHoldAttempt};

#[derive(Clone, Debug)]
pub struct SessionManager {
    pub session_id: Uuid,
    pub start_time: DateTime<Utc>,
    pub engine: BreathingEngine,
    pub events: Vec<SessionEvent>,
    pub hold_attempts: Vec<BreathHoldAttempt>,
    final_status: Option<SessionOutcome>,
}

#[derive(Debug, Clone)]
pub struct SessionEvent {
    pub timestamp: DateTime<Utc>,
    pub event: EventKind,
    pub details: String,
}

#[derive(Debug, Clone)]
pub enum EventKind {
    Start,
    Pause,
    Resume,
    HoldStart,
    HoldEnd,
    Complete,
    Abandon,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionOutcome {
    Completed,
    Abandoned,
}

impl SessionManager {
    pub fn new(pattern: &'static super::patterns::Pattern, duration_secs: f64, tempo: f64) -> Self {
        let session_id = Uuid::new_v4();
        let start_time = Utc::now();

        let mut events = Vec::new();
        events.push(SessionEvent {
            timestamp: start_time,
            event: EventKind::Start,
            details: format!(
                "Session started: {} at tempo {}",
                pattern.display_name, tempo
            ),
        });

        Self {
            session_id,
            start_time,
            engine: BreathingEngine::new(pattern, tempo, duration_secs),
            events,
            hold_attempts: Vec::new(),
            final_status: None,
        }
    }

    pub fn toggle_pause(&mut self) {
        self.engine.toggle_pause();
        let event_kind = if self.engine.is_paused {
            EventKind::Pause
        } else {
            EventKind::Resume
        };

        self.events.push(SessionEvent {
            timestamp: Utc::now(),
            event: event_kind,
            details: format!("Paused at {:?}", self.engine.total_elapsed_secs),
        });
    }

    pub fn start_hold(&mut self, timestamp: DateTime<Utc>) {
        self.events.push(SessionEvent {
            timestamp,
            event: EventKind::HoldStart,
            details: format!(
                "Breath hold started at {:.1}s",
                self.engine.total_elapsed_secs
            ),
        });
    }

    pub fn finish_hold(&mut self, attempt: BreathHoldAttempt) {
        self.events.push(SessionEvent {
            timestamp: attempt.ended_at,
            event: EventKind::HoldEnd,
            details: format!("Breath hold ended at {:.1}s", attempt.duration_secs),
        });
        self.hold_attempts.push(attempt);
    }

    pub fn best_hold_seconds(&self) -> Option<f64> {
        best_hold_seconds(&self.hold_attempts)
    }

    pub fn hold_attempt_count(&self) -> u32 {
        self.hold_attempts.len() as u32
    }

    pub fn session_status(&self) -> Option<SessionOutcome> {
        self.final_status
    }

    pub fn complete(&mut self) {
        if self.final_status.is_some() {
            return;
        }

        self.final_status = Some(SessionOutcome::Completed);
        self.events.push(SessionEvent {
            timestamp: Utc::now(),
            event: EventKind::Complete,
            details: format!(
                "Session completed after {:?}s",
                self.engine.total_elapsed_secs
            ),
        });
    }

    pub fn abandon(&mut self) {
        if self.final_status.is_some() {
            return;
        }

        self.final_status = Some(SessionOutcome::Abandoned);
        self.events.push(SessionEvent {
            timestamp: Utc::now(),
            event: EventKind::Abandon,
            details: format!(
                "Session abandoned after {:?}s",
                self.engine.total_elapsed_secs
            ),
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::engine::PATTERNS;

    #[test]
    fn tracks_multiple_hold_attempts_and_best_duration() {
        let mut manager = SessionManager::new(&PATTERNS[0], 60.0, 1.0);
        let now = Utc::now();

        manager.start_hold(now);
        manager.finish_hold(BreathHoldAttempt {
            started_at: now,
            ended_at: now,
            duration_secs: 18.2,
        });
        manager.finish_hold(BreathHoldAttempt {
            started_at: now,
            ended_at: now,
            duration_secs: 24.4,
        });

        assert_eq!(manager.hold_attempt_count(), 2);
        assert_eq!(manager.best_hold_seconds(), Some(24.4));
    }

    #[test]
    fn preserves_final_status_once_set() {
        let mut manager = SessionManager::new(&PATTERNS[0], 60.0, 1.0);
        manager.abandon();
        manager.complete();

        assert_eq!(manager.session_status(), Some(SessionOutcome::Abandoned));
    }
}