Skip to main content

void/
model.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(rename_all = "lowercase")]
6pub enum Priority {
7    Low,
8    Medium,
9    High,
10}
11
12impl Priority {
13    pub fn label(&self) -> &'static str {
14        match self {
15            Priority::Low => "Low",
16            Priority::Medium => "Med",
17            Priority::High => "High",
18        }
19    }
20
21    pub fn rank(&self) -> u8 {
22        match self {
23            Priority::High => 3,
24            Priority::Medium => 2,
25            Priority::Low => 1,
26        }
27    }
28}
29
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "lowercase")]
32pub enum TaskStatus {
33    Pending,
34    InProgress,
35    Done,
36}
37
38impl TaskStatus {
39    pub fn label(&self) -> &'static str {
40        match self {
41            TaskStatus::Pending => "Pending",
42            TaskStatus::InProgress => "In Progress",
43            TaskStatus::Done => "Done",
44        }
45    }
46
47    pub fn short_label(&self) -> &'static str {
48        match self {
49            TaskStatus::Pending => "Todo",
50            TaskStatus::InProgress => "Active",
51            TaskStatus::Done => "Done",
52        }
53    }
54
55    pub fn icon(&self) -> &'static str {
56        match self {
57            TaskStatus::Pending => "○",
58            TaskStatus::InProgress => "◉",
59            TaskStatus::Done => "✓",
60        }
61    }
62
63    pub fn bracket_marker(&self) -> &'static str {
64        match self {
65            TaskStatus::Pending => "[ ]",
66            TaskStatus::InProgress => "[~]",
67            TaskStatus::Done => "[x]",
68        }
69    }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct Task {
74    pub id: u64,
75    pub title: String,
76    pub notes: String,
77    pub priority: Priority,
78    pub status: TaskStatus,
79    pub estimated_minutes: u32,
80    pub created_at: DateTime<Utc>,
81    pub completed_at: Option<DateTime<Utc>>,
82    pub actual_minutes: u32,
83    pub sessions: u32,
84    #[serde(default)]
85    pub tags: Vec<String>,
86    #[serde(default)]
87    pub due_date: Option<String>,
88    #[serde(default)]
89    pub today: bool,
90    #[serde(default)]
91    pub sort_order: u32,
92}
93
94impl Task {
95    pub fn new(id: u64, title: String) -> Self {
96        Self {
97            id,
98            title,
99            notes: String::new(),
100            priority: Priority::Medium,
101            status: TaskStatus::Pending,
102            estimated_minutes: 25,
103            created_at: Utc::now(),
104            completed_at: None,
105            actual_minutes: 0,
106            sessions: 0,
107            tags: Vec::new(),
108            due_date: None,
109            today: false,
110            sort_order: id as u32,
111        }
112    }
113
114    pub fn is_overdue(&self) -> bool {
115        if self.status == TaskStatus::Done {
116            return false;
117        }
118        let Some(ref due) = self.due_date else {
119            return false;
120        };
121        let today = chrono::Local::now().format("%Y-%m-%d").to_string();
122        due.as_str() < today.as_str()
123    }
124
125    pub fn progress_ratio(&self) -> f64 {
126        if self.estimated_minutes == 0 {
127            return 0.0;
128        }
129        (self.actual_minutes as f64 / self.estimated_minutes as f64).clamp(0.0, 1.0)
130    }
131}
132
133#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
134pub enum TimerState {
135    Idle,
136    Running,
137    Paused,
138    Finished,
139}
140
141#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
142#[serde(rename_all = "lowercase")]
143pub enum TimerMode {
144    Focus,
145    ShortBreak,
146    LongBreak,
147    Custom,
148}
149
150impl TimerMode {
151    pub fn label(&self) -> &'static str {
152        match self {
153            TimerMode::Focus => "FOCUS",
154            TimerMode::ShortBreak => "SHORT BREAK",
155            TimerMode::LongBreak => "LONG BREAK",
156            TimerMode::Custom => "CUSTOM",
157        }
158    }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct FocusSessionRecord {
163    pub date: String,
164    pub minutes: u32,
165    pub task_id: Option<u64>,
166    pub mode: TimerMode,
167    pub completed_at: DateTime<Utc>,
168}
169
170#[derive(Debug, Clone)]
171pub struct StoredSession {
172    pub id: i64,
173    pub record: FocusSessionRecord,
174}
175
176#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
177#[serde(rename_all = "kebab-case")]
178pub enum EmptyQueueBehavior {
179    #[default]
180    FreeFocus,
181    PauseTimer,
182    AskEachTime,
183}
184
185impl EmptyQueueBehavior {
186    pub fn label(&self) -> &'static str {
187        match self {
188            EmptyQueueBehavior::FreeFocus => "Free focus",
189            EmptyQueueBehavior::PauseTimer => "Pause timer",
190            EmptyQueueBehavior::AskEachTime => "Ask each time",
191        }
192    }
193
194    pub fn next(self) -> Self {
195        match self {
196            EmptyQueueBehavior::FreeFocus => EmptyQueueBehavior::PauseTimer,
197            EmptyQueueBehavior::PauseTimer => EmptyQueueBehavior::AskEachTime,
198            EmptyQueueBehavior::AskEachTime => EmptyQueueBehavior::FreeFocus,
199        }
200    }
201}
202
203#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
204#[serde(rename_all = "kebab-case")]
205pub enum EstimateCompleteBehavior {
206    #[default]
207    Nudge,
208    None,
209    AutoDone,
210}
211
212impl EstimateCompleteBehavior {
213    pub fn label(&self) -> &'static str {
214        match self {
215            EstimateCompleteBehavior::Nudge => "Nudge",
216            EstimateCompleteBehavior::None => "Off",
217            EstimateCompleteBehavior::AutoDone => "Auto-done",
218        }
219    }
220
221    pub fn next(self) -> Self {
222        match self {
223            EstimateCompleteBehavior::Nudge => EstimateCompleteBehavior::None,
224            EstimateCompleteBehavior::None => EstimateCompleteBehavior::AutoDone,
225            EstimateCompleteBehavior::AutoDone => EstimateCompleteBehavior::Nudge,
226        }
227    }
228}
229
230fn default_focus_minutes() -> u32 {
231    25
232}
233
234fn default_short_break() -> u32 {
235    5
236}
237
238fn default_long_break() -> u32 {
239    15
240}
241
242fn default_long_every() -> u32 {
243    4
244}
245
246fn default_true() -> bool {
247    true
248}
249
250fn default_theme_id() -> String {
251    "matrix".into()
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct AppData {
256    pub tasks: Vec<Task>,
257    pub total_focus_minutes: u32,
258    pub total_sessions: u32,
259    pub streak_days: u32,
260    pub last_session_date: Option<String>,
261    pub daily_goal_minutes: u32,
262    pub sound_enabled: bool,
263    pub auto_start_breaks: bool,
264    pub auto_start_focus: bool,
265    pub next_id: u64,
266    #[serde(default)]
267    pub today_focus_minutes: u32,
268    #[serde(default)]
269    pub today_date: Option<String>,
270    #[serde(default = "default_focus_minutes")]
271    pub focus_minutes: u32,
272    #[serde(default = "default_short_break")]
273    pub short_break_minutes: u32,
274    #[serde(default = "default_long_break")]
275    pub long_break_minutes: u32,
276    #[serde(default = "default_long_every")]
277    pub long_break_every: u32,
278    #[serde(default)]
279    pub session_history: Vec<FocusSessionRecord>,
280    #[serde(default = "default_true")]
281    pub auto_pick_task: bool,
282    #[serde(default)]
283    pub auto_advance_task: bool,
284    #[serde(default = "default_theme_id")]
285    pub theme: String,
286    #[serde(default)]
287    pub active_task_id: Option<u64>,
288    #[serde(default = "default_true")]
289    pub notify_on_finish: bool,
290    #[serde(default)]
291    pub goal_streak_days: u32,
292    #[serde(default)]
293    pub last_goal_date: Option<String>,
294    #[serde(default)]
295    pub empty_queue_behavior: EmptyQueueBehavior,
296    #[serde(default)]
297    pub log_breaks: bool,
298    #[serde(default)]
299    pub estimate_complete: EstimateCompleteBehavior,
300}
301
302impl Default for AppData {
303    fn default() -> Self {
304        Self {
305            tasks: Vec::new(),
306            total_focus_minutes: 0,
307            total_sessions: 0,
308            streak_days: 0,
309            last_session_date: None,
310            daily_goal_minutes: 120,
311            sound_enabled: true,
312            auto_start_breaks: false,
313            auto_start_focus: false,
314            next_id: 1,
315            today_focus_minutes: 0,
316            today_date: None,
317            focus_minutes: 25,
318            short_break_minutes: 5,
319            long_break_minutes: 15,
320            long_break_every: 4,
321            session_history: Vec::new(),
322            auto_pick_task: true,
323            auto_advance_task: false,
324            theme: default_theme_id(),
325            active_task_id: None,
326            notify_on_finish: true,
327            goal_streak_days: 0,
328            last_goal_date: None,
329            empty_queue_behavior: EmptyQueueBehavior::FreeFocus,
330            log_breaks: false,
331            estimate_complete: EstimateCompleteBehavior::Nudge,
332        }
333    }
334}