diff --git a/config-base.toml b/config-base.toml deleted file mode 100644 index 605922a..0000000 --- a/config-base.toml +++ /dev/null @@ -1,35 +0,0 @@ -# This is the config that's used as a fallback when a field is missing from the user's config.toml. - -bind = "0.0.0.0:28019" - -[ui] -show_engine_list_separator = false -show_version_info = false - -[image_search] -enabled = false -show_engines = true -proxy = { enabled = true, max_download_size = 10_000_000 } - -[engines] -google = { weight = 1.05 } -bing = { weight = 1.0 } -brave = { weight = 1.25 } - -google_scholar = { enabled = false, weight = 0.50 } -rightdao = { enabled = false, weight = 0.10 } -stract = { enabled = false, weight = 0.15 } -yep = { enabled = false, weight = 0.10 } - -# calculators (give them a high weight so they're always the first thing in autocomplete) -numbat = { weight = 10 } -fend = { enabled = false, weight = 10 } - -[engines.marginalia] -args = { profile = "corpo", js = "default", adtech = "default" } -weight = 0.15 - -[engines.mdn] -# the number of sections of text to display -# 1 is just the summary and 0 is no limit -max_sections = 1 diff --git a/src/config.rs b/src/config.rs index ce8f5d8..87454ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,35 +6,86 @@ use tracing::info; use crate::engines::Engine; -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct Config { pub bind: SocketAddr, - #[serde(default)] pub ui: UiConfig, - #[serde(default)] pub image_search: ImageSearchConfig, - #[serde(default)] pub engines: EnginesConfig, } -#[derive(Deserialize, Debug, Default)] +#[derive(Deserialize, Debug)] +pub struct PartialConfig { + pub bind: Option, + pub ui: Option, + pub image_search: Option, + pub engines: Option, +} + +impl Config { + pub fn overlay(&mut self, partial: PartialConfig) { + self.bind = partial.bind.unwrap_or(self.bind); + self.ui.overlay(partial.ui.unwrap_or_default()); + self.image_search + .overlay(partial.image_search.unwrap_or_default()); + self.engines.overlay(partial.engines.unwrap_or_default()); + } +} + +#[derive(Debug)] pub struct UiConfig { + pub show_engine_list_separator: bool, + pub show_version_info: bool, +} + +#[derive(Deserialize, Debug, Default)] +pub struct PartialUiConfig { #[serde(default)] pub show_engine_list_separator: Option, #[serde(default)] pub show_version_info: Option, } -#[derive(Deserialize, Debug, Default)] +impl UiConfig { + pub fn overlay(&mut self, partial: PartialUiConfig) { + self.show_engine_list_separator = partial + .show_engine_list_separator + .unwrap_or(self.show_engine_list_separator); + self.show_version_info = partial.show_version_info.unwrap_or(self.show_version_info); + } +} + +#[derive(Debug)] pub struct ImageSearchConfig { - pub enabled: Option, - pub show_engines: Option, - #[serde(default)] + pub enabled: bool, + pub show_engines: bool, pub proxy: ImageProxyConfig, } #[derive(Deserialize, Debug, Default)] +pub struct PartialImageSearchConfig { + pub enabled: Option, + pub show_engines: Option, + #[serde(default)] + pub proxy: PartialImageProxyConfig, +} + +impl ImageSearchConfig { + pub fn overlay(&mut self, partial: PartialImageSearchConfig) { + self.enabled = partial.enabled.unwrap_or(self.enabled); + self.show_engines = partial.show_engines.unwrap_or(self.show_engines); + self.proxy.overlay(partial.proxy); + } +} + +#[derive(Debug)] pub struct ImageProxyConfig { + pub enabled: bool, + pub max_download_size: u64, +} + +#[derive(Deserialize, Debug, Default)] +pub struct PartialImageProxyConfig { /// Whether we should proxy remote images through our server. This is mostly /// a privacy feature. pub enabled: Option, @@ -42,38 +93,87 @@ pub struct ImageProxyConfig { pub max_download_size: Option, } -#[derive(Deserialize, Debug, Default)] +impl ImageProxyConfig { + pub fn overlay(&mut self, partial: PartialImageProxyConfig) { + self.enabled = partial.enabled.unwrap_or(self.enabled); + self.max_download_size = partial.max_download_size.unwrap_or(self.max_download_size); + } +} + +#[derive(Debug)] pub struct EnginesConfig { + pub map: HashMap, +} + +#[derive(Deserialize, Debug, Default)] +pub struct PartialEnginesConfig { #[serde(flatten)] - pub map: HashMap, + pub map: HashMap, } #[derive(Deserialize, Clone, Debug)] #[serde(untagged)] -pub enum DefaultableEngineConfig { +pub enum PartialDefaultableEngineConfig { Boolean(bool), - Full(FullEngineConfig), + Full(PartialEngineConfig), } -#[derive(Deserialize, Clone, Debug)] -pub struct FullEngineConfig { - #[serde(default = "fn_true")] - pub enabled: bool, +impl EnginesConfig { + pub fn overlay(&mut self, partial: PartialEnginesConfig) { + for (key, value) in partial.map { + let full = match value { + PartialDefaultableEngineConfig::Boolean(enabled) => PartialEngineConfig { + enabled: Some(enabled), + ..Default::default() + }, + PartialDefaultableEngineConfig::Full(full) => full, + }; + if let Some(existing) = self.map.get_mut(&key) { + existing.overlay(full); + } else { + let mut new = EngineConfig::default(); + new.overlay(full); + self.map.insert(key, new); + } + } + } - /// The priority of this engine relative to the other engines. The default - /// is 1, and a value of 0 is treated as the default. - #[serde(default)] + pub fn get(&self, engine: Engine) -> &EngineConfig { + self.map.get(&engine).unwrap_or(&DEFAULT_ENGINE_CONFIG_REF) + } +} + +#[derive(Debug)] +pub struct EngineConfig { + pub enabled: bool, pub weight: f64, + pub extra: toml::Table, +} + +#[derive(Deserialize, Clone, Debug, Default)] +pub struct PartialEngineConfig { + #[serde(default)] + pub enabled: Option, + + /// The priority of this engine relative to the other engines. + #[serde(default)] + pub weight: Option, /// Per-engine configs. These are parsed at request time. #[serde(flatten)] - #[serde(default)] pub extra: toml::Table, } +impl EngineConfig { + pub fn overlay(&mut self, partial: PartialEngineConfig) { + self.enabled = partial.enabled.unwrap_or(self.enabled); + self.weight = partial.weight.unwrap_or(self.weight); + self.extra.extend(partial.extra); + } +} + impl Config { pub fn read_or_create(config_path: &Path) -> eyre::Result { - let base_config_str = include_str!("../config-base.toml"); - let mut config: Config = toml::from_str(base_config_str)?; + let mut config = Config::default(); if !config_path.exists() { info!("No config found, creating one at {config_path:?}"); @@ -81,122 +181,38 @@ impl Config { fs::write(config_path, default_config_str)?; } - let given_config = toml::from_str::(&fs::read_to_string(config_path)?)?; - config.update(given_config); + let given_config = toml::from_str::(&fs::read_to_string(config_path)?)?; + config.overlay(given_config); Ok(config) } - - // Update the current config with the given config. This is used to make it so - // the config-base.toml is always used as a fallback if the user decides to - // use the default for something. - pub fn update(&mut self, new: Config) { - self.bind = new.bind; - self.ui.update(new.ui); - self.image_search.update(new.image_search); - self.engines.update(new.engines); - } } -impl UiConfig { - pub fn update(&mut self, new: UiConfig) { - self.show_engine_list_separator = new - .show_engine_list_separator - .or(self.show_engine_list_separator); - assert_ne!(self.show_engine_list_separator, None); - self.show_version_info = new.show_version_info.or(self.show_version_info); - assert_ne!(self.show_version_info, None); - } -} +// +// DEFAULTS +// -impl ImageSearchConfig { - pub fn update(&mut self, new: ImageSearchConfig) { - self.enabled = new.enabled.or(self.enabled); - assert_ne!(self.enabled, None); - self.show_engines = new.show_engines.or(self.show_engines); - assert_ne!(self.show_engines, None); - self.proxy.update(new.proxy); - } -} - -impl ImageProxyConfig { - pub fn update(&mut self, new: ImageProxyConfig) { - self.enabled = new.enabled.or(self.enabled); - assert_ne!(self.enabled, None); - self.max_download_size = new.max_download_size.or(self.max_download_size); - assert_ne!(self.max_download_size, None); - } -} - -static DEFAULT_ENABLED_FULL_ENGINE_CONFIG: Lazy = - Lazy::new(FullEngineConfig::default); -static DEFAULT_DISABLED_FULL_ENGINE_CONFIG: Lazy = - Lazy::new(|| FullEngineConfig { - enabled: false, - ..Default::default() - }); - -impl EnginesConfig { - pub fn get(&self, engine: Engine) -> &FullEngineConfig { - match self.map.get(&engine) { - Some(engine_config) => match engine_config { - DefaultableEngineConfig::Boolean(enabled) => { - if *enabled { - &DEFAULT_ENABLED_FULL_ENGINE_CONFIG - } else { - &DEFAULT_DISABLED_FULL_ENGINE_CONFIG - } - } - DefaultableEngineConfig::Full(full) => full, - }, - None => &DEFAULT_ENABLED_FULL_ENGINE_CONFIG, - } - } - - pub fn update(&mut self, new: Self) { - for (key, new) in new.map { - if let Some(existing) = self.map.get_mut(&key) { - existing.update(new); - } else { - self.map.insert(key, new); - } - } - } -} - -impl DefaultableEngineConfig { - pub fn update(&mut self, new: Self) { - let mut self_full = FullEngineConfig::from(self.clone()); - let other_full = FullEngineConfig::from(new); - self_full.update(other_full); - *self = DefaultableEngineConfig::Full(self_full); - } -} - -impl Default for DefaultableEngineConfig { +impl Default for Config { fn default() -> Self { - Self::Boolean(true) - } -} - -// serde expects a function as the default, this just exists so "enabled" is -// always true by default -fn fn_true() -> bool { - true -} - -impl From for FullEngineConfig { - fn from(config: DefaultableEngineConfig) -> Self { - match config { - DefaultableEngineConfig::Boolean(enabled) => Self { - enabled, - ..Default::default() + Config { + bind: "0.0.0.0:28019".parse().unwrap(), + ui: UiConfig { + show_engine_list_separator: false, + show_version_info: false, }, - DefaultableEngineConfig::Full(full) => full, + image_search: ImageSearchConfig { + enabled: false, + show_engines: true, + proxy: ImageProxyConfig { + enabled: true, + max_download_size: 10_000_000, + }, + }, + engines: EnginesConfig::default(), } } } -impl Default for FullEngineConfig { +impl Default for EngineConfig { fn default() -> Self { Self { enabled: true, @@ -205,13 +221,93 @@ impl Default for FullEngineConfig { } } } - -impl FullEngineConfig { - pub fn update(&mut self, new: Self) { - self.enabled = new.enabled; - if new.weight != 0. { - self.weight = new.weight; +static DEFAULT_ENGINE_CONFIG_REF: Lazy = Lazy::new(EngineConfig::default); +impl EngineConfig { + pub fn new() -> Self { + Self::default() + } + pub fn with_weight(self, weight: f64) -> Self { + Self { weight, ..self } + } + pub fn disabled(self) -> Self { + Self { + enabled: false, + ..self } - self.extra = new.extra; + } + pub fn with_extra(self, extra: toml::Table) -> Self { + Self { extra, ..self } + } +} + +impl Default for EnginesConfig { + fn default() -> Self { + use toml::value::Value; + + let mut map = HashMap::new(); + // engines are enabled by default, so engines that aren't listed here are + // enabled + + // main search engines + map.insert(Engine::Google, EngineConfig::new().with_weight(1.05)); + map.insert(Engine::Bing, EngineConfig::new().with_weight(1.0)); + map.insert(Engine::Brave, EngineConfig::new().with_weight(1.25)); + map.insert( + Engine::Marginalia, + EngineConfig::new().with_weight(0.15).with_extra( + vec![( + "args".to_string(), + Value::Table( + vec![ + ("profile".to_string(), Value::String("corpo".to_string())), + ("js".to_string(), Value::String("default".to_string())), + ("adtech".to_string(), Value::String("default".to_string())), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect(), + ), + ); + + // additional search engines + map.insert( + Engine::GoogleScholar, + EngineConfig::new().with_weight(0.50).disabled(), + ); + map.insert( + Engine::RightDao, + EngineConfig::new().with_weight(0.10).disabled(), + ); + map.insert( + Engine::Stract, + EngineConfig::new().with_weight(0.15).disabled(), + ); + map.insert( + Engine::Yep, + EngineConfig::new().with_weight(0.10).disabled(), + ); + + // calculators (give them a high weight so they're always the first thing in + // autocomplete) + map.insert(Engine::Numbat, EngineConfig::new().with_weight(10.0)); + map.insert( + Engine::Fend, + EngineConfig::new().with_weight(10.0).disabled(), + ); + + // other engines + map.insert( + Engine::Mdn, + EngineConfig::new().with_extra( + vec![("max_sections".to_string(), Value::Integer(1))] + .into_iter() + .collect(), + ), + ); + + Self { map } } } diff --git a/src/engines/mod.rs b/src/engines/mod.rs index 3d42621..028dccf 100644 --- a/src/engines/mod.rs +++ b/src/engines/mod.rs @@ -537,7 +537,7 @@ pub async fn search( SearchTab::All => { make_requests(query, progress_tx, start_time, &send_engine_progress_update).await? } - SearchTab::Images if query.config.image_search.enabled.unwrap() => { + SearchTab::Images if query.config.image_search.enabled => { make_image_requests(query, progress_tx, start_time, &send_engine_progress_update) .await? } diff --git a/src/web/image_proxy.rs b/src/web/image_proxy.rs index fae3f08..22f1862 100644 --- a/src/web/image_proxy.rs +++ b/src/web/image_proxy.rs @@ -15,7 +15,7 @@ pub async fn route( ) -> Response { let image_search_config = &config.image_search; let proxy_config = &image_search_config.proxy; - if !image_search_config.enabled.unwrap() || !proxy_config.enabled.unwrap() { + if !image_search_config.enabled || !proxy_config.enabled { return (StatusCode::FORBIDDEN, "Image proxy is disabled").into_response(); }; let url = params.get("url").cloned().unwrap_or_default(); @@ -36,7 +36,7 @@ pub async fn route( } }; - let max_size = proxy_config.max_download_size.unwrap(); + let max_size = proxy_config.max_download_size; if res.content_length().unwrap_or_default() > max_size { return (StatusCode::PAYLOAD_TOO_LARGE, "Image too large").into_response(); diff --git a/src/web/index.rs b/src/web/index.rs index 46134e9..ed41aa5 100644 --- a/src/web/index.rs +++ b/src/web/index.rs @@ -33,7 +33,7 @@ pub async fn index(State(config): State>) -> impl IntoResponse { input type="submit" value="Search"; } } - @if config.ui.show_version_info.unwrap() { + @if config.ui.show_version_info { span."version-info" { @if COMMIT_HASH == "unknown" || COMMIT_HASH_SHORT == "unknown" { "Version " diff --git a/src/web/search.rs b/src/web/search.rs index 3553d38..bf3c31d 100644 --- a/src/web/search.rs +++ b/src/web/search.rs @@ -40,7 +40,7 @@ fn render_beginning_of_html(search: &SearchQuery) -> String { input #"search-input" type="text" name="q" placeholder="Search" value=(search.query) autofocus onfocus="this.select()" autocomplete="off"; input type="submit" value="Search"; } - @if search.config.image_search.enabled.unwrap() { + @if search.config.image_search.enabled { div.search-tabs { @if search.tab == SearchTab::All { span.search-tab.selected { "All" } } @else { a.search-tab href={ "?q=" (search.query) } { "All" } } @@ -105,11 +105,11 @@ fn render_engine_progress_update( pub fn render_engine_list(engines: &[engines::Engine], config: &Config) -> PreEscaped { let mut html = String::new(); for (i, engine) in engines.iter().enumerate() { - if config.ui.show_engine_list_separator.unwrap() && i > 0 { + if config.ui.show_engine_list_separator && i > 0 { html.push_str(" · "); } let raw_engine_id = &engine.id(); - let engine_id = if config.ui.show_engine_list_separator.unwrap() { + let engine_id = if config.ui.show_engine_list_separator { raw_engine_id.replace('_', " ") } else { raw_engine_id.to_string() diff --git a/src/web/search/images.rs b/src/web/search/images.rs index 846d96d..e50fff5 100644 --- a/src/web/search/images.rs +++ b/src/web/search/images.rs @@ -21,7 +21,7 @@ fn render_image_result( config: &Config, ) -> PreEscaped { let original_image_src = &result.result.image_url; - let image_src = if config.image_search.proxy.enabled.unwrap() { + let image_src = if config.image_search.proxy.enabled { // serialize url params let escaped_param = url::form_urlencoded::byte_serialize(original_image_src.as_bytes()).collect::(); @@ -40,7 +40,7 @@ fn render_image_result( span.image-result-page-url.search-result-url { (result.result.page_url) } span.image-result-title { (result.result.title) } } - @if config.image_search.show_engines.unwrap() { + @if config.image_search.show_engines { {(render_engine_list(&result.engines.iter().copied().collect::>(), &config))} } }