Architecture - ArunPrakashG/native-launcher GitHub Wiki
Technical overview of Native Launcher's design and implementation.
Native Launcher follows three core principles:
- <100ms cold start - Optimized startup sequence
- <10ms search - Cached data structures, efficient algorithms
- <30MB memory - Minimal resource footprint
- No blocking I/O on main thread
- Core functionality extracted into plugins
- Extensible without modifying
main.rs - Keyboard shortcuts owned by plugins
- Priority-based dispatch prevents conflicts
- Wayland-first (gtk4-layer-shell)
- Rust for safety and performance
- GTK4 for modern UI
- No Electron, no web technologies
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β main.rs β
β - GTK4 Application initialization β
β - Event loop and keyboard dispatch β
β - Window management (gtk4-layer-shell) β
βββββββββββββββββ¬ββββββββββββββββββββββββββ¬ββββββββββββββββββββ
β β
βββββββββββΌββββββββββ ββββββββββΌβββββββββββ
β PluginManager β β UI Widgets β
β - Plugin registry β β - SearchWidget β
β - Search dispatch β β - ResultsList β
β - Event dispatch β β - SearchFooter β
βββββββββββ¬βββββββββββ βββββββββββββββββββββ
β
ββββββββββββΌβββββββββββββββββββββββββββββββββββ
β Plugin Trait β
β - should_handle() β
β - search() β
β - handle_keyboard_event() β
ββββ¬βββββββββ¬ββββββββββ¬βββββββββββ¬βββββββββββββ
β β β β
ββββββΌββββ βββΌβββββββ ββΌβββββββββ ββΌβββββββββββ
β Apps β β WebSrchβ βCalculatorβ β SSH β
βPlugin β β Plugin β β Plugin β β Plugin β
ββββββββββ ββββββββββ ββββββββββββ βββββββββββββ
src/main.rs (328 lines)
- GTK application lifecycle
- Keyboard event capture
- Plugin manager coordination
- Does not contain: Business logic, search algorithms, keyboard handlers
src/plugins/manager.rs (446 lines)
- Plugin registration and sorting
- Search result aggregation
- Keyboard event dispatch
- Icon/command resolution
src/plugins/traits.rs (218 lines)
-
Plugintrait definition -
KeyboardEventstruct -
KeyboardActionenum -
PluginResultbuilder
src/ui/
- GTK4 widgets (search input, results list, footer)
- CSS styling
- Theme loading
src/desktop/
- Desktop file parsing (freedesktop spec)
- Desktop entry caching
- Action parsing
src/utils/
- Browser detection (
browser.rs) - Command execution (
exec.rs) - Icon resolution (
icons.rs)
The keyboard event system is the most unique architectural feature.
User presses key
β
GTK EventControllerKey captures event
β
Create KeyboardEvent {
key: Key,
modifiers: ModifierType,
query: String,
has_selection: bool
}
β
PluginManager.dispatch_keyboard_event(&event)
β
Iterate plugins by priority (highest β lowest)
β
plugin.handle_keyboard_event(&event)
β
Returns KeyboardAction:
- None: Try next plugin
- Execute { command, terminal }: Run command
- OpenUrl(url): Open browser
- Handled: Event consumed, no action
β
main.rs executes the action
Plugins are sorted by priority once at startup:
plugins.sort_by(|a, b| b.priority().cmp(&a.priority()));Typical priorities:
- Applications: 1000 (highest)
- Web Search: 600
- Calculator: 500
- Files: 400
- Shell: 300
Higher priority = sees events first = can override lower-priority plugins.
β Traditional approach (hardcoded in main.rs):
if modifiers.contains(CONTROL_MASK) && key == Enter {
if query.starts_with("google") {
// 40+ lines of URL building...
} else if query.starts_with("ddg") {
// 40+ more lines...
}
// ...
}β Plugin-driven approach:
// main.rs: Just dispatch
let action = plugin_manager.dispatch_keyboard_event(&event);
// WebSearchPlugin: Owns the logic
fn handle_keyboard_event(&self, event: &KeyboardEvent) -> KeyboardAction {
if event.key == Key::Return && event.has_ctrl() {
if let Some((_, _, url)) = self.build_search_url(&event.query) {
return KeyboardAction::OpenUrl(url);
}
}
KeyboardAction::None
}Benefits:
- main.rs stays simple - Just event routing
- Plugins own behavior - Web search logic in web search plugin
- No conflicts - Priority system prevents collisions
- Extensible - Add shortcuts without touching core
- Testable - Unit test plugins without GTK
1. PluginManager::new()
β
2. Create plugin instances
plugins.push(Box::new(WebSearchPlugin::new()))
β
3. Sort by priority
plugins.sort_by(|a, b| b.priority().cmp(&a.priority()))
β
4. For each search query:
- Filter: plugin.should_handle(&query)
- Search: plugin.search(&query, &context)
- Aggregate results
β
5. For each keyboard event:
- Dispatch: plugin.handle_keyboard_event(&event)
- First non-None action wins
pub trait Plugin: Debug + Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn command_prefixes(&self) -> Vec<&str> { Vec::new() }
fn should_handle(&self, query: &str) -> bool;
fn search(&self, query: &str, context: &PluginContext) -> Result<Vec<PluginResult>>;
fn priority(&self) -> i32 { 100 }
fn enabled(&self) -> bool { true }
fn handle_keyboard_event(&self, _event: &KeyboardEvent) -> KeyboardAction {
KeyboardAction::None
}
}1. User types query
β
2. PluginManager filters plugins:
- Command query (@ws, @shell)? β Only matching plugin
- Global query? β All plugins where should_handle() == true
β
3. Each plugin searches:
- plugin.search(query, context) β Vec<PluginResult>
β
4. Results aggregated:
- Merge all Vec<PluginResult>
- Sort by score (descending)
- Deduplicate by title+command
- Take top N (max_results)
β
5. UI updated with final results
Scores guide result ordering:
-
9000+: Explicit matches (user typed
google rust) - 1000: Application name match
- 500: High-priority plugin match (calculator)
- 100: Low-priority / fallback (generic web search)
Higher scores appear first in results list.
Native Launcher implements multiple layers of performance optimization:
- Background icon loading: Icons preload in separate thread
- Lazy initialization: Desktop entries scanned once, cached to disk
- Minimal dependencies: Carefully chosen, profile-guided
-
Release mode:
-C opt-level=3, LTO enabled - Desktop entry caching: Parsed .desktop files cached with mtime validation
- Smart debouncing (150ms): Counter-based debouncing prevents lag during rapid typing
- Two-pass search architecture: Applications searched first, expensive operations only when needed
- Smart triggering: File search skipped when β₯2 high-quality app matches exist
- Pre-sorted plugins: Sorted once at startup, not per-search
-
Early filtering:
should_handle()eliminates plugins fast - Fuzzy matching: nucleo crate (fast Rust implementation)
- Result limiting: Stop at max_results, don't compute all
- String reuse: Minimal allocations in hot paths
// First pass: Query Applications plugin only
let app_results = applications_plugin.search(&context);
// Count high-quality matches (score >= 700)
let good_matches = app_results.iter()
.filter(|r| r.score >= 700)
.count();
// Second pass: Other plugins with app count context
let context = context.with_app_results(good_matches);
let file_results = files_plugin.search(&context);Impact: 60% of queries skip expensive file search:
- "firefox" β instant (file search skipped)
- "config" β full search (intended)
- "@files test" β always searches (explicit command)
Native Linux file indexing with fallback chain:
Priority: plocate β mlocate β locate β fd β find
Speed: 20-50ms 30-80ms 30-80ms 100-300ms 500ms+
Caching Strategy:
- Results cached for 2 minutes (TTL)
- Cache hit: <1ms response
- Cache miss: 20-300ms depending on backend
- No web engine: Pure native GTK4, no Chromium/WebKit
- Shared desktop entries: One Vec for all plugins
- Icon caching: Resolved icons cached in HashMap
- Lazy plugin init: Plugins only init when enabled
- File search caching: Prevents repeated expensive searches
# Startup time
time ./target/release/native-launcher
# Search benchmarks
cargo bench
# Memory profiling
/usr/bin/time -v ./target/release/native-launcher
# CPU profiling
cargo flamegraph --bin native-launcherUser types β SearchWidget emits signal
β
main.rs: entry.connect_changed()
β
plugin_manager.borrow().search(query, max_results)
β
PluginManager aggregates results from plugins
β
ResultsList.update_plugin_results(results)
β
GTK ListBox re-renders with new items
β
User sees results (target: <10ms total)
User presses key β EventControllerKey captures
β
main.rs: key_controller.connect_key_pressed()
β
Create KeyboardEvent with context
β
plugin_manager.borrow().dispatch_keyboard_event(&event)
β
PluginManager iterates plugins by priority
β
First plugin returns non-None action
β
main.rs matches on action:
- Execute { command, terminal } β utils::execute_command()
- OpenUrl(url) β xdg-open
- Handled β Do nothing
β
Window closes (if Execute or OpenUrl)
Native Launcher is single-threaded (GTK main thread) with one exception:
- Icon preloading: Background thread preloads icon cache on startup
- Main thread: All plugins, UI, event handling
Synchronization:
-
Rc<RefCell<T>>for shared mutable state (within GTK thread) - No
Arc<Mutex<T>>needed (single-threaded)
- β Plugin-driven keyboard events
- β Priority-based dispatch
- β Desktop file parsing
- β Fuzzy search
- π Icon theme support (partially implemented)
- π Usage tracking (implemented, needs UI polish)
- π Configuration UI
- π Plugin hot-reload
- π IPC for external plugins
- π Scripting API (Lua/Rhai)
- π X11 support (compatibility layer)
- π Plugin marketplace
- π Cloud sync for settings
- Safety: No segfaults, memory leaks, or data races
- Performance: Zero-cost abstractions, fast as C
- Ergonomics: Modern tooling (cargo, clippy, rustfmt)
- Native: No web engine overhead
- Fast: Hardware-accelerated rendering
- Wayland-first: gtk4-layer-shell integration
- Extensibility: Add features without core changes
- Maintainability: Clear boundaries, testable
- Performance: Compile-time dispatch, no runtime overhead
- Conflict resolution: Clear winner when multiple plugins could handle event
- Flexibility: High-priority plugins can override defaults
- Predictability: Deterministic dispatch order
- Plugin Development - Build your own plugins
- API Reference - Complete trait documentation
- Contributing - Join development
- Performance - Benchmarking and profiling