diff --git a/Cargo.lock b/Cargo.lock index d414f49..90551b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,9 +231,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.3" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" +checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" dependencies = [ "clap", ] @@ -1330,9 +1330,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.2" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "pin-project-lite", ] diff --git a/flake.lock b/flake.lock index 0c44fe5..d6d3c39 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1775839657, - "narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=", + "lastModified": 1776635034, + "narHash": "sha256-OEOJrT3ZfwbChzODfIH4GzlNTtOFuZFWPtW7jIeR8xU=", "owner": "ipetkov", "repo": "crane", - "rev": "7cf72d978629469c4bd4206b95c402514c1f6000", + "rev": "dc7496d8ea6e526b1254b55d09b966e94673750f", "type": "github" }, "original": { @@ -76,11 +76,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775710090, - "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4c1018dae018162ec878d42fec712642d214fdfa", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { @@ -105,11 +105,11 @@ ] }, "locked": { - "lastModified": 1775963625, - "narHash": "sha256-OmwF0Rd/HDbEGC0ZcBS2jPMvmCcn3HDqUypjXrR7KfM=", + "lastModified": 1776568404, + "narHash": "sha256-Xng/brVgk+0+ggo/4xnaxb5v4lU82RxPpmFlMHXLGYg=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "573a61faa8ec910a6b8576cc3c145844245574f3", + "rev": "e65c31bc66b9194a9fc5b5a9f97ac049523f9438", "type": "github" }, "original": { diff --git a/src/cli/args.rs b/src/cli/args.rs index af1303d..5666337 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -20,11 +20,10 @@ pub enum TodoAction { /// Task number to edit number: usize, }, - /// Mark one or more tasks as done + /// Mark a task as done Done { - /// Task numbers to mark as done (processed highest-index-first for stable indices) - #[arg(required = true, num_args = 1..)] - numbers: Vec, + /// Task number to mark as done + number: usize, }, } diff --git a/src/cli/commands/todo.rs b/src/cli/commands/todo.rs index ca38fbd..66176cf 100644 --- a/src/cli/commands/todo.rs +++ b/src/cli/commands/todo.rs @@ -95,12 +95,15 @@ pub fn run_edit(number: usize) -> Result<(), StreamdError> { Ok(()) } -pub fn mark_task_done(task_number: usize, tasks: &[LocalizedShard]) -> Result<(), StreamdError> { - if task_number == 0 || task_number > tasks.len() { - return Err(StreamdError::InvalidTaskNumber(task_number, tasks.len())); +pub fn run_done(number: usize) -> Result<(), StreamdError> { + // Always include all tasks for done (user might want to mark a future task as done) + let tasks = collect_open_tasks(true)?; + + if number == 0 || number > tasks.len() { + return Err(StreamdError::InvalidTaskNumber(number, tasks.len())); } - let task = &tasks[task_number - 1]; + let task = &tasks[number - 1]; let file_path = task .location .get("file") @@ -109,6 +112,7 @@ pub fn mark_task_done(task_number: usize, tasks: &[LocalizedShard]) -> Result<() let content = fs::read_to_string(file_path)?; let mut lines: Vec = content.lines().map(String::from).collect(); + // Find the line containing @Task (should be at start_line) let task_line_idx = task.start_line.saturating_sub(1); if task_line_idx >= lines.len() { return Err(StreamdError::InvalidLineNumber); @@ -116,6 +120,7 @@ pub fn mark_task_done(task_number: usize, tasks: &[LocalizedShard]) -> Result<() let line = &lines[task_line_idx]; + // Check for multiple @Task occurrences let task_count = line.matches("@Task").count(); if task_count > 1 { return Err(StreamdError::MultipleTaskMarkers( @@ -130,9 +135,11 @@ pub fn mark_task_done(task_number: usize, tasks: &[LocalizedShard]) -> Result<() )); } + // Insert @Done after @Task let new_line = line.replacen("@Task", "@Task @Done", 1); - lines[task_line_idx] = new_line.clone(); + lines[task_line_idx] = new_line; + // Write back to file, preserving trailing newline if present let new_content = if content.ends_with('\n') { format!("{}\n", lines.join("\n")) } else { @@ -140,39 +147,7 @@ pub fn mark_task_done(task_number: usize, tasks: &[LocalizedShard]) -> Result<() }; fs::write(file_path, new_content)?; - // Print the completed task block - let start = task.start_line.saturating_sub(1); - let end = std::cmp::min(task.end_line, lines.len()); - println!( - "Done: [{}] --- {}:{} ---", - task_number, file_path, task.start_line - ); - for line in &lines[start..end] { - println!("{}", line); - } - println!(); - - Ok(()) -} - -pub fn run_done(numbers: &[usize]) -> Result<(), StreamdError> { - let tasks = collect_open_tasks(true)?; - - // Validate all numbers before processing any - for &number in numbers { - if number == 0 || number > tasks.len() { - return Err(StreamdError::InvalidTaskNumber(number, tasks.len())); - } - } - - // Process highest-index-first so earlier indices remain valid - let mut sorted_numbers: Vec = numbers.to_vec(); - sorted_numbers.sort_unstable_by(|a, b| b.cmp(a)); - sorted_numbers.dedup(); - - for number in sorted_numbers { - mark_task_done(number, &tasks)?; - } + println!("Marked task {} as done", number); Ok(()) } @@ -341,65 +316,4 @@ mod tests { "No @Task marker found in /path/file.md:42" ); } - - #[test] - fn test_done_numbers_sorted_highest_first() { - let mut numbers: Vec = vec![1, 3, 2]; - numbers.sort_unstable_by(|a, b| b.cmp(a)); - assert_eq!(numbers, vec![3, 2, 1]); - } - - #[test] - fn test_done_numbers_deduped() { - let mut numbers: Vec = vec![3, 2, 3, 1]; - numbers.sort_unstable_by(|a, b| b.cmp(a)); - numbers.dedup(); - assert_eq!(numbers, vec![3, 2, 1]); - } - - #[test] - fn test_mark_task_done_writes_file_and_prints() { - use std::io::Write; - use tempfile::NamedTempFile; - - let mut tmp = NamedTempFile::new().unwrap(); - writeln!(tmp, "## Fix the thing @Task").unwrap(); - let path = tmp.path().to_str().unwrap().to_string(); - - let now = Utc::now(); - let mut location = IndexMap::new(); - location.insert("file".to_string(), path.clone()); - location.insert("task".to_string(), "open".to_string()); - - let task = LocalizedShard { - markers: vec!["Task".to_string()], - tags: vec![], - start_line: 1, - end_line: 1, - moment: now, - location, - children: vec![], - }; - - let tasks = vec![task]; - mark_task_done(1, &tasks).unwrap(); - - let result = fs::read_to_string(&path).unwrap(); - assert!(result.contains("@Task @Done")); - } - - #[test] - fn test_mark_task_done_invalid_number_zero() { - let tasks = vec![]; - let err = mark_task_done(0, &tasks).unwrap_err(); - assert!(matches!(err, StreamdError::InvalidTaskNumber(0, 0))); - } - - #[test] - fn test_mark_task_done_invalid_number_exceeds() { - let now = Utc::now(); - let tasks = vec![make_task_shard(now, "a.md")]; - let err = mark_task_done(2, &tasks).unwrap_err(); - assert!(matches!(err, StreamdError::InvalidTaskNumber(2, 1))); - } } diff --git a/src/main.rs b/src/main.rs index e7fc178..d543ea5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ fn main() -> miette::Result<()> { }) => match action { None => streamd::cli::commands::todo::run_list(show_future)?, Some(TodoAction::Edit { number }) => streamd::cli::commands::todo::run_edit(number)?, - Some(TodoAction::Done { numbers }) => streamd::cli::commands::todo::run_done(&numbers)?, + Some(TodoAction::Done { number }) => streamd::cli::commands::todo::run_done(number)?, }, Some(Commands::Edit { number }) => streamd::cli::commands::edit::run(number)?, Some(Commands::Timesheet { decimal, debug }) => {