feat: define keybinding types and refactor keybinding configuration

This commit is contained in:
Anwarul Islam
2025-07-18 15:19:46 +06:00
parent c3caefc001
commit 23001f8442
2 changed files with 63 additions and 30 deletions
+28 -30
View File
@@ -4,12 +4,7 @@ import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { ActionWithOptionalArgs } from "@/constants/actions"; import { ActionWithOptionalArgs } from "@/constants/actions";
import { isAppleDevice, isDOMElement, isTypableElement } from "@/lib/utils"; import { isAppleDevice, isDOMElement, isTypableElement } from "@/lib/utils";
import { KeybindingConfig, ShortcutKey } from "@/types/keybinding";
export type ShortcutKey = string;
export interface KeybindingConfig {
[key: ShortcutKey]: ActionWithOptionalArgs;
}
// Default keybindings configuration // Default keybindings configuration
export const defaultKeybindings: KeybindingConfig = { export const defaultKeybindings: KeybindingConfig = {
@@ -19,23 +14,23 @@ export const defaultKeybindings: KeybindingConfig = {
l: "seek-forward", l: "seek-forward",
left: "frame-step-backward", left: "frame-step-backward",
right: "frame-step-forward", right: "frame-step-forward",
"shift-left": "jump-backward", "shift+left": "jump-backward",
"shift-right": "jump-forward", "shift+right": "jump-forward",
home: "goto-start", home: "goto-start",
end: "goto-end", end: "goto-end",
s: "split-element", s: "split-element",
n: "toggle-snapping", n: "toggle-snapping",
"ctrl-a": "select-all", "ctrl+a": "select-all",
"ctrl-d": "duplicate-selected", "ctrl+d": "duplicate-selected",
"ctrl-z": "undo", "ctrl+z": "undo",
"ctrl-shift-z": "redo", "ctrl+shift+z": "redo",
"ctrl-y": "redo", "ctrl+y": "redo",
delete: "delete-selected", delete: "delete-selected",
backspace: "delete-selected", backspace: "delete-selected",
}; };
export interface KeybindingConflict { export interface KeybindingConflict {
key: string; key: ShortcutKey;
existingAction: ActionWithOptionalArgs; existingAction: ActionWithOptionalArgs;
newAction: ActionWithOptionalArgs; newAction: ActionWithOptionalArgs;
} }
@@ -46,8 +41,8 @@ interface KeybindingsState {
keybindingsEnabled: boolean; keybindingsEnabled: boolean;
// Actions // Actions
updateKeybinding: (key: string, action: ActionWithOptionalArgs) => void; updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => void;
removeKeybinding: (key: string) => void; removeKeybinding: (key: ShortcutKey) => void;
resetToDefaults: () => void; resetToDefaults: () => void;
importKeybindings: (config: KeybindingConfig) => void; importKeybindings: (config: KeybindingConfig) => void;
exportKeybindings: () => KeybindingConfig; exportKeybindings: () => KeybindingConfig;
@@ -56,13 +51,13 @@ interface KeybindingsState {
// Validation // Validation
validateKeybinding: ( validateKeybinding: (
key: string, key: ShortcutKey,
action: ActionWithOptionalArgs action: ActionWithOptionalArgs
) => KeybindingConflict | null; ) => KeybindingConflict | null;
getKeybindingsForAction: (action: ActionWithOptionalArgs) => string[]; getKeybindingsForAction: (action: ActionWithOptionalArgs) => ShortcutKey[];
// Utility // Utility
getKeybindingString: (ev: KeyboardEvent) => string | null; getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
} }
export const useKeybindingsStore = create<KeybindingsState>()( export const useKeybindingsStore = create<KeybindingsState>()(
@@ -72,7 +67,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
isCustomized: false, isCustomized: false,
keybindingsEnabled: true, keybindingsEnabled: true,
updateKeybinding: (key: string, action: ActionWithOptionalArgs) => { updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => {
set((state) => { set((state) => {
const newKeybindings = { ...state.keybindings }; const newKeybindings = { ...state.keybindings };
newKeybindings[key] = action; newKeybindings[key] = action;
@@ -84,7 +79,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
}); });
}, },
removeKeybinding: (key: string) => { removeKeybinding: (key: ShortcutKey) => {
set((state) => { set((state) => {
const newKeybindings = { ...state.keybindings }; const newKeybindings = { ...state.keybindings };
delete newKeybindings[key]; delete newKeybindings[key];
@@ -129,7 +124,10 @@ export const useKeybindingsStore = create<KeybindingsState>()(
return get().keybindings; return get().keybindings;
}, },
validateKeybinding: (key: string, action: ActionWithOptionalArgs) => { validateKeybinding: (
key: ShortcutKey,
action: ActionWithOptionalArgs
) => {
const { keybindings } = get(); const { keybindings } = get();
const existingAction = keybindings[key]; const existingAction = keybindings[key];
@@ -147,12 +145,12 @@ export const useKeybindingsStore = create<KeybindingsState>()(
getKeybindingsForAction: (action: ActionWithOptionalArgs) => { getKeybindingsForAction: (action: ActionWithOptionalArgs) => {
const { keybindings } = get(); const { keybindings } = get();
return Object.keys(keybindings).filter( return Object.keys(keybindings).filter(
(key) => keybindings[key] === action (key) => keybindings[key as ShortcutKey] === action
); ) as ShortcutKey[];
}, },
getKeybindingString: (ev: KeyboardEvent) => { getKeybindingString: (ev: KeyboardEvent) => {
return generateKeybindingString(ev); return generateKeybindingString(ev) as ShortcutKey | null;
}, },
}), }),
{ {
@@ -163,7 +161,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
); );
// Utility functions // Utility functions
function generateKeybindingString(ev: KeyboardEvent): string | null { function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
const target = ev.target; const target = ev.target;
// We may or may not have a modifier key // We may or may not have a modifier key
@@ -184,14 +182,14 @@ function generateKeybindingString(ev: KeyboardEvent): string | null {
return null; return null;
} }
return `${modifierKey}-${key}`; return `${modifierKey}+${key}` as ShortcutKey;
} }
// no modifier key here then we do not do anything while on input // no modifier key here then we do not do anything while on input
if (isDOMElement(target) && isTypableElement(target)) return null; if (isDOMElement(target) && isTypableElement(target)) return null;
// single key while not input // single key while not input
return `${key}`; return `${key}` as ShortcutKey;
} }
function getPressedKey(ev: KeyboardEvent): string | null { function getPressedKey(ev: KeyboardEvent): string | null {
@@ -233,11 +231,11 @@ function getActiveModifier(ev: KeyboardEvent): string | null {
shift: ev.shiftKey, shift: ev.shiftKey,
}; };
// active modifier: ctrl | alt | ctrl-alt | ctrl-shift | ctrl-alt-shift | alt-shift // active modifier: ctrl | alt | ctrl+alt | ctrl+shift | ctrl+alt+shift | alt+shift
// modiferKeys object's keys are sorted to match the above order // modiferKeys object's keys are sorted to match the above order
const activeModifier = Object.keys(modifierKeys) const activeModifier = Object.keys(modifierKeys)
.filter((key) => modifierKeys[key as keyof typeof modifierKeys]) .filter((key) => modifierKeys[key as keyof typeof modifierKeys])
.join("-"); .join("+");
return activeModifier === "" ? null : activeModifier; return activeModifier === "" ? null : activeModifier;
} }
+35
View File
@@ -0,0 +1,35 @@
import { ActionWithOptionalArgs } from "@/constants/actions";
/**
* Alt is also regarded as macOS OPTION (⌥) key
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
*/
export type ModifierKeys =
| "ctrl"
| "alt"
| "shift"
| "ctrl+shift"
| "alt+shift"
| "ctrl+alt"
| "ctrl+alt+shift";
/* eslint-disable prettier/prettier */
// prettier-ignore
export type Key =
| "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j"
| "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
| "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
| "right" | "/" | "?" | "." | "enter" | "tab" | "space"
| "escape" | "esc" | "backspace" | "delete" | "home" | "end"
/* eslint-enable */
export type ModifierBasedShortcutKey = `${ModifierKeys}+${Key}`;
// Singular keybindings (these will be disabled when an input-ish area has been focused)
export type SingleCharacterShortcutKey = `${Key}`;
export type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey;
export type KeybindingConfig = {
[key in ShortcutKey]?: ActionWithOptionalArgs;
};