Commands & shortcuts
Use a command when the same action should appear in several places, or when the available actions need to be discoverable. For a single control, call its callback directly.
Define Save, New file, or Run tests once. Fleury can then expose the action through buttons, keyboard shortcuts, command palettes, semantics, tests, and agents.
One action, everywhere
Section titled “One action, everywhere”An AppCommand ties together the pieces every entry point needs: a stable ID,
user-facing title, availability, shortcuts, and one callback. Define Save
current file once, and every surface reads the same definition:
Edit the document. Save, Ctrl+S, and the Ctrl+K palette all reach
_save(). The same enabled rule disables every surface after saving, while
the command’s ID remains stable for semantics,
tests, and agents.
CommandButton is an ordinary Fleury button backed by a command ID. It reads
the current title, availability, and callback from the active registry instead
of repeating them at the control.
Put commands in context
Section titled “Put commands in context”Register commands where they are relevant:
FleuryApp( title: 'Editor', commands: [openCommandsCommand()], home: CommandScope( commands: [newFileCommand(), saveFileCommand()], child: const EditorScreen(), ),)Commands passed to FleuryApp remain available across the app. A
CommandScope adds actions for its subtree and installs their shortcuts.
Editor commands disappear when that scope leaves the tree.
The palette itself is opened by a command. Fleury doesn’t reserve a palette shortcut, so choose one:
AppCommand openCommandsCommand() => AppCommand( id: const CommandId('app.commands'), title: 'Open commands', shortcuts: [KeySequence.ctrl.k], showInPalette: false, run: (invocation) { final context = invocation.buildContext; if (context != null) unawaited(CommandPalette.open(context)); },);invocation.buildContext is where the command ran. For a shortcut on a command
registered with FleuryApp, that is the focused widget, so a palette opened
there with CommandPalette.open lists every command active at that point:
inside the editor, both the app’s commands and the editor’s.
This is the main value of the model: the interface can ask which actions are available here instead of every button, shortcut, palette, test, or agent maintaining its own catalog.
Decide where a command applies
Section titled “Decide where a command applies”A command’s enabled and visible predicates decide where it applies, and
availability says when to ask them again. Here document is a Notifier
that notifies whenever it changes:
AppCommand saveFileCommand(EditorDocument document) => AppCommand( id: const CommandId('editor.save'), title: 'Save current file', shortcuts: [KeySequence.ctrl.s], enabled: (_) => document.isDirty, visible: (_) => !document.isReadOnly, availability: document, run: (_) => document.save(),);- While
enabledreturns false, the command stays listed but can’t run: its buttons and palette row are disabled, invoking it returns a disabled result, and its shortcut passes the key on to outer bindings. - While
visiblereturns false, the command leaves its scope: no button, palette row, semantics, or shortcut, and invoking it reports that it wasn’t found. availabilityis aListenablethat notifies when either answer may change. Fleury then refreshes the command’s shortcut hints and semantics when it notifies, instead of asking the predicates on every frame. Keys, clicks, and invocations always ask the predicates.
The palette opener above uses a fourth switch: showInPalette: false keeps a
command and its shortcut active but out of palettes.
Discover commands with a palette
Section titled “Discover commands with a palette”A command palette turns the active command registry into searchable UI. This editor defines New file and Save current file once; Ctrl+K discovers both without maintaining a second list.
The palette opens with the editor’s active actions. Invoke New file, or edit the document and reopen Ctrl+K to run Save current file. Palette rows, buttons, and shortcuts always share the same command and enabled state.
The complete deterministic example is
packages/samples/lib/src/commands_showcase.dart.
Choose the smallest tool
Section titled “Choose the smallest tool”| You need to… | Use |
|---|---|
| Run one callback from one control | Button |
| Handle keyboard-only interaction | KeyBinding |
| Share one discoverable action across several surfaces | AppCommand |
| Show a fixed palette of unrelated callbacks | CommandPaletteItem |
A command callback may be synchronous or asynchronous, but loading and progress remain ordinary application state.
Invoke commands from tests and agents
Section titled “Invoke commands from tests and agents”A test can run a command by its stable CommandId, without finding a button or
pressing a shortcut. This test from the
Testing guide saves
a draft through the editor’s editor.save command:
testWidgets('saves the draft by its command ID', (tester) async { String? saved; tester.pumpWidget( FleuryApp( title: 'Draft editor', home: DraftEditor(save: (text) async => saved = text), ), ); await tester.field('Draft').fill('Ready for review.');
final result = await tester.invokeCommand(const CommandId('editor.save')); expect(result.completed, isTrue); expect(saved, 'Ready for review.');
final again = await tester.invokeCommand(const CommandId('editor.save')); expect(again.status, CommandInvocationStatus.disabled);});invokeCommand waits for the command to finish. The result’s completed is
false when the command was disabled, missing, or threw, and status says which.
An agent sees each active command as a semantic node with the role command,
the command’s title as its label, and its ID in state.commandId. Through
fleury_mcp, find_nodes with the role command lists them, and
invoke_action with activate runs one; see
Driving with an agent.
Next steps
Section titled “Next steps”- Key handling for keyboard-only input.
- Testing for semantic and interaction tests.
CommandPalettefor palette behavior.