Smoother Todo Editing #63

Closed
opened 2026-03-29 19:44:58 +02:00 by kfickel · 1 comment
Owner

every entry a number on streamd todo, and with streamd todo 4 edit, one can edit entry 4 (like with streamd new), and with streamd todo 4 donea @Done is inserted directly after @Task.

Also, if the flag --show-future is not set, all tasks with a date in the LocalizedDate set in the future won't be shown.

Test the feature thoroughly.

every entry a number on `streamd todo`, and with `streamd todo 4 edit`, one can edit entry 4 (like with streamd new), and with `streamd todo 4 done`a `@Done` is inserted directly after `@Task`. Also, if the flag --show-future is not set, all tasks with a date in the LocalizedDate set in the future won't be shown. Test the feature thoroughly.
Author
Owner

Implementation Plan

Overview

Enhance the streamd todo command to support task numbering, inline editing, marking tasks as done, and filtering out future tasks.

Requirements Summary

  • Display numbered tasks with streamd todo
  • Edit task N with streamd todo N edit (opens full file at task line)
  • Mark task N as done with streamd todo N done (inserts @Done after @Task)
  • Hide future tasks by default (based on moment field vs current UTC time)
  • Add --show-future flag to include future tasks in listing

Implementation Steps

1. Modify CLI Arguments (src/cli/args.rs)

Update the Todo command to accept subcommands:

/// Display open tasks
Todo {
    /// Show tasks with dates in the future
    #[arg(long)]
    show_future: bool,

    #[command(subcommand)]
    action: Option<TodoAction>,
},

Add a new enum for todo actions:

#[derive(Subcommand)]
pub enum TodoAction {
    /// Edit a task by its number
    Edit {
        /// Task number to edit
        number: usize,
    },
    /// Mark a task as done
    Done {
        /// Task number to mark as done
        number: usize,
    },
}

Alternative (simpler): Use positional arguments instead of subcommands:

Todo {
    #[arg(long)]
    show_future: bool,

    /// Task number (optional)
    number: Option<usize>,

    /// Action: 'edit' or 'done'
    action: Option<String>,
},

2. Refactor Todo Command (src/cli/commands/todo.rs)

2.1 Add helper to collect and sort tasks:

fn collect_open_tasks(show_future: bool) -> Result<Vec<LocalizedShard>, StreamdError> {
    let all_shards = all_files()?;
    let now = Utc::now();
    
    let mut tasks: Vec<LocalizedShard> = find_shard_by_position(&all_shards, "task", "open")
        .into_iter()
        .filter(|shard| show_future || shard.moment <= now)
        .collect();
    
    // Sort by moment ascending (oldest first = task 1)
    tasks.sort_by(|a, b| a.moment.cmp(&b.moment));
    
    tasks
}

2.2 Modify the list display to show numbers:

pub fn run_list(show_future: bool) -> Result<(), StreamdError> {
    let tasks = collect_open_tasks(show_future)?;
    
    for (index, task_shard) in tasks.iter().enumerate() {
        let task_number = index + 1; // 1-indexed
        if let Some(file_path) = task_shard.location.get("file") {
            let content = fs::read_to_string(file_path)?;
            let lines: Vec<&str> = content.lines().collect();
            
            let start = task_shard.start_line.saturating_sub(1);
            let end = std::cmp::min(task_shard.end_line, lines.len());
            
            println!("[{}] --- {}:{} ---", task_number, file_path, task_shard.start_line);
            for line in &lines[start..end] {
                println!("{}", line);
            }
            println!();
        }
    }
    
    Ok(())
}

3. Implement todo N edit (src/cli/commands/todo.rs)

