A file-based task manager
1#![allow(dead_code)]
2//! The Task stack. Tasks created with `push` end up at the top here. It is invalid for a task that
3//! has been completed/archived to be on the stack.
4
5use crate::errors::{Error, Result};
6use crate::util;
7use std::collections::vec_deque::Iter;
8use std::collections::VecDeque;
9use std::fmt::Display;
10use std::io::{self, BufRead, BufReader, Seek, Write};
11use std::str::FromStr;
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13use std::{fs::File, path::PathBuf};
14
15use nix::fcntl::{Flock, FlockArg};
16
17use crate::workspace::{Id, Task};
18
19const TASKSFOLDER: &str = "tasks";
20const INDEXFILE: &str = "index";
21
22pub struct StackItem {
23 pub id: Id,
24 pub title: String,
25 pub modify_time: SystemTime,
26}
27
28impl Display for StackItem {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 // .trim is used here on the title because there may be a newline in here if we read the
31 // title from the task file.
32 write!(
33 f,
34 // NOTE: we do NOT print the access time.
35 "{}\t{}",
36 self.id,
37 self.title.trim(),
38 )
39 }
40}
41
42impl TryFrom<Task> for StackItem {
43 type Error = Error;
44
45 fn try_from(value: Task) -> std::result::Result<Self, Self::Error> {
46 let modify_time = value.file.metadata()?.modified()?;
47 Ok(Self {
48 id: value.id,
49 // replace tabs with spaces, they're not valid in StackItem titles.
50 title: value.title.replace("\t", " "),
51 modify_time,
52 })
53 }
54}
55
56fn eof() -> Error {
57 Error::Io(io::Error::new(
58 io::ErrorKind::UnexpectedEof,
59 "Unexpected end of file",
60 ))
61}
62
63impl FromStr for StackItem {
64 type Err = Error;
65
66 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
67 let mut parts = s.trim().split("\t");
68 let id: Id = parts
69 .next()
70 .ok_or(Error::Parse(format!(
71 "Incomplete index line. Missing tsk ID"
72 )))?
73 .parse()?;
74 let title: String = parts
75 .next()
76 .ok_or(Error::Parse(format!(
77 "Incomplete index line. Missing title."
78 )))?
79 .trim()
80 .to_string();
81 // parse the timestamp as an integer
82 let index_epoch: u64 = parts.next().unwrap_or("0").parse()?;
83 // get a usable system time from the UNIX epoch, defaulting to the UNIX_EPOCH if there's
84 // any failures. This means that if there's errors, we will always read the title and
85 // modify_time from the task file.
86 let modify_time = UNIX_EPOCH
87 .checked_add(Duration::from_secs(index_epoch))
88 .unwrap_or(UNIX_EPOCH);
89 Ok(Self {
90 id,
91 title,
92 modify_time,
93 })
94 }
95}
96
97impl StackItem {
98 /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the
99 /// files: task id title
100 fn from_line(workspace_path: &PathBuf, line: String) -> Result<Self> {
101 let mut stack_item: StackItem = line.parse()?;
102
103 let task = util::flopen(
104 workspace_path
105 .join(TASKSFOLDER)
106 .join(stack_item.id.to_filename()),
107 FlockArg::LockExclusive,
108 )?;
109 let task_modify_time = task.metadata()?.modified()?;
110 // if the task file has been modified since we last looked at it, re-read the title and
111 // metadata
112 if (task_modify_time - Duration::from_secs(1)) > stack_item.modify_time {
113 stack_item.title.clear();
114 BufReader::new(&*task).read_line(&mut stack_item.title)?;
115 stack_item.modify_time = task_modify_time;
116 }
117 Ok(stack_item)
118 }
119}
120
121pub struct TaskStack {
122 /// All items within the stack
123 all: VecDeque<StackItem>,
124 file: Flock<File>,
125}
126
127impl TaskStack {
128 pub fn from_tskdir(workspace_path: &PathBuf) -> Result<Self> {
129 let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?;
130 let index = BufReader::new(&*file).lines();
131 let mut all = VecDeque::new();
132 for line in index {
133 let stack_item = StackItem::from_line(workspace_path, line?)?;
134 all.push_back(stack_item);
135 }
136 Ok(Self { all, file })
137 }
138
139 /// Saves the task stack to disk.
140 pub fn save(mut self) -> Result<()> {
141 // Clear the file
142 self.file.seek(std::io::SeekFrom::Start(0))?;
143 self.file.set_len(0)?;
144 for item in self.all.iter() {
145 self.file.write_all(format!("{item}\n").as_bytes())?;
146 }
147 Ok(())
148 }
149
150 pub fn push(&mut self, item: StackItem) {
151 self.all.push_front(item);
152 }
153
154 pub fn push_back(&mut self, item: StackItem) {
155 self.all.push_back(item);
156 }
157
158 pub fn pop(&mut self) -> Option<StackItem> {
159 self.all.pop_front()
160 }
161
162 pub fn swap(&mut self) {
163 let tip = self.all.pop_front();
164 let second = self.all.pop_front();
165 if tip.is_some() && second.is_some() {
166 self.all.push_front(tip.unwrap());
167 self.all.push_front(second.unwrap());
168 }
169 }
170
171 pub fn empty(&self) -> bool {
172 self.all.is_empty()
173 }
174
175 pub fn remove(&mut self, index: usize) -> Option<StackItem> {
176 self.all.remove(index)
177 }
178
179 pub fn iter(&self) -> Iter<StackItem> {
180 self.all.iter()
181 }
182
183 pub fn get(&self, index: usize) -> Option<&StackItem> {
184 self.all.get(index)
185 }
186}
187
188impl IntoIterator for TaskStack {
189 type Item = StackItem;
190
191 type IntoIter = std::collections::vec_deque::IntoIter<Self::Item>;
192
193 fn into_iter(self) -> Self::IntoIter {
194 self.all.into_iter()
195 }
196}