25 Commits

Author SHA1 Message Date
3ef9398655 chore: bump all crates to 0.4.8 2026-01-01 23:30:45 +01:00
46bb4bfb38 chore: bump version to 0.4.8 2026-01-01 23:28:09 +01:00
c8aed5faf5 fix(dmenu): print selection to stdout instead of executing
dmenu mode was incorrectly trying to execute the selected item
as a command (via hyprctl/sh). Now it properly prints the
selection to stdout, enabling standard dmenu piping workflows
like: git branch | owlry -m dmenu | xargs git checkout
2026-01-01 23:28:03 +01:00
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
35a0f580c3 chore(owlry-rune): bump version to 0.4.6 2025-12-30 20:23:58 +01:00
7ed36c58c2 chore(owlry-lua): bump version to 0.4.6 2025-12-30 20:23:57 +01:00
7cccd3b512 chore(plugins): bump all plugins to 0.4.6 2025-12-30 20:23:48 +01:00
9f6d0c5935 chore: bump version to 0.4.6 2025-12-30 20:23:38 +01:00
026a232e0c docs: add ROADMAP.md with feature ideas
- High value/low effort: hot-reload, frecency pruning, :recent, clipboard images
- Medium effort: universal actions, plugin settings UI, result capture
- Bigger bets: window switcher, cross-device sync, natural language, plugin marketplace
- Technical debt: meval→evalexpr, API compat, per-plugin config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 09:04:55 +01:00
1557119448 docs: comprehensive documentation update
README.md:
- Fix bundle package names (add meta- prefix)
- Add Firefox support to bookmarks plugin description
- Add system paths table (plugins, runtimes, example config)
- Add Quick Start section for copying example config
- Expand config example with providers section

docs/PLUGINS.md:
- Add Firefox support to bookmarks
- Fix bundle package names
- Remove outdated [plugins.weather] and [plugins.pomodoro] config examples

docs/PLUGIN_DEVELOPMENT.md:
- Fix Rust edition from 2024 to 2021
- Add position and priority fields to ProviderInfo
- Add ProviderPosition enum documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 08:49:30 +01:00
b814d07382 chore(owlry-rune): bump version to 0.4.5 2025-12-30 08:29:57 +01:00
0dead603ec chore(owlry-lua): bump version to 0.4.5 2025-12-30 08:29:57 +01:00
c1eb5ae2eb chore(plugins): bump all plugins to 0.4.5 2025-12-30 08:29:47 +01:00
07847c76d8 chore: bump version to 0.4.5 2025-12-30 08:29:25 +01:00
2dfce67f3b chore(owlry-rune): bump version to 0.4.4 2025-12-30 08:01:52 +01:00
b1198f4600 chore(owlry-lua): bump version to 0.4.4 2025-12-30 08:01:51 +01:00
e6776b803c chore(plugins): bump all plugins to 0.4.4 2025-12-30 08:01:15 +01:00
6e2d60466b chore: bump version to 0.4.4 2025-12-30 07:45:57 +01:00
8c1cf88474 feat: simplify ProviderType, add plugin priority, fix bookmarks SQLite
Core changes:
- Simplified ProviderType enum to 4 core types + Plugin(String)
- Added priority field to plugin API (API_VERSION = 3)
- Removed hardcoded plugin-specific code from core
- Updated filter.rs to use Plugin(type_id) for all plugins
- Updated main_window.rs UI mappings to derive from type_id
- Fixed weather/media SVG icon colors

Plugin changes:
- All plugins now declare their own priority values
- Widget plugins: weather(12000), pomodoro(11500), media(11000)
- Dynamic plugins: calc(10000), websearch(9000), filesearch(8000)
- Static plugins: priority 0 (frecency-based)

Bookmarks plugin:
- Replaced SQLx with rusqlite + bundled SQLite
- Fixes "undefined symbol: sqlite3_db_config" build errors
- No longer depends on system SQLite version

Config:
- Fixed config.example.toml invalid nested TOML sections
- Removed [providers.websearch], [providers.weather], etc.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 07:45:49 +01:00
ecaaae39e3 refactor(aur): rename meta packages to owlry-meta-*
Renamed for consistency:
- owlry-essentials → owlry-meta-essentials
- owlry-tools → owlry-meta-tools
- owlry-widgets → owlry-meta-widgets
- owlry-full → owlry-meta-full

New packages include replaces/conflicts for smooth transition.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 06:46:19 +01:00
96e9b09a31 docs(justfile): clarify meta-package static versioning
Meta-packages now use static 1.0.0 version, only bumping pkgrel
when dependencies change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 06:42:54 +01:00
e053f7d5d5 refactor(justfile): simplify AUR update for unified versioning
Removed _srcver handling since all packages now share the same version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 06:29:56 +01:00
54 changed files with 1004 additions and 1306 deletions

717
Cargo.lock generated

File diff suppressed because it is too large Load Diff

114
README.md
View File

