1use std::fs::{self, File};
19use std::io;
20use std::io::prelude::*;
21use std::path::Path;
22use std::result;
23
24#[doc(inline)]
25use crate::error::{Error, PathError};
26#[doc(inline)]
27use crate::file;
28
29#[derive(Debug)]
31pub struct Stack(String);
32
33impl Stack {
34 pub fn new(file: &str) -> result::Result<Self, PathError> {
42 file::canonical_filename(file, file::FileKind::StackFile).map(Self)
43 }
44
45 pub fn open(&self) -> result::Result<File, PathError> {
51 File::open(&self.0).map_err(|e| PathError::FileAccess(self.clone_file(), e.to_string()))
52 }
53
54 fn clone_file(&self) -> String { self.0.clone() }
56
57 pub fn exists(&self) -> bool { Path::new(&self.0).exists() }
59
60 pub fn clear(&self) -> result::Result<(), PathError> {
66 fs::remove_file(&self.0).map_err(|e| PathError::FileAccess(self.clone_file(), e.to_string()))
67 }
68
69 pub fn push(&self, task: &str) -> result::Result<(), PathError> {
76 let file = file::append_open(&self.0)?;
77 let mut stream = io::BufWriter::new(file);
78 writeln!(&mut stream, "{task}")
79 .map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
80 stream
81 .flush()
82 .map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
83 Ok(())
84 }
85
86 pub fn pop(&self) -> Option<String> {
88 let mut file = file::rw_open(&self.0).ok()?;
89 file::pop_last_line(&mut file)
90 }
91
92 pub fn drop(&self, arg: u32) -> crate::Result<()> {
101 (0..std::cmp::max(1, arg))
102 .try_for_each(|_| self.pop().map(|_| ()))
103 .ok_or(Error::StackPop)
104 }
105
106 pub fn keep(&self, num: u32) -> crate::Result<()> {
114 let file = self.open()?;
115 let unum = num 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 spectral::prelude::*;
192 use tempfile::TempDir;
193
194 use super::*;
195
196 #[test]
197 fn test_new_missing_file() {
198 assert_that!(Stack::new("")).is_err_containing(PathError::FilenameMissing);
199 }
200
201 #[test]
202 fn test_new_bad_path() {
203 let mut stackdir = TempDir::new()
204 .expect("Cannot make tempdir")
205 .path()
206 .to_path_buf();
207 stackdir.push("foo");
208 stackdir.push("stack.txt");
209
210 let file = stackdir.as_path().to_str().unwrap();
211 assert_that!(Stack::new(file)).is_err_containing(PathError::InvalidPath(
212 file.to_string(),
213 "No such file or directory (os error 2)".to_string()
214 ));
215 }
216
217 #[test]
218 fn test_new() {
219 let tmpdir = TempDir::new().expect("Cannot make tempfile");
220 let mut path = tmpdir.path().to_path_buf();
221 path.push("stack.txt");
222
223 let filename = path.to_str().unwrap();
224 assert_that!(Stack::new(filename)).is_ok();
225 }
226
227 #[test]
228 fn test_push() {
229 let tmpdir = TempDir::new().expect("Cannot make tempfile");
230 let mut path = tmpdir.path().to_path_buf();
231 path.push("stack.txt");
232
233 let filename = path.to_str().unwrap();
234 let stack = Stack::new(filename).expect("Cannot create stack");
235 assert_that!(Path::new(filename).exists()).is_false();
236
237 let task = "+house @todo change filters";
238 assert_that!(stack.push(task)).is_ok();
239 let path = Path::new(filename);
240 assert_that!(path.is_file()).is_true();
241
242 let filelen = path.metadata().expect("metadata fail").len() as usize;
244 assert_that!(filelen).is_equal_to(task.len() + 1);
245
246 assert_that!(stack.push(task)).is_ok();
247 let filelen = path.metadata().expect("metadata fail").len() as usize;
248 assert_that!(filelen).is_equal_to(2 * task.len() + 2);
249 }
250
251 #[test]
252 fn test_pop() {
253 let tmpdir = TempDir::new().expect("Cannot make tempfile");
254 let mut path = tmpdir.path().to_path_buf();
255 path.push("stack.txt");
256
257 let filename = path.to_str().unwrap();
258 let mut file = File::create(filename).expect("Cannot create file");
259 file.write_all(b"+home @todo first\n+home @todo second\n")
260 .expect("Cannot fill file");
261
262 let stack = Stack::new(filename).expect("Cannot create stack");
263 assert_that!(stack.pop()).contains(String::from("+home @todo second"));
264 assert_that!(stack.pop()).contains(String::from("+home @todo first"));
265
266 let path = Path::new(filename);
267 assert_that!(path.is_file()).is_true();
268 let filelen = path.metadata().expect("metadata fail").len() as usize;
270 assert_that!(filelen).is_equal_to(0);
271 }
272
273 #[test]
274 fn test_clear() {
275 let tmpdir = TempDir::new().expect("Cannot make tempfile");
276 let mut path = tmpdir.path().to_path_buf();
277 path.push("stack.txt");
278
279 let filename = path.to_str().unwrap();
280 let mut file = File::create(filename).expect("Cannot create file");
281 file.write_all(b"+home @todo first\n+home @todo second\n")
282 .expect("Cannot fill file");
283
284 let stack = Stack::new(filename).expect("Cannot create stack");
285 assert_that!(stack.clear()).is_ok();
286 assert_that!(Path::new(filename).exists()).is_false();
287 }
288
289 #[test]
290 fn test_drop_0() {
291 let tmpdir = TempDir::new().expect("Cannot make tempfile");
292 let mut path = tmpdir.path().to_path_buf();
293 path.push("stack.txt");
294
295 let filename = path.to_str().unwrap();
296 let mut file = File::create(filename).expect("Cannot create file");
297 file.write_all(b"+home @todo first\n+home @todo second\n")
298 .expect("Cannot fill file");
299
300 let stack = Stack::new(filename).expect("Cannot create stack");
301 assert_that!(stack.drop(0)).is_ok();
302 assert_that!(stack.pop()).contains(String::from("+home @todo first"));
303 }
304
305 #[test]
306 fn test_drop_1() {
307 let tmpdir = TempDir::new().expect("Cannot make tempfile");
308 let mut path = tmpdir.path().to_path_buf();
309 path.push("stack.txt");
310
311 let filename = path.to_str().unwrap();
312 let mut file = File::create(filename).expect("Cannot create file");
313 file.write_all(b"+home @todo first\n+home @todo second\n")
314 .expect("Cannot fill file");
315
316 let stack = Stack::new(filename).expect("Cannot create stack");
317 assert_that!(stack.drop(1)).is_ok();
318 assert_that!(stack.pop()).contains(String::from("+home @todo first"));
319 }
320
321 #[test]
322 fn test_drop_2() {
323 let tmpdir = TempDir::new().expect("Cannot make tempfile");
324 let mut path = tmpdir.path().to_path_buf();
325 path.push("stack.txt");
326
327 let filename = path.to_str().unwrap();
328 let mut file = File::create(filename).expect("Cannot create file");
329 file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n")
330 .expect("Cannot fill file");
331
332 let stack = Stack::new(filename).expect("Cannot create stack");
333 assert_that!(stack.drop(2)).is_ok();
334 assert_that!(stack.pop()).contains(String::from("+home @todo first"));
335 }
336
337 #[test]
338 fn test_list() {
339 let tmpdir = TempDir::new().expect("Cannot make tempfile");
340 let mut path = tmpdir.path().to_path_buf();
341 path.push("stack.txt");
342
343 let filename = path.to_str().unwrap();
344 let mut file = File::create(filename).expect("Cannot create file");
345 file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n")
346 .expect("Cannot fill file");
347
348 let stack = Stack::new(filename).expect("Cannot create stack");
349 assert_that!(stack.list()).is_equal_to(String::from(
350 "+home @todo third\n+home @todo second\n+home @todo first\n"
351 ));
352 }
353
354 #[test]
355 fn test_top() {
356 let tmpdir = TempDir::new().expect("Cannot make tempfile");
357 let mut path = tmpdir.path().to_path_buf();
358 path.push("stack.txt");
359
360 let filename = path.to_str().unwrap();
361 let mut file = File::create(filename).expect("Cannot create file");
362 file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n")
363 .expect("Cannot fill file");
364
365 let stack = Stack::new(filename).expect("Cannot create stack");
366 assert_that!(stack.top()).contains(String::from("+home @todo third"));
367 }
368
369 #[test]
370 fn test_top_empty() {
371 let tmpdir = TempDir::new().expect("Cannot make tempfile");
372 let mut path = tmpdir.path().to_path_buf();
373 path.push("stack.txt");
374
375 let filename = path.to_str().unwrap();
376 File::create(filename).expect("Cannot create file");
377
378 let stack = Stack::new(filename).expect("Cannot create stack");
379 assert_that!(stack.top()).contains(String::new());
380 }
381}