fetch functions complete

This commit is contained in:
Vomitblood 2024-04-02 02:27:09 +08:00
parent 0df646844a
commit a4f32ff5b1
10 changed files with 1441 additions and 375 deletions

1014
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,12 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
crossterm = "0.27.0"
# clap = "2.0.0"
rand = { version = "0.8.4", features = ["small_rng"] }
serde_json = "1.0"
rust-embed = "8.3.0"
ctrlc = "3.4.4" ctrlc = "3.4.4"
dirs = "5.0.1"
image = "0.25.1"
rand = { version = "0.8.4", features = ["small_rng"] }
reqwest = { version = "0.11", features = ["json", "blocking"] } reqwest = { version = "0.11", features = ["json", "blocking"] }
zip = "0.5" rust-embed = "8.3.0"
serde_json = "1.0.115"
zip = "0.6.6"

View file

@ -9,7 +9,7 @@ struct Args {
name: String, name: String,
// big // big
/// Show a larger version of the sprite /// Show a bigger version of the sprite
#[arg(short, long, default_value_t = false)] #[arg(short, long, default_value_t = false)]
big: bool, big: bool,

View file

@ -1,90 +0,0 @@
// this fetches the sprites and downloads to the user's local machine
// https://github.com/Vomitblood/pokesprite/archive/refs/heads/master.zip
// TODO: pass in url as argument and use it
// for now, we use this as default
const TARGET_URL: &str = "https://github.com/Vomitblood/pokesprite/archive/refs/heads/master.zip";
const WORKING_DIRECTORY: &str = "/tmp/rustmon/";
fn main() {
// // create a working directory for the program to use
// match create_working_directory() {
// Ok(_) => (),
// Err(e) => eprintln!("Error creating working directory: {}", e),
// }
// match download_colorscripts_archive(TARGET_URL) {
// Ok(_) => (),
// Err(e) => eprintln!("Error downloading file: {}", e),
// }
// TODO: extract here as default unless specified in flags
extract_colorscripts_archive(std::path::Path::new(WORKING_DIRECTORY)).unwrap();
}
fn create_working_directory() -> std::io::Result<()> {
println!("Creating working directory at {}...", WORKING_DIRECTORY);
std::fs::create_dir(WORKING_DIRECTORY)?;
return Ok(());
}
fn download_colorscripts_archive(target_url: &str) -> Result<(), Box<dyn std::error::Error>> {
println!("Fetching colorscripts archive...");
let response = reqwest::blocking::get(target_url)?;
let mut dest = std::fs::File::create(format!("{}colorscripts.zip", WORKING_DIRECTORY))?;
let response_body = response.error_for_status()?.bytes()?;
std::io::copy(&mut response_body.as_ref(), &mut dest)?;
println!("Downloaded pokesprite.zip");
return Ok(());
}
fn extract_colorscripts_archive(extract_location: &std::path::Path) -> zip::result::ZipResult<()> {
// let archive_file = std::fs::File::open(&archive_path)?;
let archive_file = std::fs::File::open(std::path::Path::new(
format!("{}colorscripts.zip", WORKING_DIRECTORY).as_str(),
))?;
let mut archive = zip::read::ZipArchive::new(std::io::BufReader::new(archive_file))?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let file_path = std::path::Path::new(file.name());
let parent_dir = file_path
.parent()
.and_then(std::path::Path::file_name)
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("");
if (file
.name()
.starts_with("pokesprite-master/pokemon-gen8/regular/")
&& parent_dir == "regular")
|| (file
.name()
.starts_with("pokesprite-master/pokemon-gen8/shiny/")
&& parent_dir == "shiny")
{
let file_name = file_path
.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("");
let outpath = extract_location.join(parent_dir).join(file_name);
if !file.name().ends_with('/') {
if let Some(p) = outpath.parent() {
if !p.exists() {
std::fs::create_dir_all(&p)?;
}
}
let mut outfile = std::fs::File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
}
}
}
Ok(())
}

4
src/constants.rs Normal file
View file

