All skills
lassejlv avatar

/build-gpui-apps

@52f4cfd
by Lasselassejlv/skills50 stars
1

Build, scaffold, refactor, debug, review, and validate native Rust desktop applications with GPUI. Recommend GPUI Kit and ask before adopting it; use gpui_kit imports after agreement, or preserve the chosen upstream GPUI stack. Includes the merged GPUI Kit component and design skills, full application/Base/Component/Shell documentation, coding and design guides, state, actions, async, input, accessibility, motion, themes, native integration, packaging, auto updates, testing, and production delivery. Use paper-to-gpui when the primary task is faithfully translating a selected Paper.design frame into an existing view.

Use this Skill: https://skilld.dev/gh/lassejlv/skills/build-gpui-apps

This session only. Nothing lands on disk.

referencesgpui-kitupstreamcomponentcommand.md

≈4.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Command

A command palette is a filtered list of commands with groups, Action-derived keybinding hints, and keyboard navigation. Use it inline or compose it into an existing dialog for a ⌘K-style menu. On invalidation, Command creates and layout-measures every flattened row; v_virtual_list then renders and paints only viewport rows.

Command owns the entries and presentation policy. CommandState owns the interaction state: query input, focus, selection, scrolling, and loading.

Import

use gpui_kit::component::command::{Command, CommandEntry, CommandGroup, CommandItem, CommandState};

Composition

Build the palette structure directly on Command; create an empty state once and reuse it while the palette is shown.

Command
├── CommandItem                 // ungrouped
├── CommandGroup
│   ├── CommandItem
│   └── CommandItem
├── separator
└── CommandGroup
    ├── CommandItem
    └── CommandItem

CommandState                    // query, focus, selection, scrolling

Usage

Inline

Define Actions and bindings in application setup. The default row resolves an Action's active binding in the Command focus scope and then at application scope, rendering a Kbd hint only when it finds one.

use gpui_kit::{actions, KeyBinding};

actions!(my_app, [OpenProfile, OpenBilling]);

// During application setup:
cx.bind_keys([
    KeyBinding::new("cmd-p", OpenProfile, Some("Command")),
    KeyBinding::new("cmd-b", OpenBilling, Some("Command")),
]);

let state = cx.new(|cx| CommandState::new(window, cx));

Command::new(&state)
    .group(
        CommandGroup::new().label("Suggestions")
            .item(CommandItem::new().label("Calendar").icon(IconName::Calendar))
            .item(CommandItem::new().label("Search Emoji").icon(IconName::Search))
            .item(CommandItem::new().label("Calculator").disabled(true)),
    )
    .separator()
    .group(
        CommandGroup::new().label("Settings")
            .item(
                CommandItem::new().label("Profile")
                    .icon(IconName::User)
                    .action(Box::new(OpenProfile)),
            )
            .item(
                CommandItem::new().label("Billing")
                    .action(Box::new(OpenBilling)),
            ),
    )
    .placeholder("Type a command or search...")
    .empty(|_, _, cx| {
        v_flex()
            .items_center()
            .gap_2()
            .child(Icon::new(IconName::Search).size_8())
            .child("No results found.")
    })
    .w(px(380.))

Do not provide a manually formatted shortcut string. CommandItem::action provides both the executable behavior and, for the default row, the displayed binding. A custom row owns its complete presentation, including any key hint.

Quick Actions Without Search

Disable search for a compact action palette. It has no search field, retains all entries, and state.focus(window, cx) focuses the Command frame so its arrow, Enter, and Escape actions remain available.

let actions = cx.new(|cx| CommandState::new(window, cx));

Command::new(&actions)
    .searchable(false)
    .items([
        CommandItem::new().label("New File").icon(IconName::Plus),
        CommandItem::new().label("Duplicate").icon(IconName::Copy),
        CommandItem::new().label("Move to Trash").icon(IconName::Delete),
    ])
    .w(px(380.))

With the default .searchable(true), state.focus(window, cx) and [Focusable::focus_handle] target the search input instead. A non-searchable palette never invokes on_query.

In a Dialog

Compose the palette with the existing [WindowExt::open_dialog] API. header renders above the optional search field and list; footer renders below the list. In a searchable palette, Escape clears a non-empty query. Otherwise— including a non-searchable palette with a hidden programmatic query—Command calls on_cancel and then propagates Cancel. Let the hosting Dialog perform dismissal—do not close it again in on_cancel.

use gpui_kit::component::WindowExt as _;

