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}