Architecture - ArunPrakashG/native-launcher GitHub Wiki

Architecture

Technical overview of Native Launcher's design and implementation.

Table of Contents

Design Philosophy

Native Launcher follows three core principles:

1. Performance First

  • <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

2. Plugin-Driven

  • Core functionality extracted into plugins
  • Extensible without modifying main.rs
  • Keyboard shortcuts owned by plugins
  • Priority-based dispatch prevents conflicts

3. Modern Native

  • Wayland-first (gtk4-layer-shell)
  • Rust for safety and performance
  • GTK4 for modern UI
  • No Electron, no web technologies

System Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         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   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Components

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)

  • Plugin trait definition
  • KeyboardEvent struct
  • KeyboardAction enum
  • PluginResult builder

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)

Keyboard Event System

The keyboard event system is the most unique architectural feature.

Event Flow

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

Priority-Based Dispatch

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.

Why This Design?

❌ 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:

  1. main.rs stays simple - Just event routing
  2. Plugins own behavior - Web search logic in web search plugin
  3. No conflicts - Priority system prevents collisions
  4. Extensible - Add shortcuts without touching core
  5. Testable - Unit test plugins without GTK

Plugin System

Plugin Lifecycle

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

Plugin Trait

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

Search Result Aggregation

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

Plugin Result Scoring

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.

Performance Optimization

Native Launcher implements multiple layers of performance optimization:

Startup Performance (<100ms)

  1. Background icon loading: Icons preload in separate thread
  2. Lazy initialization: Desktop entries scanned once, cached to disk
  3. Minimal dependencies: Carefully chosen, profile-guided
  4. Release mode: -C opt-level=3, LTO enabled
  5. Desktop entry caching: Parsed .desktop files cached with mtime validation

Search Performance (<10ms)

  1. Smart debouncing (150ms): Counter-based debouncing prevents lag during rapid typing
  2. Two-pass search architecture: Applications searched first, expensive operations only when needed
  3. Smart triggering: File search skipped when β‰₯2 high-quality app matches exist
  4. Pre-sorted plugins: Sorted once at startup, not per-search
  5. Early filtering: should_handle() eliminates plugins fast
  6. Fuzzy matching: nucleo crate (fast Rust implementation)
  7. Result limiting: Stop at max_results, don't compute all
  8. String reuse: Minimal allocations in hot paths

Two-Pass Search Architecture

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

File Index Integration

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

Memory Efficiency (<30MB)

  1. No web engine: Pure native GTK4, no Chromium/WebKit
  2. Shared desktop entries: One Vec for all plugins
  3. Icon caching: Resolved icons cached in HashMap
  4. Lazy plugin init: Plugins only init when enabled
  5. File search caching: Prevents repeated expensive searches

Profiling Tools

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

Data Flow

Search Data Flow

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

Keyboard Data Flow

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)

Thread Safety

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)

Future Architecture Goals

Phase 1 (Current)

  • βœ… Plugin-driven keyboard events
  • βœ… Priority-based dispatch
  • βœ… Desktop file parsing
  • βœ… Fuzzy search

Phase 2 (In Progress)

  • πŸ”„ Icon theme support (partially implemented)
  • πŸ”„ Usage tracking (implemented, needs UI polish)
  • πŸ“‹ Configuration UI

Phase 3 (Planned)

  • πŸ“‹ Plugin hot-reload
  • πŸ“‹ IPC for external plugins
  • πŸ“‹ Scripting API (Lua/Rhai)

Phase 4 (Future)

  • πŸ“‹ X11 support (compatibility layer)
  • πŸ“‹ Plugin marketplace
  • πŸ“‹ Cloud sync for settings

Technical Decisions

Why Rust?

  • Safety: No segfaults, memory leaks, or data races
  • Performance: Zero-cost abstractions, fast as C
  • Ergonomics: Modern tooling (cargo, clippy, rustfmt)

Why GTK4?

  • Native: No web engine overhead
  • Fast: Hardware-accelerated rendering
  • Wayland-first: gtk4-layer-shell integration

Why Plugin System?

  • Extensibility: Add features without core changes
  • Maintainability: Clear boundaries, testable
  • Performance: Compile-time dispatch, no runtime overhead

Why Priority-Based Events?

  • Conflict resolution: Clear winner when multiple plugins could handle event
  • Flexibility: High-priority plugins can override defaults
  • Predictability: Deterministic dispatch order

Next Steps

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