1use std::{path::Path, str::FromStr};
2
3use chrono::NaiveDate;
4use srctrait_common_chronox::DateTimeFormat;
5
6use crate::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq,
9 strum::Display, strum::IntoStaticStr, strum::AsRefStr, strum::EnumIter, strum::EnumString)]
10#[strum(serialize_all = "kebab-case")]
11pub enum NoteKind {
12 Today,
13 Idea,
14 Todo,
15 Plan,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, strum::Display)]
19#[strum(serialize_all = "kebab-case")]
20pub enum NoteType {
21 Today(Date),
22 Idea(String),
23 Todo(String),
24 Plan(Option<String>),
25}
26
27impl NoteType {
28 pub fn kind(&self) -> NoteKind {
29 match self {
30 Self::Today(_) => NoteKind::Today,
31 Self::Idea(_) => NoteKind::Idea,
32 Self::Todo(_) => NoteKind::Todo,
33 Self::Plan(_) => NoteKind::Plan,
34 }
35 }
36
37 pub fn topic(&self) -> Option<&str> {
38 match self {
39 Self::Today(_) => None,
40 Self::Idea(topic) => Some(topic),
41 Self::Todo(topic) => Some(topic),
42 Self::Plan(topic) => topic.as_deref(),
43 }
44 }
45
46 pub fn date(&self) -> Option<&Date> {
47 match self {
48 Self::Today(date) => Some(date),
49 _ => None,
50 }
51 }
52
53 pub fn is_topical(&self) -> bool {
54 match self {
55 Self::Today(_) => false,
56 Self::Idea(_) => true,
57 Self::Todo(_) => true,
58 Self::Plan(topic) => topic.is_some(),
59 }
60 }
61
62 pub fn is_topical_optional(&self) -> bool {
63 match self {
64 Self::Today(_) => false,
65 Self::Idea(_) => false,
66 Self::Todo(_) => false,
67 Self::Plan(_) => true,
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Note(NoteType);
74
75impl Note {
76 pub fn new(note_type: NoteType) -> Self {
77 Self (note_type)
78 }
79
80 pub fn from_filepath(notes_dir: &NotesDir, file: &Path) -> Result<Self> {
81 let dir: &Path = notes_dir;
82 let relpath = file.strip_prefix(dir)
83 .map_err(|_| Error::InvalidNote(format!("File is not in the notes directory: {}", file.display())))?;
84
85 let mut path_components = relpath.components();
86 let kind_dir_name = path_components.next()
87 .ok_or_else(|| Error::InvalidNote(format!("Unexpected note type: {}", file.display())))?
88 .as_os_str().to_string_lossy();
89
90 let kind = NoteKind::from_str(&kind_dir_name)
91 .map_err(|_| Error::InvalidNote(format!("Unable to determine note type for file path: {}", file.display())))?;
92
93 if relpath.extension().is_none_or(|s| s != "md") {
94 return Err(Error::InvalidNote(format!("Not a markdown file: {}", file.display())));
95 }
96
97 let filestem = relpath.file_stem()
98 .ok_or_else(|| Error::InvalidNote(format!("Unable to determine note type for file path: {}", file.display())))?
99 .to_string_lossy();
100
101 let mut parts = filestem.split('-');
102 let prefix = parts.next();
103 let rest = parts.collect::<Vec<_>>().join("-");
104
105 if prefix.is_none() || prefix.is_some_and(|s| s != kind.as_ref()) {
106 return Err(Error::InvalidNote(format!("Unable to determine note type for file name: {filestem}")));
107 }
108
109 let note_type = match kind {
110 NoteKind::Today => {
111 let date = NaiveDate::parse_from_str(&rest, DateTimeFormat::YmdDash.strftime_format())
112 .map_err(|_| Error::InvalidNote(format!("Unable to determine note type for file path: {}", file.display())))?;
113
114 NoteType::Today(Date(date))
115 },
116 NoteKind::Idea => NoteType::Idea(rest),
117 NoteKind::Todo => NoteType::Todo(rest),
118 NoteKind::Plan => if rest.is_empty() {
119 NoteType::Plan(None)
120 } else {
121 NoteType::Plan(Some(rest))
122 }
123 };
124
125 Ok(Note::new(note_type))
126 }
127
128 pub fn note_type(&self) -> &NoteType {
129 &self.0
130 }
131
132 pub fn kind(&self) -> NoteKind {
133 self.0.kind()
134 }
135
136 pub fn topic(&self) -> Option<&str> {
137 self.0.topic()
138 }
139
140 pub fn date(&self) -> Option<&Date> {
141 self.0.date()
142 }
143}