Dynamic Plugins - ArunPrakashG/native-launcher GitHub Wiki

Dynamic Plugin Development

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.

Quick Start

1. Create Plugin Project

cargo new --lib my-awesome-plugin
cd my-awesome-plugin

2. Configure as Dynamic Library

Edit 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 library

3. Implement Plugin API

See the complete example template or the full guide.

4. Build and Install

# 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-launcher

Plugin API Overview

All 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

Example Plugin

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)

Plugin Discovery Paths

Plugins are loaded from these directories (in order):

  1. User plugins (highest priority): ~/.config/native-launcher/plugins/
  2. System plugins: /usr/local/share/native-launcher/plugins/
  3. Distribution plugins: /usr/share/native-launcher/plugins/

Priority Guidelines

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

Keyboard Event Handling

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,
        }
    }
}

Plugin Ideas

Here are some ideas for useful plugins:

Data Sources

  • GitHub Search - Search repositories, issues, PRs
  • Stack Overflow - Search questions and answers
  • Documentation - Search language/framework docs
  • Wikipedia - Quick Wikipedia lookups

Utilities

  • Translation - Translate text (Google/DeepL)
  • Currency Converter - Live exchange rates
  • Weather - Weather forecasts by city
  • Time Zones - World clock converter

Productivity

  • Bookmarks - Search browser bookmarks
  • Clipboard History - Search clipboard items
  • Notes - Quick note taking and search
  • Tasks - Todo list integration
  • Snippets - Code snippet manager

System

  • Docker - Manage containers
  • Git - Repository operations
  • VMs - Launch virtual machines
  • SSH - Connection manager

Memory Management

⚠️ Important: Always free allocated memory!

#[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);
        }
    }
}

Debugging

Enable Debug Logging

RUST_LOG=debug native-launcher

Look 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)

Common Issues

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

Best Practices

1. Use Clear Prefixes

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")
}

2. Provide Help Text

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,
    });
}

3. Handle Errors Gracefully

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())
        }
    }
}

4. Use Good Icons

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"),
    ...
}

Adding Dependencies

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)
}

Security Considerations

⚠️ Plugins run with full launcher permissions!

  • Only install plugins from trusted sources
  • Review plugin code before installation
  • Plugins can access all system resources
  • Bad plugins can crash the launcher

Distribution

Packaging Your Plugin

# 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.sh

Sharing on GitHub

Create a release with:

  • Binary .so file
  • Install script
  • README with usage instructions
  • Version number and changelog

Resources

Complete Example

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 test

Ready to build your first plugin? Use the template in examples/plugin-template/ as a starting point! 🚀

⚠️ **GitHub.com Fallback** ⚠️