Compare commits

...

4 Commits

Author SHA1 Message Date
bf8a31af78 chore: bump all crates to 0.4.7 2026-01-01 22:29:00 +01:00
e23bdf5cee fix(providers): enable submenu support for static native plugins
Static native plugins (systemd, clipboard, etc.) were being boxed as
Box<dyn Provider>, which lost access to the query() method needed for
submenu support. The Provider trait only has refresh() and items().

Add static_native_providers field to keep static native plugins as
NativeProvider instances, preserving their query() method. Update all
search methods and query_submenu_actions() to include this new list.

Fixes systemd plugin submenu not showing actions when selecting a service.
2026-01-01 22:14:43 +01:00
25c4d40d36 docs: add comprehensive usage documentation
- Expand CLI --help with examples, dmenu mode, and search prefixes
- Add dmenu mode section to README with practical examples
- Add plugin management CLI reference to README
- Update argument descriptions with all valid modes listed
2026-01-01 21:45:52 +01:00
b36dd2a438 chore: update bump-all to include core in single commit 2025-12-30 20:32:28 +01:00
22 changed files with 317 additions and 162 deletions

34
Cargo.lock generated
View File

@@ -2373,7 +2373,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry" name = "owlry"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@@ -2402,7 +2402,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-lua" name = "owlry-lua"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"chrono", "chrono",
@@ -2420,7 +2420,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-api" name = "owlry-plugin-api"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"serde", "serde",
@@ -2428,7 +2428,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-bookmarks" name = "owlry-plugin-bookmarks"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2440,7 +2440,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-calculator" name = "owlry-plugin-calculator"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"meval", "meval",
@@ -2449,7 +2449,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-clipboard" name = "owlry-plugin-clipboard"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2457,7 +2457,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-emoji" name = "owlry-plugin-emoji"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2465,7 +2465,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-filesearch" name = "owlry-plugin-filesearch"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2474,7 +2474,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-media" name = "owlry-plugin-media"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2482,7 +2482,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-pomodoro" name = "owlry-plugin-pomodoro"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2494,7 +2494,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-scripts" name = "owlry-plugin-scripts"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2503,7 +2503,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-ssh" name = "owlry-plugin-ssh"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2512,7 +2512,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-system" name = "owlry-plugin-system"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2520,7 +2520,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-systemd" name = "owlry-plugin-systemd"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2528,7 +2528,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-weather" name = "owlry-plugin-weather"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2541,7 +2541,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-websearch" name = "owlry-plugin-websearch"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2549,7 +2549,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-rune" name = "owlry-rune"
version = "0.4.6" version = "0.4.7"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs", "dirs",

View File

