Skip to main content

tickit/
models.rs

1//! Data models for Tickit
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Priority level for tasks
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum Priority {
11    /// Low priority
12    Low,
13    /// Normal/Medium priority (default)
14    #[default]
15    Medium,
16    /// High priority
17    High,
18    /// Urgent priority
19    Urgent,
20}
21
22impl Priority {
23    /// Get all priority levels
24    pub const fn all() -> &'static [Self] {
25        &[Self::Low, Self::Medium, Self::High, Self::Urgent]
26    }
27
28    /// Get the display name
29    pub const fn name(&self) -> &'static str {
30        match self {
31            Self::Low => "Low",
32            Self::Medium => "Medium",
33            Self::High => "High",
34            Self::Urgent => "Urgent",
35        }
36    }
37
38    /// Get the icon for this priority
39    pub const fn icon(&self) -> &'static str {
40        match self {
41            Self::Low => "○",
42            Self::Medium => "◐",
43            Self::High => "●",
44            Self::Urgent => "◉",
45        }
46    }
47
48    /// Get next priority (cycles)
49    pub fn next(&self) -> Self {
50        match self {
51            Self::Low => Self::Medium,
52            Self::Medium => Self::High,
53            Self::High => Self::Urgent,
54            Self::Urgent => Self::Low,
55        }
56    }
57
58    /// Get previous priority (cycles)
59    pub fn prev(&self) -> Self {
60        match self {
61            Self::Low => Self::Urgent,
62            Self::Medium => Self::Low,
63            Self::High => Self::Medium,
64            Self::Urgent => Self::High,
65        }
66    }
67}
68
69impl std::fmt::Display for Priority {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        write!(f, "{}", self.name())
72    }
73}
74
75/// A task/todo item
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct Task {
78    /// Unique identifier
79    pub id: Uuid,
80    /// Task title
81    pub title: String,
82    /// Optional description
83    pub description: Option<String>,
84    /// Optional URL (can be opened in browser)
85    pub url: Option<String>,
86    /// Priority level
87    pub priority: Priority,
88    /// Whether the task is completed
89    pub completed: bool,
90    /// ID of the list this task belongs to
91    pub list_id: Uuid,
92    /// IDs of tags attached to this task
93    pub tag_ids: Vec<Uuid>,
94    /// Creation timestamp
95    pub created_at: DateTime<Utc>,
96    /// Last update timestamp
97    pub updated_at: DateTime<Utc>,
98    /// Completion timestamp (if completed)
99    pub completed_at: Option<DateTime<Utc>>,
100    /// Optional due date
101    pub due_date: Option<DateTime<Utc>>,
102}
103
104impl Task {
105    /// Create a new task with the given title
106    pub fn new(title: impl Into<String>, list_id: Uuid) -> Self {
107        let now = Utc::now();
108        Self {
109            id: Uuid::new_v4(),
110            title: title.into(),
111            description: None,
112            url: None,
113            priority: Priority::default(),
114            completed: false,
115            list_id,
116            tag_ids: Vec::new(),
117            created_at: now,
118            updated_at: now,
119            completed_at: None,
120            due_date: None,
121        }
122    }
123
124    /// Mark the task as completed
125    pub fn complete(&mut self) {
126        self.completed = true;
127        self.completed_at = Some(Utc::now());
128        self.updated_at = Utc::now();
129    }
130
131    /// Mark the task as not completed
132    pub fn uncomplete(&mut self) {
133        self.completed = false;
134        self.completed_at = None;
135        self.updated_at = Utc::now();
136    }
137
138    /// Toggle completion status
139    pub fn toggle(&mut self) {
140        if self.completed {
141            self.uncomplete();
142        } else {
143            self.complete();
144        }
145    }
146
147    /// Set the description
148    pub fn with_description(mut self, description: impl Into<String>) -> Self {
149        self.description = Some(description.into());
150        self
151    }
152
153    /// Set the URL
154    pub fn with_url(mut self, url: impl Into<String>) -> Self {
155        self.url = Some(url.into());
156        self
157    }
158
159    /// Set the priority
160    pub fn with_priority(mut self, priority: Priority) -> Self {
161        self.priority = priority;
162        self
163    }
164
165    /// Add a tag
166    pub fn with_tag(mut self, tag_id: Uuid) -> Self {
167        if !self.tag_ids.contains(&tag_id) {
168            self.tag_ids.push(tag_id);
169        }
170        self
171    }
172
173    /// Set the due date
174    pub fn with_due_date(mut self, due_date: DateTime<Utc>) -> Self {
175        self.due_date = Some(due_date);
176        self
177    }
178}
179
180/// A list/project that contains tasks
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct List {
183    /// Unique identifier
184    pub id: Uuid,
185    /// List name
186    pub name: String,
187    /// Optional description
188    pub description: Option<String>,
189    /// Icon/emoji for the list
190    pub icon: String,
191    /// Color for the list (hex code or name)
192    pub color: Option<String>,
193    /// Whether this is the default inbox list
194    pub is_inbox: bool,
195    /// Creation timestamp
196    pub created_at: DateTime<Utc>,
197    /// Last update timestamp
198    pub updated_at: DateTime<Utc>,
199    /// Sort order
200    pub sort_order: i32,
201}
202
203impl List {
204    /// Create a new list with the given name
205    pub fn new(name: impl Into<String>) -> Self {
206        let now = Utc::now();
207        Self {
208            id: Uuid::new_v4(),
209            name: name.into(),
210            description: None,
211            icon: "📋".to_string(),
212            color: None,
213            is_inbox: false,
214            created_at: now,
215            updated_at: now,
216            sort_order: 0,
217        }
218    }
219
220    /// Create the default Inbox list
221    pub fn inbox() -> Self {
222        let now = Utc::now();
223        Self {
224            id: Uuid::new_v4(),
225            name: "Inbox".to_string(),
226            description: Some("Default list for new tasks".to_string()),
227            icon: "📥".to_string(),
228            color: None,
229            is_inbox: true,
230            created_at: now,
231            updated_at: now,
232            sort_order: -1, // Always first
233        }
234    }
235
236    /// Set the icon
237    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
238        self.icon = icon.into();
239        self
240    }
241
242    /// Set the color
243    pub fn with_color(mut self, color: impl Into<String>) -> Self {
244        self.color = Some(color.into());
245        self
246    }
247
248    /// Set the description
249    pub fn with_description(mut self, description: impl Into<String>) -> Self {
250        self.description = Some(description.into());
251        self
252    }
253}
254
255/// A tag that can be attached to tasks
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct Tag {
258    /// Unique identifier
259    pub id: Uuid,
260    /// Tag name
261    pub name: String,
262    /// Color for the tag (hex code)
263    pub color: String,
264    /// Creation timestamp
265    pub created_at: DateTime<Utc>,
266}
267
268impl Tag {
269    /// Create a new tag with the given name
270    pub fn new(name: impl Into<String>) -> Self {
271        Self {
272            id: Uuid::new_v4(),
273            name: name.into(),
274            color: Self::random_color(),
275            created_at: Utc::now(),
276        }
277    }
278
279    /// Set the color
280    pub fn with_color(mut self, color: impl Into<String>) -> Self {
281        self.color = color.into();
282        self
283    }
284
285    /// Generate a random pleasant color
286    fn random_color() -> String {
287        const COLORS: &[&str] = &[
288            "#f38ba8", // Red
289            "#fab387", // Peach
290            "#f9e2af", // Yellow
291            "#a6e3a1", // Green
292            "#94e2d5", // Teal
293            "#89b4fa", // Blue
294            "#cba6f7", // Mauve
295            "#f5c2e7", // Pink
296            "#eba0ac", // Maroon
297            "#89dceb", // Sky
298        ];
299        use std::time::{SystemTime, UNIX_EPOCH};
300        let seed = SystemTime::now()
301            .duration_since(UNIX_EPOCH)
302            .unwrap()
303            .as_nanos() as usize;
304        COLORS[seed % COLORS.len()].to_string()
305    }
306}
307
308/// Export format for tasks
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(rename_all = "lowercase")]
311pub enum ExportFormat {
312    /// JSON format
313    Json,
314    /// todo.txt format
315    TodoTxt,
316    /// Markdown format
317    Markdown,
318    /// CSV format
319    Csv,
320}
321
322impl ExportFormat {
323    /// Get all export formats
324    pub const fn all() -> &'static [Self] {
325        &[Self::Json, Self::TodoTxt, Self::Markdown, Self::Csv]
326    }
327
328    /// Get the display name
329    pub const fn name(&self) -> &'static str {
330        match self {
331            Self::Json => "JSON",
332            Self::TodoTxt => "todo.txt",
333            Self::Markdown => "Markdown",
334            Self::Csv => "CSV",
335        }
336    }
337
338    /// Get the file extension
339    pub const fn extension(&self) -> &'static str {
340        match self {
341            Self::Json => "json",
342            Self::TodoTxt => "txt",
343            Self::Markdown => "md",
344            Self::Csv => "csv",
345        }
346    }
347}