support web search, fixes #55

This commit is contained in:
Alexander Mohr 2025-06-01 14:31:12 +02:00
parent 8abcdfcf4a
commit bf951bf0c8
8 changed files with 107 additions and 8 deletions

7
Cargo.lock generated
View file

@ -2358,6 +2358,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -2892,6 +2898,7 @@ dependencies = [
"thiserror 2.0.12",
"toml",
"tree_magic_mini",
"urlencoding",
"which",
"wl-clipboard-rs",
]

View file

@ -9,17 +9,13 @@ Worf has more features than wofi though, so there won't be 100% compatibility.
Broken compatibility with wofi is not considered a bug, but issues regarding that will be accepted if you
would rather use worf than wofi.
**While the main branch in general is stable and I am using this on a daily basis, it is not ready yet
to be used as a library, a lot of things are still moving around.**
It supports various modes:
* Math
* DRun
* File
* Ssh
* Run
* Emoji
* // WebSearch --> tracked by https://github.com/alexmohr/worf/issues/55
* Emoji
* Auto
Auto mode tries to detect the desired mode automatically, to achieve this some modes require a prefix in the search.

View file

@ -53,3 +53,4 @@ emoji = "0.2.1"
wl-clipboard-rs = "0.9.2"
notify-rust="4.11.7"
thiserror = "2.0.12"
urlencoding = "2.1.3"

View file

