use std::{
ops::Range,
process::{ExitCode, Termination},
};
use camino::{Utf8Path, Utf8PathBuf};
use clap::Parser as _;
use indexmap::IndexMap;
use maimai::{ErrorKind, FileProvider, FsFileProvider, IncompleteMemeDefinition};
use tracing_subscriber::EnvFilter;
fn main() -> CliResult {
fn _main() -> maimai::Result<()> {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.or_else(|_| {
EnvFilter::try_new(if cli.verbose {
"info,maimai=debug"
} else if cli.quiet {
"error,maimai=warn"
} else {
"warn,maimai=info"
})
})
.map_err(maimai::Error::other)?,
)
.init();
for input_path in cli.input_files {
let output_path = match &cli.output_dir {
Some(dir) => dir.join(
input_path
.with_extension(cli.format.as_ref())
.file_name()
.ok_or_else(|| maimai::Error::other("input filename must not be none"))?,
),
None => input_path.with_extension(cli.format.as_ref()),
};
maimai::Meme::from_files(&FsFileProvider, &input_path)?
.render_to_file(&output_path, cli.debug)?;
}
Ok(())
}
CliResult(_main())
}
#[derive(Debug, clap::Parser)]
pub struct Cli {
input_files: Vec<Utf8PathBuf>,
#[arg(long, default_value_t = ImageFormat::Webp)]
format: ImageFormat,
#[arg(long)]
output_dir: Option<Utf8PathBuf>,
#[arg(long)]
debug: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
quiet: bool,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display, strum::EnumString, strum::AsRefStr)]
#[strum(serialize_all = "lowercase")]
pub enum ImageFormat {
Png,
#[strum(to_string = "jpg", serialize = "jpeg")]
Jpeg,
Webp,
}
impl From<ImageFormat> for image::ImageFormat {
fn from(format: ImageFormat) -> Self {
match format {
ImageFormat::Png => image::ImageFormat::Png,
ImageFormat::Jpeg => image::ImageFormat::Jpeg,
ImageFormat::Webp => image::ImageFormat::WebP,
}
}
}
struct CliResult(maimai::Result<()>);
impl Termination for CliResult {
fn report(self) -> std::process::ExitCode {
match &self.0 {
Ok(()) => ExitCode::SUCCESS,
Err(_) => {
eprintln!("{self}");
ExitCode::FAILURE
}
}
}
}
impl std::fmt::Display for CliResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Err(error) = &self.0 {
let path = error.path();
let mut sources = ariadne::FnCache::new(
(|path| FsFileProvider.read_string(path))
as fn(&&Utf8Path) -> std::io::Result<String>,
);
match error.kind() {
ErrorKind::Parse(e) => {
if let Some(span) = e.span() {
let path = path.expect("missing file path");
let report = ariadne::Report::build(
ariadne::ReportKind::Error,
(path, span.clone()),
)
.with_message(error)
.with_label(ariadne::Label::new((path, span)).with_message(e.message()))
.finish();
let mut buf = Vec::new();
report.write(sources, &mut buf).map_err(|e| {
tracing::error!(?e);
std::fmt::Error
})?;
f.write_str(&String::from_utf8_lossy(&buf))?;
}
}
ErrorKind::Incomplete(errors) => {
let path = path.expect("missing file path");
let doc: toml_edit::ImDocument<String> =
FsFileProvider.read_string(path).unwrap().parse().unwrap();
let mut buf = Vec::new();
let mut spans: IndexMap<
(&Utf8Path, Range<usize>),
Vec<&IncompleteMemeDefinition>,
> = IndexMap::new();
for e in errors {
let mut item = doc.as_item();
for key in e.keys() {
if let Some(child) = item.get(key.as_ref()) {
if item.span().is_some() {
item = child
}
}
}
spans
.entry((path, item.span().unwrap_or(0..0)))
.or_default()
.push(e);
}
for (span, es) in spans {
ariadne::Report::build(ariadne::ReportKind::Error, span.clone())
.with_message(error)
.with_labels(
es.into_iter()
.map(|e| ariadne::Label::new(span.clone()).with_message(e)),
)
.finish()
.write(&mut sources, &mut buf)
.map_err(|e| {
tracing::error!(?e);
std::fmt::Error
})?;
}
f.write_str(&String::from_utf8_lossy(&buf))?;
}
_ => {
write!(f, "Error: {error}")?;
let mut e: &dyn std::error::Error = error;
while let Some(source) = e.source() {
e = source;
write!(f, "\n ⤷ {e}")?;
}
}
}
}
Ok(())
}
}