@ -0,0 +1,4 @@
pub const TARGET_URL: &str =
"https://github.com/Vomitblood/pokesprite/archive/refs/heads/master.zip";
pub const WORKING_DIRECTORY: &str = "/tmp/rustmon/";
pub const EXTRACT_DESTINATION: &str = "~/.local/share/rustmon/";

349
src/fetch.rs Normal file
View file

@ -0,0 +1,349 @@
use image::GenericImageView;
use std::io::Write;
pub fn fetch(extract_destination: &std::path::Path, silent: bool) {
// prep working directory
match create_working_directory() {
Ok(_) => (),
Err(e) => eprintln!("Error creating working directory: {}", e),
};
// download colorscripts archive
match download_colorscripts_archive(crate::constants::TARGET_URL) {
Ok(_) => (),
Err(e) => eprintln!("Error downloading colorscripts archive: {}", e),
};
// extract colorscripts archive
// now we have the raw images
match extract_colorscripts_archive() {
Ok(_) => (),
Err(e) => eprintln!("Error extracting colorscripts archive: {}", e),
};
// crop images to content
match crop_all_images_in_directory() {
Ok(_) => (),
Err(e) => eprintln!("Error cropping images: {}", e),
};
// convert images to unicode, both small and big
match convert_images_to_ascii(extract_destination, silent) {
Ok(_) => (),
Err(e) => eprintln!("Error converting images to ASCII: {}", e),
};
// cleanup
match cleanup() {
Ok(_) => (),
Err(e) => eprintln!("Error cleaning up: {}", e),
};
}
fn create_working_directory() -> std::io::Result<()> {
println!(
"Creating working directory at {}...",
&crate::constants::WORKING_DIRECTORY
);
// create intermediate directories also
std::fs::create_dir(&crate::constants::WORKING_DIRECTORY)?;
println!("Created working directory");
return Ok(());
}
fn download_colorscripts_archive(target_url: &str) -> Result<(), Box<dyn std::error::Error>> {
println!("Fetching colorscripts archive...");
let response = reqwest::blocking::get(target_url)?;
let mut dest = std::fs::File::create(format!(
"{}pokesprite.zip",
&crate::constants::WORKING_DIRECTORY
))?;
let response_body = response.error_for_status()?.bytes()?;
std::io::copy(&mut response_body.as_ref(), &mut dest)?;
println!("Downloaded colorscripts archive");
return Ok(());
}
fn extract_colorscripts_archive() -> zip::result::ZipResult<()> {
println!("Extracting colorscripts archive...");
let archive_file = std::fs::File::open(std::path::Path::new(
format!("{}pokesprite.zip", &crate::constants::WORKING_DIRECTORY).as_str(),
))?;
let mut archive = zip::read::ZipArchive::new(std::io::BufReader::new(archive_file))?;
// iterate over every single file in the archive
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let file_path = std::path::Path::new(file.name());
let parent_dir = file_path
.parent()
.and_then(std::path::Path::file_name)
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("");
// check if the file is a in the correct directory that is NOT a directory
if (file
.name()
.starts_with("pokesprite-master/pokemon-gen8/regular/")
&& parent_dir == "regular")
|| (file
.name()
.starts_with("pokesprite-master/pokemon-gen8/shiny/")
&& parent_dir == "shiny")
{
let file_name = file_path
.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap();
let outpath = std::path::Path::new(&crate::constants::WORKING_DIRECTORY)
.join("raw_images")
.join(parent_dir)
.join(file_name);
if !file.name().ends_with('/') {
if let Some(p) = outpath.parent() {
if !p.exists() {
std::fs::create_dir_all(&p)?;
}
}
let mut outfile = std::fs::File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
};
};
}
println!("Extracted colorscripts archive");
return Ok(());
}
fn crop_all_images_in_directory() -> std::io::Result<()> {
println!("Cropping images...");
// make sure the cropped_images directory exists
std::fs::create_dir_all(
std::path::Path::new(crate::constants::WORKING_DIRECTORY).join("cropped_images"),
)?;
// do for both regular and shiny subdirectories
for subdirectory in ["regular", "shiny"].iter() {
let input_subdirectory_path = std::path::Path::new(crate::constants::WORKING_DIRECTORY)
.join("raw_images")
.join(subdirectory);
let output_subdirectory_path = std::path::Path::new(crate::constants::WORKING_DIRECTORY)
.join("cropped_images")
.join(subdirectory);
std::fs::create_dir_all(&output_subdirectory_path)?;
for entry in std::fs::read_dir(&input_subdirectory_path)? {
let entry = entry?;
let path = entry.path();
let output_path = output_subdirectory_path.join(path.file_name().unwrap());
crop_to_content(&path, &output_path).unwrap();
}
}
println!("Cropped images");
return Ok(());
}
fn crop_to_content(
input_path: &std::path::Path,
output_path: &std::path::Path,
) -> image::ImageResult<image::DynamicImage> {
// load the image
let img = image::open(input_path)?;
let (width, height) = img.dimensions();
let mut min_x = width;
let mut min_y = height;
let mut max_x = 0;
let mut max_y = 0;
for y in 0..height {
for x in 0..width {
let pixel = img.get_pixel(x, y);
if pixel[3] != 0 {
// if pixel is not transparent
if x < min_x {
min_x = x;
}
if y < min_y {
min_y = y;
}
if x > max_x {
max_x = x;
}
if y > max_y {
max_y = y;
}
}
}
}
let cropped_width = max_x - min_x + 1;
let cropped_height = max_y - min_y + 1;
let mut cropped_img: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> =
image::ImageBuffer::new(cropped_width, cropped_height);
for y in 0..cropped_height {
for x in 0..cropped_width {
let pixel = img.get_pixel(x + min_x, y + min_y);
cropped_img.put_pixel(x, y, pixel);
}
}
let cropped_img = image::DynamicImage::ImageRgba8(cropped_img);
// write the cropped image
cropped_img.save(output_path)?;
return Ok(cropped_img);
}
fn convert_images_to_ascii(
output_directory_path: &std::path::Path,
silent: bool,
) -> std::io::Result<()> {
println!("Extract destination: {:?}", output_directory_path);
println!("Converting images to ASCII...");
std::fs::create_dir_all(output_directory_path)?;
for size in ["small", "big"].iter() {
for subdirectory in ["regular", "shiny"].iter() {
let input_subdirectory_path =
std::path::PathBuf::from(crate::constants::WORKING_DIRECTORY)
.join("cropped_images")
.join(subdirectory);
let output_subdirectory_path = output_directory_path
.join("colorscripts")
.join(size)
.join(subdirectory);
std::fs::create_dir_all(&output_subdirectory_path)?;
for entry in std::fs::read_dir(&input_subdirectory_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let img = image::open(&path).unwrap();
let ascii_art = if *size == "small" {
convert_image_to_unicode_small(&img)
} else {
convert_image_to_unicode_big(&img)
};
// print for fun
if silent == false {
println!("{}", ascii_art);
};
let output_path = output_subdirectory_path.join(path.file_stem().unwrap());
let mut file = std::fs::File::create(output_path)?;
file.write_all(ascii_art.as_bytes())?;
}
}
}
}
println!("Converted images to ASCII");
return Ok(());
}
fn convert_image_to_unicode_small(img: &image::DynamicImage) -> String {
let mut unicode_sprite = String::new();
let (width, height) = img.dimensions();
for y in (0..height).step_by(2) {
for x in 0..width {
let upper_pixel = img.get_pixel(x, y);
let lower_pixel = if y + 1 < height {
img.get_pixel(x, y + 1)
} else {
upper_pixel // Fallback to upper pixel if there's no lower pixel.
};
if upper_pixel[3] == 0 && lower_pixel[3] == 0 {
unicode_sprite.push(' ');
} else if upper_pixel[3] == 0 {
unicode_sprite.push_str(&get_color_escape_code(lower_pixel, false));
unicode_sprite.push('▄');
} else if lower_pixel[3] == 0 {
unicode_sprite.push_str(&get_color_escape_code(upper_pixel, false));
unicode_sprite.push('▀');
} else {
unicode_sprite.push_str(&get_color_escape_code(upper_pixel, false));
unicode_sprite.push_str(&get_color_escape_code(lower_pixel, true));
unicode_sprite.push('▀');
}
unicode_sprite.push_str("\x1b[0m"); // Reset ANSI code after each character
}
unicode_sprite.push('\n'); // New line for each row, plus reset might be added here too if colors extend beyond.
}
return unicode_sprite;
}
fn convert_image_to_unicode_big(img: &image::DynamicImage) -> String {
let mut unicode_sprite = String::new();
let (width, height) = img.dimensions();
for y in 0..height {
for x in 0..width {
let pixel = img.get_pixel(x, y);
if pixel[3] == 0 {
unicode_sprite.push_str(" ");
} else {
unicode_sprite.push_str(&get_color_escape_code(pixel, false));
unicode_sprite.push_str("██");
}
}
unicode_sprite.push('\n');
}
return unicode_sprite;
}
fn get_color_escape_code(pixel: image::Rgba<u8>, background: bool) -> String {
if pixel[3] == 0 {
return format!("{}", crossterm::style::ResetColor);
}
let color = crossterm::style::Color::Rgb {
r: pixel[0],
g: pixel[1],
b: pixel[2],
};
if background {
format!("{}", crossterm::style::SetBackgroundColor(color))
} else {
format!("{}", crossterm::style::SetForegroundColor(color))
}
}
fn cleanup() -> std::io::Result<()> {
println!("Cleaning up...");
std::fs::remove_dir_all(std::path::Path::new(crate::constants::WORKING_DIRECTORY))?;
println!("Cleaned up");
return Ok(());
}

