Files
owlry/crates/owlry/src/backend.rs

267 lines
9.0 KiB
Rust

//! Abstraction over search backends for the UI.
//!
//! In normal mode, the UI talks to the owlry-core daemon via IPC.
//! In dmenu mode, the UI uses a local ProviderManager directly (no daemon).
use crate::client::CoreClient;
use log::warn;
use owlry_core::config::Config;
use owlry_core::data::FrecencyStore;
use owlry_core::filter::ProviderFilter;
use owlry_core::ipc::ResultItem;
use owlry_core::providers::{LaunchItem, ProviderManager, ProviderType};
/// Backend for search operations. Wraps either an IPC client (daemon mode)
/// or a local ProviderManager (dmenu mode).
pub enum SearchBackend {
/// IPC client connected to owlry-core daemon
Daemon(CoreClient),
/// Direct local provider manager (dmenu mode only)
Local {
providers: Box<ProviderManager>,
frecency: FrecencyStore,
},
}
impl SearchBackend {
/// Search for items matching the query.
///
/// In daemon mode, sends query over IPC. The modes list is derived from
/// the ProviderFilter's enabled set.
///
/// In local mode, delegates to ProviderManager directly.
pub fn search(
&mut self,
query: &str,
max_results: usize,
filter: &ProviderFilter,
config: &Config,
) -> Vec<LaunchItem> {
match self {
SearchBackend::Daemon(client) => {
let modes: Vec<String> = filter
.enabled_providers()
.iter()
.map(|p| p.to_string())
.collect();
let modes_param = if modes.is_empty() { None } else { Some(modes) };
match client.query(query, modes_param) {
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
Err(e) => {
warn!("IPC query failed: {}", e);
Vec::new()
}
}
}
SearchBackend::Local {
providers,
frecency,
} => {
let frecency_weight = config.providers.frecency_weight;
let use_frecency = config.providers.frecency;
if use_frecency {
providers
.search_with_frecency(
query,
max_results,
filter,
frecency,
frecency_weight,
None,
)
.into_iter()
.map(|(item, _)| item)
.collect()
} else {
providers
.search_filtered(query, max_results, filter)
.into_iter()
.map(|(item, _)| item)
.collect()
}
}
}
}
/// Search with tag filter support.
pub fn search_with_tag(
&mut self,
query: &str,
max_results: usize,
filter: &ProviderFilter,
config: &Config,
tag_filter: Option<&str>,
) -> Vec<LaunchItem> {
match self {
SearchBackend::Daemon(client) => {
// Daemon doesn't support tag filtering in IPC yet — pass query as-is.
// If there's a tag filter, prepend it so the daemon can handle it.
let effective_query = if let Some(tag) = tag_filter {
format!(":tag:{} {}", tag, query)
} else {
query.to_string()
};
let modes: Vec<String> = filter
.enabled_providers()
.iter()
.map(|p| p.to_string())
.collect();
let modes_param = if modes.is_empty() { None } else { Some(modes) };
match client.query(&effective_query, modes_param) {
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
Err(e) => {
warn!("IPC query failed: {}", e);
Vec::new()
}
}
}
SearchBackend::Local {
providers,
frecency,
} => {
let frecency_weight = config.providers.frecency_weight;
let use_frecency = config.providers.frecency;
if use_frecency {
providers
.search_with_frecency(
query,
max_results,
filter,
frecency,
frecency_weight,
tag_filter,
)
.into_iter()
.map(|(item, _)| item)
.collect()
} else {
providers
.search_filtered(query, max_results, filter)
.into_iter()
.map(|(item, _)| item)
.collect()
}
}
}
}
/// Execute a plugin action command. Returns true if handled.
pub fn execute_plugin_action(&mut self, command: &str) -> bool {
match self {
SearchBackend::Daemon(client) => match client.plugin_action(command) {
Ok(handled) => handled,
Err(e) => {
warn!("IPC plugin_action failed: {}", e);
false
}
},
SearchBackend::Local { providers, .. } => providers.execute_plugin_action(command),
}
}
/// Query submenu actions for a plugin item.
/// Returns (display_name, actions) if available.
pub fn query_submenu_actions(
&mut self,
plugin_id: &str,
data: &str,
display_name: &str,
) -> Option<(String, Vec<LaunchItem>)> {
match self {
SearchBackend::Daemon(client) => match client.submenu(plugin_id, data) {
Ok(items) if !items.is_empty() => {
let actions: Vec<LaunchItem> =
items.into_iter().map(result_to_launch_item).collect();
Some((display_name.to_string(), actions))
}
Ok(_) => None,
Err(e) => {
warn!("IPC submenu query failed: {}", e);
None
}
},
SearchBackend::Local { providers, .. } => {
providers.query_submenu_actions(plugin_id, data, display_name)
}
}
}
/// Record a launch event for frecency tracking.
pub fn record_launch(&mut self, item_id: &str, provider: &str) {
match self {
SearchBackend::Daemon(client) => {
if let Err(e) = client.launch(item_id, provider) {
warn!("IPC launch notification failed: {}", e);
}
}
SearchBackend::Local { frecency, .. } => {
frecency.record_launch(item_id);
}
}
}
/// Whether this backend is in dmenu mode.
pub fn is_dmenu_mode(&self) -> bool {
match self {
SearchBackend::Daemon(_) => false,
SearchBackend::Local { providers, .. } => providers.is_dmenu_mode(),
}
}
/// Refresh widget providers. No-op for daemon mode (daemon handles refresh).
pub fn refresh_widgets(&mut self) {
if let SearchBackend::Local { providers, .. } = self {
providers.refresh_widgets();
}
}
/// Get available provider type IDs from the daemon, or from local manager.
#[allow(dead_code)]
pub fn available_provider_ids(&mut self) -> Vec<String> {
match self {
SearchBackend::Daemon(client) => match client.providers() {
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
Err(e) => {
warn!("IPC providers query failed: {}", e);
Vec::new()
}
},
SearchBackend::Local { providers, .. } => providers
.available_providers()
.into_iter()
.map(|d| d.id)
.collect(),
}
}
}
/// Convert an IPC ResultItem to the internal LaunchItem type.
fn result_to_launch_item(item: ResultItem) -> LaunchItem {
let provider: ProviderType = item.provider.parse().unwrap_or(ProviderType::Application);
LaunchItem {
id: item.id,
name: item.title,
description: if item.description.is_empty() {
None
} else {
Some(item.description)
},
icon: if item.icon.is_empty() {
None
} else {
Some(item.icon)
},
provider,
command: item.command.unwrap_or_default(),
terminal: item.terminal,
tags: item.tags,
}
}