use anyhow::Result;
use camino::Utf8PathBuf;
use clap::{Args, ValueEnum};
use tracing::warn;
use cookcli_core::{
format::shopping_list as fmt,
shopping_list::{generate, GenerateRequest, ScaledRecipe},
ConfigSource,
};
use crate::{
util::{cli_error, format::Style, split_recipe_name_and_scaling_factor, write_to_output},
Context,
};
#[derive(Debug, Args)]
#[command()]
pub struct ShoppingListArgs {
recipes: Vec<String>,
#[arg(short, long, value_hint = clap::ValueHint::DirPath)]
base_path: Option<Utf8PathBuf>,
#[arg(short, long, value_hint = clap::ValueHint::FilePath)]
output: Option<Utf8PathBuf>,
#[arg(short, long)]
plain: bool,
#[arg(short, long, value_enum)]
format: Option<OutputFormat>,
#[arg(long)]
pretty: bool,
#[arg(short, long, value_hint = clap::ValueHint::FilePath)]
aisle: Option<Utf8PathBuf>,
#[arg(long, value_hint = clap::ValueHint::FilePath)]
pantry: Option<Utf8PathBuf>,
#[arg(short, long)]
ignore_references: bool,
#[arg(long)]
ignore_pantry: bool,
#[arg(long)]
ingredients_only: bool,
#[arg(long, value_name = "ITEM")]
extra: Vec<String>,
#[arg(long)]
include_optional: bool,
}
impl ShoppingListArgs {
pub fn get_base_path(&self) -> Option<Utf8PathBuf> {
self.base_path.clone()
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum OutputFormat {
Human,
Json,
Yaml,
Markdown,
}
pub fn run(ctx: &Context, args: ShoppingListArgs) -> Result<()> {
let mut expanded_recipes = Vec::new();
for entry in &args.recipes {
let path = if entry.contains(':') {
let (recipe_path, _) = entry.split_once(':').unwrap();
Utf8PathBuf::from(recipe_path)
} else {
Utf8PathBuf::from(entry)
};
if path.is_dir() {
for dir_entry in std::fs::read_dir(&path)? {
let dir_entry = dir_entry?;
if dir_entry.file_name().to_string_lossy().starts_with('.') {
continue;
}
let file_path = dir_entry.path();
if let Some(ext) = file_path.extension() {
if ext == "cook" {
if let Ok(utf8_path) = Utf8PathBuf::from_path_buf(file_path) {
if entry.contains(':') {
let scaling = entry.split_once(':').unwrap().1;
expanded_recipes.push(format!("{utf8_path}:{scaling}"));
} else {
expanded_recipes.push(utf8_path.to_string());
}
}
}
}
}
} else {
expanded_recipes.push(entry.clone());
}
}
if expanded_recipes.is_empty() && !args.recipes.is_empty() {
expanded_recipes = args.recipes.clone();
}
let mut core_ctx = ctx.clone();
if let Some(path) = args.aisle {
core_ctx = core_ctx.with_aisle(ConfigSource::Path(path));
}
if args.ignore_pantry {
tracing::debug!("Pantry ignored via --ignore-pantry");
core_ctx = core_ctx.with_pantry(ConfigSource::None);
} else if let Some(path) = args.pantry {
core_ctx = core_ctx.with_pantry(ConfigSource::Path(path));
} else if let Err(e) = core_ctx.pantry().read() {
warn!("Failed to read pantry file: {e}");
core_ctx = core_ctx.with_pantry(ConfigSource::None);
}
let format = args.format.unwrap_or_else(|| match &args.output {
Some(p) => match p.extension() {
Some("json") => OutputFormat::Json,
_ => OutputFormat::Human,
},
None => OutputFormat::Human,
});
let recipes = expanded_recipes
.iter()
.map(|entry| match split_recipe_name_and_scaling_factor(entry) {
Some((name, scale)) => {
ScaledRecipe::scaled(cookcli_core::RecipeSource::Path(name.into()), scale)
}
None => ScaledRecipe::new(cookcli_core::RecipeSource::Path(entry.as_str().into())),
})
.collect();
let outcome = generate(
&core_ctx,
GenerateRequest {
recipes,
ignore_references: args.ignore_references,
extra_items: args.extra,
include_optional: args.include_optional,
},
)
.map_err(cli_error)?;
for diagnostic in &outcome.diagnostics {
match diagnostic.location.as_ref().and_then(|l| l.file.as_ref()) {
Some(file) => warn!("{file}: {}", diagnostic.message),
None => warn!("{}", diagnostic.message),
}
}
let list = outcome.value;
write_to_output(args.output.as_deref(), |w| {
if args.ingredients_only {
match format {
OutputFormat::Human => {
for item in &list.items {
if item.optional {
writeln!(w, "{} (optional)", item.name)?;
} else {
writeln!(w, "{}", item.name)?;
}
}
}
OutputFormat::Json => {
let ingredients: Vec<&str> =
list.items.iter().map(|i| i.name.as_str()).collect();
if args.pretty {
serde_json::to_writer_pretty(w, &ingredients)?;
} else {
serde_json::to_writer(w, &ingredients)?;
}
}
OutputFormat::Yaml => {
let ingredients: Vec<&str> =
list.items.iter().map(|i| i.name.as_str()).collect();
serde_yaml::to_writer(w, &ingredients)?;
}
OutputFormat::Markdown => {
let value = fmt::build_md_value(list, args.plain, args.ingredients_only);
write!(w, "{value}")?;
}
}
} else {
match format {
OutputFormat::Human => {
let table = fmt::build_human_table(list, args.plain, Style::Ansi);
write!(w, "{table}")?;
}
OutputFormat::Json => {
let value = fmt::build_json_value(list, args.plain);
if args.pretty {
serde_json::to_writer_pretty(w, &value)?;
} else {
serde_json::to_writer(w, &value)?;
}
}
OutputFormat::Yaml => {
let value = fmt::build_yaml_value(list, args.plain);
serde_yaml::to_writer(w, &value)?;
}
OutputFormat::Markdown => {
let value = fmt::build_md_value(list, args.plain, args.ingredients_only);
write!(w, "{value}")?;
}
}
}
Ok(())
})
}