@@ -32,10 +32,10 @@ yay -S owlry
yay -S owlry-plugin-calculator owlry-plugin-weather
# Or install bundles:
yay -S owlry-essentials # calculator, system, ssh, scripts, bookmarks
yay -S owlry-widgets # weather, media, pomodoro
yay -S owlry-tools # clipboard, emoji, websearch, filesearch, systemd
yay -S owlry-full # everything
yay -S owlry-meta-essentials # calculator, system, ssh, scripts, bookmarks
yay -S owlry-meta-widgets # weather, media, pomodoro
yay -S owlry-meta-tools # clipboard, emoji, websearch, filesearch, systemd
yay -S owlry-meta-full # everything
# For custom Lua/Rune plugins
yay -S owlry-lua # Lua 5.4 runtime
@@ -53,7 +53,7 @@ yay -S owlry-rune # Rune runtime
| `owlry-plugin-clipboard` | History via cliphist |
| `owlry-plugin-emoji` | 400+ searchable emoji |
| `owlry-plugin-scripts` | User scripts |
| `owlry-plugin-bookmarks` | Chrome, Brave, Edge bookmarks |
| `owlry-plugin-bookmarks` | Firefox, Chrome, Brave, Edge bookmarks |
| `owlry-plugin-websearch` | Web search (`? query`) |
| `owlry-plugin-filesearch` | File search (`/ filename`) |
| `owlry-plugin-systemd` | User services with actions |
@@ -99,12 +99,40 @@ sudo cp target/release/libowlry_plugin_*.so /usr/lib/owlry/plugins/
## Usage
```bash
owlry # Launch with defaults
owlry --mode app # Applications only
owlry --providers app,cmd # Specific providers
owlry --help # Show all options
owlry # Launch with all providers
owlry -m app # Applications only
owlry -m cmd # PATH commands only
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
| Key | Action |
@@ -158,6 +186,21 @@ Owlry follows the [XDG Base Directory Specification](https://specifications.free
| `~/.local/share/owlry/scripts/` | User scripts |
| `~/.local/share/owlry/frecency.json` | Usage history |
System locations:
| Path | Purpose |
|------|---------|
| `/usr/lib/owlry/plugins/*.so` | Installed native plugins |
| `/usr/lib/owlry/runtimes/*.so` | Lua/Rune script runtimes |
| `/usr/share/doc/owlry/config.example.toml` | Example configuration |
### Quick Start
```bash
# Copy example config
mkdir -p ~/.config/owlry
cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml
```
### Example Configuration
```toml
@@ -169,8 +212,8 @@ tabs = ["app", "cmd", "uuctl"]
# launch_wrapper = "uwsm app --" # Auto-detected
[appearance]
width = 700
height = 500
width = 850
height = 650
font_size = 14
border_radius = 12
# theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc.
@@ -178,17 +221,18 @@ border_radius = 12
[plugins]
disabled = [] # Plugin IDs to disable, e.g., ["emoji", "pomodoro"]
# Per-plugin configuration (new in 0.4.0)
[plugins.weather]
provider = "wttr.in" # or: openweathermap, open-meteo
location = "Berlin" # city name or "lat,lon"
# api_key = "..." # Required for OpenWeatherMap
[providers]
applications = true # .desktop files
commands = true # PATH executables
frecency = true # Boost frequently used items
frecency_weight = 0.3 # 0.0-1.0
[plugins.pomodoro]
work_mins = 25 # Work session duration
break_mins = 5 # Break duration
# Web search engine: google, duckduckgo, bing, startpage, brave, ecosia
search_engine = "duckduckgo"
```
See `/usr/share/doc/owlry/config.example.toml` for all options with documentation.
## Plugin System
Owlry uses a modular plugin architecture. Plugins are loaded from:
@@ -205,6 +249,38 @@ Add plugin IDs to the disabled list in your config:
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
See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for:

109
ROADMAP.md Normal file
View File

@@ -0,0 +1,109 @@
# Owlry Roadmap
Feature ideas and future development plans for Owlry.
## High Value, Low Effort
### Plugin hot-reload
Detect `.so` file changes in `/usr/lib/owlry/plugins/` and reload without restarting the launcher. The loader infrastructure already exists.
### Frecency pruning
Add `max_entries` and `max_age_days` config options. Prune old entries on startup to prevent `frecency.json` from growing unbounded.
### `:recent` prefix
Show last N launched items. Data already exists in frecency.json — just needs a provider to surface it.
### Clipboard images
`cliphist` supports images. Extend the clipboard plugin to show image thumbnails in results.
---
## Medium Effort, High Value
### Actions on any result
Generalize the submenu system beyond systemd. Every result type gets contextual actions:
| Provider | Actions |
|----------|---------|
| Applications | Open, Open in terminal, Show .desktop location |
| Files | Open, Open folder, Copy path, Delete |
| SSH | Connect, Copy hostname, Edit config |
| Bookmarks | Open, Copy URL, Open incognito |
| Clipboard | Paste, Delete from history |
This is the difference between a launcher and a command palette.
### Plugin settings UI
A `:settings` provider that lists installed plugins and their configurable options. Edit values inline, writes to `config.toml`.
### Result action capture
Calculator shows `= 5+3 → 8`. Allow pressing Tab or Ctrl+C to copy the result to clipboard instead of "launching" it. Useful for calculator, file paths, URLs.
---
## Bigger Bets
### Window switcher with live thumbnails
A `windows` plugin using Wayland screencopy to show live thumbnails of open windows. Hyprland and Sway expose window lists via IPC. Could replace Alt+Tab.
### Cross-device bookmark sync
Firefox and Chrome sync bookmarks across devices. Parse sync metadata to show "recently added on other devices" or "bookmarks from phone".
### Natural language commands
Parse simple natural language into system commands:
```
"shutdown in 30 minutes" → systemd-run --user --on-active=30m systemctl poweroff
"remind me in 1 hour" → notify-send scheduled via at/systemd timer
"volume 50%" → wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.5
```
Local pattern matching, no AI/cloud required.
### Plugin marketplace
A curated registry of third-party Lua/Rune plugins with one-command install:
```bash
owlry plugin install github-notifications
owlry plugin install todoist
owlry plugin install spotify-controls
```
The script runtimes make this viable without recompiling.
---
## Technical Debt
### Split monorepo for user build efficiency
Currently, a small core fix requires all 16 AUR packages to rebuild (same source tarball). Split into 3 repos:
| Repo | Contents | Versioning |
|------|----------|------------|
| `owlry` | Core binary | Independent |
| `owlry-plugin-api` | ABI interface (crates.io) | Semver, conservative |
| `owlry-plugins` | 13 plugins + 2 runtimes | Independent per plugin |
**Execution order:**
1. Publish `owlry-plugin-api` to crates.io
2. Update monorepo to use crates.io dependency
3. Create `owlry-plugins` repo, move plugins + runtimes
4. Slim current repo to core-only
5. Update AUR PKGBUILDs with new source URLs
**Benefit:** Core bugfix = 1 rebuild. Plugin fix = 1 rebuild. Third-party plugins possible via crates.io.
### Replace meval with evalexpr
`meval` depends on `nom v1.2.4` which will be rejected by future Rust versions. Migrate calculator plugin and Lua runtime to `evalexpr` v13+.
### Plugin API backwards compatibility
When `API_VERSION` increments, provide a compatibility shim so v3 plugins work with v4 core. Prevents ecosystem fragmentation.
### Per-plugin configuration
Current flat `[providers]` config doesn't scale. Design a `[plugins.weather]`, `[plugins.pomodoro]` structure that plugins can declare and the core validates.
---
## Priority
If we had to pick one: **Actions on any result**. It transforms every provider from "search and launch" to "search and do anything". The ROI is massive.

View File

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

View File

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

View File

@@ -31,7 +31,9 @@ use abi_stable::StableAbi;
pub use abi_stable::std_types::{ROption, RStr, RString, RVec};
/// Current plugin API version - plugins must match this
pub const API_VERSION: u32 = 1;
/// v2: Added ProviderPosition for widget support
/// v3: Added priority field for plugin-declared result ordering
pub const API_VERSION: u32 = 3;
/// Plugin metadata returned by the info function
#[repr(C)]
@@ -65,6 +67,14 @@ pub struct ProviderInfo {
pub provider_type: ProviderKind,
/// Short type identifier for UI badges (e.g., "calc", "web")
pub type_id: RString,
/// Display position (Normal or Widget)
pub position: ProviderPosition,
/// Priority for result ordering (higher values appear first)
/// Suggested ranges:
/// - Widgets: 10000-12000
/// - Dynamic providers: 7000-10000
/// - Static providers: 0-5000 (use 0 for frecency-based ordering)
pub priority: i32,
}
/// Provider behavior type
@@ -77,6 +87,20 @@ pub enum ProviderKind {
Dynamic,
}
/// Provider display position
///
/// Controls where in the result list this provider's items appear.
#[repr(C)]
#[derive(StableAbi, Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ProviderPosition {
/// Standard position in results (sorted by score/frecency)
#[default]
Normal,
/// Widget position - appears at top of results when query is empty
/// Widgets are always visible regardless of filter settings
Widget,
}
/// A single searchable/launchable item returned by providers
#[repr(C)]
#[derive(StableAbi, Clone, Debug)]

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-plugin-bookmarks"
version = "0.4.3"
version = "0.4.8"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
@@ -27,5 +27,5 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# For reading Firefox bookmarks (places.sqlite)
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1", features = ["rt-multi-thread"] }
# Use bundled SQLite to avoid system library version conflicts
rusqlite = { version = "0.32", features = ["bundled"] }

View File

@@ -3,7 +3,7 @@
//! A static provider that reads browser bookmarks from various browsers.
//!
//! Supported browsers:
//! - Firefox (via places.sqlite using SQLx)
//! - Firefox (via places.sqlite using rusqlite with bundled SQLite)
//! - Chrome
//! - Chromium
//! - Brave
@@ -11,11 +11,11 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
use rusqlite::{Connection, OpenFlags};
use serde::Deserialize;
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::Row;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -248,19 +248,9 @@ impl BookmarksState {
}
}
// Load Firefox bookmarks with favicons (async via tokio)
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => {
loading.store(false, Ordering::SeqCst);
return;
}
};
// Load Firefox bookmarks with favicons (synchronous with rusqlite)
for path in Self::firefox_places_paths() {
rt.block_on(async {
Self::read_firefox_bookmarks_async(&path, &mut items).await;
});
Self::read_firefox_bookmarks(&path, &mut items);
}
// Save to cache for next startup
@@ -323,21 +313,24 @@ impl BookmarksState {
}
}
/// Read Firefox bookmarks asynchronously
async fn read_firefox_bookmarks_async(places_path: &PathBuf, items: &mut Vec<PluginItem>) {
/// Read Firefox bookmarks using rusqlite (synchronous, bundled SQLite)
fn read_firefox_bookmarks(places_path: &PathBuf, items: &mut Vec<PluginItem>) {
let temp_dir = std::env::temp_dir();
let temp_db = temp_dir.join("owlry_places_temp.sqlite");
// Copy database to temp location to avoid locking issues
if fs::copy(places_path, &temp_db).is_err() {
return;
}
// Also copy WAL file if it exists
let wal_path = places_path.with_extension("sqlite-wal");
if wal_path.exists() {
let temp_wal = temp_db.with_extension("sqlite-wal");
let _ = fs::copy(&wal_path, &temp_wal);
}
// Copy favicons database if available
let favicons_path = Self::firefox_favicons_path(places_path);
let temp_favicons = temp_dir.join("owlry_favicons_temp.sqlite");
if let Some(ref fp) = favicons_path {
@@ -348,21 +341,10 @@ impl BookmarksState {
}
}
let db_url = format!("sqlite:{}?mode=ro", temp_db.display());
let favicons_url = if favicons_path.is_some() {
Some(format!("sqlite:{}?mode=ro", temp_favicons.display()))
} else {
None
};
let cache_dir = Self::ensure_favicon_cache_dir();
let bookmarks = Self::fetch_firefox_bookmarks_with_favicons(
&db_url,
favicons_url.as_deref(),
cache_dir.as_ref(),
)
.await;
// Read bookmarks from places.sqlite
let bookmarks = Self::fetch_firefox_bookmarks(&temp_db, &temp_favicons, cache_dir.as_ref());
// Clean up temp files
let _ = fs::remove_file(&temp_db);
@@ -385,19 +367,18 @@ impl BookmarksState {
}
}
/// Fetch Firefox bookmarks with their favicons
async fn fetch_firefox_bookmarks_with_favicons(
places_url: &str,
favicons_url: Option<&str>,
/// Fetch Firefox bookmarks with optional favicons
fn fetch_firefox_bookmarks(
places_path: &Path,
favicons_path: &Path,
cache_dir: Option<&PathBuf>,
) -> Vec<(String, String, Option<String>)> {
// First, fetch bookmarks from places.sqlite
let pool = match SqlitePoolOptions::new()
.max_connections(1)
.connect(places_url)
.await
{
Ok(p) => p,
// Open places.sqlite in read-only mode
let conn = match Connection::open_with_flags(
places_path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
@@ -416,43 +397,38 @@ impl BookmarksState {
LIMIT 500
"#;
let rows = match sqlx::query(query).fetch_all(&pool).await {
Ok(r) => r,
let mut stmt = match conn.prepare(query) {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let bookmarks: Vec<(String, String)> = rows
.into_iter()
.filter_map(|row| {
let title: Option<String> = row.get("title");
let url: Option<String> = row.get("url");
match (title, url) {
(Some(t), Some(u)) => Some((t, u)),
_ => None,
}
let bookmarks: Vec<(String, String)> = stmt
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.collect();
.ok()
.map(|rows| rows.filter_map(|r| r.ok()).collect())
.unwrap_or_default();
// If no favicons database or cache dir, return without favicons
let (favicons_url, cache_dir) = match (favicons_url, cache_dir) {
(Some(f), Some(c)) => (f, c),
_ => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(),
// If no favicons or cache dir, return without favicons
let cache_dir = match cache_dir {
Some(c) => c,
None => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(),
};
// Connect to favicons database
let fav_pool = match SqlitePoolOptions::new()
.max_connections(1)
.connect(favicons_url)
.await
{
Ok(p) => p,
// Try to open favicons database
let fav_conn = match Connection::open_with_flags(
favicons_path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
) {
Ok(c) => c,
Err(_) => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(),
};
// Fetch favicons for each URL
let mut results = Vec::new();
for (title, url) in bookmarks {
let favicon_path = Self::get_favicon_for_url(&fav_pool, &url, cache_dir).await;
let favicon_path = Self::get_favicon_for_url(&fav_conn, &url, cache_dir);
results.push((title, url, favicon_path));
}
@@ -460,8 +436,8 @@ impl BookmarksState {
}
/// Get favicon for a URL, caching to file if needed
async fn get_favicon_for_url(
pool: &sqlx::SqlitePool,
fn get_favicon_for_url(
conn: &Connection,
page_url: &str,
cache_dir: &Path,
) -> Option<String> {
@@ -486,13 +462,11 @@ impl BookmarksState {
LIMIT 1
"#;
let row = sqlx::query(query)
.bind(page_url)
.fetch_optional(pool)
.await
.ok()??;
let data: Option<Vec<u8>> = conn
.query_row(query, [page_url], |row| row.get(0))
.ok();
let data: Vec<u8> = row.get("data");
let data = data?;
if data.is_empty() {
return None;
}
@@ -549,6 +523,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 0, // Static: use frecency ordering
}]
.into()
}
@@ -623,7 +599,7 @@ mod tests {
// This will find paths if Firefox is installed
let paths = BookmarksState::firefox_places_paths();
// Path detection should work (may be empty if Firefox not installed)
assert!(paths.len() >= 0);
let _ = paths.len(); // Just ensure it doesn't panic
}
#[test]
@@ -656,7 +632,7 @@ mod tests {
#[test]
fn test_process_folder() {
let mut state = BookmarksState::new();
let mut items = Vec::new();
let folder = ChromeBookmarkNode {
name: Some("Test Folder".to_string()),
@@ -672,9 +648,9 @@ mod tests {
]),
};
BookmarksState::process_chrome_folder_static(&folder, &mut state.items);
assert_eq!(state.items.len(), 1);
assert_eq!(state.items[0].name.as_str(), "Test Bookmark");
BookmarksState::process_chrome_folder_static(&folder, &mut items);
assert_eq!(items.len(), 1);
assert_eq!(items[0].name.as_str(), "Test Bookmark");
}
#[test]

View File

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

View File

@@ -10,7 +10,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
// Plugin metadata
@@ -51,6 +52,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Dynamic,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 10000, // Dynamic: calculator results first
}]
.into()
}

View File

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

View File

@@ -9,7 +9,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
use std::process::Command;
@@ -137,6 +138,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 0, // Static: use frecency ordering
}]
.into()
}

View File

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

View File

@@ -9,7 +9,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
// Plugin metadata
@@ -453,6 +454,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 0, // Static: use frecency ordering
}]
.into()
}

View File

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

View File

@@ -12,7 +12,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
use std::path::Path;
use std::process::Command;
@@ -207,6 +208,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Dynamic,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 8000, // Dynamic: file search
}]
.into()
}

View File

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

View File

@@ -5,7 +5,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
use std::process::Command;
@@ -355,6 +356,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Widget,
priority: 11000, // Widget: media player
}]
.into()
}

View File

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

View File

@@ -16,7 +16,7 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
notify_with_urgency, owlry_plugin, NotifyUrgency, PluginInfo, PluginItem, ProviderHandle,
ProviderInfo, ProviderKind, API_VERSION,
ProviderInfo, ProviderKind, ProviderPosition, API_VERSION,
};
use serde::{Deserialize, Serialize};
use std::fs;
@@ -396,6 +396,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Widget,
priority: 11500, // Widget: pomodoro timer
}]
.into()
}

View File

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

View File

@@ -12,7 +12,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
use std::fs;
use std::os::unix::fs::PermissionsExt;
@@ -187,6 +188,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 0, // Static: use frecency ordering
}]
.into()
}

View File

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

View File

@@ -9,7 +9,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
use std::fs;
use std::path::PathBuf;
@@ -204,6 +205,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 0, // Static: use frecency ordering
}]
.into()
}

View File

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

View File

@@ -13,7 +13,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
// Plugin metadata
@@ -129,6 +130,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 0, // Static: use frecency ordering
}]
.into()
}

View File

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

View File

@@ -10,7 +10,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
use std::process::Command;
@@ -285,6 +286,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 0, // Static: use frecency ordering
}]
.into()
}

View File

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

View File

@@ -20,7 +20,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
use serde::{Deserialize, Serialize};
use std::fs;
@@ -642,6 +643,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Widget,
priority: 12000, // Widget: highest priority
}]
.into()
}

View File

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

View File

@@ -10,7 +10,8 @@
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, API_VERSION,
};
// Plugin metadata
@@ -164,6 +165,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Dynamic,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 9000, // Dynamic: web search
}]
.into()
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry"
version = "0.4.3"
version = "0.4.8"
edition = "2024"
rust-version = "1.90"
description = "A lightweight, owl-themed application launcher for Wayland"

View File

@@ -10,19 +10,55 @@ use crate::providers::ProviderType;
#[command(
name = "owlry",
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 {
/// Start in single-provider mode (app, cmd, uuctl)
#[arg(long, short = 'm', value_parser = parse_provider)]
/// Start in single-provider mode
///
/// 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>,
/// Comma-separated list of enabled providers (app,cmd,uuctl)
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)]
/// Comma-separated list of enabled providers
///
/// 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>>,
/// Custom prompt text for the search input (useful for dmenu mode)
#[arg(long)]
/// Custom prompt text for the search input
///
/// Useful in dmenu mode to indicate what the user is selecting.
/// Example: --prompt "Select file:"
#[arg(long, value_name = "TEXT")]
pub prompt: Option<String>,
/// Subcommand to run (if any)

View File

@@ -37,43 +37,44 @@ impl ProviderFilter {
} else {
// Use config file settings, default to apps only
let mut set = HashSet::new();
// Core providers
if config_providers.applications {
set.insert(ProviderType::Application);
}
if config_providers.commands {
set.insert(ProviderType::Command);
}
// Plugin providers - use Plugin(type_id) for all
if config_providers.uuctl {
set.insert(ProviderType::Uuctl);
set.insert(ProviderType::Plugin("uuctl".to_string()));
}
if config_providers.system {
set.insert(ProviderType::System);
set.insert(ProviderType::Plugin("system".to_string()));
}
if config_providers.ssh {
set.insert(ProviderType::Ssh);
set.insert(ProviderType::Plugin("ssh".to_string()));
}
if config_providers.clipboard {
set.insert(ProviderType::Clipboard);
set.insert(ProviderType::Plugin("clipboard".to_string()));
}
if config_providers.bookmarks {
set.insert(ProviderType::Bookmarks);
set.insert(ProviderType::Plugin("bookmarks".to_string()));
}
if config_providers.emoji {
set.insert(ProviderType::Emoji);
set.insert(ProviderType::Plugin("emoji".to_string()));
}
if config_providers.scripts {
set.insert(ProviderType::Scripts);
set.insert(ProviderType::Plugin("scripts".to_string()));
}
// Dynamic providers: add to filter set so they work in "All" mode
// but can still be excluded when in single-provider mode
// Dynamic providers
if config_providers.files {
set.insert(ProviderType::Files);
set.insert(ProviderType::Plugin("filesearch".to_string()));
}
if config_providers.calculator {
set.insert(ProviderType::Calculator);
set.insert(ProviderType::Plugin("calc".to_string()));
}
if config_providers.websearch {
set.insert(ProviderType::WebSearch);
set.insert(ProviderType::Plugin("websearch".to_string()));
}
// Default to apps if nothing enabled
if set.is_empty() {
@@ -170,6 +171,7 @@ impl ProviderFilter {
}
/// Parse query for prefix syntax
/// Prefixes map to Plugin(type_id) for plugin providers
pub fn parse_query(query: &str) -> ParsedQuery {
let trimmed = query.trim_start();
@@ -197,37 +199,57 @@ impl ProviderFilter {
}
}
// Check for prefix patterns (with trailing space)
let prefixes = [
// Core provider prefixes
let core_prefixes: &[(&str, ProviderType)] = &[
(":app ", ProviderType::Application),
(":apps ", ProviderType::Application),
(":bm ", ProviderType::Bookmarks),
(":bookmark ", ProviderType::Bookmarks),
(":bookmarks ", ProviderType::Bookmarks),
(":calc ", ProviderType::Calculator),
(":calculator ", ProviderType::Calculator),
(":clip ", ProviderType::Clipboard),
(":clipboard ", ProviderType::Clipboard),
(":cmd ", ProviderType::Command),
(":command ", ProviderType::Command),
(":emoji ", ProviderType::Emoji),
(":emojis ", ProviderType::Emoji),
(":file ", ProviderType::Files),
(":files ", ProviderType::Files),
(":find ", ProviderType::Files),
(":script ", ProviderType::Scripts),
(":scripts ", ProviderType::Scripts),
(":ssh ", ProviderType::Ssh),
(":sys ", ProviderType::System),
(":system ", ProviderType::System),
(":power ", ProviderType::System),
(":uuctl ", ProviderType::Uuctl),
(":web ", ProviderType::WebSearch),
(":search ", ProviderType::WebSearch),
];
for (prefix_str, provider) in prefixes {
// Plugin provider prefixes - mapped to Plugin(type_id)
let plugin_prefixes: &[(&str, &str)] = &[
(":bm ", "bookmarks"),
(":bookmark ", "bookmarks"),
(":bookmarks ", "bookmarks"),
(":calc ", "calc"),
(":calculator ", "calc"),
(":clip ", "clipboard"),
(":clipboard ", "clipboard"),
(":emoji ", "emoji"),
(":emojis ", "emoji"),
(":file ", "filesearch"),
(":files ", "filesearch"),
(":find ", "filesearch"),
(":script ", "scripts"),
(":scripts ", "scripts"),
(":ssh ", "ssh"),
(":sys ", "system"),
(":system ", "system"),
(":power ", "system"),
(":uuctl ", "uuctl"),
(":systemd ", "uuctl"),
(":web ", "websearch"),
(":search ", "websearch"),
];
// Check core prefixes
for (prefix_str, provider) in core_prefixes {
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
return ParsedQuery {
prefix: Some(provider.clone()),
tag_filter: None,
query: rest.to_string(),
};
}
}
// Check plugin prefixes
for (prefix_str, type_id) in plugin_prefixes {
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
let provider = ProviderType::Plugin(type_id.to_string());
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
return ParsedQuery {
@@ -238,37 +260,54 @@ impl ProviderFilter {
}
}
// Handle prefix without trailing space (still typing)
let partial_prefixes = [
// Handle partial prefixes (still typing)
let partial_core: &[(&str, ProviderType)] = &[
(":app", ProviderType::Application),
(":apps", ProviderType::Application),
(":bm", ProviderType::Bookmarks),
(":bookmark", ProviderType::Bookmarks),
(":bookmarks", ProviderType::Bookmarks),
(":calc", ProviderType::Calculator),
(":calculator", ProviderType::Calculator),
(":clip", ProviderType::Clipboard),
(":clipboard", ProviderType::Clipboard),
(":cmd", ProviderType::Command),
(":command", ProviderType::Command),
(":emoji", ProviderType::Emoji),
(":emojis", ProviderType::Emoji),
(":file", ProviderType::Files),
(":files", ProviderType::Files),
(":find", ProviderType::Files),
(":script", ProviderType::Scripts),
(":scripts", ProviderType::Scripts),
(":ssh", ProviderType::Ssh),
(":sys", ProviderType::System),
(":system", ProviderType::System),
(":power", ProviderType::System),
(":uuctl", ProviderType::Uuctl),
(":web", ProviderType::WebSearch),
(":search", ProviderType::WebSearch),
];
for (prefix_str, provider) in partial_prefixes {
if trimmed == prefix_str {
let partial_plugin: &[(&str, &str)] = &[
(":bm", "bookmarks"),
(":bookmark", "bookmarks"),
(":bookmarks", "bookmarks"),
(":calc", "calc"),
(":calculator", "calc"),
(":clip", "clipboard"),
(":clipboard", "clipboard"),
(":emoji", "emoji"),
(":emojis", "emoji"),
(":file", "filesearch"),
(":files", "filesearch"),
(":find", "filesearch"),
(":script", "scripts"),
(":scripts", "scripts"),
(":ssh", "ssh"),
(":sys", "system"),
(":system", "system"),
(":power", "system"),
(":uuctl", "uuctl"),
(":systemd", "uuctl"),
(":web", "websearch"),
(":search", "websearch"),
];
for (prefix_str, provider) in partial_core {
if trimmed == *prefix_str {
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
return ParsedQuery {
prefix: Some(provider.clone()),
tag_filter: None,
query: String::new(),
};
}
}
for (prefix_str, type_id) in partial_plugin {
if trimmed == *prefix_str {
let provider = ProviderType::Plugin(type_id.to_string());
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
return ParsedQuery {
@@ -296,22 +335,9 @@ impl ProviderFilter {
let mut providers: Vec<_> = self.enabled.iter().cloned().collect();
providers.sort_by_key(|p| match p {
ProviderType::Application => 0,
ProviderType::Bookmarks => 1,
ProviderType::Calculator => 2,
ProviderType::Clipboard => 3,
ProviderType::Command => 4,
ProviderType::Dmenu => 5,
ProviderType::Emoji => 6,
ProviderType::Files => 7,
ProviderType::MediaPlayer => 8,
ProviderType::Pomodoro => 9,
ProviderType::Scripts => 10,
ProviderType::Ssh => 11,
ProviderType::System => 12,
ProviderType::Uuctl => 13,
ProviderType::Weather => 14,
ProviderType::WebSearch => 15,
ProviderType::Plugin(_) => 100, // Plugin providers sort last
ProviderType::Command => 1,
ProviderType::Dmenu => 2,
ProviderType::Plugin(_) => 100, // Plugin providers sort after core
});
providers
}
@@ -321,21 +347,8 @@ impl ProviderFilter {
if let Some(ref prefix) = self.active_prefix {
return match prefix {
ProviderType::Application => "Apps",
ProviderType::Bookmarks => "Bookmarks",
ProviderType::Calculator => "Calc",
ProviderType::Clipboard => "Clipboard",
ProviderType::Command => "Commands",
ProviderType::Dmenu => "dmenu",
ProviderType::Emoji => "Emoji",
ProviderType::Files => "Files",
ProviderType::MediaPlayer => "Media",
ProviderType::Pomodoro => "Pomodoro",
ProviderType::Scripts => "Scripts",
ProviderType::Ssh => "SSH",
ProviderType::System => "System",
ProviderType::Uuctl => "uuctl",
ProviderType::Weather => "Weather",
ProviderType::WebSearch => "Web",
ProviderType::Plugin(_) => "Plugin",
};
}
@@ -344,21 +357,8 @@ impl ProviderFilter {
if enabled.len() == 1 {
match &enabled[0] {
ProviderType::Application => "Apps",
ProviderType::Bookmarks => "Bookmarks",
ProviderType::Calculator => "Calc",
ProviderType::Clipboard => "Clipboard",
ProviderType::Command => "Commands",
ProviderType::Dmenu => "dmenu",
ProviderType::Emoji => "Emoji",
ProviderType::Files => "Files",
ProviderType::MediaPlayer => "Media",
ProviderType::Pomodoro => "Pomodoro",
ProviderType::Scripts => "Scripts",
ProviderType::Ssh => "SSH",
ProviderType::System => "System",
ProviderType::Uuctl => "uuctl",
ProviderType::Weather => "Weather",
ProviderType::WebSearch => "Web",
ProviderType::Plugin(_) => "Plugin",
}
} else {
@@ -392,6 +392,13 @@ mod tests {
assert_eq!(result.query, "");
}
#[test]
fn test_parse_query_plugin_prefix() {
let result = ProviderFilter::parse_query(":calc 5+3");
assert_eq!(result.prefix, Some(ProviderType::Plugin("calc".to_string())));
assert_eq!(result.query, "5+3");
}
#[test]
fn test_toggle_ensures_one_enabled() {
let mut filter = ProviderFilter::apps_only();

View File

@@ -44,27 +44,17 @@ pub struct LaunchItem {
/// Provider type identifier for filtering and badge display
///
/// Note: Plugin is a special case that stores a type_id string
/// for custom plugin-defined provider types.
/// Core types are built-in providers. All native plugins use Plugin(type_id).
/// This keeps the core app free of plugin-specific knowledge.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ProviderType {
/// Built-in: Desktop applications from XDG directories
Application,
Bookmarks,
Calculator,
Clipboard,
/// Built-in: Shell commands from PATH
Command,
/// Built-in: Pipe-based input (dmenu compatibility)
Dmenu,
Emoji,
Files,
MediaPlayer,
Pomodoro,
Scripts,
Ssh,
System,
Uuctl,
Weather,
WebSearch,
/// Plugin-defined provider type with custom type_id
/// Plugin-defined provider type with its type_id (e.g., "calc", "weather", "emoji")
Plugin(String),
}
@@ -73,27 +63,11 @@ impl std::str::FromStr for ProviderType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
// Core built-in providers
"app" | "apps" | "application" | "applications" => Ok(ProviderType::Application),
"bookmark" | "bookmarks" | "bm" => Ok(ProviderType::Bookmarks),
"calc" | "calculator" => Ok(ProviderType::Calculator),
"clip" | "clipboard" => Ok(ProviderType::Clipboard),
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
"dmenu" => Ok(ProviderType::Dmenu),
"emoji" | "emojis" => Ok(ProviderType::Emoji),
"file" | "files" | "find" | "filesearch" => Ok(ProviderType::Files),
"media" | "mpris" | "player" => Ok(ProviderType::MediaPlayer),
"pomo" | "pomodoro" | "timer" => Ok(ProviderType::Pomodoro),
"script" | "scripts" => Ok(ProviderType::Scripts),
"ssh" => Ok(ProviderType::Ssh),
"sys" | "system" | "power" => Ok(ProviderType::System),
"uuctl" | "systemd" => Ok(ProviderType::Uuctl),
"weather" => Ok(ProviderType::Weather),
"web" | "websearch" | "search" => Ok(ProviderType::WebSearch),
// Plugin types are prefixed with "plugin:" (e.g., "plugin:github-repos")
other if other.starts_with("plugin:") => {
Ok(ProviderType::Plugin(other[7..].to_string()))
}
// Unknown types become plugin types
// Everything else is a plugin
other => Ok(ProviderType::Plugin(other.to_string())),
}
}
@@ -103,21 +77,8 @@ impl std::fmt::Display for ProviderType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProviderType::Application => write!(f, "app"),
ProviderType::Bookmarks => write!(f, "bookmark"),
ProviderType::Calculator => write!(f, "calc"),
ProviderType::Clipboard => write!(f, "clip"),
ProviderType::Command => write!(f, "cmd"),
ProviderType::Dmenu => write!(f, "dmenu"),
ProviderType::Emoji => write!(f, "emoji"),
ProviderType::Files => write!(f, "file"),
ProviderType::MediaPlayer => write!(f, "media"),
ProviderType::Pomodoro => write!(f, "pomo"),
ProviderType::Scripts => write!(f, "script"),
ProviderType::Ssh => write!(f, "ssh"),
ProviderType::System => write!(f, "sys"),
ProviderType::Uuctl => write!(f, "uuctl"),
ProviderType::Weather => write!(f, "weather"),
ProviderType::WebSearch => write!(f, "web"),
ProviderType::Plugin(type_id) => write!(f, "{}", type_id),
}
}
@@ -134,8 +95,10 @@ pub trait Provider: Send {
/// Manages all providers and handles searching
pub struct ProviderManager {
/// Static providers (apps, commands, and native static plugins)
/// Core static providers (apps, commands, dmenu)
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)
/// These are queried per-keystroke, not cached
dynamic_providers: Vec<NativeProvider>,
@@ -146,22 +109,18 @@ pub struct ProviderManager {
matcher: SkimMatcherV2,
}
/// Known dynamic provider type IDs (need per-query evaluation)
const DYNAMIC_TYPE_IDS: &[&str] = &["calc", "websearch", "filesearch"];
/// Known widget provider type IDs (appear at top of results)
const WIDGET_TYPE_IDS: &[&str] = &["weather", "media", "pomodoro"];
impl ProviderManager {
/// Create a new ProviderManager with native plugins
///
/// Native plugins are loaded from /usr/lib/owlry/plugins/ and categorized into:
/// - Static providers (added to providers vec)
/// - Dynamic providers (queried per-keystroke: calculator, websearch, filesearch)
/// - Widget providers (shown at top: weather, media, pomodoro)
/// Native plugins are loaded from /usr/lib/owlry/plugins/ and categorized based on
/// their declared ProviderKind and ProviderPosition:
/// - Static providers with Normal position (added to providers vec)
/// - Dynamic providers (queried per-keystroke, declared via ProviderKind::Dynamic)
/// - Widget providers (shown at top, declared via ProviderPosition::Widget)
pub fn with_native_plugins(native_providers: Vec<NativeProvider>) -> Self {
let mut manager = Self {
providers: Vec::new(),
static_native_providers: Vec::new(),
dynamic_providers: Vec::new(),
widget_providers: Vec::new(),
matcher: SkimMatcherV2::default(),
@@ -180,19 +139,22 @@ impl ProviderManager {
manager.providers.push(Box::new(ApplicationProvider::new()));
manager.providers.push(Box::new(CommandProvider::new()));
// Categorize native plugins
// Categorize native plugins based on their declared ProviderKind and ProviderPosition
for provider in native_providers {
let type_id = provider.type_id();
if DYNAMIC_TYPE_IDS.contains(&type_id) {
if provider.is_dynamic() {
// Dynamic providers declare ProviderKind::Dynamic
info!("Registered dynamic provider: {} ({})", provider.name(), type_id);
manager.dynamic_providers.push(provider);
} else if WIDGET_TYPE_IDS.contains(&type_id) {
} else if provider.is_widget() {
// Widgets declare ProviderPosition::Widget
info!("Registered widget provider: {} ({})", provider.name(), type_id);
manager.widget_providers.push(provider);
} else {
// Static native providers (keep as NativeProvider for query/submenu support)
info!("Registered static provider: {} ({})", provider.name(), type_id);
manager.providers.push(Box::new(provider));
manager.static_native_providers.push(provider);
}
}
}
@@ -211,7 +173,7 @@ impl ProviderManager {
}
pub fn refresh_all(&mut self) {
// Refresh static providers (fast, local operations)
// Refresh core providers (apps, commands)
for provider in &mut self.providers {
provider.refresh();
info!(
@@ -221,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
// Call refresh_widgets() after window is shown
@@ -242,9 +214,13 @@ impl ProviderManager {
}
/// 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> {
// 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) {
return Some(p);
}
@@ -287,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)]
pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> {
if query.is_empty() {
// Return recent/popular items when query is empty
return self.providers
.iter()
.flat_map(|p| p.items().iter().cloned())
return self.all_static_items()
.take(max_results)
.map(|item| (item, 0))
.map(|item| (item.clone(), 0))
.collect();
}
let mut results: Vec<(LaunchItem, i64)> = self.providers
.iter()
.flat_map(|provider| {
provider.items().iter().filter_map(|item| {
// Match against name and description
let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let mut results: Vec<(LaunchItem, i64)> = self.all_static_items()
.filter_map(|item| {
// Match against name and description
let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let score = match (name_score, desc_score) {
(Some(n), Some(d)) => Some(n.max(d)),
(Some(n), None) => Some(n),
(None, Some(d)) => Some(d / 2), // Lower weight for description matches
(None, None) => None,
};
let score = match (name_score, desc_score) {
(Some(n), Some(d)) => Some(n.max(d)),
(Some(n), None) => Some(n),
(None, Some(d)) => Some(d / 2), // Lower weight for description matches
(None, None) => None,
};
score.map(|s| (item.clone(), s))
})
score.map(|s| (item.clone(), s))
})
.collect();
@@ -334,38 +313,45 @@ impl ProviderManager {
max_results: usize,
filter: &crate::filter::ProviderFilter,
) -> 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() {
return self
.providers
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned())
return core_items
.chain(native_items)
.take(max_results)
.map(|item| (item, 0))
.collect();
}
let mut results: Vec<(LaunchItem, i64)> = self
.providers
.iter()
.filter(|provider| filter.is_active(provider.provider_type()))
.flat_map(|provider| {
provider.items().iter().filter_map(|item| {
let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item
.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let mut results: Vec<(LaunchItem, i64)> = core_items
.chain(native_items)
.filter_map(|item| {
let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item
.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let score = match (name_score, desc_score) {
(Some(n), Some(d)) => Some(n.max(d)),
(Some(n), None) => Some(n),
(None, Some(d)) => Some(d / 2),
(None, None) => None,
};
let score = match (name_score, desc_score) {
(Some(n), Some(d)) => Some(n.max(d)),
(Some(n), None) => Some(n),
(None, Some(d)) => Some(d / 2),
(None, None) => None,
};
score.map(|s| (item.clone(), s))
})
score.map(|s| (item, s))
})
.collect();
@@ -393,19 +379,11 @@ impl ProviderManager {
// 1. No specific filter prefix is active
// 2. Query is empty (user hasn't started searching)
// This keeps widgets visible on launch but hides them during active search
// Widgets are always visible regardless of filter settings (they declare position via API)
if filter.active_prefix().is_none() && query.is_empty() {
// Widget priority scores based on type
// Widget priority comes from plugin-declared priority field
for provider in &self.widget_providers {
// Skip if this provider type is filtered out
if !filter.is_active(provider.provider_type()) {
continue;
}
let base_score = match provider.type_id() {
"weather" => 12000,
"pomodoro" => 11500,
"media" => 11000,
_ => 10500,
};
let base_score = provider.priority() as i64;
for (idx, item) in provider.items().iter().enumerate() {
results.push((item.clone(), base_score - idx as i64));
}
@@ -417,18 +395,14 @@ impl ProviderManager {
// 1. Their specific filter is active (e.g., :file prefix or Files tab selected), OR
// 2. No specific single-mode filter is active (showing all providers)
if !query.is_empty() {
for (provider_idx, provider) in self.dynamic_providers.iter().enumerate() {
for provider in &self.dynamic_providers {
// Skip if this provider type is explicitly filtered out
if !filter.is_active(provider.provider_type()) {
continue;
}
let dynamic_results = provider.query(query);
let base_score = match provider.type_id() {
"calc" => 10000,
"websearch" => 9000,
"filesearch" => 8000,
_ => 7000 - (provider_idx as i64 * 1000),
};
// Priority comes from plugin-declared priority field
let base_score = provider.priority() as i64;
for (idx, item) in dynamic_results.into_iter().enumerate() {
results.push((item, base_score - idx as i64));
}
@@ -437,11 +411,22 @@ impl ProviderManager {
// Empty query (after checking special providers) - return frecency-sorted items
if query.is_empty() {
let items: Vec<(LaunchItem, i64)> = self
// 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())
.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| {
// Apply tag filter if present
if let Some(tag) = tag_filter {
@@ -465,53 +450,70 @@ impl ProviderManager {
}
// Regular search with frecency boost and tag matching
let search_results: Vec<(LaunchItem, i64)> = self
.providers
.iter()
.filter(|provider| filter.is_active(provider.provider_type()))
.flat_map(|provider| {
provider.items().iter().filter_map(|item| {
// Apply tag filter if present
if let Some(tag) = tag_filter
&& !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) {
return None;
}
// Helper closure for scoring items
let score_item = |item: &LaunchItem| -> Option<(LaunchItem, i64)> {
// Apply tag filter if present
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 desc_score = item
.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item
.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
// Also match against tags (lower weight)
let tag_score = item
.tags
.iter()
.filter_map(|t| self.matcher.fuzzy_match(t, query))
.max()
.map(|s| s / 3); // Lower weight for tag matches
// Also match against tags (lower weight)
let tag_score = item
.tags
.iter()
.filter_map(|t| self.matcher.fuzzy_match(t, query))
.max()
.map(|s| s / 3); // Lower weight for tag matches
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), None) => Some(n.max(d)),
(Some(n), None, Some(t)) => Some(n.max(t)),
(Some(n), None, None) => Some(n),
(None, Some(d), Some(t)) => Some((d / 2).max(t)),
(None, Some(d), None) => Some(d / 2),
(None, None, Some(t)) => Some(t),
(None, None, None) => None,
};
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), None) => Some(n.max(d)),
(Some(n), None, Some(t)) => Some(n.max(t)),
(Some(n), None, None) => Some(n),
(None, Some(d), Some(t)) => Some((d / 2).max(t)),
(None, Some(d), None) => Some(d / 2),
(None, None, Some(t)) => Some(t),
(None, None, None) => None,
};
base_score.map(|s| {
let frecency_score = frecency.get_score(&item.id);
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
(item.clone(), s + frecency_boost)
})
})
base_score.map(|s| {
let frecency_score = frecency.get_score(&item.id);
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
(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.truncate(max_results);
@@ -532,7 +534,11 @@ impl ProviderManager {
/// Get all available provider types (for UI tabs)
#[allow(dead_code)]
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")
@@ -572,6 +578,16 @@ impl ProviderManager {
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
for provider in &self.dynamic_providers {
if provider.type_id() == plugin_id {
@@ -592,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")]
debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id);

View File

@@ -9,7 +9,7 @@
use std::sync::{Arc, RwLock};
use log::debug;
use owlry_plugin_api::{PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind};
use owlry_plugin_api::{PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition};
use super::{LaunchItem, Provider, ProviderType};
use crate::plugins::native_loader::NativePlugin;
@@ -43,13 +43,9 @@ impl NativeProvider {
}
/// Get the ProviderType for this native provider
/// Maps type_id string to the appropriate ProviderType variant
/// All native plugins return Plugin(type_id) - the core has no hardcoded plugin types
fn get_provider_type(&self) -> ProviderType {
// Parse type_id to get the proper ProviderType
// This uses the FromStr impl which maps strings like "clipboard" -> ProviderType::Clipboard
self.info.type_id.as_str().parse().unwrap_or_else(|_| {
ProviderType::Plugin(self.info.type_id.to_string())
})
ProviderType::Plugin(self.info.type_id.to_string())
}
/// Convert a plugin API item to a core LaunchItem
@@ -109,6 +105,17 @@ impl NativeProvider {
self.info.type_id.as_str()
}
/// Check if this is a widget provider (appears at top of results)
pub fn is_widget(&self) -> bool {
self.info.position == ProviderPosition::Widget
}
/// Get the provider's priority for result ordering
/// Higher values appear first in results
pub fn priority(&self) -> i32 {
self.info.priority
}
/// 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) {

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#e0e0e0">
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>

Before

Width:  |  Height:  |  Size: 188 B

After

Width:  |  Height:  |  Size: 183 B

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M3.89,17.6c0-0.99,0.31-1.88,0.93-2.65s1.41-1.27,2.38-1.49c0.26-1.17,0.85-2.14,1.78-2.88c0.93-0.75,2-1.12,3.22-1.12
c1.18,0,2.24,0.36,3.16,1.09c0.93,0.73,1.53,1.66,1.8,2.8h0.27c1.18,0,2.18,0.41,3.01,1.24s1.25,1.83,1.25,3
c0,1.18-0.42,2.18-1.25,3.01s-1.83,1.25-3.01,1.25H8.16c-0.58,0-1.13-0.11-1.65-0.34S5.52,21,5.14,20.62

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M1.56,16.9c0,0.9,0.22,1.73,0.66,2.49s1.04,1.36,1.8,1.8c0.76,0.44,1.58,0.66,2.47,0.66h10.83c0.89,0,1.72-0.22,2.48-0.66
c0.76-0.44,1.37-1.04,1.81-1.8c0.44-0.76,0.67-1.59,0.67-2.49c0-0.66-0.14-1.33-0.42-2C22.62,13.98,23,12.87,23,11.6
c0-0.71-0.14-1.39-0.41-2.04c-0.27-0.65-0.65-1.2-1.12-1.67C21,7.42,20.45,7.04,19.8,6.77c-0.65-0.28-1.33-0.41-2.04-0.41

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.37,14.62c0-0.24,0.08-0.45,0.25-0.62c0.17-0.16,0.38-0.24,0.6-0.24h2.04c0.23,0,0.42,0.08,0.58,0.25
c0.15,0.17,0.23,0.37,0.23,0.61S8,15.06,7.85,15.23c-0.15,0.17-0.35,0.25-0.58,0.25H5.23c-0.23,0-0.43-0.08-0.6-0.25
C4.46,15.06,4.37,14.86,4.37,14.62z M7.23,21.55c0-0.23,0.08-0.43,0.23-0.61l1.47-1.43c0.15-0.16,0.35-0.23,0.59-0.23

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M2.62,21.05c0-0.24,0.08-0.45,0.25-0.61c0.17-0.16,0.38-0.24,0.63-0.24h18.67c0.25,0,0.45,0.08,0.61,0.24
c0.16,0.16,0.24,0.36,0.24,0.61c0,0.23-0.08,0.43-0.25,0.58c-0.17,0.16-0.37,0.23-0.6,0.23H3.5c-0.25,0-0.46-0.08-0.63-0.23
C2.7,21.47,2.62,21.28,2.62,21.05z M5.24,17.91c0-0.24,0.09-0.44,0.26-0.6c0.15-0.15,0.35-0.23,0.59-0.23h18.67

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M7.91,14.48c0-0.96,0.19-1.87,0.56-2.75s0.88-1.63,1.51-2.26c0.63-0.63,1.39-1.14,2.27-1.52c0.88-0.38,1.8-0.57,2.75-0.57
h1.14c0.16,0.04,0.23,0.14,0.23,0.28l0.05,0.88c0.04,1.27,0.49,2.35,1.37,3.24c0.88,0.89,1.94,1.37,3.19,1.42l0.82,0.07
c0.16,0,0.24,0.08,0.24,0.23v0.98c0.01,1.28-0.3,2.47-0.93,3.56c-0.63,1.09-1.48,1.95-2.57,2.59c-1.08,0.63-2.27,0.95-3.55,0.95

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.64,16.91c0-1.15,0.36-2.17,1.08-3.07c0.72-0.9,1.63-1.47,2.73-1.73c0.31-1.36,1.02-2.48,2.11-3.36s2.34-1.31,3.75-1.31
c1.38,0,2.6,0.43,3.68,1.28c1.08,0.85,1.78,1.95,2.1,3.29h0.32c0.89,0,1.72,0.22,2.48,0.65s1.37,1.03,1.81,1.78
c0.44,0.75,0.67,1.58,0.67,2.47c0,0.88-0.21,1.69-0.63,2.44c-0.42,0.75-1,1.35-1.73,1.8c-0.73,0.45-1.53,0.69-2.4,0.71

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.64,16.95c0-1.16,0.35-2.18,1.06-3.08s1.62-1.48,2.74-1.76c0.31-1.36,1.01-2.48,2.1-3.36s2.34-1.31,3.75-1.31
c1.38,0,2.6,0.43,3.68,1.28c1.08,0.85,1.78,1.95,2.1,3.29h0.32c0.89,0,1.72,0.22,2.48,0.66c0.76,0.44,1.37,1.04,1.81,1.8
c0.44,0.76,0.67,1.59,0.67,2.48c0,1.32-0.46,2.47-1.39,3.42c-0.92,0.96-2.05,1.46-3.38,1.5c-0.13,0-0.2-0.06-0.2-0.17v-1.33

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M9.91,19.56c0-0.85,0.2-1.64,0.59-2.38s0.94-1.35,1.65-1.84V5.42c0-0.8,0.27-1.48,0.82-2.03S14.2,2.55,15,2.55
c0.81,0,1.49,0.28,2.04,0.83c0.55,0.56,0.83,1.23,0.83,2.03v9.92c0.71,0.49,1.25,1.11,1.64,1.84s0.58,1.53,0.58,2.38
c0,0.92-0.23,1.78-0.68,2.56s-1.07,1.4-1.85,1.85s-1.63,0.68-2.56,0.68c-0.92,0-1.77-0.23-2.55-0.68s-1.4-1.07-1.86-1.85

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.63,16.91c0,1.11,0.33,2.1,0.99,2.97s1.52,1.47,2.58,1.79l-0.66,1.68c-0.03,0.14,0.02,0.22,0.14,0.22h2.13l-0.98,4.3h0.28
l3.92-5.75c0.04-0.04,0.04-0.09,0.01-0.14c-0.03-0.05-0.08-0.07-0.15-0.07h-2.18l2.48-4.64c0.07-0.14,0.02-0.22-0.14-0.22h-2.94
c-0.09,0-0.17,0.05-0.23,0.15l-1.07,2.87c-0.71-0.18-1.3-0.57-1.77-1.16c-0.47-0.59-0.7-1.26-0.7-2.01c0-0.83,0.28-1.55,0.85-2.17

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -47,6 +47,8 @@ struct LazyLoadState {
/// Number of items to display initially and per batch
const INITIAL_RESULTS: usize = 15;
const LOAD_MORE_BATCH: usize = 10;
/// Debounce delay for search input (milliseconds)
const SEARCH_DEBOUNCE_MS: u64 = 50;
pub struct MainWindow {
window: ApplicationWindow,
@@ -69,6 +71,8 @@ pub struct MainWindow {
custom_prompt: Option<String>,
/// Lazy loading state
lazy_state: Rc<RefCell<LazyLoadState>>,
/// Debounce source ID for cancelling pending searches
debounce_source: Rc<RefCell<Option<gtk4::glib::SourceId>>>,
}
impl MainWindow {
@@ -210,6 +214,7 @@ impl MainWindow {
tab_order,
custom_prompt,
lazy_state,
debounce_source: Rc::new(RefCell::new(None)),
};
main_window.setup_signals();
@@ -315,48 +320,54 @@ impl MainWindow {
}
/// Get display label for a provider tab
/// Core types have fixed labels; plugins derive labels from type_id
fn provider_tab_label(provider: &ProviderType) -> &'static str {
match provider {
ProviderType::Application => "Apps",
ProviderType::Bookmarks => "Bookmarks",
ProviderType::Calculator => "Calc",
ProviderType::Clipboard => "Clip",
ProviderType::Command => "Cmds",
ProviderType::Dmenu => "Dmenu",
ProviderType::Emoji => "Emoji",
ProviderType::Files => "Files",
ProviderType::MediaPlayer => "Media",
ProviderType::Pomodoro => "Pomo",
ProviderType::Scripts => "Scripts",
ProviderType::Ssh => "SSH",
ProviderType::System => "System",
ProviderType::Uuctl => "uuctl",
ProviderType::Weather => "Weather",
ProviderType::WebSearch => "Web",
ProviderType::Plugin(_) => "Plugin",
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => "Bookmarks",
"calc" => "Calc",
"clipboard" => "Clip",
"emoji" => "Emoji",
"filesearch" => "Files",
"media" => "Media",
"pomodoro" => "Pomo",
"scripts" => "Scripts",
"ssh" => "SSH",
"system" => "System",
"uuctl" => "uuctl",
"weather" => "Weather",
"websearch" => "Web",
_ => "Plugin",
},
}
}
/// Get CSS class for a provider
/// Core types have fixed CSS classes; plugins derive from type_id
fn provider_css_class(provider: &ProviderType) -> &'static str {
match provider {
ProviderType::Application => "owlry-filter-app",
ProviderType::Bookmarks => "owlry-filter-bookmark",
ProviderType::Calculator => "owlry-filter-calc",
ProviderType::Clipboard => "owlry-filter-clip",
ProviderType::Command => "owlry-filter-cmd",
ProviderType::Dmenu => "owlry-filter-dmenu",
ProviderType::Emoji => "owlry-filter-emoji",
ProviderType::Files => "owlry-filter-file",
ProviderType::MediaPlayer => "owlry-filter-media",
ProviderType::Pomodoro => "owlry-filter-pomodoro",
ProviderType::Scripts => "owlry-filter-script",
ProviderType::Ssh => "owlry-filter-ssh",
ProviderType::System => "owlry-filter-sys",
ProviderType::Uuctl => "owlry-filter-uuctl",
ProviderType::Weather => "owlry-filter-weather",
ProviderType::WebSearch => "owlry-filter-web",
ProviderType::Plugin(_) => "owlry-filter-plugin",
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => "owlry-filter-bookmark",
"calc" => "owlry-filter-calc",
"clipboard" => "owlry-filter-clip",
"emoji" => "owlry-filter-emoji",
"filesearch" => "owlry-filter-file",
"media" => "owlry-filter-media",
"pomodoro" => "owlry-filter-pomodoro",
"scripts" => "owlry-filter-script",
"ssh" => "owlry-filter-ssh",
"system" => "owlry-filter-sys",
"uuctl" => "owlry-filter-uuctl",
"weather" => "owlry-filter-weather",
"websearch" => "owlry-filter-web",
_ => "owlry-filter-plugin",
},
}
}
@@ -366,22 +377,24 @@ impl MainWindow {
.iter()
.map(|p| match p {
ProviderType::Application => "applications",
ProviderType::Bookmarks => "bookmarks",
ProviderType::Calculator => "calculator",
ProviderType::Clipboard => "clipboard",
ProviderType::Command => "commands",
ProviderType::Dmenu => "options",
ProviderType::Emoji => "emoji",
ProviderType::Files => "files",
ProviderType::MediaPlayer => "media",
ProviderType::Pomodoro => "pomodoro",
ProviderType::Scripts => "scripts",
ProviderType::Ssh => "SSH hosts",
ProviderType::System => "system",
ProviderType::Uuctl => "uuctl units",
ProviderType::Weather => "weather",
ProviderType::WebSearch => "web",
ProviderType::Plugin(_) => "plugins",
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => "bookmarks",
"calc" => "calculator",
"clipboard" => "clipboard",
"emoji" => "emoji",
"filesearch" => "files",
"media" => "media",
"pomodoro" => "pomodoro",
"scripts" => "scripts",
"ssh" => "SSH hosts",
"system" => "system",
"uuctl" => "uuctl units",
"weather" => "weather",
"websearch" => "web",
_ => "plugins",
},
})
.collect();
@@ -546,7 +559,7 @@ impl MainWindow {
}
fn setup_signals(&self) {
// Search input handling with prefix detection
// Search input handling with prefix detection and debouncing
let providers = self.providers.clone();
let results_list = self.results_list.clone();
let config = self.config.clone();
@@ -557,11 +570,12 @@ impl MainWindow {
let search_entry_for_change = self.search_entry.clone();
let submenu_state = self.submenu_state.clone();
let lazy_state = self.lazy_state.clone();
let debounce_source = self.debounce_source.clone();
self.search_entry.connect_changed(move |entry| {
let raw_query = entry.text();
// If in submenu, filter the submenu items
// If in submenu, filter immediately (no debounce needed for small local lists)
if submenu_state.borrow().active {
let state = submenu_state.borrow();
let query = raw_query.to_lowercase();
@@ -599,7 +613,7 @@ impl MainWindow {
return;
}
// Normal mode: parse prefix and search
// Normal mode: update prefix/UI immediately for responsiveness
let parsed = ProviderFilter::parse_query(&raw_query);
{
@@ -612,74 +626,102 @@ impl MainWindow {
if let Some(ref prefix) = parsed.prefix {
let prefix_name = match prefix {
ProviderType::Application => "applications",
ProviderType::Bookmarks => "bookmarks",
ProviderType::Calculator => "calculator",
ProviderType::Clipboard => "clipboard",
ProviderType::Command => "commands",
ProviderType::Dmenu => "options",
ProviderType::Emoji => "emoji",
ProviderType::Files => "files",
ProviderType::MediaPlayer => "media",
ProviderType::Pomodoro => "pomodoro",
ProviderType::Scripts => "scripts",
ProviderType::Ssh => "SSH hosts",
ProviderType::System => "system",
ProviderType::Uuctl => "uuctl units",
ProviderType::Weather => "weather",
ProviderType::WebSearch => "web",
ProviderType::Plugin(_) => "plugins",
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => "bookmarks",
"calc" => "calculator",
"clipboard" => "clipboard",
"emoji" => "emoji",
"filesearch" => "files",
"media" => "media",
"pomodoro" => "pomodoro",
"scripts" => "scripts",
"ssh" => "SSH hosts",
"system" => "system",
"uuctl" => "uuctl units",
"weather" => "weather",
"websearch" => "web",
_ => "plugins",
},
};
search_entry_for_change
.set_placeholder_text(Some(&format!("Search {}...", prefix_name)));
}
let cfg = config.borrow();
let max_results = cfg.general.max_results;
let frecency_weight = cfg.providers.frecency_weight;
let use_frecency = cfg.providers.frecency;
drop(cfg);
let results: Vec<LaunchItem> = if use_frecency {
providers
.borrow_mut()
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight, parsed.tag_filter.as_deref())
.into_iter()
.map(|(item, _)| item)
.collect()
} else {
providers
.borrow()
.search_filtered(&parsed.query, max_results, &filter.borrow())
.into_iter()
.map(|(item, _)| item)
.collect()
};
// Clear existing results
while let Some(child) = results_list.first_child() {
results_list.remove(&child);
// Cancel any pending debounced search
if let Some(source_id) = debounce_source.borrow_mut().take() {
source_id.remove();
}
// Lazy loading: store all results but only display initial batch
let initial_count = INITIAL_RESULTS.min(results.len());
{
let mut lazy = lazy_state.borrow_mut();
lazy.all_results = results.clone();
lazy.displayed_count = initial_count;
}
// Clone references for the debounced closure
let providers = providers.clone();
let results_list = results_list.clone();
let config = config.clone();
let frecency = frecency.clone();
let current_results = current_results.clone();
let filter = filter.clone();
let lazy_state = lazy_state.clone();
let debounce_source_for_closure = debounce_source.clone();
// Display only initial batch
for item in results.iter().take(initial_count) {
let row = ResultRow::new(item);
results_list.append(&row);
}
// Schedule debounced search
let source_id = gtk4::glib::timeout_add_local_once(
std::time::Duration::from_millis(SEARCH_DEBOUNCE_MS),
move || {
// Clear the source ID since we're now executing
*debounce_source_for_closure.borrow_mut() = None;
if let Some(first_row) = results_list.row_at_index(0) {
results_list.select_row(Some(&first_row));
}
let cfg = config.borrow();
let max_results = cfg.general.max_results;
let frecency_weight = cfg.providers.frecency_weight;
let use_frecency = cfg.providers.frecency;
drop(cfg);
// current_results holds only what's displayed (for selection/activation)
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
let results: Vec<LaunchItem> = if use_frecency {
providers
.borrow_mut()
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight, parsed.tag_filter.as_deref())
.into_iter()
.map(|(item, _)| item)
.collect()
} else {
providers
.borrow()
.search_filtered(&parsed.query, max_results, &filter.borrow())
.into_iter()
.map(|(item, _)| item)
.collect()
};
// Clear existing results
while let Some(child) = results_list.first_child() {
results_list.remove(&child);
}
// Lazy loading: store all results but only display initial batch
let initial_count = INITIAL_RESULTS.min(results.len());
{
let mut lazy = lazy_state.borrow_mut();
lazy.all_results = results.clone();
lazy.displayed_count = initial_count;
}
// Display only initial batch
for item in results.iter().take(initial_count) {
let row = ResultRow::new(item);
results_list.append(&row);
}
if let Some(first_row) = results_list.row_at_index(0) {
results_list.select_row(Some(&first_row));
}
// current_results holds only what's displayed (for selection/activation)
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
},
);
*debounce_source.borrow_mut() = Some(source_id);
});
// Entry activate signal (Enter key in search entry)
@@ -1228,6 +1270,12 @@ impl MainWindow {
}
fn launch_item(item: &LaunchItem, config: &Config, frecency: &Rc<RefCell<FrecencyStore>>) {
// dmenu mode: print selection to stdout instead of executing
if matches!(item.provider, ProviderType::Dmenu) {
println!("{}", item.name);
return;
}
// Record this launch for frecency tracking
if config.providers.frecency {
frecency.borrow_mut().record_launch(&item.id);

View File

@@ -56,6 +56,10 @@ impl ResultRow {
let img = Image::from_resource(icon_path);
img.set_pixel_size(32);
img.add_css_class("owlry-result-icon");
// SVG icons from resources should be treated as symbolic for color inheritance
if icon_path.ends_with(".svg") {
img.add_css_class("owlry-symbolic-icon");
}
img.upcast()
} else if icon_path.starts_with('/') {
// Absolute file path
@@ -68,27 +72,19 @@ impl ResultRow {
let img = Image::from_icon_name(icon_path);
img.set_pixel_size(32);
img.add_css_class("owlry-result-icon");
// Add symbolic class for icons ending with "-symbolic"
if icon_path.ends_with("-symbolic") {
img.add_css_class("owlry-symbolic-icon");
}
img.upcast()
}
} else {
// Default icon based on provider type (using symbolic icons for theme color support)
// Default icon based on provider type (only core types, plugins should provide icons)
let default_icon = match &item.provider {
crate::providers::ProviderType::Application => "application-x-executable-symbolic",
crate::providers::ProviderType::Bookmarks => "user-bookmarks-symbolic",
crate::providers::ProviderType::Calculator => "accessories-calculator-symbolic",
crate::providers::ProviderType::Clipboard => "edit-paste-symbolic",
crate::providers::ProviderType::Command => "utilities-terminal-symbolic",
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
crate::providers::ProviderType::Emoji => "face-smile-symbolic",
crate::providers::ProviderType::Files => "folder-symbolic",
crate::providers::ProviderType::Scripts => "application-x-executable-symbolic",
crate::providers::ProviderType::Ssh => "network-server-symbolic",
crate::providers::ProviderType::System => "system-shutdown-symbolic",
crate::providers::ProviderType::Uuctl => "system-run-symbolic",
crate::providers::ProviderType::WebSearch => "web-browser-symbolic",
crate::providers::ProviderType::Weather => "weather-clear-symbolic",
crate::providers::ProviderType::MediaPlayer => "media-playback-start-symbolic",
crate::providers::ProviderType::Pomodoro => "alarm-symbolic",
// Plugins should provide their own icon; fallback to generic addon icon
crate::providers::ProviderType::Plugin(_) => "application-x-addon-symbolic",
};
let img = Image::from_icon_name(default_icon);

View File

@@ -49,9 +49,8 @@ font_size = 14
border_radius = 12
# Theme name - loads ~/.config/owlry/themes/{name}.css
# Built-in: owl, catppuccin-mocha, dracula, gruvbox-dark, nord,
# one-dark, rose-pine, solarized-dark, tokyo-night
# Or leave unset for GTK default
# Built-in: owl
# Or leave unset/empty for GTK default
# theme = "owl"
# Color overrides (applied on top of theme)
@@ -81,10 +80,12 @@ disabled = []
# disabled = ["weather", "media"] # Disable widget plugins
# ═══════════════════════════════════════════════════════════════════════
# CORE PROVIDERS
# PROVIDERS
# ═══════════════════════════════════════════════════════════════════════
#
# These are built into the core binary, not plugins.
# Enable/disable providers and configure their settings.
# Core providers (applications, commands) are built into the binary.
# Plugin providers require their .so to be installed.
[providers]
# Core providers (always available)
@@ -96,36 +97,25 @@ commands = true # Executables from $PATH
frecency = true
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
# ═══════════════════════════════════════════════════════════════════════
# PLUGIN SETTINGS
# ═══════════════════════════════════════════════════════════════════════
#
# Settings for specific plugins. Only applies if the plugin is installed.
# ─────────────────────────────────────────────────────────────────────────
# Plugin provider toggles (require corresponding plugin installed)
# ─────────────────────────────────────────────────────────────────────────
uuctl = true # systemd user units
system = true # System commands (shutdown, reboot, etc.)
ssh = true # SSH hosts from ~/.ssh/config
clipboard = true # Clipboard history (requires cliphist)
bookmarks = true # Browser bookmarks
emoji = true # Emoji picker
scripts = true # Custom scripts from ~/.local/share/owlry/scripts/
files = true # File search (requires fd or mlocate)
calculator = true # Calculator (= expression)
websearch = true # Web search (? query)
# Web Search plugin
[providers.websearch]
search_engine = "duckduckgo"
# ─────────────────────────────────────────────────────────────────────────
# Plugin settings
# ─────────────────────────────────────────────────────────────────────────
# Web search engine
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
# Custom URL: "https://search.example.com/?q={query}"
# File Search plugin
[providers.filesearch]
max_results = 50
# search_paths = ["/home", "/etc"] # Custom paths (default: $HOME)
# Weather widget plugin
[providers.weather]
enabled = true
provider = "wttr.in" # wttr.in (default), openweathermap, open-meteo
location = "" # City name, "lat,lon", or empty for auto-detect
# api_key = "" # Required for OpenWeatherMap
# Pomodoro timer plugin
[providers.pomodoro]
enabled = true
work_mins = 25 # Work session duration
break_mins = 5 # Break duration
# Media controls plugin
[providers.media]
enabled = true
# Or custom URL: "https://search.example.com/?q={query}"
search_engine = "duckduckgo"

View File

@@ -143,9 +143,10 @@ chmod +x ~/.local/share/owlry/scripts/backup.sh
**Prefix:** `:bm`
**Package:** `owlry-plugin-bookmarks`
Browser bookmarks from Chromium-based browsers.
Browser bookmarks from Firefox and Chromium-based browsers.
**Supported browsers:**
- Firefox (reads places.sqlite)
- Google Chrome
- Brave
- Microsoft Edge
@@ -236,13 +237,7 @@ Current weather displayed at the top of results.
- OpenWeatherMap (requires API key)
- Open-Meteo (no API key required)
**Configuration:**
```toml
[plugins.weather]
provider = "wttr.in" # or: openweathermap, open-meteo
location = "London" # city name or "lat,lon" (empty for auto-detect)
# api_key = "..." # Required for OpenWeatherMap
```
**Note:** Weather configuration is currently embedded in the plugin. Future versions will support runtime configuration.
**Features:**
- Temperature, condition, humidity, wind speed
@@ -274,13 +269,6 @@ MPRIS media player controls.
Pomodoro timer with work/break cycles.
**Configuration:**
```toml
[plugins.pomodoro]
work_mins = 25 # Work session duration (default: 25)
break_mins = 5 # Break duration (default: 5)
```
**Features:**
- Configurable work session duration
- Configurable break duration
@@ -301,17 +289,17 @@ For convenience, plugins are available in bundle meta-packages:
| Bundle | Plugins |
|--------|---------|
| `owlry-essentials` | calculator, system, ssh, scripts, bookmarks |
| `owlry-widgets` | weather, media, pomodoro |
| `owlry-tools` | clipboard, emoji, websearch, filesearch, systemd |
| `owlry-full` | All of the above |
| `owlry-meta-essentials` | calculator, system, ssh, scripts, bookmarks |
| `owlry-meta-widgets` | weather, media, pomodoro |
| `owlry-meta-tools` | clipboard, emoji, websearch, filesearch, systemd |
| `owlry-meta-full` | All of the above |
```bash
# Install everything
yay -S owlry-full
yay -S owlry-meta-full
# Or pick a bundle
yay -S owlry-essentials owlry-widgets
yay -S owlry-meta-essentials owlry-meta-widgets
```
---

View File

@@ -23,7 +23,7 @@ Edit `Cargo.toml`:
[package]
name = "owlry-plugin-myplugin"
version = "0.1.0"
edition = "2024"
edition = "2021"
[lib]
crate-type = ["cdylib"]
@@ -38,7 +38,7 @@ Edit `src/lib.rs`:
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo,
ProviderKind, API_VERSION,
ProviderKind, ProviderPosition, API_VERSION,
};
extern "C" fn plugin_info() -> PluginInfo {
@@ -59,6 +59,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from("application-x-executable"),
provider_type: ProviderKind::Static,
type_id: RString::from("myplugin"),
position: ProviderPosition::Normal,
priority: 0, // Use frecency-based ordering
}].into()
}
@@ -198,12 +200,19 @@ pub struct ProviderInfo {
pub icon: RString, // Default icon name
pub provider_type: ProviderKind, // Static or Dynamic
pub type_id: RString, // Short ID for badges
pub position: ProviderPosition, // Normal or Widget
pub priority: i32, // Result ordering (higher = first)
}
pub enum ProviderKind {
Static, // Items loaded at startup via refresh()
Dynamic, // Items computed per-query via query()
}
pub enum ProviderPosition {
Normal, // Standard results (sorted by score/frecency)
Widget, // Displayed at top when query is empty
}
```
### PluginItem

View File

@@ -63,6 +63,14 @@ install-local:
echo "Cleaning up stale files..."
# Remove runtime files that may have ended up in plugins dir (from old installs)
sudo rm -f /usr/lib/owlry/plugins/libowlry_lua.so /usr/lib/owlry/plugins/libowlry_rune.so
# Remove old short-named plugin files (from old AUR packages before naming standardization)
sudo rm -f /usr/lib/owlry/plugins/libbookmarks.so /usr/lib/owlry/plugins/libcalculator.so \
/usr/lib/owlry/plugins/libclipboard.so /usr/lib/owlry/plugins/libemoji.so \
/usr/lib/owlry/plugins/libfilesearch.so /usr/lib/owlry/plugins/libmedia.so \
/usr/lib/owlry/plugins/libpomodoro.so /usr/lib/owlry/plugins/libscripts.so \
/usr/lib/owlry/plugins/libssh.so /usr/lib/owlry/plugins/libsystem.so \
/usr/lib/owlry/plugins/libsystemd.so /usr/lib/owlry/plugins/libweather.so \
/usr/lib/owlry/plugins/libwebsearch.so
echo "Installing core binary..."
sudo install -Dm755 target/release/owlry /usr/bin/owlry
@@ -160,7 +168,7 @@ bump-plugins new_version:
bump-meta new_version:
#!/usr/bin/env bash
set -euo pipefail
for pkg in owlry-essentials owlry-tools owlry-widgets owlry-full; do
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
file="aur/$pkg/PKGBUILD"
old=$(grep '^pkgver=' "$file" | sed 's/pkgver=//')
if [ "$old" != "{{new_version}}" ]; then
@@ -171,11 +179,18 @@ bump-meta new_version:
done
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:
#!/usr/bin/env bash
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
crate=$(basename $(dirname "$toml"))
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
@@ -196,9 +211,9 @@ bump-all new_version:
fi
done
cargo check --workspace
git add crates/owlry-plugin-*/Cargo.toml crates/owlry-lua/Cargo.toml crates/owlry-rune/Cargo.toml Cargo.lock
git commit -m "chore: bump all plugins and runtimes to {{new_version}}"
echo "All plugins and runtimes bumped to {{new_version}}"
git add crates/*/Cargo.toml Cargo.lock
git commit -m "chore: bump all crates to {{new_version}}"
echo "All crates bumped to {{new_version}}"
# Bump core version (usage: just bump 0.2.0)
bump new_version:
@@ -295,12 +310,11 @@ aur-update-pkg pkg:
fi
url="https://somegit.dev/Owlibou/owlry"
core_ver="{{version}}"
# Determine crate version
# Determine crate version (unified versioning: all crates share same version)
case "{{pkg}}" in
owlry-essentials|owlry-tools|owlry-widgets|owlry-full)
# Meta-packages have no crate, keep current version
owlry-meta-essentials|owlry-meta-tools|owlry-meta-widgets|owlry-meta-full)
# Meta-packages use static versioning (1.0.0), only bump pkgrel for dep changes
crate_ver=$(grep '^pkgver=' "$aur_dir/PKGBUILD" | sed 's/pkgver=//')
;;
*)
@@ -322,16 +336,8 @@ aur-update-pkg pkg:
sed -i "s/^pkgver=.*/pkgver=$crate_ver/" PKGBUILD
sed -i 's/^pkgrel=.*/pkgrel=1/' PKGBUILD
# Update _srcver for plugins/runtimes (they download from core version tag)
if grep -q "^_srcver=" PKGBUILD; then
echo " _srcver=$core_ver"
sed -i "s/^_srcver=.*/_srcver=$core_ver/" PKGBUILD
# Update checksum using core version
echo "Updating checksums (from v$core_ver)..."
b2sum=$(curl -sL "$url/archive/v$core_ver.tar.gz" | b2sum | cut -d' ' -f1)
sed -i "s/^b2sums=.*/b2sums=('$b2sum')/" PKGBUILD
elif grep -q "^source=" PKGBUILD; then
# Core package uses pkgver for source
# Update checksums (unified versioning: all packages use same version)
if grep -q "^source=" PKGBUILD; then
echo "Updating checksums..."
b2sum=$(curl -sL "$url/archive/v$crate_ver.tar.gz" | b2sum | cut -d' ' -f1)
sed -i "s/^b2sums=.*/b2sums=('$b2sum')/" PKGBUILD
@@ -404,7 +410,7 @@ aur-publish-plugins:
aur-publish-meta:
#!/usr/bin/env bash
set -euo pipefail
for pkg in owlry-essentials owlry-tools owlry-widgets owlry-full; do
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
echo "=== Publishing $pkg ==="
just aur-publish-pkg "$pkg"
done
@@ -446,9 +452,8 @@ aur-update-all:
just aur-update-pkg owlry-rune
echo ""
echo "=== Updating meta-packages ==="
for pkg in owlry-essentials owlry-tools owlry-widgets owlry-full; do
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
echo "--- $pkg ---"
# Use subshell to avoid cd issues
(cd "aur/$pkg" && makepkg --printsrcinfo > .SRCINFO)
done
echo ""
@@ -473,7 +478,7 @@ aur-publish-all:
just aur-publish-pkg owlry-rune
echo ""
echo "=== Publishing meta-packages ==="
for pkg in owlry-essentials owlry-tools owlry-widgets owlry-full; do
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
echo "--- $pkg ---"
just aur-publish-pkg "$pkg"
done