Skip to content

Command launcher

The command launcher is a Spotlight-style search overlay activated with Cmd+K (macOS) or Ctrl+K (Linux/Windows). Pressing / also opens it. It provides filtered search across all registered commands and dispatches the selected action.

The launcher is implemented across two modules:

ModuleResponsibility
src/features/command_palette.rsPopup window, search input, filtered results list, keyboard navigation
src/services/commands.rsCommand registry, availability checks, action execution

If you are setting up the project for the first time, the getting started guide covers the build and run steps. For a broader look at how the pieces fit together, see the architecture overview.

The launcher opens as a WindowKind::PopUp window centered on the primary display. A LauncherOpen global tracks whether it is already open. The Launcher entity manages the search state and result list. When the user selects a command, the LauncherRoot parent handles the LauncherEvent::Act event, calls commands::execute(), and closes the popup window.

User presses Cmd+K
-> ToggleSearch action
-> open_launcher()
-> PopUp window with LauncherRoot entity
-> Launcher entity (search input + result list)
-> User selects a command
-> LauncherEvent::Act(LauncherActionKind::Execute(CommandId))
-> commands::execute(command_id, cx)
-> Window closes

LauncherRoot also listens for window deactivation events. When the user clicks outside the popup, macOS changes the blur treatment on the deactivated window, which causes a visible flash. To avoid this, LauncherRoot calls remove_window() directly on deactivation instead of waiting a frame.

Every command has a unique variant in the CommandId enum. This is the stable identifier used throughout the system for registry lookups, availability checks, and dispatch.

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CommandId {
OpenHome,
OpenForm,
OpenHttpLab,
OpenSettings,
OpenNotifications,
OpenDiagnostics,
OpenAbout,
ThemeLight,
ThemeDark,
StartDemoTask,
CheckConnectivity,
CopyDiagnostics,
OpenLogsFolder,
OpenConfigFolder,
Undo,
Redo,
}

Navigation commands like OpenHome and OpenForm emit AppEventKind::Navigate(AppRoute) through the app event bus. The routing system picks up the event and changes the active page. See routing and navigation for how that dispatch works end to end.

Each command is described by a CommandSpec, which provides the data shown in the launcher UI.

pub struct CommandSpec {
pub id: CommandId,
pub title: SharedString,
pub subtitle: SharedString,
pub icon: IconName,
}
FieldPurpose
idThe CommandId variant used for dispatch
titlePrimary label shown in the launcher
subtitleSecondary description below the title
iconIcon rendered next to the title

The registry() function returns the full list of commands available in the launcher. The order of entries determines the display order when the search query is empty.

pub fn registry() -> Vec<CommandSpec> {
vec![
command(CommandId::OpenHome, "Home", "Open the Home page", IconName::Inbox),
command(CommandId::OpenForm, "Form", "Open the Form page", IconName::File),
command(CommandId::OpenHttpLab, "HTTP Lab", "Explore httpbin requests", IconName::Globe),
command(CommandId::OpenSettings, "Settings", "Open the Settings page", IconName::Settings2),
command(CommandId::ThemeLight, "Light Mode", "Switch to light theme", IconName::Sun),
command(CommandId::ThemeDark, "Dark Mode", "Switch to dark theme", IconName::Moon),
// ...
]
}
fn command(id: CommandId, title: &str, subtitle: &str, icon: IconName) -> CommandSpec {
CommandSpec {
id,
title: title.into(),
subtitle: subtitle.into(),
icon,
}
}
CommandIdTitleDescription
OpenHomeHomeNavigate to the home page
OpenFormFormNavigate to the form page
OpenHttpLabHTTP LabNavigate to the HTTP lab page
OpenSettingsSettingsNavigate to settings
OpenNotificationsNotificationsNavigate to notifications
OpenDiagnosticsDiagnosticsNavigate to diagnostics
OpenAboutAboutNavigate to about page
ThemeLightLight ModeSwitch to light theme
ThemeDarkDark ModeSwitch to dark theme
StartDemoTaskStart Demo TaskStart a background task
CheckConnectivityCheck ConnectivityRun a network probe
CopyDiagnosticsCopy DiagnosticsCopy diagnostics to clipboard
OpenLogsFolderOpen Logs FolderOpen logs in file manager
OpenConfigFolderOpen Config FolderOpen config in file manager
UndoUndoUndo last reversible command
RedoRedoRedo last reversed command

The ThemeLight and ThemeDark commands call set_theme_mode, which swaps the active theme at runtime. The themes guide covers how theme switching works and how to add custom themes.

Some commands depend on runtime conditions. The availability() function checks whether a command can run and returns a reason when it cannot.

pub fn availability(id: CommandId, cx: &App) -> CommandAvailability {
let desktop = crate::desktop_actions::snapshot(cx);
match id {
CommandId::OpenHome
| CommandId::OpenForm
| CommandId::OpenHttpLab
| CommandId::OpenSettings
| CommandId::ThemeLight
| CommandId::ThemeDark
| CommandId::StartDemoTask => CommandAvailability {
enabled: true,
disabled_reason: None,
},
CommandId::CopyDiagnostics => CommandAvailability {
enabled: desktop.clipboard_available,
disabled_reason: (!desktop.clipboard_available)
.then_some("Clipboard backend unavailable".into()),
},
CommandId::Undo => CommandAvailability {
enabled: crate::undo_stack::can_undo(cx).is_some(),
disabled_reason: crate::undo_stack::can_undo(cx)
.is_none()
.then_some("No undo available".into()),
},
// ... Redo follows the same pattern
}
}
ConditionCommands affected
Clipboard unavailableCopyDiagnostics
System opener unavailableOpenLogsFolder, OpenConfigFolder
No undo historyUndo
No redo historyRedo

