use clap::{Arg, ArgAction, ArgMatches, Command, ValueHint, value_parser};
use indicatif::{ProgressBar, ProgressStyle};
use png::ColorType;
use std::{env, path::PathBuf};
use libmosh::{
MoshCore,
ops::{read_file, write_file},
};
const BANNER: &str = "\u{250C}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2510}\n\u{2502} \u{2588}\
\u{2580}\u{2584} \u{2588} \u{2580}\u{2584}\u{2580} \
\u{2588}\u{2588}\u{2580} \u{2588} \u{2588}\u{2584} \
\u{2584}\u{2588} \u{2584}\u{2580}\u{2584} \u{2584}\
\u{2580}\u{2580} \u{2588}\u{2584}\u{2588} \u{2502}\n\
\u{2502} \u{2588}\u{2580} \u{2588} \u{2588} \u{2588} \
\u{2588}\u{2584}\u{2584} \u{2588}\u{2584}\u{2584} \
\u{2588} \u{2580} \u{2588} \u{2580}\u{2584}\u{2580} \
\u{2584}\u{2588}\u{2588} \u{2588} \u{2588} \u{2502}\n\
\u{2514}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\
\u{2500}\u{2500}\u{2500}\u{2518}";
const SPINNER_1: [&str; 7] = [
"\u{2219}\u{2219}\u{2219}\u{2219}\u{2219}",
"\u{25CF}\u{2219}\u{2219}\u{2219}\u{2219}",
"\u{2219}\u{25CF}\u{2219}\u{2219}\u{2219}",
"\u{2219}\u{2219}\u{25CF}\u{2219}\u{2219}",
"\u{2219}\u{2219}\u{2219}\u{25CF}\u{2219}",
"\u{2219}\u{2219}\u{2219}\u{2219}\u{25CF}",
"\u{2219}\u{2219}\u{2219}\u{2219}\u{2219}",
];
const SPINNER_2: [&str; 7] = [
"\u{25B1}\u{25B1}\u{25B1}\u{25B1}\u{25B1}",
"\u{25B0}\u{25B1}\u{25B1}\u{25B1}\u{25B1}",
"\u{25B1}\u{25B0}\u{25B1}\u{25B1}\u{25B1}",
"\u{25B1}\u{25B1}\u{25B0}\u{25B1}\u{25B1}",
"\u{25B1}\u{25B1}\u{25B1}\u{25B0}\u{25B1}",
"\u{25B1}\u{25B1}\u{25B1}\u{25B1}\u{25B0}",
"\u{25B0}\u{25B0}\u{25B0}\u{25B0}\u{25B0}",
];
fn display_var() -> bool {
env::var("DISPLAY").is_ok()
}
fn color_type_str(container: &MoshCore) -> &str {
match container.data.color_type {
ColorType::Grayscale => "Grayscale",
ColorType::Indexed => "Indexed",
ColorType::GrayscaleAlpha => "Grayscale/A",
ColorType::Rgb => "RGB",
ColorType::Rgba => "RGB/A",
}
}
fn arg_matches() -> (ArgMatches, MoshCore) {
let container = MoshCore::new();
let matches = Command::new(env!("CARGO_PKG_NAME"))
.author(env!("CARGO_PKG_AUTHORS"))
.about(BANNER)
.version(crate::version())
.help_template(
"{name} v{version} \
CLI\n{about-with-newline}\
PNG corrupter\n\n{usage-heading} \
{usage}\n\n{all-args}{after-help}",
)
.arg(
Arg::new("file")
.action(ArgAction::Set)
.value_name("FILE")
.help("File path")
.long_help("Path to an image file")
.required(true)
.value_hint(ValueHint::FilePath)
.value_parser(value_parser!(PathBuf)),
)
.arg(
Arg::new("minrate")
.short('n')
.long("min-rate")
.value_name("VALUE")
.help("Minimum chunks to process")
.value_parser(value_parser!(u16))
.default_value(container.options.min_rate.to_string()),
)
.arg(
Arg::new("maxrate")
.short('m')
.long("max-rate")
.value_name("VALUE")
.help("Maximum chunks to process")
.value_parser(value_parser!(u16))
.default_value(container.options.max_rate.to_string()),
)
.arg(
Arg::new("pixelation")
.short('p')
.long("pixelation")
.value_name("VALUE")
.help("Pixelation rate")
.value_parser(value_parser!(u8))
.default_value(container.options.pixelation.to_string()),
)
.arg(
Arg::new("lineshift")
.short('l')
.long("line-shift")
.value_name("VALUE")
.help("Line shift rate")
.value_parser(value_parser!(f64))
.default_value(container.options.line_shift.to_string()),
)
.arg(
Arg::new("reverse")
.short('r')
.long("reverse")
.value_name("VALUE")
.help("Reverse rate")
.value_parser(value_parser!(f64))
.default_value(container.options.reverse.to_string()),
)
.arg(
Arg::new("flip")
.short('f')
.long("flip")
.value_name("VALUE")
.help("Flip rate")
.value_parser(value_parser!(f64))
.default_value(container.options.flip.to_string()),
)
.arg(
Arg::new("channelswap")
.short('c')
.long("channel-swap")
.value_name("VALUE")
.help("Channel swap rate")
.value_parser(value_parser!(f64))
.default_value(container.options.channel_swap.to_string()),
)
.arg(
Arg::new("channelshift")
.short('t')
.long("channel-shift")
.value_name("VALUE")
.help("Channel shift rate")
.value_parser(value_parser!(f64))
.default_value(container.options.channel_shift.to_string()),
)
.arg(
Arg::new("ansi")
.short('a')
.long("ansi")
.help("Use ANSI color palette")
.long_help("Use ANSI color set")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("seed")
.short('s')
.long("seed")
.value_name("VALUE")
.help("Custom seed")
.long_help("Set a custom seed value")
.hide_default_value(true)
.value_parser(value_parser!(u64))
.default_value(container.options.seed.to_string()),
)
.arg(
Arg::new("batch")
.short('b')
.long("batch")
.value_name("VALUE")
.help("Number of files to output")
.long_help("Enable batch mode and set the number of files to output")
.hide_default_value(true)
.value_parser(value_parser!(u8))
.default_value("1"),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("VALUE")
.help("Output filename")
.hide_default_value(true)
.default_value("moshed"),
);
(matches.get_matches(), container)
}
fn args() -> (PathBuf, String, MoshCore, u8) {
let (matches, mut container) = arg_matches();
let input = matches.get_one::<PathBuf>("file").unwrap();
let output = matches.get_one::<String>("output").unwrap();
let batch = matches.get_one::<u8>("batch").unwrap();
container.options.min_rate = *matches.get_one::<u16>("minrate").unwrap();
container.options.max_rate = *matches.get_one::<u16>("maxrate").unwrap();
container.options.pixelation = *matches.get_one::<u8>("pixelation").unwrap();
container.options.line_shift = *matches.get_one::<f64>("lineshift").unwrap();
container.options.reverse = *matches.get_one::<f64>("reverse").unwrap();
container.options.flip = *matches.get_one::<f64>("flip").unwrap();
container.options.channel_swap = *matches.get_one::<f64>("channelswap").unwrap();
container.options.channel_shift = *matches.get_one::<f64>("channelshift").unwrap();
container.options.ansi = *matches.get_one::<bool>("ansi").unwrap();
container.options.seed = *matches.get_one::<u64>("seed").unwrap();
(input.clone(), output.to_string(), container, *batch)
}
fn filename(output: &str, index: u8, batch: u8) -> String {
if batch > 1 {
format!("{output}-{index:03}.png")
} else {
format!("{output}.png")
}
}
fn cli(input: PathBuf, output: &str, mut container: MoshCore, batch: u8) {
let mut index = 0;
let spinner = ProgressBar::new_spinner();
let spinner_style = if cfg!(unix) {
if display_var() | cfg!(target_os = "macos") {
SPINNER_2
} else {
SPINNER_1
}
} else {
SPINNER_1
};
if let Some(file_name) = input.file_name().and_then(|name| name.to_str()) {
println!("file: {file_name}");
}
println!("seed: \x1b[3m{}\x1b[0m", &container.options.seed);
spinner.enable_steady_tick(std::time::Duration::from_millis(90));
spinner.set_style(ProgressStyle::default_spinner().tick_strings(&spinner_style));
spinner.set_message("\x1b[36mreading input\x1b[0m");
let image = match read_file(input) {
Ok(image) => image,
Err(error) => {
spinner.finish_with_message("\x1b[1;31mERROR\x1b[0m");
eprintln!("{error}");
std::process::exit(1)
}
};
match container.read_image(&image) {
Ok(new_image) => new_image,
Err(error) => {
spinner.finish_with_message("\x1b[1;31mERROR\x1b[0m");
eprintln!("{error}");
std::process::exit(1)
}
}
spinner.println(format!("mode: {}", color_type_str(&container)));
for _ in 0..batch {
spinner.set_message("\x1b[94mprocessing\x1b[0m");
if let Err(error) = container.mosh() {
spinner.finish_with_message("\x1b[1;31mERROR\x1b[0m");
eprintln!("{error}");
std::process::exit(1);
}
index += 1;
container.options.seed += 1;
spinner.set_message("\x1b[33mwriting output\x1b[0m");
if let Err(error) = write_file(
&filename(output, index, batch),
&container.data,
&container.options,
) {
spinner.finish_with_message("\x1b[1;31mERROR\x1b[0m");
eprintln!("{error}");
std::process::exit(1);
}
}
spinner.finish_with_message("\x1b[1;32mDONE\x1b[0m");
}
pub fn start() {
let (input, output, container, series) = args();
cli(input, &output, container, series);
}