**Swift Launcher update

So i started building a Linux launcher in Rust and Slint.

it turned into is way more interesting. Here's everything i built, from the big stuff to the small details.


1. The WebAssembly Plugin System

This is probably the biggest change. instead of hardcoding everything in Rust, i turned Swift into a plugin-based system using WebAssembly Components.

Why WASM?

i wanted plugins that could:

  • Run safely - sandboxed, can't crash the main app
  • Be written in any language - as long as it compiles to WASM
  • Load dynamically without recompiling the launcher

The Component Model is this new-ish standard that lets you define interfaces using WiT (WebAssembly interface Types). You define your interface, run a code generator, and get type-safe bindings for both host and guest.

How it Works

i defined a simple interface in plugin.wit:

package swift:launcher;

interface runner {
  record action-item {
    name: string,
    exec: string,
    keywords: string,
  }

  get-trigger: func() -> string;
  handle: func(input: string) -> list<action-item>;
}

world plugin-world {
  export runner;
}

Each plugin has a trigger character. When you type that character and the plugin takes over.

The Plugin Manager

The host code uses wasmtime with component model support. it:

  • Compiles plugins to .cwasm files and caches them
  • Loads plugins in parallel using rayon - tho i might change this, rayon isnt working very well
  • Gives them sandboxed filesystem access via WASI
  • Lets them make HTTP requests if needed

Plugins are stored in a HashMap by their trigger character, so routing is instant.


2. The Actual Plugins

i built three plugins to start with:

Directory Scanner (trigger: /)

Type /u and it fuzzy-finds /usr. it uses the Skim fuzzy matcher to score matches, so /usr/lo will match /usr/local even though it's not a prefix match.

The plugin:

  • Parses the input to separate the directory path from the search pattern
  • Reads the directory entries
  • Fuzzy matches filenames against your pattern
  • Returns scored results, sorted by score then alphabetically
  • Uses xdg-open to open files when you hit Enter

Search Plugin (trigger: @)

Web search without opening a browser first. Type @g rust wasm and it searches Google. Type @d linux for DuckDuckGo.

it reads engine definitions from ~/.config/swift/search.conf, so you can add your own:

g = google.com/search
d = duckduckgo.com/
yt = youtube.com/results

if you just type @, it shows all available engines. if you type @g without a query, it shows "Using Google" as a placeholder.

Calculator Plugin (trigger: =)

The simplest but most useful one. Type =2+2 and it shows 2+2 = 4. Hit Enter and it copies the result to your clipboard using wl-copy.

Uses the evalexpr crate for evaluation, so it handles proper math with operator precedence and all that.


3. The Spell Framework Thing

To make the launcher actually behave like a launcher (floating above everything, grabbing focus, etc.), i used this framework called Spell. it handles:

  • Layer shell stuff: Making the window appear as an overlay
  • Window management: Sizing, positioning, the boring stuff

The code looks like this:

let window_conf = WindowConf::builder()
    .width(600)
    .height(400)
    .board_interactivity(BoardType::Exclusive)
    .layer_type(LayerType::Overlay)
    .build()
    .unwrap();

let ui = LauncherWindowSpell::invoke_spell("swift-launcher", window_conf);

cast_spell!(ui)

Way cleaner than manually fighting with winit or whatever.


4. Fuzzy Search for Everything Else

When you're not using a plugin trigger, the launcher does fuzzy search across:

  • All your desktop applications (from .desktop files)
  • Custom actions from your config file

it uses SkimMatcherV2 to score matches across name, keywords, and the exec command. So searching for "browser" will find Firefox even if the desktop file doesn't have "browser" in its name (because the exec command is firefox).

The matching tries name first, then keywords, then exec. This gives better results than just matching one field.


5. Desktop File Scraping

The scraper module reads all your .desktop files from XDG_DATA_DiRS. it:

  • Parses the iNi format
  • Strips field codes from exec lines (those %U and %F things)
  • Filters out non-application entries
  • Handles Flatpak apps properly

i optimized it to use iterator chains instead of nested loops, and it logs how long it takes so i can see if it gets slow.


6. Theming

i added a full theming system using iNi files. You can customize:

Window stuff:

  • Size, background color, border radius, border color

Action items:

  • Height, colors for selected/unselected, font sizes
  • Whether to show the exec command under the name

input box:

  • Font size, colors, border radius, height

Example theme.conf:

[Window]
width = 600
height = 400
background-color = #1a1a1a
border-radius = 15

[Action]
option-color = #f0f0f0
option-color-selected = #e0e0e0
name-font-size = 16
exec-show = true

[Runner]
background-color = #c1c1c1
border-radius = 10

The theme is exposed to Slint as a global singleton, so the Ui updates automatically when values change.


7. Config File Actions

You can define custom actions in ~/.config/swift/config.conf:

[variables]
editor = hx
projects_dir = /home/user/dev

[action:dotfiles]
name = Open dotfiles
exec = $editor ~/dotfiles
keywords = config vim

[action:projects]
name = Open projects folder
exec = thunar $projects_dir
keywords = files manager

Variables get substituted with their values, so $editor becomes hx everywhere. These actions get merged with the desktop apps at startup.


8. Ui Polish and Keyboard Stuff

  • Up/Down arrows to move through results
  • Enter to launch the selected item
  • Escape to quit immediately

Custom input Component

i made a Styledinput that wraps Slint's Textinput but adds:

  • Key event forwarding (so Up/Down work for navigation)
  • Theme integration
  • Proper focus handling

Launching

When you hit Enter:

  1. Spawn the command using sh -c
  2. Call slint::quit_event_loop()
  3. Call std::process::exit(0) just to be sure

i had issues in GNOME because they dont implement the "wlr-layer-shell" protocol, so its unsupported


What's Actually Working

  • WASM plugin system with 3 built-in plugins
  • Fuzzy search for desktop apps and custom actions
  • Full theming via config files
  • Keyboard navigation
  • Calculator with clipboard copy
  • Directory browsing with fuzzy matching
  • Web search with configurable engines

What's Next

  • More plugins (clipboard history, window switcher, etc.)
  • Maybe Flatpak packaging if i feel like suffering that day

The code is at pezfisk/swift-launcher if you want to mess with it or add plugins.


Random Things i Learned

  1. WASM components are actually usable now. The tooling still has rough edges but the core works great for plugins.

  2. Slint is really nice for system tools. Way less boilerplate than egui or iced, and the styling is flexible enough for a launcher.

  3. Fuzzy matching is essential. Once you have it, prefix matching feels broken.

  4. Config files over hardcoding. Being able to tweak colors and add actions without recompiling is huge for a daily driver app.

The whole thing started as "let me make a simple window" and became this extensible platform thing. The WASM part was definitely the most complex, but it's also what makes this different from just another rofi clone.