colo 0.4.1

CLI tool to show and convert colors between various color spaces
use anyhow::{bail, Result};
use clap::{Arg, SubCommand};
use console::Term;

use super::{util, Cmd};
use crate::{
    color::{self, Color, ColorFormat, ColorSpace},
    terminal::{self, stdin},
    State,
};

pub struct Gradient {
    colors: Vec<(Color, ColorFormat)>,
    color_space: ColorSpace,
    output: ColorFormat,
    color_num: Option<usize>,
}

const COLOR_HELP_MESSAGE: &str = "\
The input colors. Multiple colors can be specified. Supported formats:

* HTML color name, e.g. 'rebeccapurple'
* Hexadecimal RGB color, e.g. '07F', '0077FF'
* Color components, e.g. 'hsl(30, 100%, 50%)'
  Commas and parentheses are optional.
  For supported color spaces, see <https://aloso.github.io/colo/color_spaces>

If colo is used behind a pipe or outside of a terminal, the colors can be provided via stdin, e.g.

$ echo orange blue FF7700 | colo gradient";

impl Cmd for Gradient {
    fn command<'a, 'b>(state: State) -> clap::App<'a, 'b> {
        SubCommand::with_name("gradient")
            .about("Create a gradient between colors")
            .args(&[
                Arg::with_name("colors")
                    .takes_value(true)
                    .index(1)
                    .required(state.interactive)
                    .multiple(true)
                    .use_delimiter(false)
                    .help(COLOR_HELP_MESSAGE),
                Arg::with_name("color-space")
                    .long("color-space")
                    .short("c")
                    .help(
                        "The color space which the colors are mixed in. \
                        Color spaces are explained here: \
                        <https://aloso.github.io/colo/color_spaces>",
                    )
                    .possible_values(&[
                        "rgb",
                        "cmy",
                        "cmyk",
                        "luv",
                        "lab",
                        "hunterlab",
                        "xyz",
                        "yxy",
                    ])
                    .case_insensitive(true)
                    .default_value("lab"),
                Arg::with_name("output-format")
                    .long("out")
                    .short("o")
                    .takes_value(true)
                    .possible_values(super::COLOR_FORMATS)
                    .hide_possible_values(true)
                    .case_insensitive(true)
                    .help(
                        "Output format (html, hex, or color space) [possible values: rgb, cmy, \
                        cmyk, hsv, hsl, lch, luv, lab, hunterlab, xyz, yxy, gry, hex, html]",
                    ),
                Arg::with_name("steps")
                    .long("steps")
                    .short("s")
                    .takes_value(true)
                    .help("Number of color steps, defaults to 10"),
            ])
    }

    fn parse(matches: &clap::ArgMatches, &mut state: &mut State) -> Result<Self> {
        let color_space = matches
            .value_of("color-space")
            .unwrap()
            .to_lowercase()
            .parse()?;

        let mut colors = match matches.values_of("colors") {
            Some(values) => util::values_to_colors(values, state)?,
            None => vec![],
        };

        if !state.interactive && colors.is_empty() {
            let input = stdin::read_all()?;
            colors = color::parse(&input, state)?;
        }

        if colors.len() != 2 {
            bail!("You have to enter exactly 2 colors");
        }

        let output = util::get_color_format(&matches, "output-format")?
            .or_else(|| {
                if colors.is_empty() {
                    None
                } else if colors.windows(2).all(|c| c[0].1 == c[1].1) {
                    Some(colors[0].1).filter(|&c| c != ColorFormat::Html)
                } else {
                    None
                }
            })
            .unwrap_or_default();

        let color_num = matches
            .value_of("steps")
            .map(|s| {
                let s = s.parse::<usize>()? + 1;
                if s < colors.len() {
                    bail!("Fewer steps than colors");
                }
                Ok(s)
            })
            .transpose()?;

        Ok(Gradient {
            colors,
            color_space,
            output,
            color_num,
        })
    }

    fn run(&self, state: State) -> Result<()> {
        let mut term_width = None;
        let mut get_term_width = || {
            if let Some(w) = term_width {
                w
            } else {
                let w = Term::stdout().size().1 as usize;
                term_width = Some(w);
                w
            }
        };

        let color_steps = self.color_num.unwrap_or_else(|| {
            if state.color {
                (get_term_width() * 2) - 1
            } else {
                10
            }
        });
        let (c1, c2) = (self.colors[0].0, self.colors[1].0);

        if state.color {
            let w = get_term_width();
            terminal::list_small(
                state,
                None,
                (0..=color_steps).map(|i| {
                    let ratio = (i as f64) / (color_steps as f64);
                    let color = c1.mix_with(c2, self.color_space, ratio);
                    (color, self.output)
                }),
                2 * w / (color_steps + 1),
            )?;
        } else {
            for i in 0..=color_steps {
                let ratio = (i as f64) / (color_steps as f64);
                let color = c1.mix_with(c2, self.color_space, ratio);
                println!("{}", self.output.format_or_hex(color));
            }
        }
        Ok(())
    }
}