use crate::{
cli_config,
util::{get_current_vault, CommandResult},
};
use anyhow::{anyhow, Context};
use clap::{Args, Subcommand};
use dialoguer::{theme::ColorfulTheme, Select};
use std::{fs, io, path::PathBuf};
use tabled::{builder::Builder, settings::Style};
#[derive(Args, Debug, Clone)]
#[command(args_conflicts_with_subcommands = true)]
#[command(arg_required_else_help = true)]
pub struct VaultsCommand {
#[command(subcommand)]
command: Option<Subcommands>,
}
#[derive(Debug, Subcommand, Clone)]
enum Subcommands {
Create(CreateArgs),
List(ListArgs),
Switch(SwitchArgs),
Current,
Path,
}
#[derive(Args, Debug, Clone)]
struct VaultArgs {}
#[derive(Args, Debug, Clone)]
struct CreateArgs {
#[arg(help = "Path to the vault to be created")]
vault_path: PathBuf,
#[arg(long, help = "Explicitly name the vault")]
name: Option<String>,
}
#[derive(Args, Debug, Clone)]
struct SwitchArgs {
#[arg(help = "The name of the vault to switch to")]
vault: Option<String>,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum ListFormats {
Pretty,
Json,
}
#[derive(Args, Debug, Clone)]
struct ListArgs {
#[arg(long, short = 'f', default_value = "pretty")]
format: ListFormats,
}
pub fn entry(cmd: &VaultsCommand) -> anyhow::Result<Option<String>> {
match &cmd.command {
Some(Subcommands::Create(CreateArgs {
vault_path: vault,
name,
})) => create(&vault, name.clone()),
Some(Subcommands::List(ListArgs { format })) => list(format),
Some(Subcommands::Switch(SwitchArgs { vault })) => switch(vault),
Some(Subcommands::Current) => current(),
Some(Subcommands::Path) => path(),
None => todo!(),
}
}
fn create(vault_path: &PathBuf, vault_name_override: Option<String>) -> CommandResult {
let vault_name = vault_name_override.unwrap_or_else(|| {
vault_path
.components()
.last()
.unwrap()
.as_os_str()
.to_str()
.unwrap()
.to_string()
});
let resolved_path = fs::canonicalize(vault_path).map_err(|err| {
if err.kind() == io::ErrorKind::NotFound {
anyhow!(
"Could not create vault at path `{}`, directory not found",
vault_path.display()
)
} else {
anyhow!(err)
}
})?;
if !resolved_path.is_dir() {
return Err(anyhow!(
"Could not create vault at path `{}`, path must be a directory",
vault_path.display()
));
}
let mut cfg = cli_config::read()?;
cfg.current_vault = vault_name.clone();
cfg.vaults.push(cli_config::Vault {
name: vault_name.clone(),
path: resolved_path.to_path_buf(),
});
let _ = cli_config::write(cfg);
Ok(Some(format!("Created vault {vault_name}")))
}
fn list(list_format: &ListFormats) -> CommandResult {
let config = cli_config::read()?;
let formatted = match list_format {
&ListFormats::Json => {
let json = serde_json::to_string(&config.vaults)?;
json
}
ListFormats::Pretty => format_vault_table(&config),
};
Ok(Some(formatted))
}
pub fn format_vault_table(config: &cli_config::File) -> String {
let mut builder = Builder::new();
for v in &config.vaults {
builder.push_record([v.name.clone(), v.path.display().to_string()])
}
builder.insert_record(0, vec!["Name", "Path"]);
let mut table = builder.build();
table.with(Style::sharp());
format!("{table}")
}
fn switch(vault_name_arg: &Option<String>) -> CommandResult {
let mut config = cli_config::read()?;
let vault_name: String = match vault_name_arg {
Some(s) => s.to_string(),
None => interactive_switch(&config, "Select a vault"),
};
config
.vaults
.iter()
.find(|v| v.name == vault_name)
.with_context(|| {
format!("Could not switch to vault `{vault_name}`, vault doesn't exist")
})?;
config.current_vault = vault_name.to_string();
let _ = cli_config::write(config);
Ok(Some(format!("Switched to vault {vault_name}")))
}
pub fn interactive_switch(config: &cli_config::File, message: &str) -> String {
let vaults: Vec<String> = config
.vaults
.iter()
.map(|v| format!("{} ({})", v.name.clone(), v.path.display()))
.collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt(message)
.items(&vaults)
.interact()
.unwrap();
let selected_vault = &config.vaults[selection];
selected_vault.name.to_string()
}
fn current() -> CommandResult {
let config = cli_config::read()?;
let found_vault = config
.vaults
.iter()
.find(|v| v.name == config.current_vault)
.context("Expected to find vault matching current_vault in config")?;
let out = format!(
"Current vault is `{name}` at path `{path}`",
name = found_vault.name,
path = found_vault.path.display()
);
Ok(Some(out))
}
fn path() -> CommandResult {
let vault = get_current_vault(None)?;
let vault_path = vault.path.to_str().unwrap().to_string();
Ok(Some(vault_path))
}