lospec_cli/cmd/
download.rs

1use std::{
2    path::{Path, PathBuf},
3    str::FromStr,
4};
5
6use clap::{builder::PossibleValue, ValueEnum};
7use serde_json::json;
8use thiserror::Error;
9
10use crate::palette::Color;
11
12#[derive(Clone, Debug)]
13pub enum PngSize {
14    /// 1x1px
15    X1,
16    /// 8x8px
17    X8,
18    /// 32x32px
19    X32,
20}
21
22impl PngSize {
23    fn slug(&self) -> &'static str {
24        match self {
25            PngSize::X1 => "-1x",
26            PngSize::X8 => "-8x",
27            PngSize::X32 => "-32x",
28        }
29    }
30}
31
32impl ValueEnum for PngSize {
33    fn value_variants<'a>() -> &'a [Self] {
34        &[Self::X1, Self::X8, Self::X32]
35    }
36
37    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
38        Some(PossibleValue::new(match self {
39            Self::X1 => "x1",
40            Self::X8 => "x8",
41            Self::X32 => "x32",
42        }))
43    }
44}
45
46#[derive(Clone, Debug)]
47pub enum Format {
48    /// Xcode's `.colorset` folder format
49    Colorset,
50    /// List of hex values
51    Hex,
52    /// PNG image
53    Png,
54    /// JASC Pal file
55    Pal,
56    /// Photoshop ASE file
57    Ase,
58    /// Paint.NET TXT file
59    Txt,
60    /// GIMP GPL file
61    Gpl,
62}
63
64impl Format {
65    /// Return the file extension used by Lospec.
66    fn file_extension(&self) -> &'static str {
67        match self {
68            Format::Colorset | Format::Hex => "hex",
69            Format::Png => "png",
70            Format::Pal => "pal",
71            Format::Ase => "ase",
72            Format::Txt => "txt",
73            Format::Gpl => "gpl",
74        }
75    }
76}
77
78impl ValueEnum for Format {
79    fn value_variants<'a>() -> &'a [Self] {
80        &[
81            Self::Colorset,
82            Self::Hex,
83            Self::Png,
84            Self::Pal,
85            Self::Ase,
86            Self::Txt,
87            Self::Gpl,
88        ]
89    }
90
91    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
92        Some(PossibleValue::new(match self {
93            Self::Colorset => "colorset",
94            Self::Hex => "hex",
95            Self::Png => "png",
96            Self::Pal => "pal",
97            Self::Ase => "ase",
98            Self::Txt => "txt",
99            Self::Gpl => "gpl",
100        }))
101    }
102}
103
104#[derive(Debug, Error)]
105pub enum Error {
106    #[error(transparent)]
107    IoError(#[from] std::io::Error),
108
109    #[error(transparent)]
110    RequestError(#[from] reqwest::Error),
111}
112
113#[derive(Debug)]
114pub struct Download {
115    /// Palette slug.
116    slug: String,
117    /// Output file path.
118    path: PathBuf,
119    /// Output file format.
120    format: Format,
121    /// Output file size (only if PNG).
122    size: Option<PngSize>,
123}
124
125impl Download {
126    pub fn new(slug: String, path: PathBuf, format: Format, size: Option<PngSize>) -> Self {
127        Self {
128            slug,
129            path,
130            format,
131            size,
132        }
133    }
134
135    /// Execute the download request.
136    pub async fn execute(mut self) -> Result<(), Error> {
137        let client = reqwest::Client::new();
138
139        match (&self.format, &self.size) {
140            (Format::Png, None) => self.slug.push_str(PngSize::X32.slug()),
141            (Format::Png, Some(size)) => self.slug.push_str(size.slug()),
142            _ => {}
143        }
144
145        let response = client
146            .get(format!(
147                "https://lospec.com/palette-list/{}.{}",
148                self.slug,
149                self.format.file_extension()
150            ))
151            .send()
152            .await?;
153
154        match self.format {
155            Format::Colorset => {
156                let contents = response.text().await?;
157                let colors = contents.split("\n").filter(|s| !s.is_empty());
158                export_colorset(self.path, colors).map_err(Error::IoError)
159            }
160            Format::Hex | Format::Ase | Format::Gpl | Format::Pal | Format::Png | Format::Txt => {
161                std::fs::write(self.path, response.bytes().await?).map_err(Error::IoError)
162            }
163        }
164    }
165}
166
167/// Export a pallete as `.colorset`.
168fn export_colorset<'a, P, I>(path: P, colors: I) -> Result<(), std::io::Error>
169where
170    P: AsRef<Path>,
171    I: Iterator<Item = &'a str>,
172{
173    // Create the target folder
174    std::fs::create_dir_all(&path)?;
175
176    // Create the folder's Contents.json
177    std::fs::write(
178        path.as_ref().join("Contents.json"),
179        generate_folder_contents(),
180    )?;
181
182    for color in colors {
183        let colorset_path = path.as_ref().join(format!("{}.colorset", color));
184        // Prepare the folder
185        std::fs::create_dir_all(&colorset_path)?;
186        // Write all colors
187        std::fs::write(
188            colorset_path.join("Contents.json"),
189            generate_contents(Color::from_str(color).unwrap()),
190        )?;
191    }
192
193    Ok(())
194}
195
196/// Generate the `Contents.json` for the palette folder.
197fn generate_folder_contents() -> String {
198    let contents_json = json! {
199        {
200            "info" : {
201              "author" : "xcode",
202              "version" : 1
203            }
204          }
205    };
206    serde_json::to_string_pretty(&contents_json).expect("json should be valid")
207}
208
209/// Generate the `Contents.json` for the `.colorset` folder.
210fn generate_contents(color: Color) -> String {
211    let contents_json = json! {
212        {
213            "colors": [
214                {
215                    "color": {
216                        "color-space": "srgb",
217                        "components": {
218                            "alpha": "1.000",
219                            "blue": format!("{:#X}", color.blue),
220                            "green": format!("{:#X}", color.green),
221                            "red": format!("{:#X}", color.red)
222                        }
223                    },
224                    "idiom": "universal"
225                }
226            ],
227            "info": {
228                "author": "xcode",
229                "version": 1
230            }
231        }
232    };
233    serde_json::to_string_pretty(&contents_json).expect("json should be valid")
234}