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 Subtask {
74    pub id: u64,
75    pub title: String,
76    #[serde(default)]
77    pub done: bool,
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
81#[serde(rename_all = "lowercase")]
82pub enum TaskRecurrence {
83    #[default]
84    None,
85    Daily,
86    Weekly,
87    Weekdays,
88}
89
90impl TaskRecurrence {
91    pub fn label(&self) -> &'static str {
92        match self {
93            TaskRecurrence::None => "None",
94            TaskRecurrence::Daily => "Daily",
95            TaskRecurrence::Weekly => "Weekly",
96            TaskRecurrence::Weekdays => "Weekdays",
97        }
98    }
99
100    pub fn next(self) -> Self {
101        match self {
102            TaskRecurrence::None => TaskRecurrence::Daily,
103            TaskRecurrence::Daily => TaskRecurrence::Weekly,
104            TaskRecurrence::Weekly => TaskRecurrence::Weekdays,
105            TaskRecurrence::Weekdays => TaskRecurrence::None,
106        }
107    }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Task {
112    pub id: u64,
113    pub title: String,
114    pub notes: String,
115    pub priority: Priority,
116    pub status: TaskStatus,
117    pub estimated_minutes: u32,
118    pub created_at: DateTime<Utc>,
119    pub completed_at: Option<DateTime<Utc>>,
120    pub actual_minutes: u32,
121    pub sessions: u32,
122    #[serde(default)]
123    pub tags: Vec<String>,
124    #[serde(default)]
125    pub due_date: Option<String>,
126    #[serde(default)]
127    pub today: bool,
128    #[serde(default)]
129    pub sort_order: u32,
130    #[serde(default)]
131    pub subtasks: Vec<Subtask>,
132    #[serde(default)]
133    pub recurrence: TaskRecurrence,
134    #[serde(default)]
135    pub blocked_by: Vec<u64>,
136    #[serde(default)]
137    pub archived: bool,
138}
139
140impl Task {
141    pub fn new(id: u64, title: String) -> Self {
142        Self {
143            id,
144            title,
145            notes: String::new(),
146            priority: Priority::Medium,
147            status: TaskStatus::Pending,
148            estimated_minutes: 25,
149            created_at: Utc::now(),
150            completed_at: None,
151            actual_minutes: 0,
152            sessions: 0,
153            tags: Vec::new(),
154            due_date: None,
155            today: false,
156            sort_order: id as u32,
157            subtasks: Vec::new(),
158            recurrence: TaskRecurrence::None,
159            blocked_by: Vec::new(),
160            archived: false,
161        }
162    }
163
164    pub fn is_blocked(&self, tasks: &[Task]) -> bool {
165        self.blocked_by.iter().any(|&blocker_id| {
166            tasks
167                .iter()
168                .find(|t| t.id == blocker_id)
169                .is_some_and(|t| t.status != TaskStatus::Done)
170        })
171    }
172
173    pub fn subtask_progress(&self) -> Option<(usize, usize)> {
174        if self.subtasks.is_empty() {
175            return None;
176        }
177        let done = self.subtasks.iter().filter(|s| s.done).count();
178        Some((done, self.subtasks.len()))
179    }
180
181    pub fn is_overdue(&self) -> bool {
182        if self.status == TaskStatus::Done {
183            return false;
184        }
185        let Some(ref due) = self.due_date else {
186            return false;
187        };
188        let today = chrono::Local::now().format("%Y-%m-%d").to_string();
189        due.as_str() < today.as_str()
190    }
191
192    pub fn progress_ratio(&self) -> f64 {
193        if self.estimated_minutes == 0 {
194            return 0.0;
195        }
196        (self.actual_minutes as f64 / self.estimated_minutes as f64).clamp(0.0, 1.0)
197    }
198}
199
200#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
201pub enum TimerState {
202    Idle,
203    Running,
204    Paused,
205    Finished,
206}
207
208#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
209#[serde(rename_all = "lowercase")]
210pub enum TimerMode {
211    Focus,
212    ShortBreak,
213    LongBreak,
214    Custom,
215}
216
217impl TimerMode {
218    pub fn label(&self) -> &'static str {
219        match self {
220            TimerMode::Focus => "FOCUS",
221            TimerMode::ShortBreak => "SHORT BREAK",
222            TimerMode::LongBreak => "LONG BREAK",
223            TimerMode::Custom => "CUSTOM",
224        }
225    }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct FocusSessionRecord {
230    pub date: String,
231    pub minutes: u32,
232    pub task_id: Option<u64>,
233    pub mode: TimerMode,
234    pub completed_at: DateTime<Utc>,
235    #[serde(default)]
236    pub note: String,
237    #[serde(default)]
238    pub tags: Vec<String>,
239    #[serde(default)]
240    pub pause_count: u32,
241    #[serde(default)]
242    pub pause_seconds: u32,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct TimerPreset {
247    pub name: String,
248    pub focus_minutes: u32,
249    pub short_break_minutes: u32,
250    pub long_break_minutes: u32,
251    pub long_break_every: u32,
252}
253
254impl TimerPreset {
255    pub fn deep_work() -> Self {
256        Self {
257            name: "Deep Work 50/10".into(),
258            focus_minutes: 50,
259            short_break_minutes: 10,
260            long_break_minutes: 20,
261            long_break_every: 3,
262        }
263    }
264
265    pub fn quick() -> Self {
266        Self {
267            name: "Quick 15/3".into(),
268            focus_minutes: 15,
269            short_break_minutes: 3,
270            long_break_minutes: 10,
271            long_break_every: 4,
272        }
273    }
274}
275
276#[derive(Debug, Clone)]
277pub struct StoredSession {
278    pub id: i64,
279    pub record: FocusSessionRecord,
280}
281
282#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
283#[serde(rename_all = "kebab-case")]
284pub enum EmptyQueueBehavior {
285    #[default]
286    FreeFocus,
287    PauseTimer,
288    AskEachTime,
289}
290
291impl EmptyQueueBehavior {
292    pub fn label(&self) -> &'static str {
293        match self {
294            EmptyQueueBehavior::FreeFocus => "Free focus",
295            EmptyQueueBehavior::PauseTimer => "Pause timer",
296            EmptyQueueBehavior::AskEachTime => "Ask each time",
297        }
298    }
299
300    pub fn next(self) -> Self {
301        match self {
302            EmptyQueueBehavior::FreeFocus => EmptyQueueBehavior::PauseTimer,
303            EmptyQueueBehavior::PauseTimer => EmptyQueueBehavior::AskEachTime,
304            EmptyQueueBehavior::AskEachTime => EmptyQueueBehavior::FreeFocus,
305        }
306    }
307}
308
309#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
310#[serde(rename_all = "kebab-case")]
311pub enum EstimateCompleteBehavior {
312    #[default]
313    Nudge,
314    None,
315    AutoDone,
316}
317
318impl EstimateCompleteBehavior {
319    pub fn label(&self) -> &'static str {
320        match self {
321            EstimateCompleteBehavior::Nudge => "Nudge",
322            EstimateCompleteBehavior::None => "Off",
323            EstimateCompleteBehavior::AutoDone => "Auto-done",
324        }
325    }
326
327    pub fn next(self) -> Self {
328        match self {
329            EstimateCompleteBehavior::Nudge => EstimateCompleteBehavior::None,
330            EstimateCompleteBehavior::None => EstimateCompleteBehavior::AutoDone,
331            EstimateCompleteBehavior::AutoDone => EstimateCompleteBehavior::Nudge,
332        }
333    }
334}
335
336fn default_focus_minutes() -> u32 {
337    25
338}
339
340fn default_short_break() -> u32 {
341    5
342}
343
344fn default_long_break() -> u32 {
345    15
346}
347
348fn default_long_every() -> u32 {
349    4
350}
351
352fn default_true() -> bool {
353    true
354}
355
356fn default_theme_id() -> String {
357    "matrix".into()
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct AppData {
362    pub tasks: Vec<Task>,
363    pub total_focus_minutes: u32,
364    pub total_sessions: u32,
365    pub streak_days: u32,
366    pub last_session_date: Option<String>,
367    pub daily_goal_minutes: u32,
368    pub sound_enabled: bool,
369    pub auto_start_breaks: bool,
370    pub auto_start_focus: bool,
371    pub next_id: u64,
372    #[serde(default)]
373    pub today_focus_minutes: u32,
374    #[serde(default)]
375    pub today_date: Option<String>,
376    #[serde(default = "default_focus_minutes")]
377    pub focus_minutes: u32,
378    #[serde(default = "default_short_break")]
379    pub short_break_minutes: u32,
380    #[serde(default = "default_long_break")]
381    pub long_break_minutes: u32,
382    #[serde(default = "default_long_every")]
383    pub long_break_every: u32,
384    #[serde(default)]
385    pub session_history: Vec<FocusSessionRecord>,
386    #[serde(default = "default_true")]
387    pub auto_pick_task: bool,
388    #[serde(default)]
389    pub auto_advance_task: bool,
390    #[serde(default = "default_theme_id")]
391    pub theme: String,
392    #[serde(default)]
393    pub active_task_id: Option<u64>,
394    #[serde(default = "default_true")]
395    pub notify_on_finish: bool,
396    #[serde(default)]
397    pub goal_streak_days: u32,
398    #[serde(default)]
399    pub last_goal_date: Option<String>,
400    #[serde(default)]
401    pub empty_queue_behavior: EmptyQueueBehavior,
402    #[serde(default)]
403    pub log_breaks: bool,
404    #[serde(default)]
405    pub estimate_complete: EstimateCompleteBehavior,
406    #[serde(default = "default_true")]
407    pub show_terminal_title: bool,
408    #[serde(default = "default_true")]
409    pub warn_one_minute: bool,
410    #[serde(default)]
411    pub auto_pause_idle_minutes: u32,
412    #[serde(default = "default_archive_days")]
413    pub archive_after_days: u32,
414    #[serde(default)]
415    pub weekly_streak_weeks: u32,
416    #[serde(default)]
417    pub monthly_streak_months: u32,
418    #[serde(default)]
419    pub last_weekly_streak_key: Option<String>,
420    #[serde(default)]
421    pub last_monthly_streak_key: Option<String>,
422    #[serde(default = "default_timer_presets")]
423    pub timer_presets: Vec<TimerPreset>,
424    #[serde(default)]
425    pub active_preset: Option<String>,
426}
427
428fn default_archive_days() -> u32 {
429    30
430}
431
432fn default_timer_presets() -> Vec<TimerPreset> {
433    vec![TimerPreset::deep_work(), TimerPreset::quick()]
434}
435
436impl Default for AppData {
437    fn default() -> Self {
438        Self {
439            tasks: Vec::new(),
440            total_focus_minutes: 0,
441            total_sessions: 0,
442            streak_days: 0,
443            last_session_date: None,
444            daily_goal_minutes: 120,
445            sound_enabled: true,
446            auto_start_breaks: false,
447            auto_start_focus: false,
448            next_id: 1,
449            today_focus_minutes: 0,
450            today_date: None,
451            focus_minutes: 25,
452            short_break_minutes: 5,
453            long_break_minutes: 15,
454            long_break_every: 4,
455            session_history: Vec::new(),
456            auto_pick_task: true,
457            auto_advance_task: false,
458            theme: default_theme_id(),
459            active_task_id: None,
460            notify_on_finish: true,
461            goal_streak_days: 0,
462            last_goal_date: None,
463            empty_queue_behavior: EmptyQueueBehavior::FreeFocus,
464            log_breaks: false,
465            estimate_complete: EstimateCompleteBehavior::Nudge,
466            show_terminal_title: true,
467            warn_one_minute: true,
468            auto_pause_idle_minutes: 0,
469            archive_after_days: default_archive_days(),
470            weekly_streak_weeks: 0,
471            monthly_streak_months: 0,
472            last_weekly_streak_key: None,
473            last_monthly_streak_key: None,
474            timer_presets: default_timer_presets(),
475            active_preset: None,
476        }
477    }
478}