Compare commits

..

1 commit

Author SHA1 Message Date
5076821ba4 chore(deps): lock file maintenance
Some checks failed
Continuous Integration / Lint, Check & Test (push) Failing after 4m54s
Continuous Integration / Build Package (push) Successful in 5m18s
2026-04-20 00:07:58 +00:00
5 changed files with 30 additions and 117 deletions

8
Cargo.lock generated
View file

@ -231,9 +231,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.6.3" version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb"
dependencies = [ dependencies = [
"clap", "clap",
] ]
@ -1330,9 +1330,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.2" version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]

18
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1775839657, "lastModified": 1776635034,
"narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=", "narHash": "sha256-OEOJrT3ZfwbChzODfIH4GzlNTtOFuZFWPtW7jIeR8xU=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "7cf72d978629469c4bd4206b95c402514c1f6000", "rev": "dc7496d8ea6e526b1254b55d09b966e94673750f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -76,11 +76,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1775710090, "lastModified": 1776169885,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa", "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -105,11 +105,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1775963625, "lastModified": 1776568404,
"narHash": "sha256-OmwF0Rd/HDbEGC0ZcBS2jPMvmCcn3HDqUypjXrR7KfM=", "narHash": "sha256-Xng/brVgk+0+ggo/4xnaxb5v4lU82RxPpmFlMHXLGYg=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "573a61faa8ec910a6b8576cc3c145844245574f3", "rev": "e65c31bc66b9194a9fc5b5a9f97ac049523f9438",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -20,11 +20,10 @@ pub enum TodoAction {
/// Task number to edit /// Task number to edit
number: usize, number: usize,
}, },
/// Mark one or more tasks as done /// Mark a task as done
Done { Done {
/// Task numbers to mark as done (processed highest-index-first for stable indices) /// Task number to mark as done
#[arg(required = true, num_args = 1..)] number: usize,
numbers: Vec<usize>,
}, },
} }

View file

@ -95,12 +95,15 @@ pub fn run_edit(number: usize) -> Result<(), StreamdError> {
Ok(()) Ok(())
} }
pub fn mark_task_done(task_number: usize, tasks: &[LocalizedShard]) -> Result<(), StreamdError> { pub fn run_done(number: usize) -> Result<(), StreamdError> {
if task_number == 0 || task_number > tasks.len() { // Always include all tasks for done (user might want to mark a future task as done)
return Err(StreamdError::InvalidTaskNumber(task_number, tasks.len())); 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 let file_path = task
.location .location
.get("file") .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 content = fs::read_to_string(file_path)?;
let mut lines: Vec<String> = content.lines().map(String::from).collect(); let mut lines: Vec<String> = 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); let task_line_idx = task.start_line.saturating_sub(1);
if task_line_idx >= lines.len() { if task_line_idx >= lines.len() {
return Err(StreamdError::InvalidLineNumber); 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]; let line = &lines[task_line_idx];
// Check for multiple @Task occurrences
let task_count = line.matches("@Task").count(); let task_count = line.matches("@Task").count();
if task_count > 1 { if task_count > 1 {
return Err(StreamdError::MultipleTaskMarkers( 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); 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') { let new_content = if content.ends_with('\n') {
format!("{}\n", lines.join("\n")) format!("{}\n", lines.join("\n"))
} else { } else {
@ -140,39 +147,7 @@ pub fn mark_task_done(task_number: usize, tasks: &[LocalizedShard]) -> Result<()
}; };
fs::write(file_path, new_content)?; fs::write(file_path, new_content)?;
// Print the completed task block println!("Marked task {} as done", number);
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<usize> = 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)?;
}
Ok(()) Ok(())
} }
@ -341,65 +316,4 @@ mod tests {
"No @Task marker found in /path/file.md:42" "No @Task marker found in /path/file.md:42"
); );
} }
#[test]
fn test_done_numbers_sorted_highest_first() {
let mut numbers: Vec<usize> = 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<usize> = 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)));
}
} }

View file

@ -12,7 +12,7 @@ fn main() -> miette::Result<()> {
}) => match action { }) => match action {
None => streamd::cli::commands::todo::run_list(show_future)?, None => streamd::cli::commands::todo::run_list(show_future)?,
Some(TodoAction::Edit { number }) => streamd::cli::commands::todo::run_edit(number)?, 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::Edit { number }) => streamd::cli::commands::edit::run(number)?,
Some(Commands::Timesheet { decimal, debug }) => { Some(Commands::Timesheet { decimal, debug }) => {