feat: Add shell completions using clap_complete_command
This commit is contained in:
parent
f23beef347
commit
263b033035
2 changed files with 123 additions and 67 deletions
|
|
@ -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]
|
||||
|
|
|
|||
189
src/main.rs
189
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<Vec<String>, 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<Vec<String>, 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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue