Dynamic Plugins - ArunPrakashG/native-launcher GitHub Wiki
Native Launcher supports loading external plugins as compiled shared libraries (.so files) at runtime. This allows you to extend functionality without modifying the core launcher code.
cargo new --lib my-awesome-plugin
cd my-awesome-pluginEdit Cargo.toml:
[package]
name = "my-awesome-plugin"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_awesome_plugin"
crate-type = ["cdylib"] # ← Important: compile as dynamic librarySee the complete example template or the full guide.
# Build
cargo build --release
# Install
mkdir -p ~/.config/native-launcher/plugins
cp target/release/libmy_awesome_plugin.so ~/.config/native-launcher/plugins/
# Restart launcher
native-launcherAll dynamic plugins must implement these C-compatible functions:
| Function | Purpose |
|---|---|
plugin_get_abi_version() |
Return ABI version (must be 1) |
plugin_get_name() |
Plugin name displayed to users |
plugin_get_description() |
Short description of functionality |
plugin_get_priority() |
Search priority (higher = searched first) |
plugin_should_handle() |
Check if plugin handles a query |
plugin_search() |
Return search results |
plugin_handle_keyboard_event() |
Handle keyboard shortcuts (optional) |
plugin_free_results() |
Free memory allocated for results |
plugin_free_string() |
Free memory allocated for strings |
Here's a minimal working example:
use std::ffi::CString;
use std::os::raw::{c_char, c_int};
const PLUGIN_ABI_VERSION: u32 = 1;
#[repr(C)]
pub struct CStringSlice {
pub ptr: *const c_char,
pub len: usize,
}
impl CStringSlice {
fn from_string(s: &str) -> Self {
let cstr = CString::new(s).unwrap();
let len = cstr.as_bytes().len();
let ptr = cstr.into_raw();
Self { ptr, len }
}
}
#[no_mangle]
pub extern "C" fn plugin_get_abi_version() -> u32 {
PLUGIN_ABI_VERSION
}
#[no_mangle]
pub extern "C" fn plugin_get_name() -> CStringSlice {
CStringSlice::from_string("My Plugin")
}
#[no_mangle]
pub extern "C" fn plugin_get_description() -> CStringSlice {
CStringSlice::from_string("Does awesome things")
}
#[no_mangle]
pub extern "C" fn plugin_get_priority() -> c_int {
200 // Medium priority
}
#[no_mangle]
pub extern "C" fn plugin_should_handle(query: CStringSlice) -> bool {
unsafe {
if query.ptr.is_null() {
return false;
}
let slice = std::slice::from_raw_parts(query.ptr as *const u8, query.len);
let query_str = String::from_utf8_lossy(slice);
query_str.starts_with("myplugin:")
}
}
// ... implement remaining functions (see full example)Plugins are loaded from these directories (in order):
-
User plugins (highest priority):
~/.config/native-launcher/plugins/ -
System plugins:
/usr/local/share/native-launcher/plugins/ -
Distribution plugins:
/usr/share/native-launcher/plugins/
Set appropriate priority for your plugin:
- 1000: Applications (reserved)
- 500-700: Core functionality (calculator, web search)
- 200-400: Custom plugins (recommended)
- 100: Default priority
Higher priority plugins are:
- Searched first
- Get keyboard events first
- Appear higher in results
Handle custom keyboard shortcuts:
#[repr(C)]
pub struct CKeyboardEvent {
pub key_val: u32, // Unicode value of key
pub modifiers: u32, // Ctrl=0x04, Shift=0x01, Alt=0x08, Super=0x40
pub query: CStringSlice,
pub has_selection: bool,
}
#[repr(C)]
pub enum CKeyboardAction {
None, // Don't handle this event
Execute, // Execute command
OpenUrl, // Open URL in browser
Handled, // Handled but don't close window
}
#[no_mangle]
pub extern "C" fn plugin_handle_keyboard_event(
event: CKeyboardEvent
) -> CKeyboardActionData {
unsafe {
// Check for Ctrl+E
if event.modifiers & 0x04 != 0 && event.key_val == 'e' as u32 {
return CKeyboardActionData {
action: CKeyboardAction::OpenUrl,
data: CStringSlice::from_string("https://example.com"),
terminal: false,
};
}
// Don't handle
CKeyboardActionData {
action: CKeyboardAction::None,
data: CStringSlice::empty(),
terminal: false,
}
}
}Here are some ideas for useful plugins:
- GitHub Search - Search repositories, issues, PRs
- Stack Overflow - Search questions and answers
- Documentation - Search language/framework docs
- Wikipedia - Quick Wikipedia lookups
- Translation - Translate text (Google/DeepL)
- Currency Converter - Live exchange rates
- Weather - Weather forecasts by city
- Time Zones - World clock converter
- Bookmarks - Search browser bookmarks
- Clipboard History - Search clipboard items
- Notes - Quick note taking and search
- Tasks - Todo list integration
- Snippets - Code snippet manager
- Docker - Manage containers
- Git - Repository operations
- VMs - Launch virtual machines
- SSH - Connection manager
#[no_mangle]
pub extern "C" fn plugin_free_results(results: CResultArray) {
if !results.ptr.is_null() {
unsafe {
let results_vec = Vec::from_raw_parts(
results.ptr,
results.len,
results.capacity
);
for result in results_vec {
plugin_free_string(result.title);
plugin_free_string(result.subtitle);
plugin_free_string(result.icon);
plugin_free_string(result.command);
}
}
}
}
#[no_mangle]
pub extern "C" fn plugin_free_string(data: CStringSlice) {
if !data.ptr.is_null() {
unsafe {
let _ = CString::from_raw(data.ptr as *mut c_char);
}
}
}RUST_LOG=debug native-launcherLook for these messages:
INFO native_launcher::plugins::dynamic: Loading plugin from: ~/.config/native-launcher/plugins/myplugin.so
INFO native_launcher::plugins::dynamic: Loaded plugin 'My Plugin' (priority: 200)
Plugin not loading:
- Check file permissions:
chmod +x plugin.so - Verify ABI version is
1 - Check logs with
RUST_LOG=debug
Segmentation fault:
- Memory not freed correctly
- Null pointer dereference
- Invalid string encoding
Results not showing:
-
plugin_should_handle()returns false - Priority too low (other plugins winning)
- Empty results array returned
Help users discover your plugin:
pub extern "C" fn plugin_should_handle(query: CStringSlice) -> bool {
// Support multiple prefixes
query.starts_with("github:") || query.starts_with("@gh")
}Show usage when query is empty:
if search_term.is_empty() {
results.push(CPluginResult {
title: CStringSlice::from_string("GitHub Search"),
subtitle: CStringSlice::from_string("Type 'github: <query>' to search"),
icon: CStringSlice::from_string("github"),
command: CStringSlice::from_string(""),
terminal: false,
score: 1000,
});
}Return empty results on error:
pub extern "C" fn plugin_search(...) -> CResultArray {
match do_search(query) {
Ok(results) => CResultArray::from_vec(results),
Err(e) => {
eprintln!("Plugin error: {}", e);
CResultArray::from_vec(Vec::new())
}
}
}Provide recognizable icons:
CPluginResult {
// Prefer theme icon names
icon: CStringSlice::from_string("web-browser"),
// Or absolute paths
icon: CStringSlice::from_string("/usr/share/icons/hicolor/48x48/apps/myapp.png"),
...
}You can use any Rust crate in your plugin:
[dependencies]
serde = "1.0"
serde_json = "1.0"
reqwest = { version = "0.11", features = ["blocking"] }Example using reqwest:
use reqwest::blocking::get;
fn fetch_data(query: &str) -> Result<Vec<CPluginResult>, Box<dyn std::error::Error>> {
let url = format!("https://api.example.com/search?q={}", query);
let response = get(&url)?.json::<ApiResponse>()?;
let results = response.items.iter().map(|item| {
CPluginResult {
title: CStringSlice::from_string(&item.name),
subtitle: CStringSlice::from_string(&item.description),
icon: CStringSlice::from_string("web-browser"),
command: CStringSlice::from_string(&item.url),
terminal: false,
score: item.score,
}
}).collect();
Ok(results)
}- Only install plugins from trusted sources
- Review plugin code before installation
- Plugins can access all system resources
- Bad plugins can crash the launcher
# Build for release
cargo build --release --target x86_64-unknown-linux-gnu
# Create archive
tar -czf my-plugin-v1.0.0.tar.gz \
-C target/release \
libmy_plugin.so
# Create install script
cat > install.sh << 'EOF'
#!/bin/bash
mkdir -p ~/.config/native-launcher/plugins
cp libmy_plugin.so ~/.config/native-launcher/plugins/
echo "Plugin installed! Restart Native Launcher."
EOF
chmod +x install.shCreate a release with:
- Binary
.sofile - Install script
- README with usage instructions
- Version number and changelog
- Full API Documentation: DYNAMIC_PLUGINS.md
- Example Template: examples/plugin-template/
- Built-in Plugins: src/plugins/ (for reference)
- GitHub Discussions: Ask questions and share plugins
For a fully working example with all required functions, see:
examples/plugin-template/src/lib.rs
Build and test it:
cd examples/plugin-template
cargo build --release
mkdir -p ~/.config/native-launcher/plugins
cp target/release/libexample_plugin.so ~/.config/native-launcher/plugins/
native-launcher
# Type "example:" to testReady to build your first plugin? Use the template in examples/plugin-template/ as a starting point! 🚀