1use serde::{Deserialize, Serialize};
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
18pub struct Stopped {
19 pub text: String,
20 pub at: i64,
22 #[serde(default, skip_serializing_if = "String::is_empty")]
23 pub task: String,
24}
25
26impl Stopped {
27 pub fn default_path() -> PathBuf {
28 crate::config::state_dir().join("stopped.toml")
29 }
30
31 pub fn load(path: &Path) -> Option<Stopped> {
33 std::fs::read_to_string(path).ok().and_then(|t| toml::from_str::<Stopped>(&t).ok()).filter(|s| !s.text.trim().is_empty())
34 }
35
36 pub fn keep(path: &Path, text: &str, task: &str, at: i64) -> Result<(), String> {
38 let stopped = Stopped { text: text.trim().to_string(), at, task: task.to_string() };
39 let fail = |e: std::io::Error| format!("{}: {e}", path.display());
40 if let Some(parent) = path.parent() {
41 std::fs::create_dir_all(parent).map_err(fail)?;
42 }
43 if stopped.text.is_empty() {
44 return match std::fs::remove_file(path) {
45 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(fail(e)),
46 _ => Ok(()),
47 };
48 }
49 crate::filelock::with_lock(path, || crate::filelock::replace(path, toml::to_string(&stopped).map_err(|e| e.to_string())?.as_bytes()).map_err(fail))
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn a_line_left_then_done() {
60 let dir = std::env::temp_dir().join(format!("sioul-stopped-{}", std::process::id()));
61 let path = dir.join("stopped.toml");
62 let _ = std::fs::remove_dir_all(&dir);
63 assert_eq!(Stopped::load(&path), None);
64 Stopped::keep(&path, " the second paragraph, after the quote ", "t1", 100).unwrap();
65 assert_eq!(Stopped::load(&path), Some(Stopped { text: "the second paragraph, after the quote".into(), at: 100, task: "t1".into() }));
66 Stopped::keep(&path, "", "", 200).unwrap();
67 assert_eq!(Stopped::load(&path), None, "done: taken away");
68 let _ = std::fs::remove_dir_all(&dir);
69 }
70}