pub mod cli;
pub mod langs;
pub mod lib;
use clap::Parser;
use colored::*;
use dialoguer::{theme::ColorfulTheme, Select};
use dirs::data_dir;
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::get;
use std::{
cmp::min,
fs::{create_dir, read_dir, read_to_string, remove_file, File},
io::{self, BufRead, Error, Write},
};
use cli::Cli;
use langs::{LOCALES, SUPPORTED_LANGS};
use lib::{edit_distance, insert_and_shift, yank};
fn main() {
std::process::exit(match run_app() {
Ok(_) => 0,
Err(error) => {
eprintln!("Error: {:?}", error);
1
}
});
}
fn run_app() -> std::result::Result<(), Error> {
#[cfg(windows)]
colored::control::set_virtual_terminal(true).ok();
let args = Cli::parse();
if args.print_langs {
println!("Supported Languages:");
let mut langs: Vec<String> = vec![];
for key in SUPPORTED_LANGS.keys() {
langs.push(format!(" - {}: {}", key, SUPPORTED_LANGS.get(key).unwrap()));
}
langs.sort();
for lang in langs {
println!("{}", lang);
}
std::process::exit(0);
}
if args.update_langs {
update_langs();
std::process::exit(0);
}
let mut search_term = String::new();
if args.search_term == None {
if atty::is(atty::Stream::Stdin) {
let mut cmd = clap::Command::new("dym [OPTIONS] <SEARCH_TERM>");
let error = cmd.error(
clap::ErrorKind::MissingRequiredArgument,
format!(
"The {} argument was not provided.\n\n\tEither provide it as an argument or pass it in from standard input.",
"<SEARCH_TERM>".green()
)
);
clap::Error::exit(&error);
} else {
let stdin = io::stdin();
stdin.lock().read_line(&mut search_term).unwrap();
}
} else {
search_term = args.search_term.unwrap();
}
if SUPPORTED_LANGS.contains_key(args.lang.as_str()) {
fetch_word_list(args.lang.to_owned());
} else {
let mut cmd = clap::Command::new("dym [OPTIONS] <SEARCH_TERM>");
let error_string = if LOCALES.contains_key(args.lang.as_str()) {
format!(
"There is currently no word list for {}",
LOCALES.get(args.lang.as_str()).cloned().unwrap()
)
} else {
format!("{} is not a recognized localed code", args.lang)
};
let error = cmd.error(clap::ErrorKind::MissingRequiredArgument, error_string);
clap::Error::exit(&error);
}
let word_list = read_to_string(dirs::data_dir().unwrap().join("didyoumean").join(args.lang))
.expect("Error reading file");
let dictionary = word_list.split('\n');
let mut top_n_words = vec![""; args.number];
let mut top_n_dists = vec![search_term.len() * 10; args.number];
let search_chars = search_term.chars().collect::<Vec<_>>();
for word in dictionary {
let dist = edit_distance(&search_chars, word);
if dist < top_n_dists[args.number - 1] {
for i in 0..args.number {
if dist < top_n_dists[i] {
insert_and_shift(&mut top_n_dists, i, dist);
insert_and_shift(&mut top_n_words, i, word);
break;
}
}
}
}
if !args.clean_output {
if top_n_dists[0] == 0 {
println!("{} is spelled correctly\n", search_term.bold().green());
}
println!("{}", "Did you mean?".blue().bold());
}
let mut items = vec!["".to_string(); args.number];
for i in 0..args.number {
let mut output: String = "".to_string();
let indent = args.number.to_string().len();
if !args.clean_output {
output.push_str(&format!(
"{:>indent$}{} ",
(i + 1).to_string().purple(),
".".purple()
));
}
output.push_str(top_n_words[i]);
if args.verbose {
output.push_str(&format!(" (edit distance: {})", top_n_dists[i]));
}
items[i] = output;
}
if args.yank {
println!(
"{} {}",
"?".yellow(),
"[↑↓ to move, ↵ to select, esc/q to cancel]".bold()
);
let chosen = Select::with_theme(&ColorfulTheme::default())
.items(&items)
.default(0)
.interact_opt()?;
for item in items {
println!(" {}", item);
}
match chosen {
Some(index) => {
yank(top_n_words[index]);
println!(
"{}",
format!("\"{}\" copied to clipboard", top_n_words[index]).green()
);
}
None => {
println!("{}", "No selection made".red());
std::process::exit(1);
}
}
} else {
for item in items {
println!("{}", item);
}
}
Ok(())
}
#[tokio::main]
async fn fetch_word_list(lang: String) {
let data_dir = dirs::data_dir().unwrap().join("didyoumean");
if !data_dir.is_dir() {
create_dir(data_dir).expect("Failed to create data directory");
}
let file_path = dirs::data_dir().unwrap().join("didyoumean").join(&lang);
if !file_path.is_file() {
println!(
"Downloading {} word list...",
LOCALES.get(&lang).unwrap().to_string().blue()
);
let url = format!(
"https://raw.githubusercontent.com/hisbaan/wordlists/main/{}",
&lang
);
let response = get(&url).await.expect("Request failed");
let total_size = response.content_length().unwrap();
let mut file = File::create(file_path).expect("Failed to create file");
let mut downloaded: u64 = 0;
let mut stream = response.bytes_stream();
let pb = ProgressBar::new(total_size);
pb.set_style(
ProgressStyle::default_bar()
.template(
"[{elapsed_precise}] [{wide_bar:.blue/cyan}] {bytes}/{total_bytes} ({eta})",
)
.progress_chars("#>-"),
);
while let Some(item) = stream.next().await {
let chunk = item.expect("Error downloading file");
file.write_all(&chunk).expect("Error while writing to file");
let new = min(downloaded + (chunk.len() as u64), total_size);
downloaded = new;
pb.set_position(new);
}
pb.finish_at_current_pos();
}
}
fn update_langs() {
let data = data_dir().unwrap().join("didyoumean");
if !data.is_dir() {
create_dir(&data).expect("Failed to create data directory");
}
let data_dir_files = read_dir(&data).unwrap();
for file in data_dir_files {
let file_name = file.unwrap().file_name();
let string: &str = file_name.to_str().unwrap();
if SUPPORTED_LANGS.contains_key(string) {
remove_file(data.join(&string)).expect("Failed to update file (deletion failed)");
fetch_word_list(string.to_string());
}
}
}