= Default::default();
27 | man.render(&mut buffer).expect("Man page generation failed");
28 | std::fs::write(man_dir.join("dym.1"), buffer).expect("Failed to write man page");
29 |
30 | // Generate shell completions.
31 | for shell in [Bash, Elvish, Fish, PowerShell, Zsh] {
32 | generate_to(shell, &mut cmd, "dym", &comp_dir).unwrap();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | DidYouMean
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | DidYouMean (or `dym`) is a command-line spelling corrector written in rust utilizing a simplified version of [Damerau-Levenshtein distance](https://en.wikipedia.org/wiki/Damerau-Levenshtein_distance). DidYouMean is for those moments when you know what a word sounds like, but you're not quite sure how it's spelled.
14 |
15 |
16 |
17 |
18 |
19 | ## Installation
20 |
21 | ### Arch Linux (and derivatives)
22 |
23 | DidYouMean is available on the AUR as three different packages:
24 |
25 | - [didyoumean](https://aur.archlinux.org/packages/didyoumean): Last stable release, built from source (Thank you [orhun](https://github.com/orhun)!).
26 | - [didyoumean-git](https://aur.archlinux.org/packages/didyoumean-git): Last git commit, built from source. This is the most up to date, but the least stable.
27 | - [didyoumean-bin](https://aur.archlinux.org/packages/didyoumean-bin): Last stable release, distributed as a binary. This is only available for `x86_64` at the moment.
28 |
29 | You can install it using any AUR helper. Using `paru`, the command would be as follows:
30 |
31 | ```sh
32 | paru -S
33 | ```
34 |
35 | ### Homebrew (macOS)
36 |
37 | Homebrew is a package manager for macOS. Currently, I have only packaged an x86\_64 binary. The command to install it is as follows:
38 |
39 | ```sh
40 | brew tap hisbaan/tap
41 | brew install didyoumean
42 | ```
43 |
44 | ### NixOS
45 |
46 | [evanjs](https://github.com/evanjs) very kindly packaged `didyoumean` for NixOS. The command to install is as follows:
47 |
48 | ```sh
49 | nix-env install -iA nixpkgs.didyoumean
50 | ```
51 |
52 | ### Cargo
53 |
54 | Run the following command to build `dym` from source and install it in your home directory. Ensure that you have `$CARGO_HOME/bin/` in your path.
55 |
56 | ```sh
57 | cargo install didyoumean
58 | ```
59 |
60 | ## Developer Installation
61 |
62 | The build dependencies for this project are `git`, `rust`, `rustc`, and `cargo`. First, clone this repository, then run
63 |
64 | ```sh
65 | cargo run --
66 | ```
67 |
68 | where `` are the command-line arguments you would pass the DidYouMean binary. Note that this is an unoptimized build contianing debug information so it runs much, much slower.
69 |
--------------------------------------------------------------------------------
/docs/img/cyclophosphamide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hisbaan/didyoumean/b599cb21adb5738f18708fd3590f0df86ec59a9b/docs/img/cyclophosphamide.png
--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 |
3 | // Parse command line arguments to get the search term.
4 | #[derive(Parser)]
5 | #[clap(author = "Hisbaan Noorani", version = "1.1.4", about = "Did You Mean: A cli spelling corrector", long_about = None)]
6 | pub struct Cli {
7 | pub search_term: Option,
8 | #[clap(
9 | short = 'n',
10 | long = "number",
11 | default_value_t = 5,
12 | help = "Change the number of matches printed",
13 | long_help = "Change the number of words the program will print. The default value is five."
14 | )]
15 | pub number: usize,
16 | #[clap(
17 | short = 'c',
18 | long = "clean-output",
19 | help = "Print clean output",
20 | long_help = "Print a clean version of the output without the title, numbers or colour."
21 | )]
22 | pub clean_output: bool,
23 | #[clap(
24 | short = 'v',
25 | long = "verbose",
26 | help = "Print verbose output",
27 | long_help = "Print verbose output including the edit distance of the found word to the queried word."
28 | )]
29 | pub verbose: bool,
30 | #[clap(
31 | short = 'y',
32 | long = "yank",
33 | help = "Yank (copy) to the system cliboard",
34 | long_help = "Yank (copy) the selected word to the system clipboard. If no word is selected, the clipboard will not be altered."
35 | )]
36 | pub yank: bool,
37 | #[clap(
38 | short = 'l',
39 | long = "lang",
40 | help = "Select the desired language using the locale code (en, fr, sp, etc.)",
41 | long_help = "Select the desired language using its locale code. For example, English would have the locale code en and French would have the locale code fr. See --print-langs for a list of locale codes and the corresponding languages.",
42 | default_value = "en"
43 | )]
44 | pub lang: String,
45 | #[clap(
46 | long = "print-langs",
47 | help = "Display a list of supported languages",
48 | long_help = "Display a list of supported languages and their respective locale codes."
49 | )]
50 | pub print_langs: bool,
51 | #[clap(
52 | long = "update-langs",
53 | help = "Update all language files",
54 | long_help = "Update all language files from the repository https://github.com/hisbaan/wordlists."
55 | )]
56 | pub update_langs: bool,
57 | }
58 |
--------------------------------------------------------------------------------
/src/langs.rs:
--------------------------------------------------------------------------------
1 | use phf::phf_map;
2 |
3 | pub static LOCALES: phf::Map<&'static str, &'static str> = phf_map! {
4 | "af" => "Afrikaans",
5 | "ar" => "Arabic",
6 | "az" => "Azerbaijani",
7 | "be" => "Belarusian",
8 | "bg" => "Bulgarian",
9 | "br" => "Breton",
10 | "bs" => "Bosnian",
11 | "ca" => "Catalan",
12 | "cs" => "Czech",
13 | "cy" => "Welsh",
14 | "da" => "Danish",
15 | "de" => "German",
16 | "el" => "Greek",
17 | "en" => "English",
18 | "es" => "Spanish",
19 | "et" => "Estonian",
20 | "eu" => "Basque",
21 | "fa" => "Farsi",
22 | "fi" => "Finnish",
23 | "fo" => "Faeroese",
24 | "fr" => "French",
25 | "fy" => "Frisian",
26 | "ga" => "Irish",
27 | "gd" => "Gaelic",
28 | "gl" => "Galician",
29 | "he" => "Hebrew",
30 | "hi" => "Hindi",
31 | "hr" => "Croatian",
32 | "hu" => "Hungarian",
33 | "id" => "Indonesian",
34 | "is" => "Icelandic",
35 | "it" => "Italian",
36 | "ja" => "Japanese",
37 | "ji" => "Yiddish",
38 | "kk" => "Kazach",
39 | "ko" => "Korean",
40 | "la" => "Latin",
41 | "lb" => "Luxembourgish",
42 | "lt" => "Lithuanian",
43 | "lv" => "Latvian",
44 | "mk" => "Macedonian",
45 | "ml" => "Malalyalam",
46 | "ms" => "Malaysian",
47 | "mt" => "Maltese",
48 | "nb" => "Norwegian Bokmal",
49 | "nl" => "Dutch",
50 | "nn" => "Norwegian Nynorsk",
51 | "oc" => "Occitan",
52 | "pa" => "Punjabi",
53 | "pl" => "Polish",
54 | "pt" => "Portugese",
55 | "rm" => "Rhaeto-Romanic",
56 | "ro" => "Romanian",
57 | "ru" => "Russian",
58 | "sb" => "Sorbian",
59 | "se" => "Northern Sami",
60 | "sk" => "Slovak",
61 | "sl" => "Slovenian",
62 | "sq" => "Albanian",
63 | "sr" => "Serbian",
64 | "st" => "Sesotho",
65 | "sv" => "Swedish",
66 | "sw" => "Swahili",
67 | "tg" => "Tajik",
68 | "th" => "Thai",
69 | "tk" => "Turkmen",
70 | "tl" => "Tagalog",
71 | "tn" => "Tswana",
72 | "tr" => "Turkish",
73 | "ts" => "Tsonga",
74 | "uk" => "Ukranian",
75 | "ur" => "Urdu",
76 | "ve" => "Venda",
77 | "vi" => "Vietnamese",
78 | "xh" => "Xhosa",
79 | "yi" => "Yiddish",
80 | "zh" => "Chinese",
81 | "zu" => "Zulu",
82 | };
83 |
84 | pub static SUPPORTED_LANGS: phf::Map<&'static str, &'static str> = phf_map! {
85 | "af" => "Afrikaans",
86 | "ar" => "Arabic",
87 | "az" => "Azerbaijani",
88 | "be" => "Belarusian",
89 | "bg" => "Bulgarian",
90 | "br" => "Breton",
91 | "bs" => "Bosnian",
92 | "ca" => "Catalan",
93 | "cs" => "Czech",
94 | "cy" => "Welsh",
95 | "da" => "Danish",
96 | "de" => "German",
97 | "el" => "Greek",
98 | "en" => "English",
99 | "es" => "Spanish",
100 | "et" => "Estonian",
101 | "eu" => "Basque",
102 | "fo" => "Faeroese",
103 | "fr" => "French",
104 | "fy" => "Frisian",
105 | "gl" => "Galician",
106 | "hu" => "Hungarian",
107 | "id" => "Indonesian",
108 | "is" => "Icelandic",
109 | "it" => "Italian",
110 | "kk" => "Kazach",
111 | "ko" => "Korean",
112 | "la" => "Latin",
113 | "lb" => "Luxembourgish",
114 | "lt" => "Lithuanian",
115 | "lv" => "Latvian",
116 | "ms" => "Malaysian",
117 | "nb" => "Norwegian Bokmal",
118 | "nl" => "Dutch",
119 | "nn" => "Norwegian Nynorsk",
120 | "oc" => "Occitan",
121 | "pl" => "Polish",
122 | "ro" => "Romanian",
123 | "ru" => "Russian",
124 | "se" => "Northern Sami",
125 | "sk" => "Slovak",
126 | "sl" => "Slovenian",
127 | "sq" => "Albanian",
128 | "sr" => "Serbian",
129 | "st" => "Sesotho",
130 | "sv" => "Swedish",
131 | "sw" => "Swahili",
132 | "tg" => "Tajik",
133 | "tk" => "Turkmen",
134 | "tl" => "Tagalog",
135 | "tn" => "Tswana",
136 | "tr" => "Turkish",
137 | "ts" => "Tsonga",
138 | "tt" => "Tatar",
139 | "uk" => "Ukranian",
140 | "ve" => "Venda",
141 | "vi" => "Vietnamese",
142 | "xh" => "Xhosa",
143 | "yi" => "Yiddish",
144 | "zu" => "Zulu",
145 | };
146 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | use cli_clipboard::{ClipboardContext, ClipboardProvider};
2 | use colored::*;
3 | use std::cmp::min;
4 |
5 | #[cfg(unix)]
6 | use nix::unistd::{fork, ForkResult};
7 |
8 | /// Copy `string` to the system clipboard
9 | ///
10 | /// # Arguments
11 | ///
12 | /// * `string` - the string to be copied.
13 | pub fn yank(string: &str) {
14 | let platform = std::env::consts::OS;
15 | if vec![
16 | "linux",
17 | "freebsd",
18 | "netbsd",
19 | "dragonfly",
20 | "netbsd",
21 | "openbsd",
22 | "solaris",
23 | ]
24 | .contains(&platform)
25 | {
26 | // The platform is linux/*bsd and is likely using X11 or Wayland.
27 | // There is a fix needed for clipboard use in cases like these.
28 | // The clipboard is cleared on X11/Wayland after the process that set it exist.
29 | // To combat this, we will fork and keep a process around until the clipboard
30 | // is cleared.
31 | // Ideally, this wouldn't be an issue but it was a conscious design decision
32 | // on X11/Wayland
33 | #[cfg(unix)]
34 | match unsafe { fork() } {
35 | Ok(ForkResult::Child) => {
36 | let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
37 | ctx.set_contents(string.to_owned()).unwrap();
38 |
39 | // Keep the process running until the clipboard changes.
40 | loop {
41 | let clipboard = ctx.get_contents().unwrap();
42 | std::thread::sleep(std::time::Duration::from_secs(1));
43 | if clipboard != string {
44 | std::process::exit(0);
45 | }
46 | }
47 | }
48 | Err(_) => {
49 | println!("{}", "Error: Clipboard fork failed".red());
50 | std::process::exit(1);
51 | }
52 | _ => {}
53 | }
54 | } else {
55 | // The platform is NOT running X11/Wayland and thus, we don't have to handle
56 | // the clipboard clearing behaviour.
57 | let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
58 | ctx.set_contents(string.to_owned()).unwrap();
59 | }
60 | }
61 |
62 | /// Insert `element` at `index` preserving length.
63 | ///
64 | /// # Arguments
65 | ///
66 | /// * `list` - A vec to be shifted down
67 | /// * `index` - The index at which to insert `element`
68 | /// * `element` - The element to insert at `index`
69 | ///
70 | /// # Examples
71 | ///
72 | /// ```
73 | /// # use didyoumean::insert_and_shift;
74 | /// let mut to_shift = vec![0, 1, 2, 3, 4];
75 | /// insert_and_shift(&mut to_shift, 2, 11);
76 | ///
77 | /// assert_eq!(to_shift, vec![0, 1, 11, 2, 3]);
78 | /// ```
79 | pub fn insert_and_shift(list: &mut Vec, index: usize, element: T) {
80 | if index > list.len() - 1 {
81 | return;
82 | }
83 |
84 | list.insert(index, element);
85 | list.truncate(list.len() - 1);
86 | }
87 |
88 | /// Return the edit distance between `search_term` and `known_term`.
89 | /// Currently implemented using a modified version of
90 | /// [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance).
91 | ///
92 | /// # Arguments
93 | ///
94 | /// * `search_chars` - The first `Vec` to compare, in most time search_term will not change, so
95 | /// we would like to share the same `Vec` between multiple calls. you could use `search_string.chars().collect::>()` to
96 | /// convert a string to a `Vec`
97 | /// * `known_term` - The second string to compare
98 | ///
99 | /// # Examples
100 | ///
101 | /// ```
102 | /// # use didyoumean::edit_distance;
103 | /// let dist = edit_distance(&"sitting".chars().collect::>(), "kitten");
104 | /// assert_eq!(dist, 3);
105 | /// assert_eq!(edit_distance(&"geek".chars().collect::>(), "gesek"), 1);
106 | /// assert_eq!(edit_distance(&"cat".chars().collect::>(), "cut"), 1);
107 | /// assert_eq!(edit_distance(&"sunday".chars().collect::>(), "saturday"), 3);
108 | /// assert_eq!(edit_distance(&"tset".chars().collect::>(), "test"), 1);
109 | /// ```
110 | #[allow(clippy::iter_count, clippy::needless_range_loop)]
111 | pub fn edit_distance(search_chars: &[char], known_term: &str) -> usize {
112 | // Set local constants for repeated use later.
113 | let known_chars: Vec = known_term.chars().collect();
114 | let n = search_chars.iter().count() + 1;
115 | let m = known_chars.iter().count() + 1;
116 |
117 | // Setup matrix 2D vector.
118 | let mut mat = vec![0; m * n];
119 |
120 | // Initialize values of the matrix.
121 | for i in 1..n {
122 | mat[i * m] = i;
123 | }
124 | for i in 1..m {
125 | mat[i] = i;
126 | }
127 |
128 | // Run the algorithm.
129 | for i in 1..n {
130 | // let search_char_i_minus_one = search_chars[i - 1];
131 | // let search_char_i_minus_two = if i > 1 { search_chars[i - 2] } else { ' ' };
132 | for j in 1..m {
133 | let sub_cost = if search_chars[i - 1] == known_chars[j - 1] {
134 | 0
135 | } else {
136 | 1
137 | };
138 |
139 | mat[i * m + j] = min(
140 | mat[(i - 1) * m + j - 1] + sub_cost, // substitution cost
141 | min(
142 | mat[(i - 1) * m + j] + 1, // deletion cost
143 | mat[i * m + j - 1] + 1, // insertion cost
144 | ),
145 | );
146 | if i > 1
147 | && j > 1
148 | && search_chars[i - 1] == known_chars[j - 2]
149 | && search_chars[i - 2] == known_chars[j - 1]
150 | {
151 | mat[i * m + j] = min(
152 | mat[i * m + j],
153 | mat[(i - 2) * m + j - 2] + 1, // transposition cost
154 | );
155 | }
156 | }
157 | }
158 |
159 | // Return the bottom left corner of the matrix.
160 | mat[m * n - 1]
161 | }
162 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | pub mod cli;
2 | pub mod langs;
3 |
4 | use clap::Parser;
5 | use clap::error::ErrorKind;
6 | use colored::*;
7 | use dialoguer::{theme::ColorfulTheme, Select};
8 | use dirs::data_dir;
9 | use futures_util::StreamExt;
10 | use indicatif::{ProgressBar, ProgressStyle};
11 | use reqwest::get;
12 | use std::{
13 | cmp::min,
14 | fs::{create_dir, read_dir, read_to_string, remove_file, File},
15 | io::{self, BufRead, Error, Write},
16 | };
17 |
18 | use cli::Cli;
19 | use didyoumean::{edit_distance, insert_and_shift, yank};
20 | use langs::{LOCALES, SUPPORTED_LANGS};
21 |
22 | fn main() {
23 | std::process::exit(match run_app() {
24 | Ok(_) => 0,
25 | Err(error) => {
26 | eprintln!("Error: {:?}", error);
27 | 1
28 | }
29 | });
30 | }
31 |
32 | /// Main function to run the application. Return `std::result::Result<(), std::io::Error>`.
33 | fn run_app() -> std::result::Result<(), Error> {
34 | // Correctly output ANSI escape codes on Windows.
35 | #[cfg(windows)]
36 | colored::control::set_virtual_terminal(true).ok();
37 |
38 | // Parse args using clap.
39 | let args = Cli::parse();
40 |
41 | // Print all supported languages.
42 | if args.print_langs {
43 | println!("Supported Languages:");
44 | let mut langs: Vec = vec![];
45 |
46 | // Add words to vector.
47 | for key in SUPPORTED_LANGS.keys() {
48 | langs.push(format!(" - {}: {}", key, SUPPORTED_LANGS.get(key).unwrap()));
49 | }
50 |
51 | // Sort and print vector.
52 | langs.sort();
53 | for lang in langs {
54 | println!("{}", lang);
55 | }
56 |
57 | std::process::exit(0);
58 | }
59 |
60 | // Update all downloaded languages.
61 | if args.update_langs {
62 | update_langs();
63 | std::process::exit(0);
64 | }
65 |
66 | let mut search_term = String::new();
67 |
68 | // Check if nothing was passed in as the search term.
69 | if args.search_term == None {
70 | // Check if stdin is empty, produce error if so.
71 | if atty::is(atty::Stream::Stdin) {
72 | let mut cmd = clap::Command::new("dym [OPTIONS] ");
73 | let error = cmd.error(
74 | ErrorKind::MissingRequiredArgument,
75 | format!(
76 | "The {} argument was not provided.\n\n\tEither provide it as an argument or pass it in from standard input.",
77 | "".green()
78 | )
79 | );
80 | clap::Error::exit(&error);
81 | } else {
82 | // Read search_term from standard input if stdin is not empty.
83 | let stdin = io::stdin();
84 | stdin.lock().read_line(&mut search_term).unwrap();
85 | }
86 | } else {
87 | // Unwrap Option that was read from the client.
88 | search_term = args.search_term.unwrap();
89 | }
90 |
91 | if SUPPORTED_LANGS.contains_key(args.lang.as_str()) {
92 | fetch_word_list(args.lang.to_owned());
93 | } else {
94 | // Not supported
95 | // Initialize new command.
96 | let mut cmd = clap::Command::new("dym [OPTIONS] ");
97 |
98 | // Whether or not locale code is valid.
99 | let error_string = if LOCALES.contains_key(args.lang.as_str()) {
100 | format!(
101 | "There is currently no word list for {}",
102 | LOCALES.get(args.lang.as_str()).cloned().unwrap()
103 | )
104 | } else {
105 | format!("{} is not a recognized localed code", args.lang)
106 | };
107 |
108 | // Set error.
109 | let error = cmd.error(ErrorKind::MissingRequiredArgument, error_string);
110 |
111 | // Exit with error.
112 | clap::Error::exit(&error);
113 | }
114 |
115 | // Get word list. The program will only get here if/when this is a valid word list.
116 | let word_list = read_to_string(dirs::data_dir().unwrap().join("didyoumean").join(args.lang))
117 | .expect("Error reading file");
118 |
119 | // Get dictionary of words from words.txt.
120 | let dictionary = word_list.split('\n');
121 |
122 | // Create mutable vecs for storing the top n words.
123 | let mut top_n_words = vec![""; args.number];
124 | let mut top_n_dists = vec![search_term.len() * 10; args.number];
125 |
126 | // Loop over the words in the dictionary, run the algorithm, and
127 | // add to the list if appropriate
128 | let search_chars = search_term.chars().collect::>();
129 | for word in dictionary {
130 | // Get edit distance.
131 | let dist = edit_distance(&search_chars, word);
132 |
133 | // Add to the list if appropriate.
134 | if dist < top_n_dists[args.number - 1] {
135 | for i in 0..args.number {
136 | if dist < top_n_dists[i] {
137 | insert_and_shift(&mut top_n_dists, i, dist);
138 | insert_and_shift(&mut top_n_words, i, word);
139 | break;
140 | }
141 | }
142 | }
143 | }
144 |
145 | // Print out results.
146 | if !args.clean_output {
147 | if top_n_dists[0] == 0 {
148 | println!("{} is spelled correctly\n", search_term.bold().green());
149 | }
150 | println!("{}", "Did you mean?".blue().bold());
151 | }
152 | let mut items = vec!["".to_string(); args.number];
153 | for i in 0..args.number {
154 | let mut output: String = "".to_string();
155 | let indent = args.number.to_string().len();
156 |
157 | // Add numbers if not clean.
158 | if !args.clean_output {
159 | output.push_str(&format!(
160 | "{:>indent$}{} ",
161 | (i + 1).to_string().purple(),
162 | ".".purple()
163 | ));
164 | }
165 |
166 | // Add words in order of edit distance.
167 | output.push_str(top_n_words[i]);
168 |
169 | // Add edit distance if verbose.
170 | if args.verbose {
171 | output.push_str(&format!(" (edit distance: {})", top_n_dists[i]));
172 | }
173 |
174 | // Print concatenated string.
175 | items[i] = output;
176 | }
177 |
178 | // If the yank argument is set, copy the item to the clipboard.
179 | if args.yank {
180 | // Print prompt
181 | println!(
182 | "{} {}",
183 | "?".yellow(),
184 | "[↑↓ to move, ↵ to select, esc/q to cancel]".bold()
185 | );
186 | // Get the chosen argument.
187 | let chosen = Select::with_theme(&ColorfulTheme::default())
188 | .items(&items)
189 | .default(0)
190 | .interact_opt()
191 | .unwrap();
192 |
193 | // Print out items since dialoguer clears.
194 | for item in items {
195 | println!(" {}", item);
196 | }
197 |
198 | match chosen {
199 | // If the chosen arguemnt is valid.
200 | Some(index) => {
201 | yank(top_n_words[index]);
202 | println!(
203 | "{}",
204 | format!("\"{}\" copied to clipboard", top_n_words[index]).green()
205 | );
206 | }
207 | // If no argument is chosen.
208 | None => {
209 | println!("{}", "No selection made".red());
210 | std::process::exit(1);
211 | }
212 | }
213 | } else {
214 | // If yank is not set, print out all the items.
215 | for item in items {
216 | println!("{}", item);
217 | }
218 | }
219 |
220 | Ok(())
221 | }
222 |
223 | /// Fetch the word list specified by `lang` from https://github.com/hisbaan/wordlists
224 | ///
225 | /// # Arguments
226 | ///
227 | /// * `lang` - A locale code string to define the word list file to fetch.
228 | #[tokio::main]
229 | async fn fetch_word_list(lang: String) {
230 | // Get data directory.
231 | let data_dir = dirs::data_dir().unwrap().join("didyoumean");
232 |
233 | // Create data directory if it doesn't exist.
234 | if !data_dir.is_dir() {
235 | create_dir(data_dir).expect("Failed to create data directory");
236 | }
237 |
238 | // Get file path.
239 | let file_path = dirs::data_dir().unwrap().join("didyoumean").join(&lang);
240 |
241 | // If the file does not exist, fetch it from the server.
242 | if !file_path.is_file() {
243 | println!(
244 | "Downloading {} word list...",
245 | LOCALES.get(&lang).unwrap().to_string().blue()
246 | );
247 |
248 | let url = format!(
249 | "https://raw.githubusercontent.com/hisbaan/wordlists/main/{}",
250 | &lang
251 | );
252 |
253 | // Setup reqwest.
254 | let response = get(&url).await.expect("Request failed");
255 | let total_size = response.content_length().unwrap();
256 | let mut file = File::create(file_path).expect("Failed to create file");
257 | let mut downloaded: u64 = 0;
258 | let mut stream = response.bytes_stream();
259 |
260 | // Setup indicatif.
261 | let pb = ProgressBar::new(total_size);
262 | pb.set_style(
263 | ProgressStyle::default_bar()
264 | .template(
265 | "[{elapsed_precise}] [{wide_bar:.blue/cyan}] {bytes}/{total_bytes} ({eta})",
266 | )
267 | .unwrap()
268 | .progress_chars("#>-"),
269 | );
270 |
271 | // Read from stream into file.
272 | while let Some(item) = stream.next().await {
273 | let chunk = item.expect("Error downloading file");
274 | file.write_all(&chunk).expect("Error while writing to file");
275 | let new = min(downloaded + (chunk.len() as u64), total_size);
276 | downloaded = new;
277 | pb.set_position(new);
278 | }
279 |
280 | // Print completed bar.
281 | pb.finish();
282 | }
283 | }
284 |
285 | /// Update the word list files by deleting and downloading the files from the repository.
286 | fn update_langs() {
287 | let data = data_dir().unwrap().join("didyoumean");
288 |
289 | // Create data directory if it doesn't exist.
290 | if !data.is_dir() {
291 | create_dir(&data).expect("Failed to create data directory");
292 | }
293 |
294 | // Get files in data directory.
295 | let data_dir_files = read_dir(&data).unwrap();
296 |
297 | // Delete and update all files.
298 | for file in data_dir_files {
299 | let file_name = file.unwrap().file_name();
300 | let string: &str = file_name.to_str().unwrap();
301 |
302 | // Only delete and download if the language is supported.
303 | if SUPPORTED_LANGS.contains_key(string) {
304 | remove_file(data.join(&string)).expect("Failed to update file (deletion failed)");
305 | fetch_word_list(string.to_string());
306 | }
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/tests/integration_test.rs:
--------------------------------------------------------------------------------
1 | use cli_clipboard::{ClipboardContext, ClipboardProvider};
2 | use didyoumean::yank;
3 |
4 | #[test]
5 | fn yank_test() {
6 | let string = "test";
7 | let not_string = "not test";
8 | let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
9 |
10 | // Run the yank function.
11 | yank(string);
12 |
13 | // Sleep to allow the function time to write to the clipboard.
14 | std::thread::sleep(std::time::Duration::from_secs(1));
15 |
16 | // Get the clipboard contents.
17 | let clipboard = format!("{}", ctx.get_contents().unwrap());
18 |
19 | assert_eq!(clipboard, string);
20 |
21 | // Set the clipboard contents to something else to get the process to exit.
22 | ctx.set_contents(not_string.to_owned()).unwrap();
23 | }
24 |
--------------------------------------------------------------------------------