From 263b033035a4dd32fa9c2b08989f34ef982b5440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 23 Feb 2025 16:56:12 +0100 Subject: [PATCH] feat: Add shell completions using clap_complete_command --- Cargo.toml | 1 + src/main.rs | 189 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 123 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d132488..920d17b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] thiserror = "1.0" clap = { version = "4.4", features = ["derive"] } +clap_complete_command = "0.5.1" fs-err = "2.11" [dev-dependencies] diff --git a/src/main.rs b/src/main.rs index deb2912..687f573 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ -mod tag_engine; -mod symlink; +use clap::CommandFactory; mod error; +mod symlink; +mod tag_engine; -use std::collections::BTreeSet; -use std::path::PathBuf; +use crate::error::FileTagsError; use clap::{Parser, Subcommand}; use fs_err as fs; -use crate::error::FileTagsError; +use std::collections::BTreeSet; +use std::path::PathBuf; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -16,7 +17,13 @@ struct Cli { } #[derive(Subcommand)] +#[command(subcommand_required = true)] enum Commands { + /// Generate shell completions + Completion { + #[arg(value_enum)] + shell: clap_complete_command::Shell, + }, /// List all unique tags found in files List { /// Files to process @@ -47,7 +54,11 @@ enum Commands { #[arg(long, required = true, help = "Target directory for creating the tree")] dir: String, /// Maximum depth of the tree - #[arg(long, default_value = "3", help = "Maximum depth of the directory tree")] + #[arg( + long, + default_value = "3", + help = "Maximum depth of the directory tree" + )] depth: usize, /// Files to process #[arg(required = true, help = "One or more files to create tree from")] @@ -61,10 +72,12 @@ fn list_tags(files: &[String]) -> Result, FileTagsError> { for file in files { match tag_engine::parse_tags(file) { Ok((_, tags, _)) => unique_tags.extend(tags), - Err(e) => return Err(FileTagsError::Parse { - file: PathBuf::from(file), - source: e, - }), + Err(e) => { + return Err(FileTagsError::Parse { + file: PathBuf::from(file), + source: e, + }) + } } } @@ -72,17 +85,19 @@ fn list_tags(files: &[String]) -> Result, FileTagsError> { } fn add_tags_to_file(file: &str, new_tags: &[String]) -> Result<(), FileTagsError> { - let (base, current_tags, ext) = tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse { - file: PathBuf::from(file), - source: e, - })?; + let (base, current_tags, ext) = + tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse { + file: PathBuf::from(file), + source: e, + })?; let merged_tags = tag_engine::add_tags(current_tags, new_tags.to_vec()); // Preserve the original directory - let parent = std::path::Path::new(file).parent() + let parent = std::path::Path::new(file) + .parent() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(); - + let new_filename = tag_engine::serialize_tags(&base, &merged_tags, &ext); let new_path = if parent.is_empty() { new_filename @@ -104,8 +119,11 @@ fn add_tags_to_file(file: &str, new_tags: &[String]) -> Result<(), FileTagsError fn main() -> Result<(), FileTagsError> { let cli = Cli::parse(); - + match cli.command { + Commands::Completion { shell } => { + shell.generate(&mut Cli::command(), &mut std::io::stdout()); + } Commands::List { files } => { let tags = list_tags(&files)?; for tag in tags { @@ -131,19 +149,21 @@ fn main() -> Result<(), FileTagsError> { } fn remove_tags_from_file(file: &str, remove_tags: &[String]) -> Result<(), FileTagsError> { - let (base, current_tags, ext) = tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse { - file: PathBuf::from(file), - source: e, - })?; + let (base, current_tags, ext) = + tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse { + file: PathBuf::from(file), + source: e, + })?; let filtered_tags = tag_engine::filter_tags(current_tags, remove_tags); let new_filename = tag_engine::serialize_tags(&base, &filtered_tags, &ext); - + // Preserve the original directory - let parent = std::path::Path::new(file).parent() + let parent = std::path::Path::new(file) + .parent() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(); - + let new_path = if parent.is_empty() { new_filename } else { @@ -164,12 +184,13 @@ fn remove_tags_from_file(file: &str, remove_tags: &[String]) -> Result<(), FileT fn create_tag_tree(files: &[String], target_dir: &str, depth: usize) -> Result<(), FileTagsError> { let target = PathBuf::from(target_dir); - + for file in files { - let (_base, tags, _ext) = tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse { - file: PathBuf::from(file), - source: e, - })?; + let (_base, tags, _ext) = + tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse { + file: PathBuf::from(file), + source: e, + })?; // Create root symlink let paths = vec![PathBuf::from(file)]; @@ -182,7 +203,7 @@ fn create_tag_tree(files: &[String], target_dir: &str, depth: usize) -> Result<( for tag in &combo { dir_path.push(tag); } - + let paths = vec![PathBuf::from(file)]; symlink::create_symlink_tree(paths, &dir_path)?; } @@ -202,9 +223,7 @@ mod tests { fs::create_dir_all(parent)?; } fs::write(&path, "")?; - Ok(path.to_str() - .ok_or("Invalid path")? - .to_string()) + Ok(path.to_str().ok_or("Invalid path")?.to_string()) } #[test] @@ -235,15 +254,18 @@ mod tests { fn test_add_tags_to_file() -> Result<(), Box> { let tmp_dir = TempDir::new()?; let file = create_test_file(&tmp_dir, "test.txt")?; - + // Verify file was created let initial_path = tmp_dir.path().join("test.txt"); assert!(initial_path.exists(), "Initial test file was not created"); - + add_tags_to_file(&file, &vec!["tag1".to_string(), "tag2".to_string()])?; - + // Verify original is gone and new exists - assert!(!initial_path.exists(), "Original file still exists after rename"); + assert!( + !initial_path.exists(), + "Original file still exists after rename" + ); let new_path = tmp_dir.path().join("test -- tag1 tag2.txt"); assert!(new_path.exists(), "Tagged file was not created"); Ok(()) @@ -253,15 +275,18 @@ mod tests { fn test_add_tags_to_existing_tags() -> Result<(), Box> { let tmp_dir = TempDir::new()?; let file = create_test_file(&tmp_dir, "test -- existing.txt")?; - + // Verify file was created let initial_path = tmp_dir.path().join("test -- existing.txt"); assert!(initial_path.exists(), "Initial test file was not created"); - + add_tags_to_file(&file, &vec!["new".to_string()])?; - + // Verify original is gone and new exists - assert!(!initial_path.exists(), "Original file still exists after rename"); + assert!( + !initial_path.exists(), + "Original file still exists after rename" + ); let new_name = tmp_dir.path().join("test -- existing new.txt"); assert!(new_name.exists(), "Tagged file was not created"); Ok(()) @@ -271,13 +296,13 @@ mod tests { fn test_add_duplicate_tags() -> Result<(), Box> { let tmp_dir = TempDir::new()?; let file = create_test_file(&tmp_dir, "test -- tag1.txt")?; - + // Verify file was created let initial_path = tmp_dir.path().join("test -- tag1.txt"); assert!(initial_path.exists(), "Initial test file was not created"); - + add_tags_to_file(&file, &vec!["tag1".to_string()])?; - + // Original should still exist since no change was needed assert!(initial_path.exists(), "Original file should still exist"); Ok(()) @@ -287,17 +312,23 @@ mod tests { fn test_add_tags_nested_path() -> Result<(), Box> { let tmp_dir = TempDir::new()?; let file = create_test_file(&tmp_dir, "nested/path/test.txt")?; - + // Verify file was created let initial_path = tmp_dir.path().join("nested/path/test.txt"); assert!(initial_path.exists(), "Initial test file was not created"); - + add_tags_to_file(&file, &vec!["tag1".to_string()])?; - + // Verify original is gone and new exists in same directory - assert!(!initial_path.exists(), "Original file still exists after rename"); + assert!( + !initial_path.exists(), + "Original file still exists after rename" + ); let new_path = tmp_dir.path().join("nested/path/test -- tag1.txt"); - assert!(new_path.exists(), "Tagged file was not created in original directory"); + assert!( + new_path.exists(), + "Tagged file was not created in original directory" + ); Ok(()) } @@ -305,9 +336,9 @@ mod tests { fn test_remove_tags() -> Result<(), Box> { let tmp_dir = TempDir::new()?; let file = create_test_file(&tmp_dir, "test -- tag1 tag2 tag3.txt")?; - + remove_tags_from_file(&file, &vec!["tag2".to_string()])?; - + let new_path = tmp_dir.path().join("test -- tag1 tag3.txt"); assert!(new_path.exists(), "File with removed tag not found"); Ok(()) @@ -318,17 +349,21 @@ mod tests { let tmp_dir = TempDir::new()?; let source = create_test_file(&tmp_dir, "test -- tag1 tag2.txt")?; let tree_dir = tmp_dir.path().join("tree"); - + create_tag_tree(&[source], tree_dir.to_str().unwrap(), 2)?; - + // Check root symlink assert!(tree_dir.join("test -- tag1 tag2.txt").exists()); - + // Check tag directories assert!(tree_dir.join("tag1").join("test -- tag1 tag2.txt").exists()); assert!(tree_dir.join("tag2").join("test -- tag1 tag2.txt").exists()); - assert!(tree_dir.join("tag1").join("tag2").join("test -- tag1 tag2.txt").exists()); - + assert!(tree_dir + .join("tag1") + .join("tag2") + .join("test -- tag1 tag2.txt") + .exists()); + Ok(()) } @@ -337,32 +372,52 @@ mod tests { let tmp_dir = TempDir::new()?; let file1 = create_test_file(&tmp_dir, "doc1.txt")?; let file2 = create_test_file(&tmp_dir, "doc2.txt")?; - + // Add tags add_tags_to_file(&file1, &vec!["work".to_string(), "draft".to_string()])?; add_tags_to_file(&file2, &vec!["work".to_string(), "final".to_string()])?; - + // List tags let tags = list_tags(&[ - tmp_dir.path().join("doc1 -- draft work.txt").to_str().unwrap().to_string(), - tmp_dir.path().join("doc2 -- final work.txt").to_str().unwrap().to_string(), + tmp_dir + .path() + .join("doc1 -- draft work.txt") + .to_str() + .unwrap() + .to_string(), + tmp_dir + .path() + .join("doc2 -- final work.txt") + .to_str() + .unwrap() + .to_string(), ])?; assert_eq!(tags, vec!["draft", "final", "work"]); - + // Remove a tag - let file_to_remove = tmp_dir.path().join("doc1 -- draft work.txt").to_str().unwrap().to_string(); + let file_to_remove = tmp_dir + .path() + .join("doc1 -- draft work.txt") + .to_str() + .unwrap() + .to_string(); remove_tags_from_file(&file_to_remove, &vec!["draft".to_string()])?; - + // Create tree let tree_dir = tmp_dir.path().join("tree"); create_tag_tree( - &[tmp_dir.path().join("doc1 -- work.txt").to_str().unwrap().to_string()], + &[tmp_dir + .path() + .join("doc1 -- work.txt") + .to_str() + .unwrap() + .to_string()], tree_dir.to_str().unwrap(), - 2 + 2, )?; - + assert!(tree_dir.join("work").join("doc1 -- work.txt").exists()); - + Ok(()) } }