2
src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod constants;
pub mod fetch;

View file

@ -1,279 +1,99 @@
use rand::Rng; use clap::Parser;
// set global constants /*
#[derive(rust_embed::RustEmbed)] # Arguments
#[folder = "colorscripts/"]
struct ColorScriptsDir;
const POKEMON_JSON: &str = std::include_str!("../pokemon.json"); ## Fetch
- `fetch` - Fetch the latest colorscripts from the repository
- `silent` - Don't print colorscripts to the console when generating
- `extract_destination` - eXtract the colorscripts archive to a specified location
const REGULAR_SUBDIR: &str = "regular"; ## Print
const SHINY_SUBDIR: &str = "shiny"; - `name` - Select pokemon by name
- `big` - Show a bigger version of the sprite
- `list` - Show a list of all pokemon names
- `no-title` - Do not display pokemon name
- `shiny` - Show the shiny version of the sprite
*/
const LARGE_SUBDIR: &str = "large"; /// Pokemon Colorscripts written in Rust
const SMALL_SUBDIR: &str = "small"; #[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
// fetch
/// Fetch the latest colorscripts from the repository
#[arg(short, long, default_value_t = false)]
fetch: bool,
const SHINY_RATE: f64 = 1.0 / 128.0; // silent
/// Don't print colorscripts to the console when generating
#[arg(long = "silent", default_value_t = false)]
silent: bool,
const GENERATIONS: [(&str, (u32, u32)); 8] = [ // extract destination
("1", (1, 151)), /// eXtract the colorscripts archive to a specified location
("2", (152, 251)), #[arg(short = 'x', long = "extract", default_value_t = String::from(""))]
("3", (252, 386)), extract_destination: String,
("4", (387, 493)), /*
("5", (494, 649)), // big
("6", (650, 721)), /// Show a bigger version of the sprite
("7", (722, 809)), #[arg(short, long, default_value_t = false)]
("8", (810, 898)), big: bool,
];
fn print_file(filepath: &str) -> std::io::Result<()> { // list
return ColorScriptsDir::get(filepath) /// Show a list of all pokemon names
.map(|file| { #[arg(short, long, default_value_t = false)]
let content = std::str::from_utf8(file.data.as_ref()).unwrap(); list: bool,
println!("{}", content);
})
.ok_or(std::io::Error::new(
std::io::ErrorKind::NotFound,
"File not found",
));
}
fn list_pokemon_names() { // name
let pokemon_json: serde_json::Value = serde_json::from_str(POKEMON_JSON).unwrap(); /// Select pokemon by name
#[arg(short = 'a', long, default_value_t = String::from(""))]
name: String,
let mut count = 0; // no-title
// NOTE: clap will convert the kebab-case to snake_case
// very smart!
// ...but very annoying for beginners
/// Do not display pokemon name
#[arg(long, default_value_t = false)]
no_title: bool,
if let serde_json::Value::Array(array) = pokemon_json { // shiny
for pokemon in array { /// Show the shiny version of the sprite
if let Some(name) = pokemon.get("name") { #[arg(short, long, default_value_t = false)]
if let serde_json::Value::String(name_str) = name {
println!("{}", name_str);
count += 1;
}
}
}
}
println!("-------------------");
println!("Total: {} Pokémons", count);
println!("Use the --name flag to view a specific Pokémon");
println!("Tip: Use `grep` to search for a specific Pokémon");
}
fn show_pokemon_by_name(
name: &str,
show_title: bool,
shiny: bool, shiny: bool,
is_large: bool, */
form: Option<&str>,
) -> std::io::Result<()> {
// set variables
let color_subdir = if shiny { SHINY_SUBDIR } else { REGULAR_SUBDIR };
let size_subdir = if is_large { LARGE_SUBDIR } else { SMALL_SUBDIR };
let pokemon_json: serde_json::Value = serde_json::from_str(POKEMON_JSON)?;
let pokemon_names: Vec<&str> = pokemon_json
.as_array()
.unwrap()
.iter()
.map(|pokemon| pokemon["name"].as_str().unwrap())
.collect();
if !pokemon_names.contains(&name) {
println!("Invalid pokemon {}", name);
std::process::exit(1);
}
let mut name = name.to_string();
if let Some(form) = form {
let forms: Vec<&str> = pokemon_json
.as_array()
.unwrap()
.iter()
.filter(|pokemon| pokemon["name"].as_str().unwrap() == name)
.flat_map(|pokemon| pokemon["forms"].as_array().unwrap().iter())
.map(|form| form.as_str().unwrap())
.collect();
let alternate_forms: Vec<&str> =
forms.iter().filter(|&f| *f != "regular").cloned().collect();
if alternate_forms.contains(&form) {
name.push_str(&format!("-{}", form));
} else {
println!("Invalid form '{}' for pokemon {}", form, name);
if alternate_forms.is_empty() {
println!("No alternate forms available for {}", name);
} else {
println!("Available alternate forms are");
for form in alternate_forms {
println!("- {}", form);
}
}
std::process::exit(1);
}
}
if show_title {
if shiny {
println!("{} (shiny)", name);
} else {
println!("{}", name);
}
}
// Construct the embedded file path
let file_path = format!("{}/{}/{}", size_subdir, color_subdir, name);
// Use the adjusted function to print file contents from embedded resources
print_file(&file_path)?;
return Ok(());
}
fn show_random_pokemon(
generations: &str,
show_title: bool,
shiny: bool,
is_large: bool,
) -> std::io::Result<()> {
let mut rng = rand::thread_rng();
let start_gen = if generations.is_empty() {
"1"
} else if generations.contains(",") {
let gens: Vec<&str> = generations.split(",").collect();
let gen = gens[rng.gen_range(0..gens.len())];
gen
} else if generations.contains("-") {
let gens: Vec<&str> = generations.split("-").collect();
gens[0]
} else {
generations
};
let pokemon_json: serde_json::Value = serde_json::from_str(POKEMON_JSON)?;
let pokemon: Vec<String> = pokemon_json
.as_array()
.unwrap()
.iter()
.map(|p| p["name"].as_str().unwrap().to_string())
.collect();
let generations_map: std::collections::HashMap<_, _> = GENERATIONS.iter().cloned().collect();
if let Some((start_idx, end_idx)) = generations_map.get(start_gen) {
let random_idx = rng.gen_range(*start_idx..=*end_idx);
let random_pokemon = &pokemon[random_idx as usize - 1];
let shiny = if !shiny {
rng.gen::<f64>() <= SHINY_RATE
} else {
shiny
};
show_pokemon_by_name(random_pokemon, show_title, shiny, is_large, None)?;
} else {
println!("Invalid generation '{}'", generations);
std::process::exit(1);
}
return Ok(());
}
#[cfg(target_os = "windows")]
fn pause() {
use std::io::{self, Read, Write};
let mut stdout = io::stdout();
let mut stdin = io::stdin();
stdout.write_all(b"Press any key to continue...").unwrap();
stdout.flush().unwrap();
stdin.read(&mut [0]).unwrap();
}
#[cfg(not(target_os = "windows"))]
fn pause() {
// do literally nothing
} }
fn main() { fn main() {
let matches = clap::App::new("rustmon") let args = argument_validation();
.about("CLI utility to print out unicode image of a pokemon in your shell")
.arg(
clap::Arg::with_name("list")
.short("l")
.long("list")
.help("Print list of all pokemon"),
)
.arg(
clap::Arg::with_name("name")
.short("n")
.long("name")
.value_name("POKEMON NAME")
.help("Select pokemon by name. Generally spelled like in the games."),
)
.arg(
clap::Arg::with_name("form")
.short("f")
.long("form")
.value_name("FORM")
.help("Show an alternate form of a pokemon"),
)
.arg(
clap::Arg::with_name("no-title")
.long("no-title")
.help("Do not display pokemon name"),
)
.arg(
clap::Arg::with_name("shiny")
.short("s")
.long("shiny")
.help("Show the shiny version of the pokemon instead"),
)
.arg(
clap::Arg::with_name("big")
.short("b")
.long("big")
.help("Show a larger version of the sprite"),
)
.arg(
clap::Arg::with_name("random")
.short("r")
.long("random")
.value_name("GENERATION")
.help("Show a random pokemon. This flag can optionally be followed by a generation number or range (1-8) to show random pokemon from a specific generation or range of generations. The generations can be provided as a continuous range (eg. 1-3) or as a list of generations (1,3,6)"),
)
.after_help("P.S. Use the minimon command for a minimalistic version of this tool")
.get_matches();
if matches.is_present("list") { if args.fetch == true {
list_pokemon_names(); // get data directory
} else if matches.is_present("name") { let data_directory = match dirs::data_dir() {
let name = matches.value_of("name").unwrap(); Some(dir) => dir.join("rustmon"),
let no_title = matches.is_present("no-title"); None => {
let shiny = matches.is_present("shiny"); println!("Data directory not found");
let big = matches.is_present("big");
let form = matches.value_of("form");
show_pokemon_by_name(name, !no_title, shiny, big, form).unwrap();
} else if matches.is_present("random") {
let random = matches.value_of("random").unwrap_or("");
let no_title = matches.is_present("no-title");
let shiny = matches.is_present("shiny");
let big = matches.is_present("big");
if matches.is_present("form") {
println!("--form flag unexpected with --random");
std::process::exit(1); std::process::exit(1);
} }
show_random_pokemon(random, !no_title, shiny, big).unwrap(); };
} else {
// show random pokemon by default with support for other flags
let no_title = matches.is_present("no-title");
let shiny = matches.is_present("shiny");
let big = matches.is_present("big");
show_random_pokemon("", !no_title, shiny, big).unwrap();
}
// pause the program before exiting only for windows // decicde whether to use the default data directory or the one specified by the user
pause(); // if the user specifies a directory, use that
let extract_destination = if args.extract_destination.is_empty() {
data_directory
} else {
std::path::PathBuf::from(&args.extract_destination)
};
rustmon::fetch::fetch(&extract_destination, args.silent);
} else {
println!("print deez nuts");
}
}
fn argument_validation() -> Args {
let args = Args::parse();
return args;
} }