feat: add external vcard-cli
This commit is contained in:
47
__external/vcard-qr/src/cli.rs
Normal file
47
__external/vcard-qr/src/cli.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use clap::{Parser, ValueEnum};
|
||||
use qrcode_generator::QrCodeEcc;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
/// The desired name of the output file, sans extension.
|
||||
#[arg(short, long, default_value = "vcard")]
|
||||
pub output_name: String,
|
||||
/// The desired output format of the QR code.
|
||||
#[arg(short, long, value_enum, default_value_t=OutputFormat::Png)]
|
||||
pub format: OutputFormat,
|
||||
/// The desired error correction level.
|
||||
/// Higher levels generate larger QR codes, but make it more likely
|
||||
/// the code will remain readable if it is damaged.
|
||||
#[arg(short, long, value_enum, default_value_t=ErrorCorrection::Low)]
|
||||
pub error_correction: ErrorCorrection,
|
||||
/// The size of the output image, in pixels.
|
||||
#[arg(short, long, default_value = "1024")]
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
pub enum ErrorCorrection {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Max,
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<QrCodeEcc> for ErrorCorrection {
|
||||
fn into(self) -> QrCodeEcc {
|
||||
match self {
|
||||
ErrorCorrection::Low => QrCodeEcc::Low,
|
||||
ErrorCorrection::Medium => QrCodeEcc::Medium,
|
||||
ErrorCorrection::High => QrCodeEcc::Quartile,
|
||||
ErrorCorrection::Max => QrCodeEcc::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
pub enum OutputFormat {
|
||||
Png,
|
||||
Svg,
|
||||
}
|
||||
142
__external/vcard-qr/src/main.rs
Normal file
142
__external/vcard-qr/src/main.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
#![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<String> {
|
||||
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<String, std::io::Error> {
|
||||
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<bool, std::io::Error> {
|
||||
Confirm::new()
|
||||
.with_prompt(prompt)
|
||||
.default(default)
|
||||
.interact()
|
||||
}
|
||||
|
||||
/// Collects an arbitrary number of addresses from the user, in a loop.
|
||||
fn query_addresses() -> Result<Vec<String>> {
|
||||
let mut addresses: Vec<String> = 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)
|
||||
}
|
||||
45
__external/vcard-qr/src/vcard.rs
Normal file
45
__external/vcard-qr/src/vcard.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
const VCARD_HEADER: &str = "BEGIN:VCARD\nVERSION:4.0\n";
|
||||
const VCARD_FOOTER: &str = "END:VCARD";
|
||||
|
||||
pub struct VCard {
|
||||
buf: String,
|
||||
}
|
||||
|
||||
impl VCard {
|
||||
pub fn new() -> Self {
|
||||
VCard {
|
||||
buf: VCARD_HEADER.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push<P, V>(&mut self, property: P, value: V)
|
||||
where
|
||||
P: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
self.buf
|
||||
.push_str(&format!("{}:{}\n", property.as_ref(), value.as_ref()))
|
||||
}
|
||||
|
||||
pub fn optional_push<P, V>(&mut self, property: P, value: V)
|
||||
where
|
||||
P: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
if !value.as_ref().is_empty() {
|
||||
self.push(property, value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_explicit<T>(&mut self, data: T)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
self.buf.push_str(&format!("{}\n", data.as_ref()))
|
||||
}
|
||||
|
||||
pub fn finalize(mut self) -> String {
|
||||
self.push_explicit(VCARD_FOOTER);
|
||||
self.buf.trim().to_owned()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user