Skip to main content

todo_txt/task/
note.rs

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}