let state = self.command_state.clone();
window.open_dialog(cx, move |dialog, _, _| {
    let state = state.clone();
    dialog.close_button(false).p_0().content(move |content, _, _| {
        content.child(
            Command::new(&state)
                .bordered(false)
                .placeholder("Type a command or search...")
                .items([
                    CommandItem::new().label("Profile"),
                    CommandItem::new().label("Billing"),
                ])
                .on_confirm(|index, window, cx| {
                    window.push_notification(format!("Selected {index}"), cx);
                })
                // Record local cleanup only; Dialog handles the propagated Cancel.
                .on_cancel(|window, cx| {
                    window.push_notification("Command palette cancelled", cx);
                })
                .header(|state, _, cx| {
                    h_flex()
                        .justify_between()
                        .px_3()
                        .py_2()
                        .border_b_1()
                        .border_color(cx.theme().border)
                        .child("Commands")
                        .child(format!("{} matches", state.matched_count()))
                })
                .footer(|_, _, cx| {
                    h_flex()
                        .gap_3()
                        .px_3()
                        .py_2()
                        .border_t_1()
                        .border_color(cx.theme().border)
                        .child("↑↓ Navigate")
                        .child("Enter Select")
                        .child("Escape Close")
                }),
        )
    })
});

Callbacks and Actions

Callbacks are configured on Command, not subscribed from CommandState. They notify the palette owner directly:

Command::new(&state)
    .items(entries)
    .on_query(|query, window, cx| {
        // Start or update an application-owned search.
    })
    .on_select(|index, window, cx| {
        // Preview the newly highlighted IndexPath.
    })
    .on_confirm(|index, window, cx| {
        // Finish with this IndexPath, whether or not it has an Action.
    })
    .on_cancel(|window, cx| {
        // Clean up local palette state before Cancel propagates.
    })

An IndexPath always addresses the model supplied by the latest Command render, before local filtering. Items passed to .items(...) are in section 0, with row equal to their position in that iterator. Explicit groups use their group and item positions; when both forms are mixed, they follow the implicit ungrouped section. Filtering changes what is visible, not these coordinates.

on_query runs only when a searchable query actually changes. Refiltering can move the highlight, so its on_select runs first when the selected IndexPath changes; then on_query runs. These callbacks, and on_confirm, are delivered after the current CommandState update releases its lease. Keyboard and pointer highlight changes run on_select but never dispatch an Action. While the source window remains live, confirming an enabled item dispatches its Action first and then invokes on_confirm; if that Action closes the window, the callback cannot be delivered. An item without an Action still invokes on_confirm. In a searchable palette, Escape clears a non-empty query. Otherwise—including a non-searchable palette with a hidden programmatic query—it invokes on_cancel, then propagates Cancel.

Dynamic Entries

Keep asynchronous or changing entries in the owner view, then reconstruct the Command from the owner's current data when that view renders. Do not mutate the state with an entry builder or set_entries.

struct StockSearch {
    state: Entity<CommandState>,
    results: Vec<CommandItem>,
}

impl StockSearch {
    fn render_palette(&self, owner: WeakEntity<Self>) -> Command {
        let results = self.results.clone();

        Command::new(&self.state)
            .items(results)
            .on_query(move |query, window, cx| {
                _ = owner.update(cx, |this, cx| this.search(query, window, cx));
            })
    }
}

The installed model remains in CommandState while query, selection, and scrolling change, so those interactions do not need an owner rerender. A later owner render installs the new model, preserves the selected IndexPath when it is still present, and remeasures rows.

Searching

Command uses a case-insensitive substring match against each item's label and keywords. Empty queries match every item. A group whose items all filter out hides its heading; a separator left leading, trailing, or adjacent to another separator is omitted.

CommandItem::new().label("Profile")
    .keywords(["account", "user"])

For custom or remote search, update owner-held entries in on_query and call state.set_loading(true, window, cx) while waiting so the empty message is suppressed. Render the new entries when the response arrives.

Custom Rows and Virtualization

CommandItem::child replaces an item's icon and label content with a lazy child factory. The factory can run more than once for measurement, viewport entry, and typography or width invalidation, so it must be side-effect-free.

On invalidation, Command creates and layout-measures every flattened row before supplying independent sizes to v_virtual_list. Custom rows may therefore have different intrinsic heights; v_virtual_list still renders and paints only viewport rows. Build them for the available list width and keep their rendered content stable until the owner updates the entries.

Command::new(&state)
    .item(CommandItem::new().label("compact").child(|_, _| {
        h_flex().w_full().py_1().child("Compact custom row")
    }))
    .item(CommandItem::new().label("expanded").child(|_, cx| {
        v_flex()
            .w_full()
            .py_4()
            .child("Expanded custom row")
            .child(div().text_xs().text_color(cx.theme().muted_foreground).child("Extra detail"))
    }))

Command

