feat: add context-aware ask keymaps

This commit is contained in:
Erik Kotsiuba
2026-05-06 12:44:37 +02:00
parent 3672ada13f
commit 808b62cc77
18 changed files with 1158 additions and 444 deletions
+429 -231
View File
@@ -1,4 +1,4 @@
import { Key, matchesKey } from "@mariozechner/pi-tui";
import { matchesKey } from "@mariozechner/pi-tui";
import type { AskConfig, AskConfigKeymaps } from "../config/schema.ts";
const DIGIT_SHORTCUT_PATTERN = /^[1-9]$/;
@@ -37,14 +37,7 @@ const KNOWN_SPECIAL_KEYS = new Set([
"f11",
"f12",
] as const);
const RESERVED_CONFIGURABLE_BINDINGS = new Set([
"?",
"tab",
"shift+tab",
"left",
"right",
"up",
"down",
const RESERVED_BINDINGS = new Set([
"1",
"2",
"3",
@@ -55,39 +48,27 @@ const RESERVED_CONFIGURABLE_BINDINGS = new Set([
"8",
"9",
]);
const ACTION_LABEL_OVERRIDES: Partial<Record<AskKeyBindingId, string>> = {
nextTab: "Tab / →",
previousTab: "Shift+Tab / ←",
previousOption: "↑ / ↓",
nextOption: "↓",
numberShortcut: "1..9",
const CONTEXT_LABELS: Record<AskKeymapContext, string> = {
global: "Global",
main: "Main flow",
editor: "Answer editor",
noteEditor: "Note editor",
settingsModal: "Settings modal",
};
type KeyId = Parameters<typeof matchesKey>[1];
type InputKey = KeyId | (string & {});
type ConfigurableBindingMetadata = Omit<AskKeyBinding, "keys" | "label"> & {
id: AskConfigurableKeyAction;
};
type FixedBindingMetadata = Omit<AskKeyBinding, "keys" | "label"> & {
id: AskFixedKeyBindingId;
keys: readonly InputKey[];
label?: string;
};
type ModifierName = (typeof MODIFIER_ORDER)[number];
export type AskKeymapContext = keyof AskConfigKeymaps;
export type AskKeymapAction<C extends AskKeymapContext = AskKeymapContext> =
keyof AskConfigKeymaps[C] & string;
export type AskKeyBindingKind = "command" | "affordance";
export type AskConfigurableKeyAction = keyof AskConfigKeymaps;
export type AskFixedKeyBindingId =
| "settings"
| "nextTab"
| "previousTab"
| "previousOption"
| "nextOption"
export type AskKeyBindingId =
| `${AskKeymapContext}.${string}`
| "numberShortcut"
| "fileReference";
export type AskKeyBindingId = AskConfigurableKeyAction | AskFixedKeyBindingId;
export interface AskKeyBinding {
contexts: readonly string[];
@@ -106,105 +87,84 @@ export type FooterKeymapContext =
| "note";
export const DEFAULT_ASK_KEYMAPS: AskConfigKeymaps = {
cancel: "esc",
dismiss: "ctrl+c",
toggle: "space",
confirm: "enter",
optionNote: "n",
questionNote: "shift+n",
global: {
dismiss: ["ctrl+c"],
settings: ["?"],
},
main: {
confirm: ["enter"],
cancel: ["esc"],
toggle: ["space"],
nextTab: ["tab", "right"],
previousTab: ["shift+tab", "left"],
nextOption: ["down"],
previousOption: ["up"],
optionNote: ["n"],
questionNote: ["shift+n"],
},
editor: {
submit: ["enter"],
close: ["esc"],
nextTabWhenEmpty: ["tab", "right"],
previousTabWhenEmpty: ["shift+tab", "left"],
nextOptionWhenEmpty: ["down"],
previousOptionWhenEmpty: ["up"],
},
noteEditor: {
save: ["enter"],
close: ["esc"],
nextTabWhenEmpty: ["tab", "right"],
previousTabWhenEmpty: ["shift+tab", "left"],
nextOptionWhenEmpty: ["down"],
previousOptionWhenEmpty: ["up"],
},
settingsModal: {
close: ["esc", "ctrl+c", "?"],
nextOption: ["down"],
previousOption: ["up"],
toggle: ["enter", "space"],
},
};
const CONFIGURABLE_BINDING_METADATA: readonly ConfigurableBindingMetadata[] = [
{
id: "cancel",
kind: "command",
description: "Cancel flow or close/save editor draft",
contexts: ["Main flow", "Editors"],
const DESCRIPTIONS: Record<AskKeymapContext, Record<string, string>> = {
global: {
dismiss: "Dismiss the active ask surface",
settings: "Open ask settings",
},
{
id: "dismiss",
kind: "command",
description: "Dismiss entire ask flow immediately",
contexts: ["Anywhere"],
main: {
confirm: "Confirm selection, continue, or submit",
cancel: "Cancel flow",
toggle: "Toggle selected option",
nextTab: "Switch to next tab",
previousTab: "Switch to previous tab",
nextOption: "Move to next option/action",
previousOption: "Move to previous option/action",
optionNote: "Edit selected option note",
questionNote: "Edit question note",
},
{
id: "toggle",
kind: "command",
description: "Toggle selected option",
contexts: ["Questions"],
editor: {
submit: "Submit custom answer",
close: "Close editor and preserve draft",
nextTabWhenEmpty: "Switch to next tab when editor is empty",
previousTabWhenEmpty: "Switch to previous tab when editor is empty",
nextOptionWhenEmpty: "Move to next option when editor is empty",
previousOptionWhenEmpty: "Move to previous option when editor is empty",
},
{
id: "confirm",
kind: "command",
description: "Confirm selection, continue, save, or submit",
contexts: ["Main flow", "Editors"],
noteEditor: {
save: "Save note",
close: "Close note editor",
nextTabWhenEmpty: "Switch to next tab when editor is empty",
previousTabWhenEmpty: "Switch to previous tab when editor is empty",
nextOptionWhenEmpty: "Move to next option when editor is empty",
previousOptionWhenEmpty: "Move to previous option when editor is empty",
},
{
id: "optionNote",
kind: "command",
description: "Edit selected option note",
contexts: ["Question tabs"],
settingsModal: {
close: "Close settings",
nextOption: "Move to next setting",
previousOption: "Move to previous setting",
toggle: "Toggle selected setting",
},
{
id: "questionNote",
kind: "command",
description: "Edit question note",
contexts: ["Question tabs"],
},
] as const;
const FIXED_BINDING_METADATA: readonly FixedBindingMetadata[] = [
{
id: "settings",
keys: ["?"],
kind: "command",
description: "Open ask settings",
contexts: ["Main flow", "Empty editor"],
},
{
id: "nextTab",
keys: [Key.tab, Key.right],
kind: "command",
description: "Switch to next tab",
contexts: ["Main flow", "Empty editor"],
},
{
id: "previousTab",
keys: [Key.shift("tab"), Key.left],
kind: "command",
description: "Switch to previous tab",
contexts: ["Main flow", "Empty editor"],
},
{
id: "previousOption",
keys: [Key.up],
kind: "command",
description: "Move between options/actions",
contexts: ["Main flow", "Empty editor"],
},
{
id: "nextOption",
keys: [Key.down],
kind: "command",
description: "Move to next option/action",
contexts: ["Main flow", "Empty editor"],
},
{
id: "numberShortcut",
keys: ["1..9"],
kind: "command",
label: "1..9",
description: "Quick-select option or submit action",
contexts: ["Main flow", "Submit tab"],
},
{
id: "fileReference",
keys: ["@"],
kind: "affordance",
description: "Reference files while typing answers or notes",
contexts: ["Editors"],
},
] as const;
};
const footerHint = (
binding: AskKeyBinding,
@@ -213,6 +173,18 @@ const footerHint = (
) => `${label} ${action}`;
export function formatKeybindingLabel(key: string): string {
if (key === "up") {
return "↑";
}
if (key === "down") {
return "↓";
}
if (key === "left") {
return "←";
}
if (key === "right") {
return "→";
}
return key
.split("+")
.map((part) => {
@@ -227,6 +199,10 @@ export function formatKeybindingLabel(key: string): string {
.join("+");
}
export function formatBindingLabel(keys: readonly string[]): string {
return keys.map(formatKeybindingLabel).join(" / ");
}
export function matchesBinding(data: string, binding: AskKeyBinding): boolean {
return binding.keys.some((key) => {
if (key.length === 1) {
@@ -242,33 +218,71 @@ export function matchesDigitShortcut(data: string): number | null {
export function getAskKeyBindings(
config: AskConfig
): Record<AskKeyBindingId, AskKeyBinding> {
const customizable: Record<AskConfigurableKeyAction, AskKeyBinding> = {
cancel: createCustomizableBinding(config, "cancel"),
dismiss: createCustomizableBinding(config, "dismiss"),
toggle: createCustomizableBinding(config, "toggle"),
confirm: createCustomizableBinding(config, "confirm"),
optionNote: createCustomizableBinding(config, "optionNote"),
questionNote: createCustomizableBinding(config, "questionNote"),
};
const fixed = Object.fromEntries(
FIXED_BINDING_METADATA.map((binding) => [
binding.id,
{
...binding,
label:
binding.label ??
ACTION_LABEL_OVERRIDES[binding.id] ??
binding.keys
.map((key) => formatKeybindingLabel(String(key)))
.join(" / "),
},
): Record<string, AskKeyBinding> {
const entries: [string, AskKeyBinding][] = [];
for (const context of Object.keys(config.keymaps) as AskKeymapContext[]) {
const actions = config.keymaps[context] as Record<
string,
readonly string[]
>;
for (const [action, keys] of Object.entries(actions)) {
const id = `${context}.${action}` as AskKeyBindingId;
entries.push([
id,
{
contexts: [CONTEXT_LABELS[context]],
description: DESCRIPTIONS[context][action] ?? action,
id,
keys,
kind: "command",
label: formatBindingLabel(keys),
},
]);
}
}
entries.push([
"numberShortcut",
{
contexts: ["Main flow", "Submit tab"],
description: "Quick-select option or submit action",
id: "numberShortcut",
keys: ["1..9"],
kind: "command",
label: "1..9",
},
]);
entries.push([
"fileReference",
{
contexts: ["Editors"],
description: "Reference files while typing answers or notes",
id: "fileReference",
keys: ["@"],
kind: "affordance",
label: "@",
},
]);
return Object.fromEntries(entries);
}
export function getAskContextBindings<C extends AskKeymapContext>(
config: AskConfig,
context: C
): Record<AskKeymapAction<C>, AskKeyBinding> {
const bindings = getAskKeyBindings(config);
const actions = config.keymaps[context] as Record<string, readonly string[]>;
return Object.fromEntries(
Object.keys(actions).map((action) => [
action,
bindings[`${context}.${action}`],
])
) as Record<AskFixedKeyBindingId, AskKeyBinding>;
return {
...customizable,
...fixed,
};
) as Record<AskKeymapAction<C>, AskKeyBinding>;
}
export function getGlobalBindings(
config: AskConfig
): Record<AskKeymapAction<"global">, AskKeyBinding> {
return getAskContextBindings(config, "global");
}
export function getAskKeymaps(config: AskConfig): {
@@ -277,8 +291,10 @@ export function getAskKeymaps(config: AskConfig): {
} {
const bindings = getAskKeyBindings(config);
return {
customizable: CONFIGURABLE_BINDING_METADATA.map(({ id }) => bindings[id]),
fixed: FIXED_BINDING_METADATA.map(({ id }) => bindings[id]),
customizable: Object.keys(bindings)
.filter((id) => id !== "numberShortcut" && id !== "fileReference")
.map((id) => bindings[id]),
fixed: [bindings.numberShortcut, bindings.fileReference],
};
}
@@ -286,99 +302,260 @@ export function renderFooterKeymaps(
config: AskConfig,
context: FooterKeymapContext
): string {
const global = getGlobalBindings(config);
const main = getAskContextBindings(config, "main");
const editor = getAskContextBindings(config, "editor");
const noteEditor = getAskContextBindings(config, "noteEditor");
const bindings = getAskKeyBindings(config);
const optionNavigationLabel = bindings.previousOption.label.replaceAll(
" / ",
""
);
const optionNavigationLabel =
`${main.previousOption.label}${main.nextOption.label}`.replaceAll(
" / ",
""
);
const tabNavigationLabel = "⇆";
const noteNavigationLabel = `${bindings.optionNote.label}/${bindings.questionNote.label}`;
const noteNavigationLabel = `${main.optionNote.label}/${main.questionNote.label}`;
const hintsByContext: Record<FooterKeymapContext, readonly string[]> = {
input: [
footerHint(bindings.confirm, "submit"),
footerHint(bindings.cancel, "close"),
footerHint(bindings.settings, "settings"),
footerHint(editor.submit, "submit"),
footerHint(editor.close, "close"),
footerHint(global.settings, "settings"),
],
note: [
footerHint(bindings.confirm, "save"),
footerHint(bindings.cancel, "close"),
footerHint(bindings.settings, "settings"),
footerHint(noteEditor.save, "save"),
footerHint(noteEditor.close, "close"),
footerHint(global.settings, "settings"),
],
submit: [
footerHint(bindings.numberShortcut, "hotkeys"),
footerHint(bindings.previousOption, "select", optionNavigationLabel),
footerHint(bindings.confirm, "confirm"),
footerHint(bindings.cancel, "cancel"),
footerHint(bindings.settings, "settings"),
footerHint(main.previousOption, "select", optionNavigationLabel),
footerHint(main.confirm, "confirm"),
footerHint(main.cancel, "cancel"),
footerHint(global.settings, "settings"),
],
multi: [
footerHint(bindings.nextTab, "tab", tabNavigationLabel),
footerHint(bindings.previousOption, "select", optionNavigationLabel),
footerHint(bindings.toggle, "toggle"),
footerHint(bindings.confirm, "continue"),
footerHint(bindings.optionNote, "note", noteNavigationLabel),
footerHint(bindings.cancel, "dismiss"),
footerHint(bindings.settings, "settings"),
footerHint(main.nextTab, "tab", tabNavigationLabel),
footerHint(main.previousOption, "select", optionNavigationLabel),
footerHint(main.toggle, "toggle"),
footerHint(main.confirm, "continue"),
footerHint(main.optionNote, "note", noteNavigationLabel),
footerHint(main.cancel, "dismiss"),
footerHint(global.settings, "settings"),
],
default: [
footerHint(bindings.nextTab, "tab", tabNavigationLabel),
footerHint(bindings.previousOption, "select", optionNavigationLabel),
footerHint(bindings.confirm, "confirm"),
footerHint(bindings.optionNote, "note", noteNavigationLabel),
footerHint(bindings.cancel, "dismiss"),
footerHint(bindings.settings, "settings"),
footerHint(main.nextTab, "tab", tabNavigationLabel),
footerHint(main.previousOption, "select", optionNavigationLabel),
footerHint(main.confirm, "confirm"),
footerHint(main.optionNote, "note", noteNavigationLabel),
footerHint(main.cancel, "dismiss"),
footerHint(global.settings, "settings"),
],
};
return ` ${hintsByContext[context].join(" · ")}`;
}
export function renderSettingsFooterKeymaps(config: AskConfig): string {
const bindings = getAskContextBindings(config, "settingsModal");
return `${bindings.toggle.label} to change · ${bindings.close.label} to close`;
}
export function normalizeConfiguredKeymaps(
keymaps: Partial<Record<AskConfigurableKeyAction, unknown>> | undefined
keymaps: unknown
): { ok: true; keymaps: AskConfigKeymaps } | { ok: false; error: string } {
if (!keymaps) {
return { ok: true, keymaps: { ...DEFAULT_ASK_KEYMAPS } };
const candidate = normalizeLegacyFlatKeymaps(keymaps) ?? keymaps;
if (!candidate) {
return { ok: true, keymaps: cloneKeymaps(DEFAULT_ASK_KEYMAPS) };
}
if (!(candidate && typeof candidate === "object")) {
return { ok: false, error: "Keymaps must be an object." };
}
const normalized = {} as AskConfigKeymaps;
for (const action of Object.keys(
for (const context of Object.keys(
DEFAULT_ASK_KEYMAPS
) as AskConfigurableKeyAction[]) {
const rawValue = keymaps[action];
if (typeof rawValue !== "string") {
return { ok: false, error: `Missing keymap for ${action}.` };
) as AskKeymapContext[]) {
const rawContext = (candidate as Record<string, unknown>)[context];
if (!(rawContext && typeof rawContext === "object")) {
return { ok: false, error: `Missing keymap context ${context}.` };
}
const parsed = normalizeKeyId(rawValue);
if (!parsed.ok) {
return {
ok: false,
error: `Invalid keymap for ${action}: ${parsed.error}`,
};
const contextResult = normalizeContextKeymaps(
context,
rawContext as Record<string, unknown>
);
if (!contextResult.ok) {
return contextResult;
}
if (RESERVED_CONFIGURABLE_BINDINGS.has(parsed.keyId)) {
return {
ok: false,
error: `Reserved keymap for ${action}: ${formatKeybindingLabel(parsed.keyId)}.`,
};
}
normalized[action] = parsed.keyId;
normalized[context] = contextResult.keymaps as never;
}
const seen = new Map<string, AskConfigurableKeyAction>();
for (const action of Object.keys(normalized) as AskConfigurableKeyAction[]) {
const key = normalized[action];
const existing = seen.get(key);
if (existing) {
return {
ok: false,
error: `Duplicate keymap ${formatKeybindingLabel(key)} for ${existing} and ${action}.`,
};
}
seen.set(key, action);
const globalDuplicates = validateCrossContextConflicts(normalized);
if (globalDuplicates) {
return { ok: false, error: globalDuplicates };
}
return { ok: true, keymaps: normalized };
}
function normalizeContextKeymaps(
context: AskKeymapContext,
rawContext: Record<string, unknown>
):
| { ok: true; keymaps: Record<string, string[]> }
| { ok: false; error: string } {
const defaults = DEFAULT_ASK_KEYMAPS[context] as Record<
string,
readonly string[]
>;
const normalized: Record<string, string[]> = {};
for (const action of Object.keys(defaults)) {
const rawValue = rawContext[action];
if (rawValue === undefined) {
return { ok: false, error: `Missing keymap for ${context}.${action}.` };
}
const parsed = normalizeBindingList(rawValue);
if (!parsed.ok) {
return {
ok: false,
error: `Invalid keymap for ${context}.${action}: ${parsed.error}`,
};
}
normalized[action] = parsed.keys;
}
const duplicateError = validateContextBindings(context, normalized);
return duplicateError
? { ok: false, error: duplicateError }
: { ok: true, keymaps: normalized };
}
function normalizeBindingList(
rawValue: unknown
): { ok: true; keys: string[] } | { ok: false; error: string } {
const rawKeys = typeof rawValue === "string" ? [rawValue] : rawValue;
if (!Array.isArray(rawKeys)) {
return { ok: false, error: "binding must be a string or array of strings" };
}
if (rawKeys.length === 0) {
return { ok: false, error: "empty binding list" };
}
const keys: string[] = [];
for (const rawKey of rawKeys) {
if (typeof rawKey !== "string") {
return { ok: false, error: "binding list must contain only strings" };
}
const parsed = normalizeKeyId(rawKey);
if (!parsed.ok) {
return parsed;
}
if (!keys.includes(parsed.keyId)) {
keys.push(parsed.keyId);
}
}
return { ok: true, keys };
}
function validateContextBindings(
context: AskKeymapContext,
bindings: Record<string, readonly string[]>
): string | undefined {
const seen = new Map<string, string>();
for (const { action, key } of flattenBindings(bindings)) {
if (RESERVED_BINDINGS.has(key)) {
return `Reserved keymap for ${context}.${action}: ${formatKeybindingLabel(key)}.`;
}
const existing = seen.get(key);
if (existing) {
return `Duplicate keymap ${formatKeybindingLabel(key)} for ${context}.${existing} and ${context}.${action}.`;
}
seen.set(key, action);
}
return;
}
function validateCrossContextConflicts(
keymaps: AskConfigKeymaps
): string | undefined {
const globalSeen = new Map(
flattenBindings(keymaps.global).map(({ action, key }) => [
key,
`global.${action}`,
])
);
for (const { context, action, key } of flattenNonGlobalBindings(keymaps)) {
const existing = globalSeen.get(key);
if (existing) {
return `Duplicate global keymap ${formatKeybindingLabel(key)} for ${existing} and ${context}.${action}.`;
}
}
return;
}
function flattenNonGlobalBindings(keymaps: AskConfigKeymaps): Array<{
action: string;
context: AskKeymapContext;
key: string;
}> {
return (Object.keys(keymaps) as AskKeymapContext[])
.filter((context) => context !== "global" && context !== "settingsModal")
.flatMap((context) =>
flattenBindings(
keymaps[context] as Record<string, readonly string[]>
).map((binding) => ({ ...binding, context }))
);
}
function flattenBindings(
bindings: Record<string, readonly string[]>
): Array<{ action: string; key: string }> {
return Object.entries(bindings).flatMap(([action, keys]) =>
keys.map((key) => ({ action, key }))
);
}
export function normalizeLegacyFlatKeymaps(
keymaps: unknown
): AskConfigKeymaps | undefined {
if (!(keymaps && typeof keymaps === "object")) {
return;
}
if ("main" in keymaps || "global" in keymaps) {
return;
}
const raw = keymaps as Record<string, unknown>;
const defaults = cloneKeymaps(DEFAULT_ASK_KEYMAPS);
return {
...defaults,
global: {
...defaults.global,
dismiss: coerceLegacyBinding(raw.dismiss, defaults.global.dismiss),
},
main: {
...defaults.main,
confirm: coerceLegacyBinding(raw.confirm, defaults.main.confirm),
cancel: coerceLegacyBinding(raw.cancel, defaults.main.cancel),
toggle: coerceLegacyBinding(raw.toggle, defaults.main.toggle),
optionNote: coerceLegacyBinding(raw.optionNote, defaults.main.optionNote),
questionNote: coerceLegacyBinding(
raw.questionNote,
defaults.main.questionNote
),
},
editor: {
...defaults.editor,
submit: coerceLegacyBinding(raw.confirm, defaults.editor.submit),
close: coerceLegacyBinding(raw.cancel, defaults.editor.close),
},
noteEditor: {
...defaults.noteEditor,
save: coerceLegacyBinding(raw.confirm, defaults.noteEditor.save),
close: coerceLegacyBinding(raw.cancel, defaults.noteEditor.close),
},
};
}
function coerceLegacyBinding(value: unknown, fallback: string[]): string[] {
return typeof value === "string" ? [value] : fallback;
}
function normalizeKeyId(
rawKey: string
): { ok: true; keyId: string } | { ok: false; error: string } {
@@ -418,27 +595,48 @@ function normalizeKeyId(
const modifierPrefix = MODIFIER_ORDER.filter((modifier) =>
modifiers.has(modifier)
);
return {
ok: true,
keyId: [...modifierPrefix, baseKey].join("+"),
};
return { ok: true, keyId: [...modifierPrefix, baseKey].join("+") };
}
function createCustomizableBinding(
config: AskConfig,
id: AskConfigurableKeyAction
): AskKeyBinding {
const binding = CONFIGURABLE_BINDING_METADATA.find(
(entry) => entry.id === id
);
if (!binding) {
throw new Error(`Unknown customizable ask key binding: ${id}`);
}
const key = config.keymaps[id];
function cloneKeymaps(keymaps: AskConfigKeymaps): AskConfigKeymaps {
return {
...binding,
keys: [key],
label: formatKeybindingLabel(key),
global: {
dismiss: [...keymaps.global.dismiss],
settings: [...keymaps.global.settings],
},
main: {
confirm: [...keymaps.main.confirm],
cancel: [...keymaps.main.cancel],
toggle: [...keymaps.main.toggle],
nextTab: [...keymaps.main.nextTab],
previousTab: [...keymaps.main.previousTab],
nextOption: [...keymaps.main.nextOption],
previousOption: [...keymaps.main.previousOption],
optionNote: [...keymaps.main.optionNote],
questionNote: [...keymaps.main.questionNote],
},
editor: {
submit: [...keymaps.editor.submit],
close: [...keymaps.editor.close],
nextTabWhenEmpty: [...keymaps.editor.nextTabWhenEmpty],
previousTabWhenEmpty: [...keymaps.editor.previousTabWhenEmpty],
nextOptionWhenEmpty: [...keymaps.editor.nextOptionWhenEmpty],
previousOptionWhenEmpty: [...keymaps.editor.previousOptionWhenEmpty],
},
noteEditor: {
save: [...keymaps.noteEditor.save],
close: [...keymaps.noteEditor.close],
nextTabWhenEmpty: [...keymaps.noteEditor.nextTabWhenEmpty],
previousTabWhenEmpty: [...keymaps.noteEditor.previousTabWhenEmpty],
nextOptionWhenEmpty: [...keymaps.noteEditor.nextOptionWhenEmpty],
previousOptionWhenEmpty: [...keymaps.noteEditor.previousOptionWhenEmpty],
},
settingsModal: {
close: [...keymaps.settingsModal.close],
nextOption: [...keymaps.settingsModal.nextOption],
previousOption: [...keymaps.settingsModal.previousOption],
toggle: [...keymaps.settingsModal.toggle],
},
};
}