use clap::Args;
use serde::Serialize;
use crate::commands::Output;
use crate::error::Result;
use crate::fs::FileSystem;
use crate::vault::VaultManager;
#[derive(Args)]
pub struct TagArgs {
pub path: Option<String>,
pub tag: Option<String>,
#[arg(short, long)]
pub list: bool,
#[arg(long)]
pub list_all: bool,
#[arg(long)]
pub create: Option<String>,
#[arg(long)]
pub delete: Option<String>,
#[arg(long)]
pub rename: Option<String>,
}
#[derive(Serialize)]
struct TagOutput {
tag: String,
action: String,
}
#[derive(Serialize)]
struct TagListOutput {
path: Option<String>,
tags: Vec<String>,
}
#[derive(Serialize)]
struct AllTagsOutput {
count: usize,
tags: Vec<TagInfo>,
}
#[derive(Serialize)]
struct TagInfo {
name: String,
file_count: usize,
}
pub fn run(args: TagArgs, output: &Output, vault: Option<String>) -> Result<()> {
let manager = VaultManager::new()?;
let backend = match vault {
Some(name) => manager.open(&name)?,
None => manager.open_current()?,
};
let fs = FileSystem::new(backend.clone());
if args.list_all {
let tags = backend.list_tags()?;
let tag_infos: Vec<TagInfo> = tags
.iter()
.map(|t| {
let file_count = backend.get_files_with_tag(t.id).map(|f| f.len()).unwrap_or(0);
TagInfo {
name: t.name.clone(),
file_count,
}
})
.collect();
if output.is_json() {
output.print_json(&AllTagsOutput {
count: tag_infos.len(),
tags: tag_infos,
});
} else {
if tag_infos.is_empty() {
println!("No tags in vault");
} else {
println!("Tags in vault:");
for t in tag_infos {
println!(" {} ({} files)", t.name, t.file_count);
}
}
}
return Ok(());
}
if let Some(ref tag_name) = args.create {
if backend.get_tag_by_name(tag_name)?.is_some() {
return Err(crate::error::VfsError::AlreadyExists(
std::path::PathBuf::from(format!("tag:{}", tag_name)),
));
}
backend.create_tag(tag_name)?;
if output.is_json() {
output.print_json(&TagOutput {
tag: tag_name.clone(),
action: "created".to_string(),
});
} else {
println!("Created tag: {}", tag_name);
}
return Ok(());
}
if let Some(ref tag_name) = args.delete {
let tag = backend.get_tag_by_name(tag_name)?.ok_or_else(|| {
crate::error::VfsError::NotFound(std::path::PathBuf::from(format!("tag:{}", tag_name)))
})?;
backend.delete_tag(tag.id)?;
if output.is_json() {
output.print_json(&TagOutput {
tag: tag_name.clone(),
action: "deleted".to_string(),
});
} else {
println!("Deleted tag: {}", tag_name);
}
return Ok(());
}
if let Some(ref rename_spec) = args.rename {
let parts: Vec<&str> = rename_spec.split(':').collect();
if parts.len() != 2 {
return Err(crate::error::VfsError::Internal(
"rename format should be old:new".to_string(),
));
}
let old_name = parts[0];
let new_name = parts[1];
let tag = backend.get_tag_by_name(old_name)?.ok_or_else(|| {
crate::error::VfsError::NotFound(std::path::PathBuf::from(format!("tag:{}", old_name)))
})?;
if backend.get_tag_by_name(new_name)?.is_some() {
return Err(crate::error::VfsError::AlreadyExists(
std::path::PathBuf::from(format!("tag:{}", new_name)),
));
}
backend.rename_tag(tag.id, new_name)?;
if output.is_json() {
output.print_json(&serde_json::json!({
"old_name": old_name,
"new_name": new_name,
"action": "renamed"
}));
} else {
println!("Renamed tag: {} -> {}", old_name, new_name);
}
return Ok(());
}
if args.list {
let path = args.path.as_ref().ok_or_else(|| {
crate::error::VfsError::Internal("path required for --list".to_string())
})?;
let entry = fs.get_entry(path)?;
let tags = backend.get_file_tags(entry.id)?;
let tag_names: Vec<String> = tags.iter().map(|t| t.name.clone()).collect();
if output.is_json() {
output.print_json(&TagListOutput {
path: Some(path.clone()),
tags: tag_names,
});
} else {
if tag_names.is_empty() {
println!("No tags on {}", path);
} else {
println!("Tags on {}:", path);
for t in tag_names {
println!(" {}", t);
}
}
}
return Ok(());
}
let path = args.path.as_ref().ok_or_else(|| {
crate::error::VfsError::Internal("path required".to_string())
})?;
let tag_name = args.tag.as_ref().ok_or_else(|| {
crate::error::VfsError::Internal("tag name required".to_string())
})?;
let entry = fs.get_entry(path)?;
let tag_id = backend.get_or_create_tag(tag_name)?;
backend.add_tag_to_file(entry.id, tag_id)?;
if output.is_json() {
output.print_json(&serde_json::json!({
"path": path,
"tag": tag_name,
"action": "added"
}));
} else {
println!("Tagged {} with '{}'", path, tag_name);
}
Ok(())
}