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}