diff --git a/Cargo.lock b/Cargo.lock index bcab362..f6057ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,9 +90,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-compression" -version = "0.4.6" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ "brotli", "flate2", @@ -275,9 +275,9 @@ dependencies = [ [[package]] name = "brotli" -version = "3.4.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -286,9 +286,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -422,9 +422,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -564,9 +564,9 @@ checksum = "09078d60b5387e99317a3ecadd61b5a521deab55186e9dab76d7f0ff66838670" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", diff --git a/README b/README index 29d6c99..2b3a125 100644 --- a/README +++ b/README @@ -1,25 +1,72 @@ -a cute metasearch engine + ========== + metasearch + ========== -it sources from google, bing, brave, and a few others. + https://github.com/mat-1/metasearch2 -it's written in rust, using as little client-side javascript as possible. +---- +INFO +---- -there's a demo instance at https://s.matdoes.dev, but don't use it as your -default or rely on it, please (so i don't get ratelimited by google). +metasearch (aka metasearch2) is a cute metasearch engine. It sources its results +from Google, Bing, Brave, and several others. It's designed to be as lightweight +as possible, both on the server and client. There is no required client-side +JavaScript. -USAGE +There's a public demo instance at https://s.matdoes.dev, but please do not use +it as your default or rely on it. This is so I don't get ratelimited by Google +or other engines. Run your own instance instead! -build it with `cargo b -r`, the resulting binary will be at -`target/release/metasearch2`. +------------ +INSTALLATION +------------ -the config.toml file is created in your current working directory on the first -run of metasearch2. alternatively, you can copy the config-default.toml in the -repo and rename it to config.toml. +The easiest way to install metasearch is with `cargo install metasearch`. To get +the unstable version with the latest features, you can install it with +`cargo install --git https://github.com/mat-1/metasearch2`. -the default port is 28019. +Usage: `metasearch [config_file]` -API +The config_file argument is optional; if it's not specified then it'll be +checked at the following locations: -metasearch has a JSON API that can be enabled by setting `api = true` in your config. to use it, -just set the `Accept: application/json` header. as the api works by serializing internal structs, -it's not guaranteed to be stable across metasearch versions. + - $XDG_CONFIG_HOME/metasearch/config.toml + - $HOME/.config/metasearch/config.toml + - ./config.toml + +If no config file exists, it'll be created at the first valid path in the list. + +By default, metasearch runs on the port 28019. You are recommended to use a +reverse proxy. + +------------- +CONFIGURATION +------------- + +You can see all the default config options at `src/config.rs`. Some interesting +options you may want to change are: + + - bind - the host and port that the web server runs on, for example + `0.0.0.0:28019`. + - api - whether your instance is accessible through a JSON API. See below for + more details. + - ui.stylesheet_url - a link to a stylesheet that will be loaded alongside the + main one, for example `/themes/catppuccin-mocha.css`. + - image_search.enabled - add a tab for viewing image results for your query. + this is disabled by default as the image proxy could be used to make GET + requests to arbitrary URLs from your server. + - engines.google.weight - the ranking score multiplier for an engine, you can + modify this if you prefer the results from certain engines. + +-------- +JSON API +-------- + +metasearch has a JSON API that can be enabled by setting `api = true` in your +config. To use it, set the `Accept: application/json` header in your requests. + +For example: +curl 'http://localhost:28019/search?q=sandcats' -H 'Accept: application/json' + +The structure of the API is not guaranteed to be stable, as it relies on +serializing internal structs. It may break across versions! diff --git a/src/config.rs b/src/config.rs index 56ac643..1118d5a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,134 @@ use tracing::info; use crate::engines::Engine; +impl Default for Config { + fn default() -> Self { + Config { + bind: "0.0.0.0:28019".parse().unwrap(), + api: false, + ui: UiConfig { + show_engine_list_separator: false, + show_version_info: false, + site_name: "metasearch".to_string(), + show_settings_link: true, + stylesheet_url: "".to_string(), + stylesheet_str: "".to_string(), + }, + image_search: ImageSearchConfig { + enabled: false, + show_engines: true, + proxy: ImageProxyConfig { + enabled: true, + max_download_size: 10_000_000, + }, + }, + engines: Arc::new(EnginesConfig::default()), + } + } +} + +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 } + } +} + +impl Default for EngineConfig { + fn default() -> Self { + Self { + enabled: true, + weight: 1.0, + extra: Default::default(), + } + } +} +static DEFAULT_ENGINE_CONFIG_REF: LazyLock = LazyLock::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 + } + } + pub fn with_extra(self, extra: toml::Table) -> Self { + Self { extra, ..self } + } +} + +// + #[derive(Debug, Clone)] pub struct Config { pub bind: SocketAddr, @@ -214,133 +342,3 @@ impl Config { Ok(config) } } - -// -// DEFAULTS -// - -impl Default for Config { - fn default() -> Self { - Config { - bind: "0.0.0.0:28019".parse().unwrap(), - api: false, - ui: UiConfig { - show_engine_list_separator: false, - show_version_info: false, - site_name: "metasearch".to_string(), - show_settings_link: true, - stylesheet_url: "".to_string(), - stylesheet_str: "".to_string(), - }, - image_search: ImageSearchConfig { - enabled: false, - show_engines: true, - proxy: ImageProxyConfig { - enabled: true, - max_download_size: 10_000_000, - }, - }, - engines: Arc::new(EnginesConfig::default()), - } - } -} - -impl Default for EngineConfig { - fn default() -> Self { - Self { - enabled: true, - weight: 1.0, - extra: Default::default(), - } - } -} -static DEFAULT_ENGINE_CONFIG_REF: LazyLock = LazyLock::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 - } - } - 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/main.rs b/src/main.rs index 85812ea..f545759 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ -use std::path::Path; +use std::{ + env, + path::{Path, PathBuf}, +}; use config::Config; use tracing::error; @@ -13,10 +16,13 @@ pub mod web; async fn main() { tracing_subscriber::fmt::init(); - let config_path = std::env::args().nth(1).unwrap_or("config.toml".into()); - let config_path = Path::new(&config_path); + if env::args().any(|arg| arg == "--help" || arg == "-h" || arg == "help" || arg == "h") { + println!("Usage: metasearch [config_path]"); + return; + } - let config = match Config::read_or_create(config_path) { + let config_path = config_path(); + let config = match Config::read_or_create(&config_path) { Ok(config) => config, Err(err) => { error!("Couldn't parse config:\n{err}"); @@ -25,3 +31,47 @@ async fn main() { }; web::run(config).await; } + +fn config_path() -> PathBuf { + if let Some(config_path) = env::args().nth(1) { + return PathBuf::from(config_path); + } + + let app_name = env!("CARGO_PKG_NAME"); + + let mut default_config_dir = None; + + // $XDG_CONFIG_HOME/metasearch/config.toml + if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") { + let path = PathBuf::from(xdg_config_home) + .join(app_name) + .join("config.toml"); + if path.is_file() { + return path; + } + if default_config_dir.is_none() { + default_config_dir = Some(path); + } + } + + // $HOME/.config/metasearch/config.toml + if let Ok(home) = env::var("HOME") { + let path = PathBuf::from(home) + .join(".config") + .join(app_name) + .join("config.toml"); + if path.is_file() { + return path; + } + if default_config_dir.is_none() { + default_config_dir = Some(path); + } + } + + // ./config.toml + let path = Path::new("config.toml"); + if path.exists() { + return path.to_path_buf(); + } + default_config_dir.unwrap_or(PathBuf::from("config.toml")) +} diff --git a/src/web/assets/style.css b/src/web/assets/style.css index d2438ee..2bbecc8 100644 --- a/src/web/assets/style.css +++ b/src/web/assets/style.css @@ -448,6 +448,7 @@ h3.answer-thesaurus-category-title { .image-result { min-width: 12rem; position: relative; + flex-grow: 1; } .image-result-img-container { margin: 0 auto; @@ -459,7 +460,7 @@ h3.answer-thesaurus-category-title { } .image-result-page-anchor { display: block; - height: 2em; + height: 2.25em; } .image-result-page-url { overflow: hidden;