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}