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;
16use std::fmt;
17
18enum IMcmd {
19    Magick,
20    Convert,
21}
22
23impl fmt::Display for IMcmd {
24    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
25        match self {
26            IMcmd::Magick => write!(f, "magick"),
27            IMcmd::Convert => write!(f, "convert"),
28        }
29    }
30}
31
32
33/// Inspired by how pywal uses Image Magick :)
34pub fn wal(f: &Path) -> Result<Vec<u8>> {
35    let mut cols: Vec<Srgb<u8>> = Vec::with_capacity(16); // there will be no more than 16 colors
36
37    let magick_command = has_im()?.to_string();
38
39    let mut raw_colors = String::new();
40
41    // we start with 1, since we already 'did' an iteration by initializing the variable.
42    for i in 0..20 {
43        raw_colors = imagemagick(16 + i, f, &magick_command)?;
44
45        if raw_colors.lines().count() > 16 { break }
46
47        if i == 19 {
48            anyhow::bail!("Imagemagick couldn't generate a suitable palette.");
49        }
50        // else {
51            // No need to print, just keep trying.
52            // eprintln!("Imagemagick couldn't generate a palette.");
53            // eprintln!("Trying a larger palette size {}", 16 + i);
54        // }
55    }
56
57    for line in raw_colors.lines().skip(1) {
58        let mut s = line.split_ascii_whitespace().skip(1);
59        let hex = s.next().expect("Should always be present, without spaces in between e.g. (0,0,0)");
60        //let hex : Srgb<u8> = *hex.parse::<Srgba<u8>>()?.into_format::<u8, u8>();
61        let hex = &hex[1..hex.len() - 1];
62        let rgbs: Vec<u8> = hex
63                                .split(',')
64                                .map(|x| x.parse::<u8>().expect("Should be a number"))
65                                .collect();
66        let hex = Srgb::new(rgbs[0], rgbs[1], rgbs[2]);
67        cols.push(hex);
68    }
69
70    Ok(cols.as_components().to_vec())
71}
72
73fn imagemagick(color_count: u8, img: &Path, magick_command: &str) -> Result<String> {
74    let im = Command::new(magick_command)
75        .args([
76            &format!("{}[0]", img.display()), // gif edge case, use the first frame
77            "-resize", "25%",
78            "-colors", &color_count.to_string(),
79            "-unique-colors",
80            "-colorspace", "srgb", //srgb
81            "-depth", "8", // 8 bit
82            "txt:-",
83        ])
84        .output()
85        .expect("This should run, given that `has_im()` should fail first, unless IM flags are deprecated.");
86
87    Ok(str::from_utf8(&im.stdout)?.to_owned())
88}
89
90///whether to use `magick` or good old `convert`
91fn has_im() -> Result<IMcmd> {
92    let m = "magick";
93    let c = "convert";
94
95    // .output() is used to 'eat' the output, instead of .spawn()
96    match Command::new(m).output() {
97        Ok(_) => Ok(IMcmd::Magick),
98        Err(e) => {
99            match Command::new(c).output() {
100                Ok(_) => Ok(IMcmd::Convert),
101                Err(e2) => Err(anyhow::anyhow!("Neither `magick` nor `convert` is invokable:\n{e} {e2}")),
102            }
103            // if let std::io::ErrorKind::NotFound = e.kind() {
104            //     Ok("convert".to_owned())
105            // } else {
106            //     Err(anyhow::anyhow!("An error ocurred while executing magick: {e}"))
107            // }
108        },
109    }
110}