1#[derive(Clone, Debug, Default, PartialEq, Eq)]
2#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3#[cfg_attr(feature = "serde", serde(untagged))]
4pub enum Note {
5 #[default]
6 None,
7 Short(String),
8 Long {
9 filename: String,
10 content: String,
11 },
12}
13
14impl Note {
15 pub fn from_file(filename: &str) -> Self {
16 use std::io::Read;
17
18 if filename.is_empty() {
19 return Note::None;
20 }
21
22 let note_file = match Self::note_file(filename) {
23 Ok(note_file) => note_file,
24 Err(err) => {
25 log::error!("{err}");
26 return Note::Short(filename.to_string());
27 }
28 };
29
30 let file = match std::fs::File::open(note_file.clone()) {
31 Ok(file) => file,
32 Err(_) => {
33 log::error!("Unable to open {note_file:?}");
34 return Note::Short(filename.to_string());
35 }
36 };
37
38 let mut buffer = std::io::BufReader::new(file);
39 let mut content = String::new();
40
41 match buffer.read_to_string(&mut content) {
42 Ok(_) => (),
43 Err(_) => {
44 log::error!("Unable to read {note_file:?}");
45 return Note::Short(filename.to_string());
46 }
47 };
48
49 Note::Long {
50 filename: filename.to_string(),
51 content,
52 }
53 }
54
55 pub fn content(&self) -> Option<String> {
56 match *self {
57 Note::None => None,
58 Note::Short(ref content) | Note::Long { ref content, .. } => Some(content.clone()),
59 }
60 }
61
62 pub fn write(&mut self) -> crate::Result {
63 if self == &Note::None {
64 return Ok(());
65 }
66
67 if let Note::Short(ref content) = *self {
68 *self = Note::Long {
69 filename: Self::new_filename(),
70 content: content.clone(),
71 }
72 }
73
74 if let Note::Long {
75 ref filename,
76 ref content,
77 } = *self
78 {
79 use std::io::Write;
80
81 let note_file = Self::note_file(filename)?;
82
83 if let Some(note_dir) = note_file.parent()
84 && !note_dir.exists()
85 {
86 std::fs::create_dir_all(note_dir).map_err(crate::Error::Note)?;
87 }
88
89 let mut f = std::fs::File::create(note_file).map_err(crate::Error::Note)?;
90 f.write(content.as_bytes()).map_err(crate::Error::Note)?;
91 }
92
93 Ok(())
94 }
95
96 pub fn delete(&mut self) -> crate::Result {
97 if let Self::Long { filename, .. } = self {
98 std::fs::remove_file(filename).map_err(crate::Error::Note)?;
99 }
100
101 *self = Self::None;
102
103 Ok(())
104 }
105
106 fn new_filename() -> String {
107 let ext = match std::env::var("TODO_NOTE_EXT") {
108 Ok(ext) => ext,
109 Err(_) => ".txt".to_string(),
110 };
111
112 let name = Self::new_note_id();
113
114 format!("{name}{ext}")
115 }
116
117 fn new_note_id() -> String {
118 use rand::RngExt as _;
119
120 rand::rng()
121 .sample_iter(&rand::distr::Alphanumeric)
122 .map(char::from)
123 .take(3)
124 .collect()
125 }
126
127 fn note_file(filename: &str) -> crate::Result<std::path::PathBuf> {
128 let todo_dir = match std::env::var("TODO_DIR") {
129 Ok(todo_dir) => todo_dir,
130 Err(_) => return Err(crate::Error::Env),
131 };
132
133 let note_dir = match std::env::var("TODO_NOTES_DIR") {
134 Ok(note_dir) => note_dir,
135 Err(_) => format!("{todo_dir}/notes"),
136 };
137
138 let path = format!("{note_dir}/{filename}");
139
140 Ok(path.into())
141 }
142}
143
144impl std::fmt::Display for Note {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 let tag = match std::env::var("TODO_NOTE_TAG") {
147 Ok(tag) => tag,
148 Err(_) => "note".to_string(),
149 };
150
151 let tag = match *self {
152 Note::None => String::new(),
153 Note::Short(ref content) => format!("{tag}:{content}"),
154 Note::Long { ref filename, .. } => format!("{tag}:{filename}"),
155 };
156
157 f.write_str(&tag)
158 }
159}
160
161impl From<String> for Note {
162 fn from(value: String) -> Self {
163 Self::Short(value)
164 }
165}
166
167impl std::str::FromStr for Note {
168 type Err = std::convert::Infallible;
169
170 fn from_str(s: &str) -> Result<Self, Self::Err> {
171 Ok(Self::from(s.to_string()))
172 }
173}