Plugin Development - ArunPrakashG/native-launcher GitHub Wiki
Native Launcher has a powerful plugin system that allows you to extend functionality without modifying core code.
- Overview
- Creating a Basic Plugin
- Keyboard Event Handling
- Plugin API Reference
- Best Practices
- Examples
Plugins in Native Launcher:
- Implement the
Plugintrait - Can handle search queries with custom logic
- Can respond to keyboard events
- Are prioritized and dispatched in order
- Run in the same process (no IPC overhead)
- Plugin is registered in
PluginManager - Plugins are sorted by priority (highest first)
- For each search query:
-
should_handle()determines if plugin processes the query -
search()returns results
-
- For each keyboard event:
-
handle_keyboard_event()can intercept and handle the event - First plugin to return non-
Noneaction wins
-
use crate::plugins::traits::{Plugin, PluginContext, PluginResult};
use anyhow::Result;
#[derive(Debug)]
pub struct MyPlugin {
enabled: bool,
}
impl MyPlugin {
pub fn new() -> Self {
Self { enabled: true }
}
}impl Plugin for MyPlugin {
fn name(&self) -> &str {
"my_plugin"
}
fn description(&self) -> &str {
"My awesome plugin"
}
fn should_handle(&self, query: &str) -> bool {
// Return true if your plugin can handle this query
// Examples:
// - Check for specific prefix: query.starts_with("@myprefix")
// - Check query length: query.len() >= 3
// - Pattern matching: query.contains("keyword")
query.starts_with("@my")
}
fn search(&self, query: &str, context: &PluginContext) -> Result<Vec<PluginResult>> {
// Return search results
let result = PluginResult::new(
"My Result".to_string(),
"echo 'Hello from plugin'".to_string(),
self.name().to_string(),
)
.with_subtitle("This is a subtitle".to_string())
.with_icon("application-x-executable".to_string())
.with_score(500); // Higher score = appears first
Ok(vec![result])
}
fn priority(&self) -> i32 {
500 // Higher = searched first
// Typical priorities:
// - Applications: 1000
// - Calculator: 500
// - Web Search: 600
// - Files: 400
// - Shell: 300
}
fn enabled(&self) -> bool {
self.enabled
}
}Add your plugin to src/plugins/manager.rs:
use super::MyPlugin;
impl PluginManager {
pub fn new(...) -> Self {
let mut plugins: Vec<Box<dyn Plugin>> = Vec::new();
// Add your plugin
plugins.push(Box::new(MyPlugin::new()));
// ...
}
}One of the most powerful features is the ability to handle keyboard shortcuts directly in your plugin.
use crate::plugins::traits::{KeyboardEvent, KeyboardAction};
use gtk4::gdk::Key;
impl Plugin for MyPlugin {
fn handle_keyboard_event(&self, event: &KeyboardEvent) -> KeyboardAction {
// Handle Ctrl+= for calculator-like behavior
if event.key == Key::equal && event.has_ctrl() {
let result = calculate(&event.query);
return KeyboardAction::Execute {
command: format!("echo '{}'", result),
terminal: false,
};
}
// Handle Alt+Enter for alternative action
if event.key == Key::Return && event.has_alt() {
return KeyboardAction::OpenUrl(
format!("https://example.com/search?q={}", event.query)
);
}
KeyboardAction::None // Let other plugins handle it
}
}pub struct KeyboardEvent {
pub key: Key, // The key pressed (from GTK)
pub modifiers: ModifierType, // Ctrl, Shift, Alt, Super
pub query: String, // Current search query
pub has_selection: bool, // Whether a result is selected
}event.has_ctrl() // Check if Ctrl is pressed
event.has_shift() // Check if Shift is pressed
event.has_alt() // Check if Alt is pressed
event.has_super() // Check if Super/Meta is pressedpub enum KeyboardAction {
None, // Don't handle, pass to next plugin
Execute { command, terminal }, // Execute command and close window
OpenUrl(String), // Open URL in browser and close window
Handled, // Event handled, but don't close window
}- Keyboard events are dispatched to plugins in priority order (highest first)
- The first plugin to return a non-
Noneaction wins - This prevents conflicts when multiple plugins could handle the same key
Example:
Event: Ctrl+Enter
↓
WebSearchPlugin (priority 600) → Returns OpenUrl("https://google.com/...")
↓
Event handled! Other plugins don't see it.
PluginResult::new(title, command, plugin_name)
.with_subtitle(String) // Optional description
.with_icon(String) // Icon name or path
.with_score(i64) // Search relevance score
.with_terminal(bool) // Run in terminal?
.with_sub_results(Vec<PluginResult>) // Nested results
.with_parent_app(String) // Parent app for icon resolutionpub struct PluginContext {
pub max_results: usize, // Maximum results requested
pub include_low_scores: bool, // Whether to include low-score results
}Plugins can register command prefixes for targeted searches:
fn command_prefixes(&self) -> Vec<&str> {
vec!["@my", "@myplugin"]
}When a query starts with a registered prefix, only that plugin is queried.
// ❌ Bad: Expensive operation on every query
fn search(&self, query: &str, context: &PluginContext) -> Result<Vec<PluginResult>> {
let all_items = scan_entire_filesystem(); // Slow!
filter_results(all_items, query)
}
// ✅ Good: Cache expensive operations
pub struct MyPlugin {
cached_items: Vec<Item>, // Pre-computed
}
fn search(&self, query: &str, context: &PluginContext) -> Result<Vec<PluginResult>> {
Ok(filter_results(&self.cached_items, query))
}// ❌ Bad: Panics on error
fn search(&self, query: &str, context: &PluginContext) -> Result<Vec<PluginResult>> {
let file = std::fs::read_to_string("config.json").unwrap(); // Panics!
// ...
}
// ✅ Good: Returns error
fn search(&self, query: &str, context: &PluginContext) -> Result<Vec<PluginResult>> {
let file = std::fs::read_to_string("config.json")?; // Returns error
// ...
}// Typical score ranges:
// 9000+ : Exact matches (explicit web search: "google query")
// 1000 : Applications (when app name matches)
// 500 : High-priority plugins (calculator with exact match)
// 100 : Low-priority / fallback results (generic web search)
fn search(&self, query: &str, context: &PluginContext) -> Result<Vec<PluginResult>> {
let score = if query.starts_with("@exact") {
9000 // User explicitly requested this
} else if query.contains("keyword") {
500 // Good match
} else {
100 // Fallback / generic
};
Ok(vec![result.with_score(score)])
}// ❌ Bad: Greedy keyboard handling
fn handle_keyboard_event(&self, event: &KeyboardEvent) -> KeyboardAction {
if event.key == Key::Return {
return KeyboardAction::Execute { ... }; // Blocks all Enter keys!
}
KeyboardAction::None
}
// ✅ Good: Specific conditions
fn handle_keyboard_event(&self, event: &KeyboardEvent) -> KeyboardAction {
// Only handle if specific conditions met
if event.key == Key::Return && event.has_ctrl() && self.should_handle(&event.query) {
return KeyboardAction::Execute { ... };
}
KeyboardAction::None
}#[derive(Debug)]
pub struct CalculatorPlugin {
enabled: bool,
}
impl Plugin for CalculatorPlugin {
fn name(&self) -> &str {
"calculator"
}
fn should_handle(&self, query: &str) -> bool {
// Handle queries that look like math expressions
query.chars().any(|c| "+-*/^()".contains(c))
|| query.parse::<f64>().is_ok()
}
fn search(&self, query: &str, _context: &PluginContext) -> Result<Vec<PluginResult>> {
let result = match evaluate_expression(query) {
Ok(value) => PluginResult::new(
format!("= {}", value),
format!("echo {} | wl-copy", value), // Copy to clipboard
self.name().to_string(),
)
.with_subtitle(format!("Press Enter to copy"))
.with_icon("accessories-calculator")
.with_score(500),
Err(_) => return Ok(vec![]),
};
Ok(vec![result])
}
fn handle_keyboard_event(&self, event: &KeyboardEvent) -> KeyboardAction {
// Handle = key to auto-evaluate
if event.key == Key::equal && !event.has_ctrl() {
if let Ok(result) = evaluate_expression(&event.query) {
return KeyboardAction::Execute {
command: format!("echo {} | wl-copy", result),
terminal: false,
};
}
}
KeyboardAction::None
}
fn priority(&self) -> i32 {
500
}
}#[derive(Debug)]
pub struct SshPlugin {
enabled: bool,
known_hosts: Vec<String>,
}
impl Plugin for SshPlugin {
fn name(&self) -> &str {
"ssh"
}
fn command_prefixes(&self) -> Vec<&str> {
vec!["@ssh", "@connect"]
}
fn should_handle(&self, query: &str) -> bool {
query.starts_with("@ssh") || query.starts_with("@connect")
}
fn search(&self, query: &str, _context: &PluginContext) -> Result<Vec<PluginResult>> {
let query = query.trim_start_matches("@ssh")
.trim_start_matches("@connect")
.trim();
let matches = self.known_hosts
.iter()
.filter(|host| host.contains(query))
.map(|host| {
PluginResult::new(
format!("SSH to {}", host),
format!("ssh {}", host),
self.name().to_string(),
)
.with_subtitle(host.clone())
.with_icon("network-server")
.with_terminal(true)
.with_score(700)
})
.collect();
Ok(matches)
}
fn handle_keyboard_event(&self, event: &KeyboardEvent) -> KeyboardAction {
// Alt+Enter: Open in new terminal window
if event.key == Key::Return && event.has_alt() {
if let Some(host) = extract_host(&event.query) {
return KeyboardAction::Execute {
command: format!("alacritty -e ssh {}", host),
terminal: false, // Already wrapped in terminal
};
}
}
KeyboardAction::None
}
fn priority(&self) -> i32 {
700
}
}See the complete implementation in src/plugins/web_search.rs.
Key features:
- Handles Ctrl+Enter for immediate web search
- Supports multiple search engines (google, ddg, wiki, github, youtube)
- Fallback to Google for queries without explicit engine
- URL encoding for search terms
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_handle() {
let plugin = MyPlugin::new();
assert!(plugin.should_handle("@my test"));
assert!(!plugin.should_handle("firefox"));
}
#[test]
fn test_search_results() {
let plugin = MyPlugin::new();
let ctx = PluginContext::new(10);
let results = plugin.search("@my query", &ctx).unwrap();
assert!(!results.is_empty());
assert!(results[0].title.contains("query"));
}
#[test]
fn test_keyboard_event() {
use gtk4::gdk::{Key, ModifierType};
let plugin = MyPlugin::new();
let event = KeyboardEvent::new(
Key::Return,
ModifierType::CONTROL_MASK,
"@my test".to_string(),
false,
);
match plugin.handle_keyboard_event(&event) {
KeyboardAction::Execute { .. } => (), // Expected
_ => panic!("Expected Execute action"),
}
}
}- See Architecture for understanding plugin lifecycle
- Check API Reference for complete trait documentation
- Browse
src/plugins/for more examples - Join Discussions for help
Want to contribute your plugin?
- Follow the Contributing Guide
- Ensure your plugin has tests
- Document keyboard shortcuts in the PR
- Add configuration options if needed
Popular plugin ideas: Clipboard manager, window switcher, system commands, bookmarks, password manager integration, emoji picker, color picker, unit converter.