feat: add rm subcommand

This commit is contained in:
swaits committed 2025-07-27 14:48:04 -04:00
1 parent de6b4f4d30
commit 5c56eab61c
3 files changed
+207 -1

No files matched your search

+55 -1
View File
@@ -214,7 +214,61 @@ pub fn archive_items(note_path: &Path, archive_path: &Path, numbers: &[usize]) -
} }
} }
/// Interactive review mode that presents each item for user decision. /// Removes specified items from the note file without archiving them.
/// Takes 1-based item numbers, validates them, and permanently deletes them.
pub fn remove_items(note_path: &Path, numbers: &[usize]) -> io::Result<()> {
if !note_path.exists() {
println!(
"{}No notes found.{}",
color::Fg(color::Yellow),
color::Fg(color::Reset)
);
return Ok(());
}
let content = fs::read_to_string(note_path)?;
let lines: Vec<String> = content.lines().map(str::to_string).collect();
if lines.is_empty() {
println!(
"{}No notes to remove.{}",
color::Fg(color::Yellow),
color::Fg(color::Reset)
);
return Ok(());
}
match archive_items_in_content(&lines, numbers) {
Ok((new_content, removed_items)) => {
fs::write(note_path, new_content)?;
println!(
"{}✓ Removed {} item(s){}",
color::Fg(color::Green),
removed_items.len(),
color::Fg(color::Reset)
);
for item in removed_items {
println!(
" {}{}{}",
color::Fg(color::Red),
item,
color::Fg(color::Reset)
);
}
Ok(())
}
Err(msg) => {
println!(
"{}{}{}",
color::Fg(color::Red),
msg,
color::Fg(color::Reset)
);
Ok(())
}
}
}
/// Supports prioritizing, archiving, skipping items with keyboard navigation and undo functionality. /// Supports prioritizing, archiving, skipping items with keyboard navigation and undo functionality.
pub fn review_note(note_path: &Path, archive_path: &Path) -> io::Result<()> { pub fn review_note(note_path: &Path, archive_path: &Path) -> io::Result<()> {
if !note_path.exists() { if !note_path.exists() {
+7
View File
@@ -56,6 +56,12 @@ enum Commands {
/// Item numbers to archive (from "parts ls") /// Item numbers to archive (from "parts ls")
numbers: Vec<usize>, numbers: Vec<usize>,
}, },
/// Remove items without archiving
#[command(aliases = ["remove", "delete", "destroy", "forget"])]
Rm {
/// Item numbers to remove (from "parts ls")
numbers: Vec<usize>,
},
/// Scan and review items interactively, from top to bottom /// Scan and review items interactively, from top to bottom
#[command(alias = "review")] #[command(alias = "review")]
Scan, Scan,
@@ -82,6 +88,7 @@ fn main() -> io::Result<()> {
} }
Commands::Up { numbers } => commands::prioritize_items(&note_path, &numbers), Commands::Up { numbers } => commands::prioritize_items(&note_path, &numbers),
Commands::Do { numbers } => commands::archive_items(&note_path, &archive_path, &numbers), Commands::Do { numbers } => commands::archive_items(&note_path, &archive_path, &numbers),
Commands::Rm { numbers } => commands::remove_items(&note_path, &numbers),
Commands::Scan => commands::review_note(&note_path, &archive_path), Commands::Scan => commands::review_note(&note_path, &archive_path),
Commands::Edit => commands::edit_note(&note_path), Commands::Edit => commands::edit_note(&note_path),
} }
+145
View File
@@ -442,3 +442,148 @@ mod review_tests {
assert_eq!(prioritized, expected_prioritized); assert_eq!(prioritized, expected_prioritized);
} }
} }
#[cfg(test)]
mod remove_tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
/// Helper function to create temporary test files with content
fn create_test_file_with_content(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", content).unwrap();
file
}
/// Tests removing a single item
#[test]
fn test_remove_single_item() {
let content = "First item\nSecond item\nThird item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[2]).unwrap();
});
assert!(result.is_ok());
// Verify the item was removed from the file
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "First item\nThird item\n");
}
/// Tests removing multiple items
#[test]
fn test_remove_multiple_items() {
let content = "First item\nSecond item\nThird item\nFourth item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[1, 3]).unwrap();
});
assert!(result.is_ok());
// Verify the items were removed from the file
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "Second item\nFourth item\n");
}
/// Tests removing all items results in empty file
#[test]
fn test_remove_all_items() {
let content = "First item\nSecond item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[1, 2]).unwrap();
});
assert!(result.is_ok());
// Verify the file is empty
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "");
}
/// Tests handling of invalid item numbers
#[test]
fn test_remove_invalid_item_number() {
let content = "First item\nSecond item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[3]).unwrap();
});
assert!(result.is_ok());
// Verify the file content is unchanged
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, content);
}
/// Tests handling of duplicate item numbers
#[test]
fn test_remove_duplicate_item_numbers() {
let content = "First item\nSecond item\nThird item\n";
let file = create_test_file_with_content(content);
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[2, 2, 1]).unwrap();
});
assert!(result.is_ok());
// Verify only unique items were removed
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "Third item\n");
}
/// Tests handling of non-existent file
#[test]
fn test_remove_from_nonexistent_file() {
let temp_dir = tempfile::tempdir().unwrap();
let nonexistent_path = temp_dir.path().join("nonexistent.txt");
let result = std::panic::catch_unwind(|| {
remove_items(&nonexistent_path, &[1]).unwrap();
});
assert!(result.is_ok());
// Verify no file was created
assert!(!nonexistent_path.exists());
}
/// Tests handling of empty file
#[test]
fn test_remove_from_empty_file() {
let file = create_test_file_with_content("");
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[1]).unwrap();
});
assert!(result.is_ok());
// Verify the file remains empty
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "");
}
/// Tests that remove doesn't create an archive file (unlike do command)
#[test]
fn test_remove_does_not_archive() {
let content = "First item\nSecond item\n";
let file = create_test_file_with_content(content);
let temp_dir = tempfile::tempdir().unwrap();
let archive_path = temp_dir.path().join("archive.txt");
let result = std::panic::catch_unwind(|| {
remove_items(file.path(), &[1]).unwrap();
});
assert!(result.is_ok());
// Verify no archive file was created
assert!(!archive_path.exists());
// Verify the item was removed from the original file
let updated_content = std::fs::read_to_string(file.path()).unwrap();
assert_eq!(updated_content, "Second item\n");
}
}