use crate::{
fs_utils::find_extensions_icase,
themes::theme::{CursorMapping, CursorTheme, CursorType, TypedCursor},
warn,
};
use std::{fs, path::PathBuf};
use anyhow::{Result, anyhow, bail};
use clap::{Parser, ValueEnum};
use fast_image_resize::{FilterType, ResizeAlg};
use dialoguer::{
Select,
console::{Term, style},
theme::ColorfulTheme,
};
use documented::DocumentedVariants;
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Args {
#[arg(required = true)]
paths: Vec<PathBuf>,
#[arg(long, verbatim_doc_comment)]
manual: bool,
#[arg(
long,
default_value = "box",
value_name = "ALGORITHM",
verbatim_doc_comment
)]
scale_with: ScalingAlgorithm,
#[arg(long, value_name = "ALGORITHM")]
upscale_with: Option<ScalingAlgorithm>,
#[arg(long, value_name = "ALGORITHM")]
downscale_with: Option<ScalingAlgorithm>,
#[arg(long, value_parser, num_args(1..), value_name = "F64_SCALE_FACTORS")]
scale_to: Vec<f64>,
#[arg(short, long, default_value = "./")]
out: PathBuf,
}
#[derive(Debug, Clone, ValueEnum)]
enum ScalingAlgorithm {
Nearest,
Box,
Bilinear,
Mitchell,
Lanczos3,
}
impl From<&ScalingAlgorithm> for FilterType {
fn from(alg: &ScalingAlgorithm) -> Self {
match alg {
ScalingAlgorithm::Nearest => unreachable!(),
ScalingAlgorithm::Box => Self::Box,
ScalingAlgorithm::Bilinear => Self::Bilinear,
ScalingAlgorithm::Mitchell => Self::Mitchell,
ScalingAlgorithm::Lanczos3 => Self::Lanczos3,
}
}
}
impl From<&ScalingAlgorithm> for ResizeAlg {
fn from(alg: &ScalingAlgorithm) -> Self {
match alg {
ScalingAlgorithm::Nearest => Self::Nearest,
v => Self::Convolution(FilterType::from(v)),
}
}
}
#[derive(Debug)]
pub struct ParsedArgs {
pub installer_files: Vec<PathBuf>,
pub cursor_files: Vec<PathBuf>,
pub manual: bool,
pub scale_to: Vec<f64>,
pub upscale_with: ResizeAlg,
pub downscale_with: ResizeAlg,
pub out: PathBuf,
}
impl ParsedArgs {
pub fn from_args(args: Args) -> Result<Self> {
let paths = args.paths;
let manual = args.manual;
let mut installer_files = Vec::new();
let mut cursor_files = Vec::new();
for path in paths {
let path_display = path.display();
if !path.exists() {
#[cfg(windows)]
bail!(
"path={path_display} doesn't exist. \n\
note that if you use powershell and your path looks similar to the \
first, convert it to the second by removing the trailing backslash: \n\
.\\currust.exe '.\\a path\\to a\\dir\\' -> .\\currust.exe '.\\a path\\to a\\dir'"
);
bail!("path={path_display} doesn't exist");
}
if path.is_dir() {
cursor_files.extend(find_extensions_icase(&path, &["cur", "ani"])?);
} else if path.is_file() {
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
warn!("ignoring file {path_display} as it has no extension");
continue;
};
match ext.to_ascii_lowercase().as_str() {
"inf" | "crs" => installer_files.push(path),
"cur" | "ani" => cursor_files.push(path),
_ => warn!("ignoring file {path_display} as it is not a cursor"),
}
} else {
warn!("ignoring path={path_display} as it is neither a dir or a file",);
}
}
let mut scale_to = args.scale_to;
for &sf in &scale_to {
if sf.is_nan() || sf.is_infinite() {
bail!("invalid sf={sf}: can't be NaN or pos/neg infinity")
}
if sf <= 0.1 {
bail!("invalid sf={sf}: can't be 0.1 or less");
}
if sf > 100.0 {
bail!("invalid sf={sf}: can't be greater than 100.0")
}
}
scale_to.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
scale_to.dedup();
let (upscale_with, downscale_with) = (
ResizeAlg::from(args.upscale_with.as_ref().unwrap_or(&args.scale_with)),
ResizeAlg::from(args.downscale_with.as_ref().unwrap_or(&args.scale_with)),
);
let out = args.out;
fs::create_dir_all(&out)?;
Ok(Self {
installer_files,
cursor_files,
manual,
scale_to,
upscale_with,
downscale_with,
out,
})
}
#[must_use]
pub const fn get_algorithm(&self, scale_factor: f64) -> ResizeAlg {
if scale_factor > 1.0 {
self.upscale_with
} else {
self.downscale_with
}
}
}
pub(super) fn prompt_for_theme(cursor_files: &[PathBuf]) -> Result<CursorTheme> {
let mut mappings = Vec::with_capacity(cursor_files.len());
let mut cursor_paths_display: Vec<_> = cursor_files
.iter()
.map(|p| {
p.file_name()
.map(|f| format!("'{}' ", f.display()))
.ok_or_else(|| anyhow!("no file name for cursor path, p={}", p.display()))
})
.collect::<Result<_>>()?;
for r#type in CursorType::VARIANTS {
let prompt = format!(
"Select the file representing '{:?}'.\n{}",
r#type,
r#type.get_variant_docs()
);
let chosen_index = Select::with_theme(&ColorfulTheme::default())
.items(&cursor_paths_display)
.with_prompt(prompt)
.default(0)
.report(false) .interact()?;
cursor_paths_display[chosen_index].push_str(&style("✓").green().to_string());
let path = cursor_files[chosen_index].clone();
mappings.push(CursorMapping { r#type, path });
}
let name = loop {
eprint!("Enter a theme name: ");
let theme_name = Term::stderr().read_line()?;
if theme_name.contains(['/', '\\']) {
eprintln!("Theme name can't contain '/' or '\\'.");
} else {
break theme_name;
}
};
let typed_cursors = mappings
.into_iter()
.map(TypedCursor::from_mapping)
.collect::<Result<_>>()?;
let theme = CursorTheme::new(typed_cursors, name)?;
Ok(theme)
}