Compare commits

...

6 Commits

Author SHA1 Message Date
1caa0506a2 chore(aur): update PKGBUILDs 2026-04-09 21:18:51 +02:00
61411cd094 chore(owlry): bump version to 1.0.10 2026-04-09 21:16:48 +02:00
4e310223cf chore(owlry-rune): bump version to 1.1.6 2026-04-09 21:16:45 +02:00
6446a253e8 chore(owlry-lua): bump version to 1.1.5 2026-04-09 21:16:42 +02:00
72dcd74e65 chore(owlry-core): bump version to 1.3.6 2026-04-09 21:16:39 +02:00
774b2a4700 feat: configurable tab labels and search nouns from plugin metadata
Script plugins can now declare tab_label and search_noun in their
plugin.toml [[providers]] section. These flow through the Provider
trait, IPC ProviderDesc, and into the UI via provider_meta::resolve().
Unknown plugins auto-generate labels from type_id instead of showing
a generic "Plugin" label.
2026-04-09 21:16:36 +02:00
34 changed files with 386 additions and 118 deletions

View File

@@ -276,7 +276,7 @@ Communication uses newline-delimited JSON over a Unix domain socket at `$XDG_RUN
| Type | Purpose | | Type | Purpose |
|------|---------| |------|---------|
| `Results` | Search results with `Vec<ResultItem>` | | `Results` | Search results with `Vec<ResultItem>` |
| `Providers` | Provider list with `Vec<ProviderDesc>` | | `Providers` | Provider list with `Vec<ProviderDesc>` (includes optional `tab_label`, `search_noun`) |
| `SubmenuItems` | Submenu actions for a plugin | | `SubmenuItems` | Submenu actions for a plugin |
| `Ack` | Success acknowledgement | | `Ack` | Success acknowledgement |
| `Error` | Error with message | | `Error` | Error with message |
@@ -331,6 +331,8 @@ All other providers are native plugins in the separate `owlry-plugins` repo (`so
**Submenu System**: Plugins can return items with `SUBMENU:plugin_id:data` commands. When selected, the plugin is queried with `?SUBMENU:data` to get action items (e.g., systemd service actions). **Submenu System**: Plugins can return items with `SUBMENU:plugin_id:data` commands. When selected, the plugin is queried with `?SUBMENU:data` to get action items (e.g., systemd service actions).
**Provider Display Metadata**: Script plugins can declare `tab_label` and `search_noun` in their `plugin.toml` `[[providers]]` section, or at runtime via `owlry.provider.register()` (Lua) / `register_provider()` (Rune). These flow through the `Provider` trait → `ProviderDescriptor` → IPC `ProviderDesc` → UI. The UI resolves metadata via `provider_meta::resolve()`, checking IPC metadata first, then falling back to the hardcoded match table. Unknown plugins auto-generate labels from `type_id` (e.g., "hs" → "Hs"). Native plugins use the hardcoded match table since `owlry-plugin-api::ProviderInfo` doesn't include these fields (deferred to API v5).
### Plugin API ### Plugin API
Native plugins use the ABI-stable interface in `owlry-plugin-api`: Native plugins use the ABI-stable interface in `owlry-plugin-api`:
@@ -428,6 +430,7 @@ Plugins live in a separate repository: `somegit.dev/Owlibou/owlry-plugins`
- **ABI stability**: Plugin interface uses `abi_stable` crate for safe Rust dynamic linking - **ABI stability**: Plugin interface uses `abi_stable` crate for safe Rust dynamic linking
- **Plugin API v3**: Adds `position` (Normal/Widget) and `priority` fields to ProviderInfo - **Plugin API v3**: Adds `position` (Normal/Widget) and `priority` fields to ProviderInfo
- **ProviderType simplification**: Core uses only `Application`, `Command`, `Dmenu`, `Plugin(String)` - all plugin-specific types removed from core - **ProviderType simplification**: Core uses only `Application`, `Command`, `Dmenu`, `Plugin(String)` - all plugin-specific types removed from core
- **Provider display metadata**: Tab labels and search nouns flow from plugin.toml → `Provider` trait → IPC `ProviderDesc` → UI `provider_meta::resolve()`. Known native plugins use a hardcoded match table; script plugins declare metadata in their manifest or at runtime registration
## Dependencies (Rust 1.90+, GTK 4.12+) ## Dependencies (Rust 1.90+, GTK 4.12+)

8
Cargo.lock generated
View File

@@ -2325,7 +2325,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry" name = "owlry"
version = "1.0.9" version = "1.0.10"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@@ -2346,7 +2346,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-core" name = "owlry-core"
version = "1.3.5" version = "1.3.6"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs", "dirs",
@@ -2374,7 +2374,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-lua" name = "owlry-lua"
version = "1.1.4" version = "1.1.5"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"chrono", "chrono",
@@ -2399,7 +2399,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-rune" name = "owlry-rune"
version = "1.1.5" version = "1.1.6"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs", "dirs",

View File

@@ -459,6 +459,32 @@ owlry plugin create my-plugin -r rune # Rune
owlry plugin run <plugin-id> <command> [args...] owlry plugin run <plugin-id> <command> [args...]
``` ```
### Plugin Display Metadata
Plugins can declare how they appear in the UI via their `plugin.toml`. Without these fields, the tab label and search placeholder default to a capitalized version of the `type_id`.
```toml
[[providers]]
id = "my-plugin"
name = "My Plugin"
type_id = "myplugin"
tab_label = "My Plugin" # Tab button and page title
search_noun = "plugin items" # Search placeholder: "Search plugin items..."
```
Both fields are optional. If omitted, the UI auto-generates from `type_id` (e.g., `type_id = "hs"` shows "Hs" as the tab label).
Lua plugins can also set these at runtime via `owlry.provider.register()`:
```lua
owlry.provider.register({
id = "my-plugin",
name = "My Plugin",
tab_label = "My Plugin",
search_noun = "plugin items",
})
```
### Creating Custom Plugins ### Creating Custom Plugins
See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for: See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for:

View File

