feat: add rm subcommand
This commit is contained in:
3 files changed
+207
-1
No files matched your search
+55
-1
@@ -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.
|
||||
pub fn review_note(note_path: &Path, archive_path: &Path) -> io::Result<()> {
|
||||
if !note_path.exists() {
|
||||
|
||||
@@ -56,6 +56,12 @@ enum Commands {
|
||||
/// Item numbers to archive (from "parts ls")
|
||||
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
|
||||
#[command(alias = "review")]
|
||||
Scan,
|
||||
@@ -82,6 +88,7 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
Commands::Up { numbers } => commands::prioritize_items(¬e_path, &numbers),
|
||||
Commands::Do { numbers } => commands::archive_items(¬e_path, &archive_path, &numbers),
|
||||
Commands::Rm { numbers } => commands::remove_items(¬e_path, &numbers),
|
||||
Commands::Scan => commands::review_note(¬e_path, &archive_path),
|
||||
Commands::Edit => commands::edit_note(¬e_path),
|
||||
}
|
||||
|
||||
+145
@@ -442,3 +442,148 @@ mod review_tests {
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user