Method Signature and description
new new(&Entity<CommandState>) -> Command creates a palette for a state.
item / items item(CommandItem) -> Self and items(impl IntoIterator<Item = CommandItem>) -> Self add ungrouped entries.
group / separator group(CommandGroup) -> Self adds a group; separator() -> Self adds a divider.
searchable searchable(bool) -> Self shows or hides the search field and local filtering. Default: true.
on_query on_query<F>(F) -> Self, where F: Fn(&str, &mut Window, &mut App) + 'static, runs after a searchable query changes.
on_select on_select<F>(F) -> Self, where F: Fn(IndexPath, &mut Window, &mut App) + 'static, runs when the highlighted path changes.
on_confirm on_confirm<F>(F) -> Self, with the same IndexPath callback bounds; while the source window remains live, runs after the confirmed Action dispatches.
on_cancel on_cancel<F>(F) -> Self, where F: Fn(&mut Window, &mut App) + 'static, runs before Cancel propagates when Escape does not clear a searchable query.
placeholder placeholder(impl Into<SharedString>) -> Self sets the search-field placeholder.
empty empty<F, E>(F) -> Self renders custom content when there are no matches.
max_h max_h(impl Into<DefiniteLength>) -> Self sets the list maximum. Default: 18.75rem (300px).
bordered bordered(bool) -> Self draws the surrounding border and rounding. Default: true.
header header<F, E>(F) -> Self, where F: Fn(&CommandState, &mut Window, &mut App) -> E + 'static and E: IntoElement; renders above search and list.
footer footer<F, E>(F) -> Self, with the same callback bounds; renders below the list.

Command implements [Styled], so w, max_w, bg, and other styles apply to the palette frame.

CommandItem

Method Description
new Creates an item; Command generates its internal rendering identity.
label Sets the visible label and default search text.
icon Sets the leading icon for the default row.
action action(Box<dyn Action>) -> Self sets the behavior dispatched on click or confirm. The default row displays its resolved binding.
checked Draws a trailing check. A resolved Action binding uses that position instead.
keywords Adds default-match terms.
disabled Disableable::disabled(bool) -> Self makes the item non-interactive and skips it during keyboard navigation.
child child<F, E>(F) -> Self, where F: Fn(&mut Window, &mut App) -> E + 'static and E: IntoElement; lazily replaces the default row content.

CommandGroup

Method Description
new Creates an unlabeled group.
label Sets the group heading, which hides when all items filter out.
item / items Add one or many CommandItems to the group.
heading Returns the optional heading.

CommandEntry is the public enum for an item, group, or separator. It is useful when an owner stores a mixed dynamic entry collection; replay each variant onto a newly constructed Command during rendering.

CommandState

Method Signature and description
new new(&mut Window, &mut Context<Self>) -> Self creates empty interaction state.
query / set_query Read the query, or set_query(query, window, cx) as if it were typed.
selected_index Returns the highlighted item's original IndexPath; section identifies the top-level entry and row identifies the item within a group.
matched_count Returns the number of matching items.
focus focus(&self, &mut Window, &mut App) focuses the input when searchable, otherwise the Command frame.
set_loading / is_loading Show or read the search spinner; loading suppresses the empty message.

Keyboard Shortcuts

Key Action
↑ / ↓ Move the highlight, wrapping around and skipping disabled items.
Enter Confirm the highlighted item.
Escape In a searchable palette, clear a non-empty query; otherwise call on_cancel and propagate Cancel.

Best Practices

  1. Build static entries, groups, separators, searchability, and filters on Command.
  2. Keep dynamic entries and asynchronous results in the palette owner; rebuild Command from them when rendering.
  3. Bind real Actions instead of supplying shortcut text, so hints and dispatch stay in sync.
  4. Keep child factories side-effect-free and use them for rows that need custom presentation or variable heights.
  5. Let a hosting Dialog own cancellation after on_cancel; use header and footer for application-owned status and hints.
  6. Give each independently rendered palette its own [CommandState].

Documentation license: original prose and illustrations for which GPUI Kit holds licensing rights are also offered under CC BY 4.0. When copying or adapting, credit GPUI Kit, link the source (https://gpui-kit.com/component/command) and https://creativecommons.org/licenses/by/4.0/, and indicate changes. Code examples and software source use Apache-2.0; third-party material retains its terms; existing Apache-2.0 permissions remain.

Bundled from GPUI Kit. Documentation prose: CC BY 4.0; code examples: Apache-2.0. Changes: documentation links localized, asset URLs made absolute, and this attribution added.

Source: SKILL.md on GitHub

No alerts3d3 checks · Risk SAFE
  • Gen Agent Trust Hub3d

    The skill provides comprehensive documentation and reference material for building native desktop applications using Rust and GPUI. No security risks, prompt injections, or malicious behaviors were identified. External dependencies originate from reputable organizations in the Rust ecosystem.

  • Socket3d

    No alerts

  • Snyk3d

    Risk: LOW · No issues

Signed by skilld at 52f4cfd. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 4 days ago

README badge

README badge for lassejlv/skills/build-gpui-apps