@@ -1,6 +1,6 @@
pkgbase = owlry-core pkgbase = owlry-core
pkgdesc = Core daemon for the Owlry application launcher — manages plugins, providers, and search pkgdesc = Core daemon for the Owlry application launcher — manages plugins, providers, and search
pkgver = 1.3.4 pkgver = 1.3.6
pkgrel = 1 pkgrel = 1
url = https://somegit.dev/Owlibou/owlry url = https://somegit.dev/Owlibou/owlry
arch = x86_64 arch = x86_64
@@ -8,7 +8,7 @@ pkgbase = owlry-core
makedepends = cargo makedepends = cargo
depends = gcc-libs depends = gcc-libs
depends = openssl depends = openssl
source = owlry-core-1.3.4.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v1.3.4.tar.gz source = owlry-core-1.3.6.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v1.3.6.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f b2sums = be68489a6148ed3fb12c257994d1c1293a82b9cc2dd1f9a86cfdc780d27c0d07ed31b309b1119525880d605910f653ba7c35b49ae0472cb5a944bb39d36ebaa2
pkgname = owlry-core pkgname = owlry-core

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-core pkgname=owlry-core
pkgver=1.3.4 pkgver=1.3.6
pkgrel=1 pkgrel=1
pkgdesc='Core daemon for the Owlry application launcher — manages plugins, providers, and search' pkgdesc='Core daemon for the Owlry application launcher — manages plugins, providers, and search'
arch=('x86_64') arch=('x86_64')
@@ -9,7 +9,7 @@ license=('GPL-3.0-or-later')
depends=('gcc-libs' 'openssl') depends=('gcc-libs' 'openssl')
makedepends=('cargo') makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v$pkgver.tar.gz")
b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f') b2sums=('be68489a6148ed3fb12c257994d1c1293a82b9cc2dd1f9a86cfdc780d27c0d07ed31b309b1119525880d605910f653ba7c35b49ae0472cb5a944bb39d36ebaa2')
prepare() { prepare() {
cd "owlry" cd "owlry"

View File

@@ -1,6 +1,6 @@
pkgbase = owlry-lua pkgbase = owlry-lua
pkgdesc = Lua scripting runtime for Owlry — enables user-created Lua plugins pkgdesc = Lua scripting runtime for Owlry — enables user-created Lua plugins
pkgver = 1.1.3 pkgver = 1.1.5
pkgrel = 1 pkgrel = 1
url = https://somegit.dev/Owlibou/owlry url = https://somegit.dev/Owlibou/owlry
arch = x86_64 arch = x86_64
@@ -9,7 +9,7 @@ pkgbase = owlry-lua
depends = gcc-libs depends = gcc-libs
depends = lua54 depends = lua54
optdepends = owlry-core: daemon that loads this runtime optdepends = owlry-core: daemon that loads this runtime
source = owlry-lua-1.1.3.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v1.1.3.tar.gz source = owlry-lua-1.1.5.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v1.1.5.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f b2sums = be68489a6148ed3fb12c257994d1c1293a82b9cc2dd1f9a86cfdc780d27c0d07ed31b309b1119525880d605910f653ba7c35b49ae0472cb5a944bb39d36ebaa2
pkgname = owlry-lua pkgname = owlry-lua

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-lua pkgname=owlry-lua
pkgver=1.1.3 pkgver=1.1.5
pkgrel=1 pkgrel=1
pkgdesc="Lua scripting runtime for Owlry — enables user-created Lua plugins" pkgdesc="Lua scripting runtime for Owlry — enables user-created Lua plugins"
arch=('x86_64') arch=('x86_64')
@@ -10,7 +10,7 @@ depends=('gcc-libs' 'lua54')
optdepends=('owlry-core: daemon that loads this runtime') optdepends=('owlry-core: daemon that loads this runtime')
makedepends=('cargo') makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v$pkgver.tar.gz")
b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f') b2sums=('be68489a6148ed3fb12c257994d1c1293a82b9cc2dd1f9a86cfdc780d27c0d07ed31b309b1119525880d605910f653ba7c35b49ae0472cb5a944bb39d36ebaa2')
_cratename=owlry-lua _cratename=owlry-lua

View File

@@ -1,6 +1,6 @@
pkgbase = owlry-rune pkgbase = owlry-rune
pkgdesc = Rune scripting runtime for Owlry — enables user-created Rune plugins pkgdesc = Rune scripting runtime for Owlry — enables user-created Rune plugins
pkgver = 1.1.4 pkgver = 1.1.6
pkgrel = 1 pkgrel = 1
url = https://somegit.dev/Owlibou/owlry url = https://somegit.dev/Owlibou/owlry
arch = x86_64 arch = x86_64
@@ -8,7 +8,7 @@ pkgbase = owlry-rune
makedepends = cargo makedepends = cargo
depends = gcc-libs depends = gcc-libs
optdepends = owlry-core: daemon that loads this runtime optdepends = owlry-core: daemon that loads this runtime
source = owlry-rune-1.1.4.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v1.1.4.tar.gz source = owlry-rune-1.1.6.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v1.1.6.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f b2sums = be68489a6148ed3fb12c257994d1c1293a82b9cc2dd1f9a86cfdc780d27c0d07ed31b309b1119525880d605910f653ba7c35b49ae0472cb5a944bb39d36ebaa2
pkgname = owlry-rune pkgname = owlry-rune

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-rune pkgname=owlry-rune
pkgver=1.1.4 pkgver=1.1.6
pkgrel=1 pkgrel=1
pkgdesc="Rune scripting runtime for Owlry — enables user-created Rune plugins" pkgdesc="Rune scripting runtime for Owlry — enables user-created Rune plugins"
arch=('x86_64') arch=('x86_64')
@@ -10,7 +10,7 @@ depends=('gcc-libs')
optdepends=('owlry-core: daemon that loads this runtime') optdepends=('owlry-core: daemon that loads this runtime')
makedepends=('cargo') makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v$pkgver.tar.gz")
b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f') b2sums=('be68489a6148ed3fb12c257994d1c1293a82b9cc2dd1f9a86cfdc780d27c0d07ed31b309b1119525880d605910f653ba7c35b49ae0472cb5a944bb39d36ebaa2')
_cratename=owlry-rune _cratename=owlry-rune

View File

@@ -1,6 +1,6 @@
pkgbase = owlry pkgbase = owlry
pkgdesc = Lightweight Wayland application launcher with plugin support pkgdesc = Lightweight Wayland application launcher with plugin support
pkgver = 1.0.8 pkgver = 1.0.10
pkgrel = 1 pkgrel = 1
url = https://somegit.dev/Owlibou/owlry url = https://somegit.dev/Owlibou/owlry
arch = x86_64 arch = x86_64
@@ -28,7 +28,7 @@ pkgbase = owlry
optdepends = owlry-plugin-pomodoro: pomodoro timer widget optdepends = owlry-plugin-pomodoro: pomodoro timer widget
optdepends = owlry-lua: Lua runtime for user plugins optdepends = owlry-lua: Lua runtime for user plugins
optdepends = owlry-rune: Rune runtime for user plugins optdepends = owlry-rune: Rune runtime for user plugins
source = owlry-1.0.8.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v1.0.8.tar.gz source = owlry-1.0.10.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v1.0.10.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f b2sums = be68489a6148ed3fb12c257994d1c1293a82b9cc2dd1f9a86cfdc780d27c0d07ed31b309b1119525880d605910f653ba7c35b49ae0472cb5a944bb39d36ebaa2
pkgname = owlry pkgname = owlry

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry pkgname=owlry
pkgver=1.0.8 pkgver=1.0.10
pkgrel=1 pkgrel=1
pkgdesc="Lightweight Wayland application launcher with plugin support" pkgdesc="Lightweight Wayland application launcher with plugin support"
arch=('x86_64') arch=('x86_64')
@@ -29,7 +29,7 @@ optdepends=(
'owlry-rune: Rune runtime for user plugins' 'owlry-rune: Rune runtime for user plugins'
) )
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v$pkgver.tar.gz")
b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f') b2sums=('be68489a6148ed3fb12c257994d1c1293a82b9cc2dd1f9a86cfdc780d27c0d07ed31b309b1119525880d605910f653ba7c35b49ae0472cb5a944bb39d36ebaa2')
prepare() { prepare() {
cd "owlry" cd "owlry"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-core" name = "owlry-core"
version = "1.3.5" version = "1.3.6"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -90,4 +90,8 @@ pub struct ProviderDesc {
pub prefix: Option<String>, pub prefix: Option<String>,
pub icon: String, pub icon: String,
pub position: String, pub position: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tab_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub search_noun: Option<String>,
} }

View File

@@ -73,6 +73,12 @@ pub struct ProviderSpec {
pub provider_type: String, pub provider_type: String,
#[serde(default)] #[serde(default)]
pub type_id: Option<String>, pub type_id: Option<String>,
/// Short label for UI tab button (e.g., "Shutdown"). Defaults to provider name.
#[serde(default)]
pub tab_label: Option<String>,
/// Noun for search placeholder (e.g., "shutdown actions"). Defaults to provider name.
#[serde(default)]
pub search_noun: Option<String>,
} }
fn default_provider_type() -> String { fn default_provider_type() -> String {

View File

@@ -41,6 +41,8 @@ pub struct ScriptProviderInfo {
pub default_icon: RString, pub default_icon: RString,
pub is_static: bool, pub is_static: bool,
pub prefix: owlry_plugin_api::ROption<RString>, pub prefix: owlry_plugin_api::ROption<RString>,
pub tab_label: owlry_plugin_api::ROption<RString>,
pub search_noun: owlry_plugin_api::ROption<RString>,
} }
// Type alias for backwards compatibility // Type alias for backwards compatibility
@@ -273,6 +275,14 @@ impl Provider for RuntimeProvider {
fn items(&self) -> &[LaunchItem] { fn items(&self) -> &[LaunchItem] {
&self.items &self.items
} }
fn tab_label(&self) -> Option<&str> {
self.info.tab_label.as_ref().map(|s| s.as_str()).into()
}
fn search_noun(&self) -> Option<&str> {
self.info.search_noun.as_ref().map(|s| s.as_str()).into()
}
} }
// RuntimeProvider is Send + Sync because: // RuntimeProvider is Send + Sync because:

View File

@@ -42,6 +42,8 @@ pub struct ProviderDescriptor {
pub prefix: Option<String>, pub prefix: Option<String>,
pub icon: String, pub icon: String,
pub position: String, pub position: String,
pub tab_label: Option<String>,
pub search_noun: Option<String>,
} }
/// Trust level of a [`LaunchItem`]'s command, used to gate `sh -c` execution. /// Trust level of a [`LaunchItem`]'s command, used to gate `sh -c` execution.
@@ -148,6 +150,14 @@ pub trait Provider: Send + Sync {
fn provider_type(&self) -> ProviderType; fn provider_type(&self) -> ProviderType;
fn refresh(&mut self); fn refresh(&mut self);
fn items(&self) -> &[LaunchItem]; fn items(&self) -> &[LaunchItem];
/// Short label for UI tab button (e.g., "Shutdown"). None = use default.
fn tab_label(&self) -> Option<&str> {
None
}
/// Noun for search placeholder (e.g., "shutdown actions"). None = use default.
fn search_noun(&self) -> Option<&str> {
None
}
} }
/// Trait for built-in providers that produce results per-keystroke. /// Trait for built-in providers that produce results per-keystroke.
@@ -1022,6 +1032,8 @@ impl ProviderManager {
prefix, prefix,
icon, icon,
position: "normal".to_string(), position: "normal".to_string(),
tab_label: provider.tab_label().map(String::from),
search_noun: provider.search_noun().map(String::from),
}); });
} }
@@ -1033,6 +1045,8 @@ impl ProviderManager {
prefix: provider.prefix().map(String::from), prefix: provider.prefix().map(String::from),
icon: provider.icon().to_string(), icon: provider.icon().to_string(),
position: provider.position_str().to_string(), position: provider.position_str().to_string(),
tab_label: None,
search_noun: None,
}); });
} }
@@ -1044,6 +1058,8 @@ impl ProviderManager {
prefix: provider.prefix().map(String::from), prefix: provider.prefix().map(String::from),
icon: provider.icon().to_string(), icon: provider.icon().to_string(),
position: provider.position_str().to_string(), position: provider.position_str().to_string(),
tab_label: None,
search_noun: None,
}); });
} }
@@ -1055,6 +1071,8 @@ impl ProviderManager {
prefix: provider.prefix().map(String::from), prefix: provider.prefix().map(String::from),
icon: provider.icon().to_string(), icon: provider.icon().to_string(),
position: provider.position_str().to_string(), position: provider.position_str().to_string(),
tab_label: None,
search_noun: None,
}); });
} }

