A file-based task manager
0
fork

Configure Feed

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

Add search-archived flag to find command

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

+144 -21
+5
.tsk/archive/tsk-32.tsk
··· 1 + Use git refs to store tasks 2 + 3 + When we detect we're in a git repo, store tasks in a special ref namespace called tsk 4 + instead of plain files (though if it's easiest, cache files on disk in .tsk/ as is done 5 + now.)
+2 -6
.tsk/index
··· 1 - tsk-30 Add flag to only print IDs in list command 1763257109 2 - tsk-28 Add tool to clean up old tasks not in index 1735006519 3 - tsk-10 foreign workspaces 1732594198 4 - tsk-21 Add command to setup git stuff 1732594198 5 - tsk-8 IMAP4-based sync 1767469318 6 - tsk-17 Add reopen command 1732594198 1 + tsk-32 Use git refs to store tasks 1776481730 7 2 tsk-16 Add ability to search archived tasks with find command 1767466011 8 3 tsk-15 Add link identification to tasks 1732594198 9 4 tsk-9 fix timestamp storage and parsing 1732594198 10 5 tsk-7 allow for creating tasks that don't go to top of stack 1732594198 11 6 tsk-13 user-defined labels 1732594198 12 7 tsk-18 Add reindex command 1735006716 8 + tsk-8 IMAP4-based sync 1767469318
+1 -1
.tsk/next
··· 1 - 32 1 + 33
-1
.tsk/tasks/tsk-10.tsk
··· 1 - ../archive/tsk-10.tsk
-1
.tsk/tasks/tsk-17.tsk
··· 1 - ../archive/tsk-17.tsk
-1
.tsk/tasks/tsk-21.tsk
··· 1 - ../archive/tsk-21.tsk
-1
.tsk/tasks/tsk-28.tsk
··· 1 - ../archive/tsk-28.tsk
-1
.tsk/tasks/tsk-30.tsk
··· 1 - ../archive/tsk-30.tsk
+1
.tsk/tasks/tsk-32.tsk
··· 1 + ../archive/tsk-32.tsk
+1 -1
src/attrs.rs
··· 1 - use std::collections::BTreeMap; 2 1 use std::collections::btree_map::Entry; 3 2 use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter}; 3 + use std::collections::BTreeMap; 4 4 use std::iter::Chain; 5 5 6 6 type Map = BTreeMap<String, String>;
+5 -4
src/main.rs
··· 266 266 /// Exclude the contents of tasks in the search criteria. 267 267 #[arg(short = 'b', default_value_t = false)] 268 268 exclude_body: bool, 269 - /* TODO: implement this 270 269 /// Include archived tasks in the search criteria. Combine with `-b` to include archived 271 270 /// bodies in the search criteria. 272 271 #[arg(short = 'a', default_value_t = false)] 273 272 search_archived: bool, 274 - */ 275 273 } 276 274 277 275 impl From<TaskId> for TaskIdentifier { ··· 281 279 } else if value.find.find { 282 280 TaskIdentifier::Find { 283 281 exclude_body: value.find.args.exclude_body, 284 - archived: false, 282 + archived: value.find.args.search_archived, 285 283 } 286 284 } else { 287 285 TaskIdentifier::Relative(value.relative_id) ··· 342 340 relative_id: 0, 343 341 find: Find { 344 342 find: false, 345 - args: FindArgs { exclude_body: true }, 343 + args: FindArgs { 344 + exclude_body: true, 345 + search_archived: false, 346 + }, 346 347 }, 347 348 } 348 349 }
+129 -4
src/workspace.rs
··· 350 350 &self, 351 351 stack: Option<TaskStack>, 352 352 search_body: bool, 353 - _include_archived: bool, 353 + include_archived: bool, 354 354 ) -> Result<Option<Id>> { 355 355 let stack = if let Some(stack) = stack { 356 356 stack 357 357 } else { 358 358 self.read_stack()? 359 359 }; 360 - if search_body { 360 + if include_archived { 361 + let archive_dir = self.path.join("archive"); 362 + let mut all_tasks: Vec<SearchTask> = stack 363 + .into_iter() 364 + .filter_map(|item| { 365 + self.task(TaskIdentifier::Id(item.id)) 366 + .ok() 367 + .map(|t| t.bare()) 368 + }) 369 + .collect(); 370 + let mut indexed_ids: HashSet<Id> = HashSet::new(); 371 + for t in &all_tasks { 372 + indexed_ids.insert(t.id); 373 + } 374 + if archive_dir.exists() { 375 + for entry in std::fs::read_dir(&archive_dir)? { 376 + let entry = entry?; 377 + let path = entry.path(); 378 + if !path.is_file() { 379 + continue; 380 + } 381 + let filename = entry.file_name(); 382 + let filename_str = filename.to_string_lossy(); 383 + if let Some(id_str) = filename_str 384 + .strip_prefix("tsk-") 385 + .and_then(|s| s.strip_suffix(".tsk")) 386 + { 387 + if let Ok(id_num) = id_str.parse::<u32>() { 388 + let id = Id(id_num); 389 + if !indexed_ids.contains(&id) { 390 + if let Ok(contents) = std::fs::read_to_string(&path) { 391 + let mut lines = contents.splitn(2, '\n'); 392 + let title = lines.next().unwrap_or("").trim().to_string(); 393 + let body = lines.next().unwrap_or("").trim().to_string(); 394 + all_tasks.push(SearchTask { id, title, body }); 395 + } 396 + } 397 + } 398 + } 399 + } 400 + } 401 + if search_body { 402 + Ok(fzf::select::<_, Id, _>( 403 + all_tasks, 404 + [ 405 + "--no-multi-line", 406 + "--accept-nth=1", 407 + "--delimiter=\t", 408 + "--preview=tsk show -T {1}", 409 + "--preview-window=top", 410 + "--ansi", 411 + "--info-command=tsk show -T {1} | head -n1", 412 + "--info=inline-right", 413 + ], 414 + )?) 415 + } else { 416 + Ok(fzf::select::<_, Id, _>( 417 + all_tasks, 418 + ["--delimiter=\t", "--accept-nth=1"], 419 + )?) 420 + } 421 + } else if search_body { 361 422 let loader = LazyTaskLoader { 362 423 files: stack.into_iter(), 363 424 workspace: self, 364 425 }; 365 - // search the entirety of a task 366 426 Ok(fzf::select::<_, Id, _>( 367 427 loader, 368 428 [ ··· 377 437 ], 378 438 )?) 379 439 } else { 380 - // just search the stack 381 440 Ok(fzf::select::<_, Id, _>( 382 441 stack, 383 442 ["--delimiter=\t", "--accept-nth=1"], ··· 920 979 921 980 let result = workspace.reopen(TaskIdentifier::Id(task_id)); 922 981 assert!(result.is_err()); 982 + } 983 + 984 + #[test] 985 + fn test_search_archived_includes_dropped_tasks() { 986 + let (_dir, workspace) = setup_test_workspace(); 987 + 988 + let task_id = { 989 + let ws = Workspace::from_path(workspace.path.clone()).unwrap(); 990 + let task = ws 991 + .new_task("Archived task".to_string(), "archived body".to_string()) 992 + .unwrap(); 993 + let id = task.id; 994 + ws.push_task(task).unwrap(); 995 + id 996 + }; 997 + 998 + let stack_count = { 999 + let stack = workspace.read_stack().unwrap(); 1000 + stack.iter().count() 1001 + }; 1002 + assert_eq!(stack_count, 1); 1003 + 1004 + workspace.drop(TaskIdentifier::Id(task_id)).unwrap(); 1005 + 1006 + let stack_after_drop = { 1007 + let stack = workspace.read_stack().unwrap(); 1008 + stack.iter().count() 1009 + }; 1010 + assert_eq!(stack_after_drop, 0); 1011 + 1012 + let archive_dir = workspace.path.join("archive"); 1013 + assert!(archive_dir.join(format!("tsk-{}.tsk", task_id.0)).exists()); 1014 + 1015 + let archive_tasks_dir = workspace.path.join("tasks"); 1016 + assert!(!archive_tasks_dir 1017 + .join(format!("tsk-{}.tsk", task_id.0)) 1018 + .exists()); 1019 + 1020 + let archived_tasks: Vec<SearchTask> = std::fs::read_dir(&archive_dir) 1021 + .unwrap() 1022 + .filter_map(|entry| { 1023 + let entry = entry.ok()?; 1024 + let path = entry.path(); 1025 + if !path.is_file() { 1026 + return None; 1027 + } 1028 + let filename = entry.file_name(); 1029 + let filename_str = filename.to_string_lossy(); 1030 + let id_str = filename_str 1031 + .strip_prefix("tsk-") 1032 + .and_then(|s| s.strip_suffix(".tsk"))?; 1033 + let id_num = id_str.parse::<u32>().ok()?; 1034 + let contents = std::fs::read_to_string(&path).ok()?; 1035 + let mut lines = contents.splitn(2, '\n'); 1036 + let title = lines.next().unwrap_or("").trim().to_string(); 1037 + let body = lines.next().unwrap_or("").trim().to_string(); 1038 + Some(SearchTask { 1039 + id: Id(id_num), 1040 + title, 1041 + body, 1042 + }) 1043 + }) 1044 + .collect(); 1045 + 1046 + assert_eq!(archived_tasks.len(), 1); 1047 + assert_eq!(archived_tasks[0].title, "Archived task"); 923 1048 } 924 1049 }