A file-based task manager
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at fa5327e49123fbfc98101ddb019337980b6e5ef7 152 lines 4.1 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//! 5//! The stack is persisted as a single blob keyed `index` in the workspace's 6//! [`Store`](crate::backend::Store). Each line is `tsk-N\ttitle\ttimestamp`. 7 8use crate::backend::Store; 9use crate::errors::{Error, Result}; 10use std::collections::VecDeque; 11use std::collections::vec_deque::Iter; 12use std::fmt::Display; 13use std::str::FromStr; 14use std::time::{Duration, SystemTime, UNIX_EPOCH}; 15 16use crate::workspace::{Id, Task}; 17 18#[derive(Clone)] 19pub struct StackItem { 20 pub id: Id, 21 pub title: String, 22 pub modify_time: SystemTime, 23} 24 25impl Display for StackItem { 26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 write!(f, "{}\t{}", self.id, self.title.trim()) 28 } 29} 30 31impl From<&Task> for StackItem { 32 fn from(value: &Task) -> Self { 33 Self { 34 id: value.id, 35 title: value.title.replace("\t", " "), 36 modify_time: SystemTime::now(), 37 } 38 } 39} 40 41impl FromStr for StackItem { 42 type Err = Error; 43 44 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 45 let mut parts = s.trim().split('\t'); 46 let id: Id = parts 47 .next() 48 .ok_or(Error::Parse("Incomplete index line. Missing tsk ID".into()))? 49 .parse()?; 50 let title = parts 51 .next() 52 .ok_or(Error::Parse("Incomplete index line. Missing title.".into()))? 53 .trim() 54 .to_string(); 55 let index_epoch: u64 = parts.next().unwrap_or("0").parse().unwrap_or(0); 56 let modify_time = UNIX_EPOCH 57 .checked_add(Duration::from_secs(index_epoch)) 58 .unwrap_or(UNIX_EPOCH); 59 Ok(Self { 60 id, 61 title, 62 modify_time, 63 }) 64 } 65} 66 67pub struct TaskStack { 68 pub all: VecDeque<StackItem>, 69} 70 71impl TaskStack { 72 pub fn parse(text: &str) -> Result<Self> { 73 text.lines() 74 .filter(|l| !l.trim().is_empty()) 75 .map(str::parse) 76 .collect::<Result<VecDeque<_>>>() 77 .map(|all| Self { all }) 78 } 79 80 pub fn load(store: &dyn Store) -> Result<Self> { 81 Self::parse(&String::from_utf8_lossy( 82 &store.read("index")?.unwrap_or_default(), 83 )) 84 } 85 86 pub fn serialize(&self) -> String { 87 self.all 88 .iter() 89 .map(|i| { 90 let ts = i 91 .modify_time 92 .duration_since(UNIX_EPOCH) 93 .map_or(0, |d| d.as_secs()); 94 format!("{i}\t{ts}\n") 95 }) 96 .collect() 97 } 98 99 pub fn save(&self, store: &dyn Store) -> Result<()> { 100 store.write("index", self.serialize().as_bytes()) 101 } 102 103 pub fn push(&mut self, item: StackItem) { 104 self.all.push_front(item); 105 } 106 107 pub fn push_back(&mut self, item: StackItem) { 108 self.all.push_back(item); 109 } 110 111 pub fn pop(&mut self) -> Option<StackItem> { 112 self.all.pop_front() 113 } 114 115 pub fn swap(&mut self) { 116 let tip = self.all.pop_front(); 117 let second = self.all.pop_front(); 118 if let Some((tip, second)) = tip.zip(second) { 119 self.all.push_front(tip); 120 self.all.push_front(second); 121 } 122 } 123 124 pub fn empty(&self) -> bool { 125 self.all.is_empty() 126 } 127 128 pub fn remove(&mut self, index: usize) -> Option<StackItem> { 129 self.all.remove(index) 130 } 131 132 pub fn iter(&self) -> Iter<'_, StackItem> { 133 self.all.iter() 134 } 135 136 pub fn get(&self, index: usize) -> Option<&StackItem> { 137 self.all.get(index) 138 } 139 140 pub fn position(&self, id: Id) -> Option<usize> { 141 self.all.iter().position(|i| i.id == id) 142 } 143} 144 145impl IntoIterator for TaskStack { 146 type Item = StackItem; 147 type IntoIter = std::collections::vec_deque::IntoIter<Self::Item>; 148 149 fn into_iter(self) -> Self::IntoIter { 150 self.all.into_iter() 151 } 152}