wallust/backends/
wal.rs

1//! # Wal
2//! * Uses image magick to generate the colors
3//! * We parse the hex string because the tuples seems to change, like if there are no green and
4//!   blue values and only red, the output would be like `(238)`, instead of `(238, 0, 0)`
5//! ## Sample output of `convert` is like the following:
6//! ```txt
7//!   0,0: (92,64,54)  #5C4036  srgb(36.1282%,25.1188%,21.1559%)
8//!   skip      ^
9//!       we care bout this one
10//! ```
11use crate::backends::*;
12use std::process::Command;
13use std::str;
14use palette::Srgb;
15use palette::cast::AsComponents;
16
17/// Inspired by how pywal uses Image Magick :)
18pub fn wal(f: &Path) -> Result<Vec<u8>> {
19    let mut cols: Vec<Srgb<u8>> = Vec::with_capacity(16); // there will be no more than 16 colors
20
21    let magick_command = has_im()?;
22
23    let mut raw_colors = imagemagick(16 /* + 0*/, f, &magick_command)?;
24
25    // we start with 1, since we already 'did' an iteration by initializing the variable.
26    for i in 1..20 {
27        raw_colors = imagemagick(16 + i, f, &magick_command)?;
28
29        if raw_colors.lines().count() > 16 { break }
30
31        if i == 19 {
32            anyhow::bail!("Imagemagick couldn't generate a suitable palette.");
33        }
34        // else {
35            // No need to print, just keep trying.
36            // eprintln!("Imagemagick couldn't generate a palette.");
37            // eprintln!("Trying a larger palette size {}", 16 + i);
38        // }
39    }
40
41    // TODO pywal uses the first, last and from 6-8 colors from the pallete, We need a way to tell
42    // `colorspaces` module to not chop off these colors (maybe in another backend that ensures pywal parity?)
43    // https://github.com/dylanaraps/pywal/blob/236aa48e741ff8d65c4c3826db2813bf2ee6f352/pywal/backends/wal.py#L60
44
45    for line in raw_colors.lines().skip(1) {
46        let mut s = line.split_ascii_whitespace().skip(1);
47        let hex = s.next().expect("Should always be present, without spaces in between e.g. (0,0,0)");
48        //let hex : Srgb<u8> = *hex.parse::<Srgba<u8>>()?.into_format::<u8, u8>();
49        let hex = &hex[1..hex.len() - 1];
50        let rgbs: Vec<u8> = hex
51                                .split(',')
52                                .map(|x| x.parse::<u8>().expect("Should be a number"))
53                                .collect();
54        let hex = Srgb::new(rgbs[0], rgbs[1], rgbs[2]);
55        cols.push(hex);
56    }
57
58    Ok(cols.as_components().to_vec())
59}
60
61fn imagemagick(color_count: u8, img: &Path, magick_command: &str) -> Result<String> {
62    let im = Command::new(magick_command)
63        .args([
64            &format!("{}[0]", img.display()), // gif edge case, use the first frame
65            "-resize", "25%",
66            "-colors", &color_count.to_string(),
67            "-unique-colors",
68            "-colorspace", "srgb", //srgb
69            "-depth", "8", // 8 bit
70            "txt:-",
71        ])
72        .output()
73        .expect("This should run, given that `has_im()` should fail first, unless IM flags are deprecated.");
74
75    Ok(str::from_utf8(&im.stdout)?.to_owned())
76}
77
78///whether to use `magick` or good old `convert`
79fn has_im() -> Result<String> {
80    let m = String::from("magick");
81    let c = String::from("convert");
82
83    // .output() is used to 'eat' the output, instead of .spawn()
84    match Command::new(&m).output() {
85        Ok(_) => Ok(m),
86        Err(e) => {
87            match Command::new(&c).output() {
88                Ok(_) => Ok(c),
89                Err(e2) => Err(anyhow::anyhow!("Neither `magick` nor `convert` is invokable:\n{e} {e2}")),
90            }
91            // if let std::io::ErrorKind::NotFound = e.kind() {
92            //     Ok("convert".to_owned())
93            // } else {
94            //     Err(anyhow::anyhow!("An error ocurred while executing magick: {e}"))
95            // }
96        },
97    }
98}