Compare commits
3 commits
renovate/h
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| eff6016efa | |||
| 7162e52325 | |||
| 4d4118f4ce |
6 changed files with 111 additions and 24 deletions
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Repository
|
- name: Check out Repository
|
||||||
uses: https://git.konstantinfickel.de/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: https://git.konstantinfickel.de/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v6
|
||||||
|
|
||||||
- run: nix --version
|
- run: nix --version
|
||||||
- run: nix flake check
|
- run: nix flake check
|
||||||
|
|
@ -22,6 +22,6 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Repository
|
- name: Check out Repository
|
||||||
uses: https://git.konstantinfickel.de/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: https://git.konstantinfickel.de/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v6
|
||||||
|
|
||||||
- run: nix build
|
- run: nix build
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Repository
|
- name: Check out Repository
|
||||||
uses: https://git.konstantinfickel.de/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: https://git.konstantinfickel.de/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|
|
||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -231,9 +231,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_complete"
|
name = "clap_complete"
|
||||||
version = "4.6.2"
|
version = "4.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb"
|
checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
]
|
]
|
||||||
|
|
@ -1330,9 +1330,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.1"
|
version = "1.52.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,11 @@ pub enum TodoAction {
|
||||||
/// Task number to edit
|
/// Task number to edit
|
||||||
number: usize,
|
number: usize,
|
||||||
},
|
},
|
||||||
/// Mark a task as done
|
/// Mark one or more tasks as done
|
||||||
Done {
|
Done {
|
||||||
/// Task number to mark as done
|
/// Task numbers to mark as done (processed highest-index-first for stable indices)
|
||||||
number: usize,
|
#[arg(required = true, num_args = 1..)]
|
||||||
|
numbers: Vec<usize>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,15 +95,12 @@ pub fn run_edit(number: usize) -> Result<(), StreamdError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_done(number: usize) -> Result<(), StreamdError> {
|
pub fn mark_task_done(task_number: usize, tasks: &[LocalizedShard]) -> Result<(), StreamdError> {
|
||||||
// Always include all tasks for done (user might want to mark a future task as done)
|
if task_number == 0 || task_number > tasks.len() {
|
||||||
let tasks = collect_open_tasks(true)?;
|
return Err(StreamdError::InvalidTaskNumber(task_number, tasks.len()));
|
||||||
|
|
||||||
if number == 0 || number > tasks.len() {
|
|
||||||
return Err(StreamdError::InvalidTaskNumber(number, tasks.len()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let task = &tasks[number - 1];
|
let task = &tasks[task_number - 1];
|
||||||
let file_path = task
|
let file_path = task
|
||||||
.location
|
.location
|
||||||
.get("file")
|
.get("file")
|
||||||
|
|
@ -112,7 +109,6 @@ pub fn run_done(number: usize) -> Result<(), StreamdError> {
|
||||||
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);
|
||||||
|
|
@ -120,7 +116,6 @@ pub fn run_done(number: usize) -> Result<(), StreamdError> {
|
||||||
|
|
||||||
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(
|
||||||
|
|
@ -135,11 +130,9 @@ pub fn run_done(number: usize) -> Result<(), StreamdError> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
lines[task_line_idx] = new_line.clone();
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|
@ -147,7 +140,39 @@ pub fn run_done(number: usize) -> Result<(), StreamdError> {
|
||||||
};
|
};
|
||||||
fs::write(file_path, new_content)?;
|
fs::write(file_path, new_content)?;
|
||||||
|
|
||||||
println!("Marked task {} as done", number);
|
// 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<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(())
|
||||||
}
|
}
|
||||||
|
|
@ -316,4 +341,65 @@ 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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { number }) => streamd::cli::commands::todo::run_done(number)?,
|
Some(TodoAction::Done { numbers }) => streamd::cli::commands::todo::run_done(&numbers)?,
|
||||||
},
|
},
|
||||||
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 }) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue