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}