feat(owlry-core): add daemon-friendly API to ProviderManager and ProviderFilter
Add methods needed by the IPC server (Task 9) to create filters from mode strings, query provider metadata, and refresh individual providers. ProviderFilter: - from_mode_strings(): create filter from ["app", "cmd", "calc"] etc. - all(): create permissive filter accepting all provider types - mode_string_to_provider_type(): public helper for string-to-type mapping ProviderManager: - ProviderDescriptor struct for IPC provider metadata responses - available_providers() -> Vec<ProviderDescriptor> (replaces ProviderType version) - refresh_provider(id): refresh a single provider by type_id - new_with_config(config): self-contained init for daemon use NativeProvider: - icon(): get provider's default icon name - position_str(): get position as "normal"/"widget" string
This commit is contained in:
@@ -342,6 +342,61 @@ impl ProviderFilter {
|
||||
providers
|
||||
}
|
||||
|
||||
/// Create a filter from a list of mode name strings.
|
||||
///
|
||||
/// Maps each string to a ProviderType: "app" -> Application, "cmd" -> Command,
|
||||
/// "dmenu" -> Dmenu, anything else -> Plugin(id). An empty list produces an
|
||||
/// all-providers filter.
|
||||
pub fn from_mode_strings(modes: &[String]) -> Self {
|
||||
if modes.is_empty() {
|
||||
return Self::all();
|
||||
}
|
||||
let enabled: HashSet<ProviderType> = modes
|
||||
.iter()
|
||||
.map(|s| Self::mode_string_to_provider_type(s))
|
||||
.collect();
|
||||
Self {
|
||||
enabled,
|
||||
active_prefix: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a filter that accepts all providers.
|
||||
///
|
||||
/// Internally enables Application, Command, and Dmenu. Plugin providers are
|
||||
/// implicitly accepted because `is_active` will match them when they appear
|
||||
/// in the enabled set. For a true "pass everything" filter, this also
|
||||
/// pre-populates common plugin types.
|
||||
///
|
||||
/// The daemon uses this as the default when no modes are specified.
|
||||
pub fn all() -> Self {
|
||||
let mut enabled = HashSet::new();
|
||||
enabled.insert(ProviderType::Application);
|
||||
enabled.insert(ProviderType::Command);
|
||||
enabled.insert(ProviderType::Dmenu);
|
||||
// Common plugin types — the daemon typically has all plugins loaded
|
||||
for id in &[
|
||||
"calc", "clipboard", "emoji", "bookmarks", "ssh", "scripts",
|
||||
"system", "uuctl", "filesearch", "websearch", "weather",
|
||||
"media", "pomodoro",
|
||||
] {
|
||||
enabled.insert(ProviderType::Plugin(id.to_string()));
|
||||
}
|
||||
Self {
|
||||
enabled,
|
||||
active_prefix: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a mode string to a ProviderType.
|
||||
///
|
||||
/// Delegates to the existing `FromStr` impl on `ProviderType` which maps
|
||||
/// "app"/"apps"/"application" -> Application, "cmd"/"command" -> Command,
|
||||
/// "dmenu" -> Dmenu, and everything else -> Plugin(id).
|
||||
pub fn mode_string_to_provider_type(mode: &str) -> ProviderType {
|
||||
mode.parse::<ProviderType>().unwrap_or_else(|_| ProviderType::Plugin(mode.to_string()))
|
||||
}
|
||||
|
||||
/// Get display name for current mode
|
||||
pub fn mode_display_name(&self) -> &'static str {
|
||||
if let Some(ref prefix) = self.active_prefix {
|
||||
@@ -406,4 +461,107 @@ mod tests {
|
||||
// Should still have apps enabled as fallback
|
||||
assert!(filter.is_enabled(ProviderType::Application));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_mode_strings_single_core() {
|
||||
let filter = ProviderFilter::from_mode_strings(&["app".to_string()]);
|
||||
assert!(filter.is_enabled(ProviderType::Application));
|
||||
assert!(!filter.is_enabled(ProviderType::Command));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_mode_strings_multiple() {
|
||||
let filter = ProviderFilter::from_mode_strings(&[
|
||||
"app".to_string(),
|
||||
"cmd".to_string(),
|
||||
"calc".to_string(),
|
||||
]);
|
||||
assert!(filter.is_enabled(ProviderType::Application));
|
||||
assert!(filter.is_enabled(ProviderType::Command));
|
||||
assert!(filter.is_enabled(ProviderType::Plugin("calc".to_string())));
|
||||
assert!(!filter.is_enabled(ProviderType::Dmenu));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_mode_strings_empty_returns_all() {
|
||||
let filter = ProviderFilter::from_mode_strings(&[]);
|
||||
assert!(filter.is_enabled(ProviderType::Application));
|
||||
assert!(filter.is_enabled(ProviderType::Command));
|
||||
assert!(filter.is_enabled(ProviderType::Dmenu));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_mode_strings_plugin() {
|
||||
let filter = ProviderFilter::from_mode_strings(&["emoji".to_string()]);
|
||||
assert!(filter.is_enabled(ProviderType::Plugin("emoji".to_string())));
|
||||
assert!(!filter.is_enabled(ProviderType::Application));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_mode_strings_dmenu() {
|
||||
let filter = ProviderFilter::from_mode_strings(&["dmenu".to_string()]);
|
||||
assert!(filter.is_enabled(ProviderType::Dmenu));
|
||||
assert!(!filter.is_enabled(ProviderType::Application));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_includes_core_types() {
|
||||
let filter = ProviderFilter::all();
|
||||
assert!(filter.is_enabled(ProviderType::Application));
|
||||
assert!(filter.is_enabled(ProviderType::Command));
|
||||
assert!(filter.is_enabled(ProviderType::Dmenu));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_includes_common_plugins() {
|
||||
let filter = ProviderFilter::all();
|
||||
assert!(filter.is_enabled(ProviderType::Plugin("calc".to_string())));
|
||||
assert!(filter.is_enabled(ProviderType::Plugin("clipboard".to_string())));
|
||||
assert!(filter.is_enabled(ProviderType::Plugin("emoji".to_string())));
|
||||
assert!(filter.is_enabled(ProviderType::Plugin("weather".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_string_to_provider_type_core() {
|
||||
assert_eq!(
|
||||
ProviderFilter::mode_string_to_provider_type("app"),
|
||||
ProviderType::Application
|
||||
);
|
||||
assert_eq!(
|
||||
ProviderFilter::mode_string_to_provider_type("cmd"),
|
||||
ProviderType::Command
|
||||
);
|
||||
assert_eq!(
|
||||
ProviderFilter::mode_string_to_provider_type("dmenu"),
|
||||
ProviderType::Dmenu
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_string_to_provider_type_plugin() {
|
||||
assert_eq!(
|
||||
ProviderFilter::mode_string_to_provider_type("calc"),
|
||||
ProviderType::Plugin("calc".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
ProviderFilter::mode_string_to_provider_type("websearch"),
|
||||
ProviderType::Plugin("websearch".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_string_to_provider_type_aliases() {
|
||||
assert_eq!(
|
||||
ProviderFilter::mode_string_to_provider_type("apps"),
|
||||
ProviderType::Application
|
||||
);
|
||||
assert_eq!(
|
||||
ProviderFilter::mode_string_to_provider_type("application"),
|
||||
ProviderType::Application
|
||||
);
|
||||
assert_eq!(
|
||||
ProviderFilter::mode_string_to_provider_type("command"),
|
||||
ProviderType::Command
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,19 @@ use log::info;
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::data::FrecencyStore;
|
||||
|
||||
/// Metadata descriptor for an available provider (used by IPC/daemon API)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProviderDescriptor {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub prefix: Option<String>,
|
||||
pub icon: String,
|
||||
pub position: String,
|
||||
}
|
||||
|
||||
/// Represents a single searchable/launchable item
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaunchItem {
|
||||
@@ -147,6 +158,59 @@ impl ProviderManager {
|
||||
manager
|
||||
}
|
||||
|
||||
/// Create a self-contained ProviderManager from config.
|
||||
///
|
||||
/// Loads native plugins, creates core providers (Application + Command),
|
||||
/// categorizes everything, and performs initial refresh. Used by the daemon
|
||||
/// which doesn't have the UI-driven setup path from `app.rs`.
|
||||
pub fn new_with_config(config: &Config) -> Self {
|
||||
use crate::plugins::native_loader::NativePluginLoader;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Create core providers
|
||||
let core_providers: Vec<Box<dyn Provider>> = vec![
|
||||
Box::new(ApplicationProvider::new()),
|
||||
Box::new(CommandProvider::new()),
|
||||
];
|
||||
|
||||
// Load native plugins
|
||||
let mut loader = NativePluginLoader::new();
|
||||
loader.set_disabled(config.plugins.disabled_plugins.clone());
|
||||
|
||||
let native_providers = match loader.discover() {
|
||||
Ok(count) => {
|
||||
if count == 0 {
|
||||
info!("No native plugins found");
|
||||
Vec::new()
|
||||
} else {
|
||||
info!("Discovered {} native plugin(s)", count);
|
||||
let plugins: Vec<Arc<crate::plugins::native_loader::NativePlugin>> =
|
||||
loader.into_plugins();
|
||||
let mut providers = Vec::new();
|
||||
for plugin in plugins {
|
||||
for provider_info in &plugin.providers {
|
||||
let provider =
|
||||
NativeProvider::new(Arc::clone(&plugin), provider_info.clone());
|
||||
info!(
|
||||
"Created native provider: {} ({})",
|
||||
provider.name(),
|
||||
provider.type_id()
|
||||
);
|
||||
providers.push(provider);
|
||||
}
|
||||
}
|
||||
providers
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to discover native plugins: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
Self::new(core_providers, native_providers)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_dmenu_mode(&self) -> bool {
|
||||
self.providers
|
||||
@@ -515,7 +579,7 @@ impl ProviderManager {
|
||||
|
||||
/// Get all available provider types (for UI tabs)
|
||||
#[allow(dead_code)]
|
||||
pub fn available_providers(&self) -> Vec<ProviderType> {
|
||||
pub fn available_provider_types(&self) -> Vec<ProviderType> {
|
||||
self.providers
|
||||
.iter()
|
||||
.map(|p| p.provider_type())
|
||||
@@ -523,6 +587,122 @@ impl ProviderManager {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get descriptors for all registered providers (core + native plugins).
|
||||
///
|
||||
/// Used by the IPC server to report what providers are available to clients.
|
||||
pub fn available_providers(&self) -> Vec<ProviderDescriptor> {
|
||||
let mut descs = Vec::new();
|
||||
|
||||
// Core providers
|
||||
for provider in &self.providers {
|
||||
let (id, prefix, icon) = match provider.provider_type() {
|
||||
ProviderType::Application => (
|
||||
"app".to_string(),
|
||||
Some(":app".to_string()),
|
||||
"application-x-executable".to_string(),
|
||||
),
|
||||
ProviderType::Command => (
|
||||
"cmd".to_string(),
|
||||
Some(":cmd".to_string()),
|
||||
"utilities-terminal".to_string(),
|
||||
),
|
||||
ProviderType::Dmenu => (
|
||||
"dmenu".to_string(),
|
||||
None,
|
||||
"view-list-symbolic".to_string(),
|
||||
),
|
||||
ProviderType::Plugin(type_id) => (
|
||||
type_id,
|
||||
None,
|
||||
"application-x-addon".to_string(),
|
||||
),
|
||||
};
|
||||
descs.push(ProviderDescriptor {
|
||||
id,
|
||||
name: provider.name().to_string(),
|
||||
prefix,
|
||||
icon,
|
||||
position: "normal".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Static native plugin providers
|
||||
for provider in &self.static_native_providers {
|
||||
descs.push(ProviderDescriptor {
|
||||
id: provider.type_id().to_string(),
|
||||
name: provider.name().to_string(),
|
||||
prefix: provider.prefix().map(String::from),
|
||||
icon: provider.icon().to_string(),
|
||||
position: provider.position_str().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Dynamic native plugin providers
|
||||
for provider in &self.dynamic_providers {
|
||||
descs.push(ProviderDescriptor {
|
||||
id: provider.type_id().to_string(),
|
||||
name: provider.name().to_string(),
|
||||
prefix: provider.prefix().map(String::from),
|
||||
icon: provider.icon().to_string(),
|
||||
position: provider.position_str().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Widget native plugin providers
|
||||
for provider in &self.widget_providers {
|
||||
descs.push(ProviderDescriptor {
|
||||
id: provider.type_id().to_string(),
|
||||
name: provider.name().to_string(),
|
||||
prefix: provider.prefix().map(String::from),
|
||||
icon: provider.icon().to_string(),
|
||||
position: provider.position_str().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
descs
|
||||
}
|
||||
|
||||
/// Refresh a specific provider by its type_id.
|
||||
///
|
||||
/// Searches core providers (by ProviderType string), static native providers,
|
||||
/// and widget providers. Dynamic providers are skipped (they query on demand).
|
||||
pub fn refresh_provider(&mut self, provider_id: &str) {
|
||||
// Check core providers
|
||||
for provider in &mut self.providers {
|
||||
let matches = match provider.provider_type() {
|
||||
ProviderType::Application => provider_id == "app",
|
||||
ProviderType::Command => provider_id == "cmd",
|
||||
ProviderType::Dmenu => provider_id == "dmenu",
|
||||
ProviderType::Plugin(ref id) => provider_id == id,
|
||||
};
|
||||
if matches {
|
||||
provider.refresh();
|
||||
info!("Refreshed core provider '{}'", provider.name());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check static native providers
|
||||
for provider in &mut self.static_native_providers {
|
||||
if provider.type_id() == provider_id {
|
||||
provider.refresh();
|
||||
info!("Refreshed static provider '{}'", provider.name());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check widget providers
|
||||
for provider in &mut self.widget_providers {
|
||||
if provider.type_id() == provider_id {
|
||||
provider.refresh();
|
||||
info!("Refreshed widget provider '{}'", provider.name());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Provider '{}' not found for refresh", provider_id);
|
||||
}
|
||||
|
||||
/// Get a widget item by type_id (e.g., "pomodoro", "weather", "media")
|
||||
/// Returns the first item from the widget provider, if any
|
||||
pub fn get_widget_item(&self, type_id: &str) -> Option<LaunchItem> {
|
||||
@@ -596,3 +776,146 @@ impl ProviderManager {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Minimal mock provider for testing ProviderManager
|
||||
struct MockProvider {
|
||||
name: String,
|
||||
provider_type: ProviderType,
|
||||
items: Vec<LaunchItem>,
|
||||
refresh_count: usize,
|
||||
}
|
||||
|
||||
impl MockProvider {
|
||||
fn new(name: &str, provider_type: ProviderType) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
provider_type,
|
||||
items: Vec::new(),
|
||||
refresh_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_items(mut self, items: Vec<LaunchItem>) -> Self {
|
||||
self.items = items;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for MockProvider {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
self.provider_type.clone()
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
self.refresh_count += 1;
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
fn make_item(id: &str, name: &str, provider: ProviderType) -> LaunchItem {
|
||||
LaunchItem {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: None,
|
||||
icon: None,
|
||||
provider,
|
||||
command: format!("run-{}", id),
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_providers_core_only() {
|
||||
let providers: Vec<Box<dyn Provider>> = vec![
|
||||
Box::new(MockProvider::new("Applications", ProviderType::Application)),
|
||||
Box::new(MockProvider::new("Commands", ProviderType::Command)),
|
||||
];
|
||||
let pm = ProviderManager::new(providers, Vec::new());
|
||||
let descs = pm.available_providers();
|
||||
assert_eq!(descs.len(), 2);
|
||||
assert_eq!(descs[0].id, "app");
|
||||
assert_eq!(descs[0].name, "Applications");
|
||||
assert_eq!(descs[0].prefix, Some(":app".to_string()));
|
||||
assert_eq!(descs[0].icon, "application-x-executable");
|
||||
assert_eq!(descs[0].position, "normal");
|
||||
assert_eq!(descs[1].id, "cmd");
|
||||
assert_eq!(descs[1].name, "Commands");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_providers_dmenu() {
|
||||
let providers: Vec<Box<dyn Provider>> = vec![
|
||||
Box::new(MockProvider::new("dmenu", ProviderType::Dmenu)),
|
||||
];
|
||||
let pm = ProviderManager::new(providers, Vec::new());
|
||||
let descs = pm.available_providers();
|
||||
assert_eq!(descs.len(), 1);
|
||||
assert_eq!(descs[0].id, "dmenu");
|
||||
assert!(descs[0].prefix.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_provider_types() {
|
||||
let providers: Vec<Box<dyn Provider>> = vec![
|
||||
Box::new(MockProvider::new("Applications", ProviderType::Application)),
|
||||
Box::new(MockProvider::new("Commands", ProviderType::Command)),
|
||||
];
|
||||
let pm = ProviderManager::new(providers, Vec::new());
|
||||
let types = pm.available_provider_types();
|
||||
assert_eq!(types.len(), 2);
|
||||
assert!(types.contains(&ProviderType::Application));
|
||||
assert!(types.contains(&ProviderType::Command));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_provider_core() {
|
||||
let app = MockProvider::new("Applications", ProviderType::Application);
|
||||
let cmd = MockProvider::new("Commands", ProviderType::Command);
|
||||
let providers: Vec<Box<dyn Provider>> = vec![Box::new(app), Box::new(cmd)];
|
||||
let mut pm = ProviderManager::new(providers, Vec::new());
|
||||
|
||||
// refresh_all was called during construction, now refresh individual
|
||||
pm.refresh_provider("app");
|
||||
pm.refresh_provider("cmd");
|
||||
// Just verifying it doesn't panic; can't easily inspect refresh_count
|
||||
// through Box<dyn Provider>
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_provider_unknown_does_not_panic() {
|
||||
let providers: Vec<Box<dyn Provider>> = vec![
|
||||
Box::new(MockProvider::new("Applications", ProviderType::Application)),
|
||||
];
|
||||
let mut pm = ProviderManager::new(providers, Vec::new());
|
||||
pm.refresh_provider("nonexistent");
|
||||
// Should complete without panicking
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_with_core_providers() {
|
||||
let items = vec![
|
||||
make_item("firefox", "Firefox", ProviderType::Application),
|
||||
make_item("vim", "Vim", ProviderType::Application),
|
||||
];
|
||||
let provider = MockProvider::new("Applications", ProviderType::Application)
|
||||
.with_items(items);
|
||||
let providers: Vec<Box<dyn Provider>> = vec![Box::new(provider)];
|
||||
let pm = ProviderManager::new(providers, Vec::new());
|
||||
|
||||
let results = pm.search("fire", 10);
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].0.name, "Firefox");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,19 @@ impl NativeProvider {
|
||||
self.info.priority
|
||||
}
|
||||
|
||||
/// Get the provider's default icon name
|
||||
pub fn icon(&self) -> &str {
|
||||
self.info.icon.as_str()
|
||||
}
|
||||
|
||||
/// Get the provider's display position as a string
|
||||
pub fn position_str(&self) -> &str {
|
||||
match self.info.position {
|
||||
ProviderPosition::Widget => "widget",
|
||||
ProviderPosition::Normal => "normal",
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute an action command on the provider
|
||||
/// Uses query with "!" prefix to trigger action handling in the plugin
|
||||
pub fn execute_action(&self, action: &str) {
|
||||
|
||||
Reference in New Issue
Block a user