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 X1,
16 X8,
18 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 Colorset,
50 Hex,
52 Png,
54 Pal,
56 Ase,
58 Txt,
60 Gpl,
62}
63
64impl Format {
65 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 slug: String,
117 path: PathBuf,
119 format: Format,
121 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 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
167fn 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 std::fs::create_dir_all(&path)?;
175
176 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 std::fs::create_dir_all(&colorset_path)?;
186 std::fs::write(
188 colorset_path.join("Contents.json"),
189 generate_contents(Color::from_str(color).unwrap()),
190 )?;
191 }
192
193 Ok(())
194}
195
196fn 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
209fn 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}