Fast application launcher with fuzzy search and frecency ranking
garlaunch is a rofi-like application launcher built for the gardesk ecosystem. It features multiple launcher modes, fuzzy search powered by nucleo, frecency-based ranking that learns your preferences, and deep integration with the gar window manager for window switching. Supports daemon mode for instant activation via keybindings, and extensible script mode for custom launchers.
garlaunch is distributed as part of the gardesk package suite. Install via DNF on Fedora or RHEL-based systems.
Or build from source:
Launch garlaunch directly to browse and search your applications.
Start typing to filter results. Use keyboard navigation to select and launch.
Type Filter results with fuzzy matching Up/Down Navigate through results PageUp/PageDown Jump by visible page Enter Launch selected item Escape Close without launching Home/End Move cursor to start/end of input Backspace/Delete Delete characters from input Run garlaunch as a daemon for instant activation. The daemon listens on a Unix socket and spawns launcher instances on demand, eliminating startup latency.
Control the daemon:
Configure keybindings in gar to launch garlaunch instantly.
-- ~/.config/gar/init.lua
gar.keybind("Mod+d", function()
gar.spawn("garlaunchctl toggle")
end)
gar.keybind("Mod+Shift+d", function()
gar.spawn("garlaunchctl show --mode run")
end)
gar.keybind("Mod+Tab", function()
gar.spawn("garlaunchctl show --mode window")
end) Start the daemon automatically with your gar session.
-- ~/.config/gar/init.lua
gar.autostart("garlaunch --daemon") garlaunch is configured primarily through command-line arguments.
| Option | Type | Default | Description |
|---|---|---|---|
--mode, -m | - | drun | Mode to launch in |
--prompt, -p | - | "Search:" | Custom prompt text for the input field |
--source, -s | - | - | Script source path (required for script mode) |
--daemon, -d | - | false | Run as daemon listening for IPC commands |
Control utility for the garlaunch daemon.
| Option | Type | Default | Description |
|---|---|---|---|
show | - | - | Show the launcher with optional --mode and --source flags |
hide | - | - | Hide the launcher (when running) |
toggle | - | - | Toggle launcher visibility with optional --mode flag |
status | - | - | Query the daemon status (returns JSON) |
garlaunch supports multiple launcher modes for different use cases.
| Option | Type | Default | Description |
|---|---|---|---|
drun | - | - | Desktop applications from .desktop files in XDG data directories |
run | - | - | All executable binaries found in $PATH directories |
window | - | - | Window switcher mode (requires gar window manager) |
script | - | - | Custom mode driven by external script via JSON protocol |
garlaunch respects standard environment variables.
XDG_RUNTIME_DIR
Directory for the IPC socket (defaults to /tmp). Socket path: $XDG_RUNTIME_DIR/garlaunch.sock
XDG_DATA_HOME User data directory for .desktop file discovery (defaults to ~/.local/share)
XDG_CACHE_HOME
Cache directory for frecency data (defaults to ~/.cache). Cache path: $XDG_CACHE_HOME/garlaunch/
PATH Used by run mode to discover executable binaries
RUST_LOG
Logging level (trace, debug, info, warn, error). Example: RUST_LOG=garlaunch=debug
garlaunch stores frecency data per mode in the cache directory.
# Cache file locations
~/.cache/garlaunch/drun.cache # Desktop apps frecency
~/.cache/garlaunch/run.cache # Executables frecency
~/.cache/garlaunch/window.cache # Window switching frecency
# Cache format (TSV)
# id score last_access_timestamp
firefox 5.234567 1706900000
code 3.891234 1706899000 garlaunch is a fast, extensible application launcher designed for the gardesk desktop environment. It provides a unified interface for launching applications, switching windows, and running custom scripts, all with intelligent search and ranking.
garlaunch follows a modular architecture separating concerns into modes, search, UI, and IPC layers.
garlaunch/
├── app.rs # Main application state and event loop
├── config/ # Configuration (Lua/TOML planned)
├── frecency/ # Frecency scoring and persistence
├── ipc/ # Unix socket IPC (server + gar client)
├── modes/ # Mode implementations (drun, run, window, script)
├── search/ # Fuzzy matcher with frecency integration
└── ui/ # gartk-based popup rendering
Modes implement the Mode trait providing a consistent interface for loading items,
filtering, and handling selection.
pub trait Mode: Send {
/// Get the mode name
fn name(&self) -> &str;
/// Load items (called once on startup)
fn load(&mut self) -> Result<()>;
/// Get all items
fn items(&self) -> &[Item];
/// Handle item activation
fn activate(&self, item: &Item) -> Result<Action>;
} Each mode is responsible for discovering its items (desktop files, executables, windows) and returning an appropriate action when an item is selected.
Items represent selectable entries in the launcher with consistent structure across all modes.
pub struct Item {
/// Unique identifier (used for frecency tracking)
pub id: String,
/// Display name shown in the list
pub name: String,
/// Optional secondary description
pub description: Option<String>,
/// Optional icon name from theme
pub icon: Option<String>,
/// Mode-specific data (e.g., exec command, window ID)
pub data: serde_json::Value,
} When an item is activated, modes return an Action describing what should happen.
pub enum Action {
/// Close the launcher without doing anything
Close,
/// Launch a command (spawns via systemd-run)
Launch(String),
/// Switch to another mode (e.g., from drun to run)
SwitchMode(String),
/// Custom action with arbitrary JSON data
Custom(serde_json::Value),
} The application uses gartk's event loop for X11 event processing with efficient rendering.
// Simplified event loop flow
event_loop.run(|ev, event| {
match event {
InputEvent::Key(key_event) if key_event.pressed => {
self.handle_key(&key_event.key);
ev.request_redraw();
}
InputEvent::Expose => ev.request_redraw(),
InputEvent::CloseRequested => self.should_quit = true,
InputEvent::FocusIn => self.has_focus = true,
InputEvent::FocusOut if self.has_focus => {
self.should_quit = true; // Close on focus loss
}
_ => {}
}
if ev.needs_redraw() {
self.render()?;
ev.redraw_done();
}
Ok(!self.should_quit)
})?; garlaunch combines fuzzy matching with frecency scoring for intelligent result ranking.
The fuzzy matcher uses the nucleo library, the same engine used by Helix editor, providing fast and accurate fuzzy matching.
Frecency combines frequency and recency to prioritize items you use often and recently. The algorithm uses exponential decay with a configurable half-life (default: 1 week).
// Score calculation with decay
fn decay_factor(elapsed_hours: f64, half_life_hours: f64) -> f64 {
2.0_f64.powf(-elapsed_hours / half_life_hours)
}
// When recording a selection:
let decayed_score = entry.score * decay_factor(elapsed, half_life);
entry.score = decayed_score + 1.0; // Bump by 1
entry.last_access = now; Items used more frequently have higher base scores. Items used more recently have less decay applied. The combination naturally surfaces your most relevant applications.
When filtering, fuzzy scores and frecency are combined with fuzzy score dominating.
// Frecency adds a small boost (10% weight)
let frecency_boost = frecency.score(&item.id) * 0.1;
let combined = fuzzy_score + frecency_boost;
// When query is empty, sort purely by frecency
// Most recently/frequently used items appear first garlaunch uses systemd for proper process lifecycle management, ensuring launched applications integrate correctly with the desktop session.
Applications are launched via systemd-run in transient scope units, providing:
The exact systemd-run invocation used for launching applications:
systemd-run --user --scope \
--slice=app-garlaunch.slice \
--property=BindsTo=graphical-session.target \
--property=After=graphical-session.target \
--property=TimeoutStopSec=10 \
sh -c "$command"
The BindsTo=graphical-session.target property ensures applications launched via
garlaunch are properly terminated when you log out, preventing orphaned processes.
garlaunch integrates tightly with the gar window manager for window switching functionality.
Window mode connects to gar's IPC socket to query and focus windows.
// WindowMode connects to gar IPC
let client = GarClient::new();
client.connect()?;
// Query all windows
let windows = client.get_windows()?;
// Each window provides:
pub struct WindowInfo {
pub id: u32, // X11 window ID
pub title: String, // Window title
pub class: Option<String>, // WM_CLASS
pub instance: Option<String>, // WM_CLASS instance
pub workspace: u32, // Current workspace
pub focused: bool, // Is focused
}
// On selection, focus the window
client.focus_window(window_id)?; Recommended keybindings for gar integration:
-- ~/.config/gar/init.lua
-- Application launcher (like dmenu/rofi)
gar.keybind("Mod+d", function()
gar.spawn("garlaunchctl toggle")
end)
-- Run dialog (PATH executables)
gar.keybind("Mod+Shift+d", function()
gar.spawn("garlaunchctl show --mode run")
end)
-- Window switcher (like Alt+Tab)
gar.keybind("Mod+Tab", function()
gar.spawn("garlaunchctl show --mode window")
end)
-- Custom script launcher
gar.keybind("Mod+p", function()
gar.spawn("garlaunchctl show --mode script --source ~/.local/bin/project-launcher")
end) garlaunch uses the gartk UI toolkit for native X11 rendering.
The popup window is rendered using gartk components:
garlaunch uses gartk's dark theme by default with customizable colors:
let theme = Theme::dark();
// Theme includes:
// - background/foreground colors
// - input field styling
// - item highlighting
// - border radius and padding
// - font family and size The popup window is positioned centered on the monitor containing the mouse pointer, in the upper third of the screen for ergonomic access.
// Detect monitor under mouse pointer
let monitor = gartk_x11::monitor_at_pointer(&conn)?;
// Center horizontally, upper third vertically
let x = monitor.rect.x + (monitor.rect.width - width) / 2;
let y = monitor.rect.y + (monitor.rect.height - height) / 3;
// Default dimensions
let width = 600;
let height = 400;
let max_visible = 10; // Items shown at once garlaunch supports multiple modes for different use cases. Each mode provides items and handles selection differently.
Desktop application launcher. Scans XDG data directories for .desktop files and presents them as launchable applications.
garlaunch --mode drun
garlaunchctl show --mode drun | Option | Type | Default | Description |
|---|---|---|---|
~/.local/share/applications | - | - | User-installed applications |
/usr/share/applications | - | - | System-wide applications |
/usr/local/share/applications | - | - | Locally installed applications |
/run/current-system/sw/share/applications | - | - | NixOS applications |
~/.local/share/flatpak/exports/share/applications | - | - | User Flatpak apps |
/var/lib/flatpak/exports/share/applications | - | - | System Flatpak apps |
/var/lib/snapd/desktop/applications | - | - | Snap applications |
Executable launcher. Scans all directories in $PATH for executable files.
garlaunch --mode run
garlaunchctl show --mode run Window switcher. Connects to gar window manager via IPC to list and focus windows. Requires gar to be running.
garlaunch --mode window
garlaunchctl show --mode window
Window mode communicates with gar via Unix socket at $XDG_RUNTIME_DIR/gar.sock.
// Commands sent to gar:
{"command": "get_windows"}
{"command": "focus_window", "args": {"id": 12345678}} Custom script-driven mode. Run any executable that speaks the JSON protocol to provide custom launcher items and actions.
garlaunch --mode script --source /path/to/script
garlaunchctl show --mode script --source ~/.local/bin/my-launcher | Option | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique identifier for the item (required) |
name | string | - | Display name shown in the list (required) |
description | string? | - | Optional secondary text |
icon | string? | - | Icon name from system theme |
data | object? | - | Arbitrary JSON data attached to item |
{
"items": [
{"id": "proj1", "name": "Project Alpha", "description": "~/src/alpha"},
{"id": "proj2", "name": "Project Beta", "description": "~/src/beta", "icon": "folder"},
{"id": "proj3", "name": "Project Gamma", "data": {"path": "/home/user/gamma"}}
]
} {
"selected": {
"id": "proj1",
"name": "Project Alpha",
"description": "~/src/alpha",
"icon": null,
"data": null
}
} | Option | Type | Default | Description |
|---|---|---|---|
launch | - | - | Execute command (requires "command" field) |
close | - | - | Close the launcher without action |
switch | - | - | Switch to another mode (requires "mode" field) |
custom | - | - | Custom action (optional "data" field) |
// Launch a command
{"action": "launch", "command": "code ~/src/alpha"}
// Just close the launcher
{"action": "close"}
// Switch to drun mode
{"action": "switch", "mode": "drun"}
// Custom action with data
{"action": "custom", "data": {"type": "project", "id": "proj1"}} #!/bin/bash
# ~/.local/bin/project-launcher
# Output initial items (one JSON line)
echo '{"items": ['
echo ' {"id": "p1", "name": "Website", "description": "~/src/website"},'
echo ' {"id": "p2", "name": "API Server", "description": "~/src/api"},'
echo ' {"id": "p3", "name": "Mobile App", "description": "~/src/mobile"}'
echo ']}'
# Read selection from stdin
read selection
# Parse selected ID (using jq)
id=$(echo "$selection" | jq -r '.selected.id')
# Return action based on selection
case "$id" in
p1) echo '{"action": "launch", "command": "code ~/src/website"}' ;;
p2) echo '{"action": "launch", "command": "code ~/src/api"}' ;;
p3) echo '{"action": "launch", "command": "code ~/src/mobile"}' ;;
*) echo '{"action": "close"}' ;;
esac #!/usr/bin/env python3
# ~/.local/bin/ssh-launcher
import json
import sys
import os
# Read SSH config for hosts
hosts = []
ssh_config = os.path.expanduser("~/.ssh/config")
if os.path.exists(ssh_config):
with open(ssh_config) as f:
for line in f:
if line.strip().lower().startswith("host "):
host = line.split()[1]
if "*" not in host:
hosts.append({"id": host, "name": host, "icon": "network-server"})
# Output items
print(json.dumps({"items": hosts}))
sys.stdout.flush()
# Read selection
selection = json.loads(input())
host = selection["selected"]["id"]
# Return launch action
print(json.dumps({
"action": "launch",
"command": f"garterm -e ssh {host}"
}))
garlaunch supports IPC via Unix domain socket for daemon mode control. The socket is created
at $XDG_RUNTIME_DIR/garlaunch.sock when running in daemon mode.
# Default location
$XDG_RUNTIME_DIR/garlaunch.sock
# Typically resolves to
/run/user/1000/garlaunch.sock Requests are JSON objects sent as a single line followed by newline.
{
"command": "show",
"args": {
"mode": "drun",
"source": null
}
} | Option | Type | Default | Description |
|---|---|---|---|
show | - | - | Show launcher with mode and optional source |
hide | - | - | Hide the currently visible launcher |
toggle | - | - | Toggle launcher visibility in specified mode |
status | - | - | Query daemon status (returns running state) |
| Option | Type | Default | Description |
|---|---|---|---|
success: true | - | - | Command executed successfully (data field optional) |
success: false | - | - | Command failed (error field contains message) |
// Success response
{"success": true}
// Success with data
{"success": true, "data": {"running": true}}
// Error response
{"success": false, "error": "Unknown command: invalid"} # Show in default mode
echo '{"command": "show", "args": {"mode": "drun"}}' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/garlaunch.sock
# Toggle visibility
echo '{"command": "toggle", "args": {"mode": "drun"}}' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/garlaunch.sock
# Query daemon status
echo '{"command": "status"}' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/garlaunch.sock
# Hide launcher
echo '{"command": "hide"}' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/garlaunch.sock
The garlaunchctl utility provides a convenient CLI for IPC commands.
When the daemon isn't running, garlaunchctl falls back to spawning garlaunch directly.
# If daemon not running:
# garlaunchctl show --mode run
# becomes equivalent to:
garlaunch --mode run Example of connecting to the IPC socket programmatically.
import socket
import json
import os
sock_path = os.path.join(
os.environ.get("XDG_RUNTIME_DIR", "/tmp"),
"garlaunch.sock"
)
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(sock_path)
# Send command
request = {"command": "show", "args": {"mode": "drun"}}
sock.send((json.dumps(request) + "\n").encode())
# Read response
response = sock.recv(4096).decode()
print(json.loads(response)) use std::os::unix::net::UnixStream;
use std::io::{Write, BufRead, BufReader};
let socket_path = std::env::var("XDG_RUNTIME_DIR")
.unwrap_or("/tmp".into()) + "/garlaunch.sock";
let mut stream = UnixStream::connect(&socket_path)?;
// Send request
let request = serde_json::json!({
"command": "toggle",
"args": {"mode": "drun"}
});
writeln!(stream, "{}", request)?;
// Read response
let mut reader = BufReader::new(&stream);
let mut response = String::new();
reader.read_line(&mut response)?;
println!("{}", response); If garlaunch doesn't show when triggered:
# Check if daemon is running
pgrep -f "garlaunch --daemon"
# Start daemon if needed
garlaunch --daemon &
# Verify socket exists
ls -la $XDG_RUNTIME_DIR/garlaunch.sock
# Test direct launch (bypasses daemon)
garlaunch --mode drun
# Check for X11 display
echo $DISPLAY If the application list is empty:
# Check if .desktop files exist
ls ~/.local/share/applications/
ls /usr/share/applications/
# Enable debug logging
RUST_LOG=garlaunch=debug garlaunch --mode drun
# Check XDG_DATA_HOME
echo $XDG_DATA_HOME
# Verify desktop file format
cat /usr/share/applications/firefox.desktop | head -20 Window mode requires the gar window manager to be running.
# Check if gar is running
pgrep gar
# Check gar socket
ls -la $XDG_RUNTIME_DIR/gar.sock
# Test gar IPC directly
echo '{"command": "get_windows"}' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/gar.sock
# Enable debug logging
RUST_LOG=garlaunch=debug garlaunch --mode window Debug script mode issues:
# Make script executable
chmod +x ~/.local/bin/my-launcher
# Test script directly
~/.local/bin/my-launcher
# Should output valid JSON like:
# {"items": [{"id": "1", "name": "Test"}]}
# Run with debug logging
RUST_LOG=garlaunch=debug garlaunch --mode script --source ~/.local/bin/my-launcher
# Common issues:
# - Script not executable
# - Invalid JSON output
# - Missing newline after JSON
# - Script reads stdin before outputting items If frequently used apps don't appear at the top:
# Check cache directory
ls -la ~/.cache/garlaunch/
# View frecency data
cat ~/.cache/garlaunch/drun.cache
# Cache should contain lines like:
# id score timestamp
# firefox 5.234567 1706900000
# Reset frecency (start fresh)
rm ~/.cache/garlaunch/*.cache If selecting an item doesn't launch the application:
# Check systemd user session
systemctl --user status
# Verify graphical-session.target
systemctl --user status graphical-session.target
# List garlaunch-launched apps
systemctl --user list-units --type=scope 'app-garlaunch*'
# Check journal for errors
journalctl --user -u app-garlaunch.slice -f
# Test systemd-run manually
systemd-run --user --scope firefox If the UI doesn't render correctly:
# Check X11 connection
xdpyinfo | head
# Verify compositor is running (for transparency)
pgrep -f garchomp || pgrep picom
# Test without transparency
# (Currently requires code change)
# Check font availability
fc-list | grep -i sans If garlaunch closes immediately after opening:
Another application may be grabbing keyboard focus. Try clicking the launcher window.
garlaunch closes on focus loss. Check if another window is stealing focus on launch.
Check debug logs for mode initialization errors.
Enable verbose logging to diagnose issues.
Report bugs at github.com/espadon/gardesk/issues
garlaunch version, gar version (if using window mode), output of RUST_LOG=debug garlaunch,
desktop environment, distribution