#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
use core::time::Duration;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use anyhow::{Context, anyhow};
use clap::{Arg, ArgAction, command};
use directories::ProjectDirs;
use gloss_word::{compile_results, get_response_text, get_section_vec, pandoc_primary, take_chunk};
use indicatif::{ProgressBar, ProgressStyle};
use rusqlite::Connection;
use scraper::{ElementRef, Selector};
#[allow(clippy::too_many_lines)]
fn main() -> Result<(), anyhow::Error> {
let matches = command!()
.arg(
Arg::new("clear-cache")
.long("clear-cache")
.help("Delete cache directory and its contents")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("etymology")
.short('e')
.long("etymology")
.help("Search for etymology instead of definition")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("fetch-update")
.short('f')
.long("fetch-update")
.help("Fetch new data; update cache if applicable")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("INPUT")
.help("The word or phrase to look up")
.required_unless_present("clear-cache"),
)
.get_matches();
let clear_cache = matches.get_flag("clear-cache");
let etym_mode = matches.get_flag("etymology");
let force_fetch = matches.get_flag("fetch-update");
let desired_word = if clear_cache {
String::new() } else {
let input_word: &String = matches.get_one("INPUT").unwrap(); input_word.to_lowercase()
};
let mut db_path = PathBuf::new();
let mut db_available = false;
let mut cache_hit = false;
if let Some(proj_dirs) = ProjectDirs::from("com", "theobeers", "gloss-word") {
let cache_dir = proj_dirs.cache_dir();
if clear_cache {
if !cache_dir.exists() {
return Err(anyhow!("Cache directory not found"));
}
trash::delete(cache_dir)?;
eprintln!("Cache directory deleted");
return Ok(());
}
if !cache_dir.exists() {
let _dir = std::fs::create_dir_all(cache_dir);
}
db_path.push(cache_dir);
db_path.push("entries.sqlite");
}
if let Ok(db_conn) = Connection::open(&db_path) {
db_available = true;
let _create_dic = db_conn.execute(
"CREATE TABLE IF NOT EXISTS dictionary (
word TEXT UNIQUE NOT NULL,
content TEXT NOT NULL
)",
[],
);
let _create_etym = db_conn.execute(
"CREATE TABLE IF NOT EXISTS etymology (
word TEXT UNIQUE NOT NULL,
content TEXT NOT NULL
)",
[],
);
if let Ok(entry) = query_db(&db_conn, &desired_word, etym_mode) {
if force_fetch {
cache_hit = true;
} else {
print!("{entry}");
return Ok(());
}
}
}
let pb = ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(80));
pb.set_style(
ProgressStyle::default_spinner()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
.template("{spinner} {msg}")
.unwrap(),
);
pb.set_message("Fetching...");
let mut lookup_url: String;
if etym_mode {
lookup_url = "https://www.etymonline.com/word/".to_owned();
lookup_url += &desired_word.replace(' ', "%20");
} else {
lookup_url = "https://www.thefreedictionary.com/".to_owned();
lookup_url += &desired_word.replace(' ', "+");
}
let response_text = get_response_text(&lookup_url)?;
let parsed_chunk = take_chunk(&response_text);
let section_vec = get_section_vec(etym_mode, &parsed_chunk);
if !section_vec.is_empty() {
let results = compile_results(etym_mode, section_vec);
let final_output = pandoc_primary(&results, etym_mode)?;
if db_available {
let _update = update_cache(
cache_hit,
db_path,
&desired_word,
etym_mode,
&final_output,
force_fetch,
);
}
pb.finish_and_clear();
print!("{final_output}");
return Ok(());
}
if etym_mode {
pb.finish_and_clear();
return Err(anyhow!("Etymology not found"));
}
let suggestions_selector = Selector::parse("ul.suggestions li").unwrap();
let suggestions_vec: Vec<ElementRef> = parsed_chunk.select(&suggestions_selector).collect();
if !suggestions_vec.is_empty() {
let mut results = String::new();
for element in &suggestions_vec {
results.push_str(&element.html());
}
let pandoc_output = pandoc_fallback(&results)?;
pb.finish_and_clear();
println!("Did you mean:\n");
print!("{pandoc_output}");
return Ok(());
}
pb.finish_and_clear();
Err(anyhow!("Definition not found"))
}
fn pandoc_fallback(results: &str) -> Result<String, anyhow::Error> {
let mut pandoc = Command::new("pandoc")
.arg("-f")
.arg("html+smart-native_divs")
.arg("-t")
.arg("plain")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.context("Failed to start pandoc process")?;
pandoc
.stdin
.as_mut()
.context("Failed to open pandoc stdin")?
.write_all(results.as_bytes())
.context("Failed to write to pandoc stdin")?;
let output = pandoc
.wait_with_output()
.context("Failed to read pandoc output")?;
let output_str = String::from_utf8_lossy(&output.stdout).into_owned();
Ok(output_str)
}
fn query_db(
db_conn: &Connection,
desired_word: &str,
etym_mode: bool,
) -> Result<String, rusqlite::Error> {
let mut query = String::new();
if etym_mode {
query.push_str("SELECT * FROM etymology WHERE word = '");
} else {
query.push_str("SELECT * FROM dictionary WHERE word = '");
}
query.push_str(desired_word);
query.push('\'');
let mut stmt = db_conn.prepare(&query)?;
let entry_content: String = stmt.query_row([], |row| row.get(1))?;
Ok(entry_content)
}
fn update_cache(
cache_hit: bool,
db_path: PathBuf,
desired_word: &str,
etym_mode: bool,
final_output: &str,
force_fetch: bool,
) -> Result<(), rusqlite::Error> {
let db_conn = Connection::open(db_path)?;
if force_fetch && cache_hit {
if etym_mode {
db_conn.execute(
"UPDATE etymology SET content = (?1) WHERE word = (?2)",
[final_output, desired_word],
)?;
} else {
db_conn.execute(
"UPDATE dictionary SET content = (?1) WHERE word = (?2)",
[final_output, desired_word],
)?;
}
} else if etym_mode {
db_conn.execute(
"INSERT INTO etymology (word, content) VALUES (?1, ?2)",
[desired_word, final_output],
)?;
} else {
db_conn.execute(
"INSERT INTO dictionary (word, content) VALUES (?1, ?2)",
[desired_word, final_output],
)?;
}
Ok(())
}