The desktop_actions::snapshot call queries the OS for clipboard and file opener support. On platforms where these backends are missing (some Linux window managers, for example), the commands appear in the list but are grayed out with an explanation.

The execute() function dispatches a CommandId to its handler. Each arm handles errors through the error surface, which reports them to the user with actionable buttons.

pub fn execute(id: CommandId, cx: &mut App) {
match id {
CommandId::OpenHome => navigate(Page::Home, cx),
CommandId::OpenHttpLab => navigate(Page::HttpLab, cx),
CommandId::ThemeLight => crate::app::set_theme_mode(ThemeMode::Light, cx),
CommandId::StartDemoTask => crate::tasks::start_demo_task(cx),
CommandId::CopyDiagnostics => {
if let Err(error) = crate::desktop_actions::copy_diagnostics(cx) {
tracing::warn!(%error, "copy diagnostics failed");
crate::error_surface::report(
format!("Copy diagnostics failed: {error}"),
crate::errors::AppErrorSeverity::Error,
crate::error_surface::ErrorCategory::System,
vec![ErrorAction::Retry, ErrorAction::Dismiss],
cx,
);
}
}
// ...
}
}

Navigation commands emit AppEventKind::Navigate(AppRoute) through the app event bus. The error surface gives the user a retry button for transient failures (clipboard backend not ready, for example) and a dismiss button for permanent ones.

Adding a command to the launcher requires changes in src/services/commands.rs. The launcher picks up new registry entries automatically. No changes to command_palette.rs are needed.

pub enum CommandId {
// ... existing variants
MyNewCommand,
}

The Serialize, Deserialize derive macros on the enum mean this variant can be persisted in the undo stack or config file. If you change variant names later, add a serde alias to maintain backward compatibility with saved data.

command(CommandId::MyNewCommand, "My Command", "Does something useful", IconName::Star),

The position in the vec![] determines the display order when no search query is active. Group related commands together so users can find them quickly.

In availability(), either match the variant in the default enabled arm (the | chain at the top of the match) or add a dedicated arm with a condition. Commands that always work go in the default arm.

CommandId::MyNewCommand => {
// your logic here
}

If the action can fail, wrap it in if let Err(error) and report through error_surface::report. This gives the user a visible error with retry/dismiss buttons instead of a silent failure.

The launcher opens as a WindowKind::PopUp window. The window bounds and appearance are set in open_launcher():

let window_w = px(620.);
let window_h = px(460.);
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: None,
focus: true,
show: true,
kind: WindowKind::PopUp,
is_movable: true,
is_resizable: false,
window_background: WindowBackgroundAppearance::Blurred,
window_min_size: Some(gpui::Size {
width: window_w,
height: window_h,
}),
..Default::default()
};
PropertyValueNotes
Size620 x 460 pxFixed, non-resizable
PositionCentered on primary display, offset 12% from topFalls back to fixed coordinates if no display is detected
BackgroundBlurredUses WindowBackgroundAppearance::Blurred for a translucent backdrop
Title barNoneFrameless popup
MovableYesUser can drag the window

On macOS, LauncherRoot::new also installs a LiquidGlass effect, which creates an NSGlassEffectView directly in the native view hierarchy. This gives the popup window a system-native translucency effect without patching GPUI source.

The Launcher entity performs case-insensitive substring matching against both title and subtitle:

fn refilter(&mut self, cx: &mut Context<Self>) {
let q = self.input.read(cx).value().to_lowercase();
self.filtered = if q.is_empty() {
(0..self.items.len()).collect()
} else {
self.items
.iter()
.enumerate()
.filter(|(_, item)| {
item.title.to_lowercase().contains(&q)
|| item.subtitle.to_lowercase().contains(&q)
})
.map(|(i, _)| i)
.collect()
};
self.selected_index = 0;
cx.notify();
}

When the query is empty, all commands are shown. Results reset the selection to the first item on every keystroke. The current implementation uses substring matching, which is sufficient for the built-in command set. If you need ranked fuzzy matching (where “ldm” matches “Light Mode”), replace the contains check with a fuzzy scorer and sort by score.

The launcher binds three actions within its CONTEXT focus scope:

KeyActionBehavior
UpSelectPrevMove selection up, wrapping to the last item
DownSelectNextMove selection down, wrapping to the first item
EscapeDismissClose the launcher without executing

Enter is handled through the InputEvent::PressEnter subscription on the search input, which calls act() to dispatch the selected command and close the window. The mouse also works: hovering an item updates the selection, and clicking dispatches it immediately.

The footer bar renders a static hint showing the three available keys. This is pure UI text, not bound to anything. The actual keybindings are defined in init():

pub fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("down", SelectNext, Some(CONTEXT)),
KeyBinding::new("up", SelectPrev, Some(CONTEXT)),
KeyBinding::new("escape", Dismiss, Some(CONTEXT)),
]);
}

To change how the launcher appears or behaves, modify these areas.

Window size and position: edit the window_w, window_h, and the 0.12 vertical offset in open_launcher().

Search behavior: replace the substring filter in refilter() with a different matching algorithm. A fuzzy scorer with ranked results is a common upgrade. You could also add category grouping or recently-used sorting.

Item rendering: modify the Render implementation for Launcher to change how results display. The current layout renders an icon in a rounded square next to the title and subtitle. You could add keyboard shortcut hints, section dividers, or badge indicators.

Action kinds: extend LauncherActionKind to support action types beyond Execute, then handle them in LauncherRoot’s subscription callback. For example, you could add a Preview variant that shows a tooltip without closing the popup.