1use std::fs::{self, File};
19use std::io;
20use std::io::prelude::*;
21use std::num::NonZeroU32;
22use std::path::Path;
23use std::result;
24
25#[doc(inline)]
26use crate::error::{Error, PathError};
27#[doc(inline)]
28use crate::file;
29
30#[derive(Debug)]
32pub struct Stack(String);
33
34impl Stack {
35 pub fn new(file: &str) -> result::Result<Self, PathError> {
43 file::canonical_filename(file, file::FileKind::StackFile).map(Self)
44 }
45
46 pub fn open(&self) -> result::Result<File, PathError> {
52 File::open(&self.0).map_err(|e| PathError::FileAccess(self.clone_file(), e.to_string()))
53 }
54
55 fn clone_file(&self) -> String { self.0.clone() }
57
58 pub fn exists(&self) -> bool { Path::new(&self.0).exists() }
60
61 pub fn clear(&self) -> result::Result<(), PathError> {
67 fs::remove_file(&self.0).map_err(|e| PathError::FileAccess(self.clone_file(), e.to_string()))
68 }
69
70 pub fn push(&self, task: &str) -> result::Result<(), PathError> {
77 let file = file::append_open(&self.0)?;
78 let mut stream = io::BufWriter::new(file);
79 writeln!(&mut stream, "{task}")
80 .map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
81 stream
82 .flush()
83 .map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
84 Ok(())
85 }
86
87 pub fn pop(&self) -> Option<String> {
89 let mut file = file::rw_open(&self.0).ok()?;
90 file::pop_last_line(&mut file)
91 }
92
93 pub fn drop(&self, num: NonZeroU32) -> crate::Result<()> {
101 (0..num.get())
102 .try_for_each(|_| self.pop().map(|_| ()))
103 .ok_or(Error::StackPop)
104 }
105
106 pub fn keep(&self, num: NonZeroU32) -> crate::Result<()> {
114 let file = self.open()?;
115 let unum = num.get() as usize;
116 let len = io::BufReader::new(file).lines().count();
117
118 if len > unum {
119 let backfile = format!("{}-bak", self.0);
120 let outfile = file::append_open(&backfile)?;
121 let mut stream = io::BufWriter::new(outfile);
122 let reader = io::BufReader::new(self.open()?);
123 for line in reader.lines().skip(len - unum).flatten() {
124 writeln!(&mut stream, "{line}")
125 .map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
126 }
127 stream
128 .flush()
129 .map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
130
131 fs::rename(&backfile, self.clone_file())
132 .map_err(|e| PathError::RenameFailure(self.clone_file(), e.to_string()))?;
133 }
134 Ok(())
135 }
136
137 pub fn process_down_stack<F>(&self, mut func: F) -> crate::Result<()>
145 where
146 F: FnMut(usize, &str)
147 {
148 let file = self.open()?;
149 let lines: Vec<String> = io::BufReader::new(file).lines().map_while(Result::ok).collect();
150 for (i, ln) in lines.iter().rev().enumerate() {
151 func(i, ln);
152 }
153 Ok(())
154 }
155
156 pub fn list(&self) -> String {
161 let mut output = String::new();
162 let Ok(_) = self.process_down_stack(|_, l| {
163 output.push_str(l);
164 output.push('\n');
165 })
166 else {
167 return String::new();
168 };
169
170 output
171 }
172
173 pub fn top(&self) -> crate::Result<String> {
181 let file = self.open()?;
182 let reader = io::BufReader::new(file).lines().map_while(Result::ok);
183 Ok(reader.last().unwrap_or_default())
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use std::path::Path;
190
191 use assert2::{assert, let_assert};
192 use nzliteral::nzliteral;
193 use tempfile::TempDir;
194
195 use super::*;
196
197 #[test]
198 fn test_new_missing_file() {
199 let_assert!(Err(err) = Stack::new(""));
200 assert!(err == PathError::FilenameMissing);
201 }
202
203 #[test]
204 fn test_new_bad_path() {
205 let mut stackdir = TempDir::new()
206 .expect("Cannot make tempdir")
207 .path()
208 .to_path_buf();
209 stackdir.push("foo");
210 stackdir.push("stack.txt");
211
212 let_assert!(Some(file) = stackdir.as_path().to_str());
213 let_assert!(Err(e) = Stack::new(file));
214 assert!(e == PathError::InvalidPath(
215 file.to_string(),
216 "No such file or directory (os error 2)".to_string()
217 ));
218 }
219
220 #[test]
221 fn test_new() {
222 let_assert!(Ok(tmpdir) = TempDir::new());
223 let mut path = tmpdir.path().to_path_buf();
224 path.push("stack.txt");
225
226 let_assert!(Some(filename) = path.to_str());
227 assert!(Stack::new(filename).is_ok());
228 }
229
230 #[test]
231 fn test_push() {
232 let_assert!(Ok(tmpdir) = TempDir::new());
233 let mut path = tmpdir.path().to_path_buf();
234 path.push("stack.txt");
235
236 let_assert!(Some(filename) = path.to_str());
237 let_assert!(Ok(stack) = Stack::new(filename));
238 assert!(!Path::new(filename).exists());
239
240 let task = "+house @todo change filters";
241 assert!(stack.push(task).is_ok());
242 let path = Path::new(filename);
243 assert!(path.is_file());
244
245 let_assert!(Ok(filelen) = path.metadata().map(|m| m.len() as usize));
247 assert!(filelen == task.len() + 1);
248
249 assert!(stack.push(task).is_ok());
250 let_assert!(Ok(filelen) = path.metadata().map(|m| m.len() as usize));
251 assert!(filelen == 2 * task.len() + 2);
252 }
253
254 #[test]
255 fn test_pop() {
256 let_assert!(Ok(tmpdir) = TempDir::new());
257 let mut path = tmpdir.path().to_path_buf();
258 path.push("stack.txt");
259
260 let_assert!(Some(filename) = path.to_str());
261 let_assert!(Ok(mut file) = File::create(filename));
262 let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n"));
263
264 let_assert!(Ok(stack) = Stack::new(filename));
265 let_assert!(Some(line) = stack.pop());
266 assert!(line == String::from("+home @todo second"));
267 let_assert!(Some(line) = stack.pop());
268 assert!(line == String::from("+home @todo first"));
269
270 let path = Path::new(filename);
271 assert!(path.is_file());
272 let_assert!(Ok(filelen) = path.metadata().map(|m| m.len() as usize));
274 assert!(filelen == 0);
275 }
276
277 #[test]
278 fn test_clear() {
279 let_assert!(Ok(tmpdir) = TempDir::new());
280 let mut path = tmpdir.path().to_path_buf();
281 path.push("stack.txt");
282
283 let_assert!(Some(filename) = path.to_str());
284 let_assert!(Ok(mut file) = File::create(filename));
285 let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n"));
286
287 let_assert!(Ok(stack) = Stack::new(filename));
288 assert!(stack.clear().is_ok());
289 assert!(!Path::new(filename).exists());
290 }
291
292 #[test]
293 fn test_drop_1() {
294 let_assert!(Ok(tmpdir) = TempDir::new());
295 let mut path = tmpdir.path().to_path_buf();
296 path.push("stack.txt");
297
298 let_assert!(Some(filename) = path.to_str());
299 let_assert!(Ok(mut file) = File::create(filename));
300 let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n"), "Cannot fill file");
301
302 let_assert!(Ok(stack) = Stack::new(filename));
303 assert!(stack.drop(nzliteral!(1u32)).is_ok());
304 let_assert!(Some(line) = stack.pop());
305 assert!(line == String::from("+home @todo first"));
306 }
307
308 #[test]
309 fn test_drop_2() {
310 let_assert!(Ok(tmpdir) = TempDir::new());
311 let mut path = tmpdir.path().to_path_buf();
312 path.push("stack.txt");
313
314 let_assert!(Some(filename) = path.to_str());
315 let_assert!(Ok(mut file) = File::create(filename));
316 let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n"));
317
318 let_assert!(Ok(stack) = Stack::new(filename));
319 assert!(stack.drop(nzliteral!(2u32)).is_ok());
320 let_assert!(Some(line) = stack.pop());
321 assert!(line == String::from("+home @todo first"));
322 }
323
324 #[test]
325 fn test_keep_1() {
326 let_assert!(Ok(tmpdir) = TempDir::new());
327 let mut path = tmpdir.path().to_path_buf();
328 path.push("stack.txt");
329
330 let_assert!(Some(filename) = path.to_str());
331 let_assert!(Ok(mut file) = File::create(filename));
332 let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n"));
333
334 let_assert!(Ok(stack) = Stack::new(filename));
335 assert!(stack.keep(nzliteral!(1u32)).is_ok());
336 let_assert!(Some(line) = stack.pop());
337 assert!(line == String::from("+home @todo second"));
338 assert!(stack.pop().is_none());
339 }
340
341 #[test]
342 fn test_keep_2() {
343 let_assert!(Ok(tmpdir) = TempDir::new());
344 let mut path = tmpdir.path().to_path_buf();
345 path.push("stack.txt");
346
347 let_assert!(Some(filename) = path.to_str());
348 let_assert!(Ok(mut file) = File::create(filename));
349 let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n"));
350
351 let_assert!(Ok(stack) = Stack::new(filename));
352 assert!(stack.keep(nzliteral!(2u32)).is_ok());
353 let_assert!(Some(line) = stack.pop());
354 assert!(line == String::from("+home @todo third"));
355 let_assert!(Some(line) = stack.pop());
356 assert!(line == String::from("+home @todo second"));
357 assert!(stack.pop().is_none());
358 }
359
360 #[test]
361 fn test_list() {
362 let_assert!(Ok(tmpdir) = TempDir::new());
363 let mut path = tmpdir.path().to_path_buf();
364 path.push("stack.txt");
365
366 let_assert!(Some(filename) = path.to_str());
367 let_assert!(Ok(mut file) = File::create(filename));
368 let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n"));
369
370 let_assert!(Ok(stack) = Stack::new(filename));
371 assert!(stack.list() == String::from(
372 "+home @todo third\n+home @todo second\n+home @todo first\n"
373 ));
374 }
375
376 #[test]
377 fn test_top() {
378 let_assert!(Ok(tmpdir) = TempDir::new());
379 let mut path = tmpdir.path().to_path_buf();
380 path.push("stack.txt");
381
382 let_assert!(Some(filename) = path.to_str());
383 let_assert!(Ok(mut file) = File::create(filename));
384 let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n"));
385
386 let_assert!(Ok(stack) = Stack::new(filename));
387 let_assert!(Ok(line) = stack.top());
388 assert!(line == String::from("+home @todo third"));
389 }
390
391 #[test]
392 fn test_top_empty() {
393 let_assert!(Ok(tmpdir) = TempDir::new());
394 let mut path = tmpdir.path().to_path_buf();
395 path.push("stack.txt");
396
397 let_assert!(Some(filename) = path.to_str());
398 let_assert!(Ok(_) = File::create(filename));
399
400 let_assert!(Ok(stack) = Stack::new(filename));
401 let_assert!(Ok(line) = stack.top());
402 assert!(line == String::new());
403 }
404}