Repository: doprz/dipc (language: rust)

Issue:
use of unstable library feature 'is_some_and'
Compiling after merging PR #1 causes this error at `src/main.rs:93:10`
`use of unstable library feature 'is_some_and'`

```rust
// Print palettes
let color = supports_color::on_cached(supports_color::Stream::Stdout)
    .is_some_and(|level| level.has_16m);
```


Discussion / hints:
That was me forgetting to switch off of nightly. Whoops.
Can be rewritten as 
```rs
    let color = matches!(supports_color::on_cached(supports_color::Stream::Stdout), Some(level) if level.has_16m);
   
Fixed by:

```rust
// Print palettes
let color = match supports_color::on_cached(supports_color::Stream::Stdout) {
    Some(level) => level.has_16m,
    None => false,
};
```

Fix patch:
diff --git a/README.md b/README.md
index 9785674..8fa5162 100644
--- a/README.md
+++ b/README.md
@@ -21,8 +21,8 @@ Convert your favorite images and wallpapers with your favorite color palettes/th
 
 | Original | Converted |
 | -------- | --------- |
-| ![Magic view of the Gosausee in the Austrian Alps](images/ivan-rohovchenko-pkLBb75JnHc-unsplash.jpg) | ![Magic view of the Gosausee in the Austrian Alps_nord](images/ivan-rohovchenko-pkLBb75JnHc-unsplash_nord-Polar%20Night-Snow%20Storm-Frost.png) |
-| ![Human vs Nature](images/victor-rosario-pa9sROVpkgQ-unsplash.jpg) | ![Human vs Nature_gruvbox](images/victor-rosario-pa9sROVpkgQ-unsplash_gruvbox-Dark%20mode-Light%20mode.png) |
+| ![Magic view of the Gosausee in the Austrian Alps](images/ivan-rohovchenko-pkLBb75JnHc-unsplash.jpg) | ![Magic view of the Gosausee in the Austrian Alps_nord](images/ivan-rohovchenko-pkLBb75JnHc-unsplash_nord-Polar-Night-Snow-Storm-Frost.png) |
+| ![Human vs Nature](images/victor-rosario-pa9sROVpkgQ-unsplash.jpg) | ![Human vs Nature_gruvbox](images/victor-rosario-pa9sROVpkgQ-unsplash_gruvbox-Dark-mode-Light-mode.png) |
 
 ### Image Credits
 
diff --git a/images/ivan-rohovchenko-pkLBb75JnHc-unsplash_nord-Polar Night-Snow Storm-Frost.png b/images/ivan-rohovchenko-pkLBb75JnHc-unsplash_nord-Polar-Night-Snow-Storm-Frost.png
similarity index 100%
rename from images/ivan-rohovchenko-pkLBb75JnHc-unsplash_nord-Polar Night-Snow Storm-Frost.png
rename to images/ivan-rohovchenko-pkLBb75JnHc-unsplash_nord-Polar-Night-Snow-Storm-Frost.png
diff --git a/images/victor-rosario-pa9sROVpkgQ-unsplash_gruvbox-Dark mode-Light mode.png b/images/victor-rosario-pa9sROVpkgQ-unsplash_gruvbox-Dark-mode-Light-mode.png
similarity index 100%
rename from images/victor-rosario-pa9sROVpkgQ-unsplash_gruvbox-Dark mode-Light mode.png
rename to images/victor-rosario-pa9sROVpkgQ-unsplash_gruvbox-Dark-mode-Light-mode.png
diff --git a/src/cli.rs b/src/cli.rs
index e377c95..d8a0f27 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -6,36 +6,48 @@ use serde_json::Value;
 #[derive(Parser, Debug)]
 #[command(author, version, about, long_about = None)]
 pub struct Cli {
-    /// The color palette to use. The name of a builtin theme, or the path to a theme in JSON
-    /// or a JSON string with the theme(starting with `JSON: {}`). Run with --help instead of -h
-    /// for a list of all builtin themes
+    // Options
+    /// The color palette variation(s) to use
+    /// Run with --help instead of -h for a list of all possible values
     ///
-    /// Builtin themes:
-    ///
-    /// - catppuccin
-    /// - everforest
-    /// - gruvbox
-    /// - gruvbox_material
-    /// - nord
-    /// - rosepine
-    pub color_palette: ColorPalette,
-
-    /// The variations of the theme to generate images for.
-    /// Possible values: `all` to generate an image for each of the variations, `none` if you are
-    /// using a flat theme without variations, or a comma-delimited list of the names of variations
-    /// it should use
-    #[arg(short, long, value_name = "VARIATIONS", default_value = "all")]
+    /// Possible values:
+    /// - `all` to generate an image for each of the variations
+    /// - `none` if you are using a flat theme without variations
+    /// - or a comma-delimited list of the names of variations it should use
+    #[arg(
+        short,
+        long,
+        value_name = "VARIATIONS",
+        default_value = "all",
+        verbatim_doc_comment
+    )]
     pub styles: ColorPaletteStyles,
 
+    /// Output directory
+    #[arg(short, long, value_name = "PATH", default_value = "output")]
+    pub output: PathBuf,
+
     /// Verbose mode (-v, -vv, -vvv)
     #[arg(short, long, action = clap::ArgAction::Count)]
     pub verbose: u8,
 
-    /// Output directory
-    #[arg(short, long, value_name = "PATH", default_value = "output")]
-    pub output: PathBuf,
+    // Arguments
+    /// The color palette to use, name of a builtin theme, path to a theme in JSON, or a JSON string with the theme (starting with `JSON: {}`)
+    /// Run with --help instead of -h for a list of all builtin themes
+    ///
+    /// Builtin themes:
+    /// - catppuccin
+    /// - edge
+    /// - everforest
+    /// - gruvbox
+    /// - gruvbox-material
+    /// - nord
+    /// - rose-pine
+    /// - tokyo-night
+    #[arg(value_name = "PALETTE", verbatim_doc_comment)]
+    pub color_palette: ColorPalette,
 
-    /// The image to process
+    /// The image(s) to process
     #[arg(value_name = "FILE")]
     pub process: Vec<PathBuf>,
 }
@@ -59,7 +71,7 @@ impl FromStr for ColorPaletteStyles {
                     let mut vars = Vec::new();
                     for var in some.split(',') {
                         if var.is_empty() {
-                            return Err("One of the variations seems to be an empty string. Do you have a double comma in your variations list(-v)?".to_string());
+                            return Err("One of the variations seems to be an empty string. Do you have a double comma in your variations list (-v)?".to_string());
                         };
                         vars.push(var.to_string())
                     }
@@ -77,16 +89,34 @@ impl FromStr for ColorPaletteStyles {
 #[derive(Clone, Debug)]
 pub enum ColorPalette {
     RawJSON { map: serde_json::Map<String, Value> },
+    Catppuccin,
+    Edge,
     Everforest,
     Gruvbox,
     GruvboxMaterial,
     Nord,
     RosePine,
-    Catppucin,
-    Edge,
     TokyoNight,
 }
 
+impl std::fmt::Display for ColorPalette {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ColorPalette::RawJSON { map } => {
+                write!(f, "JSON: {}", serde_json::to_string(map).unwrap())
+            }
+            ColorPalette::Catppuccin => write!(f, "catppuccin"),
+            ColorPalette::Edge => write!(f, "edge"),
+            ColorPalette::Everforest => write!(f, "everforest"),
+            ColorPalette::Gruvbox => write!(f, "gruvbox"),
+            ColorPalette::GruvboxMaterial => write!(f, "gruvbox-material"),
+            ColorPalette::Nord => write!(f, "nord"),
+            ColorPalette::RosePine => write!(f, "rose-pine"),
+            ColorPalette::TokyoNight => write!(f, "tokyo-night"),
+        }
+    }
+}
+
 impl FromStr for ColorPalette {
     type Err = String;
 
@@ -100,16 +130,17 @@ impl FromStr for ColorPalette {
             return Ok(ColorPalette::RawJSON { map });
         };
         let palette = match s {
-            "catppuccin" | "catpucin" | "catppucin" | "catpuccin" => ColorPalette::Catppucin,
+            "catppuccin" => ColorPalette::Catppuccin,
+            "edge" => ColorPalette::Edge,
             "everforest" => ColorPalette::Everforest,
             "gruvbox" => ColorPalette::Gruvbox,
-            "gruvbox_material" | "gruvbox-material" | "gruvboxmaterial" => {
+            "gruvbox-material" | "gruvbox_material" | "gruvboxmaterial" => {
                 ColorPalette::GruvboxMaterial
             }
             "nord" => ColorPalette::Nord,
-            "rosepine" | "rose-pine" | "rose_pine" => ColorPalette::RosePine,
-            "edge" => ColorPalette::Edge,
-            "tokyonight" | "tokyo_night" | "tokyo-night" => ColorPalette::TokyoNight,
+            "rose-pine" | "rose_pine" | "rosepine" => ColorPalette::RosePine,
+            "tokyo-night" | "tokyo_night" | "tokyonight" => ColorPalette::TokyoNight,
+
             // The color palette seems to be the path to an external file
             external => {
                 let external: PathBuf = external.into();
diff --git a/src/config.rs b/src/config.rs
index 709501c..d26792f 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,11 +1,13 @@
+use std::path::PathBuf;
+
 use image::Rgb;
 use serde_json::Value;
 
-use crate::cli::ColorPaletteStyles;
+use crate::cli::{ColorPalette, ColorPaletteStyles};
 
 pub fn parse_palette(
     json: serde_json::Map<String, Value>,
-    styles: ColorPaletteStyles,
+    styles: &ColorPaletteStyles,
 ) -> Result<Vec<Palette>, String> {
     match styles {
         ColorPaletteStyles::None => {
@@ -30,12 +32,12 @@ pub fn parse_palette(
             let mut json = json;
             let mut out = Vec::with_capacity(styles.len());
             for style in styles {
-                let Some(Value::Object(map)) = json.remove(&style) else {
+                let Some(Value::Object(map)) = json.remove(style) else {
                     return Err(format!("Failed to parse palette style `{style}`: It does not exist in the theme JSON source"))
                 };
                 let mut palette = Palette::try_from(map)
                     .map_err(|err| format!("Failed to parse palette style `{style}`: {err}"))?;
-                palette.name = Some(style);
+                palette.name = Some(style.to_string());
                 out.push(palette);
             }
             Ok(out)
@@ -58,7 +60,7 @@ impl TryFrom<serde_json::Map<String, Value>> for Palette {
             let mut colorarr: [u8; 3] = [0_u8; 3];
             match value {
                 Value::String(hex) => {
-                    // For representing a color as a hex string `#ff8800` in JSON
+                    // For representing a color as a hex string `#FF8800` in JSON
                     if !hex.starts_with('#') {
                         return Err(format!(
                             "Encountered a color string not in the `#HEX` format: `{hex}`"
@@ -92,7 +94,7 @@ impl TryFrom<serde_json::Map<String, Value>> for Palette {
                     }
                 }
                 Value::Array(arr) => {
-                    // For representing a color as `[128,255,0]` in JSON
+                    // For representing a color as `[128, 255, 0]` in JSON
                     if arr.len() != 3 {
                         return Err(format!(
                             "Encountered a color array with {} elements instead of 3: {arr:?}",
@@ -131,3 +133,37 @@ impl TryFrom<serde_json::Map<String, Value>> for Palette {
         Ok(Palette { colors, name: None })
     }
 }
+
+pub fn output_file_name(
+    input_file_path: &PathBuf,
+    color_palette: &ColorPalette,
+    color_palette_variations: &[Palette],
+) -> String {
+    let file_stem = match input_file_path.file_stem() {
+        Some(stem) => match stem.to_str() {
+            Some(stem) => stem,
+            None => "image",
+        },
+        None => "image",
+    };
+
+    let color_palette = match &color_palette {
+        ColorPalette::RawJSON { .. } => String::new(),
+        _ => format!("{}", color_palette),
+    };
+
+    let mut output_file_name = String::new();
+
+    output_file_name.push_str(file_stem);
+    output_file_name.push_str(format!("_{}", color_palette).as_str());
+
+    color_palette_variations.iter().for_each(|variation| {
+        if let Some(name) = &variation.name {
+            output_file_name.push_str(format!("-{}", name).as_str());
+        }
+    });
+
+    output_file_name.push_str(".png");
+
+    output_file_name
+}
diff --git a/src/main.rs b/src/main.rs
index 15b9b49..f1517f9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,10 +1,6 @@
-use std::{
-    ffi::OsStr,
-    io::{self, stdout, BufWriter, Write},
-};
+use std::io::{self, stdout, BufWriter, Write};
 
 use clap::Parser;
-use cli::ColorPalette;
 use delta::Lab;
 use owo_colors::{OwoColorize, Style};
 use rayon::{
@@ -14,7 +10,10 @@ use rayon::{
     slice::{ParallelSlice, ParallelSliceMut},
 };
 
-use crate::{cli::Cli, config::parse_palette};
+use crate::{
+    cli::Cli,
+    config::{output_file_name, parse_palette},
+};
 
 mod cli;
 mod config;
@@ -23,14 +22,7 @@ mod palettes;
 
 fn main() -> io::Result<()> {
     let cli = Cli::parse();
-    let Cli {
-        color_palette,
-        styles,
-        mut output,
-        process,
-        verbose,
-    } = cli;
-    if process.is_empty() {
+    if cli.process.is_empty() {
         eprintln!(
             "{}",
             "You need to provide at least a single image to process"
@@ -38,47 +30,36 @@ fn main() -> io::Result<()> {
         );
         std::process::exit(127)
     };
-    if !output.is_dir() {
-        eprintln!(
-            "Provided output `{}` does not appear to be a directory.\nAttempting to create it!",
-            output
-                .display()
-                .if_supports_color(owo_colors::Stream::Stderr, |text| text.red())
-        );
-        if let Err(err) = std::fs::create_dir_all(&output) {
-            eprintln!(
-                "Creating provided output directory failed with error: {}",
-                err.if_supports_color(owo_colors::Stream::Stderr, |text| text.red())
-            );
-            std::process::exit(127)
-        };
-    }
+    // TODO: tmp disable
+    // if !cli.output.is_dir() {
+    //     eprintln!(
+    //         "Provided output `{}` does not appear to be a directory.\nAttempting to create it!",
+    //         cli.output
+    //             .display()
+    //             .if_supports_color(owo_colors::Stream::Stderr, |text| text.red())
+    //     );
+    //     if let Err(err) = std::fs::create_dir_all(&cli.output) {
+    //         eprintln!(
+    //             "Creating provided output directory failed with error: {}",
+    //             err.if_supports_color(owo_colors::Stream::Stderr, |text| text.red())
+    //         );
+    //         std::process::exit(127)
+    //     };
+    // }
     let stdout = stdout().lock();
     let mut writer = BufWriter::new(stdout);
-    if verbose >= 2 {
+    if cli.verbose >= 1 {
         writeln!(
             writer,
-            "\
-Using color palette: {color_palette:?}
-With styles: {styles:?}"
+            "Color palette: {}\nStyles: {:?}",
+            cli.color_palette, cli.styles
         )?;
     }
-    if verbose >= 3 {
-        writeln!(
-            writer,
-            "\
-To process {process:?}
-And writing results to {output:?}"
-        )?;
-    };
-    let mut name = {
-        if !matches!(color_palette, ColorPalette::RawJSON { .. }) {
-            format!("{color_palette:?}")
-        } else {
-            String::new()
-        }
-    };
-    let palettes = match parse_palette(color_palette.get_json(), styles) {
+    if cli.verbose >= 2 {
+        // writeln!(writer, "Processing {:#?}\nWriting results to {:#?} directory", cli.process, cli.output)?;
+        writeln!(writer, "Processing {:#?}", cli.process)?;
+    }
+    let palettes = match parse_palette(cli.color_palette.clone().get_json(), &cli.styles) {
         Ok(p) => p,
         Err(err) => {
             eprintln!(
@@ -89,8 +70,10 @@ And writing results to {output:?}"
         }
     };
     // Print palettes
-    let color = supports_color::on_cached(supports_color::Stream::Stdout)
-        .is_some_and(|level| level.has_16m);
+    let color = match supports_color::on_cached(supports_color::Stream::Stdout) {
+        Some(level) => level.has_16m,
+        None => false,
+    };
     let max_name = palettes
         .iter()
         .map(|p| p.name.as_ref().map(|n| n.len()).unwrap_or_default())
@@ -124,13 +107,9 @@ And writing results to {output:?}"
         }
     }
     writer.flush()?;
-    palettes.iter().for_each(|p| {
-        p.name.as_ref().map(|n| {
-            name.push('-');
-            name.push_str(n)
-        });
-    });
-    let palettes: Vec<_> = palettes
+
+    let palettes_lab: Vec<_> = palettes
+        .clone()
         .into_par_iter()
         .flat_map_iter(|palette| {
             palette
@@ -139,7 +118,7 @@ And writing results to {output:?}"
                 .map(|(_name, color)| Lab::from(color.0))
         })
         .collect();
-    for path in process {
+    for (idx, path) in cli.process.iter().enumerate() {
         // Open image
         let mut image = match image::open(&path) {
             Ok(i) => i.into_rgba8(),
@@ -153,7 +132,13 @@ And writing results to {output:?}"
                 std::process::exit(127)
             }
         };
-        let filename = path.file_stem().unwrap_or(&OsStr::new("image"));
+
+        println!(
+            "[{}/{}] Converting image... (this may take a while)",
+            idx + 1,
+            cli.process.len()
+        );
+
         const CHUNK: usize = 4;
         // Convert image to LAB representation
         let mut lab = Vec::with_capacity(image.as_raw().len() / CHUNK);
@@ -168,20 +153,16 @@ And writing results to {output:?}"
         lab.par_iter()
             .zip_eq(image.par_chunks_exact_mut(CHUNK))
             .for_each(|(&lab, bytes)| {
-                let new_rgb = lab.to_nearest_palette(&palettes).to_rgb();
+                let new_rgb = lab.to_nearest_palette(&palettes_lab).to_rgb();
                 bytes[..3].copy_from_slice(&new_rgb);
             });
 
-        let mut new_name = filename.to_os_string();
-        new_name.push("-");
-        new_name.push(&name);
-        new_name.push(".png");
-        output.push(new_name);
-        match image.save_with_format(&output, image::ImageFormat::Png) {
-            Ok(_) => {}
+        let output_file_name = output_file_name(&path, &cli.color_palette, &palettes);
+        match image.save_with_format(&output_file_name, image::ImageFormat::Png) {
+            Ok(_) => println!("Saved image: {}", output_file_name),
             Err(err) => {
                 eprintln!(
-                    "Encountered error while trying to save image `{}`: {}",
+                    "Encountered error while trying to save image {}: {}",
                     path.display()
                         .if_supports_color(owo_colors::Stream::Stderr, |text| text.blue()),
                     err.if_supports_color(owo_colors::Stream::Stderr, |text| text.red())
@@ -189,7 +170,6 @@ And writing results to {output:?}"
                 std::process::exit(127)
             }
         };
-        output.pop();
     }
     Ok(())
 }
diff --git a/src/palettes.rs b/src/palettes.rs
index 7c69681..0cceaf6 100644
--- a/src/palettes.rs
+++ b/src/palettes.rs
@@ -5,31 +5,31 @@ use crate::cli::ColorPalette;
 impl ColorPalette {
     pub fn get_json(self) -> serde_json::Map<String, Value> {
         let colors = match self {
-            ColorPalette::Nord => {
-                serde_json::from_str(include_str!("./palettes/nord.json")).unwrap()
-            }
-            ColorPalette::RosePine => {
-                serde_json::from_str(include_str!("./palettes/rose-pine.json")).unwrap()
-            }
-            ColorPalette::Catppucin => {
+            ColorPalette::RawJSON { map } => return map,
+            ColorPalette::Catppuccin => {
                 serde_json::from_str(include_str!("./palettes/catppuccin.json")).unwrap()
             }
+            ColorPalette::Edge => {
+                serde_json::from_str(include_str!("./palettes/edge.json")).unwrap()
+            }
             ColorPalette::Everforest => {
                 serde_json::from_str(include_str!("./palettes/everforest.json")).unwrap()
             }
+            ColorPalette::Gruvbox => {
+                serde_json::from_str(include_str!("./palettes/gruvbox.json")).unwrap()
+            }
             ColorPalette::GruvboxMaterial => {
                 serde_json::from_str(include_str!("./palettes/gruvbox-material.json")).unwrap()
             }
-            ColorPalette::Gruvbox => {
-                serde_json::from_str(include_str!("./palettes/gruvbox.json")).unwrap()
+            ColorPalette::Nord => {
+                serde_json::from_str(include_str!("./palettes/nord.json")).unwrap()
             }
-            ColorPalette::Edge => {
-                serde_json::from_str(include_str!("./palettes/edge.json")).unwrap()
+            ColorPalette::RosePine => {
+                serde_json::from_str(include_str!("./palettes/rose-pine.json")).unwrap()
             }
             ColorPalette::TokyoNight => {
                 serde_json::from_str(include_str!("./palettes/tokyo-night.json")).unwrap()
             }
-            ColorPalette::RawJSON { map } => return map,
         };
         let Value::Object(obj) = colors else {
             panic!("An included theme appears to not be a JSON object?")


Test patch: