1mod config;
2mod error;
3mod pokemon;
4
5use config::Config;
6use error::Error;
7use pokemon::*;
8
9use clap::{Args, Parser, Subcommand};
10use rand::seq::SliceRandom;
11use rand::Rng;
12use rust_embed::RustEmbed;
13
14use std::str;
15
16/// Print pokemon sprites in your terminal
17#[derive(Parser, Debug)]
18#[clap(author, version, about, long_about = None)]
19struct Cli {
20 #[clap(subcommand)]
21 command: Commands,
22}
23
24#[derive(Debug, Subcommand)]
25enum Commands {
26 /// Print list of all pokemon
27 List,
28 /// Select pokemon by name. Generally spelled like in the games.
29 /// A few exceptions are nidoran-f, nidoran-m, mr-mime, farfetchd,
30 /// flabebe type-null etc. Perhaps grep the output of list if in doubt.
31 Name(Name),
32 /// Show a random pokemon. This command can optionally be followed by a
33 /// generation number or range (1-9) to show random pokemon from a specific
34 /// generation or range of generations. The generations can be provided as
35 /// a continuous range (eg. 1-3) or as a list of generations (1,3,6)
36 Random(Random),
37}
38
39#[derive(Debug, Args)]
40struct Name {
41 /// Name of the pokemon to show
42 name: String,
43
44 /// Show an alternative form of the pokemon. For example: mega, mega-x,
45 /// mega-y, gmax, alola, hisui, galar, paldea
46 #[clap(short, long, default_value = "regular")]
47 form: String,
48
49 /// Show the shiny version of the pokemon instead
50 #[clap(short, long)]
51 shiny: bool,
52
53 /// Print pokedex entry (if it exists)
54 #[clap(short, long)]
55 info: bool,
56
57 /// Do not display pokemon name
58 #[clap(long)]
59 no_title: bool,
60
61 /// Left padding
62 #[clap(long, default_value = "0")]
63 padding_left: usize,
64}
65
66#[derive(Debug, Args)]
67struct Random {
68 /// Generation number, range (1-9), or list of generations (1,3,6)
69 #[clap(default_value = "1-9")]
70 generations: String,
71
72 /// Print pokedex entry (if it exists)
73 #[clap(short, long)]
74 info: bool,
75
76 /// Show the shiny version of the pokemon instead
77 #[clap(short, long)]
78 shiny: bool,
79
80 /// Do not display pokemon name
81 #[clap(long)]
82 no_title: bool,
83
84 /// Do not show mega pokemon
85 #[clap(long)]
86 no_mega: bool,
87
88 /// Do not show gigantamax pokemon
89 #[clap(long)]
90 no_gmax: bool,
91
92 /// Do not show regional pokemon
93 #[clap(long)]
94 no_regional: bool,
95
96 /// Left padding
97 #[clap(long, default_value = "0")]
98 padding_left: usize,
99}
100
101#[derive(RustEmbed)]
102#[folder = "assets/"]
103struct Asset;
104
105fn list_pokemon_names(pokemon_db: Vec<Pokemon>) {
106 pokemon_db.iter().for_each(|p| println!("{}", p.slug));
107}
108
109fn show_pokemon_by_name(
110 name: &Name,
111 pokemon_db: Vec<Pokemon>,
112 config: &Config,
113) -> Result<(), Error> {
114 match pokemon_db.iter().find(|p| p.slug == name.name) {
115 Some(pokemon) => {
116 let slug = if name.form == "regular" {
117 name.name.clone()
118 } else {
119 format!("{}-{}", name.name, name.form)
120 };
121 let art_path = if name.shiny {
122 format!("colorscripts/shiny/{}", slug)
123 } else {
124 format!("colorscripts/regular/{}", slug)
125 };
126 let art = Asset::get(&art_path)
127 .unwrap_or_else(|| panic!("Could not read pokemon art of '{}'", slug))
128 .data;
129 let art = str::from_utf8(&art).expect("Invalid UTF-8 in pokemon art");
130 if !name.no_title {
131 let pokemon_name = match pokemon.name.get(&config.language) {
132 Some(n) => n,
133 None => return Err(Error::InvalidLanguage(config.language.clone())),
134 };
135 print!("{: <1$}{pokemon_name}", "", name.padding_left);
136 match name.form.as_str() {
137 "regular" => println!(),
138 other => println!(" ({other})"),
139 }
140 }
141 if name.info {
142 if let Some(description) = pokemon.desc.get(&config.language) {
143 description
144 .lines()
145 .for_each(|line| println!("{: <1$}{line}", "", name.padding_left))
146 }
147 }
148 println!();
149 art.lines()
150 .for_each(|line| println!("{: <1$}{line}", "", name.padding_left))
151 }
152 None => {
153 return Err(Error::InvalidPokemon(name.name.clone()));
154 }
155 }
156 Ok(())
157}
158
159fn show_random_pokemon(
160 random: &Random,
161 pokemon_db: Vec<Pokemon>,
162 config: &Config,
163) -> Result<(), Error> {
164 let (start_gen, end_gen) = match random.generations.split_once('-') {
165 Some(gens) => gens,
166 None => {
167 let gen_list = random.generations.split(',').collect::<Vec<_>>();
168 let gen = gen_list.choose(&mut rand::thread_rng()).unwrap();
169 (*gen, *gen)
170 }
171 };
172
173 let start_gen = start_gen.parse::<u8>();
174 let end_gen = end_gen.parse::<u8>();
175 let (start_gen, end_gen) = match (start_gen, end_gen) {
176 (Ok(s), Ok(e)) => (s, e),
177 _ => return Err(Error::InvalidGeneration(random.generations.clone())),
178 };
179
180 // Filter by generation
181 let pokemon = pokemon_db
182 .iter()
183 .filter(|p| start_gen <= p.gen && end_gen >= p.gen)
184 .collect::<Vec<_>>();
185
186 let pokemon = match pokemon.choose(&mut rand::thread_rng()) {
187 Some(p) => Ok(p),
188 None => Err(Error::InvalidGeneration(random.generations.clone())),
189 }?;
190
191 let mut forms = vec!["regular".to_string()];
192 forms.extend_from_slice(&pokemon.forms);
193 // Optional filters
194 if random.no_mega {
195 forms.retain(|f| !["mega", "mega-x", "mega-y"].contains(&f.as_str()));
196 }
197 if random.no_gmax {
198 forms.retain(|f| f.as_str() != "gmax");
199 }
200 if random.no_regional {
201 forms.retain(|f| !["alola", "galar", "hisui", "paldea"].contains(&f.as_str()));
202 }
203 // forms will never be empty, so safe to unwrap
204 let form = forms.choose(&mut rand::thread_rng()).unwrap();
205 let shiny = rand::thread_rng().gen_bool(config.shiny_rate) || random.shiny;
206
207 show_pokemon_by_name(
208 &Name {
209 name: pokemon.slug.clone(),
210 form: form.to_string(),
211 shiny,
212 info: random.info,
213 no_title: random.no_title,
214 padding_left: random.padding_left,
215 },
216 pokemon_db,
217 config,
218 )
219}
220
221fn main() -> Result<(), Error> {
222 let config = Config::load()?;
223 let pokemon_db = Asset::get("pokemon.json").expect("Could not read pokemond db file");
224 let pokemon = load_pokemon(&pokemon_db)?;
225 let args = Cli::parse();
226 match args.command {
227 Commands::List => list_pokemon_names(pokemon),
228 Commands::Name(name) => show_pokemon_by_name(&name, pokemon, &config)?,
229 Commands::Random(random) => show_random_pokemon(&random, pokemon, &config)?,
230 }
231 Ok(())
232}
233