import { ChevronDownIcon, CircleXIcon, EllipsisIcon, FileJsonIcon, MinusIcon, PlusIcon, SearchIcon, TriangleAlertIcon, XIcon, } from "lucide-react"; import { type KeyboardEvent, type ReactNode, type RefObject, useCallback, useEffect, useMemo, useReducer, useRef, useState, } from "react"; import { type KeybindingCommand, type KeybindingWhenNode, type ServerRemoveKeybindingInput, type ServerUpsertKeybindingInput, } from "@t3tools/contracts"; import { useAtomValue } from "@effect/atom-react"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { isElectron } from "../../env"; import { useOpenInPreferredEditor } from "../../editorPreferences"; import { formatShortcutLabel } from "../../keybindings"; import { cn } from "../../lib/utils"; import { primaryServerAvailableEditorsAtom, primaryServerKeybindingsAtom, primaryServerKeybindingsConfigPathAtom, serverEnvironment, } from "../../state/server"; import { usePrimaryEnvironment } from "../../state/environments"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Kbd, KbdGroup } from "../ui/kbd"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { Toggle } from "../ui/toggle"; import { toastManager } from "../ui/toast"; import { buildKeybindingRows, buildKeybindingCommandOptions, buildWhenVariableOptions, commandLabel, DEFAULT_WHEN_VARIABLE, isKnownWhenVariable, keybindingConflictLabels, keybindingFromKeyboardEvent, parseWhenExpressionDraft, type KeybindingCommandOption, type KeybindingRow, type WhenVariableOption, unknownWhenVariables, whenAstToExpression, whenNodeRemoveLabel, } from "./KeybindingsSettings.logic"; import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout"; import { searchableSetting } from "./settingsSearch"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { useAtomCommand } from "../../state/use-atom-command"; function KeybindingPill({ value }: { value: string }) { // Keys dedupe repeated parts; a literal "+" in a shortcut splits into empty strings. const seenParts = new Map(); const parts = value.split("+").map((part) => { const seen = seenParts.get(part) ?? 0; seenParts.set(part, seen + 1); return { part, key: seen === 0 ? part : `${part}-${seen}` }; }); return ( {parts.map(({ part, key }) => ( {part === "mod" ? navigator.platform.toLowerCase().includes("mac") ? "⌘" : "Ctrl" : part === "shift" ? "⇧" : part === "alt" ? navigator.platform.toLowerCase().includes("mac") ? "⌥" : "Alt" : part === "ctrl" ? "⌃" : part.length === 1 ? part.toUpperCase() : part} ))} ); } function ExpandableHeaderSearch({ query, onChange, isOpen, onOpenChange, inputRef, collapsedAccessory, }: { query: string; onChange: (next: string) => void; isOpen: boolean; onOpenChange: (next: boolean) => void; inputRef?: RefObject; collapsedAccessory?: ReactNode; }) { if (!isOpen) { return ( <> {collapsedAccessory} onOpenChange(true)} aria-label="Search keybindings" > } /> Search keybindings ); } return (
onChange(event.currentTarget.value)} onBlur={() => { if (query.length === 0) onOpenChange(false); }} onKeyDown={(event) => { if (event.key === "Escape") { event.preventDefault(); onChange(""); onOpenChange(false); } }} placeholder="Search keybindings" aria-label="Search keybindings" className="w-44 [&_[data-slot=input]]:pl-7" size="sm" />
); } type BooleanOperator = "and" | "or"; function flattenWhenChildren( node: KeybindingWhenNode, operator: BooleanOperator, ): KeybindingWhenNode[] { if (node.type !== operator) return [node]; return [ ...flattenWhenChildren(node.left, operator), ...flattenWhenChildren(node.right, operator), ]; } function buildWhenExpressionGroup( children: readonly KeybindingWhenNode[], operator: BooleanOperator, ): KeybindingWhenNode | undefined { const first = children[0]; if (!first) return undefined; return children.slice(1).reduce( (left, right) => ({ type: operator, left, right, }), first, ); } function conditionParts(node: KeybindingWhenNode): { identifier: string; negated: boolean } | null { if (node.type === "identifier") return { identifier: node.name, negated: false }; if (node.type === "not" && node.node.type === "identifier") { return { identifier: node.node.name, negated: true }; } return null; } function setConditionIdentifier(node: KeybindingWhenNode, identifier: string): KeybindingWhenNode { const parts = conditionParts(node); if (!parts) return node; const next: KeybindingWhenNode = { type: "identifier", name: identifier }; return parts.negated ? { type: "not", node: next } : next; } function setConditionNegated(node: KeybindingWhenNode, negated: boolean): KeybindingWhenNode { const parts = conditionParts(node); if (!parts) return negated ? { type: "not", node } : node; const identifier: KeybindingWhenNode = { type: "identifier", name: parts.identifier }; return negated ? { type: "not", node: identifier } : identifier; } function defaultWhenCondition(): KeybindingWhenNode { return { type: "identifier", name: DEFAULT_WHEN_VARIABLE }; } function defaultWhenGroup(operator: BooleanOperator = "and"): KeybindingWhenNode { return { type: operator, left: defaultWhenCondition(), right: { type: "not", node: defaultWhenCondition() }, }; } /** Warning glyph whose explanation lives in a tooltip; the one owner of that affordance here. */ function WarningTooltipIcon({ label, focusable = true, className, children, }: { label: string; focusable?: boolean; className?: string | undefined; children: ReactNode; }) { return ( } > {children} ); } function UnknownWhenVariableWarning({ identifiers, focusable = true, }: { identifiers: ReadonlyArray; focusable?: boolean; }) { if (identifiers.length === 0) return null; const label = identifiers.length === 1 ? `Unknown condition: ${identifiers[0]}` : `Unknown conditions: ${identifiers.join(", ")}`; return ( T3 Code does not recognize this condition yet. It can still be saved, but it may not match unless the runtime provides it. ); } function KeybindingConflictWarning({ labels }: { labels: ReadonlyArray }) { if (labels.length === 0) return null; const description = labels.length === 1 ? `Conflicts with ${labels[0]}.` : `Conflicts with ${labels.slice(0, 3).join(", ")}${labels.length > 3 ? ", and more" : ""}.`; return ( {description} The most recent matching binding wins when both conditions can apply. ); } function WhenVariableSelect({ value, variables, unknownIdentifiers, onChange, }: { value: string; variables: ReadonlyArray; unknownIdentifiers?: ReadonlyArray; onChange: (value: string) => void; }) { const selected = variables.find((option) => option === value); const options = selected || variables.some((option) => option === value) ? variables : [value, ...variables]; return ( ); } function WhenExpressionRemoveButton({ label, className, onRemove, }: { label: string; className?: string | undefined; onRemove: () => void; }) { return ( } > {label} ); } function WhenExpressionNodeEditor({ node, variables, depth = 0, onChange, onRemove, }: { node: KeybindingWhenNode; variables: ReadonlyArray; depth?: number; onChange: (node: KeybindingWhenNode) => void; onRemove?: () => void; }) { const condition = conditionParts(node); if (condition) { const unknownIdentifiers = isKnownWhenVariable(condition.identifier) ? [] : [condition.identifier]; return (
onChange(setConditionNegated(node, pressed))} aria-label={`Negate ${condition.identifier}`} variant="outline" size="compact" className="min-w-10" > Not onChange(setConditionIdentifier(node, value))} /> {onRemove ? ( ) : null}
); } if (node.type === "not") { return (
0 && "border-border/50 bg-background/50", )} >
onChange(pressed ? node : node.node)} aria-label="Negate group" variant="outline" size="compact" className="min-w-10" > Not {onRemove ? ( ) : null}
onChange({ type: "not", node: next })} />
); } const operator: BooleanOperator = node.type === "or" ? "or" : "and"; const children = flattenWhenChildren(node, operator); const childKeyCounts = new Map(); const childEntries = children.map((child) => { const baseKey = `${child.type}-${whenAstToExpression(child)}`; const count = childKeyCounts.get(baseKey) ?? 0; childKeyCounts.set(baseKey, count + 1); return { child, key: count === 0 ? baseKey : `${baseKey}-${count}` }; }); const updateChild = (target: KeybindingWhenNode, next: KeybindingWhenNode) => { let didUpdate = false; const nextChildren = children.map((child) => { if (!didUpdate && child === target) { didUpdate = true; return next; } return child; }); const nextNode = buildWhenExpressionGroup(nextChildren, operator); if (nextNode) onChange(nextNode); }; const removeChild = (target: KeybindingWhenNode) => { let didRemove = false; const nextChildren = children.filter((child) => { if (!didRemove && child === target) { didRemove = true; return false; } return true; }); const nextNode = buildWhenExpressionGroup(nextChildren, operator); if (nextNode) { onChange(nextNode); } else { onChange(defaultWhenCondition()); } }; const setOperator = (nextOperator: BooleanOperator) => { if (nextOperator === operator) return; const nextNode = buildWhenExpressionGroup(children, nextOperator); if (nextNode) onChange(nextNode); }; const addCondition = () => { const nextNode = buildWhenExpressionGroup([...children, defaultWhenCondition()], operator); if (nextNode) onChange(nextNode); }; const addGroup = () => { const nestedOperator: BooleanOperator = operator === "and" ? "or" : "and"; const group: KeybindingWhenNode = { type: nestedOperator, left: defaultWhenCondition(), right: { type: "not", node: defaultWhenCondition() }, }; const nextNode = buildWhenExpressionGroup([...children, group], operator); if (nextNode) onChange(nextNode); }; return (
0 && "border-border/70 bg-background/55", )} >
{onRemove ? ( ) : null}
{childEntries.map(({ child, key }) => (
updateChild(child, next)} onRemove={() => removeChild(child)} />
))}
); } function WhenExpressionBuilder({ value, variables, onChange, onValidityChange, }: { value: KeybindingWhenNode | undefined; variables: ReadonlyArray; onChange: (value: KeybindingWhenNode | undefined) => void; onValidityChange?: (valid: boolean) => void; }) { const expression = whenAstToExpression(value); const [expressionDraft, setExpressionDraft] = useState(expression); const parseResult = useMemo(() => parseWhenExpressionDraft(expressionDraft), [expressionDraft]); const parseError = parseResult.ok ? null : parseResult.message; const unknownIdentifiers = parseResult.ok ? unknownWhenVariables(parseResult.value) : []; const updateExpressionDraft = (nextExpression: string) => { setExpressionDraft(nextExpression); const nextResult = parseWhenExpressionDraft(nextExpression); onValidityChange?.(nextResult.ok); if (nextResult.ok) { onChange(nextResult.value); } }; const updateExpressionValue = (nextValue: KeybindingWhenNode | undefined) => { setExpressionDraft(whenAstToExpression(nextValue)); onValidityChange?.(true); onChange(nextValue); }; const addRootCondition = () => { if (!value) { updateExpressionValue(defaultWhenCondition()); return; } updateExpressionValue({ type: "and", left: value, right: defaultWhenCondition() }); }; const addRootGroup = () => { const group = defaultWhenGroup("or"); if (!value) { updateExpressionValue(group); return; } updateExpressionValue({ type: "and", left: value, right: group }); }; return (
When
updateExpressionDraft(event.currentTarget.value)} placeholder="Always" aria-invalid={Boolean(parseError)} aria-label="When expression" className={cn( "h-7 rounded-md font-mono text-[12px] leading-7 sm:h-7 sm:leading-7", unknownIdentifiers.length > 0 && "pr-9", parseError && "border-destructive/70 focus-visible:border-destructive", )} /> {unknownIdentifiers.length > 0 ? ( ) : null}
{parseError ? (
{parseError}
) : null}
{value ? ( updateExpressionValue(undefined)} /> ) : (
)} {parseError ? (
Fix the expression above to continue editing visually.
) : null}
); } type KeybindingRowDraftState = { keyDraft: string; whenDraft: KeybindingWhenNode | undefined; isRecording: boolean; isWhenDraftValid: boolean; }; function createKeybindingRowDraft(row: KeybindingRow): KeybindingRowDraftState { return { keyDraft: row.key, whenDraft: row.binding.whenAst, isRecording: false, isWhenDraftValid: true, }; } function keybindingRowDraftReducer( state: KeybindingRowDraftState, patch: Partial, ): KeybindingRowDraftState { return { ...state, ...patch }; } function rowKeybindingTarget(row: KeybindingRow): ServerRemoveKeybindingInput { return { command: row.command, key: row.key, ...(row.when.trim().length > 0 ? { when: row.when } : {}), }; } /** Draft state and actions for editing one existing binding; layouts decide how to render it. */ function useKeybindingRowEditor({ row, allRows, onSave, }: { row: KeybindingRow; allRows: ReadonlyArray; onSave: (input: ServerUpsertKeybindingInput) => void; }) { const [draft, setDraft] = useReducer(keybindingRowDraftReducer, row, createKeybindingRowDraft); const { keyDraft, whenDraft, isRecording, isWhenDraftValid } = draft; const whenDraftExpression = whenAstToExpression(whenDraft); const isDirty = keyDraft !== row.key || whenDraftExpression !== row.when; const conflictLabels = keybindingConflictLabels(allRows, { rowId: row.id, key: keyDraft, when: whenDraftExpression, }); const save = () => { onSave({ command: row.command, key: keyDraft, when: whenDraftExpression.trim().length > 0 ? whenDraftExpression : undefined, replace: rowKeybindingTarget(row), }); }; const captureKeybinding = (event: KeyboardEvent) => { if (event.key === "Tab") return; event.preventDefault(); if (event.key === "Escape") { setDraft({ keyDraft: row.key, isRecording: false }); return; } const next = keybindingFromKeyboardEvent(event.nativeEvent, navigator.platform); if (!next) return; setDraft({ keyDraft: next, isRecording: false }); }; return { keyDraft, whenDraft, isRecording, isWhenDraftValid, whenDraftExpression, isDirty, conflictLabels, setDraft, save, captureKeybinding, }; } type KeybindingRowEditor = ReturnType; interface KeybindingRowActions { allRows: ReadonlyArray; variables: ReadonlyArray; onSave: (input: ServerUpsertKeybindingInput) => void; onReset: (row: KeybindingRow) => void; onRemove: (row: KeybindingRow) => void; } type KeybindingRowProps = KeybindingRowActions & { row: KeybindingRow; isSaving: boolean }; /** Shortcut pill that turns into a capture input when clicked, plus Save once the draft changes. */ function KeybindingKeyControl({ row, editor, isSaving, pillClassName, }: { row: KeybindingRow; editor: KeybindingRowEditor; isSaving: boolean; pillClassName?: string | undefined; }) { const { keyDraft, isRecording, isDirty, isWhenDraftValid, setDraft, save, captureKeybinding } = editor; const showPill = !isRecording && keyDraft === row.key && row.key.length > 0 && !isDirty; return ( <> {isDirty ? ( ) : null} {showPill ? ( ) : ( setDraft({ isRecording: true })} onBlur={() => setDraft({ isRecording: false })} onChange={(event) => setDraft({ keyDraft: event.currentTarget.value })} onKeyDown={captureKeybinding} /> )} ); } /** Quiet inline trigger showing the when clause; opens the expression builder. */ function WhenClauseControl({ label, expression, value, variables, onChange, onValidityChange, }: { label: string; expression: string; value: KeybindingWhenNode | undefined; variables: ReadonlyArray; onChange: (value: KeybindingWhenNode | undefined) => void; onValidityChange: (valid: boolean) => void; }) { return ( } aria-label={`Edit when clause for ${label}`} > {expression || "Always"} ); } function KeybindingRowMenu({ row, isSaving, onReset, onRemove, }: { row: KeybindingRow; isSaving: boolean; onReset: (row: KeybindingRow) => void; onRemove: (row: KeybindingRow) => void; }) { const canReset = row.source === "Custom" && row.defaultKey !== null; const canRemove = row.source !== "Default"; if (!canReset && !canRemove) return null; return ( } > {canReset ? ( onReset(row)}> Reset to default ) : null} {canRemove ? ( onRemove(row)}> Remove ) : null} ); } function KeybindingSourceBadge({ source }: { source: KeybindingRow["source"] }) { if (source === "Default") return null; return ( {source} ); } function KeybindingRowTitle({ row }: { row: KeybindingRow }) { return ( }> {commandLabel(row.command)} {row.command} ); } function KeybindingRowWhen({ row, editor, variables, }: { row: KeybindingRow; editor: KeybindingRowEditor; variables: ReadonlyArray; }) { return ( When editor.setDraft({ whenDraft })} onValidityChange={(isWhenDraftValid) => editor.setDraft({ isWhenDraftValid })} /> ); } /** Row actions that stay hidden until the row is hovered or holds focus. */ function KeybindingHoverRowMenu(props: { row: KeybindingRow; isSaving: boolean; onReset: (row: KeybindingRow) => void; onRemove: (row: KeybindingRow) => void; }) { return ( ); } /** One binding as a settings row: pills flush right, actions fading in beside them on hover. */ function KeybindingSettingsRow(props: KeybindingRowProps) { const { row, isSaving, allRows, variables, onSave, onReset, onRemove } = props; const editor = useKeybindingRowEditor({ row, allRows, onSave }); return ( } description={} control={
} /> ); } /** Draft state for a binding that does not exist yet. */ function useNewKeybindingDraft({ allRows, onSave, }: { allRows: ReadonlyArray; onSave: (input: ServerUpsertKeybindingInput) => void; }) { const [commandDraft, setCommandDraft] = useState(""); const [draft, setDraft] = useReducer(keybindingRowDraftReducer, { keyDraft: "", whenDraft: undefined, isRecording: false, isWhenDraftValid: true, }); const { keyDraft, whenDraft, isRecording, isWhenDraftValid } = draft; const whenDraftExpression = whenAstToExpression(whenDraft); const conflictLabels = keybindingConflictLabels(allRows, { rowId: "new", key: keyDraft, when: whenDraftExpression, }); const commandLabelText = commandDraft ? commandLabel(commandDraft) : "new keybinding"; const canSave = Boolean(commandDraft) && keyDraft.trim().length > 0 && isWhenDraftValid; const save = () => { if (!commandDraft) return; onSave({ command: commandDraft, key: keyDraft, ...(whenDraftExpression.trim().length > 0 ? { when: whenDraftExpression } : {}), }); }; const captureKeybinding = (event: KeyboardEvent) => { if (event.key === "Tab") return; event.preventDefault(); if (event.key === "Escape") { setDraft({ keyDraft: "", isRecording: false }); return; } const next = keybindingFromKeyboardEvent(event.nativeEvent, navigator.platform); if (!next) return; setDraft({ keyDraft: next, isRecording: false }); }; return { commandDraft, setCommandDraft, keyDraft, whenDraft, whenDraftExpression, isRecording, conflictLabels, commandLabelText, canSave, setDraft, save, captureKeybinding, }; } type NewKeybindingDraft = ReturnType; interface NewKeybindingProps { commandOptions: ReadonlyArray; allRows: ReadonlyArray; variables: ReadonlyArray; isSaving: boolean; onSave: (input: ServerUpsertKeybindingInput) => void; onCancel: () => void; } function NewKeybindingCommandSelect({ draft, commandOptions, className, }: { draft: NewKeybindingDraft; commandOptions: ReadonlyArray; className?: string | undefined; }) { return ( ); } function NewKeybindingKeyInput({ draft, autoFocus = false, className, }: { draft: NewKeybindingDraft; autoFocus?: boolean; className?: string | undefined; }) { return ( draft.setDraft({ isRecording: true })} onBlur={() => draft.setDraft({ isRecording: false })} onChange={(event) => draft.setDraft({ keyDraft: event.currentTarget.value })} onKeyDown={draft.captureKeybinding} /> ); } function NewKeybindingWhen({ draft, variables, }: { draft: NewKeybindingDraft; variables: ReadonlyArray; }) { return ( draft.setDraft({ whenDraft })} onValidityChange={(isWhenDraftValid) => draft.setDraft({ isWhenDraftValid })} /> ); } function NewKeybindingCancelIcon({ isSaving, onCancel, }: { isSaving: boolean; onCancel: () => void; }) { return ( } > Cancel ); } /** Add-binding form shaped like the binding rows below it. */ function NewKeybindingSettingsRow(props: NewKeybindingProps) { const { commandOptions, allRows, variables, isSaving, onSave, onCancel } = props; const draft = useNewKeybindingDraft({ allRows, onSave }); return ( When } control={
} /> ); } interface KeybindingsListProps extends KeybindingRowActions { rows: ReadonlyArray; commandOptions: ReadonlyArray; savingCommand: KeybindingCommand | null; isAddingBinding: boolean; onCancelAdd: () => void; } /** The add-binding row, one settings row per binding, and the empty state. */ function KeybindingsList(props: KeybindingsListProps) { const { rows, commandOptions, savingCommand, isAddingBinding, onCancelAdd, ...rowActions } = props; const newProps: NewKeybindingProps = { commandOptions, allRows: rows, variables: rowActions.variables, isSaving: savingCommand !== null, onSave: rowActions.onSave, onCancel: onCancelAdd, }; return (
{isAddingBinding ? : null} {rows.map((row) => ( ))} {rows.length === 0 && !isAddingBinding ? (
No keybindings match your search.
) : null}
); } /** Shown in the browser build only; the desktop app receives every shortcut. */ function BrowserKeybindingNotice() { return (
Some shortcuts may be claimed by the browser before T3 Code sees them. Use the desktop app for better keybinding support.
); } export function KeybindingsSettingsPanel() { const keybindings = useAtomValue(primaryServerKeybindingsAtom); const keybindingsConfigPath = useAtomValue(primaryServerKeybindingsConfigPathAtom); const availableEditors = useAtomValue(primaryServerAvailableEditorsAtom); const primaryEnvironment = usePrimaryEnvironment(); const upsertKeybinding = useAtomCommand(serverEnvironment.upsertKeybinding, { reportFailure: false, }); const removeKeybindingMutation = useAtomCommand(serverEnvironment.removeKeybinding, { reportFailure: false, }); const openInPreferredEditor = useOpenInPreferredEditor( primaryEnvironment?.environmentId ?? null, availableEditors, ); const [query, setQuery] = useState(""); const [isSearchOpen, setIsSearchOpen] = useState(false); const searchInputRef = useRef(null); const [savingCommand, setSavingCommand] = useState(null); const [isAddingBinding, setIsAddingBinding] = useState(false); const rows = useMemo(() => buildKeybindingRows(keybindings, query), [keybindings, query]); const commandOptions = useMemo(() => buildKeybindingCommandOptions(keybindings), [keybindings]); const whenVariables = useMemo(() => buildWhenVariableOptions(), []); useEffect(() => { const handleKeyDown = (event: globalThis.KeyboardEvent) => { const isMod = event.metaKey || event.ctrlKey; if (!isMod || event.altKey || event.key.toLowerCase() !== "f") return; const target = event.target; if ( target !== searchInputRef.current && target instanceof HTMLElement && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) ) { return; } event.preventDefault(); setIsSearchOpen(true); requestAnimationFrame(() => { searchInputRef.current?.focus(); searchInputRef.current?.select(); }); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, []); const openKeybindingsFile = useCallback(() => { if (!keybindingsConfigPath) return; void (async () => { const result = await openInPreferredEditor(keybindingsConfigPath); if (result._tag === "Success" || isAtomCommandInterrupted(result)) { return; } const error = squashAtomCommandFailure(result); toastManager.add({ title: "Unable to open keybindings file", description: error instanceof Error ? error.message : "The keybindings file was not opened.", type: "error", }); })(); }, [keybindingsConfigPath, openInPreferredEditor]); const saveKeybinding = useCallback( (input: ServerUpsertKeybindingInput) => { if (!primaryEnvironment) return; setSavingCommand(input.command); const payload: ServerUpsertKeybindingInput = { command: input.command, key: input.key.trim(), ...(input.when?.trim() ? { when: input.when.trim() } : {}), ...(input.replace ? { replace: input.replace } : {}), }; void (async () => { const result = await upsertKeybinding({ environmentId: primaryEnvironment.environmentId, input: payload, }); setSavingCommand(null); if (result._tag === "Success") { setIsAddingBinding(false); return; } if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); toastManager.add({ title: "Unable to save keybinding", description: error instanceof Error ? error.message : "The keybinding was not saved.", type: "error", }); } })(); }, [primaryEnvironment, upsertKeybinding], ); const removeKeybinding = useCallback( (row: KeybindingRow) => { if (!primaryEnvironment) return; setSavingCommand(row.command); void (async () => { const result = await removeKeybindingMutation({ environmentId: primaryEnvironment.environmentId, input: rowKeybindingTarget(row), }); setSavingCommand(null); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); toastManager.add({ title: "Unable to remove keybinding", description: error instanceof Error ? error.message : "The keybinding was not removed.", type: "error", }); } })(); }, [primaryEnvironment, removeKeybindingMutation], ); const resetKeybinding = useCallback( (row: KeybindingRow) => { if (!row.defaultKey) return; saveKeybinding({ command: row.command, key: row.defaultKey, when: row.defaultWhen.trim().length > 0 ? row.defaultWhen : undefined, replace: { command: row.command, key: row.key, ...(row.when.trim().length > 0 ? { when: row.when } : {}), }, }); }, [saveKeybinding], ); const cancelAdd = useCallback(() => setIsAddingBinding(false), []); const bindingsCount = ( {rows.length + (isAddingBinding ? 1 : 0)}{" "} {rows.length + (isAddingBinding ? 1 : 0) === 1 ? "binding" : "bindings"} ); const listProps: KeybindingsListProps = { rows, allRows: rows, commandOptions, variables: whenVariables, savingCommand, isAddingBinding, onCancelAdd: cancelAdd, onSave: saveKeybinding, onReset: resetKeybinding, onRemove: removeKeybinding, }; return ( setIsAddingBinding(true)} aria-label="Add keybinding" > } /> Add keybinding } /> Open keybindings.json } > {!isElectron ? : null} ); }