@ -86,6 +86,9 @@ pub enum Mode {
/// Emoji browser
Emoji,
/// Open search engine.
WebSearch,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -136,6 +139,7 @@ impl FromStr for Mode {
"math" => Ok(Mode::Math),
"ssh" => Ok(Mode::Ssh),
"emoji" => Ok(Mode::Emoji),
"websearch" => Ok(Mode::WebSearch),
"auto" => Ok(Mode::Auto),
_ => Err(Error::InvalidArgument(
format!("{s} is not a valid argument, see help for details").to_owned(),
@ -412,6 +416,11 @@ pub struct Config {
/// See `KeyDetectionType` for details.
#[clap(long = "key-detection-type")]
key_detection_type: Option<KeyDetectionType>,
/// Defines the search query to use.
/// Defaults to `<https://duckduckgo.com/?q=>`
#[clap(long = "search-query")]
search_query: Option<String>,
}
impl Config {
@ -499,6 +508,7 @@ impl Config {
Mode::Auto => "auto".to_owned(),
Mode::Ssh => "ssh".to_owned(),
Mode::Emoji => "emoji".to_owned(),
Mode::WebSearch => "websearch".to_owned(),
},
},
@ -646,6 +656,13 @@ impl Config {
pub fn dynamic_lines_limit(&self) -> bool {
self.dynamic_lines_limit.unwrap_or(true)
}
#[must_use]
pub fn search_query(&self) -> String {
self.search_query
.clone()
.unwrap_or_else(|| "https://duckduckgo.com/?q=".to_owned())
}
}
fn default_false() -> bool {

View file

@ -1,5 +1,6 @@
use regex::Regex;
use crate::modes::search::SearchProvider;
use crate::{
Error,
config::Config,
@ -20,7 +21,7 @@ enum AutoRunType {
DRun,
File,
Ssh,
// WebSearch,
WebSearch,
}
#[derive(Clone)]
@ -29,6 +30,7 @@ struct AutoItemProvider {
file: FileItemProvider<AutoRunType>,
math: MathProvider<AutoRunType>,
ssh: SshProvider<AutoRunType>,
search: SearchProvider<AutoRunType>,
last_mode: Option<AutoRunType>,
}
@ -44,6 +46,7 @@ impl AutoItemProvider {
file: FileItemProvider::new(AutoRunType::File, config.sort_order()),
math: MathProvider::new(AutoRunType::Math),
ssh: SshProvider::new(AutoRunType::Ssh, &config.sort_order()),
search: SearchProvider::new(AutoRunType::WebSearch, config.search_query()),
last_mode: None,
}
}
@ -66,7 +69,7 @@ impl AutoItemProvider {
fn contains_math_functions_or_starts_with_number(input: &str) -> bool {
// Regex for function names (word boundaries to match whole words)
let math_functions = r"\b(sqrt|abs|exp|ln|sin|cos|tan|asin|acos|atan|atan2|sinh|cosh|tanh|asinh|acosh|atanh|floor|ceil|round|signum|min|max|pi|e)\b";
let math_functions = r"\b(sqrt|abs|exp|ln|sin|cos|tan|asin|acos|atan|atan2|sinh|cosh|tanh|asinh|acosh|atanh|floor|ceil|round|signum|min|max|pi|e|0x|0b|\||&|<<|>>|\^)\b";
// Regex for strings that start with a number (including decimals)
let starts_with_number = r"^\s*[+-]?(\d+(\.\d*)?|\.\d+)";
@ -90,6 +93,13 @@ impl ItemProvider<AutoRunType> for AutoItemProvider {
(AutoRunType::File, self.file.get_elements(search_opt))
} else if search.starts_with("ssh") {
(AutoRunType::Ssh, self.ssh.get_elements(search_opt))
} else if search.starts_with('?') {
let re = Regex::new(r"^\?\s*").unwrap();
let query = re.replace(search, "");
(
AutoRunType::WebSearch,
self.search.get_elements(Some(&query)),
)
} else {
return self.default_auto_elements(search_opt);
};
@ -131,7 +141,7 @@ pub fn show(config: &Config) -> Result<(), Error> {
provider.clone(),
true,
Some(
vec!["ssh", "emoji", "^\\$\\w+"]
vec!["ssh", "emoji", "^\\$\\w+", "^\\?\\s*"]
.into_iter()
.map(|s| Regex::new(s).unwrap())
.collect(),
@ -160,6 +170,12 @@ pub fn show(config: &Config) -> Result<(), Error> {
ssh::launch(&selection_result, config)?;
break;
}
AutoRunType::WebSearch => {
if let Some(action) = selection_result.action {
spawn_fork(&action, None)?;
}
break;
}
}
} else if selection_result.label.starts_with("ssh") {
selection_result.label = selection_result.label.chars().skip(4).collect();

View file

@ -9,6 +9,7 @@ pub mod emoji;
pub mod file;
pub mod math;
pub mod run;
pub mod search;
pub mod ssh;
pub(crate) fn load_cache(cache_path: Option<PathBuf>) -> (Option<PathBuf>, HashMap<String, i64>) {

View file

@ -0,0 +1,60 @@
use urlencoding::encode;
use crate::desktop::spawn_fork;
use crate::{
Error,
config::Config,
gui::{self, ItemProvider, MenuItem},
};
#[derive(Clone)]
pub(crate) struct SearchProvider<T: Clone> {
search_query: String,
data: T,
}
impl<T: Clone> SearchProvider<T> {
pub fn new(data: T, search_query: String) -> Self {
Self {
search_query,
data: data.clone(),
}
}
}
impl<T: Clone> ItemProvider<T> for SearchProvider<T> {
fn get_elements(&mut self, query: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
if let Some(query) = query {
let url = format!("{}{}", self.search_query, encode(query));
let run_search = MenuItem::new(
format!("Search {query}"),
None,
Some(format!("xdg-open {url}")),
vec![],
None,
0.0,
Some(self.data.clone()),
);
(true, vec![run_search])
} else {
(false, vec![])
}
}
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
(false, None)
}
}
/// Shows the emoji mode
/// # Errors
///
/// Forwards errors from the gui. See `gui::show` for details.
pub fn show(config: &Config) -> Result<(), Error> {
let provider = SearchProvider::new(String::new(), config.search_query());
let selection_result = gui::show(config.clone(), provider, true, None, None)?;
match selection_result.menu.action {
None => Err(Error::MissingAction),
Some(action) => spawn_fork(&action, None),
}
}

View file

@ -41,6 +41,7 @@ fn main() -> anyhow::Result<()> {
Mode::Ssh => modes::ssh::show(&config),
Mode::Emoji => modes::emoji::show(&config),
Mode::Auto => modes::auto::show(&config),
Mode::WebSearch => modes::search::show(&config),
};
if let Err(err) = result {