diff --git a/src/aux.rs b/src/aux.rs new file mode 100644 index 0000000..efdc306 --- /dev/null +++ b/src/aux.rs @@ -0,0 +1,33 @@ +use crate::Value; + +#[derive(Default, Debug, Clone)] +pub struct Options { + pub limit: usize, + pub query: String, + pub interactive: bool, +} + +// +// --- Value helper +// + +pub fn value_to_bool(value: &Value) -> bool { + match value { + Value::Bool(b) => *b, + _ => unreachable!(), + } +} + +pub fn value_to_str(value: &Value) -> &str { + match value { + Value::String(s) => s, + _ => unreachable!(), + } +} + +pub fn value_to_arr(value: &Value) -> &Vec { + match value { + Value::Array(a) => a, + _ => unreachable!(), + } +} diff --git a/src/kanji_search.rs b/src/kanji_search.rs index 61628f9..8f0e868 100644 --- a/src/kanji_search.rs +++ b/src/kanji_search.rs @@ -19,7 +19,7 @@ pub fn search_by_radical(mut query: &mut String){ /* First iteration: get the baseline for the results */ let mut rad = query.chars().nth(1).unwrap(); if rad == '*' || rad == '*' { - /* if search_by_radical returned an error then something is very wrong */ + /* if search_by_strokes returned an error then something is very wrong */ rad = search_by_strokes(&mut query, &radk_list, 1).expect("Couldn't parse input"); } @@ -35,7 +35,7 @@ pub fn search_by_radical(mut query: &mut String){ /* Iterate until you've exhausted user input: refine the baseline to get final output */ for (i, mut rad) in query.clone().chars().skip(2).enumerate() { if rad == '*' || rad == '*' { - /* if search_by_radical returned an error then something is very wrong */ + /* if search_by_strokes returned an error then something is very wrong */ rad = search_by_strokes(&mut query, &radk_list, i+2).expect("Couldn't parse input"); } @@ -61,6 +61,61 @@ pub fn search_by_radical(mut query: &mut String){ } } +fn search_by_strokes(query: &mut String, radk_list: &[radk::Membership], n: usize) -> Result { + + let mut strokes = String::new(); + let mut radicals: Vec = Vec::new(); + let rad; + loop{ + print!("How many strokes does your radical have? "); + stdout().flush()?; + strokes.clear(); + stdin().read_line(&mut strokes)?; + + match strokes.trim().parse::() { + Ok(strk) => { + let mut i = 1; + for k in radk_list.iter() { + if k.radical.strokes == strk { + print!("{}{} ", i, k.radical.glyph); + radicals.push(k.radical.glyph.chars().next().unwrap()); + i += 1; + } else if k.radical.strokes > strk { + println!(); + break; + } + } + loop { + print!("Choose the radical to use for your search: "); + stdout().flush()?; + strokes.clear(); + stdin().read_line(&mut strokes)?; + + match strokes.trim().parse::() { + Ok(strk) => { + if strk < 1 || strk > i-1 { + eprintln!("Couldn't parse input: number not in range"); + } else { + rad = radicals.get(strk-1).unwrap(); + /* UTF-8 is not fun */ + let char_and_index = query.char_indices().nth(n).unwrap(); + query.replace_range(char_and_index.0.. + char_and_index.0 + + char_and_index.1.len_utf8(), + rad.to_string().as_str()); + println!("{}", query.as_str().bright_black()); + return Ok(*rad); + } + }, + Err(e) => { eprintln!("{e}"); } + } + } + }, + Err(e) => { eprintln!("{e}") } + } + } +} + #[cfg(unix)] fn get_radkfile_path() -> Option { #[allow(deprecated)] /* obviously no windows problem here */ @@ -100,62 +155,3 @@ fn get_radkfile_path() -> Option { } } } - -fn search_by_strokes(query: &mut String, radk_list: &[radk::Membership], n: usize) -> Result { - - let mut strokes = String::new(); - let mut radicals: Vec = Vec::new(); - let rad; - loop{ - print!("How many strokes does your radical have? "); - stdout().flush()?; - strokes.clear(); - if (stdin().read_line(&mut strokes).expect("Can't read from stdin")) == 0 { - std::process::exit(0); - } - - match strokes.trim().parse::() { - Ok(strk) => { - let mut i = 1; - for k in radk_list.iter() { - if k.radical.strokes == strk { - print!("{}{} ", i, k.radical.glyph); - radicals.push(k.radical.glyph.chars().next().unwrap()); - i += 1; - } else if k.radical.strokes > strk { - println!(); - break; - } - } - loop { - print!("Choose the radical to use for your search: "); - stdout().flush()?; - strokes.clear(); - if (stdin().read_line(&mut strokes).expect("Can't read from stdin")) == 0 { - std::process::exit(0); - } - - match strokes.trim().parse::() { - Ok(strk) => { - if strk < 1 || strk > i-1 { - eprintln!("Couldn't parse input: number not in range"); - } else { - rad = radicals.get(strk-1).unwrap(); - /* UTF-8 is not fun */ - let char_and_index = query.char_indices().nth(n).unwrap(); - query.replace_range(char_and_index.0.. - char_and_index.0 + - char_and_index.1.len_utf8(), - rad.to_string().as_str()); - println!("{}", query.as_str().bright_black()); - return Ok(*rad); - } - }, - Err(e) => { eprintln!("{e}"); } - } - } - }, - Err(e) => { eprintln!("{e}") } - } - } -} diff --git a/src/main.rs b/src/main.rs index 2aca60f..33c5157 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,17 @@ +mod word_search; mod kanji_search; +mod aux; use std::{ io::{stdin, stdout, Write}, process::{Command, Stdio}, - error::Error, env, }; +use word_search::word_search; use kanji_search::search_by_radical; + use argparse::{ArgumentParser, List, Print, Store, StoreTrue}; -use colored::*; use serde_json::Value; use atty::Stream; @@ -19,14 +21,9 @@ macro_rules! JISHO_URL { }; } -#[derive(Default, Debug, Clone)] -struct Options { - limit: usize, - query: String, - interactive: bool, } -fn main() -> Result<(), Box> { +fn main() -> Result<(), ureq::Error> { let term_size = if atty::is(Stream::Stdout) { terminal_size().unwrap_or(0) @@ -59,56 +56,31 @@ fn main() -> Result<(), Box> { query = query.trim().to_string(); let mut lines_output = 0; - let mut output = String::with_capacity(5242880); /* Give output 5MB of buffer; Should be enough to avoid reallocs*/ + let mut output = String::with_capacity(5242880); /* Give output 5MiB of buffer; Should be enough to avoid reallocs*/ - if query.starts_with(':') || query.starts_with(':') { + if query.starts_with(':') || query.starts_with(':') { /* Kanji search */ search_by_radical(&mut query); } else { + } else { /* Word search */ // Do API request let body: Value = ureq::get(&format!(JISHO_URL!(), query)) .call()? .into_json()?; - // Try to get the data json-object - let body = value_to_arr({ - let body = body.get("data"); - - if body.is_none() { - eprintln!("Error! Invalid response"); - return Ok(()); - } - - body.unwrap() - }); - - if options.interactive { - println!(); - } - - /* Iterate over meanings and print them */ - for (i, entry) in body.iter().enumerate() { - if i >= options.limit && options.limit != 0 { - break; - } - if let Some(r) = print_item(&query, entry, &mut output) { - lines_output += r; - } - - output.push('\n'); - lines_output += 1; - } - output.pop(); - lines_output = lines_output.saturating_sub(1); - - if lines_output >= term_size - 1 && term_size != 0 { - /* Output is a different process that is not a tty (i.e. less), but we want to keep colour */ - env::set_var("CLICOLOR_FORCE", "1"); - pipe_to_less(output); + if let Some(r) = word_search(&options, body, &query, &mut output) { + lines_output += r; } else { - print!("{}", output); + eprintln!("Error: invalid json returned"); + return Ok(()); } - + } + if lines_output >= term_size - 1 && term_size != 0 { + /* Output is a different process that is not a tty (i.e. less), but we want to keep colour */ + env::set_var("CLICOLOR_FORCE", "1"); + pipe_to_less(output); + } else { + print!("{}", output); } if !options.interactive { break; @@ -117,208 +89,8 @@ fn main() -> Result<(), Box> { Ok(()) } -fn print_item(query: &str, value: &Value, output: &mut String) -> Option { - let japanese = value_to_arr(value.get("japanese")?); - let main_form = japanese.get(0)?; - let mut num_of_lines = 0; - - *output += &format!("{} {}\n", format_form(query, main_form)?, format_result_tags(value)); - - /* Print senses */ - let senses = value_to_arr(value.get("senses")?); - let mut prev_parts_of_speech = String::new(); - - for (i, sense) in senses.iter().enumerate() { - let (sense_str, new_part_of_speech) = format_sense(sense, i, &mut prev_parts_of_speech); - if !sense_str.is_empty() { - /* - * If the current meaning of our word is a different part of speech - * (e.g. previous meaning was 'Noun' and the current is 'Adverb'), an extra line will be - * printed with this information - */ - if new_part_of_speech { - num_of_lines += 1; - } - - *output += &format!(" {}\n", sense_str); - } - } - - /* Print alternative readings and kanji usage */ - if let Some(form) = japanese.get(1) { - num_of_lines += 2; - - *output += &format!(" {}", "Other forms\n".bright_blue()); - *output += &format!(" {}", format_form(query, form)?); - - for i in 2..japanese.len() { - *output += &format!(", {}", format_form(query, japanese.get(i)?)?); - } - output.push('\n'); - } - - num_of_lines += senses.len() + 1; - Some(num_of_lines) -} - -fn format_form(query: &str, form: &Value) -> Option { - let reading = form - .get("reading") - .map(value_to_str) - .unwrap_or(query); - - let word = value_to_str(form.get("word").unwrap_or(form.get("reading")?)); - - Some(format!("{}[{}]", word, reading)) -} - -fn format_sense(value: &Value, index: usize, prev_parts_of_speech: &mut String) -> (String, bool) { - let english_definitons = value.get("english_definitions"); - let parts_of_speech = value.get("parts_of_speech"); - if english_definitons.is_none() { - return ("".to_owned(), false); - } - - let english_definiton = value_to_arr(english_definitons.unwrap()); - - let parts_of_speech = if let Some(parts_of_speech) = parts_of_speech { - let parts = value_to_arr(parts_of_speech) - .iter() - .map(value_to_str) - .collect::>() - .join(", "); - - /* Do not repeat a meaning's part of speech if it is the same as the previous meaning */ - if !parts.is_empty() && parts != *prev_parts_of_speech { - *prev_parts_of_speech = parts.clone(); - format!("{}\n ", parts.bright_blue()) - } else { - String::new() - } - } else { - String::new() - }; - - let new_part_of_speech = !parts_of_speech.is_empty(); - - let index_str = format!("{}.",(index + 1)); - let mut tags = format_sense_tags(value); - let info = format_sense_info(value); - - if !info.is_empty() && !tags.is_empty() { - tags.push(','); - } - - (format!( - "{}{} {}{}{}", - parts_of_speech, - index_str.bright_black(), - english_definiton - .iter() - .map(value_to_str) - .collect::>() - .join(", "), - tags.bright_black(), - info.bright_black(), - ), new_part_of_speech) -} - -/// Format tags from a whole meaning -fn format_result_tags(value: &Value) -> String { - let mut builder = String::new(); - - let is_common_val = value.get("is_common"); - if is_common_val.is_some() && value_to_bool(is_common_val.unwrap()) { - builder.push_str(&"(common) ".bright_green().to_string()); - } - - if let Some(jlpt) = value.get("jlpt") { - /* - * The jisho API actually returns an array of all of JLTP levels for each alternative of a word - * Since the main one is always at index 0, we take that for formatting - */ - let jlpt = value_to_arr(jlpt); - if !jlpt.is_empty() { - let jlpt = value_to_str(jlpt.get(0).unwrap()) - .replace("jlpt-", "") - .to_uppercase(); - builder.push_str(&format!("({}) ", jlpt.bright_blue())); - } - } - - builder -} - -/// Format tags from a single sense entry -fn format_sense_tags(value: &Value) -> String { - let mut builder = String::new(); - - if let Some(tags) = value.get("tags") { - let tags = value_to_arr(tags); - - if let Some(tag) = tags.get(0) { - let t = format_sense_tag(value_to_str(tag)); - builder += &format!(" {}", t.as_str()); - } - - for tag in tags.get(1).iter() { - let t = format_sense_tag(value_to_str(tag)); - builder += &format!(", {}", t.as_str()); - } - } - builder -} - -fn format_sense_tag(tag: &str) -> String { - match tag { - "Usually written using kana alone" => "UK".to_string(), - s => s.to_string(), - } -} - -fn format_sense_info(value: &Value) -> String { - let mut builder = String::new(); - if let Some(all_info) = value.get("info") { - let all_info = value_to_arr(all_info); - - if let Some(info) = all_info.get(0) { - builder += &format!(" {}", value_to_str(info)); - } - - for info in all_info.get(1).iter() { - builder += &format!(", {}", value_to_str(info)); - } - } - builder -} - -// -// --- Value helper -// - -fn value_to_bool(value: &Value) -> bool { - match value { - Value::Bool(b) => *b, - _ => unreachable!(), - } -} - -fn value_to_str(value: &Value) -> &str { - match value { - Value::String(s) => s, - _ => unreachable!(), - } -} - -fn value_to_arr(value: &Value) -> &Vec { - match value { - Value::Array(a) => a, - _ => unreachable!(), - } -} - -fn parse_args() -> Options { - let mut options = Options::default(); +fn parse_args() -> aux::Options { + let mut options = aux::Options::default(); let mut query_vec: Vec = Vec::new(); { let mut ap = ArgumentParser::new(); diff --git a/src/word_search.rs b/src/word_search.rs new file mode 100644 index 0000000..24a4b5b --- /dev/null +++ b/src/word_search.rs @@ -0,0 +1,211 @@ +use crate::aux::*; + +use serde_json::Value; +use colored::*; + +pub fn word_search(options: &Options, body: Value, query: &String, mut output: &mut String) -> Option { + let mut lines_output = 0; + + // Try to get the data json-object + let body = value_to_arr({ + let body = body.get("data"); + + if body.is_none() { + return None; + } + + body.unwrap() + }); + + /* Iterate over meanings and print them */ + for (i, entry) in body.iter().enumerate() { + if i >= options.limit && options.limit != 0 { + break; + } + if let Some(r) = print_item(&query, entry, &mut output) { + lines_output += r; + } + + output.push('\n'); + lines_output += 1; + } + output.pop(); + lines_output = lines_output.saturating_sub(1); + + Some(lines_output) +} + +fn print_item(query: &str, value: &Value, output: &mut String) -> Option { + let japanese = value_to_arr(value.get("japanese")?); + let main_form = japanese.get(0)?; + let mut num_of_lines = 0; + + *output += &format!("{} {}\n", format_form(query, main_form)?, format_result_tags(value)); + + /* Print senses */ + let senses = value_to_arr(value.get("senses")?); + let mut prev_parts_of_speech = String::new(); + + for (i, sense) in senses.iter().enumerate() { + let (sense_str, new_part_of_speech) = format_sense(sense, i, &mut prev_parts_of_speech); + if !sense_str.is_empty() { + /* + * If the current meaning of our word is a different part of speech + * (e.g. previous meaning was 'Noun' and the current is 'Adverb'), an extra line will be + * printed with this information + */ + if new_part_of_speech { + num_of_lines += 1; + } + + *output += &format!(" {}\n", sense_str); + } + } + + /* Print alternative readings and kanji usage */ + if let Some(form) = japanese.get(1) { + num_of_lines += 2; + + *output += &format!(" {}", "Other forms\n".bright_blue()); + *output += &format!(" {}", format_form(query, form)?); + + for i in 2..japanese.len() { + *output += &format!(", {}", format_form(query, japanese.get(i)?)?); + } + output.push('\n'); + } + + num_of_lines += senses.len() + 1; + Some(num_of_lines) +} + +fn format_form(query: &str, form: &Value) -> Option { + let reading = form + .get("reading") + .map(value_to_str) + .unwrap_or(query); + + let word = value_to_str(form.get("word").unwrap_or(form.get("reading")?)); + + Some(format!("{}[{}]", word, reading)) +} + +fn format_sense(value: &Value, index: usize, prev_parts_of_speech: &mut String) -> (String, bool) { + let english_definitons = value.get("english_definitions"); + let parts_of_speech = value.get("parts_of_speech"); + if english_definitons.is_none() { + return ("".to_owned(), false); + } + + let english_definiton = value_to_arr(english_definitons.unwrap()); + + let parts_of_speech = if let Some(parts_of_speech) = parts_of_speech { + let parts = value_to_arr(parts_of_speech) + .iter() + .map(value_to_str) + .collect::>() + .join(", "); + + /* Do not repeat a meaning's part of speech if it is the same as the previous meaning */ + if !parts.is_empty() && parts != *prev_parts_of_speech { + *prev_parts_of_speech = parts.clone(); + format!("{}\n ", parts.bright_blue()) + } else { + String::new() + } + } else { + String::new() + }; + + let new_part_of_speech = !parts_of_speech.is_empty(); + + let index_str = format!("{}.",(index + 1)); + let mut tags = format_sense_tags(value); + let info = format_sense_info(value); + + if !info.is_empty() && !tags.is_empty() { + tags.push(','); + } + + (format!( + "{}{} {}{}{}", + parts_of_speech, + index_str.bright_black(), + english_definiton + .iter() + .map(value_to_str) + .collect::>() + .join(", "), + tags.bright_black(), + info.bright_black(), + ), new_part_of_speech) +} + +/// Format tags from a whole meaning +fn format_result_tags(value: &Value) -> String { + let mut builder = String::new(); + + let is_common_val = value.get("is_common"); + if is_common_val.is_some() && value_to_bool(is_common_val.unwrap()) { + builder.push_str(&"(common) ".bright_green().to_string()); + } + + if let Some(jlpt) = value.get("jlpt") { + /* + * The jisho API actually returns an array of all of JLTP levels for each alternative of a word + * Since the main one is always at index 0, we take that for formatting + */ + let jlpt = value_to_arr(jlpt); + if !jlpt.is_empty() { + let jlpt = value_to_str(jlpt.get(0).unwrap()) + .replace("jlpt-", "") + .to_uppercase(); + builder.push_str(&format!("({}) ", jlpt.bright_blue())); + } + } + + builder +} + +/// Format tags from a single sense entry +fn format_sense_tags(value: &Value) -> String { + let mut builder = String::new(); + + if let Some(tags) = value.get("tags") { + let tags = value_to_arr(tags); + + if let Some(tag) = tags.get(0) { + let t = format_sense_tag(value_to_str(tag)); + builder += &format!(" {}", t.as_str()); + } + + for tag in tags.get(1).iter() { + let t = format_sense_tag(value_to_str(tag)); + builder += &format!(", {}", t.as_str()); + } + } + builder +} + +fn format_sense_tag(tag: &str) -> String { + match tag { + "Usually written using kana alone" => "UK".to_string(), + s => s.to_string(), + } +} + +fn format_sense_info(value: &Value) -> String { + let mut builder = String::new(); + if let Some(all_info) = value.get("info") { + let all_info = value_to_arr(all_info); + + if let Some(info) = all_info.get(0) { + builder += &format!(" {}", value_to_str(info)); + } + + for info in all_info.get(1).iter() { + builder += &format!(", {}", value_to_str(info)); + } + } + builder +}