Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Keyboard Shortcuts Example

Live Demo

Source Code

#![cfg_attr(target_family = "wasm", no_main)]

use std::collections::HashMap;

use gpui::{px, size, App, Bounds, WindowBounds, WindowOptions};
use vgui::prelude::*;
use vgui::show_when;
use vgui::{CommandOptions, ContextOptions, KeymapOptions};

#[cfg(not(target_family = "wasm"))]
use gpui_platform::application;

#[cfg(target_family = "wasm")]
use gpui_platform::single_threaded_web;

/// A command entry in the palette.
struct Command {
    name: &'static str,
    shortcut: &'static str,
}

const COMMANDS: &[Command] = &[
    Command { name: "New File",      shortcut: "Ctrl+N" },
    Command { name: "Save",          shortcut: "Ctrl+S" },
    Command { name: "Save As",       shortcut: "Ctrl+Shift+S" },
    Command { name: "Find",          shortcut: "Ctrl+F" },
    Command { name: "Open Palette",  shortcut: "Ctrl+K, Ctrl+P" },
    Command { name: "Quit",          shortcut: "Ctrl+Q" },
];

fn app() -> impl gpui::IntoElement {
    // ── State ──────────────────────────────────────────────────────
    let (palette_open, set_palette) = create_signal(false);
    let (selected, set_selected) = create_signal(0usize);
    let (last_action, set_last_action) =
        create_signal(String::from("Press Ctrl+K, Ctrl+P for palette"));
    let (partial_label, set_partial_label) = create_signal(String::new());

    // ── Shortcuts engine (persisted across re-renders) ─────────────
    let sc = use_shortcuts();

    // One-time initialization: keymap, handlers, partial-change listener,
    // and initial context. On re-renders the has_keymap guard skips this
    // block, preserving the engine state (sequence cursors, context stack).
    if !sc.has_keymap() {
        // ── Keymap ──────────────────────────────────────────────────
        let mut commands = HashMap::new();
        commands.insert("new_file".to_string(), CommandOptions {
            shortcut: "Ctrl+N".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        commands.insert("save".to_string(), CommandOptions {
            shortcut: "Ctrl+S".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        commands.insert("save_as".to_string(), CommandOptions {
            shortcut: "Ctrl+Shift+S".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        commands.insert("find".to_string(), CommandOptions {
            shortcut: "Ctrl+F".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        commands.insert("palette".to_string(), CommandOptions {
            shortcut: "Ctrl+K,Ctrl+P".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        commands.insert("quit".to_string(), CommandOptions {
            shortcut: "Ctrl+Q".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        // Palette-context commands
        commands.insert("next".to_string(), CommandOptions {
            shortcut: "Down".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        commands.insert("prev".to_string(), CommandOptions {
            shortcut: "Up".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        commands.insert("select".to_string(), CommandOptions {
            shortcut: "Enter".to_string(), event: None, prevent_default: None, interceptors: None,
        });
        commands.insert("close".to_string(), CommandOptions {
            shortcut: "Escape".to_string(), event: None, prevent_default: None, interceptors: None,
        });

        let mut contexts = HashMap::new();
        contexts.insert("global".to_string(), ContextOptions {
            commands: vec!["new_file".to_string(), "save".to_string(), "save_as".to_string(),
                           "find".to_string(), "palette".to_string(), "quit".to_string()],
            abstract_ctx: None, fallbacks: None,
        });
        contexts.insert("palette".to_string(), ContextOptions {
            commands: vec!["next".to_string(), "prev".to_string(),
                           "select".to_string(), "close".to_string()],
            abstract_ctx: None, fallbacks: Some(vec!["global".to_string()]),
        });

        sc.keymap(KeymapOptions { commands, contexts });

        // ── Partial-match indicator ────────────────────────────────
        // The listener receives cx, so it can update the signal directly.
        let set_pl = set_partial_label.clone();
        sc.on_partial_change(move |names: &[String], cx| {
            if names.is_empty() {
                set_pl.set(cx, String::new());
            } else {
                set_pl.set(cx, format!("Partial: {}", names.join(", ")));
            }
        });

        // ── Global command handlers ────────────────────────────────
        let set_act = set_last_action.clone();
        sc.on("new_file", move |_sev, _w, cx| {
            set_act.set(cx, "New File".to_string());
        });
        let set_act = set_last_action.clone();
        sc.on("save", move |_sev, _w, cx| {
            set_act.set(cx, "Save".to_string());
        });
        let set_act = set_last_action.clone();
        sc.on("save_as", move |_sev, _w, cx| {
            set_act.set(cx, "Save As".to_string());
        });
        let set_act = set_last_action.clone();
        sc.on("find", move |_sev, _w, cx| {
            set_act.set(cx, "Find".to_string());
        });
        let set_act = set_last_action.clone();
        sc.on("quit", move |_sev, _w, cx| {
            set_act.set(cx, "Quit".to_string());
        });

        // ── Palette command (sequence: Ctrl+K, Ctrl+P) ─────────────
        let set_pal = set_palette.clone();
        let set_sel2 = set_selected.clone();
        let sc_pal = sc.clone();
        sc.on("palette", move |_sev, _w, cx| {
            set_pal.set(cx, true);
            set_sel2.set(cx, 0);
            sc_pal.set_context("palette");
        });

        // ── Palette-context commands ───────────────────────────────
        let set_sel_p = set_selected.clone();
        sc.on("next", move |_sev, _w, cx| {
            let count = COMMANDS.len();
            set_sel_p.update(cx, |s| *s = (*s + 1) % count);
        });
        let set_sel_p = set_selected.clone();
        sc.on("prev", move |_sev, _w, cx| {
            let count = COMMANDS.len();
            set_sel_p.update(cx, |s| *s = if *s == 0 { count - 1 } else { *s - 1 });
        });
        let set_act_p = set_last_action.clone();
        let set_pal_p = set_palette.clone();
        let selected_clone = selected.clone();
        let sc_close = sc.clone();
        sc.on("select", move |_sev, _w, cx| {
            let idx = selected_clone.get();
            let cmd = &COMMANDS[idx.min(COMMANDS.len() - 1)];
            set_act_p.set(cx, format!("Executed: {}", cmd.name));
            set_pal_p.set(cx, false);
            sc_close.set_context("global");
        });
        let set_pal_c = set_palette.clone();
        let sc_close2 = sc.clone();
        sc.on("close", move |_sev, _w, cx| {
            set_pal_c.set(cx, false);
            sc_close2.set_context("global");
        });

        // Start in the global context
        sc.set_context("global");
    }

    // ── View ───────────────────────────────────────────────────────
    let cmds: Vec<(usize, &'static Command)> = COMMANDS.iter().enumerate().collect();

    view! {
        <div class="flex flex-col gap-3 p-6 bg-[#1e1e2e] w-full h-full text-white rounded-lg overflow-hidden">
            // ── Header ──────────────────────────────────────────────
            <div class="flex flex-row items-center justify-between">
                <h2 class="text-lg font-bold text-[#cdd6f4]">{"Keyboard Shortcuts"}</h2>
                <span class="text-xs text-[#6c7086]">
                    {"Ctrl+K, Ctrl+P → palette · Esc → close"}
                </span>
            </div>

            // ── Last action banner ──────────────────────────────────
            <div class="px-3 py-2 rounded bg-[#313244] text-sm text-[#a6e3a1] font-mono">
                {last_action.get()}
            </div>

            // ── Partial-match indicator ─────────────────────────────
            {show_when(!partial_label.get().is_empty(), view! {
                <div class="px-3 py-1 rounded bg-[#181825] border border-[#f9e2af] text-xs font-mono text-[#f9e2af]">
                    {partial_label.get()}
                </div>
            })}

            // ── Shortcut reference ──────────────────────────────────
            <div class="flex flex-col gap-1 px-3 py-2 rounded bg-[#181825] border border-[#313244] text-xs font-mono">
                <div class="text-[#6c7086] mb-1">{"Shortcuts (try these):"}</div>
                {vgui::for_each(cmds, move |(_i, cmd), _| {
                    view! {
                        <div class="flex flex-row justify-between text-[#cdd6f4]">
                            <span>{cmd.name}</span>
                            <span class="text-[#89b4fa]">{cmd.shortcut}</span>
                        </div>
                    }
                })}
            </div>

            // ── Command palette overlay ─────────────────────────────
            {show_when(palette_open.get(), command_palette(selected.get()))}
        </div>
    }
}

/// Command palette overlay — a visual surface for keyboard navigation.
/// All keyboard handling is done by the `shortcuts` engine; this component
/// is purely presentational.
#[allow(non_snake_case)]
fn command_palette(selected: usize) -> impl gpui::IntoElement {
    let cmds: Vec<(usize, &'static Command)> = COMMANDS.iter().enumerate().collect();
    view! {
        <div class="absolute inset-0 bg-black/40 flex items-start justify-center pt-12 z-10">
            <div class="bg-[#1e1e2e] border border-[#313244] rounded-lg w-[400px] flex flex-col gap-1 p-2">
                <div class="px-3 py-2 text-xs text-[#6c7086] border-b border-[#313244] mb-1">
                    {"Up/Down to navigate · Enter to execute · Esc to close"}
                </div>
                {vgui::for_each(cmds, move |(idx, cmd), _| {
                    let class = if idx == selected {
                        "px-3 py-2 rounded text-sm flex flex-row justify-between items-center bg-[#89b4fa] text-[#1e1e2e]"
                    } else {
                        "px-3 py-2 rounded text-sm flex flex-row justify-between items-center text-[#cdd6f4] hover:bg-[#313244]"
                    };
                    view! {
                        <div class={class}>
                            <span>{cmd.name}</span>
                            <span class="text-xs font-mono opacity-60">{cmd.shortcut}</span>
                        </div>
                    }
                })}
            </div>
        </div>
    }
}

fn run() {
    #[cfg(not(target_family = "wasm"))]
    let gpui_app = application();

    #[cfg(target_family = "wasm")]
    let gpui_app = single_threaded_web();

    let launch = |cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(560.), px(480.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |window, cx| vgui::mount(window, cx, app),
        )
        .unwrap();
    };

    #[cfg(not(target_family = "wasm"))]
    gpui_app.run(launch);

    #[cfg(target_family = "wasm")]
    std::mem::forget(gpui_app.run_embedded(launch));
}

#[cfg(not(target_family = "wasm"))]
fn main() {
    run();
}

#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
    gpui_platform::web_init();
    vgui::intercept_keyboard_events();
    run();
}

Key Concepts

use_shortcuts — declarative shortcut engine

vgui integrates the shortcuts crate to provide configurable keyboard shortcuts with human-readable combination strings ("Ctrl+K, Ctrl+P"), a macro registry, a stateful sequence matcher, a context stack with fallbacks, and an interceptor middleware chain.

#![allow(unused)]
fn main() {
let sc = use_shortcuts();
sc.keymap(KeymapOptions { commands, contexts });
sc.on("save", move |_sev, _w, cx| { /* ... */ });
}

Shortcut combinations

Shortcuts are expressed as strings using + for simultaneous keys and , for sequential steps:

CombinationMeaning
"Ctrl+S"Ctrl and S pressed together
"Ctrl+Shift+S"Ctrl, Shift, and S together (exact modifier match)
"Ctrl+K,Ctrl+P"Ctrl+K, then Ctrl+P in sequence
"G,G"G pressed twice in sequence

Context stack with fallbacks

Commands are grouped into named contexts. A context can declare fallbacks — other contexts whose commands are inherited. The switch_context method pushes a context onto the stack; a Drop guard pops it automatically.

#![allow(unused)]
fn main() {
// "palette" context inherits all commands from "global"
contexts.insert("palette", ContextOptions {
    commands: vec!["next", "prev", "select", "close"],
    fallbacks: Some(vec!["global".to_string()]),
    ..
});
}

Partial-match tracking

When a multi-step shortcut (e.g. Ctrl+K, Ctrl+P) is mid-sequence, the engine reports the partially-matched command names via on_partial_change. The example displays a yellow indicator showing which command is awaiting its next key.

prevent_defaultstop_propagation

The TS library’s preventDefault (DOM default-action cancel) maps to KeyboardEvent::stop_propagation() in vgui. When a command matches and prevent_default is true (the default), the dispatcher calls e.stop_propagation(), preventing later hand-written use_key_down handlers from also processing the key.

Running

Native

cargo run -p vgui-keyboard-shortcuts

Web (WASM)

cargo build --target wasm32-unknown-unknown -p vgui-keyboard-shortcuts --release
wasm-bindgen --target web --out-dir examples/keyboard-shortcuts/dist \
    --no-typescript target/wasm32-unknown-unknown/release/keyboard-shortcuts.wasm
python3 scripts/serve_plain.py 8080 examples/keyboard-shortcuts