A file-based task manager

ADD: automatic backreference

+61 -8
+1 -1
.tsk/archive/tsk-14.tsk
··· 7 7 8 8 here's [a link](https://ngp.computer). 9 9 10 - and an internal link: [[tsk-11]] 10 + and an internal link: [[tsk-11]]. This should add a backlink 11 11 12 12 and some _underlined text_ 13 13
+3 -1
.tsk/archive/tsk-6.tsk
··· 1 1 automatically add backlinks 2 2 3 3 I need to parse on save/edit/create for outgoing internal links. If any exist and their 4 - corresponding task exists, update the targetted task with a backlink reference 4 + corresponding task exists, update the targetted task with a backlink reference 5 + 6 + Using [[tsk-11]] as my test.
-1
.tsk/index
··· 1 - tsk-6 automatically add backlinks 2 1 tsk-21 Add command to setup git stuff 3 2 tsk-8 IMAP4-based sync 4 3 tsk-17 Add reopen command
+14 -4
Cargo.lock
··· 376 376 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 377 377 378 378 [[package]] 379 + name = "itertools" 380 + version = "0.13.0" 381 + source = "registry+https://github.com/rust-lang/crates.io-index" 382 + checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 383 + dependencies = [ 384 + "either", 385 + ] 386 + 387 + [[package]] 379 388 name = "lazy_static" 380 389 version = "1.5.0" 381 390 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 552 561 553 562 [[package]] 554 563 name = "thiserror" 555 - version = "1.0.69" 564 + version = "2.0.3" 556 565 source = "registry+https://github.com/rust-lang/crates.io-index" 557 - checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 566 + checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" 558 567 dependencies = [ 559 568 "thiserror-impl", 560 569 ] 561 570 562 571 [[package]] 563 572 name = "thiserror-impl" 564 - version = "1.0.69" 573 + version = "2.0.3" 565 574 source = "registry+https://github.com/rust-lang/crates.io-index" 566 - checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 575 + checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" 567 576 dependencies = [ 568 577 "proc-macro2", 569 578 "quote", ··· 589 598 "clap_mangen", 590 599 "colored", 591 600 "edit", 601 + "itertools", 592 602 "nix", 593 603 "open", 594 604 "thiserror",
+2 -1
Cargo.toml
··· 9 9 clap_complete = "4" 10 10 edit = "0" 11 11 nix = { version = "0", features = ["fs"] } 12 - thiserror = "1" 12 + thiserror = "2" 13 13 url = "2" 14 14 xattr = "1" 15 15 colored = "2" 16 16 open = "5" 17 + itertools = "0" 17 18 18 19 [build-dependencies] 19 20 clap_mangen = "0"
+2
src/main.rs
··· 303 303 } 304 304 } 305 305 let task = workspace.new_task(title, body)?; 306 + workspace.handle_metadata(&task)?; 306 307 workspace.push_task(task) 307 308 } 308 309 ··· 344 345 if let Some((title, body)) = new_content.split_once("\n") { 345 346 task.title = title.to_string(); 346 347 task.body = body.to_string(); 348 + workspace.handle_metadata(&task)?; 347 349 task.save()?; 348 350 } 349 351 Ok(())
+39
src/workspace.rs
··· 5 5 use crate::attrs::Attrs; 6 6 use crate::errors::{Error, Result}; 7 7 use crate::stack::{StackItem, TaskStack}; 8 + use crate::task::{parse as parse_task, ParsedLink}; 8 9 use crate::{fzf, util}; 9 10 use std::collections::{vec_deque, BTreeMap}; 10 11 use std::ffi::OsString; ··· 20 21 const INDEXFILE: &str = "index"; 21 22 const TITLECACHEFILE: &str = "cache"; 22 23 const XATTRPREFIX: &str = "user.tsk."; 24 + const BACKREFXATTR: &str = "user.tsk.references"; 23 25 /// A unique identifier for a task. When referenced in text, it is prefixed with `tsk-`. 24 26 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 25 27 pub struct Id(pub u32); ··· 181 183 }) 182 184 } 183 185 186 + pub fn handle_metadata(&self, tsk: &Task) -> Result<()> { 187 + // Parse the task and update any backlinks 188 + if let Some(parsed_task) = parse_task(&tsk.to_string()) { 189 + for link in parsed_task.links { 190 + if let ParsedLink::Internal(id) = link { 191 + self.add_backlink(id, tsk.id)?; 192 + } 193 + } 194 + } 195 + Ok(()) 196 + } 197 + 198 + pub fn add_backlink(&self, to: Id, from: Id) -> Result<()> { 199 + let to_task = self.task(TaskIdentifier::Id(to))?; 200 + let (_, current_backlinks_text) = 201 + Self::read_xattr(&to_task.file, BACKREFXATTR.into()).unwrap_or_default(); 202 + let mut backlinks: Vec<Id> = current_backlinks_text 203 + .split(',') 204 + .filter_map(|s| Id::from_str(s).ok()) 205 + .collect(); 206 + backlinks.push(from); 207 + Self::set_xattr( 208 + &to_task.file, 209 + BACKREFXATTR.into(), 210 + &itertools::join(backlinks, ","), 211 + ) 212 + } 213 + 184 214 /// Reads an xattr from a file, stripping the prefix for 185 215 fn read_xattr<D: Deref<Target = File>>(file: &D, key: OsString) -> Option<(String, String)> { 186 216 // this *shouldn't* allocate, but it does O(n) scan the str for UTF-8 correctness 187 217 let parsedkey = key.as_os_str().to_str()?.strip_prefix(XATTRPREFIX)?; 188 218 let valuebytes = file.get_xattr(&key).ok().flatten()?; 189 219 Some((parsedkey.to_string(), String::from_utf8(valuebytes).ok()?)) 220 + } 221 + 222 + fn set_xattr<D: Deref<Target = File>>(file: &D, key: &str, value: &str) -> Result<()> { 223 + let key = if !key.starts_with(XATTRPREFIX) { 224 + format!("{XATTRPREFIX}.{key}") 225 + } else { 226 + key.to_string() 227 + }; 228 + Ok(file.set_xattr(key, value.as_bytes())?) 190 229 } 191 230 192 231 pub fn read_stack(&self) -> Result<TaskStack> {