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//!
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}