View File

@@ -472,6 +472,8 @@ fn descriptor_to_desc(desc: crate::providers::ProviderDescriptor) -> ProviderDes
prefix: desc.prefix, prefix: desc.prefix,
icon: desc.icon, icon: desc.icon,
position: desc.position, position: desc.position,
tab_label: desc.tab_label,
search_noun: desc.search_noun,
} }
} }

View File

@@ -64,6 +64,8 @@ fn test_providers_response() {
prefix: Some(":app".into()), prefix: Some(":app".into()),
icon: "application-x-executable".into(), icon: "application-x-executable".into(),
position: "normal".into(), position: "normal".into(),
tab_label: None,
search_noun: None,
}], }],
}; };
let json = serde_json::to_string(&resp).unwrap(); let json = serde_json::to_string(&resp).unwrap();

View File

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

View File

@@ -34,6 +34,8 @@ fn register_provider(lua: &Lua, config: Table) -> LuaResult<()> {
.get::<Option<String>>("default_icon")? .get::<Option<String>>("default_icon")?
.unwrap_or_else(|| "application-x-addon".to_string()); .unwrap_or_else(|| "application-x-addon".to_string());
let prefix: Option<String> = config.get("prefix")?; let prefix: Option<String> = config.get("prefix")?;
let tab_label: Option<String> = config.get("tab_label")?;
let search_noun: Option<String> = config.get("search_noun")?;
// Check if it's a dynamic provider (has query function) or static (has refresh) // Check if it's a dynamic provider (has query function) or static (has refresh)
let has_query: bool = config.contains_key("query")?; let has_query: bool = config.contains_key("query")?;
@@ -70,6 +72,8 @@ fn register_provider(lua: &Lua, config: Table) -> LuaResult<()> {
default_icon, default_icon,
prefix, prefix,
is_dynamic, is_dynamic,
tab_label,
search_noun,
}); });
}); });

