A file-based task manager
0
fork

Configure Feed

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

Add reopen command to restore archived tasks

- tsk-17: Add tsk reopen command to recreate symlinks and re-add archived tasks to the stack
- Add tests for reopen: successful reopen, nonexistent task, already open task

+107 -2
+16
src/main.rs
··· 190 190 #[arg(short = 'g', default_value_t = false)] 191 191 gitignore: bool, 192 192 }, 193 + 194 + /// Reopens an archived task, recreating the symlink and adding it back to the stack. 195 + Reopen { 196 + #[command(flatten)] 197 + task_id: TaskId, 198 + }, 193 199 } 194 200 195 201 #[derive(Subcommand)] ··· 317 323 Commands::Clean => command_clean(dir), 318 324 Commands::Remote { action } => command_remote(dir, action), 319 325 Commands::GitSetup { gitignore } => command_git_setup(dir, gitignore), 326 + Commands::Reopen { task_id } => command_reopen(dir, task_id), 320 327 }; 321 328 let result = var_name; 322 329 match result { ··· 616 623 eprintln!("Added .tsk/ to {label}."); 617 624 Ok(()) 618 625 } 626 + 627 + fn command_reopen(dir: PathBuf, task_id: TaskId) -> Result<()> { 628 + let workspace = Workspace::from_path(dir)?; 629 + let id: TaskIdentifier = task_id.into(); 630 + let reopened_id = workspace.reopen(id)?; 631 + eprintln!("Reopened "); 632 + println!("{reopened_id}"); 633 + Ok(()) 634 + }
+2 -2
src/stack.rs
··· 4 4 5 5 use crate::errors::{Error, Result}; 6 6 use crate::util; 7 - use std::collections::VecDeque; 8 7 use std::collections::vec_deque::Iter; 8 + use std::collections::VecDeque; 9 9 use std::fmt::Display; 10 10 use std::fs::File; 11 11 use std::io::{self, BufRead, BufReader, Seek, Write}; ··· 98 98 impl StackItem { 99 99 /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the 100 100 /// files: task id title 101 - fn from_line(workspace_path: &Path, line: String) -> Result<Self> { 101 + pub fn from_line(workspace_path: &Path, line: String) -> Result<Self> { 102 102 let mut stack_item: StackItem = line.parse()?; 103 103 104 104 let task = util::flopen(
+89
src/workspace.rs
··· 513 513 let task = workspace.task(TaskIdentifier::Id(Id(id)))?; 514 514 Ok(Some(task)) 515 515 } 516 + 517 + pub fn reopen(&self, identifier: TaskIdentifier) -> Result<Id> { 518 + let id = self.resolve(identifier)?; 519 + let archive_path = self.path.join("archive").join(id.filename()); 520 + if !archive_path.exists() { 521 + return Err(Error::Parse(format!("Task {id} not found in archive"))); 522 + } 523 + let tasks_path = self.path.join("tasks").join(id.filename()); 524 + if tasks_path.exists() { 525 + return Err(Error::Parse(format!("Task {id} is already open"))); 526 + } 527 + symlink(PathBuf::from("../archive").join(id.filename()), &tasks_path)?; 528 + let mut stack = self.read_stack()?; 529 + let title = std::fs::read_to_string(&archive_path)? 530 + .lines() 531 + .next() 532 + .unwrap_or("") 533 + .trim() 534 + .to_string(); 535 + let modify_time = std::fs::metadata(&archive_path)?.modified()?; 536 + let stack_item = StackItem { 537 + id, 538 + title: title.replace('\t', " "), 539 + modify_time, 540 + }; 541 + stack.push(stack_item); 542 + stack.save()?; 543 + Ok(id) 544 + } 516 545 } 517 546 518 547 pub struct Task { ··· 831 860 let content = std::fs::read_to_string(&exclude_path).unwrap(); 832 861 let already_present = content.lines().any(|line| line.trim() == ".tsk/"); 833 862 assert!(already_present); 863 + } 864 + 865 + #[test] 866 + fn test_reopen_archived_task() { 867 + let (_dir, workspace) = setup_test_workspace(); 868 + 869 + let task_id = { 870 + let ws = Workspace::from_path(workspace.path.clone()).unwrap(); 871 + let task = ws 872 + .new_task("Task to reopen".to_string(), "body".to_string()) 873 + .unwrap(); 874 + let id = task.id; 875 + ws.push_task(task).unwrap(); 876 + id 877 + }; 878 + 879 + workspace.drop(TaskIdentifier::Id(task_id)).unwrap(); 880 + 881 + { 882 + let stack_after_drop = workspace.read_stack().unwrap(); 883 + assert_eq!(stack_after_drop.iter().count(), 0); 884 + } 885 + 886 + let tasks_dir = workspace.path.join("tasks"); 887 + let task_link = tasks_dir.join(task_id.filename()); 888 + assert!(!task_link.exists(), "symlink should be removed on drop"); 889 + 890 + workspace.reopen(TaskIdentifier::Id(task_id)).unwrap(); 891 + 892 + { 893 + let stack_after_reopen = workspace.read_stack().unwrap(); 894 + assert_eq!(stack_after_reopen.iter().count(), 1); 895 + } 896 + assert!(task_link.exists(), "symlink should be recreated on reopen"); 897 + } 898 + 899 + #[test] 900 + fn test_reopen_nonexistent_task_fails() { 901 + let (_dir, workspace) = setup_test_workspace(); 902 + 903 + let result = workspace.reopen(TaskIdentifier::Id(Id(999))); 904 + assert!(result.is_err()); 905 + } 906 + 907 + #[test] 908 + fn test_reopen_already_open_task_fails() { 909 + let (_dir, workspace) = setup_test_workspace(); 910 + 911 + let task_id = { 912 + let ws = Workspace::from_path(workspace.path.clone()).unwrap(); 913 + let task = ws 914 + .new_task("Open task".to_string(), "body".to_string()) 915 + .unwrap(); 916 + let id = task.id; 917 + ws.push_task(task).unwrap(); 918 + id 919 + }; 920 + 921 + let result = workspace.reopen(TaskIdentifier::Id(task_id)); 922 + assert!(result.is_err()); 834 923 } 835 924 }