Skip to main content

Commands

A key a view or an application reacts to belongs in a command, not in a switch. That is what makes it visible to the palette, to the hints box, to the keys screen and to the conflict check — and what lets it be relabeled and rebound without touching the screen.

An application command

public sealed class QuitCommand : IArlecchinoCommand
{
private readonly IHostApplicationLifetime _lifetime;

public QuitCommand(IHostApplicationLifetime lifetime) => _lifetime = lifetime;

public KeyBinding Binding => new(ConsoleKey.Q);
public string Icon => "×";
public string Label => "Quit";

public ViewRoute Execute()
{
_lifetime.StopApplication();
return ViewRoute.None;
}
}

Nothing registers it by hand: .AddGeneratedCommands() picks up every IArlecchinoCommand in the project — see Source generator. .AddCommand<QuitCommand>() is there for a command that comes from another assembly. Either way commands are singletons resolved from the container, so they can take any service — application state, the navigator, ArlecchinoState.

Execute returns a route: navigate by returning one, stay put with ViewRoute.None. Icon and Label are yours to render; the palette shows the binding and the label.

A plain letter does not fire globally

A binding that carries a modifier — new(ConsoleKey.S, KeyModifiers.Control) — fires before the key reaches the view. A plain letter does not: it would swallow typing. It stays reachable through the palette and through whatever the view does with it.

Commands of a view

private Mod? _selected;

public IReadOnlyList<ViewCommand> Commands() =>
[
ViewCommand.For(ConsoleKey.N, () => Loc(LocString.Rename), Rename),
ViewCommand.Navigating(ConsoleKey.S, () => Loc(LocString.Settings), () => ViewKind.Settings),
new()
{
Binding = new KeyBinding(ConsoleKey.D, KeyModifiers.Control),
Label = () => Loc(LocString.Delete),
IsEnabled = () => _selected is not null,
Run = () => Delete(),
},
];
MemberMeaning
BindingThe key
LabelA delegate, so the text follows the current language
IsEnabledOptional; a disabled command is grayed in the palette and lets its key through
RunReturns a route, so a command can navigate
ViewCommand.For(key, label, action)Wraps an Action for a command that stays put
ViewCommand.Navigating(key, label, route)Wraps a command whose whole job is to navigate

A disabled command is skipped, and its key carries on to the commands available everywhere and then to the view's own Handle, as if nothing had claimed it. IsEnabled says the command is unavailable, not that the key is spoken for: a view that binds Esc to "stop what is running" still wants Esc to leave a search while nothing is running. To claim the key regardless, bind it and do nothing rather than disable it.

This changed in 4.0

Until 4.0 a disabled command swallowed its key. See Migrating to 4.0.

Hints() is optional for a view with commands: when it returns nothing, the hints box is built from the command list, so a rebound key relabels itself there too.

A screen of unlike panes lists different keys depending on where the cursor is, which the box does by itself once the view says what holds the focus:

public IArlecchinoFocusable Focus => _ring;

The keys of the focused element come first — an element states its own through IArlecchinoFocusable.Hints — and the screen's keys after them, minus any the element already claimed, so one key is never listed twice under two labels. A view that leaves Focus alone gets the box it always had.

Whether the box is drawn at all is ArlecchinoOptions.Hints: Always, WhileWaiting for nothing but a half-typed chord, or Never for an application that draws the keys in its own shape from CommandKeys.IsWaiting and CommandKeys.Hints().

The box also offers the palette itself — : → commands with the default keymap — whenever at least one command is registered, which is the same condition under which the key does anything at all. The key shown is CommandPaletteKey, so rebinding it relabels the line, and the wording is ArlecchinoStrings.HintCommands. A view with no hints of its own gets a box with that one line.

Conflicts are reported

A view command shadows an application command on the same key. That is by design, and it is said out loud: when the route is first shown, CommandConflicts logs a warning naming both the view command and the application command it hides, and another one if the view binds the same key twice.

That is exactly the case that used to hide silently — a Pick a folder command and a password field both on p.

The command palette

Pressing : opens a modal listing the commands of the current view first, then the application commands, as key label. The next key either runs the matching command or, if nothing matches, closes the palette and writes unknown command: <key> to the output line. A click runs the command on that row. Esc and Enter close it silently.

Change the key with options.CommandPaletteKey = '/'. The palette does not open while no command is registered, which leaves the key free for views to handle.

Driving the registry yourself

CommandRegistry is a service, for a view that wants to list or invoke commands itself:

MemberMeaning
CommandsThe registered set, in registration order
TryFind(key, out command)Looks one up
Send(key)Executes the match and returns its route

That is what a custom launcher screen or a toolbar drawn from commands is built on.