Skip to main content

archelon_core/
labels.rs

1//! Status flag classification for entry types and freshness.
2//!
3//! This module computes machine-readable flags for an entry based on its
4//! frontmatter (task status, event presence, timestamps). Display rendering
5//! (emoji, nerd-font glyphs, initials) is handled via [`EntryFlag`] methods.
6
7use chrono::{Duration, Local, NaiveDateTime};
8
9use crate::entry::{EventMetaView, TaskMetaView};
10
11/// A computed flag describing an entry's type or freshness state.
12///
13/// Serializes to its string representation via [`EntryFlag::as_str`].
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum EntryFlag {
16    // Freshness / urgency (slot 1)
17    Overdue,
18    New,
19    Updated,
20    // Entry type (slot 2)
21    Event,
22    /// Past event whose `end` timestamp is before the current time.
23    EventClosed,
24    Done,
25    Cancelled,
26    InProgress,
27    Archived,
28    Open,
29    Note,
30}
31
32impl EntryFlag {
33    pub fn as_str(self) -> &'static str {
34        match self {
35            Self::Overdue     => "overdue",
36            Self::New         => "new",
37            Self::Updated     => "updated",
38            Self::Event       => "event",
39            Self::EventClosed => "event_closed",
40            Self::Done        => "done",
41            Self::Cancelled   => "cancelled",
42            Self::InProgress  => "in_progress",
43            Self::Archived    => "archived",
44            Self::Open        => "open",
45            Self::Note        => "note",
46        }
47    }
48
49    pub fn to_emoji(self) -> &'static str {
50        match self {
51            Self::Overdue     => "⏰",
52            Self::New         => "🆕",
53            Self::Updated     => "✏️",
54            Self::Event       => "📅",
55            Self::EventClosed => "🗓️",
56            Self::Done        => "✅",
57            Self::Cancelled   => "❌",
58            Self::InProgress  => "🔄",
59            Self::Archived    => "📦",
60            Self::Open        => "⬜",
61            Self::Note        => "📝",
62        }
63    }
64
65    pub fn to_nerd(self) -> &'static str {
66        match self {
67            Self::Overdue     => "󱦟",
68            Self::New         => "󰐕",
69            Self::Updated     => "󰏫",
70            Self::Event       => "󰃭",
71            Self::EventClosed => "󰄻",
72            Self::Done        => "󰄲",
73            Self::Cancelled   => "󰜺",
74            Self::InProgress  => "󰔛",
75            Self::Archived    => "󰀼",
76            Self::Open        => "󰄱",
77            Self::Note        => "󰈙",
78        }
79    }
80
81    pub fn to_initial(self) -> char {
82        match self {
83            Self::Overdue     => '!',
84            Self::New         => '+',
85            Self::Updated     => '~',
86            Self::Event       => 'E',
87            Self::EventClosed => 'e',
88            Self::Done        => 'D',
89            Self::Cancelled   => 'C',
90            Self::InProgress  => 'I',
91            Self::Archived    => 'A',
92            Self::Open        => 'O',
93            Self::Note        => 'N',
94        }
95    }
96}
97
98impl serde::Serialize for EntryFlag {
99    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
100        s.serialize_str(self.as_str())
101    }
102}
103
104/// Returns the canonical flag string for a task status string.
105///
106/// Conventional statuses: `open`, `in_progress`, `done`, `cancelled`, `archived`.
107/// Any unrecognised status is treated as `open`.
108pub fn task_status_label(status: &str) -> &'static str {
109    match status {
110        "done" | "completed"     => "done",
111        "cancelled" | "canceled" => "cancelled",
112        "in_progress" | "wip"   => "in_progress",
113        "archived"               => "archived",
114        _                        => "open",
115    }
116}
117
118/// Returns the computed [`EntryFlag`]s for an entry.
119///
120/// Slot 1 (urgency/freshness): `Overdue`, `New` (created <24 h), `Updated` (<24 h), absent otherwise.
121/// Slot 2 (entry type): `Event` / `EventClosed` (past event), task status flag, or `Note`.
122pub fn entry_flags(
123    task: Option<&TaskMetaView>,
124    event: Option<&EventMetaView>,
125    created_at: NaiveDateTime,
126    updated_at: NaiveDateTime,
127) -> Vec<EntryFlag> {
128    let mut flags = Vec::new();
129
130    let now = Local::now().naive_local();
131
132    // Slot 1: overdue (highest priority) > created <24h > updated <24h
133    let is_overdue = task.map_or(false, |t| {
134        t.due.map_or(false, |due| due < now) && t.closed_at.is_none()
135    });
136    if is_overdue {
137        flags.push(EntryFlag::Overdue);
138    } else {
139        let threshold = now - Duration::hours(24);
140        if created_at >= threshold {
141            flags.push(EntryFlag::New);
142        } else if updated_at >= threshold {
143            flags.push(EntryFlag::Updated);
144        }
145    }
146
147    // Slot 2: entry type
148    if let Some(ev) = event {
149        if ev.end < now {
150            flags.push(EntryFlag::EventClosed);
151        } else {
152            flags.push(EntryFlag::Event);
153        }
154    } else if let Some(task) = task {
155        let flag = match task_status_label(&task.status) {
156            "done"        => EntryFlag::Done,
157            "cancelled"   => EntryFlag::Cancelled,
158            "in_progress" => EntryFlag::InProgress,
159            "archived"    => EntryFlag::Archived,
160            _             => EntryFlag::Open,
161        };
162        flags.push(flag);
163    } else {
164        flags.push(EntryFlag::Note);
165    }
166
167    flags
168}