tempo_cli/models/
session.rs

1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5pub enum SessionContext {
6    Terminal,
7    IDE,
8    Linked,
9    Manual,
10}
11
12impl std::fmt::Display for SessionContext {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        match self {
15            SessionContext::Terminal => write!(f, "terminal"),
16            SessionContext::IDE => write!(f, "ide"),
17            SessionContext::Linked => write!(f, "linked"),
18            SessionContext::Manual => write!(f, "manual"),
19        }
20    }
21}
22
23impl std::str::FromStr for SessionContext {
24    type Err = anyhow::Error;
25
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        match s.to_lowercase().as_str() {
28            "terminal" => Ok(SessionContext::Terminal),
29            "ide" => Ok(SessionContext::IDE),
30            "linked" => Ok(SessionContext::Linked),
31            "manual" => Ok(SessionContext::Manual),
32            _ => Err(anyhow::anyhow!("Invalid session context: {}", s)),
33        }
34    }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum SessionStatus {
39    Active,
40    Paused,
41    Completed,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct Session {
46    pub id: Option<i64>,
47    pub project_id: i64,
48    pub start_time: DateTime<Utc>,
49    pub end_time: Option<DateTime<Utc>>,
50    pub context: SessionContext,
51    pub paused_duration: Duration,
52    pub notes: Option<String>,
53    pub created_at: DateTime<Utc>,
54}
55
56impl Session {
57    pub fn new(project_id: i64, context: SessionContext) -> Self {
58        let now = Utc::now();
59        Self {
60            id: None,
61            project_id,
62            start_time: now,
63            end_time: None,
64            context,
65            paused_duration: Duration::zero(),
66            notes: None,
67            created_at: now,
68        }
69    }
70
71    pub fn with_start_time(mut self, start_time: DateTime<Utc>) -> Self {
72        self.start_time = start_time;
73        self
74    }
75
76    pub fn with_notes(mut self, notes: Option<String>) -> Self {
77        self.notes = notes;
78        self
79    }
80
81    pub fn end_session(&mut self) -> anyhow::Result<()> {
82        if self.end_time.is_some() {
83            return Err(anyhow::anyhow!("Session is already ended"));
84        }
85        
86        self.end_time = Some(Utc::now());
87        Ok(())
88    }
89
90    pub fn add_pause_duration(&mut self, duration: Duration) {
91        self.paused_duration = self.paused_duration + duration;
92    }
93
94    pub fn is_active(&self) -> bool {
95        self.end_time.is_none()
96    }
97
98    pub fn status(&self) -> SessionStatus {
99        if self.end_time.is_some() {
100            SessionStatus::Completed
101        } else {
102            SessionStatus::Active
103        }
104    }
105
106    pub fn total_duration(&self) -> Option<Duration> {
107        self.end_time.map(|end| end - self.start_time)
108    }
109
110    pub fn active_duration(&self) -> Option<Duration> {
111        self.total_duration().map(|total| total - self.paused_duration)
112    }
113
114    pub fn current_duration(&self) -> Duration {
115        let end_time = self.end_time.unwrap_or_else(Utc::now);
116        end_time - self.start_time
117    }
118
119    pub fn current_active_duration(&self) -> Duration {
120        self.current_duration() - self.paused_duration
121    }
122
123    pub fn validate(&self) -> anyhow::Result<()> {
124        if let Some(end_time) = self.end_time {
125            if end_time <= self.start_time {
126                return Err(anyhow::anyhow!("End time must be after start time"));
127            }
128        }
129
130        if self.paused_duration < Duration::zero() {
131            return Err(anyhow::anyhow!("Paused duration cannot be negative"));
132        }
133
134        let total = self.current_duration();
135        if self.paused_duration > total {
136            return Err(anyhow::anyhow!("Paused duration cannot exceed total duration"));
137        }
138
139        Ok(())
140    }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct SessionEdit {
145    pub id: Option<i64>,
146    pub session_id: i64,
147    pub original_start_time: DateTime<Utc>,
148    pub original_end_time: Option<DateTime<Utc>>,
149    pub new_start_time: DateTime<Utc>,
150    pub new_end_time: Option<DateTime<Utc>>,
151    pub edit_reason: Option<String>,
152    pub created_at: DateTime<Utc>,
153}
154
155impl SessionEdit {
156    pub fn new(
157        session_id: i64,
158        original_start_time: DateTime<Utc>,
159        original_end_time: Option<DateTime<Utc>>,
160        new_start_time: DateTime<Utc>,
161        new_end_time: Option<DateTime<Utc>>,
162    ) -> Self {
163        Self {
164            id: None,
165            session_id,
166            original_start_time,
167            original_end_time,
168            new_start_time,
169            new_end_time,
170            edit_reason: None,
171            created_at: Utc::now(),
172        }
173    }
174
175    pub fn with_reason(mut self, reason: Option<String>) -> Self {
176        self.edit_reason = reason;
177        self
178    }
179}