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.
|
/// 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() {
|
||||||
|
|||||||
@@ -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(¬e_path, &numbers),
|
Commands::Up { numbers } => commands::prioritize_items(¬e_path, &numbers),
|
||||||
Commands::Do { numbers } => commands::archive_items(¬e_path, &archive_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::Scan => commands::review_note(¬e_path, &archive_path),
|
||||||
Commands::Edit => commands::edit_note(¬e_path),
|
Commands::Edit => commands::edit_note(¬e_path),
|
||||||
}
|
}
|
||||||
|
|||||||
+145
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user