use crate::browser::Browser;
use crate::params::SearchParams;
use napi_derive::napi;
fn block_on<F, T, E>(future: F) -> napi::Result<T>
where
F: std::future::Future<Output = Result<T, E>>,
E: std::fmt::Display,
{
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| napi::Error::from_reason(e.to_string()))?
.block_on(future)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
fn parse_region(code: &str) -> napi::Result<crate::params::Region> {
use crate::params::Region;
match code {
"xa-ar" => Ok(Region::XaAr),
"xa-en" => Ok(Region::XaEn),
"ar-es" => Ok(Region::ArEs),
"au-en" => Ok(Region::AuEn),
"at-de" => Ok(Region::AtDe),
"be-fr" => Ok(Region::BeFr),
"be-nl" => Ok(Region::BeNl),
"br-pt" => Ok(Region::BrPt),
"bg-bg" => Ok(Region::BgBg),
"ca-en" => Ok(Region::CaEn),
"ca-fr" => Ok(Region::CaFr),
"ct-ca" => Ok(Region::CtCa),
"cl-es" => Ok(Region::ClEs),
"cn-zh" => Ok(Region::CnZh),
"co-es" => Ok(Region::CoEs),
"hr-hr" => Ok(Region::HrHr),
"cz-cs" => Ok(Region::CzCs),
"dk-da" => Ok(Region::DkDa),
"ee-et" => Ok(Region::EeEt),
"fi-fi" => Ok(Region::FiFi),
"fr-fr" => Ok(Region::FrFr),
"de-de" => Ok(Region::DeDe),
"gr-el" => Ok(Region::GrEl),
"hk-tzh" => Ok(Region::HkTzh),
"hu-hu" => Ok(Region::HuHu),
"in-en" => Ok(Region::InEn),
"id-id" => Ok(Region::IdId),
"id-en" => Ok(Region::IdEn),
"ie-en" => Ok(Region::IeEn),
"il-he" => Ok(Region::IlHe),
"it-it" => Ok(Region::ItIt),
"jp-jp" => Ok(Region::JpJp),
"kr-kr" => Ok(Region::KrKr),
"lv-lv" => Ok(Region::LvLv),
"lt-lt" => Ok(Region::LtLt),
"xl-es" => Ok(Region::XlEs),
"my-ms" => Ok(Region::MyMs),
"my-en" => Ok(Region::MyEn),
"mx-es" => Ok(Region::MxEs),
"nl-nl" => Ok(Region::NlNl),
"nz-en" => Ok(Region::NzEn),
"no-no" => Ok(Region::NoNo),
"pe-es" => Ok(Region::PeEs),
"ph-en" => Ok(Region::PhEn),
"ph-tl" => Ok(Region::PhTl),
"pl-pl" => Ok(Region::PlPl),
"pt-pt" => Ok(Region::PtPt),
"ro-ro" => Ok(Region::RoRo),
"ru-ru" => Ok(Region::RuRu),
"sg-en" => Ok(Region::SgEn),
"sk-sk" => Ok(Region::SkSk),
"sl-sl" => Ok(Region::SlSl),
"za-en" => Ok(Region::ZaEn),
"es-es" => Ok(Region::EsEs),
"se-sv" => Ok(Region::SeSv),
"ch-de" => Ok(Region::ChDe),
"ch-fr" => Ok(Region::ChFr),
"ch-it" => Ok(Region::ChIt),
"tw-tzh" => Ok(Region::TwTzh),
"th-th" => Ok(Region::ThTh),
"tr-tr" => Ok(Region::TrTr),
"ua-uk" => Ok(Region::UaUk),
"uk-en" => Ok(Region::UkEn),
"us-en" => Ok(Region::UsEn),
"ue-es" => Ok(Region::UeEs),
"ve-es" => Ok(Region::VeEs),
"vn-vi" => Ok(Region::VnVi),
"wt-wt" => Ok(Region::WtWt),
other => Err(napi::Error::from_reason(format!(
"Unknown region code: '{other}'. See https://duckduckgo.com/duckduckgo-help-pages/settings/params/ for valid values."
))),
}
}
#[napi(object)]
pub struct LiteSearchResult {
pub title: String,
pub url: String,
pub snippet: String,
}
#[napi(object)]
pub struct ImageResult {
pub title: String,
pub image: String,
pub thumbnail: String,
pub url: String,
pub height: u32,
pub width: u32,
pub source: String,
}
#[napi(object)]
pub struct NewsResult {
pub date: String,
pub title: String,
pub body: String,
pub url: String,
pub image: Option<String>,
pub source: String,
}
#[napi(object)]
pub struct RelatedTopic {
pub text: Option<String>,
pub first_url: Option<String>,
pub url: Option<String>,
pub result: Option<String>,
}
#[napi(object)]
pub struct InstantAnswerResponse {
pub heading: Option<String>,
pub abstract_text: Option<String>,
pub abstract_source: Option<String>,
pub abstract_url: Option<String>,
pub answer: Option<String>,
pub answer_type: Option<String>,
pub definition: Option<String>,
pub definition_source: Option<String>,
pub definition_url: Option<String>,
pub entity: Option<String>,
pub image: Option<String>,
pub redirect: Option<String>,
pub response_type: String,
pub related_topics: Vec<RelatedTopic>,
}
#[napi(js_name = "SearchParams")]
pub struct NapiSearchParams {
pub(crate) inner: SearchParams,
}
#[napi]
impl NapiSearchParams {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: SearchParams::default(),
}
}
#[napi]
pub fn region(&self, code: String) -> napi::Result<NapiSearchParams> {
let region = parse_region(&code)?;
Ok(Self {
inner: self.inner.clone().region(region),
})
}
#[napi]
pub fn safe_search(&self, level: String) -> napi::Result<NapiSearchParams> {
use crate::params::SafeSearch;
let safe = match level.as_str() {
"on" => SafeSearch::On,
"moderate" => SafeSearch::Moderate,
"off" => SafeSearch::Off,
other => {
return Err(napi::Error::from_reason(format!(
"Unknown safe search level: '{other}'. Expected 'on', 'moderate', or 'off'."
)));
}
};
Ok(Self {
inner: self.inner.clone().safe_search(safe),
})
}
#[napi]
pub fn theme(&self, name: String) -> NapiSearchParams {
use crate::params::Theme;
let theme = match name.as_str() {
"default" => Theme::Default,
"contrast" => Theme::Contrast,
"retro" => Theme::Retro,
"dark" => Theme::Dark,
"terminal" => Theme::Terminal,
other => Theme::Custom(other.to_string()),
};
Self {
inner: self.inner.clone().theme(theme),
}
}
#[napi]
pub fn source(&self, src: String) -> NapiSearchParams {
Self {
inner: self.inner.clone().source(src),
}
}
#[napi]
pub fn header_color(&self, color: String) -> NapiSearchParams {
Self {
inner: self.inner.clone().header_color(color),
}
}
#[napi]
pub fn url_color(&self, color: String) -> NapiSearchParams {
Self {
inner: self.inner.clone().url_color(color),
}
}
#[napi]
pub fn background_color(&self, color: String) -> NapiSearchParams {
Self {
inner: self.inner.clone().background_color(color),
}
}
#[napi]
pub fn text_color(&self, color: String) -> NapiSearchParams {
Self {
inner: self.inner.clone().text_color(color),
}
}
#[napi]
pub fn link_color(&self, color: String) -> NapiSearchParams {
Self {
inner: self.inner.clone().link_color(color),
}
}
#[napi]
pub fn visited_link_color(&self, color: String) -> NapiSearchParams {
Self {
inner: self.inner.clone().visited_link_color(color),
}
}
#[napi]
pub fn to_query_pairs(&self) -> Vec<Vec<String>> {
self.inner
.to_query_pairs()
.into_iter()
.map(|(k, v)| vec![k.to_string(), v])
.collect()
}
}
#[napi(js_name = "Browser")]
pub struct NapiBrowser {
inner: Browser,
}
#[napi]
impl NapiBrowser {
#[napi(constructor)]
pub fn new(
user_agent: Option<String>,
cookie_store: Option<bool>,
proxy: Option<String>,
) -> napi::Result<Self> {
let mut builder = Browser::builder();
if let Some(agent) = user_agent {
builder = builder.user_agent(agent);
}
if cookie_store.unwrap_or(false) {
builder = builder.cookie_store(true);
}
if let Some(proxy_url) = proxy {
builder = builder.proxy(proxy_url);
}
let inner = builder
.build()
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
Ok(Self { inner })
}
#[napi]
pub fn lite_search(
&self,
query: String,
region: Option<String>,
limit: Option<u32>,
user_agent: Option<String>,
) -> napi::Result<Vec<LiteSearchResult>> {
let region = region.as_deref().unwrap_or("wt-wt");
let user_agent_str = user_agent.as_deref().unwrap_or("");
let limit = limit.map(|l| l as usize);
let results =
block_on(
self.inner
.lite_search(query.as_str(), region, limit, user_agent_str),
)?;
Ok(results
.into_iter()
.map(|r| LiteSearchResult {
title: r.title,
url: r.url,
snippet: r.snippet,
})
.collect())
}
#[napi]
pub fn images(
&self,
query: String,
region: Option<String>,
safesearch: Option<bool>,
limit: Option<u32>,
user_agent: Option<String>,
) -> napi::Result<Vec<ImageResult>> {
let region = region.as_deref().unwrap_or("wt-wt");
let safesearch = safesearch.unwrap_or(true);
let user_agent_str = user_agent.as_deref().unwrap_or("");
let limit = limit.map(|l| l as usize);
let results =
block_on(
self.inner
.images(query.as_str(), region, safesearch, limit, user_agent_str),
)?;
Ok(results
.into_iter()
.map(|r| ImageResult {
title: r.title,
image: r.image,
thumbnail: r.thumbnail,
url: r.url,
height: r.height,
width: r.width,
source: r.source,
})
.collect())
}
#[napi]
pub fn news(
&self,
query: String,
region: Option<String>,
safesearch: Option<bool>,
limit: Option<u32>,
user_agent: Option<String>,
) -> napi::Result<Vec<NewsResult>> {
let region = region.as_deref().unwrap_or("wt-wt");
let safesearch = safesearch.unwrap_or(true);
let user_agent_str = user_agent.as_deref().unwrap_or("");
let limit = limit.map(|l| l as usize);
let results =
block_on(
self.inner
.news(query.as_str(), region, safesearch, limit, user_agent_str),
)?;
Ok(results
.into_iter()
.map(|r| NewsResult {
date: r.date,
title: r.title,
body: r.body,
url: r.url,
image: r.image,
source: r.source,
})
.collect())
}
#[napi]
pub fn instant_answer(
&self,
query: String,
params: Option<&NapiSearchParams>,
) -> napi::Result<InstantAnswerResponse> {
let path = format!("?q={}", urlencoding::encode(&query));
let search_params = params.map(|p| &p.inner);
let resp = block_on(self.inner.get_api_response(&path, search_params))?;
Ok(InstantAnswerResponse {
heading: resp.heading,
abstract_text: resp.abstract_text,
abstract_source: resp.abstract_source,
abstract_url: resp.abstract_url,
answer: resp.answer,
answer_type: resp.answer_type,
definition: resp.definition,
definition_source: resp.definition_source,
definition_url: resp.definition_url,
entity: resp.entity,
image: resp.image,
redirect: resp.redirect,
response_type: resp.r#type,
related_topics: resp
.related_topics
.into_iter()
.map(|t| RelatedTopic {
text: t.text,
first_url: t.first_url,
url: t.url,
result: t.result,
})
.collect(),
})
}
}
#[napi]
pub fn run_cli(args: Vec<String>) {
tokio::runtime::Runtime::new()
.expect("Failed to create tokio runtime")
.block_on(async move {
if let Err(e) = crate::app::run_cli_entry(args).await {
eprintln!("Error: {}", e);
std::process::exit(1);
}
});
}