#![warn(clippy::perf, clippy::style, warnings)] mod cli; mod vcard; use crate::cli::*; use crate::vcard::VCard; use anyhow::Result; use clap::Parser; use dialoguer::{Confirm, Editor, Input}; const FORMATTED_NAME: &str = "FN"; const EMAIL: &str = "EMAIL"; const TELEPHONE: &str = "TEL"; const ADDRESS: &str = "ADR"; const WEBSITE: &str = "URL"; const NOTE: &str = "NOTE"; fn main() -> Result<()> { let cli = Cli::parse(); println!( "Building new VCard... [ Format: {:?} // ECC Level: {:?} // Size: {}px ]\n", &cli.format, &cli.error_correction, &cli.size ); let vcard = build_vcard()?; write_vcard(vcard, cli)?; Ok(()) } fn build_vcard() -> Result { let mut vcard = VCard::new(); let name = query_input("Your name (required)", false)?; let email = query_input("Contact email (recommended)", true)?; let phone = query_input("Contact phone (recommended)", true)?; let website = query_input("Your website (optional)", true)?; vcard.push(FORMATTED_NAME, name); vcard.optional_push(EMAIL, email); vcard.optional_push(TELEPHONE, phone); vcard.optional_push(WEBSITE, website); if query_bool("Do you want to add addresses?", false)? { for address in query_addresses()? { vcard.push_explicit(&address); } } if query_bool("Do you want to add a note?", false)? { if let Some(note) = Editor::new().edit("Enter your note...")? { vcard.optional_push(NOTE, note) } else { println!("Skipped adding a note.") } } Ok(vcard.finalize()) } fn write_vcard(vcard: String, config: Cli) -> Result<()> { use std::path::PathBuf; let mut path = PathBuf::from(config.output_name); match config.format { OutputFormat::Png => { path.set_extension("png"); qrcode_generator::to_png_to_file( vcard, config.error_correction.into(), config.size, &path, )? } OutputFormat::Svg => { path.set_extension("svg"); qrcode_generator::to_svg_to_file( vcard, config.error_correction.into(), config.size, None::<&str>, &path, )? } } println!("Output written to \"{}\".", path.to_string_lossy()); Ok(()) } /// Convenience function that wraps [`dialoguer::Input`] for prompting a string from the user. fn query_input(prompt: &str, optional: bool) -> Result { Input::new() .with_prompt(prompt) .allow_empty(optional) .interact_text() } /// Convenience function that wraps [`dialoguer::Confirm`] for prompting a y/n decision from the user. fn query_bool(prompt: &str, default: bool) -> Result { Confirm::new() .with_prompt(prompt) .default(default) .interact() } /// Collects an arbitrary number of addresses from the user, in a loop. fn query_addresses() -> Result> { let mut addresses: Vec = Vec::new(); loop { let street_addr = query_input("Street address (required)", false)?; let extended_addr = query_input("Extended address (e.g. apartment number, optional)", true)?; let city = query_input("Municipality (required)", false)?; let state = query_input("State/province (required)", false)?; let zip = query_input("ZIP/postal code (required)", false)?; let country = query_input("Country (optional)", true)?; let addr_type = query_input("Address type (e.g. home, optional)", true)?; let street_addr = format!("{street_addr},{extended_addr}"); let property = match addr_type.is_empty() { false => format!("{ADDRESS};TYPE={addr_type}"), true => ADDRESS.to_string(), }; addresses.push(format!( "{property}:;;{street_addr};{city};{state};{zip};{country}" )); if query_bool("Do you want to add another address?", false)? { continue; } break; } Ok(addresses) }