Yakudatsu/src/main.rs

404 lines
11 KiB
Rust

use std::{
io::{stdin, stdout, Write},
process::{Command, Stdio},
thread::{self, JoinHandle},
env,
};
use argparse::{ArgumentParser, List, Print, Store, StoreTrue};
use colored::*;
use serde_json::Value;
use atty::Stream;
macro_rules! JISHO_URL {
() => {
"https://jisho.org/api/v1/search/words?keyword={}"
};
}
#[derive(Debug, Clone)]
struct Options {
limit: usize,
query: String,
kanji: bool, // Sadly not (yet) supported by jisho.org's API
interactive: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
limit: 0,
query: String::default(),
kanji: false,
interactive: false,
}
}
}
fn main() -> Result<(), ureq::Error> {
let term_size;
if atty::is(Stream::Stdout) {
match terminal_size() {
Ok(s) => term_size = s,
Err(_e) => term_size = 0
}
} else {
term_size = 0;
}
let options = parse_args();
let mut query = {
if options.interactive {
let mut o = String::new();
while o.trim().is_empty() {
print!("=> ");
stdout().flush().unwrap();
stdin().read_line(&mut o).expect("Can't read from stdin");
}
o
} else {
options.query.clone()
}
};
loop {
let mut lines_output = 0;
let mut output = String::new();
if options.kanji {
// Open kanji page here
let threads = query
.chars()
.into_iter()
.map(|kanji| {
let kanji = kanji.clone();
thread::spawn(move || {
webbrowser::open(&format!("https://jisho.org/search/{}%23kanji", kanji))
.expect("Couldn't open browser");
})
})
.collect::<Vec<JoinHandle<()>>>();
for thread in threads {
thread.join().unwrap();
}
} else {
// 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;
}
match print_item(&query, entry, &mut output) {
Some(r) => lines_output += r,
None => continue,
}
output.push('\n');
lines_output += 1;
}
output.pop();
if lines_output > 0 {
lines_output -= 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");
match Command::new("less")
.arg("-R")
.stdin(Stdio::piped())
.spawn() {
Ok(mut process) => {
if let Err(e) = process.stdin.as_ref().unwrap().write_all(output.as_bytes()) {
panic!("couldn't pipe to less: {}", e);
}
// We don't care about the return value, only whether wait failed or not
if process.wait().is_err() {
panic!("wait() was called on non-existent child process\
- this should not be possible");
}
}
// less not found in PATH; print normally
Err(_e) => print!("{}", output)
};
} else {
print!("{}", output);
}
if !options.interactive {
break;
}
query.clear();
while query.trim().is_empty() {
print!("=> ");
stdout().flush().unwrap();
stdin()
.read_line(&mut query)
.expect("Can't read from stdin");
}
}
Ok(())
}
fn print_item(query: &str, value: &Value, output: &mut String) -> Option<usize> {
let mut aux;
let japanese = value_to_arr(value.get("japanese")?).get(0)?.to_owned();
let reading = japanese
.get("reading")
.map(|i| value_to_str(i))
.unwrap_or(query);
let word = value_to_str(japanese.get("word").unwrap_or(japanese.get("reading")?));
aux = format!("{}[{}] {}\n", word, reading, format_result_tags(value));
*output += &aux;
// Print senses
let senses = value_to_arr(value.get("senses")?);
for (i, sense) in senses.iter().enumerate() {
let sense_str = format_sense(&sense, i);
if sense_str.is_empty() {
continue;
}
aux = format!(" {}\n", sense_str);
*output += &aux;
}
Some(senses.iter().count() + 1)
}
fn format_sense(value: &Value, index: usize) -> String {
let english_definitons = value.get("english_definitions");
let parts_of_speech = value.get("parts_of_speech");
if english_definitons.is_none() {
return "".to_owned();
}
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)
.to_owned()
.iter()
.map(|i| {
let s = value_to_str(i);
match s {
"Suru verb - irregular" => "Irregular verb",
"Ichidan verb" => "iru/eru verb",
_ => {
if s.contains("Godan verb") {
"Godan verb"
} else {
s
}
}
}
})
.collect::<Vec<&str>>()
.join(", ");
if parts.is_empty() {
String::new()
} else {
format!("[{}]", parts.bright_blue())
}
} else {
String::new()
};
let tags = format_sense_tags(value);
format!(
"{}. {} {} {}",
index + 1,
english_definiton
.iter()
.map(|i| value_to_str(i))
.collect::<Vec<&str>>()
.join(", "),
tags,
parts_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") {
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().to_string()));
}
}
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);
for tag in tags {
let t = format_sense_tag(value_to_str(tag));
builder.push_str(t.as_str())
}
}
builder
}
fn format_sense_tag(tag: &str) -> String {
match tag {
"Usually written using kana alone" => "(UK)".to_string(),
s => format!("({})", s),
}
}
//
// --- 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<Value> {
match value {
Value::Array(a) => a,
_ => unreachable!(),
}
}
fn parse_args() -> Options {
let mut options = Options::default();
let mut query_vec: Vec<String> = Vec::new();
{
let mut ap = ArgumentParser::new();
ap.set_description("Use jisho.org from cli");
ap.add_option(
&["-V", "--version"],
Print(env!("CARGO_PKG_VERSION").to_string()),
"Show version",
);
ap.refer(&mut options.limit).add_option(
&["-n", "--limit"],
Store,
"Limit the amount of results",
);
ap.refer(&mut query_vec)
.add_argument("Query", List, "The query to search for");
ap.refer(&mut options.interactive).add_option(
&["-i", "--interactive"],
StoreTrue,
"Don't exit after running a query",
);
/* Uncomment when supported by jisho.org */
ap.refer(&mut options.kanji).add_option(
&["--kanji", "-k"],
StoreTrue,
"Look up a certain kanji",
);
ap.parse_args_or_exit();
}
options.query = query_vec.join(" ");
options
}
#[cfg(unix)]
fn terminal_size() -> Result<usize, i16> {
use libc::{c_ushort, ioctl, STDOUT_FILENO, TIOCGWINSZ};
unsafe {
let mut size: c_ushort = 0;
if ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut size as *mut _) != 0 {
Err(-1)
} else {
Ok(size as usize)
}
}
}
#[cfg(windows)]
fn terminal_size() -> Result<usize, i16> {
use windows_sys::Win32::System::Console::*;
unsafe {
let handle = GetStdHandle(STD_OUTPUT_HANDLE) as windows_sys::Win32::Foundation::HANDLE;
// Unlike the linux function, rust will complain if only part of the struct is sent
let mut window = CONSOLE_SCREEN_BUFFER_INFO {
dwSize: COORD { X: 0, Y: 0},
dwCursorPosition: COORD { X: 0, Y: 0},
wAttributes: 0,
dwMaximumWindowSize: COORD {X: 0, Y: 0},
srWindow: SMALL_RECT {
Top: 0,
Bottom: 0,
Left: 0,
Right: 0
}
};
if GetConsoleScreenBufferInfo(handle, &mut window) == 0 {
Err(0)
} else {
Ok((window.srWindow.Bottom - window.srWindow.Top) as usize)
}
}
}