Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0ea40a393 | |||
| 44f0915ba9 | |||
| a55567b422 | |||
| 707caefadf | |||
| 78895d34b5 | |||
| e6f217f19c | |||
| ff04675417 | |||
| b85f85c4da | |||
| 1aa92ee1e5 | |||
| 9532b3cfde | |||
| 551e5d74ae | |||
| 60eaffb2ab | |||
| 6d8d4a9f89 | |||
| 3ef9398655 | |||
| 46bb4bfb38 | |||
| c8aed5faf5 | |||
| bf8a31af78 | |||
| e23bdf5cee | |||
| 25c4d40d36 | |||
| b36dd2a438 | |||
| 35a0f580c3 | |||
| 7ed36c58c2 | |||
| 7cccd3b512 | |||
| 9f6d0c5935 | |||
| 026a232e0c | |||
| 1557119448 |
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -2373,7 +2373,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2402,7 +2402,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-lua"
|
name = "owlry-lua"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2420,7 +2420,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-api"
|
name = "owlry-plugin-api"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2428,7 +2428,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-bookmarks"
|
name = "owlry-plugin-bookmarks"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -2440,7 +2440,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-calculator"
|
name = "owlry-plugin-calculator"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"meval",
|
"meval",
|
||||||
@@ -2449,7 +2449,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-clipboard"
|
name = "owlry-plugin-clipboard"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"owlry-plugin-api",
|
"owlry-plugin-api",
|
||||||
@@ -2457,7 +2457,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-emoji"
|
name = "owlry-plugin-emoji"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"owlry-plugin-api",
|
"owlry-plugin-api",
|
||||||
@@ -2465,7 +2465,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-filesearch"
|
name = "owlry-plugin-filesearch"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -2474,7 +2474,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-media"
|
name = "owlry-plugin-media"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"owlry-plugin-api",
|
"owlry-plugin-api",
|
||||||
@@ -2482,7 +2482,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-pomodoro"
|
name = "owlry-plugin-pomodoro"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -2494,7 +2494,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-scripts"
|
name = "owlry-plugin-scripts"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -2503,7 +2503,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-ssh"
|
name = "owlry-plugin-ssh"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -2512,7 +2512,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-system"
|
name = "owlry-plugin-system"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"owlry-plugin-api",
|
"owlry-plugin-api",
|
||||||
@@ -2520,7 +2520,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-systemd"
|
name = "owlry-plugin-systemd"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"owlry-plugin-api",
|
"owlry-plugin-api",
|
||||||
@@ -2528,7 +2528,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-weather"
|
name = "owlry-plugin-weather"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -2541,7 +2541,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-plugin-websearch"
|
name = "owlry-plugin-websearch"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"owlry-plugin-api",
|
"owlry-plugin-api",
|
||||||
@@ -2549,7 +2549,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-rune"
|
name = "owlry-rune"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
|||||||
123
README.md
123
README.md
@@ -32,10 +32,10 @@ yay -S owlry
|
|||||||
yay -S owlry-plugin-calculator owlry-plugin-weather
|
yay -S owlry-plugin-calculator owlry-plugin-weather
|
||||||
|
|
||||||
# Or install bundles:
|
# Or install bundles:
|
||||||
yay -S owlry-essentials # calculator, system, ssh, scripts, bookmarks
|
yay -S owlry-meta-essentials # calculator, system, ssh, scripts, bookmarks
|
||||||
yay -S owlry-widgets # weather, media, pomodoro
|
yay -S owlry-meta-widgets # weather, media, pomodoro
|
||||||
yay -S owlry-tools # clipboard, emoji, websearch, filesearch, systemd
|
yay -S owlry-meta-tools # clipboard, emoji, websearch, filesearch, systemd
|
||||||
yay -S owlry-full # everything
|
yay -S owlry-meta-full # everything
|
||||||
|
|
||||||
# For custom Lua/Rune plugins
|
# For custom Lua/Rune plugins
|
||||||
yay -S owlry-lua # Lua 5.4 runtime
|
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-clipboard` | History via cliphist |
|
||||||
| `owlry-plugin-emoji` | 400+ searchable emoji |
|
| `owlry-plugin-emoji` | 400+ searchable emoji |
|
||||||
| `owlry-plugin-scripts` | User scripts |
|
| `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-websearch` | Web search (`? query`) |
|
||||||
| `owlry-plugin-filesearch` | File search (`/ filename`) |
|
| `owlry-plugin-filesearch` | File search (`/ filename`) |
|
||||||
| `owlry-plugin-systemd` | User services with actions |
|
| `owlry-plugin-systemd` | User services with actions |
|
||||||
@@ -99,12 +99,45 @@ sudo cp target/release/libowlry_plugin_*.so /usr/lib/owlry/plugins/
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
owlry # Launch with defaults
|
owlry # Launch with all providers
|
||||||
owlry --mode app # Applications only
|
owlry -m app # Applications only
|
||||||
owlry --providers app,cmd # Specific providers
|
owlry -m cmd # PATH commands only
|
||||||
owlry --help # Show all options
|
owlry -p app,cmd # Multiple specific providers
|
||||||
|
owlry -m calc # Calculator plugin only (if installed)
|
||||||
|
owlry --help # Show all options with examples
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### dmenu Mode
|
||||||
|
|
||||||
|
Owlry is dmenu-compatible. Pipe input for interactive selection - the selected item is printed to stdout (not executed), so you pipe the output to execute it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Screenshot menu (execute selected command)
|
||||||
|
printf '%s\n' \
|
||||||
|
"grimblast --notify copy screen" \
|
||||||
|
"grimblast --notify copy area" \
|
||||||
|
"grimblast --notify edit screen" \
|
||||||
|
| owlry -m dmenu -p "Screenshot" \
|
||||||
|
| sh
|
||||||
|
|
||||||
|
# Git branch checkout
|
||||||
|
git branch | owlry -m dmenu -p "checkout" | xargs git checkout
|
||||||
|
|
||||||
|
# Kill a process
|
||||||
|
ps -eo comm | sort -u | owlry -m dmenu -p "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 -p "install" | xargs sudo pacman -S
|
||||||
|
|
||||||
|
# Open selected file
|
||||||
|
ls ~/Documents | owlry -m dmenu | xargs xdg-open
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-p` / `--prompt` flag sets a custom label for the search input.
|
||||||
|
|
||||||
### Keyboard Shortcuts
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
@@ -158,6 +191,21 @@ Owlry follows the [XDG Base Directory Specification](https://specifications.free
|
|||||||
| `~/.local/share/owlry/scripts/` | User scripts |
|
| `~/.local/share/owlry/scripts/` | User scripts |
|
||||||
| `~/.local/share/owlry/frecency.json` | Usage history |
|
| `~/.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
|
### Example Configuration
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -165,12 +213,12 @@ Owlry follows the [XDG Base Directory Specification](https://specifications.free
|
|||||||
show_icons = true
|
show_icons = true
|
||||||
max_results = 10
|
max_results = 10
|
||||||
tabs = ["app", "cmd", "uuctl"]
|
tabs = ["app", "cmd", "uuctl"]
|
||||||
# terminal_command = "kitty" # Auto-detected
|
# terminal_command = "kitty" # Auto-detected
|
||||||
# launch_wrapper = "uwsm app --" # Auto-detected
|
# use_uwsm = false # Enable for systemd session integration
|
||||||
|
|
||||||
[appearance]
|
[appearance]
|
||||||
width = 700
|
width = 850
|
||||||
height = 500
|
height = 650
|
||||||
font_size = 14
|
font_size = 14
|
||||||
border_radius = 12
|
border_radius = 12
|
||||||
# theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc.
|
# theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc.
|
||||||
@@ -178,17 +226,18 @@ border_radius = 12
|
|||||||
[plugins]
|
[plugins]
|
||||||
disabled = [] # Plugin IDs to disable, e.g., ["emoji", "pomodoro"]
|
disabled = [] # Plugin IDs to disable, e.g., ["emoji", "pomodoro"]
|
||||||
|
|
||||||
# Per-plugin configuration (new in 0.4.0)
|
[providers]
|
||||||
[plugins.weather]
|
applications = true # .desktop files
|
||||||
provider = "wttr.in" # or: openweathermap, open-meteo
|
commands = true # PATH executables
|
||||||
location = "Berlin" # city name or "lat,lon"
|
frecency = true # Boost frequently used items
|
||||||
# api_key = "..." # Required for OpenWeatherMap
|
frecency_weight = 0.3 # 0.0-1.0
|
||||||
|
|
||||||
[plugins.pomodoro]
|
# Web search engine: google, duckduckgo, bing, startpage, brave, ecosia
|
||||||
work_mins = 25 # Work session duration
|
search_engine = "duckduckgo"
|
||||||
break_mins = 5 # Break duration
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See `/usr/share/doc/owlry/config.example.toml` for all options with documentation.
|
||||||
|
|
||||||
## Plugin System
|
## Plugin System
|
||||||
|
|
||||||
Owlry uses a modular plugin architecture. Plugins are loaded from:
|
Owlry uses a modular plugin architecture. Plugins are loaded from:
|
||||||
@@ -205,6 +254,38 @@ Add plugin IDs to the disabled list in your config:
|
|||||||
disabled = ["emoji", "pomodoro"]
|
disabled = ["emoji", "pomodoro"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Plugin Management CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List installed plugins
|
||||||
|
owlry plugin list
|
||||||
|
owlry plugin list --enabled # Only enabled
|
||||||
|
owlry plugin list --available # Show registry plugins
|
||||||
|
|
||||||
|
# Search registry
|
||||||
|
owlry plugin search "weather"
|
||||||
|
|
||||||
|
# Install/remove
|
||||||
|
owlry plugin install <name> # From registry
|
||||||
|
owlry plugin install ./my-plugin # From local path
|
||||||
|
owlry plugin remove <name>
|
||||||
|
|
||||||
|
# Enable/disable
|
||||||
|
owlry plugin enable <name>
|
||||||
|
owlry plugin disable <name>
|
||||||
|
|
||||||
|
# Plugin info
|
||||||
|
owlry plugin info <name>
|
||||||
|
owlry plugin commands <name> # List plugin CLI commands
|
||||||
|
|
||||||
|
# Create new plugin
|
||||||
|
owlry plugin create my-plugin # Lua (default)
|
||||||
|
owlry plugin create my-plugin -r rune # Rune
|
||||||
|
|
||||||
|
# Run plugin command
|
||||||
|
owlry plugin run <plugin-id> <command> [args...]
|
||||||
|
```
|
||||||
|
|
||||||
### Creating Custom Plugins
|
### Creating Custom Plugins
|
||||||
|
|
||||||
See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for:
|
See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for:
|
||||||
|
|||||||
109
ROADMAP.md
Normal file
109
ROADMAP.md
Normal 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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-lua"
|
name = "owlry-lua"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-api"
|
name = "owlry-plugin-api"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-bookmarks"
|
name = "owlry-plugin-bookmarks"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-calculator"
|
name = "owlry-plugin-calculator"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-clipboard"
|
name = "owlry-plugin-clipboard"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-emoji"
|
name = "owlry-plugin-emoji"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-filesearch"
|
name = "owlry-plugin-filesearch"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-media"
|
name = "owlry-plugin-media"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-pomodoro"
|
name = "owlry-plugin-pomodoro"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-scripts"
|
name = "owlry-plugin-scripts"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-ssh"
|
name = "owlry-plugin-ssh"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-system"
|
name = "owlry-plugin-system"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-systemd"
|
name = "owlry-plugin-systemd"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-weather"
|
name = "owlry-plugin-weather"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-websearch"
|
name = "owlry-plugin-websearch"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-rune"
|
name = "owlry-rune"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.90"
|
rust-version = "1.90"
|
||||||
description = "Rune scripting runtime for owlry plugins"
|
description = "Rune scripting runtime for owlry plugins"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "0.4.5"
|
version = "0.4.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.90"
|
rust-version = "1.90"
|
||||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||||
|
|||||||
@@ -10,19 +10,55 @@ use crate::providers::ProviderType;
|
|||||||
#[command(
|
#[command(
|
||||||
name = "owlry",
|
name = "owlry",
|
||||||
about = "An owl-themed application launcher for Wayland",
|
about = "An owl-themed application launcher for Wayland",
|
||||||
version
|
long_about = "An owl-themed application launcher for Wayland, built with GTK4 and Layer Shell.\n\n\
|
||||||
|
Owlry provides fuzzy search across applications, commands, and plugins.\n\
|
||||||
|
Native plugins add features like calculator, clipboard, emoji, weather, and more.",
|
||||||
|
version,
|
||||||
|
after_help = "\
|
||||||
|
EXAMPLES:
|
||||||
|
owlry Launch with all providers
|
||||||
|
owlry -m app Applications only
|
||||||
|
owlry -m cmd PATH commands only
|
||||||
|
owlry -m dmenu dmenu-compatible mode (reads from stdin)
|
||||||
|
owlry -p app,cmd Multiple providers
|
||||||
|
owlry -m calc Calculator plugin only (if installed)
|
||||||
|
|
||||||
|
DMENU MODE:
|
||||||
|
Pipe input to owlry for interactive selection:
|
||||||
|
|
||||||
|
echo -e \"Option A\\nOption B\" | owlry -m dmenu
|
||||||
|
ls | owlry -m dmenu
|
||||||
|
git branch | owlry -m dmenu --prompt \"checkout:\"
|
||||||
|
|
||||||
|
SEARCH PREFIXES:
|
||||||
|
:app firefox Search applications
|
||||||
|
:cmd git Search PATH commands
|
||||||
|
= 5+3 Calculator (requires plugin)
|
||||||
|
? rust docs Web search (requires plugin)
|
||||||
|
/ .bashrc File search (requires plugin)
|
||||||
|
|
||||||
|
For configuration, see ~/.config/owlry/config.toml
|
||||||
|
For plugin management, see: owlry plugin --help"
|
||||||
)]
|
)]
|
||||||
pub struct CliArgs {
|
pub struct CliArgs {
|
||||||
/// Start in single-provider mode (app, cmd, uuctl)
|
/// Start in single-provider mode
|
||||||
#[arg(long, short = 'm', value_parser = parse_provider)]
|
///
|
||||||
|
/// Core modes: app, cmd, dmenu
|
||||||
|
/// Plugin modes: calc, clip, emoji, ssh, sys, bm, file, web, uuctl, weather, media, pomodoro
|
||||||
|
#[arg(long, short = 'm', value_parser = parse_provider, value_name = "MODE")]
|
||||||
pub mode: Option<ProviderType>,
|
pub mode: Option<ProviderType>,
|
||||||
|
|
||||||
/// Comma-separated list of enabled providers (app,cmd,uuctl)
|
/// Comma-separated list of enabled providers
|
||||||
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)]
|
///
|
||||||
|
/// Examples: -p app,cmd or -p app,calc,emoji
|
||||||
|
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider, value_name = "PROVIDERS")]
|
||||||
pub providers: Option<Vec<ProviderType>>,
|
pub providers: Option<Vec<ProviderType>>,
|
||||||
|
|
||||||
/// Custom prompt text for the search input (useful for dmenu mode)
|
/// Custom prompt text for the search input
|
||||||
#[arg(long)]
|
///
|
||||||
|
/// Useful in dmenu mode to indicate what the user is selecting.
|
||||||
|
/// Example: --prompt "Select file:"
|
||||||
|
#[arg(long, value_name = "TEXT")]
|
||||||
pub prompt: Option<String>,
|
pub prompt: Option<String>,
|
||||||
|
|
||||||
/// Subcommand to run (if any)
|
/// Subcommand to run (if any)
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ pub struct GeneralConfig {
|
|||||||
/// Terminal command (auto-detected if not specified)
|
/// Terminal command (auto-detected if not specified)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub terminal_command: Option<String>,
|
pub terminal_command: Option<String>,
|
||||||
/// Launch wrapper command for app execution.
|
/// Enable uwsm (Universal Wayland Session Manager) for launching apps.
|
||||||
/// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --"
|
/// When enabled, desktop files are launched via `uwsm app -- <file>`
|
||||||
/// If None or empty, launches directly via sh -c
|
/// which starts apps in a proper systemd user session.
|
||||||
|
/// When disabled (default), apps are launched via `gio launch`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub launch_wrapper: Option<String>,
|
pub use_uwsm: bool,
|
||||||
/// Provider tabs shown in the header bar.
|
/// Provider tabs shown in the header bar.
|
||||||
/// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
|
/// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
|
||||||
#[serde(default = "default_tabs")]
|
#[serde(default = "default_tabs")]
|
||||||
@@ -44,7 +45,7 @@ impl Default for GeneralConfig {
|
|||||||
show_icons: true,
|
show_icons: true,
|
||||||
max_results: 100,
|
max_results: 100,
|
||||||
terminal_command: None,
|
terminal_command: None,
|
||||||
launch_wrapper: None,
|
use_uwsm: false,
|
||||||
tabs: default_tabs(),
|
tabs: default_tabs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,28 +397,6 @@ fn default_pomodoro_break() -> u32 {
|
|||||||
5
|
5
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect the best launch wrapper for the current session
|
|
||||||
/// Checks for uwsm (Universal Wayland Session Manager) and hyprland
|
|
||||||
fn detect_launch_wrapper() -> Option<String> {
|
|
||||||
// Check if running under uwsm (has UWSM_FINALIZE_VARNAMES or similar uwsm env vars)
|
|
||||||
if (std::env::var("UWSM_FINALIZE_VARNAMES").is_ok()
|
|
||||||
|| std::env::var("__UWSM_SELECT_TAG").is_ok())
|
|
||||||
&& command_exists("uwsm") {
|
|
||||||
debug!("Detected uwsm session, using 'uwsm app --' wrapper");
|
|
||||||
return Some("uwsm app --".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if running under Hyprland
|
|
||||||
if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok()
|
|
||||||
&& command_exists("hyprctl") {
|
|
||||||
debug!("Detected Hyprland session, using 'hyprctl dispatch exec --' wrapper");
|
|
||||||
return Some("hyprctl dispatch exec --".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// No wrapper needed for other environments
|
|
||||||
debug!("No launch wrapper detected, using direct execution");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect the best available terminal emulator
|
/// Detect the best available terminal emulator
|
||||||
/// Fallback chain:
|
/// Fallback chain:
|
||||||
@@ -578,11 +557,6 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-detect launch wrapper if not configured
|
|
||||||
if config.general.launch_wrapper.is_none() {
|
|
||||||
config.general.launch_wrapper = detect_launch_wrapper();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,23 +99,57 @@ pub fn frecency_file() -> Option<PathBuf> {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
/// System data directories for applications (XDG_DATA_DIRS)
|
/// System data directories for applications (XDG_DATA_DIRS)
|
||||||
|
///
|
||||||
|
/// Follows the XDG Base Directory Specification:
|
||||||
|
/// - $XDG_DATA_HOME/applications (defaults to ~/.local/share/applications)
|
||||||
|
/// - $XDG_DATA_DIRS/*/applications (defaults to /usr/local/share:/usr/share)
|
||||||
|
/// - Additional Flatpak and Snap directories
|
||||||
pub fn system_data_dirs() -> Vec<PathBuf> {
|
pub fn system_data_dirs() -> Vec<PathBuf> {
|
||||||
let mut dirs = Vec::new();
|
let mut dirs = Vec::new();
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
|
||||||
// User data directory first
|
// Helper to add unique directories
|
||||||
|
let mut add_dir = |path: PathBuf| {
|
||||||
|
if seen.insert(path.clone()) {
|
||||||
|
dirs.push(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. User data directory first (highest priority)
|
||||||
if let Some(data) = data_home() {
|
if let Some(data) = data_home() {
|
||||||
dirs.push(data.join("applications"));
|
add_dir(data.join("applications"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// System directories
|
// 2. XDG_DATA_DIRS - parse the environment variable
|
||||||
dirs.push(PathBuf::from("/usr/share/applications"));
|
// Default per spec: /usr/local/share:/usr/share
|
||||||
dirs.push(PathBuf::from("/usr/local/share/applications"));
|
let xdg_data_dirs = std::env::var("XDG_DATA_DIRS")
|
||||||
|
.unwrap_or_else(|_| "/usr/local/share:/usr/share".to_string());
|
||||||
|
|
||||||
// Flatpak directories
|
for dir in xdg_data_dirs.split(':') {
|
||||||
if let Some(data) = data_home() {
|
if !dir.is_empty() {
|
||||||
dirs.push(data.join("flatpak/exports/share/applications"));
|
add_dir(PathBuf::from(dir).join("applications"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dirs.push(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
|
|
||||||
|
// 3. Always include standard system directories as fallback
|
||||||
|
// Some environments set XDG_DATA_DIRS without including these
|
||||||
|
add_dir(PathBuf::from("/usr/share/applications"));
|
||||||
|
add_dir(PathBuf::from("/usr/local/share/applications"));
|
||||||
|
|
||||||
|
// 4. Flatpak directories (user and system)
|
||||||
|
if let Some(data) = data_home() {
|
||||||
|
add_dir(data.join("flatpak/exports/share/applications"));
|
||||||
|
}
|
||||||
|
add_dir(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
|
||||||
|
|
||||||
|
// 5. Snap directories
|
||||||
|
add_dir(PathBuf::from("/var/lib/snapd/desktop/applications"));
|
||||||
|
|
||||||
|
// 6. Nix directories (common on NixOS)
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
add_dir(home.join(".nix-profile/share/applications"));
|
||||||
|
}
|
||||||
|
add_dir(PathBuf::from("/run/current-system/sw/share/applications"));
|
||||||
|
|
||||||
dirs
|
dirs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,15 @@ impl Provider for ApplicationProvider {
|
|||||||
// Empty locale list for default locale
|
// Empty locale list for default locale
|
||||||
let locales: &[&str] = &[];
|
let locales: &[&str] = &[];
|
||||||
|
|
||||||
|
// Get current desktop environment(s) for OnlyShowIn/NotShowIn filtering
|
||||||
|
// XDG_CURRENT_DESKTOP can be colon-separated (e.g., "ubuntu:GNOME")
|
||||||
|
let current_desktops: Vec<String> = std::env::var("XDG_CURRENT_DESKTOP")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.split(':')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
for path in Iter::new(dirs.into_iter()) {
|
for path in Iter::new(dirs.into_iter()) {
|
||||||
let content = match std::fs::read_to_string(&path) {
|
let content = match std::fs::read_to_string(&path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -125,6 +134,24 @@ impl Provider for ApplicationProvider {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply OnlyShowIn/NotShowIn filters only if we know the current desktop
|
||||||
|
// If XDG_CURRENT_DESKTOP is not set, show all apps (don't filter)
|
||||||
|
if !current_desktops.is_empty() {
|
||||||
|
// OnlyShowIn: if set, current desktop must be in the list
|
||||||
|
if desktop_entry.only_show_in().is_some_and(|only| {
|
||||||
|
!current_desktops.iter().any(|de| only.contains(&de.as_str()))
|
||||||
|
}) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotShowIn: if current desktop is in the list, skip
|
||||||
|
if desktop_entry.not_show_in().is_some_and(|not| {
|
||||||
|
current_desktops.iter().any(|de| not.contains(&de.as_str()))
|
||||||
|
}) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let name = match desktop_entry.name(locales) {
|
let name = match desktop_entry.name(locales) {
|
||||||
Some(n) => n.to_string(),
|
Some(n) => n.to_string(),
|
||||||
None => continue,
|
None => continue,
|
||||||
@@ -135,12 +162,17 @@ impl Provider for ApplicationProvider {
|
|||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract categories as tags (lowercase for consistency)
|
// Extract categories and keywords as tags (lowercase for consistency)
|
||||||
let tags: Vec<String> = desktop_entry
|
let mut tags: Vec<String> = desktop_entry
|
||||||
.categories()
|
.categories()
|
||||||
.map(|cats| cats.into_iter().map(|s| s.to_lowercase()).collect())
|
.map(|cats| cats.into_iter().map(|s| s.to_lowercase()).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Add keywords for searchability (e.g., Nautilus has Name=Files but Keywords contains "nautilus")
|
||||||
|
if let Some(keywords) = desktop_entry.keywords(locales) {
|
||||||
|
tags.extend(keywords.into_iter().map(|s| s.to_lowercase()));
|
||||||
|
}
|
||||||
|
|
||||||
let item = LaunchItem {
|
let item = LaunchItem {
|
||||||
id: path.to_string_lossy().to_string(),
|
id: path.to_string_lossy().to_string(),
|
||||||
name,
|
name,
|
||||||
@@ -157,6 +189,13 @@ impl Provider for ApplicationProvider {
|
|||||||
|
|
||||||
debug!("Found {} applications", self.items.len());
|
debug!("Found {} applications", self.items.len());
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!(
|
||||||
|
"XDG_CURRENT_DESKTOP={:?}, scanned dirs count={}",
|
||||||
|
current_desktops,
|
||||||
|
Self::get_application_dirs().len()
|
||||||
|
);
|
||||||
|
|
||||||
// Sort alphabetically by name
|
// Sort alphabetically by name
|
||||||
self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||||
}
|
}
|
||||||
@@ -210,4 +249,18 @@ mod tests {
|
|||||||
"bash -c 'echo %u'"
|
"bash -c 'echo %u'"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clean_desktop_exec_preserves_env() {
|
||||||
|
// env VAR=value pattern should be preserved
|
||||||
|
assert_eq!(
|
||||||
|
clean_desktop_exec_field("env GDK_BACKEND=x11 UBUNTU_MENUPROXY=0 audacity %F"),
|
||||||
|
"env GDK_BACKEND=x11 UBUNTU_MENUPROXY=0 audacity"
|
||||||
|
);
|
||||||
|
// Multiple env vars
|
||||||
|
assert_eq!(
|
||||||
|
clean_desktop_exec_field("env FOO=bar BAZ=qux myapp %u"),
|
||||||
|
"env FOO=bar BAZ=qux myapp"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,8 +95,10 @@ pub trait Provider: Send {
|
|||||||
|
|
||||||
/// Manages all providers and handles searching
|
/// Manages all providers and handles searching
|
||||||
pub struct ProviderManager {
|
pub struct ProviderManager {
|
||||||
/// Static providers (apps, commands, and native static plugins)
|
/// Core static providers (apps, commands, dmenu)
|
||||||
providers: Vec<Box<dyn Provider>>,
|
providers: Vec<Box<dyn Provider>>,
|
||||||
|
/// Static native plugin providers (need query() for submenu support)
|
||||||
|
static_native_providers: Vec<NativeProvider>,
|
||||||
/// Dynamic providers from native plugins (calculator, websearch, filesearch)
|
/// Dynamic providers from native plugins (calculator, websearch, filesearch)
|
||||||
/// These are queried per-keystroke, not cached
|
/// These are queried per-keystroke, not cached
|
||||||
dynamic_providers: Vec<NativeProvider>,
|
dynamic_providers: Vec<NativeProvider>,
|
||||||
@@ -118,6 +120,7 @@ impl ProviderManager {
|
|||||||
pub fn with_native_plugins(native_providers: Vec<NativeProvider>) -> Self {
|
pub fn with_native_plugins(native_providers: Vec<NativeProvider>) -> Self {
|
||||||
let mut manager = Self {
|
let mut manager = Self {
|
||||||
providers: Vec::new(),
|
providers: Vec::new(),
|
||||||
|
static_native_providers: Vec::new(),
|
||||||
dynamic_providers: Vec::new(),
|
dynamic_providers: Vec::new(),
|
||||||
widget_providers: Vec::new(),
|
widget_providers: Vec::new(),
|
||||||
matcher: SkimMatcherV2::default(),
|
matcher: SkimMatcherV2::default(),
|
||||||
@@ -149,9 +152,9 @@ impl ProviderManager {
|
|||||||
info!("Registered widget provider: {} ({})", provider.name(), type_id);
|
info!("Registered widget provider: {} ({})", provider.name(), type_id);
|
||||||
manager.widget_providers.push(provider);
|
manager.widget_providers.push(provider);
|
||||||
} else {
|
} else {
|
||||||
// Static providers with Normal position
|
// Static native providers (keep as NativeProvider for query/submenu support)
|
||||||
info!("Registered static provider: {} ({})", provider.name(), type_id);
|
info!("Registered static provider: {} ({})", provider.name(), type_id);
|
||||||
manager.providers.push(Box::new(provider));
|
manager.static_native_providers.push(provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,7 +173,7 @@ impl ProviderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_all(&mut self) {
|
pub fn refresh_all(&mut self) {
|
||||||
// Refresh static providers (fast, local operations)
|
// Refresh core providers (apps, commands)
|
||||||
for provider in &mut self.providers {
|
for provider in &mut self.providers {
|
||||||
provider.refresh();
|
provider.refresh();
|
||||||
info!(
|
info!(
|
||||||
@@ -180,6 +183,16 @@ impl ProviderManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh static native providers (clipboard, emoji, ssh, etc.)
|
||||||
|
for provider in &mut self.static_native_providers {
|
||||||
|
provider.refresh();
|
||||||
|
info!(
|
||||||
|
"Static provider '{}' loaded {} items",
|
||||||
|
provider.name(),
|
||||||
|
provider.items().len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Widget providers are refreshed separately to avoid blocking startup
|
// Widget providers are refreshed separately to avoid blocking startup
|
||||||
// Call refresh_widgets() after window is shown
|
// Call refresh_widgets() after window is shown
|
||||||
|
|
||||||
@@ -201,9 +214,13 @@ impl ProviderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find a native provider by type ID
|
/// Find a native provider by type ID
|
||||||
/// Searches in widget providers and dynamic providers
|
/// Searches in all native provider lists (static, dynamic, widget)
|
||||||
pub fn find_native_provider(&self, type_id: &str) -> Option<&NativeProvider> {
|
pub fn find_native_provider(&self, type_id: &str) -> Option<&NativeProvider> {
|
||||||
// Check widget providers first (pomodoro, weather, media)
|
// Check static native providers first (clipboard, emoji, ssh, systemd, etc.)
|
||||||
|
if let Some(p) = self.static_native_providers.iter().find(|p| p.type_id() == type_id) {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
// Check widget providers (pomodoro, weather, media)
|
||||||
if let Some(p) = self.widget_providers.iter().find(|p| p.type_id() == type_id) {
|
if let Some(p) = self.widget_providers.iter().find(|p| p.type_id() == type_id) {
|
||||||
return Some(p);
|
return Some(p);
|
||||||
}
|
}
|
||||||
@@ -246,37 +263,40 @@ impl ProviderManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterate over all static provider items (core + native static plugins)
|
||||||
|
fn all_static_items(&self) -> impl Iterator<Item = &LaunchItem> {
|
||||||
|
self.providers
|
||||||
|
.iter()
|
||||||
|
.flat_map(|p| p.items().iter())
|
||||||
|
.chain(self.static_native_providers.iter().flat_map(|p| p.items().iter()))
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> {
|
pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
// Return recent/popular items when query is empty
|
// Return recent/popular items when query is empty
|
||||||
return self.providers
|
return self.all_static_items()
|
||||||
.iter()
|
|
||||||
.flat_map(|p| p.items().iter().cloned())
|
|
||||||
.take(max_results)
|
.take(max_results)
|
||||||
.map(|item| (item, 0))
|
.map(|item| (item.clone(), 0))
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut results: Vec<(LaunchItem, i64)> = self.providers
|
let mut results: Vec<(LaunchItem, i64)> = self.all_static_items()
|
||||||
.iter()
|
.filter_map(|item| {
|
||||||
.flat_map(|provider| {
|
// Match against name and description
|
||||||
provider.items().iter().filter_map(|item| {
|
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||||
// Match against name and description
|
let desc_score = item.description
|
||||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
.as_ref()
|
||||||
let desc_score = item.description
|
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||||
.as_ref()
|
|
||||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
|
||||||
|
|
||||||
let score = match (name_score, desc_score) {
|
let score = match (name_score, desc_score) {
|
||||||
(Some(n), Some(d)) => Some(n.max(d)),
|
(Some(n), Some(d)) => Some(n.max(d)),
|
||||||
(Some(n), None) => Some(n),
|
(Some(n), None) => Some(n),
|
||||||
(None, Some(d)) => Some(d / 2), // Lower weight for description matches
|
(None, Some(d)) => Some(d / 2), // Lower weight for description matches
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
score.map(|s| (item.clone(), s))
|
score.map(|s| (item.clone(), s))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -293,38 +313,45 @@ impl ProviderManager {
|
|||||||
max_results: usize,
|
max_results: usize,
|
||||||
filter: &crate::filter::ProviderFilter,
|
filter: &crate::filter::ProviderFilter,
|
||||||
) -> Vec<(LaunchItem, i64)> {
|
) -> Vec<(LaunchItem, i64)> {
|
||||||
|
// Collect items from core providers
|
||||||
|
let core_items = self
|
||||||
|
.providers
|
||||||
|
.iter()
|
||||||
|
.filter(|p| filter.is_active(p.provider_type()))
|
||||||
|
.flat_map(|p| p.items().iter().cloned());
|
||||||
|
|
||||||
|
// Collect items from static native providers
|
||||||
|
let native_items = self
|
||||||
|
.static_native_providers
|
||||||
|
.iter()
|
||||||
|
.filter(|p| filter.is_active(p.provider_type()))
|
||||||
|
.flat_map(|p| p.items().iter().cloned());
|
||||||
|
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
return self
|
return core_items
|
||||||
.providers
|
.chain(native_items)
|
||||||
.iter()
|
|
||||||
.filter(|p| filter.is_active(p.provider_type()))
|
|
||||||
.flat_map(|p| p.items().iter().cloned())
|
|
||||||
.take(max_results)
|
.take(max_results)
|
||||||
.map(|item| (item, 0))
|
.map(|item| (item, 0))
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut results: Vec<(LaunchItem, i64)> = self
|
let mut results: Vec<(LaunchItem, i64)> = core_items
|
||||||
.providers
|
.chain(native_items)
|
||||||
.iter()
|
.filter_map(|item| {
|
||||||
.filter(|provider| filter.is_active(provider.provider_type()))
|
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||||
.flat_map(|provider| {
|
let desc_score = item
|
||||||
provider.items().iter().filter_map(|item| {
|
.description
|
||||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
.as_ref()
|
||||||
let desc_score = item
|
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||||
.description
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
|
||||||
|
|
||||||
let score = match (name_score, desc_score) {
|
let score = match (name_score, desc_score) {
|
||||||
(Some(n), Some(d)) => Some(n.max(d)),
|
(Some(n), Some(d)) => Some(n.max(d)),
|
||||||
(Some(n), None) => Some(n),
|
(Some(n), None) => Some(n),
|
||||||
(None, Some(d)) => Some(d / 2),
|
(None, Some(d)) => Some(d / 2),
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
score.map(|s| (item.clone(), s))
|
score.map(|s| (item, s))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -384,11 +411,22 @@ impl ProviderManager {
|
|||||||
|
|
||||||
// Empty query (after checking special providers) - return frecency-sorted items
|
// Empty query (after checking special providers) - return frecency-sorted items
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
let items: Vec<(LaunchItem, i64)> = self
|
// Collect items from core providers
|
||||||
|
let core_items = self
|
||||||
.providers
|
.providers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|p| filter.is_active(p.provider_type()))
|
.filter(|p| filter.is_active(p.provider_type()))
|
||||||
.flat_map(|p| p.items().iter().cloned())
|
.flat_map(|p| p.items().iter().cloned());
|
||||||
|
|
||||||
|
// Collect items from static native providers
|
||||||
|
let native_items = self
|
||||||
|
.static_native_providers
|
||||||
|
.iter()
|
||||||
|
.filter(|p| filter.is_active(p.provider_type()))
|
||||||
|
.flat_map(|p| p.items().iter().cloned());
|
||||||
|
|
||||||
|
let items: Vec<(LaunchItem, i64)> = core_items
|
||||||
|
.chain(native_items)
|
||||||
.filter(|item| {
|
.filter(|item| {
|
||||||
// Apply tag filter if present
|
// Apply tag filter if present
|
||||||
if let Some(tag) = tag_filter {
|
if let Some(tag) = tag_filter {
|
||||||
@@ -412,53 +450,70 @@ impl ProviderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Regular search with frecency boost and tag matching
|
// Regular search with frecency boost and tag matching
|
||||||
let search_results: Vec<(LaunchItem, i64)> = self
|
// Helper closure for scoring items
|
||||||
.providers
|
let score_item = |item: &LaunchItem| -> Option<(LaunchItem, i64)> {
|
||||||
.iter()
|
// Apply tag filter if present
|
||||||
.filter(|provider| filter.is_active(provider.provider_type()))
|
if let Some(tag) = tag_filter
|
||||||
.flat_map(|provider| {
|
&& !item.tags.iter().any(|t| t.to_lowercase().contains(tag))
|
||||||
provider.items().iter().filter_map(|item| {
|
{
|
||||||
// Apply tag filter if present
|
return None;
|
||||||
if let Some(tag) = tag_filter
|
}
|
||||||
&& !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||||
let desc_score = item
|
let desc_score = item
|
||||||
.description
|
.description
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||||
|
|
||||||
// Also match against tags (lower weight)
|
// Also match against tags (lower weight)
|
||||||
let tag_score = item
|
let tag_score = item
|
||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|t| self.matcher.fuzzy_match(t, query))
|
.filter_map(|t| self.matcher.fuzzy_match(t, query))
|
||||||
.max()
|
.max()
|
||||||
.map(|s| s / 3); // Lower weight for tag matches
|
.map(|s| s / 3); // Lower weight for tag matches
|
||||||
|
|
||||||
let base_score = match (name_score, desc_score, tag_score) {
|
let base_score = match (name_score, desc_score, tag_score) {
|
||||||
(Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)),
|
(Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)),
|
||||||
(Some(n), Some(d), None) => Some(n.max(d)),
|
(Some(n), Some(d), None) => Some(n.max(d)),
|
||||||
(Some(n), None, Some(t)) => Some(n.max(t)),
|
(Some(n), None, Some(t)) => Some(n.max(t)),
|
||||||
(Some(n), None, None) => Some(n),
|
(Some(n), None, None) => Some(n),
|
||||||
(None, Some(d), Some(t)) => Some((d / 2).max(t)),
|
(None, Some(d), Some(t)) => Some((d / 2).max(t)),
|
||||||
(None, Some(d), None) => Some(d / 2),
|
(None, Some(d), None) => Some(d / 2),
|
||||||
(None, None, Some(t)) => Some(t),
|
(None, None, Some(t)) => Some(t),
|
||||||
(None, None, None) => None,
|
(None, None, None) => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
base_score.map(|s| {
|
base_score.map(|s| {
|
||||||
let frecency_score = frecency.get_score(&item.id);
|
let frecency_score = frecency.get_score(&item.id);
|
||||||
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
|
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
|
||||||
(item.clone(), s + frecency_boost)
|
(item.clone(), s + frecency_boost)
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect();
|
};
|
||||||
|
|
||||||
results.extend(search_results);
|
// Search core providers
|
||||||
|
for provider in &self.providers {
|
||||||
|
if !filter.is_active(provider.provider_type()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for item in provider.items() {
|
||||||
|
if let Some(scored) = score_item(item) {
|
||||||
|
results.push(scored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search static native providers
|
||||||
|
for provider in &self.static_native_providers {
|
||||||
|
if !filter.is_active(provider.provider_type()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for item in provider.items() {
|
||||||
|
if let Some(scored) = score_item(item) {
|
||||||
|
results.push(scored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
results.sort_by(|a, b| b.1.cmp(&a.1));
|
results.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
results.truncate(max_results);
|
results.truncate(max_results);
|
||||||
|
|
||||||
@@ -479,7 +534,11 @@ impl ProviderManager {
|
|||||||
/// Get all available provider types (for UI tabs)
|
/// Get all available provider types (for UI tabs)
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn available_providers(&self) -> Vec<ProviderType> {
|
pub fn available_providers(&self) -> Vec<ProviderType> {
|
||||||
self.providers.iter().map(|p| p.provider_type()).collect()
|
self.providers
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.provider_type())
|
||||||
|
.chain(self.static_native_providers.iter().map(|p| p.provider_type()))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a widget item by type_id (e.g., "pomodoro", "weather", "media")
|
/// Get a widget item by type_id (e.g., "pomodoro", "weather", "media")
|
||||||
@@ -519,6 +578,16 @@ impl ProviderManager {
|
|||||||
plugin_id, submenu_query
|
plugin_id, submenu_query
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Search in static native providers (clipboard, emoji, ssh, systemd, etc.)
|
||||||
|
for provider in &self.static_native_providers {
|
||||||
|
if provider.type_id() == plugin_id {
|
||||||
|
let actions = provider.query(&submenu_query);
|
||||||
|
if !actions.is_empty() {
|
||||||
|
return Some((display_name.to_string(), actions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Search in dynamic providers
|
// Search in dynamic providers
|
||||||
for provider in &self.dynamic_providers {
|
for provider in &self.dynamic_providers {
|
||||||
if provider.type_id() == plugin_id {
|
if provider.type_id() == plugin_id {
|
||||||
@@ -539,23 +608,6 @@ impl ProviderManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search in static providers (boxed)
|
|
||||||
// Note: Static providers don't typically have submenu support,
|
|
||||||
// but we check for completeness
|
|
||||||
for provider in &self.providers {
|
|
||||||
if let ProviderType::Plugin(type_id) = provider.provider_type()
|
|
||||||
&& type_id == plugin_id
|
|
||||||
{
|
|
||||||
// Static providers use the items() method, not query
|
|
||||||
// Submenu support requires dynamic query capability
|
|
||||||
#[cfg(feature = "dev-logging")]
|
|
||||||
debug!(
|
|
||||||
"[Submenu] Plugin '{}' is static, cannot query for submenu",
|
|
||||||
plugin_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "dev-logging")]
|
#[cfg(feature = "dev-logging")]
|
||||||
debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id);
|
debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id);
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ struct LazyLoadState {
|
|||||||
/// Number of items to display initially and per batch
|
/// Number of items to display initially and per batch
|
||||||
const INITIAL_RESULTS: usize = 15;
|
const INITIAL_RESULTS: usize = 15;
|
||||||
const LOAD_MORE_BATCH: usize = 10;
|
const LOAD_MORE_BATCH: usize = 10;
|
||||||
|
/// Debounce delay for search input (milliseconds)
|
||||||
|
const SEARCH_DEBOUNCE_MS: u64 = 50;
|
||||||
|
|
||||||
pub struct MainWindow {
|
pub struct MainWindow {
|
||||||
window: ApplicationWindow,
|
window: ApplicationWindow,
|
||||||
@@ -69,6 +71,10 @@ pub struct MainWindow {
|
|||||||
custom_prompt: Option<String>,
|
custom_prompt: Option<String>,
|
||||||
/// Lazy loading state
|
/// Lazy loading state
|
||||||
lazy_state: Rc<RefCell<LazyLoadState>>,
|
lazy_state: Rc<RefCell<LazyLoadState>>,
|
||||||
|
/// Debounce source ID for cancelling pending searches
|
||||||
|
debounce_source: Rc<RefCell<Option<gtk4::glib::SourceId>>>,
|
||||||
|
/// Whether we're in dmenu mode (stdin pipe input)
|
||||||
|
is_dmenu_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainWindow {
|
impl MainWindow {
|
||||||
@@ -193,6 +199,9 @@ impl MainWindow {
|
|||||||
|
|
||||||
let lazy_state = Rc::new(RefCell::new(LazyLoadState::default()));
|
let lazy_state = Rc::new(RefCell::new(LazyLoadState::default()));
|
||||||
|
|
||||||
|
// Check if we're in dmenu mode (stdin pipe input)
|
||||||
|
let is_dmenu_mode = providers.borrow().is_dmenu_mode();
|
||||||
|
|
||||||
let main_window = Self {
|
let main_window = Self {
|
||||||
window,
|
window,
|
||||||
search_entry,
|
search_entry,
|
||||||
@@ -210,6 +219,8 @@ impl MainWindow {
|
|||||||
tab_order,
|
tab_order,
|
||||||
custom_prompt,
|
custom_prompt,
|
||||||
lazy_state,
|
lazy_state,
|
||||||
|
debounce_source: Rc::new(RefCell::new(None)),
|
||||||
|
is_dmenu_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
main_window.setup_signals();
|
main_window.setup_signals();
|
||||||
@@ -554,7 +565,7 @@ impl MainWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup_signals(&self) {
|
fn setup_signals(&self) {
|
||||||
// Search input handling with prefix detection
|
// Search input handling with prefix detection and debouncing
|
||||||
let providers = self.providers.clone();
|
let providers = self.providers.clone();
|
||||||
let results_list = self.results_list.clone();
|
let results_list = self.results_list.clone();
|
||||||
let config = self.config.clone();
|
let config = self.config.clone();
|
||||||
@@ -565,11 +576,12 @@ impl MainWindow {
|
|||||||
let search_entry_for_change = self.search_entry.clone();
|
let search_entry_for_change = self.search_entry.clone();
|
||||||
let submenu_state = self.submenu_state.clone();
|
let submenu_state = self.submenu_state.clone();
|
||||||
let lazy_state = self.lazy_state.clone();
|
let lazy_state = self.lazy_state.clone();
|
||||||
|
let debounce_source = self.debounce_source.clone();
|
||||||
|
|
||||||
self.search_entry.connect_changed(move |entry| {
|
self.search_entry.connect_changed(move |entry| {
|
||||||
let raw_query = entry.text();
|
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 {
|
if submenu_state.borrow().active {
|
||||||
let state = submenu_state.borrow();
|
let state = submenu_state.borrow();
|
||||||
let query = raw_query.to_lowercase();
|
let query = raw_query.to_lowercase();
|
||||||
@@ -607,7 +619,7 @@ impl MainWindow {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal mode: parse prefix and search
|
// Normal mode: update prefix/UI immediately for responsiveness
|
||||||
let parsed = ProviderFilter::parse_query(&raw_query);
|
let parsed = ProviderFilter::parse_query(&raw_query);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -643,53 +655,79 @@ impl MainWindow {
|
|||||||
.set_placeholder_text(Some(&format!("Search {}...", prefix_name)));
|
.set_placeholder_text(Some(&format!("Search {}...", prefix_name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cfg = config.borrow();
|
// Cancel any pending debounced search
|
||||||
let max_results = cfg.general.max_results;
|
if let Some(source_id) = debounce_source.borrow_mut().take() {
|
||||||
let frecency_weight = cfg.providers.frecency_weight;
|
source_id.remove();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy loading: store all results but only display initial batch
|
// Clone references for the debounced closure
|
||||||
let initial_count = INITIAL_RESULTS.min(results.len());
|
let providers = providers.clone();
|
||||||
{
|
let results_list = results_list.clone();
|
||||||
let mut lazy = lazy_state.borrow_mut();
|
let config = config.clone();
|
||||||
lazy.all_results = results.clone();
|
let frecency = frecency.clone();
|
||||||
lazy.displayed_count = initial_count;
|
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
|
// Schedule debounced search
|
||||||
for item in results.iter().take(initial_count) {
|
let source_id = gtk4::glib::timeout_add_local_once(
|
||||||
let row = ResultRow::new(item);
|
std::time::Duration::from_millis(SEARCH_DEBOUNCE_MS),
|
||||||
results_list.append(&row);
|
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) {
|
let cfg = config.borrow();
|
||||||
results_list.select_row(Some(&first_row));
|
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)
|
let results: Vec<LaunchItem> = if use_frecency {
|
||||||
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
|
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)
|
// Entry activate signal (Enter key in search entry)
|
||||||
@@ -703,12 +741,14 @@ impl MainWindow {
|
|||||||
let mode_label_for_activate = self.mode_label.clone();
|
let mode_label_for_activate = self.mode_label.clone();
|
||||||
let hints_label_for_activate = self.hints_label.clone();
|
let hints_label_for_activate = self.hints_label.clone();
|
||||||
let search_entry_for_activate = self.search_entry.clone();
|
let search_entry_for_activate = self.search_entry.clone();
|
||||||
|
let is_dmenu_mode_for_activate = self.is_dmenu_mode;
|
||||||
|
|
||||||
self.search_entry.connect_activate(move |entry| {
|
self.search_entry.connect_activate(move |entry| {
|
||||||
let selected = results_list_for_activate
|
let selected = results_list_for_activate
|
||||||
.selected_row()
|
.selected_row()
|
||||||
.or_else(|| results_list_for_activate.row_at_index(0));
|
.or_else(|| results_list_for_activate.row_at_index(0));
|
||||||
|
|
||||||
|
// Handle the case where we have a selected item
|
||||||
if let Some(row) = selected {
|
if let Some(row) = selected {
|
||||||
let index = row.index() as usize;
|
let index = row.index() as usize;
|
||||||
let results = current_results_for_activate.borrow();
|
let results = current_results_for_activate.borrow();
|
||||||
@@ -755,6 +795,10 @@ impl MainWindow {
|
|||||||
&providers_for_activate,
|
&providers_for_activate,
|
||||||
);
|
);
|
||||||
if should_close {
|
if should_close {
|
||||||
|
// In dmenu mode, exit with success code
|
||||||
|
if is_dmenu_mode_for_activate {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
window_for_activate.close();
|
window_for_activate.close();
|
||||||
} else {
|
} else {
|
||||||
// Trigger search refresh for updated widget state
|
// Trigger search refresh for updated widget state
|
||||||
@@ -762,6 +806,16 @@ impl MainWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No item selected/matched - in dmenu mode, output the typed text
|
||||||
|
if is_dmenu_mode_for_activate {
|
||||||
|
let text = entry.text();
|
||||||
|
if !text.is_empty() {
|
||||||
|
println!("{}", text);
|
||||||
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -802,6 +856,7 @@ impl MainWindow {
|
|||||||
let hints_label = self.hints_label.clone();
|
let hints_label = self.hints_label.clone();
|
||||||
let submenu_state = self.submenu_state.clone();
|
let submenu_state = self.submenu_state.clone();
|
||||||
let tab_order = self.tab_order.clone();
|
let tab_order = self.tab_order.clone();
|
||||||
|
let is_dmenu_mode = self.is_dmenu_mode;
|
||||||
|
|
||||||
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
||||||
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
||||||
@@ -824,6 +879,10 @@ impl MainWindow {
|
|||||||
);
|
);
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
} else {
|
} else {
|
||||||
|
// In dmenu mode, exit with cancel code (1)
|
||||||
|
if is_dmenu_mode {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
window.close();
|
window.close();
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
@@ -841,6 +900,10 @@ impl MainWindow {
|
|||||||
);
|
);
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
} else {
|
} else {
|
||||||
|
// In dmenu mode, exit with cancel code (1)
|
||||||
|
if is_dmenu_mode {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
window.close();
|
window.close();
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
@@ -1238,6 +1301,12 @@ impl MainWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn launch_item(item: &LaunchItem, config: &Config, frecency: &Rc<RefCell<FrecencyStore>>) {
|
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
|
// Record this launch for frecency tracking
|
||||||
if config.providers.frecency {
|
if config.providers.frecency {
|
||||||
frecency.borrow_mut().record_launch(&item.id);
|
frecency.borrow_mut().record_launch(&item.id);
|
||||||
@@ -1248,18 +1317,89 @@ impl MainWindow {
|
|||||||
info!("Launching: {} ({})", item.name, item.command);
|
info!("Launching: {} ({})", item.name, item.command);
|
||||||
|
|
||||||
#[cfg(feature = "dev-logging")]
|
#[cfg(feature = "dev-logging")]
|
||||||
debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider);
|
debug!("[UI] Launch details: terminal={}, provider={:?}, id={}", item.terminal, item.provider, item.id);
|
||||||
|
|
||||||
let cmd = if item.terminal {
|
// Check if this is a desktop application (has .desktop file as ID)
|
||||||
let terminal = config.general.terminal_command.as_deref().unwrap_or("xterm");
|
let is_desktop_app = matches!(item.provider, ProviderType::Application)
|
||||||
format!("{} -e {}", terminal, item.command)
|
&& item.id.ends_with(".desktop");
|
||||||
|
|
||||||
|
// Desktop files should be launched via proper launchers that implement the
|
||||||
|
// freedesktop Desktop Entry spec (D-Bus activation, field codes, env vars, etc.)
|
||||||
|
// We delegate to: uwsm (if configured), gio launch, or gtk-launch as fallback.
|
||||||
|
//
|
||||||
|
// Non-desktop items (commands, plugins) use sh -c for shell execution.
|
||||||
|
let result = if is_desktop_app {
|
||||||
|
Self::launch_desktop_file(&item.id, config)
|
||||||
} else {
|
} else {
|
||||||
item.command.clone()
|
Self::launch_command(&item.command, item.terminal, config)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Detect if this is a shell command vs an application launch
|
if let Err(e) = result {
|
||||||
// Shell commands: playerctl, dbus-send, systemctl, journalctl, or anything with shell operators
|
let msg = format!("Failed to launch '{}': {}", item.name, e);
|
||||||
let is_shell_command = cmd.starts_with("playerctl ")
|
log::error!("{}", msg);
|
||||||
|
crate::notify::notify("Launch failed", &msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Launch a .desktop file.
|
||||||
|
///
|
||||||
|
/// When `use_uwsm` is enabled in config, launches via `uwsm app -- <file>`
|
||||||
|
/// which starts the app in a proper systemd user session.
|
||||||
|
///
|
||||||
|
/// Otherwise, uses `gio launch` which is always available (part of glib2/GTK4)
|
||||||
|
/// and handles D-Bus activation, field codes, Terminal flag, etc.
|
||||||
|
fn launch_desktop_file(desktop_path: &str, config: &Config) -> std::io::Result<std::process::Child> {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
// Check if desktop file exists
|
||||||
|
if !Path::new(desktop_path).exists() {
|
||||||
|
let msg = format!("Desktop file not found: {}", desktop_path);
|
||||||
|
log::error!("{}", msg);
|
||||||
|
crate::notify::notify("Launch failed", &msg);
|
||||||
|
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.general.use_uwsm {
|
||||||
|
// Check if uwsm is available
|
||||||
|
let uwsm_available = Command::new("which")
|
||||||
|
.arg("uwsm")
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !uwsm_available {
|
||||||
|
let msg = "uwsm is enabled in config but not installed";
|
||||||
|
log::error!("{}", msg);
|
||||||
|
crate::notify::notify("Launch failed", msg);
|
||||||
|
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Launching via uwsm: {}", desktop_path);
|
||||||
|
Command::new("uwsm")
|
||||||
|
.args(["app", "--", desktop_path])
|
||||||
|
.spawn()
|
||||||
|
} else {
|
||||||
|
info!("Launching via gio: {}", desktop_path);
|
||||||
|
Command::new("gio")
|
||||||
|
.args(["launch", desktop_path])
|
||||||
|
.spawn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Launch a shell command (for non-desktop items like PATH commands, plugins, etc.)
|
||||||
|
fn launch_command(command: &str, terminal: bool, config: &Config) -> std::io::Result<std::process::Child> {
|
||||||
|
let cmd = if terminal {
|
||||||
|
let terminal_cmd = config.general.terminal_command.as_deref().unwrap_or("xterm");
|
||||||
|
format!("{} -e {}", terminal_cmd, command)
|
||||||
|
} else {
|
||||||
|
command.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shell/system commands run directly without uwsm wrapper
|
||||||
|
// (they're typically short-lived or system utilities)
|
||||||
|
let is_system_command = cmd.starts_with("playerctl ")
|
||||||
|| cmd.starts_with("dbus-send ")
|
|| cmd.starts_with("dbus-send ")
|
||||||
|| cmd.starts_with("systemctl ")
|
|| cmd.starts_with("systemctl ")
|
||||||
|| cmd.starts_with("journalctl ")
|
|| cmd.starts_with("journalctl ")
|
||||||
@@ -1269,28 +1409,14 @@ impl MainWindow {
|
|||||||
|| cmd.contains(" > ")
|
|| cmd.contains(" > ")
|
||||||
|| cmd.contains(" < ");
|
|| cmd.contains(" < ");
|
||||||
|
|
||||||
// Use launch wrapper if configured (uwsm, hyprctl, etc.)
|
// Use uwsm for regular commands if enabled (and not a system command)
|
||||||
// But skip wrapper for shell commands - they need sh -c
|
if config.general.use_uwsm && !is_system_command {
|
||||||
let result = match &config.general.launch_wrapper {
|
info!("Launching command via uwsm: {}", cmd);
|
||||||
Some(wrapper) if !wrapper.is_empty() && !is_shell_command => {
|
Command::new("uwsm")
|
||||||
info!("Using launch wrapper: {}", wrapper);
|
.args(["app", "--", "sh", "-c", &cmd])
|
||||||
// Split wrapper into command and args (e.g., "uwsm app --" -> ["uwsm", "app", "--"])
|
.spawn()
|
||||||
let mut wrapper_parts: Vec<&str> = wrapper.split_whitespace().collect();
|
} else {
|
||||||
if wrapper_parts.is_empty() {
|
Command::new("sh").arg("-c").arg(&cmd).spawn()
|
||||||
Command::new("sh").arg("-c").arg(&cmd).spawn()
|
|
||||||
} else {
|
|
||||||
let wrapper_cmd = wrapper_parts.remove(0);
|
|
||||||
Command::new(wrapper_cmd)
|
|
||||||
.args(&wrapper_parts)
|
|
||||||
.arg(&cmd)
|
|
||||||
.spawn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Command::new("sh").arg("-c").arg(&cmd).spawn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
|
||||||
log::error!("Failed to launch '{}': {}", item.name, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,22 +17,47 @@
|
|||||||
# │ Runtimes: /usr/lib/owlry/runtimes/*.so Lua/Rune runtimes │
|
# │ Runtimes: /usr/lib/owlry/runtimes/*.so Lua/Rune runtimes │
|
||||||
# └─────────────────────────────────────────────────────────────────────┘
|
# └─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# DMENU MODE
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
#
|
||||||
|
# Dmenu mode provides interactive selection from piped input.
|
||||||
|
# The selected item is printed to stdout (not executed), so pipe
|
||||||
|
# the output to execute it:
|
||||||
|
#
|
||||||
|
# ┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
# │ # Screenshot menu │
|
||||||
|
# │ printf '%s\n' \ │
|
||||||
|
# │ "grimblast --notify copy screen" \ │
|
||||||
|
# │ "grimblast --notify copy area" \ │
|
||||||
|
# │ | owlry -m dmenu -p "Screenshot" \ │
|
||||||
|
# │ | sh │
|
||||||
|
# │ │
|
||||||
|
# │ # Git branch checkout │
|
||||||
|
# │ git branch | owlry -m dmenu -p "checkout" | xargs git checkout │
|
||||||
|
# │ │
|
||||||
|
# │ # Package search │
|
||||||
|
# │ pacman -Ssq | owlry -m dmenu -p "install" | xargs sudo pacman -S │
|
||||||
|
# └─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
# GENERAL
|
# GENERAL
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
[general]
|
[general]
|
||||||
show_icons = true
|
show_icons = true
|
||||||
max_results = 10
|
max_results = 100
|
||||||
|
|
||||||
# Terminal emulator for SSH, scripts, etc.
|
# Terminal emulator for SSH, scripts, etc.
|
||||||
# Auto-detection order: $TERMINAL → xdg-terminal-exec → DE-native → Wayland → X11 → xterm
|
# Auto-detection order: $TERMINAL → xdg-terminal-exec → DE-native → Wayland → X11 → xterm
|
||||||
# Uncomment to override:
|
# Uncomment to override:
|
||||||
# terminal_command = "kitty"
|
# terminal_command = "kitty"
|
||||||
|
|
||||||
# Launch wrapper for app execution (auto-detected for uwsm/Hyprland)
|
# Enable uwsm (Universal Wayland Session Manager) for launching apps.
|
||||||
# Examples: "uwsm app --", "hyprctl dispatch exec --", ""
|
# When enabled, apps are launched via "uwsm app --" which starts them
|
||||||
# launch_wrapper = "uwsm app --"
|
# in a proper systemd user session for better process management.
|
||||||
|
# Requires: uwsm to be installed
|
||||||
|
# use_uwsm = true
|
||||||
|
|
||||||
# Header tabs - providers shown as toggle buttons (Ctrl+1, Ctrl+2, etc.)
|
# Header tabs - providers shown as toggle buttons (Ctrl+1, Ctrl+2, etc.)
|
||||||
# Values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
|
# Values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
|
||||||
@@ -62,22 +87,54 @@ border_radius = 12
|
|||||||
# text_secondary = "#565f89"
|
# text_secondary = "#565f89"
|
||||||
# accent = "#7aa2f7"
|
# accent = "#7aa2f7"
|
||||||
# accent_bright = "#89b4fa"
|
# accent_bright = "#89b4fa"
|
||||||
|
#
|
||||||
|
# Provider badge colors (optional)
|
||||||
|
# badge_app = "#7aa2f7"
|
||||||
|
# badge_cmd = "#9ece6a"
|
||||||
|
# badge_bookmark = "#e0af68"
|
||||||
|
# badge_calc = "#bb9af7"
|
||||||
|
# badge_clip = "#7dcfff"
|
||||||
|
# badge_dmenu = "#c0caf5"
|
||||||
|
# badge_emoji = "#f7768e"
|
||||||
|
# badge_file = "#73daca"
|
||||||
|
# badge_script = "#ff9e64"
|
||||||
|
# badge_ssh = "#2ac3de"
|
||||||
|
# badge_sys = "#f7768e"
|
||||||
|
# badge_uuctl = "#9ece6a"
|
||||||
|
# badge_web = "#7aa2f7"
|
||||||
|
# badge_media = "#bb9af7"
|
||||||
|
# badge_weather = "#7dcfff"
|
||||||
|
# badge_pomo = "#f7768e"
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
# PLUGINS
|
# PLUGINS
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
#
|
#
|
||||||
# All installed plugins are loaded by default. Use 'disabled' to blacklist.
|
# All installed plugins are loaded by default. Use 'disabled_plugins' to blacklist.
|
||||||
# Plugin IDs: calculator, system, ssh, clipboard, emoji, scripts, bookmarks,
|
# Plugin IDs: calculator, system, ssh, clipboard, emoji, scripts, bookmarks,
|
||||||
# websearch, filesearch, systemd, weather, media, pomodoro
|
# websearch, filesearch, systemd, weather, media, pomodoro
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
enabled = true # Master switch for all plugins
|
||||||
|
|
||||||
# Plugins to disable (by ID)
|
# Plugins to disable (by ID)
|
||||||
disabled = []
|
disabled_plugins = []
|
||||||
|
|
||||||
# Examples:
|
# Examples:
|
||||||
# disabled = ["emoji", "pomodoro"] # Disable specific plugins
|
# disabled_plugins = ["emoji", "pomodoro"] # Disable specific plugins
|
||||||
# disabled = ["weather", "media"] # Disable widget plugins
|
# disabled_plugins = ["weather", "media"] # Disable widget plugins
|
||||||
|
|
||||||
|
# Custom plugin registry URL (defaults to official registry)
|
||||||
|
# registry_url = "https://my-registry.example.com/plugins.json"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Sandbox settings (for Lua/Rune script plugins)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# [plugins.sandbox]
|
||||||
|
# allow_filesystem = false # Allow file system access beyond plugin dir
|
||||||
|
# allow_network = false # Allow network requests
|
||||||
|
# allow_commands = false # Allow shell command execution
|
||||||
|
# memory_limit = 67108864 # Memory limit in bytes (64 MB default)
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
# PROVIDERS
|
# PROVIDERS
|
||||||
@@ -112,10 +169,26 @@ calculator = true # Calculator (= expression)
|
|||||||
websearch = true # Web search (? query)
|
websearch = true # Web search (? query)
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
# Plugin settings
|
# Widget providers (displayed at top of results)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
media = true # MPRIS media player controls
|
||||||
|
weather = false # Weather widget (disabled by default)
|
||||||
|
pomodoro = false # Pomodoro timer (disabled by default)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Provider settings
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Web search engine
|
# Web search engine
|
||||||
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
||||||
# Or custom URL: "https://search.example.com/?q={query}"
|
# Or custom URL: "https://search.example.com/?q={query}"
|
||||||
search_engine = "duckduckgo"
|
search_engine = "duckduckgo"
|
||||||
|
|
||||||
|
# Weather settings (when weather = true)
|
||||||
|
# weather_provider = "wttr.in" # Options: wttr.in, openweathermap, open-meteo
|
||||||
|
# weather_location = "Berlin" # City name or coordinates
|
||||||
|
# weather_api_key = "" # Required for openweathermap
|
||||||
|
|
||||||
|
# Pomodoro settings (when pomodoro = true)
|
||||||
|
# pomodoro_work_mins = 25 # Work session duration
|
||||||
|
# pomodoro_break_mins = 5 # Break duration
|
||||||
|
|||||||
@@ -143,9 +143,10 @@ chmod +x ~/.local/share/owlry/scripts/backup.sh
|
|||||||
**Prefix:** `:bm`
|
**Prefix:** `:bm`
|
||||||
**Package:** `owlry-plugin-bookmarks`
|
**Package:** `owlry-plugin-bookmarks`
|
||||||
|
|
||||||
Browser bookmarks from Chromium-based browsers.
|
Browser bookmarks from Firefox and Chromium-based browsers.
|
||||||
|
|
||||||
**Supported browsers:**
|
**Supported browsers:**
|
||||||
|
- Firefox (reads places.sqlite)
|
||||||
- Google Chrome
|
- Google Chrome
|
||||||
- Brave
|
- Brave
|
||||||
- Microsoft Edge
|
- Microsoft Edge
|
||||||
@@ -236,13 +237,7 @@ Current weather displayed at the top of results.
|
|||||||
- OpenWeatherMap (requires API key)
|
- OpenWeatherMap (requires API key)
|
||||||
- Open-Meteo (no API key required)
|
- Open-Meteo (no API key required)
|
||||||
|
|
||||||
**Configuration:**
|
**Note:** Weather configuration is currently embedded in the plugin. Future versions will support runtime 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
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Temperature, condition, humidity, wind speed
|
- Temperature, condition, humidity, wind speed
|
||||||
@@ -274,13 +269,6 @@ MPRIS media player controls.
|
|||||||
|
|
||||||
Pomodoro timer with work/break cycles.
|
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:**
|
**Features:**
|
||||||
- Configurable work session duration
|
- Configurable work session duration
|
||||||
- Configurable break duration
|
- Configurable break duration
|
||||||
@@ -301,17 +289,17 @@ For convenience, plugins are available in bundle meta-packages:
|
|||||||
|
|
||||||
| Bundle | Plugins |
|
| Bundle | Plugins |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `owlry-essentials` | calculator, system, ssh, scripts, bookmarks |
|
| `owlry-meta-essentials` | calculator, system, ssh, scripts, bookmarks |
|
||||||
| `owlry-widgets` | weather, media, pomodoro |
|
| `owlry-meta-widgets` | weather, media, pomodoro |
|
||||||
| `owlry-tools` | clipboard, emoji, websearch, filesearch, systemd |
|
| `owlry-meta-tools` | clipboard, emoji, websearch, filesearch, systemd |
|
||||||
| `owlry-full` | All of the above |
|
| `owlry-meta-full` | All of the above |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install everything
|
# Install everything
|
||||||
yay -S owlry-full
|
yay -S owlry-meta-full
|
||||||
|
|
||||||
# Or pick a bundle
|
# Or pick a bundle
|
||||||
yay -S owlry-essentials owlry-widgets
|
yay -S owlry-meta-essentials owlry-meta-widgets
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Edit `Cargo.toml`:
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-myplugin"
|
name = "owlry-plugin-myplugin"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
@@ -38,7 +38,7 @@ Edit `src/lib.rs`:
|
|||||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||||
use owlry_plugin_api::{
|
use owlry_plugin_api::{
|
||||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo,
|
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo,
|
||||||
ProviderKind, API_VERSION,
|
ProviderKind, ProviderPosition, API_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
extern "C" fn plugin_info() -> PluginInfo {
|
extern "C" fn plugin_info() -> PluginInfo {
|
||||||
@@ -59,6 +59,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
|||||||
icon: RString::from("application-x-executable"),
|
icon: RString::from("application-x-executable"),
|
||||||
provider_type: ProviderKind::Static,
|
provider_type: ProviderKind::Static,
|
||||||
type_id: RString::from("myplugin"),
|
type_id: RString::from("myplugin"),
|
||||||
|
position: ProviderPosition::Normal,
|
||||||
|
priority: 0, // Use frecency-based ordering
|
||||||
}].into()
|
}].into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,12 +200,19 @@ pub struct ProviderInfo {
|
|||||||
pub icon: RString, // Default icon name
|
pub icon: RString, // Default icon name
|
||||||
pub provider_type: ProviderKind, // Static or Dynamic
|
pub provider_type: ProviderKind, // Static or Dynamic
|
||||||
pub type_id: RString, // Short ID for badges
|
pub type_id: RString, // Short ID for badges
|
||||||
|
pub position: ProviderPosition, // Normal or Widget
|
||||||
|
pub priority: i32, // Result ordering (higher = first)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ProviderKind {
|
pub enum ProviderKind {
|
||||||
Static, // Items loaded at startup via refresh()
|
Static, // Items loaded at startup via refresh()
|
||||||
Dynamic, // Items computed per-query via query()
|
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
|
### PluginItem
|
||||||
|
|||||||
17
justfile
17
justfile
@@ -179,11 +179,18 @@ bump-meta new_version:
|
|||||||
done
|
done
|
||||||
echo "Meta-packages bumped to {{new_version}}"
|
echo "Meta-packages bumped to {{new_version}}"
|
||||||
|
|
||||||
# Bump all non-core crates (plugins + runtimes) to same version
|
# Bump all crates (core + plugins + runtimes) to same version
|
||||||
bump-all new_version:
|
bump-all new_version:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Bump plugins
|
# Bump core
|
||||||
|
toml="crates/owlry/Cargo.toml"
|
||||||
|
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||||
|
if [ "$old" != "{{new_version}}" ]; then
|
||||||
|
echo "Bumping owlry from $old to {{new_version}}"
|
||||||
|
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
|
||||||
|
fi
|
||||||
|
# Bump plugins (including plugin-api)
|
||||||
for toml in crates/owlry-plugin-*/Cargo.toml; do
|
for toml in crates/owlry-plugin-*/Cargo.toml; do
|
||||||
crate=$(basename $(dirname "$toml"))
|
crate=$(basename $(dirname "$toml"))
|
||||||
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||||
@@ -204,9 +211,9 @@ bump-all new_version:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
cargo check --workspace
|
cargo check --workspace
|
||||||
git add crates/owlry-plugin-*/Cargo.toml crates/owlry-lua/Cargo.toml crates/owlry-rune/Cargo.toml Cargo.lock
|
git add crates/*/Cargo.toml Cargo.lock
|
||||||
git commit -m "chore: bump all plugins and runtimes to {{new_version}}"
|
git commit -m "chore: bump all crates to {{new_version}}"
|
||||||
echo "All plugins and runtimes bumped to {{new_version}}"
|
echo "All crates bumped to {{new_version}}"
|
||||||
|
|
||||||
# Bump core version (usage: just bump 0.2.0)
|
# Bump core version (usage: just bump 0.2.0)
|
||||||
bump new_version:
|
bump new_version:
|
||||||
|
|||||||
Reference in New Issue
Block a user