pub fn run_edit(number: usize, show_future: bool) -> Result<(), StreamdError> {
    let tasks = collect_open_tasks(true)?; // Always include all tasks for edit
    
    if number == 0 || number > tasks.len() {
        return Err(StreamdError::InvalidTaskNumber(number, tasks.len()));
    }
    
    let task = &tasks[number - 1]; // Convert to 0-indexed
    let file_path = task.location.get("file")
        .ok_or_else(|| StreamdError::MissingFilePath)?;
    
    let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
    let line_arg = format!("+{}", task.start_line);
    
    let status = Command::new(&editor)
        .arg(&line_arg)
        .arg(file_path)
        .status()?;
    
    if !status.success() {
        return Err(StreamdError::IoError(std::io::Error::other(
            "Editor exited with non-zero status",
        )));
    }
    
    Ok(())
}

4. Implement todo N done (src/cli/commands/todo.rs)

pub fn run_done(number: usize, show_future: bool) -> Result<(), StreamdError> {
    let tasks = collect_open_tasks(true)?; // Always include all tasks for done
    
    if number == 0 || number > tasks.len() {
        return Err(StreamdError::InvalidTaskNumber(number, tasks.len()));
    }
    
    let task = &tasks[number - 1];
    let file_path = task.location.get("file")
        .ok_or_else(|| StreamdError::MissingFilePath)?;
    
    let content = fs::read_to_string(file_path)?;
    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);
    if task_line_idx >= lines.len() {
        return Err(StreamdError::InvalidLineNumber);
    }
    
    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(file_path.clone(), task.start_line));
    }
    if task_count == 0 {
        return Err(StreamdError::NoTaskMarker(file_path.clone(), task.start_line));
    }
    
    // Insert @Done after @Task
    let new_line = line.replacen("@Task", "@Task @Done", 1);
    lines[task_line_idx] = new_line;
    
    // Write back to file
    let new_content = lines.join("\n");
    fs::write(file_path, new_content)?;
    
    println!("Marked task {} as done", number);
    
    Ok(())
}

5. Add New Error Variants (src/errors.rs)

pub enum StreamdError {
    // ... existing variants ...
    
    #[error("Invalid task number {0}: only {1} tasks available")]
    InvalidTaskNumber(usize, usize),
    
    #[error("Task shard missing file path")]
    MissingFilePath,
    
    #[error("Invalid line number in task")]
    InvalidLineNumber,
    
    #[error("Multiple @Task markers found in {0}:{1} - cannot auto-insert @Done")]
    MultipleTaskMarkers(String, usize),
    
    #[error("No @Task marker found in {0}:{1}")]
    NoTaskMarker(String, usize),
}

6. Update Main Router (src/main.rs)

Some(Commands::Todo { show_future, action }) => {
    match action {
        None => streamd::cli::commands::todo::run_list(show_future)?,
        Some(TodoAction::Edit { number }) => {
            streamd::cli::commands::todo::run_edit(number, show_future)?
        }
        Some(TodoAction::Done { number }) => {
            streamd::cli::commands::todo::run_done(number, show_future)?
        }
    }
}

7. Testing Strategy

Unit Tests:

  • Test collect_open_tasks() with various shard configurations
  • Test future filtering logic with mocked current time
  • Test @Done insertion with various line formats
  • Test error cases (multiple @Task, missing @Task, invalid numbers)

Integration Tests:

  • Create temporary markdown files with tasks
  • Run streamd todo and verify numbered output
  • Run streamd todo N done and verify file modification
  • Test --show-future flag behavior

Edge Cases to Test:

  • Task at line 1 of file
  • Task at last line of file
  • File with single task
  • Empty task list
  • Task number 0 (should error)
  • Task number exceeding count (should error)
  • Line with @Task in content (not as marker) - should this be handled?
  • Preserving trailing newline in file after modification

8. Documentation Updates

Update README.md and REQUIREMENTS.md:

  • Document new streamd todo numbered output
  • Document streamd todo N edit command
  • Document streamd todo N done command
  • Document --show-future flag

File Changes Summary

File Changes
src/cli/args.rs Add TodoAction enum, update Todo command
src/cli/commands/todo.rs Add run_list, run_edit, run_done functions
src/main.rs Update command routing for new todo subcommands
src/errors.rs Add new error variants
README.md Document new commands
REQUIREMENTS.md Add formal specification for new features