@@ -99,12 +99,40 @@ sudo cp target/release/libowlry_plugin_*.so /usr/lib/owlry/plugins/
## Usage ## Usage
```bash ```bash
owlry # Launch with defaults owlry # Launch with all providers
owlry --mode app # Applications only owlry -m app # Applications only
owlry --providers app,cmd # Specific providers owlry -m cmd # PATH commands only
owlry --help # Show all options owlry -p app,cmd # Multiple specific providers
owlry -m calc # Calculator plugin only (if installed)
owlry --help # Show all options with examples
``` ```
### dmenu Mode
Owlry is dmenu-compatible. Pipe input for interactive selection:
```bash
# Basic selection
echo -e "Option A\nOption B\nOption C" | owlry -m dmenu
# Select from files
ls ~/Documents | owlry -m dmenu
# Git branch checkout
git branch | owlry -m dmenu --prompt "checkout:" | xargs git checkout
# Kill a process
ps -eo comm | sort -u | owlry -m dmenu --prompt "kill:" | xargs pkill
# Select and open a project
find ~/projects -maxdepth 1 -type d | owlry -m dmenu | xargs code
# Package manager search
pacman -Ssq | owlry -m dmenu --prompt "install:" | xargs sudo pacman -S
```
The `--prompt` flag sets a custom label for the search input.
### Keyboard Shortcuts ### Keyboard Shortcuts
| Key | Action | | Key | Action |
@@ -221,6 +249,38 @@ Add plugin IDs to the disabled list in your config:
disabled = ["emoji", "pomodoro"] disabled = ["emoji", "pomodoro"]
``` ```
### Plugin Management CLI
```bash
# List installed plugins
owlry plugin list
owlry plugin list --enabled # Only enabled
owlry plugin list --available # Show registry plugins
# Search registry
owlry plugin search "weather"
# Install/remove
owlry plugin install <name> # From registry
owlry plugin install ./my-plugin # From local path
owlry plugin remove <name>
# Enable/disable
owlry plugin enable <name>
owlry plugin disable <name>
# Plugin info
owlry plugin info <name>
owlry plugin commands <name> # List plugin CLI commands
# Create new plugin
owlry plugin create my-plugin # Lua (default)
owlry plugin create my-plugin -r rune # Rune
# Run plugin command
owlry plugin run <plugin-id> <command> [args...]
```
### Creating Custom Plugins ### Creating Custom Plugins
See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for: See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for:

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-lua" name = "owlry-lua"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-api" name = "owlry-plugin-api"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-bookmarks" name = "owlry-plugin-bookmarks"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-calculator" name = "owlry-plugin-calculator"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-clipboard" name = "owlry-plugin-clipboard"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-emoji" name = "owlry-plugin-emoji"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-filesearch" name = "owlry-plugin-filesearch"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-media" name = "owlry-plugin-media"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-pomodoro" name = "owlry-plugin-pomodoro"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-scripts" name = "owlry-plugin-scripts"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-ssh" name = "owlry-plugin-ssh"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-system" name = "owlry-plugin-system"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-systemd" name = "owlry-plugin-systemd"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-weather" name = "owlry-plugin-weather"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-websearch" name = "owlry-plugin-websearch"
version = "0.4.6" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-rune" name = "owlry-rune"
version = "0.4.6" version = "0.4.7"
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.90"
description = "Rune scripting runtime for owlry plugins" description = "Rune scripting runtime for owlry plugins"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry" name = "owlry"
version = "0.4.6" version = "0.4.7"
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"

View File

@@ -10,19 +10,55 @@ use crate::providers::ProviderType;
#[command( #[command(
name = "owlry", name = "owlry",
about = "An owl-themed application launcher for Wayland", about = "An owl-themed application launcher for Wayland",
version long_about = "An owl-themed application launcher for Wayland, built with GTK4 and Layer Shell.\n\n\
Owlry provides fuzzy search across applications, commands, and plugins.\n\
Native plugins add features like calculator, clipboard, emoji, weather, and more.",
version,
after_help = "\
EXAMPLES:
owlry Launch with all providers
owlry -m app Applications only
owlry -m cmd PATH commands only
owlry -m dmenu dmenu-compatible mode (reads from stdin)
owlry -p app,cmd Multiple providers
owlry -m calc Calculator plugin only (if installed)
DMENU MODE:
Pipe input to owlry for interactive selection:
echo -e \"Option A\\nOption B\" | owlry -m dmenu
ls | owlry -m dmenu
git branch | owlry -m dmenu --prompt \"checkout:\"
SEARCH PREFIXES:
:app firefox Search applications
:cmd git Search PATH commands
= 5+3 Calculator (requires plugin)
? rust docs Web search (requires plugin)
/ .bashrc File search (requires plugin)
For configuration, see ~/.config/owlry/config.toml
For plugin management, see: owlry plugin --help"
)] )]
pub struct CliArgs { pub struct CliArgs {
/// Start in single-provider mode (app, cmd, uuctl) /// Start in single-provider mode
#[arg(long, short = 'm', value_parser = parse_provider)] ///
/// Core modes: app, cmd, dmenu
/// Plugin modes: calc, clip, emoji, ssh, sys, bm, file, web, uuctl, weather, media, pomodoro
#[arg(long, short = 'm', value_parser = parse_provider, value_name = "MODE")]
pub mode: Option<ProviderType>, pub mode: Option<ProviderType>,
/// Comma-separated list of enabled providers (app,cmd,uuctl) /// Comma-separated list of enabled providers
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)] ///
/// Examples: -p app,cmd or -p app,calc,emoji
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider, value_name = "PROVIDERS")]
pub providers: Option<Vec<ProviderType>>, pub providers: Option<Vec<ProviderType>>,
/// Custom prompt text for the search input (useful for dmenu mode) /// Custom prompt text for the search input
#[arg(long)] ///
/// Useful in dmenu mode to indicate what the user is selecting.
/// Example: --prompt "Select file:"
#[arg(long, value_name = "TEXT")]
pub prompt: Option<String>, pub prompt: Option<String>,
/// Subcommand to run (if any) /// Subcommand to run (if any)

