Skip to content

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.

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:

You write · editable
class _CommandIntroState extends State<_CommandIntro> {
static const _initialText = 'Hello, Fleury!';
final _document = TextEditingController(text: _initialText);
String _savedText = _initialText;
int _saveCount = 0;
bool get _isDirty => _document.text != _savedText;
@override
void dispose() {
_document.dispose();
super.dispose();
}
void _save() {
setState(() {
_savedText = _document.text;
_saveCount += 1;
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final saveCommand = AppCommand(
id: _saveCurrentFileId,
title: 'Save current file',
description: 'Save the document being edited',
category: 'File',
shortcuts: <KeySequence>[KeySequence.ctrl.s],
enabled: (_) => _isDirty,
semanticAction: SemanticAction.submit,
run: (_) => _save(),
);
final openCommands = AppCommand(
id: _openCommandsId,
title: 'Open commands',
description: 'Search commands available in the editor',
category: 'Application',
shortcuts: <KeySequence>[KeySequence.ctrl.k],
showInPalette: false,
semanticAction: SemanticAction.open,
run: (command) {
final source = command.buildContext;
if (source != null) {
unawaited(CommandPalette.open(source, width: 46, maxVisible: 5));
}
},
);
return CommandScope(
label: 'Editor commands',
commands: <AppCommand>[openCommands, saveCommand],
child: Padding(
padding: const EdgeInsets.all(1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'ONE COMMAND · EVERY ENTRY POINT',
style: CellStyle(
foreground: theme.colorScheme.primary,
bold: true,
),
),
const Text(
'Save current file · editor.save',
style: CellStyle(dim: true),
),
const SizedBox(height: 1),
SizedBox(
height: 3,
child: TextArea(
controller: _document,
autofocus: true,
semanticLabel: 'Intro document',
onChanged: (_) => setState(() {}),
minLines: 3,
maxLines: 3,
),
),
const SizedBox(height: 1),
Row(
children: <Widget>[
const CommandButton(
command: _saveCurrentFileId,
label: 'Save',
variant: ButtonVariant.primary,
),
const SizedBox(width: 2),
const Text('Ctrl+S', style: CellStyle(dim: true)),
const Spacer(),
const Text('Ctrl+K ', style: CellStyle(dim: true)),
const CommandButton(
command: _openCommandsId,
label: 'Commands',
),
],
),
Align(
alignment: Alignment.centerRight,
child: Text(
_isDirty ? 'UNSAVED' : 'SAVED · $_saveCount',
style: CellStyle(
foreground: _isDirty
? theme.colorScheme.warning
: theme.colorScheme.success,
bold: true,
),
),
),
],
),
),
);
}
}
Live preview
Use one Save command from a button, Ctrl+S, or the command palette
Live · interactive
click & type to interact ⓘ how this demo runs

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.

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.

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 enabled returns 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 visible returns false, the command leaves its scope: no button, palette row, semantics, or shortcut, and invoking it reports that it wasn’t found.
  • availability is a Listenable that 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.

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.

Open Ctrl+K to discover and invoke the editor's active commands
Live · interactive
click & type to interact ⓘ how this demo runs

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.

You need to…Use
Run one callback from one controlButton
Handle keyboard-only interactionKeyBinding
Share one discoverable action across several surfacesAppCommand
Show a fixed palette of unrelated callbacksCommandPaletteItem

A command callback may be synchronous or asynchronous, but loading and progress remain ordinary application state.

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.