View File

@@ -113,6 +113,10 @@ pub struct LuaProviderInfo {
pub is_static: bool, pub is_static: bool,
/// Optional prefix trigger /// Optional prefix trigger
pub prefix: ROption<RString>, pub prefix: ROption<RString>,
/// Short label for UI tab button
pub tab_label: ROption<RString>,
/// Noun for search placeholder
pub search_noun: ROption<RString>,
} }
/// Internal runtime state /// Internal runtime state
@@ -184,6 +188,8 @@ impl LuaRuntimeState {
default_icon: RString::from(reg.default_icon.as_str()), default_icon: RString::from(reg.default_icon.as_str()),
is_static: !reg.is_dynamic, is_static: !reg.is_dynamic,
prefix: reg.prefix.map(RString::from).into(), prefix: reg.prefix.map(RString::from).into(),
tab_label: reg.tab_label.map(RString::from).into(),
search_noun: reg.search_noun.map(RString::from).into(),
}); });
} }
} }

View File

@@ -19,6 +19,8 @@ pub struct ProviderRegistration {
pub default_icon: String, pub default_icon: String,
pub prefix: Option<String>, pub prefix: Option<String>,
pub is_dynamic: bool, pub is_dynamic: bool,
pub tab_label: Option<String>,
pub search_noun: Option<String>,
} }
/// A loaded plugin instance /// A loaded plugin instance
@@ -113,6 +115,8 @@ impl LoadedPlugin {
.unwrap_or_else(|| "application-x-addon".to_string()), .unwrap_or_else(|| "application-x-addon".to_string()),
prefix: decl.prefix.clone(), prefix: decl.prefix.clone(),
is_dynamic: decl.provider_type == "dynamic", is_dynamic: decl.provider_type == "dynamic",
tab_label: decl.tab_label.clone(),
search_noun: decl.search_noun.clone(),
}); });
} }
} }

View File

@@ -34,6 +34,10 @@ pub struct ProviderDecl {
pub provider_type: String, pub provider_type: String,
#[serde(default)] #[serde(default)]
pub type_id: Option<String>, pub type_id: Option<String>,
#[serde(default)]
pub tab_label: Option<String>,
#[serde(default)]
pub search_noun: Option<String>,
} }
fn default_provider_type() -> String { fn default_provider_type() -> String {

View File

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

View File

@@ -16,6 +16,8 @@ pub struct ProviderRegistration {
pub default_icon: String, pub default_icon: String,
pub is_static: bool, pub is_static: bool,
pub prefix: Option<String>, pub prefix: Option<String>,
pub tab_label: Option<String>,
pub search_noun: Option<String>,
} }
/// An item returned by a provider /// An item returned by a provider

View File

@@ -55,6 +55,8 @@ pub struct RuneProviderInfo {
pub default_icon: RString, pub default_icon: RString,
pub is_static: bool, pub is_static: bool,
pub prefix: ROption<RString>, pub prefix: ROption<RString>,
pub tab_label: ROption<RString>,
pub search_noun: ROption<RString>,
} }
/// Opaque handle to runtime state /// Opaque handle to runtime state
@@ -126,6 +128,16 @@ extern "C" fn runtime_init(plugins_dir: RStr<'_>, owlry_version: RStr<'_>) -> Ru
.as_ref() .as_ref()
.map(|p| RString::from(p.as_str())) .map(|p| RString::from(p.as_str()))
.into(), .into(),
tab_label: reg
.tab_label
.as_ref()
.map(|s| RString::from(s.as_str()))
.into(),
search_noun: reg
.search_noun
.as_ref()
.map(|s| RString::from(s.as_str()))
.into(),
}); });
} }
state.plugins.insert(id, plugin); state.plugins.insert(id, plugin);

View File

@@ -70,6 +70,8 @@ impl LoadedPlugin {
default_icon: decl.icon.clone().unwrap_or_else(|| "application-x-addon".to_string()), default_icon: decl.icon.clone().unwrap_or_else(|| "application-x-addon".to_string()),
is_static: decl.provider_type != "dynamic", is_static: decl.provider_type != "dynamic",
prefix: decl.prefix.clone(), prefix: decl.prefix.clone(),
tab_label: decl.tab_label.clone(),
search_noun: decl.search_noun.clone(),
}); });
} }
} }

View File

