Compare commits
5 Commits
owlry-v1.0
...
owlry-v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 61411cd094 | |||
| 4e310223cf | |||
| 6446a253e8 | |||
| 72dcd74e65 | |||
| 774b2a4700 |
@@ -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
8
Cargo.lock
generated
@@ -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",
|
||||
|
||||
26
README.md
26
README.md
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +355,8 @@ mod tests {
|
||||
prefix: Some(":app".into()),
|
||||
icon: "application-x-executable".into(),
|
||||
position: "normal".into(),
|
||||
tab_label: None,
|
||||
search_noun: None,
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", &[]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user