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
250#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
251#[serde(rename_all = "kebab-case")]
252pub enum ThemeVariant {
253    Dark,
254    Light,
255    Polaris,
256    #[default]
257    Matrix,
258}
259
260impl ThemeVariant {
261    pub fn label(&self) -> &'static str {
262        match self {
263            ThemeVariant::Dark => "Dark",
264            ThemeVariant::Light => "Light",
265            ThemeVariant::Polaris => "Polaris",
266            ThemeVariant::Matrix => "Matrix",
267        }
268    }
269
270    pub fn next(self) -> Self {
271        match self {
272            ThemeVariant::Dark => ThemeVariant::Light,
273            ThemeVariant::Light => ThemeVariant::Polaris,
274            ThemeVariant::Polaris => ThemeVariant::Matrix,
275            ThemeVariant::Matrix => ThemeVariant::Dark,
276        }
277    }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct AppData {
282    pub tasks: Vec<Task>,
283    pub total_focus_minutes: u32,
284    pub total_sessions: u32,
285    pub streak_days: u32,
286    pub last_session_date: Option<String>,
287    pub daily_goal_minutes: u32,
288    pub sound_enabled: bool,
289    pub auto_start_breaks: bool,
290    pub auto_start_focus: bool,
291    pub next_id: u64,
292    #[serde(default)]
293    pub today_focus_minutes: u32,
294    #[serde(default)]
295    pub today_date: Option<String>,
296    #[serde(default = "default_focus_minutes")]
297    pub focus_minutes: u32,
298    #[serde(default = "default_short_break")]
299    pub short_break_minutes: u32,
300    #[serde(default = "default_long_break")]
301    pub long_break_minutes: u32,
302    #[serde(default = "default_long_every")]
303    pub long_break_every: u32,
304    #[serde(default)]
305    pub session_history: Vec<FocusSessionRecord>,
306    #[serde(default = "default_true")]
307    pub auto_pick_task: bool,
308    #[serde(default)]
309    pub auto_advance_task: bool,
310    #[serde(default)]
311    pub theme: ThemeVariant,
312    #[serde(default)]
313    pub active_task_id: Option<u64>,
314    #[serde(default = "default_true")]
315    pub notify_on_finish: bool,
316    #[serde(default)]
317    pub goal_streak_days: u32,
318    #[serde(default)]
319    pub last_goal_date: Option<String>,
320    #[serde(default)]
321    pub empty_queue_behavior: EmptyQueueBehavior,
322    #[serde(default)]
323    pub log_breaks: bool,
324    #[serde(default)]
325    pub estimate_complete: EstimateCompleteBehavior,
326}
327
328impl Default for AppData {
329    fn default() -> Self {
330        Self {
331            tasks: Vec::new(),
332            total_focus_minutes: 0,
333            total_sessions: 0,
334            streak_days: 0,
335            last_session_date: None,
336            daily_goal_minutes: 120,
337            sound_enabled: true,
338            auto_start_breaks: false,
339            auto_start_focus: false,
340            next_id: 1,
341            today_focus_minutes: 0,
342            today_date: None,
343            focus_minutes: 25,
344            short_break_minutes: 5,
345            long_break_minutes: 15,
346            long_break_every: 4,
347            session_history: Vec::new(),
348            auto_pick_task: true,
349            auto_advance_task: false,
350            theme: ThemeVariant::Matrix,
351            active_task_id: None,
352            notify_on_finish: true,
353            goal_streak_days: 0,
354            last_goal_date: None,
355            empty_queue_behavior: EmptyQueueBehavior::FreeFocus,
356            log_breaks: false,
357            estimate_complete: EstimateCompleteBehavior::Nudge,
358        }
359    }
360}