@@ -29,6 +29,10 @@ pub struct ProviderDecl {
pub provider_type: String, pub provider_type: String,
#[serde(default)] #[serde(default)]
pub type_id: Option<String>, pub type_id: Option<String>,
#[serde(default)]
pub tab_label: Option<String>,
#[serde(default)]
pub search_noun: Option<String>,
} }
fn default_provider_type() -> String { fn default_provider_type() -> String {

View File

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

View File

@@ -8,7 +8,7 @@ use log::warn;
use owlry_core::config::Config; use owlry_core::config::Config;
use owlry_core::data::FrecencyStore; use owlry_core::data::FrecencyStore;
use owlry_core::filter::ProviderFilter; use owlry_core::filter::ProviderFilter;
use owlry_core::ipc::ResultItem; use owlry_core::ipc::{ProviderDesc, ResultItem};
use owlry_core::providers::{ItemSource, LaunchItem, ProviderManager, ProviderType}; use owlry_core::providers::{ItemSource, LaunchItem, ProviderManager, ProviderType};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@@ -349,13 +349,12 @@ impl SearchBackend {
} }
} }
/// Get available provider type IDs from the daemon, or from local manager. /// Get available provider descriptors from the daemon, or from local manager.
#[allow(dead_code)] pub fn available_providers(&mut self) -> Vec<ProviderDesc> {
pub fn available_provider_ids(&mut self) -> Vec<String> {
match self { match self {
SearchBackend::Daemon(handle) => match handle.client.lock() { SearchBackend::Daemon(handle) => match handle.client.lock() {
Ok(mut client) => match client.providers() { Ok(mut client) => match client.providers() {
Ok(descs) => descs.into_iter().map(|d| d.id).collect(), Ok(descs) => descs,
Err(e) => { Err(e) => {
warn!("IPC providers query failed: {}", e); warn!("IPC providers query failed: {}", e);
Vec::new() Vec::new()
@@ -369,7 +368,15 @@ impl SearchBackend {
SearchBackend::Local { providers, .. } => providers SearchBackend::Local { providers, .. } => providers
.available_providers() .available_providers()
.into_iter() .into_iter()
.map(|d| d.id) .map(|d| ProviderDesc {
id: d.id,
name: d.name,
prefix: d.prefix,
icon: d.icon,
position: d.position,
tab_label: d.tab_label,
search_noun: d.search_noun,
})
.collect(), .collect(),
} }
} }

View File

@@ -355,6 +355,8 @@ mod tests {
prefix: Some(":app".into()), prefix: Some(":app".into()),
icon: "application-x-executable".into(), icon: "application-x-executable".into(),
position: "normal".into(), position: "normal".into(),
tab_label: None,
search_noun: None,
}], }],
}; };

View File

@@ -841,6 +841,8 @@ type = "static"
type_id = "{type_id}" type_id = "{type_id}"
icon = "application-x-addon" icon = "application-x-addon"
# prefix = ":{type_id}" # prefix = ":{type_id}"
# tab_label = "{display}"
# search_noun = "{type_id} items"
"#, "#,
name = name, name = name,
display = display, display = display,

View File

@@ -11,6 +11,7 @@ use gtk4::{
use log::info; use log::info;
use owlry_core::config::Config; use owlry_core::config::Config;
use owlry_core::filter::ProviderFilter; use owlry_core::filter::ProviderFilter;
use owlry_core::ipc::ProviderDesc;
use owlry_core::providers::{ItemSource, LaunchItem, ProviderType}; use owlry_core::providers::{ItemSource, LaunchItem, ProviderType};
#[cfg(feature = "dev-logging")] #[cfg(feature = "dev-logging")]
@@ -77,6 +78,8 @@ pub struct MainWindow {
debounce_source: Rc<RefCell<Option<gtk4::glib::SourceId>>>, debounce_source: Rc<RefCell<Option<gtk4::glib::SourceId>>>,
/// Whether we're in dmenu mode (stdin pipe input) /// Whether we're in dmenu mode (stdin pipe input)
is_dmenu_mode: bool, is_dmenu_mode: bool,
/// Provider display metadata from daemon (keyed by provider id)
provider_descs: Rc<HashMap<String, ProviderDesc>>,
} }
impl MainWindow { impl MainWindow {
@@ -143,8 +146,24 @@ impl MainWindow {
let tab_strings: Vec<String> = enabled.iter().map(|p| p.to_string()).collect(); let tab_strings: Vec<String> = enabled.iter().map(|p| p.to_string()).collect();
let tab_order = Rc::new(enabled); let tab_order = Rc::new(enabled);
// Fetch provider display metadata from daemon/local backend
let provider_descs: HashMap<String, ProviderDesc> = backend
.borrow_mut()
.available_providers()
.into_iter()
.map(|d| (d.id.clone(), d))
.collect();
let provider_descs = Rc::new(provider_descs);
// Update mode label with resolved plugin name (if applicable)
mode_label.set_label(&Self::mode_display_name(
&filter.borrow(),
&provider_descs,
));
// Create toggle buttons for each enabled provider // Create toggle buttons for each enabled provider
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &tab_strings); let filter_buttons =
Self::create_filter_buttons(&filter_tabs, &filter, &tab_strings, &provider_descs);
let filter_buttons = Rc::new(RefCell::new(filter_buttons)); let filter_buttons = Rc::new(RefCell::new(filter_buttons));
header_box.append(&mode_label); header_box.append(&mode_label);
@@ -153,7 +172,7 @@ impl MainWindow {
// Search entry with dynamic placeholder (or custom prompt if provided) // Search entry with dynamic placeholder (or custom prompt if provided)
let placeholder = custom_prompt let placeholder = custom_prompt
.clone() .clone()
.unwrap_or_else(|| Self::build_placeholder(&filter.borrow())); .unwrap_or_else(|| Self::build_placeholder(&filter.borrow(), &provider_descs));
let search_entry = Entry::builder() let search_entry = Entry::builder()
.placeholder_text(&placeholder) .placeholder_text(&placeholder)
.hexpand(true) .hexpand(true)
@@ -223,6 +242,7 @@ impl MainWindow {
lazy_state, lazy_state,
debounce_source: Rc::new(RefCell::new(None)), debounce_source: Rc::new(RefCell::new(None)),
is_dmenu_mode, is_dmenu_mode,
provider_descs,
}; };
main_window.setup_signals(); main_window.setup_signals();
@@ -267,6 +287,7 @@ impl MainWindow {
container: &GtkBox, container: &GtkBox,
filter: &Rc<RefCell<ProviderFilter>>, filter: &Rc<RefCell<ProviderFilter>>,
tabs: &[String], tabs: &[String],
descs: &HashMap<String, ProviderDesc>,
) -> HashMap<ProviderType, ToggleButton> { ) -> HashMap<ProviderType, ToggleButton> {
let mut buttons = HashMap::new(); let mut buttons = HashMap::new();
@@ -280,7 +301,7 @@ impl MainWindow {
} }
}; };
let base_label = Self::provider_tab_label(&provider_type); let meta = Self::provider_meta(&provider_type, descs);
// Show number hint in the label for first 9 tabs (using superscript) // Show number hint in the label for first 9 tabs (using superscript)
let label = if idx < 9 { let label = if idx < 9 {
let superscript = match idx + 1 { let superscript = match idx + 1 {
@@ -295,9 +316,9 @@ impl MainWindow {
9 => "", 9 => "",
_ => "", _ => "",
}; };
format!("{}{}", base_label, superscript) format!("{}{}", meta.tab_label, superscript)
} else { } else {
base_label.to_string() meta.tab_label.clone()
}; };
let shortcut = format!("Ctrl+{}", idx + 1); let shortcut = format!("Ctrl+{}", idx + 1);
@@ -308,8 +329,7 @@ impl MainWindow {
.build(); .build();
button.add_css_class("owlry-filter-button"); button.add_css_class("owlry-filter-button");
let css_class = Self::provider_css_class(&provider_type); button.add_css_class(&meta.css_class);
button.add_css_class(css_class);
container.append(&button); container.append(&button);
buttons.insert(provider_type, button); buttons.insert(provider_type, button);
@@ -318,21 +338,42 @@ impl MainWindow {
buttons buttons
} }
/// Get display label for a provider tab /// Get the mode display name, resolving plugin names via IPC metadata.
/// Core types have fixed labels; plugins derive labels from type_id fn mode_display_name(
fn provider_tab_label(provider: &ProviderType) -> &'static str { filter: &ProviderFilter,
provider_meta::meta_for(provider).tab_label descs: &HashMap<String, ProviderDesc>,
) -> String {
let base = filter.mode_display_name();
// "Plugin" is the generic fallback — resolve it to the actual name
if base == "Plugin" {
let enabled = filter.enabled_providers();
if enabled.len() == 1
&& let ProviderType::Plugin(_) = &enabled[0]
{
return Self::provider_meta(&enabled[0], descs).tab_label;
}
}
base.to_string()
} }
fn provider_css_class(provider: &ProviderType) -> &'static str { /// Resolve display metadata for a provider, checking IPC data first.
provider_meta::meta_for(provider).css_class fn provider_meta(
provider: &ProviderType,
descs: &HashMap<String, ProviderDesc>,
) -> provider_meta::ProviderMeta {
let type_id = provider.to_string();
let desc = descs.get(&type_id);
provider_meta::resolve(provider, desc)
} }
fn build_placeholder(filter: &ProviderFilter) -> String { fn build_placeholder(
let active: Vec<&str> = filter filter: &ProviderFilter,
descs: &HashMap<String, ProviderDesc>,
) -> String {
let active: Vec<String> = filter
.enabled_providers() .enabled_providers()
.iter() .iter()
.map(|p| provider_meta::meta_for(p).search_noun) .map(|p| Self::provider_meta(p, descs).search_noun)
.collect(); .collect();
format!("Search {}...", active.join(", ")) format!("Search {}...", active.join(", "))
@@ -451,6 +492,7 @@ impl MainWindow {
search_entry: &Entry, search_entry: &Entry,
filter: &Rc<RefCell<ProviderFilter>>, filter: &Rc<RefCell<ProviderFilter>>,
config: &Rc<RefCell<Config>>, config: &Rc<RefCell<Config>>,
descs: &HashMap<String, ProviderDesc>,
) { ) {
#[cfg(feature = "dev-logging")] #[cfg(feature = "dev-logging")]
debug!("[UI] Exiting submenu"); debug!("[UI] Exiting submenu");
@@ -463,9 +505,12 @@ impl MainWindow {
}; };
// Restore UI // Restore UI
mode_label.set_label(filter.borrow().mode_display_name()); mode_label.set_label(&Self::mode_display_name(&filter.borrow(), descs));
hints_label.set_label(&Self::build_hints(&config.borrow().providers)); hints_label.set_label(&Self::build_hints(&config.borrow().providers));
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow()))); search_entry.set_placeholder_text(Some(&Self::build_placeholder(
&filter.borrow(),
descs,
)));
search_entry.set_text(&saved_search); search_entry.set_text(&saved_search);
// Trigger refresh by emitting changed signal // Trigger refresh by emitting changed signal
@@ -484,6 +529,7 @@ impl MainWindow {
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(); let debounce_source = self.debounce_source.clone();
let descs = self.provider_descs.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();
@@ -532,7 +578,7 @@ impl MainWindow {
f.set_prefix(parsed.prefix.clone()); f.set_prefix(parsed.prefix.clone());
} }
mode_label.set_label(filter.borrow().mode_display_name()); mode_label.set_label(&Self::mode_display_name(&filter.borrow(), &descs));
if let Some(ref prefix) = parsed.prefix { if let Some(ref prefix) = parsed.prefix {
let prefix_name = match prefix { let prefix_name = match prefix {
@@ -779,6 +825,7 @@ impl MainWindow {
let search_entry = self.search_entry.clone(); let search_entry = self.search_entry.clone();
let mode_label = self.mode_label.clone(); let mode_label = self.mode_label.clone();
let ptype = provider_type.clone(); let ptype = provider_type.clone();
let descs = self.provider_descs.clone();
button.connect_toggled(move |btn| { button.connect_toggled(move |btn| {
{ {
@@ -789,8 +836,11 @@ impl MainWindow {
f.disable(ptype.clone()); f.disable(ptype.clone());
} }
} }
mode_label.set_label(filter.borrow().mode_display_name()); mode_label.set_label(&Self::mode_display_name(&filter.borrow(), &descs));
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow()))); search_entry.set_placeholder_text(Some(&Self::build_placeholder(
&filter.borrow(),
&descs,
)));
search_entry.emit_by_name::<()>("changed", &[]); search_entry.emit_by_name::<()>("changed", &[]);
}); });
} }
@@ -811,6 +861,7 @@ impl MainWindow {
let tab_order = self.tab_order.clone(); let tab_order = self.tab_order.clone();
let is_dmenu_mode = self.is_dmenu_mode; let is_dmenu_mode = self.is_dmenu_mode;
let lazy_state_for_keys = self.lazy_state.clone(); let lazy_state_for_keys = self.lazy_state.clone();
let provider_descs = self.provider_descs.clone();
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);
@@ -833,6 +884,7 @@ impl MainWindow {
&search_entry, &search_entry,
&filter, &filter,
&config, &config,
&provider_descs,
); );
gtk4::glib::Propagation::Stop gtk4::glib::Propagation::Stop
} else { } else {
@@ -854,6 +906,7 @@ impl MainWindow {
&search_entry, &search_entry,
&filter, &filter,
&config, &config,
&provider_descs,
); );
gtk4::glib::Propagation::Stop gtk4::glib::Propagation::Stop
} else { } else {
@@ -901,6 +954,7 @@ impl MainWindow {
&mode_label, &mode_label,
&tab_order, &tab_order,
!shift, !shift,
&provider_descs,
); );
} }
gtk4::glib::Propagation::Stop gtk4::glib::Propagation::Stop
@@ -914,6 +968,7 @@ impl MainWindow {
&mode_label, &mode_label,
&tab_order, &tab_order,
false, false,
&provider_descs,
); );
} }
gtk4::glib::Propagation::Stop gtk4::glib::Propagation::Stop
@@ -953,6 +1008,7 @@ impl MainWindow {
&filter_buttons, &filter_buttons,
&search_entry, &search_entry,
&mode_label, &mode_label,
&provider_descs,
); );
} else { } else {
info!( info!(
@@ -1041,6 +1097,7 @@ impl MainWindow {
mode_label: &Label, mode_label: &Label,
tab_order: &[ProviderType], tab_order: &[ProviderType],
forward: bool, forward: bool,
descs: &HashMap<String, ProviderDesc>,
) { ) {
if tab_order.is_empty() { if tab_order.is_empty() {
return; return;
@@ -1113,8 +1170,8 @@ impl MainWindow {
} }
} }
mode_label.set_label(filter.borrow().mode_display_name()); mode_label.set_label(&Self::mode_display_name(&filter.borrow(), descs));
entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow()))); entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow(), descs)));
entry.emit_by_name::<()>("changed", &[]); entry.emit_by_name::<()>("changed", &[]);
} }
@@ -1124,6 +1181,7 @@ impl MainWindow {
buttons: &Rc<RefCell<HashMap<ProviderType, ToggleButton>>>, buttons: &Rc<RefCell<HashMap<ProviderType, ToggleButton>>>,
entry: &Entry, entry: &Entry,
mode_label: &Label, mode_label: &Label,
descs: &HashMap<String, ProviderDesc>,
) { ) {
{ {
let mut f = filter.borrow_mut(); let mut f = filter.borrow_mut();
@@ -1134,8 +1192,8 @@ impl MainWindow {
button.set_active(filter.borrow().is_enabled(provider)); button.set_active(filter.borrow().is_enabled(provider));
} }
mode_label.set_label(filter.borrow().mode_display_name()); mode_label.set_label(&Self::mode_display_name(&filter.borrow(), descs));
entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow()))); entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow(), descs)));
entry.emit_by_name::<()>("changed", &[]); entry.emit_by_name::<()>("changed", &[]);
} }

