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