Design Decisions (Clarified)

  1. Task numbering: Oldest task = 1 (sorted by moment ascending)
  2. Edit behavior: Opens full file with +LINE argument for cursor positioning
  3. Done insertion: @Done inserted immediately after @Task on the same line
  4. Future filtering: Only affects list view; edit/done can target any task by number
  5. Future check: Compares LocalizedShard.moment against Utc::now()
  6. Multiple @Task: Error and abort if multiple @Task markers found on same line
## Implementation Plan ### Overview Enhance the `streamd todo` command to support task numbering, inline editing, marking tasks as done, and filtering out future tasks. ### Requirements Summary - Display numbered tasks with `streamd todo` - Edit task N with `streamd todo N edit` (opens full file at task line) - Mark task N as done with `streamd todo N done` (inserts `@Done` after `@Task`) - Hide future tasks by default (based on `moment` field vs current UTC time) - Add `--show-future` flag to include future tasks in listing ### Implementation Steps #### 1. Modify CLI Arguments (`src/cli/args.rs`) Update the `Todo` command to accept subcommands: ```rust /// Display open tasks Todo { /// Show tasks with dates in the future #[arg(long)] show_future: bool, #[command(subcommand)] action: Option<TodoAction>, }, ``` Add a new enum for todo actions: ```rust #[derive(Subcommand)] pub enum TodoAction { /// Edit a task by its number Edit { /// Task number to edit number: usize, }, /// Mark a task as done Done { /// Task number to mark as done number: usize, }, } ``` **Alternative (simpler):** Use positional arguments instead of subcommands: ```rust Todo { #[arg(long)] show_future: bool, /// Task number (optional) number: Option<usize>, /// Action: 'edit' or 'done' action: Option<String>, }, ``` #### 2. Refactor Todo Command (`src/cli/commands/todo.rs`) **2.1 Add helper to collect and sort tasks:** ```rust fn collect_open_tasks(show_future: bool) -> Result<Vec<LocalizedShard>, StreamdError> { let all_shards = all_files()?; let now = Utc::now(); let mut tasks: Vec<LocalizedShard> = find_shard_by_position(&all_shards, "task", "open") .into_iter() .filter(|shard| show_future || shard.moment <= now) .collect(); // Sort by moment ascending (oldest first = task 1) tasks.sort_by(|a, b| a.moment.cmp(&b.moment)); tasks } ``` **2.2 Modify the list display to show numbers:** ```rust pub fn run_list(show_future: bool) -> Result<(), StreamdError> { let tasks = collect_open_tasks(show_future)?; for (index, task_shard) in tasks.iter().enumerate() { let task_number = index + 1; // 1-indexed if let Some(file_path) = task_shard.location.get("file") { let content = fs::read_to_string(file_path)?; let lines: Vec<&str> = content.lines().collect(); let start = task_shard.start_line.saturating_sub(1); let end = std::cmp::min(task_shard.end_line, lines.len()); println!("[{}] --- {}:{} ---", task_number, file_path, task_shard.start_line); for line in &lines[start..end] { println!("{}", line); } println!(); } } Ok(()) } ``` #### 3. Implement `todo N edit` (`src/cli/commands/todo.rs`) ```rust pub fn run_edit(number: usize, show_future: bool) -> Result<(), StreamdError> { let tasks = collect_open_tasks(true)?; // Always include all tasks for edit if number == 0 || number > tasks.len() { return Err(StreamdError::InvalidTaskNumber(number, tasks.len())); } let task = &tasks[number - 1]; // Convert to 0-indexed let file_path = task.location.get("file") .ok_or_else(|| StreamdError::MissingFilePath)?; let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); let line_arg = format!("+{}", task.start_line); let status = Command::new(&editor) .arg(&line_arg) .arg(file_path) .status()?; if !status.success() { return Err(StreamdError::IoError(std::io::Error::other( "Editor exited with non-zero status", ))); } Ok(()) } ``` #### 4. Implement `todo N done` (`src/cli/commands/todo.rs`) ```rust pub fn run_done(number: usize, show_future: bool) -> Result<(), StreamdError> { let tasks = collect_open_tasks(true)?; // Always include all tasks for done if number == 0 || number > tasks.len() { return Err(StreamdError::InvalidTaskNumber(number, tasks.len())); } let task = &tasks[number - 1]; let file_path = task.location.get("file") .ok_or_else(|| StreamdError::MissingFilePath)?; let content = fs::read_to_string(file_path)?; 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); if task_line_idx >= lines.len() { return Err(StreamdError::InvalidLineNumber); } 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(file_path.clone(), task.start_line)); } if task_count == 0 { return Err(StreamdError::NoTaskMarker(file_path.clone(), task.start_line)); } // Insert @Done after @Task let new_line = line.replacen("@Task", "@Task @Done", 1); lines[task_line_idx] = new_line; // Write back to file let new_content = lines.join("\n"); fs::write(file_path, new_content)?; println!("Marked task {} as done", number); Ok(()) } ``` #### 5. Add New Error Variants (`src/errors.rs`) ```rust pub enum StreamdError { // ... existing variants ... #[error("Invalid task number {0}: only {1} tasks available")] InvalidTaskNumber(usize, usize), #[error("Task shard missing file path")] MissingFilePath, #[error("Invalid line number in task")] InvalidLineNumber, #[error("Multiple @Task markers found in {0}:{1} - cannot auto-insert @Done")] MultipleTaskMarkers(String, usize), #[error("No @Task marker found in {0}:{1}")] NoTaskMarker(String, usize), } ``` #### 6. Update Main Router (`src/main.rs`) ```rust Some(Commands::Todo { show_future, action }) => { match action { None => streamd::cli::commands::todo::run_list(show_future)?, Some(TodoAction::Edit { number }) => { streamd::cli::commands::todo::run_edit(number, show_future)? } Some(TodoAction::Done { number }) => { streamd::cli::commands::todo::run_done(number, show_future)? } } } ``` ### 7. Testing Strategy **Unit Tests:** - Test `collect_open_tasks()` with various shard configurations - Test future filtering logic with mocked current time - Test `@Done` insertion with various line formats - Test error cases (multiple @Task, missing @Task, invalid numbers) **Integration Tests:** - Create temporary markdown files with tasks - Run `streamd todo` and verify numbered output - Run `streamd todo N done` and verify file modification - Test `--show-future` flag behavior **Edge Cases to Test:** - Task at line 1 of file - Task at last line of file - File with single task - Empty task list - Task number 0 (should error) - Task number exceeding count (should error) - Line with `@Task` in content (not as marker) - should this be handled? - Preserving trailing newline in file after modification ### 8. Documentation Updates Update `README.md` and `REQUIREMENTS.md`: - Document new `streamd todo` numbered output - Document `streamd todo N edit` command - Document `streamd todo N done` command - Document `--show-future` flag ### File Changes Summary | File | Changes | |------|---------| | `src/cli/args.rs` | Add `TodoAction` enum, update `Todo` command | | `src/cli/commands/todo.rs` | Add `run_list`, `run_edit`, `run_done` functions | | `src/main.rs` | Update command routing for new todo subcommands | | `src/errors.rs` | Add new error variants | | `README.md` | Document new commands | | `REQUIREMENTS.md` | Add formal specification for new features | ### Design Decisions (Clarified) 1. **Task numbering**: Oldest task = 1 (sorted by `moment` ascending) 2. **Edit behavior**: Opens full file with `+LINE` argument for cursor positioning 3. **Done insertion**: `@Done` inserted immediately after `@Task` on the same line 4. **Future filtering**: Only affects list view; edit/done can target any task by number 5. **Future check**: Compares `LocalizedShard.moment` against `Utc::now()` 6. **Multiple @Task**: Error and abort if multiple `@Task` markers found on same line
kfickel added the
planned
label 2026-03-29 19:52:58 +02:00
Sign in to join this conversation.
No labels
planned
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference: kfickel/streamd#63
No description provided.