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}