View File

@@ -95,8 +95,10 @@ pub trait Provider: Send {
/// Manages all providers and handles searching /// Manages all providers and handles searching
pub struct ProviderManager { pub struct ProviderManager {
/// Static providers (apps, commands, and native static plugins) /// Core static providers (apps, commands, dmenu)
providers: Vec<Box<dyn Provider>>, providers: Vec<Box<dyn Provider>>,
/// Static native plugin providers (need query() for submenu support)
static_native_providers: Vec<NativeProvider>,
/// Dynamic providers from native plugins (calculator, websearch, filesearch) /// Dynamic providers from native plugins (calculator, websearch, filesearch)
/// These are queried per-keystroke, not cached /// These are queried per-keystroke, not cached
dynamic_providers: Vec<NativeProvider>, dynamic_providers: Vec<NativeProvider>,
@@ -118,6 +120,7 @@ impl ProviderManager {
pub fn with_native_plugins(native_providers: Vec<NativeProvider>) -> Self { pub fn with_native_plugins(native_providers: Vec<NativeProvider>) -> Self {
let mut manager = Self { let mut manager = Self {
providers: Vec::new(), providers: Vec::new(),
static_native_providers: Vec::new(),
dynamic_providers: Vec::new(), dynamic_providers: Vec::new(),
widget_providers: Vec::new(), widget_providers: Vec::new(),
matcher: SkimMatcherV2::default(), matcher: SkimMatcherV2::default(),
@@ -149,9 +152,9 @@ impl ProviderManager {
info!("Registered widget provider: {} ({})", provider.name(), type_id); info!("Registered widget provider: {} ({})", provider.name(), type_id);
manager.widget_providers.push(provider); manager.widget_providers.push(provider);
} else { } else {
// Static providers with Normal position // Static native providers (keep as NativeProvider for query/submenu support)
info!("Registered static provider: {} ({})", provider.name(), type_id); info!("Registered static provider: {} ({})", provider.name(), type_id);
manager.providers.push(Box::new(provider)); manager.static_native_providers.push(provider);
} }
} }
} }
@@ -170,7 +173,7 @@ impl ProviderManager {
} }
pub fn refresh_all(&mut self) { pub fn refresh_all(&mut self) {
// Refresh static providers (fast, local operations) // Refresh core providers (apps, commands)
for provider in &mut self.providers { for provider in &mut self.providers {
provider.refresh(); provider.refresh();
info!( info!(
@@ -180,6 +183,16 @@ impl ProviderManager {
); );
} }
// Refresh static native providers (clipboard, emoji, ssh, etc.)
for provider in &mut self.static_native_providers {
provider.refresh();
info!(
"Static provider '{}' loaded {} items",
provider.name(),
provider.items().len()
);
}
// Widget providers are refreshed separately to avoid blocking startup // Widget providers are refreshed separately to avoid blocking startup
// Call refresh_widgets() after window is shown // Call refresh_widgets() after window is shown
@@ -201,9 +214,13 @@ impl ProviderManager {
} }
/// Find a native provider by type ID /// Find a native provider by type ID
/// Searches in widget providers and dynamic providers /// Searches in all native provider lists (static, dynamic, widget)
pub fn find_native_provider(&self, type_id: &str) -> Option<&NativeProvider> { pub fn find_native_provider(&self, type_id: &str) -> Option<&NativeProvider> {
// Check widget providers first (pomodoro, weather, media) // Check static native providers first (clipboard, emoji, ssh, systemd, etc.)
if let Some(p) = self.static_native_providers.iter().find(|p| p.type_id() == type_id) {
return Some(p);
}
// Check widget providers (pomodoro, weather, media)
if let Some(p) = self.widget_providers.iter().find(|p| p.type_id() == type_id) { if let Some(p) = self.widget_providers.iter().find(|p| p.type_id() == type_id) {
return Some(p); return Some(p);
} }
@@ -246,37 +263,40 @@ impl ProviderManager {
} }
} }
/// Iterate over all static provider items (core + native static plugins)
fn all_static_items(&self) -> impl Iterator<Item = &LaunchItem> {
self.providers
.iter()
.flat_map(|p| p.items().iter())
.chain(self.static_native_providers.iter().flat_map(|p| p.items().iter()))
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> { pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> {
if query.is_empty() { if query.is_empty() {
// Return recent/popular items when query is empty // Return recent/popular items when query is empty
return self.providers return self.all_static_items()
.iter()
.flat_map(|p| p.items().iter().cloned())
.take(max_results) .take(max_results)
.map(|item| (item, 0)) .map(|item| (item.clone(), 0))
.collect(); .collect();
} }
let mut results: Vec<(LaunchItem, i64)> = self.providers let mut results: Vec<(LaunchItem, i64)> = self.all_static_items()
.iter() .filter_map(|item| {
.flat_map(|provider| { // Match against name and description
provider.items().iter().filter_map(|item| { let name_score = self.matcher.fuzzy_match(&item.name, query);
// Match against name and description let desc_score = item.description
let name_score = self.matcher.fuzzy_match(&item.name, query); .as_ref()
let desc_score = item.description .and_then(|d| self.matcher.fuzzy_match(d, query));
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let score = match (name_score, desc_score) { let score = match (name_score, desc_score) {
(Some(n), Some(d)) => Some(n.max(d)), (Some(n), Some(d)) => Some(n.max(d)),
(Some(n), None) => Some(n), (Some(n), None) => Some(n),
(None, Some(d)) => Some(d / 2), // Lower weight for description matches (None, Some(d)) => Some(d / 2), // Lower weight for description matches
(None, None) => None, (None, None) => None,
}; };
score.map(|s| (item.clone(), s)) score.map(|s| (item.clone(), s))
})
}) })
.collect(); .collect();
@@ -293,38 +313,45 @@ impl ProviderManager {
max_results: usize, max_results: usize,
filter: &crate::filter::ProviderFilter, filter: &crate::filter::ProviderFilter,
) -> Vec<(LaunchItem, i64)> { ) -> Vec<(LaunchItem, i64)> {
// Collect items from core providers
let core_items = self
.providers
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned());
// Collect items from static native providers
let native_items = self
.static_native_providers
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned());
if query.is_empty() { if query.is_empty() {
return self return core_items
.providers .chain(native_items)
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned())
.take(max_results) .take(max_results)
.map(|item| (item, 0)) .map(|item| (item, 0))
.collect(); .collect();
} }
let mut results: Vec<(LaunchItem, i64)> = self let mut results: Vec<(LaunchItem, i64)> = core_items
.providers .chain(native_items)
.iter() .filter_map(|item| {
.filter(|provider| filter.is_active(provider.provider_type())) let name_score = self.matcher.fuzzy_match(&item.name, query);
.flat_map(|provider| { let desc_score = item
provider.items().iter().filter_map(|item| { .description
let name_score = self.matcher.fuzzy_match(&item.name, query); .as_ref()
let desc_score = item .and_then(|d| self.matcher.fuzzy_match(d, query));
.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let score = match (name_score, desc_score) { let score = match (name_score, desc_score) {
(Some(n), Some(d)) => Some(n.max(d)), (Some(n), Some(d)) => Some(n.max(d)),
(Some(n), None) => Some(n), (Some(n), None) => Some(n),
(None, Some(d)) => Some(d / 2), (None, Some(d)) => Some(d / 2),
(None, None) => None, (None, None) => None,
}; };
score.map(|s| (item.clone(), s)) score.map(|s| (item, s))
})
}) })
.collect(); .collect();
@@ -384,11 +411,22 @@ impl ProviderManager {
// Empty query (after checking special providers) - return frecency-sorted items // Empty query (after checking special providers) - return frecency-sorted items
if query.is_empty() { if query.is_empty() {
let items: Vec<(LaunchItem, i64)> = self // Collect items from core providers
let core_items = self
.providers .providers
.iter() .iter()
.filter(|p| filter.is_active(p.provider_type())) .filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned()) .flat_map(|p| p.items().iter().cloned());
// Collect items from static native providers
let native_items = self
.static_native_providers
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned());
let items: Vec<(LaunchItem, i64)> = core_items
.chain(native_items)
.filter(|item| { .filter(|item| {
// Apply tag filter if present // Apply tag filter if present
if let Some(tag) = tag_filter { if let Some(tag) = tag_filter {
@@ -412,53 +450,70 @@ impl ProviderManager {
} }
// Regular search with frecency boost and tag matching // Regular search with frecency boost and tag matching
let search_results: Vec<(LaunchItem, i64)> = self // Helper closure for scoring items
.providers let score_item = |item: &LaunchItem| -> Option<(LaunchItem, i64)> {
.iter() // Apply tag filter if present
.filter(|provider| filter.is_active(provider.provider_type())) if let Some(tag) = tag_filter
.flat_map(|provider| { && !item.tags.iter().any(|t| t.to_lowercase().contains(tag))
provider.items().iter().filter_map(|item| { {
// Apply tag filter if present return None;
if let Some(tag) = tag_filter }
&& !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) {
return None;
}
let name_score = self.matcher.fuzzy_match(&item.name, query); let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item let desc_score = item
.description .description
.as_ref() .as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query)); .and_then(|d| self.matcher.fuzzy_match(d, query));
// Also match against tags (lower weight) // Also match against tags (lower weight)
let tag_score = item let tag_score = item
.tags .tags
.iter() .iter()
.filter_map(|t| self.matcher.fuzzy_match(t, query)) .filter_map(|t| self.matcher.fuzzy_match(t, query))
.max() .max()
.map(|s| s / 3); // Lower weight for tag matches .map(|s| s / 3); // Lower weight for tag matches
let base_score = match (name_score, desc_score, tag_score) { let base_score = match (name_score, desc_score, tag_score) {
(Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)), (Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)),
(Some(n), Some(d), None) => Some(n.max(d)), (Some(n), Some(d), None) => Some(n.max(d)),
(Some(n), None, Some(t)) => Some(n.max(t)), (Some(n), None, Some(t)) => Some(n.max(t)),
(Some(n), None, None) => Some(n), (Some(n), None, None) => Some(n),
(None, Some(d), Some(t)) => Some((d / 2).max(t)), (None, Some(d), Some(t)) => Some((d / 2).max(t)),
(None, Some(d), None) => Some(d / 2), (None, Some(d), None) => Some(d / 2),
(None, None, Some(t)) => Some(t), (None, None, Some(t)) => Some(t),
(None, None, None) => None, (None, None, None) => None,
}; };
base_score.map(|s| { base_score.map(|s| {
let frecency_score = frecency.get_score(&item.id); let frecency_score = frecency.get_score(&item.id);
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64; let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
(item.clone(), s + frecency_boost) (item.clone(), s + frecency_boost)
})
})
}) })
.collect(); };
results.extend(search_results); // Search core providers
for provider in &self.providers {
if !filter.is_active(provider.provider_type()) {
continue;
}
for item in provider.items() {
if let Some(scored) = score_item(item) {
results.push(scored);
}
}
}
// Search static native providers
for provider in &self.static_native_providers {
if !filter.is_active(provider.provider_type()) {
continue;
}
for item in provider.items() {
if let Some(scored) = score_item(item) {
results.push(scored);
}
}
}
results.sort_by(|a, b| b.1.cmp(&a.1)); results.sort_by(|a, b| b.1.cmp(&a.1));
results.truncate(max_results); results.truncate(max_results);
@@ -479,7 +534,11 @@ impl ProviderManager {
/// Get all available provider types (for UI tabs) /// Get all available provider types (for UI tabs)
#[allow(dead_code)] #[allow(dead_code)]
pub fn available_providers(&self) -> Vec<ProviderType> { pub fn available_providers(&self) -> Vec<ProviderType> {
self.providers.iter().map(|p| p.provider_type()).collect() self.providers
.iter()
.map(|p| p.provider_type())
.chain(self.static_native_providers.iter().map(|p| p.provider_type()))
.collect()
} }
/// Get a widget item by type_id (e.g., "pomodoro", "weather", "media") /// Get a widget item by type_id (e.g., "pomodoro", "weather", "media")
@@ -519,6 +578,16 @@ impl ProviderManager {
plugin_id, submenu_query plugin_id, submenu_query
); );
// Search in static native providers (clipboard, emoji, ssh, systemd, etc.)
for provider in &self.static_native_providers {
if provider.type_id() == plugin_id {
let actions = provider.query(&submenu_query);
if !actions.is_empty() {
return Some((display_name.to_string(), actions));
}
}
}
// Search in dynamic providers // Search in dynamic providers
for provider in &self.dynamic_providers { for provider in &self.dynamic_providers {
if provider.type_id() == plugin_id { if provider.type_id() == plugin_id {
@@ -539,23 +608,6 @@ impl ProviderManager {
} }
} }
// Search in static providers (boxed)
// Note: Static providers don't typically have submenu support,
// but we check for completeness
for provider in &self.providers {
if let ProviderType::Plugin(type_id) = provider.provider_type()
&& type_id == plugin_id
{
// Static providers use the items() method, not query
// Submenu support requires dynamic query capability
#[cfg(feature = "dev-logging")]
debug!(
"[Submenu] Plugin '{}' is static, cannot query for submenu",
plugin_id
);
}
}
#[cfg(feature = "dev-logging")] #[cfg(feature = "dev-logging")]
debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id); debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id);

View File

@@ -179,11 +179,18 @@ bump-meta new_version:
done done
echo "Meta-packages bumped to {{new_version}}" echo "Meta-packages bumped to {{new_version}}"
# Bump all non-core crates (plugins + runtimes) to same version # Bump all crates (core + plugins + runtimes) to same version
bump-all new_version: bump-all new_version:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Bump plugins # Bump core
toml="crates/owlry/Cargo.toml"
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
if [ "$old" != "{{new_version}}" ]; then
echo "Bumping owlry from $old to {{new_version}}"
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
fi
# Bump plugins (including plugin-api)
for toml in crates/owlry-plugin-*/Cargo.toml; do for toml in crates/owlry-plugin-*/Cargo.toml; do
crate=$(basename $(dirname "$toml")) crate=$(basename $(dirname "$toml"))
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/') old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
@@ -204,9 +211,9 @@ bump-all new_version:
fi fi
done done
cargo check --workspace cargo check --workspace
git add crates/owlry-plugin-*/Cargo.toml crates/owlry-lua/Cargo.toml crates/owlry-rune/Cargo.toml Cargo.lock git add crates/*/Cargo.toml Cargo.lock
git commit -m "chore: bump all plugins and runtimes to {{new_version}}" git commit -m "chore: bump all crates to {{new_version}}"
echo "All plugins and runtimes bumped to {{new_version}}" echo "All crates bumped to {{new_version}}"
# Bump core version (usage: just bump 0.2.0) # Bump core version (usage: just bump 0.2.0)
bump new_version: bump new_version: