use std::{fmt::Write, iter};
use anyhow::{anyhow, bail, Result};
use clap::{Arg, ArgMatches, SubCommand};
use color::ColorFormat;
use super::{util, Cmd};
use crate::{
color::{self, Color, ColorSpace},
terminal::{self, stdin},
State,
};
#[derive(Debug, Clone)]
pub struct Mix {
colors: Vec<(Color, ColorFormat, f64)>,
color_space: ColorSpace,
output: ColorFormat,
size: u32,
}
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 mix";
impl Cmd for Mix {
fn command<'a, 'b>(state: State) -> clap::App<'a, 'b> {
SubCommand::with_name("mix")
.about("Mix colors in a specific color space")
.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("weights")
.long("weights")
.short("w")
.takes_value(true)
.min_values(1)
.max_values(u64::MAX)
.require_delimiter(true)
.value_delimiter(",")
.help(
"The ratio in which the colors are mixed. \
For example, `--weights 2,5` indicates a ratio of 2:5. \
The default for each color is 1.",
),
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("size")
.long("size")
.short("s")
.takes_value(true)
.default_value("4")
.help("Size of the color square in terminal rows"),
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]",
),
])
}
fn parse(matches: &ArgMatches, &mut state: &mut State) -> Result<Self> {
let size = matches
.value_of("size")
.map(util::parse_size)
.unwrap_or(Ok(4))?;
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)?;
}
let colors = colors
.into_iter()
.zip(parse_weights(matches))
.map(|((color, fmt), weight)| Ok((color, fmt, weight?)))
.collect::<Result<Vec<(Color, ColorFormat, f64)>>>()?;
fn parse_weights<'a>(matches: &'a ArgMatches) -> impl Iterator<Item = Result<f64>> + 'a {
let values = matches.values_of("weights").unwrap_or_default();
values
.map(|s| s.parse::<f64>().map_err(From::from))
.chain(iter::once(1.0).cycle().map(Ok))
}
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();
Ok(Mix {
colors,
color_space,
output,
size,
})
}
fn run(&self, state: State) -> Result<()> {
let weight_sum: f64 = self.colors.iter().map(|&(.., w)| w).sum();
if weight_sum == 0.0 {
bail!("All weights are 0");
}
let (_, components) = self
.colors
.iter()
.map(|&(c, _, w)| (c.to_color_space(self.color_space), w))
.try_fold(
(self.color_space, vec![0.0, 0.0, 0.0, 0.0]),
|left, (right, w)| add_color(left, right, w / weight_sum),
)?;
let color = Color::new_unchecked(self.color_space, &components);
if state.color {
terminal::list_small(
state,
Some("Colors"),
self.colors.iter().map(|&(a, b, _)| (a, b)),
6,
)?;
let weights =
self.colors
.iter()
.map(|&(_, _, w)| w)
.fold(String::new(), |mut acc, f| {
write!(acc, ", {}", f).unwrap();
acc
});
let weights = weights
.strip_prefix(", ")
.ok_or_else(|| anyhow!("No comma prefix"))?;
println!("Weights: {}", weights);
}
terminal::show_colors(state, iter::once(color), self.output, self.size)
}
}
fn add_color(
(cs1, items1): (ColorSpace, Vec<f64>),
right: Color,
right_weight: f64,
) -> Result<(ColorSpace, Vec<f64>)> {
let (cs2, items2) = right.divide();
if cs1 == cs2 {
let new_components = items1
.into_iter()
.zip(items2)
.map(|(a, b)| a + (b * right_weight))
.collect::<Vec<_>>();
Ok((cs1, new_components))
} else {
bail!("Unequal color spaces {:?} and {:?}", cs1, cs2);
}
}