1use std::str::FromStr;
2
3use crate::api::Url;
4use clap::{ArgGroup, Args, Parser, Subcommand};
5
6#[derive(Parser)]
7pub struct Cli {
8 #[clap(subcommand)]
9 pub command: Command,
10}
11
12#[derive(Debug, Subcommand)]
13pub enum Command {
15 Sync,
16 Add {
17 #[arg(required = true)]
18 paths: Vec<String>,
19 },
20 Remove {
21 #[arg(required = true)]
22 ids: Vec<String>,
23 },
24 List,
25 Clean,
26 Info {
28 #[arg(required = true)]
29 id: String,
30 },
31 Search(SearchArgs),
33 TagInfo(TagInfoArgs),
35 UserSettings(UserSettingsArgs),
37 UserCollections(UserCollectionsArgs),
39}
40
41#[derive(Debug, Args)]
42#[clap(group(
43 ArgGroup::new("search_method")
44 .required(true)
45 .multiple(false)
46 .args(&["query", "colors"]),
47 ))]
48pub struct SearchArgs {
49 #[clap(short = 'd', long, verbatim_doc_comment, help_heading = "DOWNLOAD")]
51 pub download: bool,
52
53 #[clap(short = 'q',
66 long,
67 verbatim_doc_comment,
68 help_heading = Some("SEARCH"),
69 value_parser = clap::value_parser!(SearchQuery),
71 )]
72 query: Option<SearchQuery>,
73
74 #[clap(short = 'c',
79 long,
80 verbatim_doc_comment,
81 help_heading = Some("SEARCH PREFERENCES"),
82 value_parser = ["100", "101", "110", "111"],
84 )]
85 categories: Option<String>,
86
87 #[clap(short = 'p',
93 long,
94 verbatim_doc_comment,
95 help_heading = Some("SEARCH PREFERENCES"),
96 value_parser = ["100", "101", "110", "111"],
98 )]
99 purity: Option<String>,
100
101 #[clap(short = 's',
104 long,
105 verbatim_doc_comment,
106 help_heading = Some("SORTING PREFERENCES"),
107 ignore_case = true,
108 value_parser = ["DATE_ADDED", "RELEVANCE", "RANDOM", "VIEWS", "FAVORITES", "TOPLIST"],
110 )]
111 sorting: Option<String>,
112
113 #[clap(short = 'o',
116 long,
117 verbatim_doc_comment,
118 help_heading = Some("SORTING PREFERENCES"),
119 ignore_case = true,
120 value_parser = ["ASC", "DESC"],
122 )]
123 order: Option<String>,
124
125 #[clap(short = 't',
129 long,
130 verbatim_doc_comment,
131 help_heading = Some("SORTING PREFERENCES"),
132 ignore_case = true,
133 value_parser = ["1D", "3D", "1W", "1M", "3M", "6M", "1Y"],
135 )]
136 toprange: Option<String>,
137
138 #[clap(short = 'a',
143 long,
144 verbatim_doc_comment,
145 help_heading = Some("WALLPAPER PREFERENCES"),
146 )]
148 atleast: Option<String>,
149
150 #[clap(short = 'r',
155 long,
156 verbatim_doc_comment,
157 help_heading = Some("WALLPAPER PREFERENCES"),
158 )]
160 resolutions: Option<String>,
161
162 #[clap(short = 'R',
169 long,
170 verbatim_doc_comment,
171 help_heading = Some("WALLPAPER PREFERENCES"),
172 )]
174 ratios: Option<String>,
175
176 #[clap(short = 'C',
182 long,
183 verbatim_doc_comment,
184 help_heading = Some("SEARCH"),
185 value_parser = valid_color
189 )]
190 colors: Option<String>,
191
192 #[clap(short = 'P',
197 long,
198 verbatim_doc_comment,
199 help_heading = Some("SEARCH PREFERENCES"),
200 value_parser = clap::value_parser!(u32).range(1..),
202 )]
203 page: Option<u32>,
204
205 #[clap(long,
210 verbatim_doc_comment,
211 help_heading = Some("SEARCH PREFERENCES"),
212 value_parser = clap::value_parser!(Seed),
214 )]
215 seed: Option<Seed>,
216}
217
218fn valid_color(s: &str) -> Result<String, String> {
219 let s = if s.starts_with("#") { &s[1..] } else { s };
220
221 let valid_hex = s.chars().into_iter().all(|c| c.is_digit(16));
222
223 if valid_hex && s.len() == 6 {
224 return Ok(String::from(s));
225 } else {
226 return Err(String::from(format!("{s} is not a valid hex color")));
227 }
228}
229
230fn valid_wallpaper_id(s: &str) -> Result<String, String> {
231 let valid_format = s
232 .chars()
233 .into_iter()
234 .all(|c| c.is_ascii_digit() || c.is_ascii_alphabetic());
235
236 if valid_format && s.len() == 6 {
237 return Ok(String::from(s));
238 } else {
239 return Err(String::from(format!("{s} is not a valid wallpaper id")));
240 }
241}
242
243#[derive(Debug, Args)]
244pub struct TagInfoArgs {
245 pub id: i32,
247}
248
249#[derive(Debug, Args)]
250pub struct UserSettingsArgs;
251
252#[derive(Debug, Args)]
253pub struct UserCollectionsArgs {
254 username: Option<String>,
259}
260
261#[derive(Debug, Default, Clone)]
262pub struct SearchQuery {
263 tags: Option<Vec<String>>,
264 username: Option<String>,
265 id: Option<String>, filetype: Option<String>, like: Option<String>,
268}
269
270impl FromStr for SearchQuery {
271 type Err = String;
272
273 fn from_str(s: &str) -> Result<Self, Self::Err> {
274 let mut q = Self::default();
275 let mut tags = Vec::<String>::default();
276
277 for token in s.split(" ") {
278 let t: Vec<&str> = token.split(":").collect();
280 if t.len() == 2 {
281 let key = t[0];
282 let value = t[1];
283
284 match key {
285 "id" => {
286 let mut q = Self::default();
290 q.id = Some(String::from(value));
291 return Ok(q);
292 }
293 "type" => {
294 if value == "png" || value == "jpg" {
295 q.filetype = Some(String::from(value));
296 } else {
297 return Err(String::from("Invalid file type - only accept png or jpg"));
298 }
299 }
300 "like" => {
301 q.like = Some(String::from(value));
305 }
306 _ => {
307 return Err(String::from(format!("{key}:{value} is not a valid query")));
308 }
309 }
310
311 continue;
312 }
313
314 if token.starts_with("@") {
316 q.username = Some(String::from(&token[1..]));
317 continue;
318 }
319
320 tags.push(token.to_string());
322 }
323
324 if !tags.is_empty() {
325 q.tags = Some(tags)
326 }
327
328 return Ok(q);
329 }
330}
331
332#[derive(Debug, Default, Clone)]
333pub struct Seed(String);
334
335impl FromStr for Seed {
336 type Err = String;
337
338 fn from_str(s: &str) -> Result<Self, Self::Err> {
339 if s.len() == 6
340 && s.chars()
341 .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit())
342 {
343 return Ok(Seed(String::from(s)));
344 } else {
345 return Err(format!("{s} is an invalid seed"));
346 }
347 }
348}
349
350impl Url for SearchArgs {
351 fn to_url(&self, base_url: &str) -> String {
352 let mut params = Vec::<String>::new();
353
354 if let Some(colors) = &self.colors {
356 params.push(format!("colors={colors}"))
357 } else {
358 if let Some(q) = &self.query {
359 if let Some(id) = &q.id {
360 params.push(format!("q=id:{id}"));
361 } else {
362 let mut query = Vec::<String>::new();
363
364 if let Some(tags) = &q.tags {
365 query.push(tags.join(" "));
366 }
367 if let Some(username) = &q.username {
368 query.push(format!("@{username}"));
369 }
370 if let Some(ft) = &q.filetype {
371 query.push(format!("type:{ft}"));
372 }
373 if let Some(w) = &q.like {
374 query.push(format!("like:{w}"));
375 }
376
377 params.push(format!("q={}", query.join(" ")));
378 }
379 }
380 }
381
382 if let Some(categories) = &self.categories {
384 params.push(format!("categories={}", categories));
385 }
386 if let Some(purity) = &self.purity {
387 params.push(format!("purity={}", purity));
388 }
389 if let Some(page) = self.page {
390 params.push(format!("page={}", page));
391 }
392 if let Some(seed) = &self.seed {
393 params.push(format!("seed={}", seed.0));
394 }
395
396 if let Some(order) = &self.order {
398 params.push(format!("order={}", order.to_ascii_lowercase()));
399 }
400 if let Some(sorting) = &self.sorting {
401 params.push(format!("sorting={}", sorting.to_ascii_lowercase()));
402 }
403 if let Some(toprange) = &self.toprange {
404 params.push(format!("topRange={}", toprange.to_ascii_lowercase()));
405 }
406
407 if let Some(atleast) = &self.atleast {
409 params.push(format!("atleast={}", atleast.to_ascii_lowercase()));
410 }
411 if let Some(resolutions) = &self.resolutions {
412 params.push(format!("resolutions={}", resolutions.to_ascii_lowercase()));
413 }
414 if let Some(ratios) = &self.ratios {
415 params.push(format!("ratios={}", ratios.to_ascii_lowercase()));
416 }
417
418 return format!("{base_url}/search?{}", params.join("&"));
419 }
420}
421
422impl Url for TagInfoArgs {
423 fn to_url(&self, base_url: &str) -> String {
424 return format!("{base_url}/tag/{}", self.id);
425 }
426}
427
428impl Url for UserSettingsArgs {
429 fn to_url(&self, base_url: &str) -> String {
430 return format!("{base_url}/settings");
431 }
432}
433
434impl Url for UserCollectionsArgs {
435 fn to_url(&self, base_url: &str) -> String {
436 match &self.username {
437 Some(username) => {
438 return format!("{base_url}/collections/{username}");
439 }
440 None => {
441 return format!("{base_url}/collections");
442 }
443 }
444 }
445}