use std::collections::HashMap;
use std::env::args;
use std::fs::{self, create_dir_all};
use std::path::PathBuf;
use std::process::Command;
use std::str;
use dirs::data_dir;
use serde_json;
use toml;
mod repo;
fn get_share_dir() -> PathBuf {
let mut share = match data_dir() {
Some(p) => p,
_ => {
println!("Couldn't solve for dir, using current");
PathBuf::from("./")
}
};
share.push("font-catcher");
create_dir_all(&share).expect("Couldn't create direcotires!");
share
}
fn get_local_repos(repo_file: PathBuf) -> repo::Repositories {
let repo_data = fs::read_to_string(&repo_file).expect("Unable to read repositories file");
toml::from_str(&repo_data).expect("Failed to read repositories file")
}
fn get_repo_json(url: &str) -> String {
if cfg!(target_os = "windows") {
return "".to_string();
}
match str::from_utf8(
&Command::new("curl")
.arg(url)
.output()
.expect("Failed to execute process")
.stdout,
) {
Ok(v) => v.to_string(),
Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
}
}
fn update_repos(repos: repo::Repositories, repos_dir: &PathBuf) {
create_dir_all(repos_dir).expect("Couldn't create direcotires!");
for repo in repos.repo.iter() {
println!("Updating {}...", repo.name);
fs::write(
repos_dir.join(format!("{}{}", &repo.name, ".json")),
match &repo.key {
Some(key) => get_repo_json(&repo.url.replace("{API_KEY}", &key)),
_ => get_repo_json(&repo.url),
},
)
.expect("Unable to write repo files");
}
}
fn family_in_repo(repos_dir: &PathBuf, repo: &str, search_string: &str) -> Vec<String> {
let fonts_as_string =
fs::read_to_string(repos_dir.join(&repo)).expect("Couldn't find specified repository");
let mut results: Vec<String> = Vec::new();
let fonts: repo::FontsList =
serde_json::from_str(&fonts_as_string).expect("Repository file bad formatted");
for font in fonts.items.iter() {
if font.family.contains(&search_string) {
results.push(font.family.to_owned());
}
}
results
}
fn print_results(repo: &str, fonts: &Vec<String>) {
if fonts.len() > 0 {
for i in fonts.iter() {
println!("{}/{}", &repo, &i);
}
} else {
println!("No results found!");
}
}
fn search_fonts(repos_dir: &PathBuf, repo: Option<&str>, search_string: &str) {
match &repo {
Some(repo) => {
let results =
family_in_repo(&repos_dir, &format!("{}{}", &repo, ".json"), search_string);
print_results(&repo, &results);
}
_ => {
let files = fs::read_dir(&repos_dir).expect("Folder not available");
for repo in files {
let results = family_in_repo(
&repos_dir,
&repo.as_ref().unwrap().file_name().to_str().unwrap(),
search_string,
);
print_results(
&repo
.as_ref()
.unwrap()
.path()
.file_stem()
.unwrap()
.to_str()
.unwrap(),
&results,
);
}
}
};
}
fn download_file(output_file: &PathBuf, url: &str) -> Result<std::process::Output, std::io::Error> {
create_dir_all(output_file.parent().unwrap()).expect("Couldn't create direcotires!");
println!(
"Downloading to {} from {}...",
output_file.as_os_str().to_str().unwrap(),
url
);
Command::new("curl")
.arg("-L") .arg("-o")
.arg(output_file.as_os_str().to_str().unwrap())
.arg(url)
.output()
}
fn files_in_repo(repos_dir: &PathBuf, repo: &str, search_string: &str) -> HashMap<String, String> {
let fonts_as_string =
fs::read_to_string(repos_dir.join(&repo)).expect("Couldn't find specified repository");
let mut results: HashMap<String, String> = HashMap::new();
let fonts: repo::FontsList =
serde_json::from_str(&fonts_as_string).expect("Repository file bad formatted");
for font in fonts.items.iter() {
if font.family == search_string {
for (i, j) in font.files.iter() {
results.insert(i.to_string(), j.to_string());
}
}
}
results
}
fn download_fonts(
repos_dir: &PathBuf,
download_dir: &PathBuf,
repo: Option<&str>,
fonts: Vec<String>,
) {
match &repo {
Some(repo) => {
for i in fonts.iter() {
let results = files_in_repo(&repos_dir, &format!("{}{}", &repo, ".json"), i);
for (variant, url) in results {
let extension: &str = url
.split(".")
.collect::<Vec<&str>>()
.last()
.expect("File extension not found!");
download_file(
&download_dir.join(&format!("{}-{}.{}", i, &variant, extension)),
&url,
)
.expect(&format!("Failed to download font {}", i));
}
}
}
_ => {
let files = fs::read_dir(&repos_dir).expect("Folder not available");
for repo in files {
for i in fonts.iter() {
let results = files_in_repo(
&repos_dir,
&repo.as_ref().unwrap().file_name().to_str().unwrap(),
i,
);
for (variant, url) in results {
let extension: &str = url
.split(".")
.collect::<Vec<&str>>()
.last()
.expect("File extension not found!");
download_file(
&download_dir.join(&format!("{}-{}.{}", i, &variant, extension)),
&url,
)
.expect(&format!("Failed to download font {}", i));
}
}
}
}
};
}
fn get_local_fonts(fonts_dir: &PathBuf) -> HashMap<String, Vec<PathBuf>> {
let files = fs::read_dir(fonts_dir).expect("Folder no available");
let mut results: HashMap<String, Vec<PathBuf>> = HashMap::new();
for font in files {
match font
.as_ref()
.unwrap()
.path()
.file_stem()
.unwrap()
.to_string_lossy()
.split("-")
.collect::<Vec<&str>>()
.first()
{
Some(font_name) => {
let counter = results.entry(font_name.to_string()).or_insert(Vec::new());
counter.push(font.as_ref().unwrap().path());
}
None => {}
}
}
results
}
fn remove_fonts(fonts_dir: &PathBuf, font_names: Vec<String>) {
let local_fonts = get_local_fonts(fonts_dir);
for (local_font_name, paths) in local_fonts {
for font_name in font_names.iter() {
if font_name == &local_font_name {
for path in paths.iter() {
println!("Removing file {}", &path.as_os_str().to_str().unwrap());
fs::remove_file(&path).expect("Couldn't remove file");
}
}
}
}
}
fn main() {
let font_catcher_dir = get_share_dir();
let repos_dir = font_catcher_dir.join("repos");
let install_dir = data_dir().expect("Couldn't solve for dir").join("fonts");
let repos_file = font_catcher_dir.join("repos.conf");
let default_repos: repo::Repositories = repo::get_default_repos();
let local_repos: repo::Repositories = get_local_repos(repos_file);
let args: Vec<String> = args().collect();
println!("{:?}", args);
if (args.len() == 1) || args[1] == "--version" || args[1] == "-v" {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
println!(
"Copyright (C) {}, all rights reserved",
env!("CARGO_PKG_AUTHORS")
);
println!("This is free software. It is licensed for use, modification and");
println!("redistribution under the terms of the GNU Affero General Public License,");
println!("version 3. <https://www.gnu.org/licenses/agpl-3.0.en.html>");
println!("");
println!("{}", env!("CARGO_PKG_DESCRIPTION"));
} else if args[1] == "update-repos" {
update_repos(default_repos, &repos_dir);
update_repos(local_repos, &repos_dir);
} else if args.len() == 2 {
if args[1] == "install"
|| args[1] == "remove"
|| args[1] == "search"
|| args[1] == "download"
{
println!("`{}` receives at least one argument", args[1]);
} else {
println!("`{}` command not found", args[1]);
}
} else {
if args[1] == "install" {
if args[2] == "--repo" {
if args.len() > 4 {
download_fonts(&repos_dir, &install_dir, Some(&args[3]), args[4..].to_vec());
} else {
println!("Missing fonts to install");
}
} else {
download_fonts(&repos_dir, &install_dir, None, args[3..].to_vec());
}
} else if args[1] == "download" {
if args[3] == "--repo" {
if args.len() > 5 {
download_fonts(
&repos_dir,
&PathBuf::from(&args[2]),
Some(&args[4]),
args[5..].to_vec(),
);
} else {
println!("Missing fonts or output directory to download");
}
} else {
download_fonts(
&repos_dir,
&PathBuf::from(&args[2]),
None,
args[3..].to_vec(),
);
}
} else if args[1] == "search" {
if args[2] == "--repo" {
if args.len() > 4 {
search_fonts(&repos_dir, Some(&args[3]), &args[4]);
} else {
println!("Missing string to search");
}
} else {
search_fonts(&repos_dir, None, &args[2]);
}
} else if args[1] == "remove" {
remove_fonts(&install_dir, args[2..].to_vec());
}
}
}