Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e680032d0e |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -971,7 +971,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "0.1.9"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "0.1.9"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.90"
|
rust-version = "1.90"
|
||||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||||
|
|||||||
@@ -48,3 +48,9 @@ calculator = true
|
|||||||
# Frecency: boost frequently/recently used items in search results
|
# Frecency: boost frequently/recently used items in search results
|
||||||
frecency = true
|
frecency = true
|
||||||
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
|
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
|
||||||
|
|
||||||
|
# Web search provider (type "? query" or "web query")
|
||||||
|
websearch = true
|
||||||
|
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
||||||
|
# Or custom URL with {query} placeholder, e.g. "https://search.example.com/?q={query}"
|
||||||
|
search_engine = "duckduckgo"
|
||||||
|
|||||||
@@ -126,6 +126,11 @@
|
|||||||
color: var(--owlry-badge-uuctl, @orange_3);
|
color: var(--owlry-badge-uuctl, @orange_3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.owlry-badge-web {
|
||||||
|
background-color: alpha(var(--owlry-badge-web, @teal_3), 0.2);
|
||||||
|
color: var(--owlry-badge-web, @teal_3);
|
||||||
|
}
|
||||||
|
|
||||||
/* Header bar */
|
/* Header bar */
|
||||||
.owlry-header {
|
.owlry-header {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
@@ -195,6 +200,12 @@
|
|||||||
border-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.4);
|
border-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.owlry-filter-web:checked {
|
||||||
|
background-color: alpha(var(--owlry-badge-web, @teal_3), 0.2);
|
||||||
|
color: var(--owlry-badge-web, @teal_3);
|
||||||
|
border-color: alpha(var(--owlry-badge-web, @teal_3), 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
/* Hints bar at bottom */
|
/* Hints bar at bottom */
|
||||||
.owlry-hints {
|
.owlry-hints {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ impl OwlryApp {
|
|||||||
debug!("Activating Owlry");
|
debug!("Activating Owlry");
|
||||||
|
|
||||||
let config = Rc::new(RefCell::new(Config::load_or_default()));
|
let config = Rc::new(RefCell::new(Config::load_or_default()));
|
||||||
let providers = Rc::new(RefCell::new(ProviderManager::new()));
|
let search_engine = config.borrow().providers.search_engine.clone();
|
||||||
|
let providers = Rc::new(RefCell::new(ProviderManager::with_search_engine(&search_engine)));
|
||||||
let frecency = Rc::new(RefCell::new(FrecencyStore::load_or_default()));
|
let frecency = Rc::new(RefCell::new(FrecencyStore::load_or_default()));
|
||||||
|
|
||||||
// Create filter from CLI args and config
|
// Create filter from CLI args and config
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub struct ThemeColors {
|
|||||||
pub badge_cmd: Option<String>,
|
pub badge_cmd: Option<String>,
|
||||||
pub badge_dmenu: Option<String>,
|
pub badge_dmenu: Option<String>,
|
||||||
pub badge_uuctl: Option<String>,
|
pub badge_uuctl: Option<String>,
|
||||||
|
pub badge_web: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -69,6 +70,18 @@ pub struct ProvidersConfig {
|
|||||||
/// Weight for frecency boost (0.0 = disabled, 1.0 = strong boost)
|
/// Weight for frecency boost (0.0 = disabled, 1.0 = strong boost)
|
||||||
#[serde(default = "default_frecency_weight")]
|
#[serde(default = "default_frecency_weight")]
|
||||||
pub frecency_weight: f64,
|
pub frecency_weight: f64,
|
||||||
|
/// Enable web search provider (? query or web query)
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub websearch: bool,
|
||||||
|
/// Search engine for web search
|
||||||
|
/// Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
||||||
|
/// Or custom URL with {query} placeholder
|
||||||
|
#[serde(default = "default_search_engine")]
|
||||||
|
pub search_engine: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_search_engine() -> String {
|
||||||
|
"duckduckgo".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
@@ -193,6 +206,8 @@ impl Default for Config {
|
|||||||
calculator: true,
|
calculator: true,
|
||||||
frecency: true,
|
frecency: true,
|
||||||
frecency_weight: 0.3,
|
frecency_weight: 0.3,
|
||||||
|
websearch: true,
|
||||||
|
search_engine: "duckduckgo".to_string(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ impl ProviderFilter {
|
|||||||
(":cmd ", ProviderType::Command),
|
(":cmd ", ProviderType::Command),
|
||||||
(":command ", ProviderType::Command),
|
(":command ", ProviderType::Command),
|
||||||
(":uuctl ", ProviderType::Uuctl),
|
(":uuctl ", ProviderType::Uuctl),
|
||||||
|
(":web ", ProviderType::WebSearch),
|
||||||
|
(":search ", ProviderType::WebSearch),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (prefix_str, provider) in prefixes {
|
for (prefix_str, provider) in prefixes {
|
||||||
@@ -154,6 +156,8 @@ impl ProviderFilter {
|
|||||||
(":cmd", ProviderType::Command),
|
(":cmd", ProviderType::Command),
|
||||||
(":command", ProviderType::Command),
|
(":command", ProviderType::Command),
|
||||||
(":uuctl", ProviderType::Uuctl),
|
(":uuctl", ProviderType::Uuctl),
|
||||||
|
(":web", ProviderType::WebSearch),
|
||||||
|
(":search", ProviderType::WebSearch),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (prefix_str, provider) in partial_prefixes {
|
for (prefix_str, provider) in partial_prefixes {
|
||||||
@@ -179,7 +183,8 @@ impl ProviderFilter {
|
|||||||
ProviderType::Calculator => 1,
|
ProviderType::Calculator => 1,
|
||||||
ProviderType::Command => 2,
|
ProviderType::Command => 2,
|
||||||
ProviderType::Uuctl => 3,
|
ProviderType::Uuctl => 3,
|
||||||
ProviderType::Dmenu => 4,
|
ProviderType::WebSearch => 4,
|
||||||
|
ProviderType::Dmenu => 5,
|
||||||
});
|
});
|
||||||
providers
|
providers
|
||||||
}
|
}
|
||||||
@@ -192,6 +197,7 @@ impl ProviderFilter {
|
|||||||
ProviderType::Calculator => "Calc",
|
ProviderType::Calculator => "Calc",
|
||||||
ProviderType::Command => "Commands",
|
ProviderType::Command => "Commands",
|
||||||
ProviderType::Uuctl => "uuctl",
|
ProviderType::Uuctl => "uuctl",
|
||||||
|
ProviderType::WebSearch => "Web",
|
||||||
ProviderType::Dmenu => "dmenu",
|
ProviderType::Dmenu => "dmenu",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -203,6 +209,7 @@ impl ProviderFilter {
|
|||||||
ProviderType::Calculator => "Calc",
|
ProviderType::Calculator => "Calc",
|
||||||
ProviderType::Command => "Commands",
|
ProviderType::Command => "Commands",
|
||||||
ProviderType::Uuctl => "uuctl",
|
ProviderType::Uuctl => "uuctl",
|
||||||
|
ProviderType::WebSearch => "Web",
|
||||||
ProviderType::Dmenu => "dmenu",
|
ProviderType::Dmenu => "dmenu",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,14 +18,17 @@ impl CalculatorProvider {
|
|||||||
/// Check if a query is a calculator expression
|
/// Check if a query is a calculator expression
|
||||||
pub fn is_calculator_query(query: &str) -> bool {
|
pub fn is_calculator_query(query: &str) -> bool {
|
||||||
let trimmed = query.trim();
|
let trimmed = query.trim();
|
||||||
trimmed.starts_with("= ") || trimmed.starts_with("calc ")
|
trimmed.starts_with("=") || trimmed.starts_with("calc ")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the expression from a calculator query
|
/// Extract the expression from a calculator query
|
||||||
fn extract_expression(query: &str) -> Option<&str> {
|
fn extract_expression(query: &str) -> Option<&str> {
|
||||||
let trimmed = query.trim();
|
let trimmed = query.trim();
|
||||||
|
// Support both "= expr" and "=expr" (with or without space)
|
||||||
if let Some(expr) = trimmed.strip_prefix("= ") {
|
if let Some(expr) = trimmed.strip_prefix("= ") {
|
||||||
Some(expr.trim())
|
Some(expr.trim())
|
||||||
|
} else if let Some(expr) = trimmed.strip_prefix("=") {
|
||||||
|
Some(expr.trim())
|
||||||
} else if let Some(expr) = trimmed.strip_prefix("calc ") {
|
} else if let Some(expr) = trimmed.strip_prefix("calc ") {
|
||||||
Some(expr.trim())
|
Some(expr.trim())
|
||||||
} else {
|
} else {
|
||||||
@@ -33,6 +36,49 @@ impl CalculatorProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if string looks like a math expression (for :calc mode)
|
||||||
|
pub fn looks_like_expression(query: &str) -> bool {
|
||||||
|
let trimmed = query.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Contains math operators or is a number
|
||||||
|
trimmed.chars().any(|c| "+-*/^()".contains(c))
|
||||||
|
|| trimmed.parse::<f64>().is_ok()
|
||||||
|
|| ["pi", "e", "sqrt", "sin", "cos", "tan", "abs", "ln", "log"]
|
||||||
|
.iter()
|
||||||
|
.any(|f| trimmed.to_lowercase().contains(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a raw expression (for :calc filter mode)
|
||||||
|
pub fn evaluate_raw(&mut self, expr: &str) -> Option<LaunchItem> {
|
||||||
|
let trimmed = expr.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match meval::eval_str(trimmed) {
|
||||||
|
Ok(result) => {
|
||||||
|
let result_str = if result.fract() == 0.0 && result.abs() < 1e15 {
|
||||||
|
format!("{}", result as i64)
|
||||||
|
} else {
|
||||||
|
format!("{:.10}", result).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(LaunchItem {
|
||||||
|
id: format!("calc:{}", trimmed),
|
||||||
|
name: format!("{} = {}", trimmed, result_str),
|
||||||
|
description: Some("Press Enter to copy result".to_string()),
|
||||||
|
icon: Some("accessories-calculator".to_string()),
|
||||||
|
provider: ProviderType::Calculator,
|
||||||
|
command: format!("echo -n '{}' | wl-copy", result_str),
|
||||||
|
terminal: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Evaluate an expression and return a LaunchItem result
|
/// Evaluate an expression and return a LaunchItem result
|
||||||
pub fn evaluate(&mut self, query: &str) -> Option<LaunchItem> {
|
pub fn evaluate(&mut self, query: &str) -> Option<LaunchItem> {
|
||||||
let expr = Self::extract_expression(query)?;
|
let expr = Self::extract_expression(query)?;
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ mod calculator;
|
|||||||
mod command;
|
mod command;
|
||||||
mod dmenu;
|
mod dmenu;
|
||||||
mod uuctl;
|
mod uuctl;
|
||||||
|
mod websearch;
|
||||||
|
|
||||||
pub use application::ApplicationProvider;
|
pub use application::ApplicationProvider;
|
||||||
pub use calculator::CalculatorProvider;
|
pub use calculator::CalculatorProvider;
|
||||||
pub use command::CommandProvider;
|
pub use command::CommandProvider;
|
||||||
pub use dmenu::DmenuProvider;
|
pub use dmenu::DmenuProvider;
|
||||||
pub use uuctl::UuctlProvider;
|
pub use uuctl::UuctlProvider;
|
||||||
|
pub use websearch::WebSearchProvider;
|
||||||
|
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
@@ -36,6 +38,7 @@ pub enum ProviderType {
|
|||||||
Command,
|
Command,
|
||||||
Dmenu,
|
Dmenu,
|
||||||
Uuctl,
|
Uuctl,
|
||||||
|
WebSearch,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for ProviderType {
|
impl std::str::FromStr for ProviderType {
|
||||||
@@ -48,6 +51,7 @@ impl std::str::FromStr for ProviderType {
|
|||||||
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
|
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
|
||||||
"uuctl" => Ok(ProviderType::Uuctl),
|
"uuctl" => Ok(ProviderType::Uuctl),
|
||||||
"dmenu" => Ok(ProviderType::Dmenu),
|
"dmenu" => Ok(ProviderType::Dmenu),
|
||||||
|
"web" | "websearch" | "search" => Ok(ProviderType::WebSearch),
|
||||||
_ => Err(format!(
|
_ => Err(format!(
|
||||||
"Unknown provider: '{}'. Valid: app, calc, cmd, uuctl",
|
"Unknown provider: '{}'. Valid: app, calc, cmd, uuctl",
|
||||||
s
|
s
|
||||||
@@ -64,6 +68,7 @@ impl std::fmt::Display for ProviderType {
|
|||||||
ProviderType::Command => write!(f, "cmd"),
|
ProviderType::Command => write!(f, "cmd"),
|
||||||
ProviderType::Dmenu => write!(f, "dmenu"),
|
ProviderType::Dmenu => write!(f, "dmenu"),
|
||||||
ProviderType::Uuctl => write!(f, "uuctl"),
|
ProviderType::Uuctl => write!(f, "uuctl"),
|
||||||
|
ProviderType::WebSearch => write!(f, "web"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,14 +86,21 @@ pub trait Provider: Send {
|
|||||||
pub struct ProviderManager {
|
pub struct ProviderManager {
|
||||||
providers: Vec<Box<dyn Provider>>,
|
providers: Vec<Box<dyn Provider>>,
|
||||||
calculator: CalculatorProvider,
|
calculator: CalculatorProvider,
|
||||||
|
websearch: WebSearchProvider,
|
||||||
matcher: SkimMatcherV2,
|
matcher: SkimMatcherV2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProviderManager {
|
impl ProviderManager {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
Self::with_search_engine("duckduckgo")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_search_engine(search_engine: &str) -> Self {
|
||||||
let mut manager = Self {
|
let mut manager = Self {
|
||||||
providers: Vec::new(),
|
providers: Vec::new(),
|
||||||
calculator: CalculatorProvider::new(),
|
calculator: CalculatorProvider::new(),
|
||||||
|
websearch: WebSearchProvider::with_engine(search_engine),
|
||||||
matcher: SkimMatcherV2::default(),
|
matcher: SkimMatcherV2::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,15 +241,31 @@ impl ProviderManager {
|
|||||||
) -> Vec<(LaunchItem, i64)> {
|
) -> Vec<(LaunchItem, i64)> {
|
||||||
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
||||||
|
|
||||||
// Check for calculator query first
|
// Check for calculator query (= or calc prefix)
|
||||||
if CalculatorProvider::is_calculator_query(query) {
|
if CalculatorProvider::is_calculator_query(query) {
|
||||||
if let Some(calc_result) = self.calculator.evaluate(query) {
|
if let Some(calc_result) = self.calculator.evaluate(query) {
|
||||||
// Calculator results get a high score to appear first
|
// Calculator results get a high score to appear first
|
||||||
results.push((calc_result, 10000));
|
results.push((calc_result, 10000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Also check for raw expression when in :calc filter mode
|
||||||
|
else if filter.active_prefix() == Some(ProviderType::Calculator)
|
||||||
|
&& CalculatorProvider::looks_like_expression(query)
|
||||||
|
{
|
||||||
|
if let Some(calc_result) = self.calculator.evaluate_raw(query) {
|
||||||
|
results.push((calc_result, 10000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Empty query (after checking calculator) - return frecency-sorted items
|
// Check for web search query
|
||||||
|
if WebSearchProvider::is_websearch_query(query) {
|
||||||
|
if let Some(web_result) = self.websearch.evaluate(query) {
|
||||||
|
// Web search results get a high score to appear first
|
||||||
|
results.push((web_result, 9000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty query (after checking special providers) - return frecency-sorted items
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
let mut items: Vec<(LaunchItem, i64)> = self
|
let mut items: Vec<(LaunchItem, i64)> = self
|
||||||
.providers
|
.providers
|
||||||
|
|||||||
185
src/providers/websearch.rs
Normal file
185
src/providers/websearch.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
use crate::providers::{LaunchItem, ProviderType};
|
||||||
|
|
||||||
|
/// Common search engine URL templates
|
||||||
|
/// {query} is replaced with the URL-encoded search term
|
||||||
|
pub const SEARCH_ENGINES: &[(&str, &str)] = &[
|
||||||
|
("google", "https://www.google.com/search?q={query}"),
|
||||||
|
("duckduckgo", "https://duckduckgo.com/?q={query}"),
|
||||||
|
("bing", "https://www.bing.com/search?q={query}"),
|
||||||
|
("startpage", "https://www.startpage.com/search?q={query}"),
|
||||||
|
("searxng", "https://searx.be/search?q={query}"),
|
||||||
|
("brave", "https://search.brave.com/search?q={query}"),
|
||||||
|
("ecosia", "https://www.ecosia.org/search?q={query}"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Default search engine if not configured
|
||||||
|
pub const DEFAULT_ENGINE: &str = "duckduckgo";
|
||||||
|
|
||||||
|
/// Web search provider - opens browser with search query
|
||||||
|
pub struct WebSearchProvider {
|
||||||
|
/// URL template with {query} placeholder
|
||||||
|
url_template: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSearchProvider {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_engine(DEFAULT_ENGINE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create provider with specific search engine
|
||||||
|
pub fn with_engine(engine_name: &str) -> Self {
|
||||||
|
let url_template = SEARCH_ENGINES
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| *name == engine_name.to_lowercase())
|
||||||
|
.map(|(_, url)| url.to_string())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
// If not a known engine, treat it as a custom URL template
|
||||||
|
if engine_name.contains("{query}") {
|
||||||
|
engine_name.to_string()
|
||||||
|
} else {
|
||||||
|
// Fall back to default
|
||||||
|
SEARCH_ENGINES
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| *name == DEFAULT_ENGINE)
|
||||||
|
.map(|(_, url)| url.to_string())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { url_template }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if query is a web search query
|
||||||
|
/// Triggers on: `? query`, `web query`, `search query`
|
||||||
|
pub fn is_websearch_query(query: &str) -> bool {
|
||||||
|
let trimmed = query.trim();
|
||||||
|
trimmed.starts_with("? ")
|
||||||
|
|| trimmed.starts_with("?")
|
||||||
|
|| trimmed.to_lowercase().starts_with("web ")
|
||||||
|
|| trimmed.to_lowercase().starts_with("search ")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the search term from the query
|
||||||
|
fn extract_search_term(query: &str) -> Option<&str> {
|
||||||
|
let trimmed = query.trim();
|
||||||
|
|
||||||
|
if let Some(rest) = trimmed.strip_prefix("? ") {
|
||||||
|
Some(rest.trim())
|
||||||
|
} else if let Some(rest) = trimmed.strip_prefix("?") {
|
||||||
|
Some(rest.trim())
|
||||||
|
} else if trimmed.to_lowercase().starts_with("web ") {
|
||||||
|
// Need to get the original casing
|
||||||
|
Some(trimmed[4..].trim())
|
||||||
|
} else if trimmed.to_lowercase().starts_with("search ") {
|
||||||
|
Some(trimmed[7..].trim())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL-encode a search query
|
||||||
|
fn url_encode(query: &str) -> String {
|
||||||
|
// TODO: This is where you can implement the URL encoding logic!
|
||||||
|
// Consider: Should we use a crate like `urlencoding` or implement manually?
|
||||||
|
// Manual encoding needs to handle: spaces, &, =, ?, #, etc.
|
||||||
|
query
|
||||||
|
.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
' ' => "+".to_string(),
|
||||||
|
'&' => "%26".to_string(),
|
||||||
|
'=' => "%3D".to_string(),
|
||||||
|
'?' => "%3F".to_string(),
|
||||||
|
'#' => "%23".to_string(),
|
||||||
|
'+' => "%2B".to_string(),
|
||||||
|
'%' => "%25".to_string(),
|
||||||
|
c if c.is_ascii_alphanumeric() || "-_.~".contains(c) => c.to_string(),
|
||||||
|
c => format!("%{:02X}", c as u32),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the search URL from a query
|
||||||
|
fn build_search_url(&self, search_term: &str) -> String {
|
||||||
|
let encoded = Self::url_encode(search_term);
|
||||||
|
self.url_template.replace("{query}", &encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a web search query and return a LaunchItem if valid
|
||||||
|
pub fn evaluate(&self, query: &str) -> Option<LaunchItem> {
|
||||||
|
let search_term = Self::extract_search_term(query)?;
|
||||||
|
|
||||||
|
if search_term.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = self.build_search_url(search_term);
|
||||||
|
|
||||||
|
// Use xdg-open to open the browser
|
||||||
|
let command = format!("xdg-open '{}'", url);
|
||||||
|
|
||||||
|
Some(LaunchItem {
|
||||||
|
id: format!("websearch:{}", search_term),
|
||||||
|
name: format!("Search: {}", search_term),
|
||||||
|
description: Some("Open in browser".to_string()),
|
||||||
|
icon: Some("web-browser".to_string()),
|
||||||
|
provider: ProviderType::WebSearch,
|
||||||
|
command,
|
||||||
|
terminal: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_websearch_query() {
|
||||||
|
assert!(WebSearchProvider::is_websearch_query("? rust programming"));
|
||||||
|
assert!(WebSearchProvider::is_websearch_query("?rust"));
|
||||||
|
assert!(WebSearchProvider::is_websearch_query("web rust"));
|
||||||
|
assert!(WebSearchProvider::is_websearch_query("search rust"));
|
||||||
|
assert!(!WebSearchProvider::is_websearch_query("rust"));
|
||||||
|
assert!(!WebSearchProvider::is_websearch_query("= 5+3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_search_term() {
|
||||||
|
assert_eq!(
|
||||||
|
WebSearchProvider::extract_search_term("? rust programming"),
|
||||||
|
Some("rust programming")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
WebSearchProvider::extract_search_term("?rust"),
|
||||||
|
Some("rust")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
WebSearchProvider::extract_search_term("web rust docs"),
|
||||||
|
Some("rust docs")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_url_encode() {
|
||||||
|
assert_eq!(WebSearchProvider::url_encode("hello world"), "hello+world");
|
||||||
|
assert_eq!(WebSearchProvider::url_encode("foo&bar"), "foo%26bar");
|
||||||
|
assert_eq!(WebSearchProvider::url_encode("a=b"), "a%3Db");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_search_url() {
|
||||||
|
let provider = WebSearchProvider::with_engine("duckduckgo");
|
||||||
|
let url = provider.build_search_url("rust programming");
|
||||||
|
assert_eq!(url, "https://duckduckgo.com/?q=rust+programming");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_evaluate() {
|
||||||
|
let provider = WebSearchProvider::new();
|
||||||
|
let item = provider.evaluate("? rust docs").unwrap();
|
||||||
|
assert_eq!(item.name, "Search: rust docs");
|
||||||
|
assert!(item.command.contains("xdg-open"));
|
||||||
|
assert!(item.command.contains("duckduckgo"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,9 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
|
|||||||
if let Some(ref badge_uuctl) = config.colors.badge_uuctl {
|
if let Some(ref badge_uuctl) = config.colors.badge_uuctl {
|
||||||
css.push_str(&format!(" --owlry-badge-uuctl: {};\n", badge_uuctl));
|
css.push_str(&format!(" --owlry-badge-uuctl: {};\n", badge_uuctl));
|
||||||
}
|
}
|
||||||
|
if let Some(ref badge_web) = config.colors.badge_web {
|
||||||
|
css.push_str(&format!(" --owlry-badge-web: {};\n", badge_web));
|
||||||
|
}
|
||||||
|
|
||||||
css.push_str("}\n");
|
css.push_str("}\n");
|
||||||
css
|
css
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ impl MainWindow {
|
|||||||
hints_box.add_css_class("owlry-hints");
|
hints_box.add_css_class("owlry-hints");
|
||||||
|
|
||||||
let hints_label = Label::builder()
|
let hints_label = Label::builder()
|
||||||
.label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc :app :cmd :uuctl")
|
.label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc ? web :app :cmd")
|
||||||
.halign(gtk4::Align::Center)
|
.halign(gtk4::Align::Center)
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.build();
|
.build();
|
||||||
@@ -209,6 +209,7 @@ impl MainWindow {
|
|||||||
ProviderType::Calculator => "owlry-filter-calc",
|
ProviderType::Calculator => "owlry-filter-calc",
|
||||||
ProviderType::Command => "owlry-filter-cmd",
|
ProviderType::Command => "owlry-filter-cmd",
|
||||||
ProviderType::Uuctl => "owlry-filter-uuctl",
|
ProviderType::Uuctl => "owlry-filter-uuctl",
|
||||||
|
ProviderType::WebSearch => "owlry-filter-web",
|
||||||
ProviderType::Dmenu => "owlry-filter-dmenu",
|
ProviderType::Dmenu => "owlry-filter-dmenu",
|
||||||
};
|
};
|
||||||
button.add_css_class(css_class);
|
button.add_css_class(css_class);
|
||||||
@@ -229,6 +230,7 @@ impl MainWindow {
|
|||||||
ProviderType::Calculator => "calculator",
|
ProviderType::Calculator => "calculator",
|
||||||
ProviderType::Command => "commands",
|
ProviderType::Command => "commands",
|
||||||
ProviderType::Uuctl => "uuctl units",
|
ProviderType::Uuctl => "uuctl units",
|
||||||
|
ProviderType::WebSearch => "web",
|
||||||
ProviderType::Dmenu => "options",
|
ProviderType::Dmenu => "options",
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -334,7 +336,7 @@ impl MainWindow {
|
|||||||
|
|
||||||
// Restore UI
|
// Restore UI
|
||||||
mode_label.set_label(filter.borrow().mode_display_name());
|
mode_label.set_label(filter.borrow().mode_display_name());
|
||||||
hints_label.set_label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc :app :cmd :uuctl");
|
hints_label.set_label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc ? web :app :cmd");
|
||||||
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
|
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
|
||||||
search_entry.set_text(&saved_search);
|
search_entry.set_text(&saved_search);
|
||||||
|
|
||||||
@@ -411,6 +413,7 @@ impl MainWindow {
|
|||||||
ProviderType::Calculator => "calculator",
|
ProviderType::Calculator => "calculator",
|
||||||
ProviderType::Command => "commands",
|
ProviderType::Command => "commands",
|
||||||
ProviderType::Uuctl => "uuctl units",
|
ProviderType::Uuctl => "uuctl units",
|
||||||
|
ProviderType::WebSearch => "web",
|
||||||
ProviderType::Dmenu => "options",
|
ProviderType::Dmenu => "options",
|
||||||
};
|
};
|
||||||
search_entry_for_change
|
search_entry_for_change
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ impl ResultRow {
|
|||||||
crate::providers::ProviderType::Command => "utilities-terminal",
|
crate::providers::ProviderType::Command => "utilities-terminal",
|
||||||
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
|
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
|
||||||
crate::providers::ProviderType::Uuctl => "system-run",
|
crate::providers::ProviderType::Uuctl => "system-run",
|
||||||
|
crate::providers::ProviderType::WebSearch => "web-browser",
|
||||||
};
|
};
|
||||||
Image::from_icon_name(default_icon)
|
Image::from_icon_name(default_icon)
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user