Skip to main content

tip_files/
lib.rs

1use std::cmp::Ordering;
2use std::collections::HashMap;
3use std::io::{BufRead, Read};
4use std::{
5    fs::{self, File},
6    io::{BufReader, Write},
7    path::PathBuf,
8};
9
10pub struct Tips<'a> {
11    root: PathBuf,
12    w: &'a mut dyn Write,
13}
14
15impl<'a> Tips<'a> {
16    pub fn new(root: PathBuf, w: &'a mut dyn Write) -> Self {
17        Self { root, w }
18    }
19
20    pub fn list(&mut self, filter_states: &[&str]) {
21        let Ok(walker) = fs::read_dir(self.root.clone()) else {
22            return;
23        };
24        let mut map = walk_to_map(walker, filter_states);
25        let mut keys = map.keys().cloned().collect::<Vec<_>>();
26        keys.sort_by(|a, b| smart_sort(a, b));
27        for k in keys {
28            let Some(t) = map.get_mut(&k) else {
29                continue;
30            };
31            writeln!(self.w, "{}", t.header()).unwrap();
32        }
33    }
34
35    pub fn details(&mut self, id: &str) {
36        let path = match self.find_file_by_id(id) {
37            Some(value) => value,
38            None => return,
39        };
40        let mut tip: Tip = path.into();
41        writeln!(self.w, "{}", tip.header()).unwrap();
42        let d = tip.details();
43        if !d.is_empty() {
44            writeln!(self.w, "\n{d}").unwrap();
45        }
46    }
47
48    pub fn create(&mut self, title: &str) {
49        let Ok(walker) = fs::read_dir(self.root.clone()) else {
50            return;
51        };
52        let map = walk_to_map(walker, &["open", "closed"]);
53        let mut keys = map.keys().cloned().collect::<Vec<_>>();
54        keys.sort_by(|a, b| smart_sort(a, b));
55        let mut id = String::from("1.tip");
56        if let Some(last) = keys.last() {
57            let (prefix, num) = de_label(last);
58            let num: usize = num.parse().unwrap();
59            if prefix.is_empty() {
60                id = format!("{}.tip", num + 1);
61            } else {
62                id = format!("{prefix}-{}.tip", num + 1);
63            }
64        }
65        let mut tipfile = self.root.clone();
66        tipfile.push("open");
67        tipfile.push(id);
68        let mut tip = Tip::create(tipfile, title);
69        writeln!(self.w, "{}", tip.header()).unwrap();
70    }
71
72    pub fn delete(&mut self, id: &str) {
73        let Some(tipfile) = self.find_file_by_id(id) else {
74            return;
75        };
76        fs::remove_file(tipfile).unwrap();
77    }
78
79    pub fn open(&mut self, id: &str) {
80        let mut path = self.root.clone();
81        path.push("closed");
82        path.push(format!("{id}.tip"));
83        let mut newpath = self.root.clone();
84        newpath.push("open");
85        newpath.push(format!("{id}.tip"));
86        if path.is_file() {
87            fs::rename(path, newpath).unwrap();
88        }
89    }
90
91    pub fn close(&mut self, id: &str) {
92        let mut path = self.root.clone();
93        path.push("open");
94        path.push(format!("{id}.tip"));
95        let mut newpath = self.root.clone();
96        newpath.push("closed");
97        newpath.push(format!("{id}.tip"));
98        if path.is_file() {
99            fs::rename(path, newpath).unwrap();
100        }
101    }
102
103    fn find_file_by_id(&mut self, id: &str) -> Option<PathBuf> {
104        let mut path = self.root.clone();
105        path.push("open");
106        path.push(format!("{id}.tip"));
107        if !path.is_file() {
108            path = self.root.clone();
109            path.push("closed");
110            path.push(format!("{id}.tip"));
111        }
112        if !path.is_file() {
113            return None;
114        }
115        Some(path)
116    }
117}
118
119struct Tip {
120    print_buf: Vec<u8>,
121    path: PathBuf,
122}
123
124impl Tip {
125    fn header(&mut self) -> &str {
126        let state = self
127            .path
128            .parent()
129            .unwrap()
130            .file_name()
131            .unwrap()
132            .to_str()
133            .unwrap();
134        let file = File::open(&self.path).unwrap();
135        let lines = BufReader::new(file).lines();
136        let title = lines.map_while(Result::ok).next().unwrap_or_default();
137        let id = self.path.file_stem().unwrap().to_str().unwrap();
138        write!(self.print_buf, "{id} ({state}): {}", title.trim()).unwrap();
139        str::from_utf8(&self.print_buf).unwrap()
140    }
141
142    fn details(&mut self) -> &str {
143        let file = File::open(&self.path).unwrap();
144        BufReader::new(file)
145            .read_to_end(&mut self.print_buf)
146            .unwrap();
147        let start = self.print_buf.iter().position(|b| *b == 10).unwrap_or(0);
148        str::from_utf8(&self.print_buf[start..]).unwrap().trim()
149    }
150
151    fn create(path: PathBuf, title: &str) -> Self {
152        let mut f = File::create(&path).unwrap();
153        writeln!(f, "{title}").unwrap();
154        path.into()
155    }
156}
157
158impl From<PathBuf> for Tip {
159    fn from(value: PathBuf) -> Self {
160        Self {
161            print_buf: vec![],
162            path: value,
163        }
164    }
165}
166
167fn walk_to_map(states: fs::ReadDir, filter_states: &[&str]) -> HashMap<String, Tip> {
168    let mut map = HashMap::<String, Tip>::new();
169    for state in states {
170        let Ok(state) = state else {
171            continue;
172        };
173        let Ok(tips) = fs::read_dir(state.path()) else {
174            continue;
175        };
176        let state = state.path();
177        let state = state.file_name().unwrap().to_str().unwrap();
178        if filter_states.is_empty() || filter_states.contains(&state) {
179            fill_tips(&mut map, tips);
180        }
181    }
182    map
183}
184
185fn fill_tips(map: &mut HashMap<String, Tip>, tips: fs::ReadDir) {
186    for tip in tips {
187        let Ok(tip) = tip else {
188            continue;
189        };
190        let tip_path = tip.path();
191        let Some(ext) = tip_path.extension() else {
192            continue;
193        };
194        if ext != "tip" {
195            continue;
196        }
197        let t: Tip = tip.path().to_path_buf().into();
198        let id = tip
199            .path()
200            .file_stem()
201            .unwrap()
202            .to_str()
203            .unwrap()
204            .to_string();
205        map.insert(id, t);
206    }
207}
208
209fn smart_sort(a: &str, b: &str) -> Ordering {
210    let (ap, asn) = de_label(a);
211    let (bp, bsn) = de_label(b);
212    if ap < bp {
213        return Ordering::Less;
214    } else if ap > bp {
215        return Ordering::Greater;
216    }
217    if let Ok(an) = asn.parse::<usize>()
218        && let Ok(bn) = bsn.parse::<usize>()
219    {
220        if an < bn {
221            return Ordering::Less;
222        } else if an > bn {
223            return Ordering::Greater;
224        } else {
225            return Ordering::Equal;
226        }
227    }
228    if asn < bsn {
229        return Ordering::Less;
230    } else if asn > bsn {
231        return Ordering::Greater;
232    }
233    Ordering::Equal
234}
235
236fn de_label(id: &str) -> (&str, &str) {
237    let Some(idx) = id.find('-') else {
238        return ("", id);
239    };
240    (&id[..idx], &id[idx + 1..])
241}