View File

@@ -1,101 +1,191 @@
use owlry_core::ipc::ProviderDesc;
use owlry_core::providers::ProviderType; use owlry_core::providers::ProviderType;
/// Display metadata for a provider. /// Display metadata for a provider.
pub struct ProviderMeta { pub struct ProviderMeta {
pub tab_label: &'static str, pub tab_label: String,
pub css_class: &'static str, pub css_class: String,
pub search_noun: &'static str, pub search_noun: String,
} }
/// Return display metadata for a provider type. /// Resolve display metadata, checking IPC-provided metadata first, then hardcoded fallback.
pub fn meta_for(provider: &ProviderType) -> ProviderMeta { pub fn resolve(provider: &ProviderType, desc: Option<&ProviderDesc>) -> ProviderMeta {
let fallback = hardcoded(provider);
ProviderMeta {
tab_label: desc
.and_then(|d| d.tab_label.clone())
.unwrap_or(fallback.tab_label),
css_class: fallback.css_class, // always auto-derived from type_id
search_noun: desc
.and_then(|d| d.search_noun.clone())
.unwrap_or(fallback.search_noun),
}
}
/// Hardcoded metadata for known provider types. Unknown plugins get auto-generated defaults.
fn hardcoded(provider: &ProviderType) -> ProviderMeta {
match provider { match provider {
ProviderType::Application => ProviderMeta { ProviderType::Application => ProviderMeta {
tab_label: "Apps", tab_label: "Apps".to_string(),
css_class: "owlry-filter-app", css_class: "owlry-filter-app".to_string(),
search_noun: "applications", search_noun: "applications".to_string(),
}, },
ProviderType::Command => ProviderMeta { ProviderType::Command => ProviderMeta {
tab_label: "Cmds", tab_label: "Cmds".to_string(),
css_class: "owlry-filter-cmd", css_class: "owlry-filter-cmd".to_string(),
search_noun: "commands", search_noun: "commands".to_string(),
}, },
ProviderType::Dmenu => ProviderMeta { ProviderType::Dmenu => ProviderMeta {
tab_label: "Dmenu", tab_label: "Dmenu".to_string(),
css_class: "owlry-filter-dmenu", css_class: "owlry-filter-dmenu".to_string(),
search_noun: "options", search_noun: "options".to_string(),
}, },
ProviderType::Plugin(type_id) => match type_id.as_str() { ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => ProviderMeta { "bookmarks" => ProviderMeta {
tab_label: "Bookmarks", tab_label: "Bookmarks".to_string(),
css_class: "owlry-filter-bookmark", css_class: "owlry-filter-bookmark".to_string(),
search_noun: "bookmarks", search_noun: "bookmarks".to_string(),
}, },
"calc" => ProviderMeta { "calc" => ProviderMeta {
tab_label: "Calc", tab_label: "Calc".to_string(),
css_class: "owlry-filter-calc", css_class: "owlry-filter-calc".to_string(),
search_noun: "calculator", search_noun: "calculator".to_string(),
}, },
"clipboard" => ProviderMeta { "clipboard" => ProviderMeta {
tab_label: "Clip", tab_label: "Clip".to_string(),
css_class: "owlry-filter-clip", css_class: "owlry-filter-clip".to_string(),
search_noun: "clipboard", search_noun: "clipboard".to_string(),
}, },
"emoji" => ProviderMeta { "emoji" => ProviderMeta {
tab_label: "Emoji", tab_label: "Emoji".to_string(),
css_class: "owlry-filter-emoji", css_class: "owlry-filter-emoji".to_string(),
search_noun: "emoji", search_noun: "emoji".to_string(),
}, },
"filesearch" => ProviderMeta { "filesearch" => ProviderMeta {
tab_label: "Files", tab_label: "Files".to_string(),
css_class: "owlry-filter-file", css_class: "owlry-filter-file".to_string(),
search_noun: "files", search_noun: "files".to_string(),
}, },
"media" => ProviderMeta { "media" => ProviderMeta {
tab_label: "Media", tab_label: "Media".to_string(),
css_class: "owlry-filter-media", css_class: "owlry-filter-media".to_string(),
search_noun: "media", search_noun: "media".to_string(),
}, },
"pomodoro" => ProviderMeta { "pomodoro" => ProviderMeta {
tab_label: "Pomo", tab_label: "Pomo".to_string(),
css_class: "owlry-filter-pomodoro", css_class: "owlry-filter-pomodoro".to_string(),
search_noun: "pomodoro", search_noun: "pomodoro".to_string(),
}, },
"scripts" => ProviderMeta { "scripts" => ProviderMeta {
tab_label: "Scripts", tab_label: "Scripts".to_string(),
css_class: "owlry-filter-script", css_class: "owlry-filter-script".to_string(),
search_noun: "scripts", search_noun: "scripts".to_string(),
}, },
"ssh" => ProviderMeta { "ssh" => ProviderMeta {
tab_label: "SSH", tab_label: "SSH".to_string(),
css_class: "owlry-filter-ssh", css_class: "owlry-filter-ssh".to_string(),
search_noun: "SSH hosts", search_noun: "SSH hosts".to_string(),
}, },
"system" => ProviderMeta { "system" => ProviderMeta {
tab_label: "System", tab_label: "System".to_string(),
css_class: "owlry-filter-sys", css_class: "owlry-filter-sys".to_string(),
search_noun: "system", search_noun: "system".to_string(),
}, },
"uuctl" => ProviderMeta { "uuctl" => ProviderMeta {
tab_label: "uuctl", tab_label: "uuctl".to_string(),
css_class: "owlry-filter-uuctl", css_class: "owlry-filter-uuctl".to_string(),
search_noun: "uuctl units", search_noun: "uuctl units".to_string(),
}, },
"weather" => ProviderMeta { "weather" => ProviderMeta {
tab_label: "Weather", tab_label: "Weather".to_string(),
css_class: "owlry-filter-weather", css_class: "owlry-filter-weather".to_string(),
search_noun: "weather", search_noun: "weather".to_string(),
}, },
"websearch" => ProviderMeta { "websearch" => ProviderMeta {
tab_label: "Web", tab_label: "Web".to_string(),
css_class: "owlry-filter-web", css_class: "owlry-filter-web".to_string(),
search_noun: "web", search_noun: "web".to_string(),
},
_ => ProviderMeta {
tab_label: "Plugin",
css_class: "owlry-filter-plugin",
search_noun: "plugins",
}, },
// Unknown plugin: auto-generate from type_id
_ => {
let label = capitalize(type_id);
ProviderMeta {
css_class: format!("owlry-filter-{}", type_id),
search_noun: type_id.to_string(),
tab_label: label,
}
}
}, },
} }
} }
/// Capitalize first character of a string.
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_plugin_returns_hardcoded() {
let meta = resolve(&ProviderType::Plugin("calc".to_string()), None);
assert_eq!(meta.tab_label, "Calc");
assert_eq!(meta.css_class, "owlry-filter-calc");
assert_eq!(meta.search_noun, "calculator");
}
#[test]
fn unknown_plugin_generates_from_type_id() {
let meta = resolve(&ProviderType::Plugin("hs".to_string()), None);
assert_eq!(meta.tab_label, "Hs");
assert_eq!(meta.css_class, "owlry-filter-hs");
assert_eq!(meta.search_noun, "hs");
}
#[test]
fn ipc_metadata_overrides_hardcoded() {
let desc = ProviderDesc {
id: "hs".to_string(),
name: "Hypr Shutdown".to_string(),
prefix: None,
icon: "system-shutdown".to_string(),
position: "normal".to_string(),
tab_label: Some("Shutdown".to_string()),
search_noun: Some("shutdown actions".to_string()),
};
let meta = resolve(&ProviderType::Plugin("hs".to_string()), Some(&desc));
assert_eq!(meta.tab_label, "Shutdown");
assert_eq!(meta.search_noun, "shutdown actions");
// css_class always from hardcoded
assert_eq!(meta.css_class, "owlry-filter-hs");
}
#[test]
fn ipc_partial_override_falls_back() {
let desc = ProviderDesc {
id: "calc".to_string(),
name: "Calculator".to_string(),
prefix: None,
icon: "calc".to_string(),
position: "normal".to_string(),
tab_label: Some("Calculator".to_string()),
search_noun: None, // not provided
};
let meta = resolve(&ProviderType::Plugin("calc".to_string()), Some(&desc));
assert_eq!(meta.tab_label, "Calculator"); // from IPC
assert_eq!(meta.search_noun, "calculator"); // from hardcoded fallback
}
#[test]
fn core_types_unaffected() {
let meta = resolve(&ProviderType::Application, None);
assert_eq!(meta.tab_label, "Apps");
assert_eq!(meta.css_class, "owlry-filter-app");
}
}