A filesystem-based task manager
at master 5.9 kB view raw
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}