A file-based task manager

ADD: basic xattr read support

This is a prerequisite for file-attributes and back linking

+112 -7
+2
.tsk/archive/tsk-13.tsk
··· 1 + user-defined labels 2 +
+2
.tsk/archive/tsk-14.tsk
··· 1 + parse internal links from body 2 +
+2
.tsk/archive/tsk-4.tsk
··· 1 1 Add basic metadata 2 2 3 + Currently have basic metadata reading done. There's nothing *writing* metadata, but 4 + we'll get there next.
+3 -2
.tsk/index
··· 1 + tsk-14 parse internal links from body 2 + tsk-6 automatically add backlinks 1 3 tsk-9 fix timestamp storage and parsing 2 4 tsk-8 IMAP4-based sync 3 5 tsk-10 foreign workspaces 4 6 tsk-7 allow for creating tasks that don't go to top of stack 5 - tsk-4 Add basic metadata 6 - tsk-6 automatically add backlinks 7 + tsk-13 user-defined labels
+1 -1
.tsk/next
··· 1 - 13 1 + 15
+1
.tsk/tasks/tsk-13.tsk
··· 1 + /home/noah/repos/tsk/.tsk/archive/tsk-13.tsk
+1
.tsk/tasks/tsk-14.tsk
··· 1 + /home/noah/repos/tsk/.tsk/archive/tsk-14.tsk
+55
src/attrs.rs
··· 1 + use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter}; 2 + use std::collections::BTreeMap; 3 + use std::iter::Chain; 4 + 5 + type Map = BTreeMap<String, String>; 6 + 7 + #[allow(dead_code)] 8 + /// Holds xattributes in a way that allows for differentiating between attributes that have been 9 + /// added/modified or that were present when reading the file. This is an *optimization* over 10 + /// infrequently modified values. 11 + #[derive(Default, Clone, Debug)] 12 + pub(crate) struct Attrs { 13 + pub written: Map, 14 + pub updated: Map, 15 + } 16 + 17 + impl IntoIterator for Attrs { 18 + type Item = (String, String); 19 + 20 + type IntoIter = Chain<BTreeIntoIter<String, String>, BTreeIntoIter<String, String>>; 21 + 22 + fn into_iter(self) -> Self::IntoIter { 23 + self.written.into_iter().chain(self.updated.into_iter()) 24 + } 25 + } 26 + 27 + #[allow(dead_code)] 28 + impl Attrs { 29 + pub(crate) fn from_written(written: Map) -> Self { 30 + Self { 31 + written, 32 + ..Default::default() 33 + } 34 + } 35 + 36 + pub(crate) fn get(&self, key: &str) -> Option<&String> { 37 + self.updated.get(key).or_else(|| self.written.get(key)) 38 + } 39 + 40 + pub(crate) fn insert(&mut self, key: String, value: String) -> Option<String> { 41 + if self.updated.contains_key(&key) { 42 + self.updated.insert(key, value) 43 + } else { 44 + let maybe_old_value = self.written.get(&key); 45 + self.updated.insert(key, value); 46 + maybe_old_value.cloned() 47 + } 48 + } 49 + 50 + pub(crate) fn iter( 51 + &self, 52 + ) -> Chain<BTreeMapIter<'_, String, String>, BTreeMapIter<'_, String, String>> { 53 + self.written.iter().chain(self.updated.iter()) 54 + } 55 + }
+17 -2
src/main.rs
··· 1 + mod attrs; 1 2 mod errors; 2 3 mod fzf; 3 4 mod stack; ··· 89 90 90 91 /// Prints the contents of a task. 91 92 Show { 93 + /// Shows raw file attributes for the file 94 + #[arg(short = 'x', default_value_t = false)] 95 + show_attrs: bool, 92 96 /// The [TSK-]ID of the task to display 93 97 #[command(flatten)] 94 98 task_id: TaskId, ··· 206 210 Commands::Push { edit, body, title } => command_push(dir, edit, body, title), 207 211 Commands::List { all, count } => command_list(dir, all, count), 208 212 Commands::Swap => command_swap(dir), 209 - Commands::Show { task_id } => command_show(dir, task_id), 213 + Commands::Show { 214 + task_id, 215 + show_attrs, 216 + } => command_show(dir, task_id, show_attrs), 210 217 Commands::Edit { task_id } => command_edit(dir, task_id), 211 218 Commands::Completion { shell } => command_completion(shell), 212 219 Commands::Drop { task_id } => command_drop(dir, task_id), ··· 341 348 Workspace::from_path(dir)?.deprioritize(task_id.into()) 342 349 } 343 350 344 - fn command_show(dir: PathBuf, task_id: TaskId) -> Result<()> { 351 + fn command_show(dir: PathBuf, task_id: TaskId, show_attrs: bool) -> Result<()> { 345 352 let task = Workspace::from_path(dir)?.task(task_id.into())?; 353 + // YAML front-matter style. YAML is gross, but it's what everyone uses! 354 + if show_attrs { 355 + println!("---"); 356 + for (attr, value) in task.attributes.iter() { 357 + println!("{attr}: \"{value}\""); 358 + } 359 + println!("---"); 360 + } 346 361 println!("{task}"); 347 362 Ok(()) 348 363 }
+28 -2
src/workspace.rs
··· 1 1 #![allow(dead_code)] 2 2 use nix::fcntl::{Flock, FlockArg}; 3 + use xattr::FileExt; 3 4 5 + use crate::attrs::Attrs; 4 6 use crate::errors::{Error, Result}; 5 7 use crate::stack::{StackItem, TaskStack}; 6 8 use crate::{fzf, util}; 7 - use std::collections::vec_deque; 9 + use std::collections::{vec_deque, BTreeMap}; 10 + use std::ffi::OsString; 8 11 use std::fmt::Display; 9 12 use std::fs::File; 10 13 use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; 14 + use std::ops::Deref; 11 15 use std::os::unix::fs::symlink; 12 16 use std::path::PathBuf; 13 17 use std::str::FromStr; ··· 15 19 16 20 const INDEXFILE: &str = "index"; 17 21 const TITLECACHEFILE: &str = "cache"; 22 + const XATTRPREFIX: &str = "user.tsk."; 18 23 /// A unique identifier for a task. When referenced in text, it is prefixed with `tsk-`. 19 24 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 20 25 pub struct Id(pub u32); ··· 109 114 } 110 115 } 111 116 117 + /// Increments the `next` counter and returns the previous value. 112 118 pub fn next_id(&self) -> Result<Id> { 113 119 let mut file = util::flopen(self.path.join("next"), FlockArg::LockExclusive)?; 114 120 let mut buf = String::new(); ··· 140 146 title, 141 147 body, 142 148 file, 149 + attributes: Default::default(), 143 150 }) 144 151 } 145 152 ··· 156 163 reader.read_line(&mut title)?; 157 164 reader.read_to_string(&mut body)?; 158 165 drop(reader); 166 + let mut read_attributes = BTreeMap::new(); 167 + if let Ok(attrs) = file.list_xattr() { 168 + for attr in attrs { 169 + if let Some((key, value)) = Self::read_xattr(&file, attr) { 170 + read_attributes.insert(key, value); 171 + } 172 + } 173 + } 159 174 Ok(Task { 160 175 id, 176 + file, 161 177 title: title.trim().to_string(), 162 178 body: body.trim().to_string(), 163 - file, 179 + attributes: Attrs::from_written(read_attributes), 164 180 }) 181 + } 182 + 183 + /// Reads an xattr from a file, stripping the prefix for 184 + fn read_xattr<D: Deref<Target = File>>(file: &D, key: OsString) -> Option<(String, String)> { 185 + // this *shouldn't* allocate, but it does O(n) scan the str for UTF-8 correctness 186 + let parsedkey = key.as_os_str().to_str()?.strip_prefix(XATTRPREFIX)?; 187 + let valuebytes = file.get_xattr(&key).ok().flatten()?; 188 + Some((parsedkey.to_string(), String::from_utf8(valuebytes).ok()?)) 165 189 } 166 190 167 191 pub fn read_stack(&self) -> Result<TaskStack> { ··· 288 312 pub title: String, 289 313 pub body: String, 290 314 pub file: Flock<File>, 315 + pub attributes: Attrs, 291 316 } 292 317 293 318 impl Display for Task { ··· 395 420 title: "Hello, world".to_string(), 396 421 body: "The body of the task.".to_string(), 397 422 file: util::flopen("/dev/null".into(), FlockArg::LockShared).unwrap(), 423 + attributes: Default::default(), 398 424 }; 399 425 assert_eq!("Hello, world\n\nThe body of the task.", task.to_string()); 400 426 }