{"id":31757317,"url":"https://github.com/sec-ant/matches-hotkeys","last_synced_at":"2026-04-12T12:01:34.614Z","repository":{"id":314455494,"uuid":"1054113077","full_name":"Sec-ant/matches-hotkeys","owner":"Sec-ant","description":null,"archived":false,"fork":false,"pushed_at":"2025-10-01T15:44:06.000Z","size":77,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-01T15:52:31.587Z","etag":null,"topics":["hotkey","hotkeys","keyboard","shortcut","shortcuts"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/matches-hotkeys/v/latest","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Sec-ant.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-10T11:37:54.000Z","updated_at":"2025-10-01T15:50:02.000Z","dependencies_parsed_at":"2025-09-12T16:32:59.608Z","dependency_job_id":"49d9ac6c-61e3-43a1-86fe-0295e95f4fab","html_url":"https://github.com/Sec-ant/matches-hotkeys","commit_stats":null,"previous_names":["sec-ant/parse-hotkeys"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/Sec-ant/matches-hotkeys","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sec-ant%2Fmatches-hotkeys","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sec-ant%2Fmatches-hotkeys/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sec-ant%2Fmatches-hotkeys/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sec-ant%2Fmatches-hotkeys/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Sec-ant","download_url":"https://codeload.github.com/Sec-ant/matches-hotkeys/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sec-ant%2Fmatches-hotkeys/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279001944,"owners_count":26083244,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-09T02:00:07.460Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hotkey","hotkeys","keyboard","shortcut","shortcuts"],"created_at":"2025-10-09T19:55:51.498Z","updated_at":"2026-04-12T12:01:34.607Z","avatar_url":"https://github.com/Sec-ant.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# matches-hotkeys\n\nParse keyboard shortcuts and match them against `KeyboardEvent` objects.\n\n## Overview\n\nThis library provides functions to:\n\n- Parse hotkey combinations (e.g., `\"ctrl+a\"`, `\"mod+shift+p\"`) into normalized representations\n- Match `KeyboardEvent` objects against parsed hotkey specifications\n- Handle platform differences (`mod` resolves to `cmd` on macOS, `ctrl` elsewhere)\n- Resolve ambiguous keys (e.g., `\"0\"` matches both top-row and numpad)\n\n**What this library does not do:**\n\n- Register global keyboard listeners\n- Manage shortcut conflicts or priorities\n- Provide UI components or visual feedback\n\nThese concerns are left to the application layer.\n\n## Installation\n\n```bash\nnpm install matches-hotkeys\n```\n\nTypeScript types are included. ES Module, CommonJS, and IIFE builds are provided.\n\n## Quick Start\n\n```ts\nimport { matchesHotkeys } from \"matches-hotkeys\";\n\n// Define shortcuts\nconst SAVE_SHORTCUT = [{ combination: \"mod+s\" }]; // cmd+s on macOS, ctrl+s elsewhere\n\n// Check if event matches\nwindow.addEventListener(\"keydown\", (event) =\u003e {\n  if (matchesHotkeys(SAVE_SHORTCUT, event)) {\n    event.preventDefault();\n    saveDocument();\n  }\n});\n```\n\n## Usage\n\n### ES Module / CommonJS\n\n```ts\nimport { matchesHotkeys, parseCombination } from \"matches-hotkeys\";\n```\n\n### IIFE\n\nFor direct browser usage via `\u003cscript\u003e` tag, IIFE builds are available. The global variable is `MatchesHotkeys`.\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/matches-hotkeys@\u003cversion\u003e/dist/iife/index.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  const { matchesHotkeys } = MatchesHotkeys;\n  // Use matchesHotkeys...\n\u003c/script\u003e\n```\n\n## API\n\n### `matchesHotkeys(hotkeys, event, options?)`\n\nTests if a `KeyboardEvent` matches any of the provided hotkey specifications.\n\n**Parameters:**\n\n- `hotkeys`: Array of `{ combination, options? }` objects\n- `event`: A `KeyboardEvent` instance\n- `options`: Optional `{ comparator? }` configuration\n\n**Returns:** `boolean` - `true` if any hotkey matches the event\n\n**Example:**\n\n```ts\nconst hotkeys = [{ combination: \"ctrl+s\" }, { combination: \"cmd+s\" }];\nmatchesHotkeys(hotkeys, event); // true if event is Ctrl+S or Cmd+S\n```\n\n### `parseCombination(combination, options?)`\n\nParses a hotkey combination string or array into normalized representations.\n\n**Parameters:**\n\n- `combination`: String (`\"ctrl+a\"`) or array (`[\"ctrl\", \"a\"]`)\n- `options`: Optional configuration\n  - `splitBy`: Separator character (default: `\"+\"`).\n\n    Change this when you need to use `\"+\"` as the actual key in your shortcut (e.g., `\"ctrl-+\"` with `splitBy: \"-\"`)\n\n  - `trim`: Whether to trim whitespace from each token after splitting (default: `true` when `combination` is a string, `false` when it's an array).\n\n    When `true`, whitespace around tokens is removed, making empty spaces in string combinations become empty tokens (which are invalid). Set to `false` for string combinations to preserve the space character as a valid key (e.g., `\"ctrl+ \"` with `trim: false` matches the space key)\n\n  - `allowCodeAsModifier`: Allow physical key codes like `\"ControlLeft\"` or `\"ShiftRight\"` as modifiers (default: `true`).\n\n    When `true`, allows both `\"ctrl+a\"` and `\"ControlLeft+a\"` (both produce the same result with `ctrlKey: true`, since browsers cannot distinguish left/right modifiers at runtime). When `false`, only logical modifier names like `\"ctrl\"` are accepted in modifier positions, rejecting `\"ControlLeft+a\"` as invalid (but `\"ControlLeft\"` alone as a main key is still valid)\n\n  - `inferShift`: Automatically infer `shiftKey: true` for shift-derived keys (default: `false`).\n\n    When `true`, keys that can only be produced with Shift (e.g., `\"+\"` from Equal key, `\"!\"` from Digit1) automatically get `shiftKey: true` for physical keys that require Shift. When `false`, `shiftKey` is only set based on explicitly provided modifiers. See [Shift-Derived Keys](#shift-derived-keys) for details.\n\n**Returns:** `ParsedCombination[]` - Array of parsed variants (empty if invalid)\n\n**Examples:**\n\n```ts\n// Basic usage\nparseCombination(\"ctrl+a\");\n// [{ code: \"KeyA\", key: \"a\", keyCode: 65, which: 65, ctrlKey: true, metaKey: false, shiftKey: false, altKey: false }]\n\n// Ambiguous keys return multiple variants\nparseCombination(\"0\");\n// [\n//   { code: \"Digit0\", key: \"0\", keyCode: 48, which: 48, ctrlKey: false, metaKey: false, shiftKey: false, altKey: false },\n//   { code: \"Numpad0\", key: \"0\", keyCode: 96, which: 96, ctrlKey: false, metaKey: false, shiftKey: false, altKey: false }\n// ]\n\n// Key aliases work for shifted keys (e.g., \"plus\" → \"+\")\nparseCombination(\"ctrl+plus\"); // \"plus\" is an alias for \"+\"\n// [\n//   { code: \"NumpadAdd\", key: \"+\", keyCode: 107, which: 107, ctrlKey: true, shiftKey: false, ... },    // Numpad\n//   { code: \"Equal\", key: \"+\", keyCode: 187, which: 187, ctrlKey: true, shiftKey: false, ... }          // Top-row\n// ]\n\n// Option: splitBy - Use different separator for literal \"+\" key\nparseCombination(\"ctrl-+\", { splitBy: \"-\" }); // Direct \"+\" character as key\n// [\n//   { code: \"NumpadAdd\", key: \"+\", keyCode: 107, which: 107, ctrlKey: true, shiftKey: false, ... },\n//   { code: \"Equal\", key: \"+\", keyCode: 187, which: 187, ctrlKey: true, shiftKey: false, ... }\n// ]\n\n// Option: trim - Preserve whitespace to match space key\nparseCombination(\"ctrl+ \"); // Default trim removes space, \" \" becomes \"\"\n// [] (empty - invalid because last token is empty)\n\nparseCombination(\"ctrl+ \", { trim: false }); // Space key preserved\n// [{ code: \"Space\", key: \" \", keyCode: 32, which: 32, ctrlKey: true, shiftKey: false, ... }]\n\n// Option: allowCodeAsModifier - Enforce logical modifiers only\nparseCombination(\"ControlLeft+a\"); // Physical code as modifier (allowed by default)\n// [{ code: \"KeyA\", key: \"a\", keyCode: 65, which: 65, ctrlKey: true, shiftKey: false, ... }]\n\nparseCombination(\"ControlLeft+a\", { allowCodeAsModifier: false }); // Reject physical codes\n// [] (empty - invalid because \"ControlLeft\" is not a logical modifier)\n\nparseCombination(\"ctrl+a\", { allowCodeAsModifier: false }); // Logical modifier OK\n// [{ code: \"KeyA\", key: \"a\", keyCode: 65, which: 65, ctrlKey: true, shiftKey: false, ... }]\n\n// Option: inferShift - Automatically infer shift for shift-derived keys\nparseCombination(\"ctrl+plus\"); // Default: inferShift=false, no automatic inference\n// [\n//   { code: \"NumpadAdd\", key: \"+\", keyCode: 107, which: 107, ctrlKey: true, shiftKey: false, ... },\n//   { code: \"Equal\", key: \"+\", keyCode: 187, which: 187, ctrlKey: true, shiftKey: false, ... }\n// ]\n\nparseCombination(\"ctrl+plus\", { inferShift: true }); // Automatic shift inference enabled\n// [\n//   { code: \"NumpadAdd\", key: \"+\", keyCode: 107, which: 107, ctrlKey: true, shiftKey: false, ... },  // Numpad doesn't need Shift\n//   { code: \"Equal\", key: \"+\", keyCode: 187, which: 187, ctrlKey: true, shiftKey: true, ... }        // Shift inferred for Equal\n// ]\n```\n\n### `resolveKey(token)`\n\nResolves a single key token into standardized key information. Used internally by `parseCombination`.\n\n**Parameters:**\n\n- `token`: A single key string (case-insensitive)\n\n**Returns:** `ResolvedKey[]` - Array of possible key resolutions\n\n**Resolution behavior:**\n\n- **Single-source keys** (e.g., `\"a\"`, `\"Escape\"`) return one result\n- **Ambiguous keys** (e.g., `\"0\"`, `\"+\"`) return multiple results for different physical keys\n- **Generic modifiers** (e.g., `\"ctrl\"`, `\"shift\"`) return both left and right variants\n- **Specific modifiers** (e.g., `\"ControlLeft\"`) return only that variant\n- **Unknown keys** return a fallback object with `keyCode: -1` and `which: -1`\n\n**Examples:**\n\n```ts\nresolveKey(\"a\"); // [{ key: \"a\", code: \"KeyA\", keyCode: 65, which: 65 }]\n\nresolveKey(\"0\"); // Ambiguous - returns both top-row and numpad\n// [\n//   { key: \"0\", code: \"Digit0\", keyCode: 48, which: 48 },\n//   { key: \"0\", code: \"Numpad0\", keyCode: 96, which: 96 }\n// ]\n\nresolveKey(\"ctrl\"); // Generic modifier - returns both variants\n// [\n//   { key: \"Control\", code: \"ControlLeft\", keyCode: 17, which: 17 },\n//   { key: \"Control\", code: \"ControlRight\", keyCode: 17, which: 17 }\n// ]\n\nresolveKey(\"ControlLeft\"); // Specific modifier - returns only left variant\n// [{ key: \"Control\", code: \"ControlLeft\", keyCode: 17, which: 17 }]\n\nresolveKey(\"unknown\"); // Unknown key - returns fallback\n// [{ key: \"unknown\", code: \"unknown\", keyCode: -1, which: -1 }]\n```\n\n## Usage Examples\n\n### Simple Shortcuts\n\n```ts\nimport { matchesHotkeys } from \"matches-hotkeys\";\n\nwindow.addEventListener(\"keydown\", (event) =\u003e {\n  // Save\n  if (matchesHotkeys([{ combination: \"mod+s\" }], event)) {\n    event.preventDefault();\n    save();\n  }\n\n  // Copy\n  if (matchesHotkeys([{ combination: \"mod+c\" }], event)) {\n    copy();\n  }\n\n  // Open command palette\n  if (matchesHotkeys([{ combination: \"mod+shift+p\" }], event)) {\n    event.preventDefault();\n    openCommandPalette();\n  }\n});\n```\n\n### Registering Multiple Shortcuts\n\n```ts\nconst shortcuts = [\n  { combination: \"mod+s\", action: save },\n  { combination: \"mod+shift+s\", action: saveAs },\n  { combination: \"mod+o\", action: open },\n  { combination: \"mod+w\", action: close },\n];\n\nwindow.addEventListener(\"keydown\", (event) =\u003e {\n  for (const { combination, action } of shortcuts) {\n    if (matchesHotkeys([{ combination }], event)) {\n      event.preventDefault();\n      action();\n      break;\n    }\n  }\n});\n```\n\n### Arrow Key Navigation\n\n```ts\nconst NAVIGATION = [\n  { combination: \"arrowup\" },\n  { combination: \"arrowdown\" },\n  { combination: \"arrowleft\" },\n  { combination: \"arrowright\" },\n];\n\nwindow.addEventListener(\"keydown\", (event) =\u003e {\n  if (matchesHotkeys(NAVIGATION, event)) {\n    event.preventDefault();\n    navigate(event.key);\n  }\n});\n```\n\n## Advanced Usage\n\n### Custom Comparators\n\nBy default, a hotkey matches if any of `key`, `code`, `keyCode`, or `which` match AND all modifier flags are identical. You can customize this by composing your own comparators or using the exported ones.\n\n#### Comparator Primitives\n\n```ts\nimport { eq, and, or } from \"matches-hotkeys\";\n\n// eq(...fields) - Creates a comparator that checks equality for specific fields\nconst checkKey = eq(\"key\");\nconst checkModifiers = eq(\"altKey\", \"ctrlKey\", \"metaKey\", \"shiftKey\");\n\n// and(...comparators) - All comparators must match\nconst strictMatch = and(checkKey, checkModifiers);\n\n// or(...comparators) - Any comparator can match\nconst flexibleMatch = or(eq(\"key\"), eq(\"code\"));\n```\n\n#### Pre-built Comparators\n\nThe library exports several pre-built comparators you can use directly or combine:\n\n```ts\nimport {\n  DEFAULT_COMPARATOR, // Matches by (key OR code OR keyCode OR which) + all modifiers\n  MODIFIERS_COMPARATOR, // Only checks modifier flags match\n  COMPARE_BY_KEY, // Matches by key (case-insensitive) + all modifiers\n  COMPARE_BY_CODE, // Matches by code + all modifiers\n  COMPARE_BY_KEY_CODE, // Matches by keyCode + all modifiers\n  COMPARE_BY_WHICH, // Matches by which + all modifiers\n} from \"matches-hotkeys\";\n```\n\n\u003e **Note:** `COMPARE_BY_KEY` uses case-insensitive matching for the `key` field. This ensures that shortcuts like `\"a\"` correctly match `Shift+KeyA` events (where `event.key` is `\"A\"`), avoiding a common mismatch when the Shift modifier changes the case of letter keys.\n\n#### Example: Ignore Shift modifier\n\nCompose a custom comparator from primitives:\n\n```ts\nimport { matchesHotkeys, eq, and, or } from \"matches-hotkeys\";\n\nconst IGNORE_SHIFT = or(\n  and(eq(\"key\", \"altKey\", \"ctrlKey\", \"metaKey\")),\n  and(eq(\"code\", \"altKey\", \"ctrlKey\", \"metaKey\")),\n);\n\n// Matches both \"a\" and \"Shift+a\"\nif (\n  matchesHotkeys([{ combination: \"a\" }], event, { comparator: IGNORE_SHIFT })\n) {\n  handleKey();\n}\n```\n\n#### Example: Use pre-built comparators\n\n```ts\nimport { matchesHotkeys, COMPARE_BY_CODE } from \"matches-hotkeys\";\n\n// Only match by physical key position, ignore key value\nif (\n  matchesHotkeys([{ combination: \"a\" }], event, { comparator: COMPARE_BY_CODE })\n) {\n  handleAction();\n}\n```\n\n#### Example: Combine pre-built comparators\n\n```ts\nimport {\n  matchesHotkeys,\n  or,\n  COMPARE_BY_KEY,\n  COMPARE_BY_CODE,\n} from \"matches-hotkeys\";\n\n// Match by either key or code (but not keyCode/which)\nconst KEY_OR_CODE = or(COMPARE_BY_KEY, COMPARE_BY_CODE);\n\nif (\n  matchesHotkeys([{ combination: \"a\" }], event, { comparator: KEY_OR_CODE })\n) {\n  handleAction();\n}\n```\n\n#### Example: Fully custom comparator\n\nYou can also write completely custom logic:\n\n```ts\nimport type { Comparator } from \"matches-hotkeys\";\n\n// Custom: Ignore Shift modifier but check the key and other modifiers\nconst IGNORE_SHIFT: Comparator = (parsed, event) =\u003e {\n  return (\n    parsed.key === event.key \u0026\u0026\n    parsed.ctrlKey === event.ctrlKey \u0026\u0026\n    parsed.metaKey === event.metaKey \u0026\u0026\n    parsed.altKey === event.altKey\n    // Note: shiftKey is intentionally not checked\n  );\n};\n\n// Now \"a\" matches both plain \"a\" and \"Shift+a\"\nif (\n  matchesHotkeys([{ combination: \"a\" }], event, { comparator: IGNORE_SHIFT })\n) {\n  handleKey();\n}\n```\n\n## Key Concepts\n\n### Keyboard Data Model\n\nThe parser relies on the W3C keyboard model exposed by `KeyboardEvent` and encoded in `src/consts.ts`:\n\n- **`key`** – The logical character or action produced by the key (e.g., `\"a\"`, `\"Enter\"`, `\"+\"`). We store this in `KEY_DEFINITIONS[code].key` and match it against `event.key`.\n- **`code`** – The physical key location (e.g., `\"KeyA\"`, `\"ShiftLeft\"`, `\"NumpadAdd\"`). This stays the same regardless of keyboard layout and is matched against `event.code`.\n- **`keyCode` / `which`** – Legacy numeric codes kept for compatibility. We surface the numeric value from `KEY_DEFINITIONS` and mirror it onto `which`, just like the browser does.\n\nEvery `ParsedCombination` exposes all three so callers can pick the level of precision they need.\n\n### Alias Layers\n\nTo keep authoring ergonomic we pre-compute several alias maps when resolving tokens:\n\n- **Key aliases (`KEY_ALIASES`)** let you write friendly names for logical keys. Examples: `\"esc\" → \"Escape\"`, `\"plus\" → \"+\"`, `\"space\" → \" \"`.\n- **Code aliases (`CODE_ALIAS_MAP`)** cover physical key nicknames such as `\"lshift\" → \"ShiftLeft\"` or `\"prtsc\" → \"PrintScreen\"`.\n- **Shift-derived symbols (`SHIFT_KEY_MAPPINGS`)** synthesize characters that only appear when Shift is held. For instance, `\"Equal\" + Shift → \"+\"`, so resolving `\"plus\"` yields both `{ code: \"NumpadAdd\", key: \"+\" }` and `{ code: \"Equal\", key: \"+\" }`.\n\nAliases are applied in this order inside `resolveKey`: exact code → code alias → key value → key alias → fallback. This ensures that precise tokens stay precise while still supporting more human-readable inputs.\n\n### Combination Syntax and Modifiers\n\nCombinations can be declared as strings (`\"ctrl+shift+p\"`) or arrays (`[\"ctrl\", \"shift\", \"p\"]`). The parser normalizes them as follows:\n\n```ts\nconst stringForm = \"ctrl+shift+p\";\nconst arrayForm: string[] = [\"ctrl\", \"shift\", \"p\"]; // Equivalent representation\n```\n\n- Tokens are split by `splitBy` (default `\"+\"`) and lower-cased via `preMap`.\n- Every segment before the last must resolve to a modifier. Supported modifier tokens are:\n  - **Control:** `ctrl`, `control`\n  - **Meta:** `meta`, `cmd`, `command`, `win`, `windows`\n  - **Shift:** `shift`\n  - **Alt:** `alt`, `option`\n- The special `mod` token resolves to `cmd` on macOS and `ctrl` elsewhere (see `preMap`).\n- The final token resolves to the main key and may expand to multiple physical variants.\n\n**Modifier side note.** Browser events only expose boolean modifier flags (`metaKey`, `ctrlKey`, `shiftKey`, `altKey`). When a shortcut includes a modifier plus another key (e.g., `ctrl+a`), the resulting `KeyboardEvent` cannot distinguish between left and right modifier keys. Consequently, combinations like `\"ControlLeft+a\"` and `\"ControlRight+a\"` are both parsed to produce the same result: `{ ctrlKey: true, ... }`. The physical `code` distinction is lost because browsers don't provide separate flags for `ctrlLeftKey` vs `ctrlRightKey`.\n\nInvalid sequences (missing main key, duplicate modifiers, empty segments) produce an empty array of parsed combinations.\n\n### Resolution Flow\n\n`parseCombination` processes each token through `resolveKey` to obtain one or more `ResolvedKey` objects, then combines modifiers with main keys:\n\n1. **Normalize tokens:** Split by `splitBy`, trim (if enabled), and convert to lowercase.\n2. **Separate modifiers from main key:** All tokens except the last must be modifiers.\n3. **Resolve modifiers:** Convert modifier tokens to boolean flags (`metaKey`, `ctrlKey`, etc.), respecting `allowCodeAsModifier`.\n4. **Resolve the main key:** Look up the last token through the alias layers described above. This may return multiple physical key variants (e.g., both `Digit0` and `Numpad0` for `\"0\"`).\n5. **Generate combinations:** Create one `ParsedCombination` for each main key variant, each including key/code/keyCode metadata plus all modifier flags.\n\n`matchesHotkeys` then compares these parsed combinations against the actual `KeyboardEvent` using the selected comparator.\n\n### Shift-Derived Keys\n\nSome keys produce different characters when Shift is held (e.g., pressing `Equal` produces `\"=\"`, but `Shift+Equal` produces `\"+\"`). The library handles these through the `SHIFT_KEY_MAPPINGS` constant, which maps base keys to their shifted characters.\n\nWhen you reference a shifted character (e.g., `\"+\"`, `\"!\"`, `\"@\"`), the library will resolve it to the appropriate physical key. For example, `\"+\"` resolves to both `NumpadAdd` (which produces `\"+\"` without Shift) and `Equal` (which produces `\"+\"` with Shift).\n\n#### Shift-Derived Keys Mapping\n\nThe following keys have shifted character mappings:\n\n- `+` (from `Equal`), `!` (from `Digit1`), `@` (from `Digit2`), `#` (from `Digit3`)\n- `$` (from `Digit4`), `%` (from `Digit5`), `^` (from `Digit6`), `\u0026` (from `Digit7`)\n- `*` (from `Digit8`), `(` (from `Digit9`), `)` (from `Digit0`)\n- `_` (from `Minus`), `~` (from `Backquote`)\n- `{` (from `BracketLeft`), `}` (from `BracketRight`), `|` (from `Backslash`)\n- `:` (from `Semicolon`), `\"` (from `Quote`)\n- `\u003c` (from `Comma`), `\u003e` (from `Period`), `?` (from `Slash`)\n\n#### Automatic Shift Inference (Optional)\n\nBy default (`inferShift: false`), the library does not automatically infer shift modifiers. You must explicitly include `shift` in your combination to match shifted characters.\n\nHowever, you can enable automatic shift inference using the `inferShift: true` option. When enabled, keys that can only be produced with Shift automatically get `shiftKey: true` for physical keys that require it:\n\n```ts\n// Default behavior (inferShift: false)\nparseCombination(\"ctrl+plus\");\n// [\n//   { code: \"NumpadAdd\", key: \"+\", ctrlKey: true, shiftKey: false, ... },\n//   { code: \"Equal\", key: \"+\", ctrlKey: true, shiftKey: false, ... }\n// ]\n\n// With inferShift: true\nparseCombination(\"ctrl+plus\", { inferShift: true });\n// [\n//   { code: \"NumpadAdd\", key: \"+\", ctrlKey: true, shiftKey: false, ... },  // Numpad doesn't need Shift\n//   { code: \"Equal\", key: \"+\", ctrlKey: true, shiftKey: true, ... }        // Shift automatically inferred\n// ]\n```\n\n**Why use automatic inference?**\n\nWithout automatic shift inference, the `Equal` variant would have `shiftKey: false`, which may not match real keyboard events where the user must hold Shift to produce `\"+\"` from the Equal key. However, the library's default comparator uses OR logic (matching on `key` OR `code` OR `keyCode` OR `which`), so matching still works correctly in most cases even without inference.\n\nAutomatic inference is useful when you want strict modifier matching or when using custom comparators that require exact modifier flag matches.\n\n#### Explicit Shift Control\n\nYou can always explicitly include `shift` in your combination regardless of the `inferShift` setting:\n\n```ts\n// Explicit shift always sets shiftKey: true\nparseCombination(\"shift+plus\");\n// [\n//   { code: \"NumpadAdd\", key: \"+\", shiftKey: true },  // Matches Shift+NumpadAdd\n//   { code: \"Equal\", key: \"+\", shiftKey: true }       // Matches Shift+Equal (produces \"+\")\n// ]\n\n// Without explicit shift and inferShift=false (default)\nparseCombination(\"plus\");\n// [\n//   { code: \"NumpadAdd\", key: \"+\", shiftKey: false },\n//   { code: \"Equal\", key: \"+\", shiftKey: false }\n// ]\n\n// Base Equal key without shift (produces \"=\")\nparseCombination(\"ctrl+=\");\n// [{ code: \"Equal\", key: \"=\", ctrlKey: true, shiftKey: false }]\n\n// Only numpad plus (no shift)\nparseCombination(\"ctrl+numpadadd\");\n// [{ code: \"NumpadAdd\", key: \"+\", ctrlKey: true, shiftKey: false }]\n```\n\n### Ambiguous Keys\n\nSome key inputs map to multiple physical keys. The parser returns all possibilities:\n\n```ts\nparseCombination(\"0\");\n// Returns both:\n// 1. { code: \"Digit0\", ... }    // Top row\n// 2. { code: \"Numpad0\", ... }   // Numpad\n\nparseCombination(\"ctrl\");\n// Returns both:\n// 1. { code: \"ControlLeft\", ctrlKey: true, ... }\n// 2. { code: \"ControlRight\", ctrlKey: true, ... }\n```\n\n`matchesHotkeys` tests all variants and returns `true` if any matches.\n\n### Unknown or Fallback Tokens\n\nUnknown key names create fallback objects with `-1` for numeric fields:\n\n```ts\nresolveKey(\"unknownkey\");\n// [{ key: \"unknownkey\", code: \"unknownkey\", keyCode: -1, which: -1 }]\n\nparseCombination(\"ctrl+unknownkey\");\n// [{ key: \"unknownkey\", code: \"unknownkey\", keyCode: -1, which: -1, ctrlKey: true, ... }]\n```\n\nThis preserves type consistency and allows detection of unknown keys. Using `-1` (instead of `undefined`) keeps the shape consistent and makes the data JSON-serializable.\n\n### Parsed Combination Payload\n\n```ts\ninterface ParsedCombination {\n  code: string; // Physical key code (e.g., \"KeyA\")\n  key: string; // Logical key value (e.g., \"a\")\n  keyCode: number; // Legacy numeric code (or -1)\n  which: number; // Alias of keyCode\n  metaKey: boolean; // Cmd/Win modifier\n  ctrlKey: boolean; // Control modifier\n  shiftKey: boolean; // Shift modifier\n  altKey: boolean; // Alt/Option modifier\n}\n```\n\nAll fields are present to match `KeyboardEvent` shape and support serialization.\n\n## Limitations\n\n- **No key sequences:** This library matches single key combinations. For sequences like `g g` (Vim-style), implement your own state machine.\n- **No automatic conflict resolution:** The library doesn't manage shortcut priorities or conflicts. This is application-layer logic.\n\n## Standards Reference\n\nThis library follows W3C specifications:\n\n- [UI Events KeyboardEvent code values](https://www.w3.org/TR/uievents-code/)\n- [UI Events KeyboardEvent key values](https://www.w3.org/TR/uievents-key/)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsec-ant%2Fmatches-hotkeys","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsec-ant%2Fmatches-hotkeys","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsec-ant%2Fmatches-hotkeys/lists"}