Compare commits

...

5 Commits

Author SHA1 Message Date
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
26 changed files with 366 additions and 98 deletions

View File

@@ -276,7 +276,7 @@ Communication uses newline-delimited JSON over a Unix domain socket at `$XDG_RUN
| Type | Purpose |
|------|---------|
| `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 |
| `Ack` | Success acknowledgement |
| `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).
**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
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
- **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
- **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+)

8
Cargo.lock generated
View File

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

View File

@@ -459,6 +459,32 @@ owlry plugin create my-plugin -r rune # Rune
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
See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for:

View File

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

View File

@@ -90,4 +90,8 @@ pub struct ProviderDesc {
pub prefix: Option<String>,
pub icon: 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,
#[serde(default)]
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 {

View File

@@ -41,6 +41,8 @@ pub struct ScriptProviderInfo {
pub default_icon: RString,
pub is_static: bool,
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
@@ -273,6 +275,14 @@ impl Provider for RuntimeProvider {
fn items(&self) -> &[LaunchItem] {
&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:

View File

@@ -42,6 +42,8 @@ pub struct ProviderDescriptor {
pub prefix: Option<String>,
pub icon: 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.
@@ -148,6 +150,14 @@ pub trait Provider: Send + Sync {
fn provider_type(&self) -> ProviderType;
fn refresh(&mut self);
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.
@@ -1022,6 +1032,8 @@ impl ProviderManager {
prefix,
icon,
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),
icon: provider.icon().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),
icon: provider.icon().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),
icon: provider.icon().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,
icon: desc.icon,
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()),
icon: "application-x-executable".into(),
position: "normal".into(),
tab_label: None,
search_noun: None,
}],
};
let json = serde_json::to_string(&resp).unwrap();

View File

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

View File

@@ -34,6 +34,8 @@ fn register_provider(lua: &Lua, config: Table) -> LuaResult<()> {
.get::<Option<String>>("default_icon")?
.unwrap_or_else(|| "application-x-addon".to_string());
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)
let has_query: bool = config.contains_key("query")?;
@@ -70,6 +72,8 @@ fn register_provider(lua: &Lua, config: Table) -> LuaResult<()> {
default_icon,
prefix,
is_dynamic,
tab_label,
search_noun,
});
});

View File

@@ -113,6 +113,10 @@ pub struct LuaProviderInfo {
pub is_static: bool,
/// Optional prefix trigger
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
@@ -184,6 +188,8 @@ impl LuaRuntimeState {
default_icon: RString::from(reg.default_icon.as_str()),
is_static: !reg.is_dynamic,
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 prefix: Option<String>,
pub is_dynamic: bool,
pub tab_label: Option<String>,
pub search_noun: Option<String>,
}
/// A loaded plugin instance
@@ -113,6 +115,8 @@ impl LoadedPlugin {
.unwrap_or_else(|| "application-x-addon".to_string()),
prefix: decl.prefix.clone(),
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,
#[serde(default)]
pub type_id: Option<String>,
#[serde(default)]
pub tab_label: Option<String>,
#[serde(default)]
pub search_noun: Option<String>,
}
fn default_provider_type() -> String {

View File

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

View File

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

View File

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

View File

@@ -70,6 +70,8 @@ impl LoadedPlugin {
default_icon: decl.icon.clone().unwrap_or_else(|| "application-x-addon".to_string()),
is_static: decl.provider_type != "dynamic",
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,
#[serde(default)]
pub type_id: Option<String>,
#[serde(default)]
pub tab_label: Option<String>,
#[serde(default)]
pub search_noun: Option<String>,
}
fn default_provider_type() -> String {

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry"
version = "1.0.9"
version = "1.0.10"
edition = "2024"
rust-version = "1.90"
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::data::FrecencyStore;
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 std::sync::{Arc, Mutex};
@@ -349,13 +349,12 @@ impl SearchBackend {
}
}
/// Get available provider type IDs from the daemon, or from local manager.
#[allow(dead_code)]
pub fn available_provider_ids(&mut self) -> Vec<String> {
/// Get available provider descriptors from the daemon, or from local manager.
pub fn available_providers(&mut self) -> Vec<ProviderDesc> {
match self {
SearchBackend::Daemon(handle) => match handle.client.lock() {
Ok(mut client) => match client.providers() {
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
Ok(descs) => descs,
Err(e) => {
warn!("IPC providers query failed: {}", e);
Vec::new()
@@ -369,7 +368,15 @@ impl SearchBackend {
SearchBackend::Local { providers, .. } => providers
.available_providers()
.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(),
}
}

View File

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

View File

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

View File

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

View File

@@ -1,101 +1,191 @@
use owlry_core::ipc::ProviderDesc;
use owlry_core::providers::ProviderType;
/// Display metadata for a provider.
pub struct ProviderMeta {
pub tab_label: &'static str,
pub css_class: &'static str,
pub search_noun: &'static str,
pub tab_label: String,
pub css_class: String,
pub search_noun: String,
}
/// Return display metadata for a provider type.
pub fn meta_for(provider: &ProviderType) -> ProviderMeta {
/// Resolve display metadata, checking IPC-provided metadata first, then hardcoded fallback.
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 {
ProviderType::Application => ProviderMeta {
tab_label: "Apps",
css_class: "owlry-filter-app",
search_noun: "applications",
tab_label: "Apps".to_string(),
css_class: "owlry-filter-app".to_string(),
search_noun: "applications".to_string(),
},
ProviderType::Command => ProviderMeta {
tab_label: "Cmds",
css_class: "owlry-filter-cmd",
search_noun: "commands",
tab_label: "Cmds".to_string(),
css_class: "owlry-filter-cmd".to_string(),
search_noun: "commands".to_string(),
},
ProviderType::Dmenu => ProviderMeta {
tab_label: "Dmenu",
css_class: "owlry-filter-dmenu",
search_noun: "options",
tab_label: "Dmenu".to_string(),
css_class: "owlry-filter-dmenu".to_string(),
search_noun: "options".to_string(),
},
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => ProviderMeta {
tab_label: "Bookmarks",
css_class: "owlry-filter-bookmark",
search_noun: "bookmarks",
tab_label: "Bookmarks".to_string(),
css_class: "owlry-filter-bookmark".to_string(),
search_noun: "bookmarks".to_string(),
},
"calc" => ProviderMeta {
tab_label: "Calc",
css_class: "owlry-filter-calc",
search_noun: "calculator",
tab_label: "Calc".to_string(),
css_class: "owlry-filter-calc".to_string(),
search_noun: "calculator".to_string(),
},
"clipboard" => ProviderMeta {
tab_label: "Clip",
css_class: "owlry-filter-clip",
search_noun: "clipboard",
tab_label: "Clip".to_string(),
css_class: "owlry-filter-clip".to_string(),
search_noun: "clipboard".to_string(),
},
"emoji" => ProviderMeta {
tab_label: "Emoji",
css_class: "owlry-filter-emoji",
search_noun: "emoji",
tab_label: "Emoji".to_string(),
css_class: "owlry-filter-emoji".to_string(),
search_noun: "emoji".to_string(),
},
"filesearch" => ProviderMeta {
tab_label: "Files",
css_class: "owlry-filter-file",
search_noun: "files",
tab_label: "Files".to_string(),
css_class: "owlry-filter-file".to_string(),
search_noun: "files".to_string(),
},
"media" => ProviderMeta {
tab_label: "Media",
css_class: "owlry-filter-media",
search_noun: "media",
tab_label: "Media".to_string(),
css_class: "owlry-filter-media".to_string(),
search_noun: "media".to_string(),
},
"pomodoro" => ProviderMeta {
tab_label: "Pomo",
css_class: "owlry-filter-pomodoro",
search_noun: "pomodoro",
tab_label: "Pomo".to_string(),
css_class: "owlry-filter-pomodoro".to_string(),
search_noun: "pomodoro".to_string(),
},
"scripts" => ProviderMeta {
tab_label: "Scripts",
css_class: "owlry-filter-script",
search_noun: "scripts",
tab_label: "Scripts".to_string(),
css_class: "owlry-filter-script".to_string(),
search_noun: "scripts".to_string(),
},
"ssh" => ProviderMeta {
tab_label: "SSH",
css_class: "owlry-filter-ssh",
search_noun: "SSH hosts",
tab_label: "SSH".to_string(),
css_class: "owlry-filter-ssh".to_string(),
search_noun: "SSH hosts".to_string(),
},
"system" => ProviderMeta {
tab_label: "System",
css_class: "owlry-filter-sys",
search_noun: "system",
tab_label: "System".to_string(),
css_class: "owlry-filter-sys".to_string(),
search_noun: "system".to_string(),
},
"uuctl" => ProviderMeta {
tab_label: "uuctl",
css_class: "owlry-filter-uuctl",
search_noun: "uuctl units",
tab_label: "uuctl".to_string(),
css_class: "owlry-filter-uuctl".to_string(),
search_noun: "uuctl units".to_string(),
},
"weather" => ProviderMeta {
tab_label: "Weather",
css_class: "owlry-filter-weather",
search_noun: "weather",
tab_label: "Weather".to_string(),
css_class: "owlry-filter-weather".to_string(),
search_noun: "weather".to_string(),
},
"websearch" => ProviderMeta {
tab_label: "Web",
css_class: "owlry-filter-web",
search_noun: "web",
},
_ => ProviderMeta {
tab_label: "Plugin",
css_class: "owlry-filter-plugin",
search_noun: "plugins",
tab_label: "Web".to_string(),
css_class: "owlry-filter-web".to_string(),
search_noun: "web".to_string(),
},
// 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");
}
}