A file-based task manager
0
fork

Configure Feed

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

Auto-manage a status property on every task

new_task initialises status=open in the task tree and the per-key index;
drop flips it to status=done and removes the task from the queue while
keeping the namespace binding so the human id stays addressable. Means
tsk prop find status {open,done} now lists in-progress vs completed
tasks across the workspace.

Tests: workspace::new_task_starts_open_drop_marks_done covers the full
round trip via the property index.

Note: tasks created before this commit have no status property and will
not appear in either filter until they are next saved or explicitly set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+55 -6
+1
src/namespace.rs
··· 149 149 Ok(human) 150 150 } 151 151 152 + #[allow(dead_code)] // kept for future "tsk forget" / hard-delete command 152 153 pub fn unassign_id(repo: &Repository, name: &str, human: u32, message: &str) -> Result<()> { 153 154 let mut ns = read(repo, name)?; 154 155 if ns.mapping.remove(&human).is_some() {
+54 -6
src/workspace.rs
··· 18 18 19 19 const NAMESPACE_FILE: &str = "namespace"; 20 20 const QUEUE_FILE: &str = "queue"; 21 + /// Auto-managed property holding the task's lifecycle state. Set to 22 + /// `STATUS_OPEN` on creation and flipped to `STATUS_DONE` by [`Workspace::drop`]. 23 + pub const STATUS_KEY: &str = "status"; 24 + pub const STATUS_OPEN: &str = "open"; 25 + pub const STATUS_DONE: &str = "done"; 21 26 /// User-local state lives under `<git-dir>/<STATE_DIR>/` so it isn't tracked 22 27 /// by the enclosing repo (the `.git/` directory is by definition not in the 23 28 /// working tree). Each clone gets its own active namespace + queue. ··· 224 229 } else { 225 230 format!("{}\n\n{}", title.trim(), body.trim()) 226 231 }; 227 - let task_obj = TaskObj::new(content); 232 + let mut task_obj = TaskObj::new(content); 233 + task_obj 234 + .properties 235 + .insert(STATUS_KEY.into(), vec![STATUS_OPEN.into()]); 228 236 let stable = object::create(&repo, &task_obj, "create")?; 237 + properties::reindex_task(&repo, &stable, &task_obj.properties)?; 229 238 let human = namespace::assign_id(&repo, &self.namespace(), stable.clone(), "assign-id")?; 230 239 Ok(Task { 231 240 id: Id(human), 232 241 stable, 233 242 title: task_obj.title().to_string(), 234 243 body: task_obj.body().to_string(), 235 - attributes: BTreeMap::new(), 244 + attributes: task_obj.properties, 236 245 }) 237 246 } 238 247 ··· 395 404 Ok(out) 396 405 } 397 406 398 - /// Drop a task from the active queue and unbind its human id in the 399 - /// active namespace. The task object's commit history at 400 - /// `refs/tsk/tasks/<stable>` is preserved. 407 + /// Drop a task from the active queue and mark it `status=done`. The 408 + /// namespace binding is kept so the task remains addressable by its 409 + /// human id (and discoverable via `tsk prop find status done`); the 410 + /// task object's commit history is preserved either way. 401 411 pub fn drop(&self, identifier: TaskIdentifier) -> Result<Option<Id>> { 402 412 let (id, stable) = self.resolve(identifier)?; 403 413 let repo = self.repo()?; 404 414 queue::remove(&repo, &self.queue(), &stable, "drop")?; 405 - namespace::unassign_id(&repo, &self.namespace(), id.0, "drop")?; 415 + // Flip status=done in the task's tree + index. 416 + let mut task = self.task(TaskIdentifier::Id(id))?; 417 + task.attributes 418 + .insert(STATUS_KEY.into(), vec![STATUS_DONE.into()]); 419 + self.save_task(&task)?; 406 420 Ok(Some(id)) 407 421 } 408 422 ··· 803 817 assert_eq!(pulled.0, id.0); 804 818 let stack = ws.read_stack().unwrap(); 805 819 assert_eq!(stack.len(), 1); 820 + } 821 + 822 + #[test] 823 + fn new_task_starts_open_drop_marks_done() { 824 + let (_d, ws) = fresh_workspace(); 825 + let t = ws.new_task("a".into(), "".into()).unwrap(); 826 + assert_eq!( 827 + t.attributes.get(STATUS_KEY), 828 + Some(&vec![STATUS_OPEN.to_string()]) 829 + ); 830 + let id = t.id; 831 + ws.push_task(t).unwrap(); 832 + 833 + // Index reflects the new open task. 834 + let opens = ws 835 + .find_by_property(STATUS_KEY, Some(STATUS_OPEN)) 836 + .unwrap(); 837 + assert_eq!(opens.len(), 1); 838 + assert_eq!(opens[0].0, id); 839 + 840 + ws.drop(TaskIdentifier::Id(id)).unwrap(); 841 + 842 + // Status flipped, queue empty, namespace binding kept. 843 + let read = ws.task(TaskIdentifier::Id(id)).unwrap(); 844 + assert_eq!( 845 + read.attributes.get(STATUS_KEY), 846 + Some(&vec![STATUS_DONE.to_string()]) 847 + ); 848 + assert!(ws.read_stack().unwrap().is_empty()); 849 + let dones = ws 850 + .find_by_property(STATUS_KEY, Some(STATUS_DONE)) 851 + .unwrap(); 852 + assert_eq!(dones.len(), 1); 853 + assert_eq!(dones[0].0, id); 806 854 } 807 855 808 856 #[test]