use crate::units::{Time, UuidVersion};
use anyhow::bail;
use clap::Subcommand;
use fs_err::File;
use ordinary_build::PercentageDisplay;
use std::env::home_dir;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use time::UtcDateTime;
use uuid::Uuid;
#[derive(Subcommand, Debug)]
pub enum Utils {
Uuid {
#[arg(long, default_value = "4")]
v: UuidVersion,
},
Timestamp {
#[arg(short, long, default_value = "seconds")]
unit: Time,
#[arg(short, long)]
fmt: Option<String>,
},
Html {
#[command(subcommand)]
html: Html,
},
Css {
#[command(subcommand)]
css: Css,
},
Js {
#[command(subcommand)]
js: Js,
},
Markdown {
#[command(subcommand)]
markdown: Markdown,
},
Exif {
#[command(subcommand)]
exif: Exif,
},
WasmOpt {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum Html {
Minify {
path: PathBuf,
#[arg(short, long, default_value_t = false)]
in_place: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum Css {
Minify {
path: PathBuf,
#[arg(short, long, default_value_t = false)]
in_place: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum Js {
Minify {
path: PathBuf,
#[arg(short, long, default_value_t = false)]
in_place: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum Markdown {
ToHtml {
path: PathBuf,
#[arg(short, long, default_value_t = false)]
safe: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum Exif {
Tool {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}
impl Utils {
#[allow(clippy::redundant_else, clippy::too_many_lines)]
pub fn handle(&self) -> anyhow::Result<()> {
match self {
Self::Uuid { v } => match v {
UuidVersion::V4 => println!("{}", Uuid::new_v4()),
UuidVersion::V7 => println!("{}", Uuid::now_v7()),
},
Self::Timestamp { unit, fmt } => {
let mut timestamp = UtcDateTime::now();
timestamp = match unit {
Time::Seconds => timestamp.truncate_to_second(),
Time::Millis => timestamp.truncate_to_millisecond(),
Time::Micros => timestamp.truncate_to_microsecond(),
Time::Nanos => timestamp,
};
if let Some(fmt) = fmt {
let format_desc = time::format_description::parse(fmt)?;
let formatted = timestamp.format(&format_desc)?;
println!("{formatted}");
} else {
let secs = timestamp.unix_timestamp();
let nanos = timestamp.unix_timestamp_nanos();
match unit {
Time::Seconds => println!("{secs}"),
Time::Millis => {
println!("{}", (secs * 1000) + (i64::try_from(nanos)? / 1000 / 1000));
}
Time::Micros => {
println!("{}", (secs * 1000 * 1000) + (i64::try_from(nanos)? / 1000));
}
Time::Nanos => println!("{nanos}"),
}
}
}
Self::Html { html } => match html {
Html::Minify { path, in_place } => {
minify(path, *in_place, ordinary_build::html::minify)?;
}
},
Self::Css { css } => match css {
Css::Minify { path, in_place } => {
minify(path, *in_place, ordinary_build::css::minify)?;
}
},
Self::Js { js } => match js {
Js::Minify { path, in_place } => {
minify(path, *in_place, ordinary_build::js::minify)?;
}
},
Self::Markdown { markdown } => match markdown {
Markdown::ToHtml { path, safe } => {
use pulldown_cmark::{Options, Parser};
if *safe {
todo!("implement safe");
}
if path.is_dir() {
bail!("doesn't yet work for directories")
} else {
let md = fs_err::read_to_string(path)?;
let options = Options::all();
let parser = Parser::new_ext(&md, options);
let mut path = path.clone();
path.set_extension("html");
let mut file = File::create(path)?;
pulldown_cmark::html::write_html_io(&mut file, parser)?;
file.flush()?;
}
}
},
Self::Exif { exif } => match exif {
Exif::Tool { args } => {
let exiftool_path = home_dir()
.expect("home dir doesn't exist")
.join(".ordinary")
.join("bin")
.join("exiftool")
.join("exiftool");
if !exiftool_path.exists() {
bail!(
"`exiftool` not installed for `ordinary` — for install, run `ordinary doctor --fix exiftool`"
);
}
let output = Command::new(exiftool_path).args(args).output()?;
print_output(&output)?;
}
},
Self::WasmOpt { args } => {
let wasm_opt_path = home_dir()
.expect("home dir doesn't exist")
.join(".ordinary")
.join("bin")
.join("wasm-opt");
if !wasm_opt_path.exists() {
tracing::warn!(
"wasm-opt not installed at {} (built WASM modules will not be further optimized) - for install, run `ordinary doctor --fix wasm-opt`",
wasm_opt_path.display()
);
}
let output = Command::new(wasm_opt_path).args(args).output()?;
print_output(&output)?;
}
}
Ok(())
}
}
fn print_output(output: &Output) -> anyhow::Result<()> {
if output.status.success() {
for line in std::str::from_utf8(&output.stderr)?.split('\n') {
if !line.trim().is_empty() {
tracing::info!(stderr = %line);
}
}
println!("{}", std::str::from_utf8(&output.stdout)?);
} else {
for line in std::str::from_utf8(&output.stderr)?.split('\n') {
if !line.trim().is_empty() {
tracing::error!(stderr = %line);
}
}
println!("{}", std::str::from_utf8(&output.stdout)?);
}
Ok(())
}
#[allow(clippy::redundant_else, clippy::cast_precision_loss)]
fn minify(
path: &Path,
in_place: bool,
minify: fn(file_str: &str) -> anyhow::Result<String>,
) -> anyhow::Result<()> {
let mut path = path.to_path_buf();
if path.is_dir() {
bail!("doesn't yet work for directories")
} else {
let file_str = fs_err::read_to_string(&path)?;
let minified = minify(&file_str)?;
if !in_place {
let ext = if let Some(ext) = path.extension() {
format!("min.{}", ext.display())
} else {
"min".to_string()
};
path.set_extension(ext);
}
let mut file = File::create(path)?;
file.write_all(minified.as_bytes())?;
file.flush()?;
tracing::info!(
size.source = %bytesize::ByteSize(file_str.len() as u64).display().si_short(),
size.minified = %bytesize::ByteSize(minified.len() as u64).display().si_short(),
size.reduction = %PercentageDisplay(((file_str.len() as f64 - minified.len() as f64)
/ file_str.len() as f64)
* 100.0),
);
}
Ok(())
}