{"id":25931170,"url":"https://github.com/robertwhurst/keystrokes","last_synced_at":"2025-04-04T18:08:40.469Z","repository":{"id":65358895,"uuid":"564693438","full_name":"RobertWHurst/Keystrokes","owner":"RobertWHurst","description":"Keystrokes as an easy to use library for binding functions to keys and key combos. It can be used with any TypeScript or JavaScript project, even in non-browser environments.","archived":false,"fork":false,"pushed_at":"2024-09-19T06:34:12.000Z","size":643,"stargazers_count":171,"open_issues_count":12,"forks_count":8,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-04T18:08:34.513Z","etag":null,"topics":["browser","combos","javascript","key-combos","keyboard","keyboard-events","keyboard-shortcuts","keystrokes","npm","shortcuts","typescript"],"latest_commit_sha":null,"homepage":"","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/RobertWHurst.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"license.txt","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},"funding":{"github":["RobertWHurst"]}},"created_at":"2022-11-11T09:22:04.000Z","updated_at":"2025-03-09T15:14:24.000Z","dependencies_parsed_at":"2023-12-12T10:28:09.045Z","dependency_job_id":"641660b5-1678-443e-872e-9a9aa3e073f5","html_url":"https://github.com/RobertWHurst/Keystrokes","commit_stats":{"total_commits":68,"total_committers":1,"mean_commits":68.0,"dds":0.0,"last_synced_commit":"5284283f7223dcae61006746c21b804e1f7a75db"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FKeystrokes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FKeystrokes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FKeystrokes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FKeystrokes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RobertWHurst","download_url":"https://codeload.github.com/RobertWHurst/Keystrokes/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247226215,"owners_count":20904465,"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","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":["browser","combos","javascript","key-combos","keyboard","keyboard-events","keyboard-shortcuts","keystrokes","npm","shortcuts","typescript"],"created_at":"2025-03-03T23:58:16.678Z","updated_at":"2025-04-04T18:08:40.447Z","avatar_url":"https://github.com/RobertWHurst.png","language":"TypeScript","readme":"\u003ch1 align=\"center\"\u003e\n  \u003cimg alt=\"Keystrokes\" title=\"Keystrokes\" src=\"https://raw.githubusercontent.com/RobertWHurst/Keystrokes/master/logo.png\"\u003e\n\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@rwh/keystrokes\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/@rwh/keystrokes\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@rwh/keystrokes\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/dm/@rwh/keystrokes\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/RobertWHurst/Keystrokes/actions/workflows/ci.yml\"\u003e\n    \u003cimg src=\"https://github.com/RobertWHurst/Keystrokes/actions/workflows/ci.yml/badge.svg\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/sponsors/RobertWHurst\"\u003e\n    \u003cimg src=\"https://img.shields.io/static/v1?label=Sponsor\u0026message=%E2%9D%A4\u0026logo=GitHub\u0026color=%23fe8e86\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n__If you encounter a bug please [report it][bug-report].__\n\nKeystrokes as a quick and easy to use library for binding functions to keys\nand key combos. It can also be used to check if keys or key combos are pressed\nad-hoc. It supports any TypeScript or JavaScript project, and can be used in\nnon browser environments too.\n\n```js\nimport { bindKey, bindKeyCombo } from '@rwh/keystrokes'\n\nbindKey('a', () =\u003e\n  console.log('You\\'re pressing \"a\"'))\n\nbindKeyCombo('control \u003e y, r', () =\u003e\n  console.log('You pressed \"control\" then \"y\", released both, and are pressing \"r\"'))\n```\n\n## Installation\n\nKeystrokes is available on [npm][npm]. This works great when using a build\nsystem like [Parcel][parcel], [Vite][vite], [Turbopack][turbopack], or\n[webpack][webpack].\n\n```sh\nnpm install @rwh/keystrokes\n```\n\n```js\nimport { bindKey } from '@rwh/keystrokes'\nbindKey('a', () =\u003e console.log('you pressed a'))\n```\n\nIf node modules aren't an option for you, you can use an npm CDN such as\n[jsDelivr][jsdelivr] or [UNPKG][unpkg].\n\n```html\n\u003c!-- ESM --\u003e\n\u003cscript src=\"https://unpkg.com/browse/@rwh/keystrokes@latest/dist/keystrokes.js\"\u003e\n\u003c!-- UMD --\u003e\n\u003cscript src=\"https://unpkg.com/browse/@rwh/keystrokes@latest/dist/keystrokes.umd.cjs\"\u003e\n\u003cscript\u003e\nkeystrokes.bindKey('a', () =\u003e console.log('you pressed a'))\n\u003c/script\u003e\n```\n\n## Available Key Names\n\nKeystrokes is environment agnostic, but by default will use built-in browser\nbindings. These bindings use the standard values for the\n[KeyboardEvent.key][key] property.\n\nTo see what key names can be used in your bindings see MDN's\n[table of key values][key-names].\n\nIt is also possible to use [KeyboardEvent.code][code] should you care more\nabout the key's location on the keyboard than it's value. To use code instead\nof key, prepend the key name with `@`. For example `@KeyW` binds the\nphysical key labeled `W` on a QWERTY keyboard, `Z` on a AZERTY keyboard, and\n`\u003c` on a Dvorak keyboard - the same physical key by position on all of these\nkeyboards.\n\nTo see what code names can be used in your bindings see MDN's\n[table of code values][code-names]. Don't forget to prepend code names with `@`\nwhen using them in bindings.\n\nNote that if you need to bind a combo containing a key name like `+` which is\nused as a combo operator, you will need to escape the character using backslash.\n\n## Available Combo Operators\n\nWhen using bindKeyCombo you will need to provide a combo expression. Combo\nexpressions are made of units, and sequences.\n\nHere is a table of operators:\n\nOperator | Description\n---------|----------------------------------------------------\n`+`      | Key name separator - separates keys in a unit\n`\u003e`      | unit separator - separates units in a sequence\n`,`      | sequence separator - separates sequences in a combo\n`\\`      | Escapes a key name that is the same as an operator\n\n### Combo Unit\n\nA combo unit is a grouping of key names separated by the `+` operator. Keys in\na unit can be pressed in any order. One or more units make up a sequence, and\nare separated by the `\u003e` operator.\n\nIn order for a unit to be satisfied all keys in the unit must be pressed, along\nwith keys from units from earlier in the sequence. Units must be satisfied in\norder. For example the combo `a + b \u003e c` can only be satified if unit `a + b`\nis pressed, then unit `c` (without releasing a and b).\n\n### Combo Sequence\n\nSequences are made up of units and are separated by the `,` operator. They\nenable the creation of multi-step combos. For example `a + b \u003e c, x \u003e y` is a\ncombo made up of two sequences - `a + b \u003e c` and `x \u003e y`. Each of these contain\ntwo units `a + b` and `c` for the first, and `x` and `y` for the second.\nIn order to satify this combo, a and b must be pressed (in any order), then\nc, then all must be released, then x, then y.\n\n## Binding Keys and Key Combos\n\nKeystrokes exports a bindKey and bindKeyCombo function. These function will bind\na handler function, or handler object to a key or key combo.\n\nTo bind a key you simply need to pass a key name to bindKey along with a\nhandler function or object. As mentioned previously, the key names available\ndepend on the bindings you are using. By default browser bindings are used, and\nthese recognize the standard values for the [KeyboardEvent.key][key] property.\n\nTo bind combos you can pass a combo expression to bindKeyCombo. bindKeyCombo,\nlike bindKey, will work with key names recognized by the bindings assigned to\nKeystrokes, and as mentioned these are browser bindings by default.\n\n```js\nimport { bindKey, bindKeyCombo } from '@rwh/keystrokes'\n\nbindKey('a', () =\u003e\n  console.log('You\\'re pressing \"a\"'))\n\nbindKeyCombo('control \u003e y, r', () =\u003e\n  console.log('You pressed \"control\" then \"y\", released both, and are pressing \"r\"'))\n\nbindKey('a', {\n  onPressed: () =\u003e console.log('You pressed \"a\"'),\n  onPressedWithRepeat: () =\u003e console.log('You\\'re pressing \"a\"'),\n  onReleased: () =\u003e console.log('You released \"a\"'),\n})\n\nbindKeyCombo('control \u003e y, r', {\n  onPressed: () =\u003e console.log('You pressed \"control\" then \"y\", released both, then pressed \"r\"'),\n  onPressedWithRepeat: () =\u003e console.log('You pressed \"control\" then \"y\", released both, and are pressing \"r\"'),\n  onReleased: () =\u003e console.log('You released \"r\"'),\n})\n```\n\nNote that when you pass a function handler instead of an object handler, it is\nshort hand for passing an object handler with a `onPressedWithRepeat` method.\n\n```js\nconst handler = () =\u003e console.log('You pressed \"control\" then \"y\", released both, and are pressing \"r\"')\n\nbindKeyCombo('control \u003e y, r', handler)\n// ...is shorthand for...\nbindKeyCombo('control \u003e y, r', { onPressedWithRepeat: handler })\n```\n\n## Unbinding Keys and Key Combos\n\nIn more complex applications it's likely you'll need to unbind handlers, such\nas when you change your view. In order to do so you just need to keep a\nreference to the handler so you can unbind it.\n\n```js\nimport { bindKeyCombo, unbindKeyCombo } from '@rwh/keystrokes'\n\nconst handler = () =\u003e ...\n\n// bind the combo to the handler\nbindKeyCombo('control \u003e y, r', handler)\n\n// ...and some time later...\n\n// unbind the handler\nunbindKeyCombo('control \u003e y, r', handler)\n```\n\nYou can also wipe out all bound handlers on a combo by excluding a handler\nreference.\n\n```js\n// unbind all handlers for the combo 'control \u003e y, r'\nunbindKeyCombo('control \u003e y, r')\n```\n\n## Checking Keys and Key Combos\n\nIf you have a situation where you want to check if a key or key combo is\npressed at anytime you can do so with `checkKey` and/or `checkKeyCombo`\n\n```js\nimport { checkKey, checkKeyCombo } from '@rwh/keystrokes'\n\n// keyIsPressed will be true if a is pressed, and false otherwise\nconst keyIsPressed = checkKey('a')\n\n// keyComboIsPressed will be true if control then y was pressed and r is pressed.\n// It will be false otherwise.\nconst keyComboIsPressed = checkKeyCombo('control \u003e y, r')\n```\n\n## Using Keystrokes with React\n\nKeystrokes has it's own react specific package with a few goodies.\n\n```sh\nnpm install @rwh/keystrokes @rwh/react-keystrokes\n```\n\nYou will find two hooks, `useKey` and `useKeyCombo`, as well as an optional\ncontext provider which allows using these hooks with custom keystrokes\ninstances.\n\nUsing it to track key or key combo states is rather easy.\n\n```js\nimport { useEffect, useState } from 'react'\nimport { useKey, useKeyCombo } from '@rwh/react-keystrokes'\n\nexport const Component = () =\u003e {\n\n  const isComboPressed = useKeyCombo('a + b')\n  const isKeyPressed = useKeyCombo('c')\n\n  /* ... */\n}\n```\n\nBy default the hooks will use the global instance of keystrokes.\n\nTo use a custom instance of keystrokes you can wrap components using `useKey`\nand/or `useKeyCombo` with `\u003cKeystrokesProvider\u003e`. This component allows\nyou to pass a custom instance of keystrokes, and all hooks rendered under it\nwill use the provided instance instead of the global one.\n\nSee [Creating Instances](#creating-instances) for more information on creating\ncustom keystrokes instances.\n\n```js\nimport { useEffect, useState } from 'react'\nimport { Keystrokes } from '@rwh/keystrokes'\nimport { Keystrokes, KeystrokesProvider, useKey, useKeyCombo } from '@rwh/react-keystrokes'\n\nexport const Component = () =\u003e {\n\n  const isComboPressed = useKeyCombo('a + b')\n  const isKeyPressed = useKeyCombo('c')\n\n  /* ... */\n}\n\nexport const App = () =\u003e {\n\n  const keystrokes = new Keystrokes({ /* custom options */ })\n\n  return (\n    \u003cKeystrokesProvider keystrokes={keystrokes}\u003e\n      \u003cComponent /\u003e\n    \u003c/KeystrokesProvider\u003e\n  )\n}\n```\n\n## Using Keystrokes with Vue\n\nLike the react package there is also one for vue which is pretty\nsimilar to the react package, but with vue appropriate details.\n\n```sh\nnpm install @rwh/keystrokes @rwh/vue-keystrokes\n```\n\nYou will find two composables, `useKey` and `useKeyCombo`, as well as\nan optional composable, `useKeystrokes`, which acts as a context\nprovider allowing the use of these composables with a custom keystrokes\ninstance.\n\nUsing it to track key or key combo states is rather easy.\n\n```vue\n\u003cscript setup\u003e\nimport { useKey, useKeyCombo } from '@rwh/vue-keystrokes'\n\nconst isPressedCombo = useKeyCombo('a+b')\nconst isPressedkey = useKeyCombo('a')\n\n...\n\u003c/script\u003e\n```\n\nBy default the hooks will use the global instance of keystrokes.\n\nTo use a custom instance of keystrokes you can wrap components using `useKey`\nand/or `useKeyCombo` with a parent vue component which calls `useKeystrokes`.\nThis composable accepts an instance of keystrokes as it's first argument.\nThe passed instance of keystrokes will be used by all composables in decendant\ncomponents.\n\nSee [Creating Instances](#creating-instances) for more information on creating\ncustom keystrokes instances.\n\n```vue\n\u003cscript setup\u003e\n  import { Keystrokes } from '@rwh/keystrokes'\n  import { useKeystrokes } from '@rwh/vue-keystrokes'\n  const keystrokes = new Keystrokes({ /* custom options */ })\n\n  useKeystrokes(keystrokes)\n  \n  ...\n\u003c/script\u003e\n```\n\n## Testing your Keystrokes bindings\n\nKeystrokes also exports a function, `createTestKeystrokes`, which creates an\ninstance of Keystrokes modified for test cases. It has four additional methods\nfor controlling the internal state.\n\n```js\nimport assert from 'assert'\nimport { createTestKeystrokes } from '@rwh/keystrokes'\n\ndescribe('MyApp', () =\u003e {\n  it('correctly handles the key combo', () =\u003e {\n    const keystrokes = createTestKeystrokes()\n\n    const app = new MyApp({ keystrokes })\n\n    keystrokes.press({ key: 'a' })\n    keystrokes.press({ key: 'b' })\n\n    await app.update()\n\n    assert(app.didComboBoundThing)\n  })\n})\n```\n\nIf your app uses the global instance of keystrokes then this can be used in\nconjunction with `setGlobalKeystrokes`.\n\n```js\nimport { describe, it, expect } from 'vitest'\nimport { createTestKeystrokes, setGlobalKeystrokes } from '@rwh/keystrokes'\n\ndescribe('MyApp', () =\u003e {\n  it('correctly handles the key combo', () =\u003e {\n    const keystrokes = createTestKeystrokes()\n    setGlobalKeystrokes(keystrokes)\n\n    const app = new MyApp()\n\n    keystrokes.press({ key: 'a' })\n    keystrokes.press({ key: 'b' })\n\n    await app.update()\n\n    expect(app.didComboBoundThing).toBe(true)\n  })\n})\n```\n\n## Creating Instances\n\nIf you'd rather create your own instances of Keystrokes, rather than using the\nglobal instance, you can do so by constructing the Keystrokes class. Keystrokes\nclass instance has all of the functions we've looked at above as methods.\n\n```js\nimport { Keystrokes } from '@rwh/keystrokes'\n\nconst keystrokes = new Keystrokes()\n\n// All of the functions we've reviewed above are methods on the instance\nkeystrokes.bindKey(...)\nkeystrokes.bindKeyCombo(...)\nkeystrokes.unbindKey(...)\nkeystrokes.unbindKeyCombo(...)\nkeystrokes.checkKey(...)\nkeystrokes.checkKeyCombo(...)\n\n```\n\nIf you want to go this route you won't have to worry about overhead from the\nglobal instance as it is only created if you use the exported functions\nassociated with it.\n\n## Configuration Options\n\nKeystrokes has a few configuration options that you can configure by passing\nthem to the `Keystrokes` constructor, or by calling the\n`setGlobalKeystrokesOptions` before using any of the functions exported by the\npackage associated with the global instance.\n\n### Available Options\n\n  selfReleasingKeys?: string[]\n  keyRemap?: Record\u003cstring, string\u003e\n\nOption            | Description\n------------------|------------------------------------------\nselfReleasingKeys | Key names added to selfReleasingKeys will be marked as released after any other key is released. Provided to deal with buggy platforms.\nkeyRemap          | An object of key value pairs with the key being the key to rename, and the value being the new name.\nonActive          | A binder to track viewport focus. See [Non Browser Environments](#non-browser-environments) for details.\nonInactive        | A binder to track viewport blur. See [Non Browser Environments](#non-browser-environments) for details.\nonKeyPressed      | A binder to track when keys are pressed. See [Non Browser Environments](#non-browser-environments) for details.\nonKeyReleased     | A binder to track when keys are released. See [Non Browser Environments](#non-browser-environments) for details.\n\nHere is an example where we are configuring the global instance.\n\n```js\nimport { bindKey, setGlobalKeystrokesOptions } from '@rwh/keystrokes'\n\n// Must be called before binding or checking keys or key combos\nsetGlobalKeystrokesOptions({\n  keyRemap: { ' ': 'spacebar' }\n})\n\nbindKey(...)\n```\n\nAnd here is an example where we are passing the options to the `Keystrokes`\nconstructor. These options will only effect the constructed instance.\n\n```js\nimport { Keystrokes } from '@rwh/keystrokes'\n\nconst keystrokes = new Keystrokes({\n  keyRemap: { ' ': 'spacebar' }\n})\n\nkeystrokes.bindKey(...)\n```\n\n## Non Browser Environments\n\nShould you wish to use Keystrokes in a non browser environment, you can do\nso with the use of the `onActive`, `onInactive`, `onKeyPressed`, and\n`onKeyReleased` binder options. Binders are functions that are called by\nkeystrokes when constructed. The binder is passed a handler function. Your\nbinder is expected to call this handler whenever the event associated with the\nbinder occurs. Binders may also return a function which will be called when the\nlibrary is unbound from the environment.\n\nBy default Keystrokes will internally setup binders that work with browser\nenvironments if you do not provide your own. This results in the same behavior\nas the following code.\n\n```js\nimport { Keystrokes } from '@rwh/keystrokes'\n\nconst keystrokes = new Keystrokes({\n  onActive: handler =\u003e {\n    const listener = () =\u003e handler()\n    window.addEventListener('focus', listener)\n    return () =\u003e {\n      window.removeEventListener('focus', listener)\n    }\n  },\n  onInactive: handler =\u003e {\n    const listener = () =\u003e handler()\n    window.addEventListener('blur', listener)\n    return () =\u003e {\n      window.removeEventListener('blur', listener)\n    }\n  },\n  onKeyPressed: handler =\u003e {\n    const listener = event =\u003e handler({ key: event.key, originalEvent: event })\n    window.addEventListener('keydown', listener)\n    return () =\u003e {\n      window.removeEventListener('keydown', listener)\n    }\n  },\n  onKeyReleased: handler =\u003e {\n    const listener = event =\u003e handler({ key: event.key, originalEvent: event })\n    window.addEventListener('keyup', listener)\n    return () =\u003e {\n      window.removeEventListener('keyup', listener)\n    }\n  }\n})\n\nkeystrokes.bindKey(...)\n```\n\n## Help Welcome\n\nIf you want to support this project by throwing me some coffee money It's\ngreatly appreciated.\n\n[![sponsor](https://img.shields.io/static/v1?label=Sponsor\u0026message=%E2%9D%A4\u0026logo=GitHub\u0026color=%23fe8e86)](https://github.com/sponsors/RobertWHurst)\n\nIf your interested in providing feedback or would like to contribute please feel\nfree to do so. I recommend first [opening an issue][feature-request] expressing\nyour feedback or intent to contribute a change, from there we can consider your\nfeedback or guide your contribution efforts. Any and all help is greatly\nappreciated since this is an open source effort after all.\n\nThank you!\n\n[npm]: https://www.npmjs.com\n[parcel]: https://parceljs.org\n[vite]: https://vitejs.dev\n[turbopack]: https://turbo.build/pack\n[webpack]: https://webpack.js.org\n[jsdelivr]: https://www.jsdelivr.com/package/npm/@rwh/keystrokes\n[unpkg]: https://unpkg.com/browse/@rwh/keystrokes@latest/\n[key-names]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\n[key]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n[code]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n[code-names]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values\n[bug-report]: https://github.com/RobertWHurst/Keystrokes/issues/new?template=bug_report.md\n[feature-request]: https://github.com/RobertWHurst/Keystrokes/issues/new?template=feature_request.md\n","funding_links":["https://github.com/sponsors/RobertWHurst"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertwhurst%2Fkeystrokes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobertwhurst%2Fkeystrokes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertwhurst%2Fkeystrokes/lists"}