Keyboard
App-wide keyboard shortcuts as one-liners: register any physical key with optional Ctrl, Alt, Shift and Meta modifiers and get a C# callback no matter where focus is. Shortcuts match on keydown by the physical key code, so they work identically across keyboard layouts.
@inject Bit.Butil.Keyboard keyboardMDN reference
Add attaches a global keydown shortcut and returns a Guid you can later pass to Remove. By default the browser action is suppressed (preventDefault and stopPropagation are true) and held keys do not auto-repeat. Two shortcuts are live on this page right now.
Bit.Butil.Keyboard keyboard
{
private Guid ctrlKId;
protected override async Task OnInitializedAsync()
{
ctrlKId = await keyboard.Add(ButilKeyCodes.KeyK, () =>
{
// open your command palette...
}, ButilModifiers.Ctrl);
await keyboard.Add(ButilKeyCodes.F9, () => { /* ... */ });
}
}ButilModifiers is a flags enum - combine Ctrl, Alt, Shift and Meta with the bitwise or operator. Build a shortcut below and register it live; press it to see it fire, and register a new one to replace it.
await keyboard.Add(ButilKeyCodes.KeyU,
() => { /* ... */ },
ButilModifiers.Ctrl | ButilModifiers.Shift);Subscribe is Add with lifecycle sugar: it returns a ButilSubscription whose DisposeAsync detaches the shortcut, so a component can await-using a shortcut or dispose it in DisposeAsync without tracking Guids.
private ButilSubscription? shortcut;
shortcut = await keyboard.Subscribe(ButilKeyCodes.KeyB, () =>
{
// toggle the sidebar...
}, ButilModifiers.Ctrl);
// later:
await shortcut.DisposeAsync();SubscribeOn binds the shortcut to a specific element: the handler only fires while that element (or one of its descendants) receives the keyboard event. Perfect for editor areas that need their own key handling without hijacking the whole page.
<textarea @ref="editorRef"></textarea>
{
private ElementReference editorRef;
private ButilSubscription? editorShortcut;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
editorShortcut = await keyboard.SubscribeOn(editorRef,
ButilKeyCodes.Enter, () => { /* submit */ }, ButilModifiers.Ctrl);
}
}Remove(Guid) detaches one shortcut by the id Add returned. Remove(Action) matches by delegate identity and returns the removed ids. RemoveAll wipes every shortcut registered through this Keyboard instance.
var id = await keyboard.Add(ButilKeyCodes.F9, handler);
await keyboard.Remove(id); // by id (recommended)
await keyboard.Remove(handler); // by the exact same delegate instance
await keyboard.RemoveAll(); // everything on this Keyboard instanceButilKeyCodes provides string constants for every physical key: letters (KeyA-KeyZ), digits (Digit0-Digit9), the numpad, function keys (F1-F12), navigation, editing and symbol keys. They are plain KeyboardEvent.code values, so any valid code string works too.
ButilKeyCodes.KeyK // "KeyK"
ButilKeyCodes.Digit1 // "Digit1"
ButilKeyCodes.F9 // "F9"
ButilKeyCodes.Enter // "Enter"
ButilKeyCodes.Escape // "Escape"
ButilKeyCodes.ArrowDown // "ArrowDown"
ButilKeyCodes.NumpadAdd // "NumpadAdd"
ButilKeyCodes.Backquote // "Backquote"Keyboard service is scoped to the whole app instance, so RemoveAll detaches
every shortcut any component registered on it. In a component, prefer removing your own ids - or use
Subscribe and dispose the returned handles - as this page does in DisposeAsync.
KeyboardEvent.code - the physical key - so ButilKeyCodes.KeyK
fires for that key position regardless of the active keyboard layout, and shortcuts keep working while
Caps Lock or Shift change the produced character.
API reference
Task<Guid> Add(string code, Action handler, ButilModifiers modifiers = ButilModifiers.None, bool preventDefault = true, bool stopPropagation = true, bool repeat = false)Task<ButilSubscription> Subscribe(string code, Action handler, ButilModifiers modifiers = ButilModifiers.None, bool preventDefault = true, bool stopPropagation = true, bool repeat = false)Task<ButilSubscription> SubscribeOn(ElementReference element, string code, Action handler, ButilModifiers modifiers = ButilModifiers.None, bool preventDefault = true, bool stopPropagation = true, bool repeat = false)ValueTask<Guid[]> Remove(Action handler)ValueTask Remove(Guid id)ValueTask RemoveAll()ValueTask DisposeAsync()void InvokeKeyboard(Guid id)enum ButilModifiers { None, Alt, Ctrl, Meta, Shift }