Skip to main content

max / goingson

Apply rustfmt across the crate Formatting only, no behavior change. cargo check --all-targets passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-21 23:46 UTC
Commit: 83b6eb65b6479af28148d1b8ae86ee4574b62938
Parent: 189683c
187 files changed, +11894 insertions, -5841 deletions
@@ -6,11 +6,11 @@
6 6 //! and sub-collections are written verbatim; this module only defines the types that
7 7 //! cross the command -> storage boundary.
8 8
9 + use crate::Contact;
9 10 use crate::models::{
10 11 Attachment, DailyNote, Email, Event, Milestone, MonthlyGoal, MonthlyReflection, Project,
11 12 SavedView, SyncAccount, Task, TimeSession, WeeklyReview,
12 13 };
13 - use crate::Contact;
14 14
15 15 /// Result of a restore operation.
16 16 #[derive(Debug, Default)]
@@ -4,9 +4,9 @@
4 4 //! and social handles. Sub-collections are stored in separate tables to
5 5 //! enable querying by email address for future integration features.
6 6
7 + use crate::id_types::{ContactEmailId, ContactId, ContactPhoneId, CustomFieldId, SocialHandleId};
7 8 use chrono::{DateTime, NaiveDate, Utc};
8 9 use serde::{Deserialize, Serialize};
9 - use crate::id_types::{ContactId, ContactEmailId, ContactPhoneId, SocialHandleId, CustomFieldId};
10 10
11 11 // ============ Main Entity ============
12 12
@@ -17,11 +17,27 @@ use crate::constants::{DEFAULT_PARSE_HOUR, DEFAULT_PARSE_MINUTE};
17 17
18 18 /// Weekday names indexed Sunday=0..Saturday=6 (matches JS `Date.getDay`).
19 19 const WEEKDAYS: [&str; 7] = [
20 - "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday",
20 + "sunday",
21 + "monday",
22 + "tuesday",
23 + "wednesday",
24 + "thursday",
25 + "friday",
26 + "saturday",
21 27 ];
22 28 const MONTHS: [&str; 12] = [
23 - "january", "february", "march", "april", "may", "june", "july", "august",
24 - "september", "october", "november", "december",
29 + "january",
30 + "february",
31 + "march",
32 + "april",
33 + "may",
34 + "june",
35 + "july",
36 + "august",
37 + "september",
38 + "october",
39 + "november",
40 + "december",
25 41 ];
26 42 const MONTH_ABBRS: [&str; 12] = [
27 43 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
@@ -102,10 +118,16 @@ fn parse_iso_datetime(s: &str) -> Option<NaiveDateTime> {
102 118 }
103 119
104 120 /// Resolve the next occurrence of a weekday, with optional `next` prefix and time.
105 - fn parse_weekday(tokens: &[&str], now: NaiveDateTime, default_time: NaiveTime) -> Option<NaiveDateTime> {
121 + fn parse_weekday(
122 + tokens: &[&str],
123 + now: NaiveDateTime,
124 + default_time: NaiveTime,
125 + ) -> Option<NaiveDateTime> {
106 126 let is_next = tokens.first() == Some(&"next");
107 127 let name_idx = if is_next { 1 } else { 0 };
108 - let target = WEEKDAYS.iter().position(|d| Some(d) == tokens.get(name_idx))? as i64;
128 + let target = WEEKDAYS
129 + .iter()
130 + .position(|d| Some(d) == tokens.get(name_idx))? as i64;
109 131
110 132 let today = now.date();
111 133 let current = today.weekday().num_days_from_sunday() as i64;
@@ -124,7 +146,11 @@ fn parse_weekday(tokens: &[&str], now: NaiveDateTime, default_time: NaiveTime) -
124 146 }
125 147
126 148 /// Resolve a "month day" reference, rolling to next year if already past.
127 - fn parse_month_day(tokens: &[&str], now: NaiveDateTime, default_time: NaiveTime) -> Option<NaiveDateTime> {
149 + fn parse_month_day(
150 + tokens: &[&str],
151 + now: NaiveDateTime,
152 + default_time: NaiveTime,
153 + ) -> Option<NaiveDateTime> {
128 154 if tokens.len() < 2 {
129 155 return None;
130 156 }
@@ -209,9 +235,33 @@ mod tests {
209 235
210 236 #[test]
211 237 fn relative_keywords() {
212 - assert_eq!(parse("today"), Some(NaiveDate::from_ymd_opt(2026, 6, 17).unwrap().and_hms_opt(9, 0, 0).unwrap()));
213 - assert_eq!(parse("tomorrow"), Some(NaiveDate::from_ymd_opt(2026, 6, 18).unwrap().and_hms_opt(9, 0, 0).unwrap()));
214 - assert_eq!(parse("yesterday"), Some(NaiveDate::from_ymd_opt(2026, 6, 16).unwrap().and_hms_opt(9, 0, 0).unwrap()));
238 + assert_eq!(
239 + parse("today"),
240 + Some(
241 + NaiveDate::from_ymd_opt(2026, 6, 17)
242 + .unwrap()
243 + .and_hms_opt(9, 0, 0)
244 + .unwrap()
245 + )
246 + );
247 + assert_eq!(
248 + parse("tomorrow"),
249 + Some(
250 + NaiveDate::from_ymd_opt(2026, 6, 18)
251 + .unwrap()
252 + .and_hms_opt(9, 0, 0)
253 + .unwrap()
254 + )
255 + );
256 + assert_eq!(
257 + parse("yesterday"),
258 + Some(
259 + NaiveDate::from_ymd_opt(2026, 6, 16)
260 + .unwrap()
261 + .and_hms_opt(9, 0, 0)
262 + .unwrap()
263 + )
264 + );
215 265 }
216 266
217 267 #[test]
@@ -226,11 +276,21 @@ mod tests {
226 276 fn iso_datetime_preserves_wall_clock() {
227 277 assert_eq!(
228 278 parse("2026-12-25T15:30"),
229 - Some(NaiveDate::from_ymd_opt(2026, 12, 25).unwrap().and_hms_opt(15, 30, 0).unwrap())
279 + Some(
280 + NaiveDate::from_ymd_opt(2026, 12, 25)
281 + .unwrap()
282 + .and_hms_opt(15, 30, 0)
283 + .unwrap()
284 + )
230 285 );
231 286 assert_eq!(
232 287 parse("2026-12-25 15:30"),
233 - Some(NaiveDate::from_ymd_opt(2026, 12, 25).unwrap().and_hms_opt(15, 30, 0).unwrap())
288 + Some(
289 + NaiveDate::from_ymd_opt(2026, 12, 25)
290 + .unwrap()
291 + .and_hms_opt(15, 30, 0)
292 + .unwrap()
293 + )
234 294 );
235 295 }
236 296
@@ -238,50 +298,140 @@ mod tests {
238 298 fn iso_date_defaults_to_nine_am() {
239 299 assert_eq!(
240 300 parse("2026-12-25"),
241 - Some(NaiveDate::from_ymd_opt(2026, 12, 25).unwrap().and_hms_opt(9, 0, 0).unwrap())
301 + Some(
302 + NaiveDate::from_ymd_opt(2026, 12, 25)
303 + .unwrap()
304 + .and_hms_opt(9, 0, 0)
305 + .unwrap()
306 + )
242 307 );
243 308 assert_eq!(
244 309 parse("2026-12-25 3pm"),
245 - Some(NaiveDate::from_ymd_opt(2026, 12, 25).unwrap().and_hms_opt(15, 0, 0).unwrap())
310 + Some(
311 + NaiveDate::from_ymd_opt(2026, 12, 25)
312 + .unwrap()
313 + .and_hms_opt(15, 0, 0)
314 + .unwrap()
315 + )
246 316 );
247 317 }
248 318
249 319 #[test]
250 320 fn in_n_days() {
251 - assert_eq!(parse("in 3 days"), Some(NaiveDate::from_ymd_opt(2026, 6, 20).unwrap().and_hms_opt(9, 0, 0).unwrap()));
252 - assert_eq!(parse("in 1 day"), Some(NaiveDate::from_ymd_opt(2026, 6, 18).unwrap().and_hms_opt(9, 0, 0).unwrap()));
321 + assert_eq!(
322 + parse("in 3 days"),
323 + Some(
324 + NaiveDate::from_ymd_opt(2026, 6, 20)
325 + .unwrap()
326 + .and_hms_opt(9, 0, 0)
327 + .unwrap()
328 + )
329 + );
330 + assert_eq!(
331 + parse("in 1 day"),
332 + Some(
333 + NaiveDate::from_ymd_opt(2026, 6, 18)
334 + .unwrap()
335 + .and_hms_opt(9, 0, 0)
336 + .unwrap()
337 + )
338 + );
253 339 }
254 340
255 341 #[test]
256 342 fn next_week_is_next_monday() {
257 343 // 2026-06-17 is a Wednesday; next Monday is 2026-06-22.
258 - assert_eq!(parse("next week"), Some(NaiveDate::from_ymd_opt(2026, 6, 22).unwrap().and_hms_opt(9, 0, 0).unwrap()));
344 + assert_eq!(
345 + parse("next week"),
346 + Some(
347 + NaiveDate::from_ymd_opt(2026, 6, 22)
348 + .unwrap()
349 + .and_hms_opt(9, 0, 0)
350 + .unwrap()
351 + )
352 + );
259 353 }
260 354
261 355 #[test]
262 356 fn weekday_finds_next_occurrence() {
263 357 // From Wednesday, "friday" is 2026-06-19.
264 - assert_eq!(parse("friday"), Some(NaiveDate::from_ymd_opt(2026, 6, 19).unwrap().and_hms_opt(9, 0, 0).unwrap()));
358 + assert_eq!(
359 + parse("friday"),
360 + Some(
361 + NaiveDate::from_ymd_opt(2026, 6, 19)
362 + .unwrap()
363 + .and_hms_opt(9, 0, 0)
364 + .unwrap()
365 + )
366 + );
265 367 // Same weekday goes to next week (Wednesday -> 2026-06-24).
266 - assert_eq!(parse("wednesday"), Some(NaiveDate::from_ymd_opt(2026, 6, 24).unwrap().and_hms_opt(9, 0, 0).unwrap()));
368 + assert_eq!(
369 + parse("wednesday"),
370 + Some(
371 + NaiveDate::from_ymd_opt(2026, 6, 24)
372 + .unwrap()
373 + .and_hms_opt(9, 0, 0)
374 + .unwrap()
375 + )
376 + );
267 377 // "friday 3pm" applies the time.
268 - assert_eq!(parse("friday 3pm"), Some(NaiveDate::from_ymd_opt(2026, 6, 19).unwrap().and_hms_opt(15, 0, 0).unwrap()));
378 + assert_eq!(
379 + parse("friday 3pm"),
380 + Some(
381 + NaiveDate::from_ymd_opt(2026, 6, 19)
382 + .unwrap()
383 + .and_hms_opt(15, 0, 0)
384 + .unwrap()
385 + )
386 + );
269 387 }
270 388
271 389 #[test]
272 390 fn next_weekday_skips_an_extra_week() {
273 391 // "next friday" skips the coming Friday (matches the prior JS quirk):
274 392 // coming Friday is 06-19, so "next friday" -> 06-26.
275 - assert_eq!(parse("next friday"), Some(NaiveDate::from_ymd_opt(2026, 6, 26).unwrap().and_hms_opt(9, 0, 0).unwrap()));
393 + assert_eq!(
394 + parse("next friday"),
395 + Some(
396 + NaiveDate::from_ymd_opt(2026, 6, 26)
397 + .unwrap()
398 + .and_hms_opt(9, 0, 0)
399 + .unwrap()
400 + )
401 + );
276 402 }
277 403
278 404 #[test]
279 405 fn month_day_rolls_to_next_year_when_past() {
280 406 // June 17 is past at 14:30; "jan 5" rolls to 2027.
281 - assert_eq!(parse("jan 5"), Some(NaiveDate::from_ymd_opt(2027, 1, 5).unwrap().and_hms_opt(9, 0, 0).unwrap()));
407 + assert_eq!(
408 + parse("jan 5"),
409 + Some(
410 + NaiveDate::from_ymd_opt(2027, 1, 5)
411 + .unwrap()
412 + .and_hms_opt(9, 0, 0)
413 + .unwrap()
414 + )
415 + );
282 416 // A future month stays this year.
283 - assert_eq!(parse("december 25"), Some(NaiveDate::from_ymd_opt(2026, 12, 25).unwrap().and_hms_opt(9, 0, 0).unwrap()));
284 - assert_eq!(parse("dec 25 3pm"), Some(NaiveDate::from_ymd_opt(2026, 12, 25).unwrap().and_hms_opt(15, 0, 0).unwrap()));
417 + assert_eq!(
418 + parse("december 25"),
419 + Some(
420 + NaiveDate::from_ymd_opt(2026, 12, 25)
421 + .unwrap()
422 + .and_hms_opt(9, 0, 0)
423 + .unwrap()
424 + )
425 + );
426 + assert_eq!(
427 + parse("dec 25 3pm"),
428 + Some(
429 + NaiveDate::from_ymd_opt(2026, 12, 25)
430 + .unwrap()
431 + .and_hms_opt(15, 0, 0)
432 + .unwrap()
433 + )
434 + );
285 435 }
286 436
287 437 #[test]
@@ -208,7 +208,10 @@ mod tests {
208 208 let dt = now - Duration::days(14);
209 209 // Exact format depends on local timezone, but should contain month abbreviation
210 210 let result = format_elapsed_time(dt, now);
211 - assert!(result.contains("Apr") || result.contains("Mar"), "Expected month abbrev, got: {result}");
211 + assert!(
212 + result.contains("Apr") || result.contains("Mar"),
213 + "Expected month abbrev, got: {result}"
214 + );
212 215 }
213 216
214 217 #[test]
@@ -48,7 +48,8 @@ pub fn detect_conflicts(items: &[TimelineItem]) -> Vec<Conflict> {
48 48
49 49 for item2 in items.iter().skip(i + 1) {
50 50 let end2 = item2.end_time.unwrap_or_else(|| {
51 - item2.start_time + chrono::Duration::minutes(item2.duration.unwrap_or(30).max(0) as i64)
51 + item2.start_time
52 + + chrono::Duration::minutes(item2.duration.unwrap_or(30).max(0) as i64)
52 53 });
53 54
54 55 // Standard interval overlap test: two intervals [s1,e1) and [s2,e2)
@@ -103,10 +104,7 @@ mod tests {
103 104
104 105 #[test]
105 106 fn test_direct_overlap() {
106 - let items = vec![
107 - make_timeline_item(9, 0, 60),
108 - make_timeline_item(9, 30, 60),
109 - ];
107 + let items = vec![make_timeline_item(9, 0, 60), make_timeline_item(9, 30, 60)];
110 108 let conflicts = detect_conflicts(&items);
111 109 assert_eq!(conflicts.len(), 1);
112 110 let overlap_duration = conflicts[0].overlap_end - conflicts[0].overlap_start;
@@ -115,10 +113,7 @@ mod tests {
115 113
116 114 #[test]
117 115 fn test_complete_containment() {
118 - let items = vec![
119 - make_timeline_item(9, 0, 120),
120 - make_timeline_item(9, 30, 30),
121 - ];
116 + let items = vec![make_timeline_item(9, 0, 120), make_timeline_item(9, 30, 30)];
122 117 let conflicts = detect_conflicts(&items);
123 118 assert_eq!(conflicts.len(), 1);
124 119 let overlap_duration = conflicts[0].overlap_end - conflicts[0].overlap_start;
@@ -137,10 +132,7 @@ mod tests {
137 132
138 133 #[test]
139 134 fn test_adjacent_no_conflict() {
140 - let items = vec![
141 - make_timeline_item(9, 0, 60),
142 - make_timeline_item(10, 0, 60),
143 - ];
135 + let items = vec![make_timeline_item(9, 0, 60), make_timeline_item(10, 0, 60)];
144 136 assert!(detect_conflicts(&items).is_empty());
145 137 }
146 138
@@ -115,9 +115,18 @@ mod tests {
115 115
116 116 #[test]
117 117 fn extract_handles_angle_brackets_and_plain() {
118 - assert_eq!(extract_email_address("Max <max@example.com>"), "max@example.com");
119 - assert_eq!(extract_email_address(" plain@example.com "), "plain@example.com");
120 - assert_eq!(extract_email_address("Name < spaced@example.com >"), "spaced@example.com");
118 + assert_eq!(
119 + extract_email_address("Max <max@example.com>"),
120 + "max@example.com"
121 + );
122 + assert_eq!(
123 + extract_email_address(" plain@example.com "),
124 + "plain@example.com"
125 + );
126 + assert_eq!(
127 + extract_email_address("Name < spaced@example.com >"),
128 + "spaced@example.com"
129 + );
121 130 assert_eq!(extract_email_address(""), "");
122 131 }
123 132
@@ -160,7 +169,10 @@ mod tests {
160 169 #[test]
161 170 fn quoted_reply_body_prefixes_each_line() {
162 171 let body = quoted_reply_body("Alice <a@x.com>", "Mar 1", "line1\nline2");
163 - assert_eq!(body, "\n\nOn Mar 1, Alice <a@x.com> wrote:\n>\n> line1\n> line2");
172 + assert_eq!(
173 + body,
174 + "\n\nOn Mar 1, Alice <a@x.com> wrote:\n>\n> line1\n> line2"
175 + );
164 176 }
165 177
166 178 #[test]
@@ -9,8 +9,7 @@ use uuid::Uuid;
9 9
10 10 /// Fixed namespace UUID for GoingsOn email IDs (generated once, never changes).
11 11 pub const GOINGSON_EMAIL_NS: Uuid = Uuid::from_bytes([
12 - 0x7a, 0x3b, 0x8c, 0x2d, 0x4e, 0x5f, 0x6a, 0x1b,
13 - 0x9c, 0x0d, 0x8e, 0x7f, 0xa2, 0xb3, 0xc4, 0xd5,
12 + 0x7a, 0x3b, 0x8c, 0x2d, 0x4e, 0x5f, 0x6a, 0x1b, 0x9c, 0x0d, 0x8e, 0x7f, 0xa2, 0xb3, 0xc4, 0xd5,
14 13 ]);
15 14
16 15 /// Generate a deterministic email ID from a Message-ID header.
@@ -66,7 +66,7 @@ pub async fn process_fetched_emails(
66 66 for e in &mut emails {
67 67 if e.message_id.is_none() {
68 68 // Use SHA-256 for a stable hash across Rust versions (DefaultHasher is not stable).
69 - use sha2::{Sha256, Digest};
69 + use sha2::{Digest, Sha256};
70 70 let mut h = Sha256::new();
71 71 h.update(e.from.as_bytes());
72 72 h.update(b"\x00");
@@ -76,19 +76,20 @@ pub async fn process_fetched_emails(
76 76 h.update(b"\x00");
77 77 h.update(e.date.to_rfc3339().as_bytes());
78 78 let hash = h.finalize();
79 - e.message_id = Some(format!("synth-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
80 - hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]));
79 + e.message_id = Some(format!(
80 + "synth-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
81 + hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]
82 + ));
81 83 }
82 84 }
83 85
84 86 // Batch check existing message IDs
85 - let msg_ids: Vec<&str> = emails.iter()
87 + let msg_ids: Vec<&str> = emails
88 + .iter()
86 89 .filter_map(|e| e.message_id.as_deref())
87 90 .collect();
88 91
89 - let existing_ids = email_repo
90 - .exists_by_message_ids(user_id, &msg_ids)
91 - .await?;
92 + let existing_ids = email_repo.exists_by_message_ids(user_id, &msg_ids).await?;
92 93
93 94 // Collect new emails for batch insert, track reply-to IDs for waiting-status clearing
94 95 let mut new_emails = Vec::new();
@@ -96,14 +97,17 @@ pub async fn process_fetched_emails(
96 97
97 98 for email in emails {
98 99 if let Some(ref msg_id) = email.message_id
99 - && existing_ids.contains(msg_id) {
100 - continue;
101 - }
100 + && existing_ids.contains(msg_id)
101 + {
102 + continue;
103 + }
102 104
103 105 // thread_id groups conversations: use in_reply_to if this is a reply,
104 106 // otherwise fall back to message_id (starts a new thread). This means
105 107 // the first email in a thread has thread_id == message_id.
106 - let thread_id = email.references_root.clone()
108 + let thread_id = email
109 + .references_root
110 + .clone()
107 111 .or_else(|| email.in_reply_to.clone())
108 112 .or_else(|| email.message_id.clone());
109 113
@@ -135,15 +139,18 @@ pub async fn process_fetched_emails(
135 139 }
136 140
137 141 // Batch insert — single transaction, no post-insert SELECTs
138 - result.emails_saved = email_repo.create_with_tracking_batch(user_id, new_emails).await?;
142 + result.emails_saved = email_repo
143 + .create_with_tracking_batch(user_id, new_emails)
144 + .await?;
139 145
140 146 // Clear waiting status for emails that received replies
141 147 for reply_to_msg_id in &reply_to_ids {
142 148 if let Ok(Some(original)) = email_repo.get_by_message_id(user_id, reply_to_msg_id).await
143 - && original.waiting_for_response {
144 - let _ = email_repo.clear_waiting(original.id, user_id).await;
145 - result.waiting_cleared += 1;
146 - }
149 + && original.waiting_for_response
150 + {
151 + let _ = email_repo.clear_waiting(original.id, user_id).await;
152 + result.waiting_cleared += 1;
153 + }
147 154 }
148 155
149 156 Ok(result)
@@ -17,10 +17,7 @@ pub enum CoreError {
17 17
18 18 /// Resource not found with type and identifier context.
19 19 #[error("Not found: {resource} with id {id}")]
20 - NotFound {
21 - resource: &'static str,
22 - id: String,
23 - },
20 + NotFound { resource: &'static str, id: String },
24 21
25 22 /// Validation error with field and message context.
26 23 #[error("Validation error: {field} - {message}")]
@@ -41,14 +41,14 @@ pub mod email_id;
41 41 pub mod email_sync;
42 42 pub mod error;
43 43 pub mod id_types;
44 + pub mod import;
44 45 pub mod models;
46 + pub mod monthly_review;
45 47 pub mod parser;
46 - pub mod import;
47 48 pub mod recurrence;
48 49 pub mod repository;
49 50 pub mod search_parser;
50 51 pub mod text_utils;
51 - pub mod monthly_review;
52 52 pub mod urgency;
53 53 pub mod validation;
54 54 pub mod weekly_review;
@@ -58,44 +58,45 @@ pub use contact::{
58 58 ContactPhone, NewContact, NewContactCustomField, NewContactEmail, NewContactPhone,
59 59 NewSocialHandle, SocialHandle, UpdateContact, merge_activity,
60 60 };
61 + pub use date_parser::parse_natural_date;
62 + pub use day_planning::{Conflict, TimelineItem, detect_conflicts};
63 + pub use email_compose::{
64 + ComposePrefill, forward_body, forward_subject, quoted_reply_body, reply_recipients,
65 + reply_subject,
66 + };
67 + pub use email_id::deterministic_email_id;
61 68 pub use error::CoreError;
62 69 pub use id_types::{
63 70 AnnotationId, AttachmentId, ContactEmailId, ContactId, ContactPhoneId, CustomFieldId,
64 - DailyNoteId, EmailAccountId, EmailId, EventId, MilestoneId, MonthlyGoalId,
65 - MonthlyReflectionId, ProjectId, SavedViewId, SocialHandleId,
66 - StatusTokenId, SubtaskId, SyncAccountId, TaskId, TimeSessionId, UserId, WeeklyReviewId,
67 - };
68 - pub use models::{
69 - Annotation, Attachment, BackupSettings, BlockType, CssClass, DailyNote, DbValue, Email, EmailAccount,
70 - EmailAuthType, EmailThread, Event, FolderSyncState, Milestone,
71 - MilestoneStatus, MonthlyGoal, MonthlyGoalStatus, MonthlyReflection,
72 - AttachmentMeta, NewAttachment, NewBackupSettings, NewEmail, NewEmailWithTracking, NewEvent, NewEventBuilder,
73 - NewMilestone, NewProject, NewSavedView, NewTask, NewTaskBuilder, Priority,
74 - Project, ParseableEnum, ProjectStatus, ProjectType, Recurrence, RecurrenceRule, MonthlySpec,
75 - SavedView, SortDirection,
76 - SyncAccount,
77 - PositiveMinutes, SortField, StatusToken, Subtask, Task, TaskFilterQuery, TaskSortColumn, TaskStatus, TimeSession,
78 - TokenState, TOKEN_KIND_COMMIT,
79 - TimeSummaryPanel, TimeSummaryProject, TimeTrackingSummary, roll_up_time_summary,
80 - UpdateEvent, UpdateProject, UpdateTask, User,
81 - ViewFilters, ViewType, WeeklyReview,
82 - format_file_size, mime_from_extension, snap_all_day_span,
71 + DailyNoteId, EmailAccountId, EmailId, EventId, MilestoneId, MonthlyGoalId, MonthlyReflectionId,
72 + ProjectId, SavedViewId, SocialHandleId, StatusTokenId, SubtaskId, SyncAccountId, TaskId,
73 + TimeSessionId, UserId, WeeklyReviewId,
83 74 };
84 - pub use parser::{parse_quick_add, parse_quick_add_with_warnings, ParsedTask, ParseResult};
85 - pub use date_parser::parse_natural_date;
86 - pub use day_planning::{Conflict, TimelineItem, detect_conflicts};
87 - pub use recurrence::{calculate_next_due, calculate_next_due_in_tz, calculate_next_due_with_day, calculate_next_due_with_day_in_tz, calculate_next_due_rich, calculate_next_due_rich_in_tz, expand_recurrence, expand_recurrence_in_tz, should_recur};
88 - pub use repository::*;
89 - pub use urgency::calculate_urgency;
90 75 pub use import::{
91 76 ImportEntityType, ImportEventData, ImportExecuteResult, ImportFailure, ImportItem,
92 77 ImportItemData, ImportOptions, ImportParseResult, ImportProjectData, ImportTaskData,
93 78 };
94 - pub use email_id::deterministic_email_id;
95 - pub use email_compose::{
96 - forward_body, forward_subject, quoted_reply_body, reply_recipients, reply_subject,
97 - ComposePrefill,
79 + pub use models::{
80 + Annotation, Attachment, AttachmentMeta, BackupSettings, BlockType, CssClass, DailyNote,
81 + DbValue, Email, EmailAccount, EmailAuthType, EmailThread, Event, FolderSyncState, Milestone,
82 + MilestoneStatus, MonthlyGoal, MonthlyGoalStatus, MonthlyReflection, MonthlySpec, NewAttachment,
83 + NewBackupSettings, NewEmail, NewEmailWithTracking, NewEvent, NewEventBuilder, NewMilestone,
84 + NewProject, NewSavedView, NewTask, NewTaskBuilder, ParseableEnum, PositiveMinutes, Priority,
85 + Project, ProjectStatus, ProjectType, Recurrence, RecurrenceRule, SavedView, SortDirection,
86 + SortField, StatusToken, Subtask, SyncAccount, TOKEN_KIND_COMMIT, Task, TaskFilterQuery,
87 + TaskSortColumn, TaskStatus, TimeSession, TimeSummaryPanel, TimeSummaryProject,
88 + TimeTrackingSummary, TokenState, UpdateEvent, UpdateProject, UpdateTask, User, ViewFilters,
89 + ViewType, WeeklyReview, format_file_size, mime_from_extension, roll_up_time_summary,
90 + snap_all_day_span,
91 + };
92 + pub use parser::{ParseResult, ParsedTask, parse_quick_add, parse_quick_add_with_warnings};
93 + pub use recurrence::{
94 + calculate_next_due, calculate_next_due_in_tz, calculate_next_due_rich,
95 + calculate_next_due_rich_in_tz, calculate_next_due_with_day, calculate_next_due_with_day_in_tz,
96 + expand_recurrence, expand_recurrence_in_tz, should_recur,
98 97 };
98 + pub use repository::*;
99 + pub use urgency::calculate_urgency;
99 100 pub use validation::Validate;
100 101
101 102 /// A specialized `Result` type for core operations.
@@ -1,8 +1,8 @@
1 1 //! File attachment domain types.
2 2
3 + use crate::id_types::{AttachmentId, EmailId, ProjectId, TaskId, UserId};
3 4 use chrono::{DateTime, Utc};
4 5 use serde::{Deserialize, Serialize};
5 - use crate::id_types::{AttachmentId, EmailId, ProjectId, TaskId, UserId};
6 6
7 7 /// A file attachment linked to a task or project.
8 8 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -43,7 +43,11 @@ pub struct AttachmentMeta {
43 43
44 44 /// Detect MIME type from file extension.
45 45 pub fn mime_from_extension(filename: &str) -> &'static str {
46 - let ext = filename.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
46 + let ext = filename
47 + .rsplit('.')
48 + .next()
49 + .unwrap_or("")
50 + .to_ascii_lowercase();
47 51 match ext.as_str() {
48 52 "pdf" => "application/pdf",
49 53 "doc" | "docx" => "application/msword",
@@ -129,7 +133,10 @@ mod tests {
129 133
130 134 #[test]
131 135 fn mime_from_dotfile() {
132 - assert_eq!(mime_from_extension(".gitignore"), "application/octet-stream");
136 + assert_eq!(
137 + mime_from_extension(".gitignore"),
138 + "application/octet-stream"
139 + );
133 140 }
134 141
135 142 #[test]
@@ -1,8 +1,8 @@
1 1 //! Daily review note domain types.
2 2
3 + use crate::id_types::{DailyNoteId, UserId};
3 4 use chrono::{DateTime, NaiveDate, Utc};
4 5 use serde::{Deserialize, Serialize};
5 - use crate::id_types::{DailyNoteId, UserId};
6 6
7 7 /// A daily review note capturing end-of-day reflection.
8 8 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -6,10 +6,10 @@
6 6 //! project linking, snoozing, and waiting-for-response tracking. JMAP-style
7 7 //! thread aggregation is available via `EmailThread` for efficient list rendering.
8 8
9 + use crate::constants::{DAYS_THRESHOLD_SHORT_FORMAT, EMAIL_BODY_PREVIEW_LENGTH};
10 + use crate::id_types::{EmailAccountId, EmailId, ProjectId};
9 11 use chrono::{DateTime, Utc};
10 12 use serde::{Deserialize, Serialize};
11 - use crate::constants::{DAYS_THRESHOLD_SHORT_FORMAT, EMAIL_BODY_PREVIEW_LENGTH};
12 - use crate::id_types::{EmailId, ProjectId, EmailAccountId};
13 13
14 14 // ============ Email ============
15 15
@@ -156,7 +156,8 @@ impl Email {
156 156 /// Returns true if the email is waiting and the expected response date has passed.
157 157 pub fn is_response_overdue(&self) -> bool {
158 158 self.waiting_for_response
159 - && self.expected_response_date
159 + && self
160 + .expected_response_date
160 161 .map(|date| date < Utc::now())
161 162 .unwrap_or(false)
162 163 }
@@ -1,9 +1,9 @@
1 1 //! Email account domain types.
2 2
3 + use crate::id_types::{EmailAccountId, UserId};
3 4 use chrono::{DateTime, Utc};
4 5 use serde::{Deserialize, Serialize};
5 6 use strum_macros::EnumString;
6 - use crate::id_types::{EmailAccountId, UserId};
7 7
8 8 use super::shared::DbValue;
9 9
@@ -232,9 +232,15 @@ mod tests {
232 232
233 233 #[test]
234 234 fn provider_id_oauth_variants() {
235 - assert_eq!(EmailAuthType::OAuth2Fastmail.provider_id(), Some("fastmail"));
235 + assert_eq!(
236 + EmailAuthType::OAuth2Fastmail.provider_id(),
237 + Some("fastmail")
238 + );
236 239 assert_eq!(EmailAuthType::OAuth2Google.provider_id(), Some("google"));
237 - assert_eq!(EmailAuthType::OAuth2Microsoft.provider_id(), Some("microsoft"));
240 + assert_eq!(
241 + EmailAuthType::OAuth2Microsoft.provider_id(),
242 + Some("microsoft")
243 + );
238 244 assert_eq!(EmailAuthType::OAuth2Yahoo.provider_id(), Some("yahoo"));
239 245 }
240 246
@@ -5,11 +5,11 @@
5 5 //! optional project and contact associations, and block-type classification
6 6 //! (focus, meeting, break, etc.).
7 7
8 + use super::shared::{BlockType, Recurrence, RecurrenceRule};
9 + use crate::constants::DAYS_THRESHOLD_SHORT_FORMAT;
10 + use crate::id_types::{ContactId, EventId, ProjectId, TaskId, UserId};
8 11 use chrono::{DateTime, Duration, TimeZone, Timelike, Utc};
9 12 use serde::{Deserialize, Serialize};
10 - use crate::constants::DAYS_THRESHOLD_SHORT_FORMAT;
11 - use crate::id_types::{EventId, UserId, ProjectId, ContactId, TaskId};
12 - use super::shared::{BlockType, Recurrence, RecurrenceRule};
13 13
14 14 // ============ Event ============
15 15
@@ -206,7 +206,10 @@ pub fn snap_all_day_span<Tz: TimeZone>(
206 206 }
207 207 }
208 208 };
209 - (local_midnight_utc(start_day, tz), local_midnight_utc(end_exclusive, tz))
209 + (
210 + local_midnight_utc(start_day, tz),
211 + local_midnight_utc(end_exclusive, tz),
212 + )
210 213 }
211 214
212 215 /// Convert a calendar date's local midnight in `tz` to the corresponding UTC instant.
@@ -1,10 +1,10 @@
1 1 //! Milestone domain types and DTOs.
2 2
3 + use super::shared::{CssClass, DbValue, ParseableEnum};
4 + use crate::id_types::{MilestoneId, ProjectId, UserId};
3 5 use chrono::{DateTime, Utc};
4 6 use serde::{Deserialize, Serialize};
5 7 use strum_macros::EnumString;
6 - use crate::id_types::{MilestoneId, UserId, ProjectId};
7 - use super::shared::{CssClass, DbValue, ParseableEnum};
8 8
9 9 // ============ Milestones ============
10 10
@@ -32,7 +32,6 @@ impl MilestoneStatus {
32 32 MilestoneStatus::Completed => "Completed",
33 33 }
34 34 }
35 -
36 35 }
37 36
38 37 impl ParseableEnum for MilestoneStatus {}
@@ -7,6 +7,7 @@ mod email;
7 7 mod email_account;
8 8 mod event;
9 9 mod milestone;
10 + mod monthly_review;
10 11 mod project;
11 12 mod saved_view;
12 13 mod shared;
@@ -14,7 +15,6 @@ mod sync_account;
14 15 mod task;
15 16 mod time_session;
16 17 mod user;
17 - mod monthly_review;
18 18 mod weekly_review;
19 19
20 20 pub use attachment::*;
@@ -1,70 +1,70 @@
1 - //! Monthly review domain types.
2 -
3 - use chrono::{DateTime, Utc};
4 - use serde::{Deserialize, Serialize};
5 - use crate::id_types::{MonthlyGoalId, MonthlyReflectionId, UserId};
6 -
7 - // ============ Monthly Goal ============
8 -
9 - /// Status of a monthly goal.
10 - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11 - #[serde(rename_all = "lowercase")]
12 - pub enum MonthlyGoalStatus {
13 - Active,
14 - Done,
15 - Abandoned,
16 - }
17 -
18 - impl MonthlyGoalStatus {
19 - pub fn as_str(&self) -> &str {
20 - match self {
21 - Self::Active => "active",
22 - Self::Done => "done",
23 - Self::Abandoned => "abandoned",
24 - }
25 - }
26 - }
27 -
28 - impl std::str::FromStr for MonthlyGoalStatus {
29 - type Err = crate::CoreError;
30 -
31 - fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
32 - match s {
33 - "active" => Ok(Self::Active),
34 - "done" => Ok(Self::Done),
35 - "abandoned" => Ok(Self::Abandoned),
36 - _ => Err(crate::CoreError::parse(format!("Invalid goal status: {s}"))),
37 - }
38 - }
39 - }
40 -
41 - /// A monthly goal — free-text item set at the start of each month.
42 - #[derive(Debug, Clone, Serialize, Deserialize)]
43 - #[serde(rename_all = "camelCase")]
44 - pub struct MonthlyGoal {
45 - pub id: MonthlyGoalId,
46 - pub user_id: UserId,
47 - /// Month in YYYY-MM format.
48 - pub month: String,
49 - pub text: String,
50 - pub status: MonthlyGoalStatus,
51 - /// Position 1-3.
52 - pub position: i32,
53 - pub created_at: DateTime<Utc>,
54 - pub updated_at: DateTime<Utc>,
55 - }
56 -
57 - // ============ Monthly Reflection ============
58 -
59 - /// A monthly reflection capturing highlights and areas for improvement.
60 - #[derive(Debug, Clone, Serialize, Deserialize)]
61 - #[serde(rename_all = "camelCase")]
62 - pub struct MonthlyReflection {
63 - pub id: MonthlyReflectionId,
64 - pub user_id: UserId,
65 - /// Month in YYYY-MM format.
66 - pub month: String,
67 - pub highlight_text: String,
68 - pub change_text: String,
69 - pub completed_at: DateTime<Utc>,
70 - }
1 + //! Monthly review domain types.
2 +
3 + use crate::id_types::{MonthlyGoalId, MonthlyReflectionId, UserId};
4 + use chrono::{DateTime, Utc};
5 + use serde::{Deserialize, Serialize};
6 +
7 + // ============ Monthly Goal ============
8 +
9 + /// Status of a monthly goal.
10 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11 + #[serde(rename_all = "lowercase")]
12 + pub enum MonthlyGoalStatus {
13 + Active,
14 + Done,
15 + Abandoned,
16 + }
17 +
18 + impl MonthlyGoalStatus {
19 + pub fn as_str(&self) -> &str {
20 + match self {
21 + Self::Active => "active",
22 + Self::Done => "done",
23 + Self::Abandoned => "abandoned",
24 + }
25 + }
26 + }
27 +
28 + impl std::str::FromStr for MonthlyGoalStatus {
29 + type Err = crate::CoreError;
30 +
31 + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
32 + match s {
33 + "active" => Ok(Self::Active),
34 + "done" => Ok(Self::Done),
35 + "abandoned" => Ok(Self::Abandoned),
36 + _ => Err(crate::CoreError::parse(format!("Invalid goal status: {s}"))),
37 + }
38 + }
39 + }
40 +
41 + /// A monthly goal — free-text item set at the start of each month.
42 + #[derive(Debug, Clone, Serialize, Deserialize)]
43 + #[serde(rename_all = "camelCase")]
44 + pub struct MonthlyGoal {
45 + pub id: MonthlyGoalId,
46 + pub user_id: UserId,
47 + /// Month in YYYY-MM format.
48 + pub month: String,
49 + pub text: String,
50 + pub status: MonthlyGoalStatus,
51 + /// Position 1-3.
52 + pub position: i32,
53 + pub created_at: DateTime<Utc>,
54 + pub updated_at: DateTime<Utc>,
55 + }
56 +
57 + // ============ Monthly Reflection ============
58 +
59 + /// A monthly reflection capturing highlights and areas for improvement.
60 + #[derive(Debug, Clone, Serialize, Deserialize)]
61 + #[serde(rename_all = "camelCase")]
62 + pub struct MonthlyReflection {
63 + pub id: MonthlyReflectionId,
64 + pub user_id: UserId,
65 + /// Month in YYYY-MM format.
66 + pub month: String,
67 + pub highlight_text: String,
68 + pub change_text: String,
69 + pub completed_at: DateTime<Utc>,
70 + }
@@ -5,11 +5,11 @@
5 5 //! Article, Painting, Other) and a lifecycle status (Active, OnHold, Completed,
6 6 //! Archived). Projects can contain milestones for phased tracking.
7 7
8 + use super::shared::{CssClass, DbValue, ParseableEnum};
9 + use crate::id_types::ProjectId;
8 10 use chrono::{DateTime, Utc};
9 11 use serde::{Deserialize, Serialize};
10 12 use strum_macros::EnumString;
11 - use crate::id_types::ProjectId;
12 - use super::shared::{CssClass, DbValue, ParseableEnum};
13 13
14 14 // ============ Project Types ============
15 15
@@ -53,7 +53,6 @@ impl ProjectType {
53 53 ProjectType::Other => "Other",
54 54 }
55 55 }
56 -
57 56 }
58 57
59 58 impl ParseableEnum for ProjectType {}
@@ -117,7 +116,6 @@ impl ProjectStatus {
117 116 ProjectStatus::Archived => "Archived",
118 117 }
119 118 }
120 -
121 119 }
122 120
123 121 impl ParseableEnum for ProjectStatus {}
@@ -218,8 +216,14 @@ mod tests {
218 216
219 217 #[test]
220 218 fn unknown_project_enum_strings_fall_back_to_default() {
221 - assert_eq!(ProjectType::from_str_or_default("bogus"), ProjectType::Other);
222 - assert_eq!(ProjectStatus::from_str_or_default("bogus"), ProjectStatus::Active);
219 + assert_eq!(
220 + ProjectType::from_str_or_default("bogus"),
221 + ProjectType::Other
222 + );
223 + assert_eq!(
224 + ProjectStatus::from_str_or_default("bogus"),
225 + ProjectStatus::Active
226 + );
223 227 }
224 228
225 229 #[test]
@@ -4,10 +4,10 @@
4 4 //! to the sidebar for quick access. Each view targets a specific domain
5 5 //! (tasks, emails, or events) and stores its filter criteria as JSON.
6 6
7 + use crate::id_types::{ProjectId, SavedViewId, UserId};
7 8 use chrono::{DateTime, Utc};
8 9 use serde::{Deserialize, Serialize};
9 10 use strum_macros::EnumString;
10 - use crate::id_types::{SavedViewId, UserId, ProjectId};
11 11
12 12 use super::shared::{DbValue, ParseableEnum, SortDirection};
13 13
@@ -38,7 +38,6 @@ impl ViewType {
38 38 ViewType::Events => "Events",
39 39 }
40 40 }
41 -
42 41 }
43 42
44 43 impl ParseableEnum for ViewType {}