├── .coffeelintignore ├── .eslintignore ├── spec ├── fixtures │ ├── a.cson │ ├── b.cson │ ├── os2.cson │ ├── c.darwin.cson │ └── d.os2.cson ├── helpers │ ├── setup.js │ ├── helpers.js │ └── keymaps │ │ ├── linux-dvorak.json │ │ ├── linux-swiss-german.json │ │ ├── windows-swedish.json │ │ ├── windows-us-international.json │ │ ├── windows-swiss-german.json │ │ ├── mac-swedish.json │ │ ├── mac-russian-pc.json │ │ ├── mac-turkish.json │ │ ├── mac-swiss-german.json │ │ ├── mac-dvorak-qwerty-cmd.json │ │ ├── mac-greek.json │ │ └── mac-hebrew.json ├── partial-keyup-matcher-spec.js ├── key-binding-spec.coffee └── helpers-spec.coffee ├── .babelrc ├── .gitignore ├── .npmignore ├── .eslintrc ├── Gruntfile.coffee ├── .github ├── workflows │ └── ci.yml └── no-response.yml ├── coffeelint.json ├── src ├── command-event.coffee ├── partial-keyup-matcher.js ├── key-binding.coffee ├── us-keymap.coffee ├── slovak-cmd-keymap.coffee ├── slovak-qwerty-cmd-keymap.coffee ├── helpers.coffee └── keymap-manager.coffee ├── LICENSE.md ├── benchmark └── benchmark.coffee ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── README.md └── package.json /.coffeelintignore: -------------------------------------------------------------------------------- 1 | spec/fixtures 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/fixtures/*.js 2 | -------------------------------------------------------------------------------- /spec/fixtures/a.cson: -------------------------------------------------------------------------------- 1 | ".a": 2 | 'ctrl-a': 'x' 3 | -------------------------------------------------------------------------------- /spec/fixtures/b.cson: -------------------------------------------------------------------------------- 1 | ".b": 2 | 'ctrl-a': 'y' 3 | -------------------------------------------------------------------------------- /spec/fixtures/os2.cson: -------------------------------------------------------------------------------- 1 | ".e": 2 | 'ctrl-a': 'Y' 3 | -------------------------------------------------------------------------------- /spec/fixtures/c.darwin.cson: -------------------------------------------------------------------------------- 1 | ".c": 2 | 'ctrl-a': 'z' 3 | -------------------------------------------------------------------------------- /spec/fixtures/d.os2.cson: -------------------------------------------------------------------------------- 1 | ".d": 2 | 'ctrl-a': 'X' 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "sourceMaps": "inline" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | .DS_Store 4 | .idea 5 | atom 6 | .coffee/ 7 | api.json 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .coffee 3 | .DS_Store 4 | .eslintignore 5 | .eslintrc 6 | .gitignore 7 | .npmignore 8 | .travis.yml 9 | benchmark 10 | Gruntfile.coffee 11 | npm-debug.log 12 | script 13 | spec 14 | src 15 | -------------------------------------------------------------------------------- /spec/helpers/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | require('coffee-script/register') 3 | 4 | global.assert = require('chai').assert 5 | 6 | if (process.env.SUPPRESS_EXIT) { 7 | process.exit = function (code) {} 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | }, 9 | "plugins": [ 10 | "promise" 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | shell: 4 | 'update-atomdoc': 5 | command: 'npm update grunt-atomdoc donna tello atomdoc' 6 | options: 7 | stdout: true 8 | stderr: true 9 | failOnError: true 10 | 11 | grunt.loadNpmTasks('grunt-shell') 12 | grunt.loadNpmTasks('grunt-atomdoc') 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | Test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: '14' 19 | - name: Install dependencies 20 | run: npm i 21 | - name: Rebuild dependencies for Electron 22 | run: npx electron-rebuild 23 | - name: Run tests 24 | uses: GabrielBB/xvfb-action@v1 25 | with: 26 | run: npm test 27 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 28 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level": "ignore" 4 | }, 5 | "no_empty_param_list": { 6 | "level": "error" 7 | }, 8 | "arrow_spacing": { 9 | "level": "error" 10 | }, 11 | "no_interpolation_in_single_quotes": { 12 | "level": "error" 13 | }, 14 | "no_debugger": { 15 | "level": "error" 16 | }, 17 | "prefer_english_operator": { 18 | "level": "error" 19 | }, 20 | "colon_assignment_spacing": { 21 | "spacing": { 22 | "left": 0, 23 | "right": 1 24 | }, 25 | "level": "error" 26 | }, 27 | "braces_spacing": { 28 | "spaces": 0, 29 | "level": "error" 30 | }, 31 | "spacing_after_comma": { 32 | "level": "error" 33 | }, 34 | "no_stand_alone_at": { 35 | "level": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/command-event.coffee: -------------------------------------------------------------------------------- 1 | # This custom subclass of CustomEvent exists to provide the ::abortKeyBinding 2 | # method, as well as versions of the ::stopPropagation methods that record the 3 | # intent to stop propagation so event bubbling can be properly simulated for 4 | # detached elements. 5 | # 6 | # CustomEvent instances are exotic objects, meaning the CustomEvent constructor 7 | # *must* be called with an exact CustomEvent instance. We work around this fact 8 | # by building a CustomEvent directly, then injecting this object into the 9 | # prototype chain by setting its __proto__ property. 10 | module.exports = 11 | class CommandEvent extends CustomEvent 12 | keyBindingAborted: false 13 | propagationStopped: false 14 | 15 | abortKeyBinding: -> 16 | @stopImmediatePropagation() 17 | @keyBindingAborted = true 18 | 19 | stopPropagation: -> 20 | @propagationStopped = true 21 | super 22 | 23 | stopImmediatePropagation: -> 24 | @propagationStopped = true 25 | super 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /benchmark/benchmark.coffee: -------------------------------------------------------------------------------- 1 | helpers = require '../src/helpers' 2 | 3 | count = 0 4 | 5 | normalize = (keystrokes) -> 6 | count++ 7 | helpers.normalizeKeystrokes(keystrokes) 8 | 9 | start = Date.now() 10 | 11 | for letter in 'abcdefghijklmnopqrztuvwxyz0123456789' 12 | normalize(letter) 13 | normalize("shift-#{letter}") 14 | 15 | normalize("cmd-#{letter}") 16 | normalize("cmd-shift-#{letter}") 17 | normalize("cmd-alt-#{letter}") 18 | normalize("cmd-alt-ctrl-#{letter}") 19 | normalize("cmd-alt-ctrl-shfit-#{letter}") 20 | normalize("cmd-#{letter} cmd-v") 21 | 22 | normalize("alt-#{letter}") 23 | normalize("alt-shift-#{letter}") 24 | normalize("alt-cmd-#{letter}") 25 | normalize("alt-cmd-ctrl-#{letter}") 26 | normalize("alt-cmd-ctrl-shift-#{letter}") 27 | normalize("alt-#{letter} alt-v") 28 | 29 | normalize("ctrl-#{letter}") 30 | normalize("ctrl-shift-#{letter}") 31 | normalize("ctrl-alt-#{letter}") 32 | normalize("ctrl-alt-cmd-#{letter}") 33 | normalize("ctrl-alt-cmd-shift#{letter}") 34 | normalize("ctrl-#{letter} ctrl-v") 35 | 36 | console.log "Normalized #{count} keystrokes in #{Date.now() - start}ms" 37 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. 4 | * All new code requires tests to ensure against regressions 5 | 6 | ### Description of the Change 7 | 8 | 13 | 14 | ### Alternate Designs 15 | 16 | 17 | 18 | ### Benefits 19 | 20 | 21 | 22 | ### Possible Drawbacks 23 | 24 | 25 | 26 | ### Applicable Issues 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/partial-keyup-matcher.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | module.exports = 4 | class PartialKeyupMatcher { 5 | 6 | constructor () { 7 | this._pendingMatches = new Set() 8 | } 9 | 10 | addPendingMatch (keyBinding) { 11 | this._pendingMatches.add(keyBinding) 12 | keyBinding['nextKeyUpIndex'] = 0 13 | } 14 | 15 | // Returns matching bindingss, if any. 16 | // Updates state for next match. 17 | getMatches (userKeyupKeystroke) { 18 | userKeyupKeystroke = this._normalizeKeystroke(userKeyupKeystroke) 19 | let matches = new Set() 20 | 21 | // Loop over each pending keyup match. 22 | for (const keyBinding of this._pendingMatches) { 23 | const bindingKeystrokeToMatch = this._normalizeKeystroke( 24 | keyBinding.getKeyups()[keyBinding['nextKeyUpIndex']] 25 | ) 26 | if (userKeyupKeystroke === bindingKeystrokeToMatch) { 27 | this._updateStateForMatch(matches, keyBinding) 28 | } 29 | } 30 | return [...matches] 31 | } 32 | 33 | /** Private Section **/ 34 | 35 | _normalizeKeystroke (keystroke) { 36 | if (keystroke[0] === '^') return keystroke.substring(1) 37 | return keystroke 38 | } 39 | 40 | _updateStateForMatch (matches, keyBinding) { 41 | if (keyBinding['nextKeyUpIndex'] === keyBinding.getKeyups().length - 1) { 42 | // Full match. Remove and return it. 43 | this._pendingMatches.delete(keyBinding) 44 | matches.add(keyBinding) 45 | } else { 46 | // Partial match. Increment what we're looking for next. 47 | keyBinding['nextKeyUpIndex']++ 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Prerequisites 10 | 11 | * [ ] Put an X between the brackets on this line if you have done all of the following: 12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode 13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ 14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq 15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom 16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages 17 | 18 | ### Description 19 | 20 | [Description of the issue] 21 | 22 | ### Steps to Reproduce 23 | 24 | 1. [First Step] 25 | 2. [Second Step] 26 | 3. [and so on...] 27 | 28 | **Expected behavior:** [What you expect to happen] 29 | 30 | **Actual behavior:** [What actually happens] 31 | 32 | **Reproduces how often:** [What percentage of the time does it reproduce?] 33 | 34 | ### Versions 35 | 36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. 37 | 38 | ### Additional Information 39 | 40 | Any additional information, configuration or data that might be necessary to reproduce the issue. 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # Atom keymap 3 | [![Build Status](https://travis-ci.org/atom/atom-keymap.svg?branch=master)](https://travis-ci.org/atom/atom-keymap) 4 | [![Dependency Status](https://david-dm.org/atom/atom-keymap.svg)](https://david-dm.org/atom/atom-keymap) 5 | 6 | Atom's DOM-aware keymap module 7 | 8 | ```js 9 | var KeymapManager, keymaps; 10 | KeymapManager = require('atom-keymap') 11 | 12 | keymaps = new KeymapManager 13 | keymaps.defaultTarget = document.body 14 | 15 | // Pass all the window's keydown events to the KeymapManager 16 | document.addEventListener('keydown', function(event) { 17 | keymaps.handleKeyboardEvent(event) 18 | }) 19 | 20 | // Add some keymaps 21 | keymaps.loadKeymap('/path/to/keymap-file.json') // can also be a directory of json / cson files 22 | // OR 23 | keymaps.add('/key/for/these/keymaps', { 24 | "body": { 25 | "up": "core:move-up", 26 | "down": "core:move-down" 27 | } 28 | }) 29 | 30 | // When a keybinding is triggered, it will dispatch it on the node that was focused 31 | window.addEventListener('core:move-up', (event) => console.log('up', event)) 32 | window.addEventListener('core:move-down', (event) => console.log('down', event)) 33 | ``` 34 | 35 | ## Development 36 | 37 | The tests for this module *must* be run in Electron because they depend on browser APIs. 38 | 39 | * [`devtool`](https://github.com/Jam3/devtool) is bundled as a development dependency to run the tests. 40 | * Native modules need to be compiled against the version of Electron included with `devtool`. **Be sure to run `electron-rebuild` be sure recompile native dependencies before running tests.** 41 | * Tests can be run in batch mode with `npm test` 42 | * If you want to use the debugger, profiler, etc or just speed up your flow by being able to refresh the `devtool` window to re-run tests, use the `npm run test-drive` script. This will keep `devtool` open instead of exiting after the test run. 43 | -------------------------------------------------------------------------------- /spec/partial-keyup-matcher-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* eslint-env mocha */ 3 | /* global assert */ 4 | 5 | const PartialKeyupMatcher = require('../src/partial-keyup-matcher.js') 6 | import {KeyBinding} from '../src/key-binding' 7 | 8 | describe('PartialKeyupMatcher', () => { 9 | it('returns a simple single-modifier-keyup match', () => { 10 | const matcher = new PartialKeyupMatcher() 11 | const kb = keyBindingArgHelper('ctrl-tab ^ctrl') 12 | matcher.addPendingMatch(kb) 13 | const matches = matcher.getMatches('^ctrl') 14 | assert.equal(matches.length, 1) 15 | assert.equal(matches[0], kb) 16 | it('removes match returned', () => { 17 | const matches = matcher.getMatches('^ctrl') 18 | assert.equal(matches.length, 0) 19 | }) 20 | }) 21 | 22 | it('does not match multiple keyup binding on single keyup events', () => { 23 | const matcher = new PartialKeyupMatcher() 24 | const kb = keyBindingArgHelper('ctrl-shift-tab ^ctrl-shift') 25 | matcher.addPendingMatch(kb) 26 | let matches = matcher.getMatches('^ctrl') 27 | assert.equal(matches.length, 0) 28 | matches = matcher.getMatches('^shift') 29 | assert.equal(matches.length, 0) 30 | }) 31 | 32 | it('for multi-keystroke bindings, matches only when all keyups are received', () => { 33 | const matcher = new PartialKeyupMatcher() 34 | const kb = keyBindingArgHelper('ctrl-shift-tab ^ctrl ^shift') 35 | matcher.addPendingMatch(kb) 36 | matches = matcher.getMatches('^shift') // no-op should return no match 37 | assert.equal(matches.length, 0) 38 | // should return no match but set state to match on next ^ctrl 39 | let matches = matcher.getMatches('^ctrl') 40 | assert.equal(matches.length, 0) 41 | matches = matcher.getMatches('^shift') 42 | assert.equal(matches.length, 1) 43 | assert.equal(matches[0], kb) 44 | it('removes match returned', () => { 45 | const matches = matcher.getMatches('^ctrl') 46 | assert.equal(matches.length, 0) 47 | }) 48 | }) 49 | }) 50 | 51 | function keyBindingArgHelper (binding) { 52 | return new KeyBinding('test', 'test', binding, 'body', 0) 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-keymap", 3 | "version": "8.2.15", 4 | "description": "Atom's DOM-aware keymap module", 5 | "main": "./lib/keymap-manager", 6 | "scripts": { 7 | "prepare": "npm run clean && npm run compile && npm run lint && npm run atomdoc", 8 | "clean": "rimraf lib && rimraf api.json", 9 | "compile": "coffee --no-header --output lib --compile src && babel src --out-dir lib", 10 | "lint": "coffeelint -r src spec && eslint src spec", 11 | "test": "electron-mocha --renderer --preload spec/helpers/setup.js spec/*", 12 | "test-drive": "SUPPRESS_EXIT=true electron-mocha --no-colors --renderer --interactive --preload spec/helpers/setup.js spec/*", 13 | "ci": "npm run compile && npm run lint && npm run test", 14 | "atomdoc": "grunt shell:update-atomdoc atomdoc" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/atom/atom-keymap.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/atom/atom-keymap/issues" 22 | }, 23 | "license": "MIT", 24 | "dependencies": { 25 | "clear-cut": "^2", 26 | "emissary": "^1.1.0", 27 | "event-kit": "^1.0.0", 28 | "fs-plus": "^3.0.0", 29 | "grim": "^1.2.1", 30 | "keyboard-layout": "2.0.17", 31 | "pathwatcher": "^8.0.0", 32 | "property-accessors": "^1", 33 | "season": "^6.0.2" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "6.5.1", 37 | "babel-eslint": "5.0.0-beta10", 38 | "babel-preset-es2015": "6.5.0", 39 | "babel-preset-stage-0": "6.5.0", 40 | "babel-register": "6.5.2", 41 | "chai": "3.5.0", 42 | "coffee-script": "1.7.0", 43 | "coffeelint": "1.16.0", 44 | "debounce": "1.0.0", 45 | "electron": "^4.2.12", 46 | "electron-mocha": "^6.0.4", 47 | "electron-rebuild": "^2.3.5", 48 | "eslint": "2.2.0", 49 | "eslint-config-standard": "5.1.0", 50 | "eslint-plugin-promise": "1.0.8", 51 | "eslint-plugin-standard": "1.3.1", 52 | "grunt": "0.4.1", 53 | "grunt-atomdoc": "1.0.0", 54 | "grunt-cli": "0.1.8", 55 | "grunt-contrib-coffee": "0.9.0", 56 | "grunt-shell": "0.2.2", 57 | "lolex": "1.4.0", 58 | "rimraf": "2.2.2", 59 | "sinon": "1.17.3", 60 | "space-pencil": "0.3.0", 61 | "temp": "0.8.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /spec/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | /* global beforeEach, afterEach */ 2 | 3 | 'use strict' 4 | 5 | import lolex from 'lolex' 6 | import sinon from 'sinon' 7 | 8 | let sinonSandbox, fakeClock, processPlatform, originalProcessPlatform 9 | 10 | originalProcessPlatform = process.platform 11 | processPlatform = process.platform 12 | Object.defineProperty(process, 'platform', {get: () => processPlatform}) 13 | 14 | beforeEach(function () { 15 | document.body.innerHTML = '' 16 | sinonSandbox = sinon.sandbox.create() 17 | fakeClock = lolex.install() 18 | }) 19 | 20 | afterEach(function () { 21 | fakeClock.uninstall() 22 | sinonSandbox.restore() 23 | processPlatform = originalProcessPlatform 24 | }) 25 | 26 | export function appendContent (element) { 27 | document.body.appendChild(element) 28 | return element 29 | } 30 | 31 | export function stub () { 32 | return sinonSandbox.stub(...arguments) 33 | } 34 | 35 | export function getFakeClock () { 36 | return fakeClock 37 | } 38 | 39 | export function mockProcessPlatform (platform) { 40 | processPlatform = platform 41 | } 42 | 43 | export function buildKeydownEvent (props) { 44 | return buildKeyboardEvent('keydown', props) 45 | } 46 | 47 | export function buildKeyupEvent (props) { 48 | return buildKeyboardEvent('keyup', props) 49 | } 50 | 51 | export function buildKeyboardEvent (type, props) { 52 | let {key, code, ctrlKey, shiftKey, altKey, metaKey, target, modifierState} = props 53 | if (!modifierState) modifierState = {} 54 | 55 | if (process.platform === 'darwin') { 56 | if (modifierState.AltGraph) { 57 | altKey = true 58 | } 59 | } else if (process.platform === 'win32') { 60 | if (modifierState.AltGraph) { 61 | ctrlKey = true 62 | altKey = true 63 | } else if (ctrlKey && altKey) { 64 | modifierState.AltGraph = true 65 | } 66 | } 67 | 68 | const event = new KeyboardEvent(type, { 69 | key, code, 70 | ctrlKey, shiftKey, altKey, metaKey, 71 | cancelable: true, bubbles: true 72 | }) 73 | 74 | if (target) { 75 | Object.defineProperty(event, 'target', {get: () => target}) 76 | Object.defineProperty(event, 'path', {get: () => [target]}) 77 | } 78 | 79 | Object.defineProperty(event, 'getModifierState', {value: (key) => { 80 | return !!modifierState[key] 81 | }}) 82 | 83 | return event 84 | } 85 | -------------------------------------------------------------------------------- /spec/key-binding-spec.coffee: -------------------------------------------------------------------------------- 1 | {KeyBinding, MATCH_TYPES} = require '../src/key-binding' 2 | 3 | describe "KeyBinding", -> 4 | describe ".matchesKeystrokes(userKeystrokes)", -> 5 | it "returns 'exact' for exact matches", -> 6 | assert.equal(keyBindingArgHelper('ctrl-tab ^tab ^ctrl').matchesKeystrokes(['ctrl-tab', '^tab', '^ctrl']), 'exact') 7 | assert.equal(keyBindingArgHelper('ctrl-tab ^ctrl').matchesKeystrokes(['ctrl-tab', '^tab', '^ctrl']), 'exact') 8 | assert.equal(keyBindingArgHelper('a b c').matchesKeystrokes(['a', '^a', 'b', '^b', 'c']), 'exact') 9 | assert.equal(keyBindingArgHelper('a b ^b c').matchesKeystrokes(['a', '^a', 'b', '^b', 'c']), 'exact') 10 | assert.equal(keyBindingArgHelper('a ^').matchesKeystrokes(['a', '^a', '^']), 'exact') 11 | 12 | it "returns false for non-matches", -> 13 | assert.equal(keyBindingArgHelper('ctrl-tab ^tab').matchesKeystrokes(['ctrl-tab', '^tab', '^ctrl']), false) 14 | assert.equal(keyBindingArgHelper('a b c').matchesKeystrokes(['a', '^a', 'b', '^b', 'c', '^c']), false) 15 | assert.equal(keyBindingArgHelper('a b ^b c').matchesKeystrokes(['a', '^a', 'b', '^b', 'c', '^c']), false) 16 | 17 | assert.equal(keyBindingArgHelper('a').matchesKeystrokes(['a', '^a', 'b', '^b', 'c', '^c']), false) 18 | assert.equal(keyBindingArgHelper('a').matchesKeystrokes(['a', '^a']), false) 19 | assert.equal(keyBindingArgHelper('a c').matchesKeystrokes(['a', '^a', 'b', '^b', 'c', '^c']), false) 20 | assert.equal(keyBindingArgHelper('a b ^d').matchesKeystrokes(['a', '^a', 'b', '^b', 'c', '^c']), false) 21 | assert.equal(keyBindingArgHelper('a d ^d').matchesKeystrokes(['a', '^a', 'b', '^b', 'c', '^c']), false) 22 | assert.equal(keyBindingArgHelper('a d ^d').matchesKeystrokes(['^c']), false) 23 | 24 | it "returns 'partial' for partial matches", -> 25 | assert.equal(keyBindingArgHelper('a b ^b').matchesKeystrokes(['a']), 'partial') 26 | assert.equal(keyBindingArgHelper('a b c').matchesKeystrokes(['a']), 'partial') 27 | assert.equal(keyBindingArgHelper('a b c').matchesKeystrokes(['a', '^a']), 'partial') 28 | assert.equal(keyBindingArgHelper('a b c').matchesKeystrokes(['a', '^a', 'b']), 'partial') 29 | assert.equal(keyBindingArgHelper('a b c').matchesKeystrokes(['a', '^a', 'b', '^b']), 'partial') 30 | assert.equal(keyBindingArgHelper('a b c').matchesKeystrokes(['a', '^a', 'd', '^d']), false) 31 | 32 | it "returns 'partial' correctly for bindings that end in ^", -> 33 | assert.equal(keyBindingArgHelper('g a ^').matchesKeystrokes(['g', '^g', 'a', '^a']), 'partial') 34 | 35 | it "returns MATCH_TYPES.PENDING_KEYUP for bindings that match and contain a remainder of only keyup events", -> 36 | assert.equal(keyBindingArgHelper('a b ^b').matchesKeystrokes(['a', 'b']), MATCH_TYPES.PENDING_KEYUP) 37 | 38 | keyBindingArgHelper = (binding) -> 39 | return new KeyBinding('test', 'test', binding, 'body', 0) 40 | -------------------------------------------------------------------------------- /spec/helpers-spec.coffee: -------------------------------------------------------------------------------- 1 | {normalizeKeystrokes, keystrokesMatch, isModifierKeyup, isKeyup} = require '../src/helpers' 2 | 3 | describe ".normalizeKeystrokes(keystrokes)", -> 4 | it "parses and normalizes the keystrokes", -> 5 | assert.equal(normalizeKeystrokes('ctrl--'), 'ctrl--') 6 | assert.equal(normalizeKeystrokes('ctrl-x'), 'ctrl-x') 7 | assert.equal(normalizeKeystrokes('a'), 'a') 8 | assert.equal(normalizeKeystrokes('shift-a'), 'shift-A') 9 | assert.equal(normalizeKeystrokes('shift-9'), 'shift-9') 10 | assert.equal(normalizeKeystrokes('-'), '-') 11 | assert.equal(normalizeKeystrokes('- -'), '- -') 12 | assert.equal(normalizeKeystrokes('a b'), 'a b') 13 | assert.equal(normalizeKeystrokes('cmd-k cmd-v'), 'cmd-k cmd-v') 14 | assert.equal(normalizeKeystrokes('cmd-cmd'), 'cmd') 15 | assert.equal(normalizeKeystrokes('cmd-shift'), 'shift-cmd') 16 | assert.equal(normalizeKeystrokes('cmd-shift-a'), 'shift-cmd-A') 17 | assert.equal(normalizeKeystrokes('cmd-ctrl-alt--'), 'ctrl-alt-cmd--') 18 | 19 | assert.equal(normalizeKeystrokes('ctrl-y ^y'), 'ctrl-y ^y') 20 | assert.equal(normalizeKeystrokes('ctrl-y ^ctrl-y'), 'ctrl-y ^y') 21 | assert.equal(normalizeKeystrokes('cmd-shift-y ^cmd-shift-y'), 'shift-cmd-Y ^y') 22 | assert.equal(normalizeKeystrokes('ctrl-y ^ctrl-y ^ctrl'), 'ctrl-y ^y ^ctrl') 23 | assert.equal(normalizeKeystrokes('ctrl-y ^ctrl-shift-alt-cmd-y ^ctrl ^shift ^alt ^cmd'), 'ctrl-y ^y ^ctrl ^shift ^alt ^cmd') 24 | assert.equal(normalizeKeystrokes('a b c ^a ^b ^c'), 'a b c ^a ^b ^c') 25 | 26 | assert.equal(normalizeKeystrokes('a-b'), false) 27 | assert.equal(normalizeKeystrokes('---'), false) 28 | assert.equal(normalizeKeystrokes('cmd-a-b'), false) 29 | assert.equal(normalizeKeystrokes('-a-b'), false) 30 | assert.equal(normalizeKeystrokes('ctrl-'), false) 31 | assert.equal(normalizeKeystrokes('--'), false) 32 | assert.equal(normalizeKeystrokes('- '), false) 33 | assert.equal(normalizeKeystrokes('a '), false) 34 | 35 | describe ".isModifierKeyup(keystroke)", -> 36 | it "returns true for single modifier keyups", -> 37 | assert.isTrue(isModifierKeyup('^ctrl')) 38 | assert.isTrue(isModifierKeyup('^shift')) 39 | assert.isTrue(isModifierKeyup('^alt')) 40 | assert.isTrue(isModifierKeyup('^cmd')) 41 | assert.isTrue(isModifierKeyup('^ctrl-shift')) 42 | assert.isTrue(isModifierKeyup('^alt-cmd')) 43 | 44 | it "returns false for modifier keydowns", -> 45 | assert.isFalse(isModifierKeyup('ctrl-x')) 46 | assert.isFalse(isModifierKeyup('shift-x')) 47 | assert.isFalse(isModifierKeyup('alt-x')) 48 | assert.isFalse(isModifierKeyup('cmd-x')) 49 | assert.isFalse(isModifierKeyup('ctrl-shift-x')) 50 | assert.isFalse(isModifierKeyup('alt-cmd-x')) 51 | 52 | describe ".isKeyup(keystrokes)", -> 53 | it "return false for single ^", -> 54 | assert.isFalse(isKeyup('^')) 55 | 56 | it "return true when keystroke starts with ^", -> 57 | assert.isTrue(isKeyup('^a')) 58 | assert.isTrue(isKeyup('^ctrl')) 59 | assert.isTrue(isKeyup('^shift')) 60 | -------------------------------------------------------------------------------- /src/key-binding.coffee: -------------------------------------------------------------------------------- 1 | {calculateSpecificity, MODIFIERS, isKeyup} = require './helpers' 2 | 3 | MATCH_TYPES = { 4 | EXACT: 'exact' 5 | PARTIAL: 'partial' 6 | PENDING_KEYUP: 'pendingKeyup' 7 | } 8 | module.exports.MATCH_TYPES = MATCH_TYPES 9 | 10 | module.exports.KeyBinding = 11 | class KeyBinding 12 | @currentIndex: 1 13 | 14 | enabled: true 15 | 16 | constructor: (@source, @command, @keystrokes, selector, @priority) -> 17 | @keystrokeArray = @keystrokes.split(' ') 18 | @keystrokeCount = @keystrokeArray.length 19 | @selector = selector.replace(/!important/g, '') 20 | @specificity = calculateSpecificity(selector) 21 | @index = @constructor.currentIndex++ 22 | @cachedKeyups = null 23 | 24 | matches: (keystroke) -> 25 | multiKeystroke = /\s/.test keystroke 26 | if multiKeystroke 27 | keystroke is @keystroke 28 | else 29 | keystroke.split(' ')[0] is @keystroke.split(' ')[0] 30 | 31 | compare: (keyBinding) -> 32 | if keyBinding.priority is @priority 33 | if keyBinding.specificity is @specificity 34 | keyBinding.index - @index 35 | else 36 | keyBinding.specificity - @specificity 37 | else 38 | keyBinding.priority - @priority 39 | 40 | # Return the keyup portion of the binding, if any, as an array of 41 | # keystrokes. 42 | getKeyups: -> 43 | return @cachedKeyups if @cachedKeyups? 44 | for keystroke, i in @keystrokeArray 45 | return @cachedKeyups = @keystrokeArray.slice(i) if isKeyup(keystroke) 46 | 47 | # userKeystrokes is an array of keystrokes e.g. 48 | # ['ctrl-y', 'ctrl-x', '^x'] 49 | matchesKeystrokes: (userKeystrokes) -> 50 | userKeystrokeIndex = -1 51 | userKeystrokesHasKeydownEvent = false 52 | matchesNextUserKeystroke = (bindingKeystroke) -> 53 | while userKeystrokeIndex < userKeystrokes.length - 1 54 | userKeystrokeIndex += 1 55 | userKeystroke = userKeystrokes[userKeystrokeIndex] 56 | isKeydownEvent = not isKeyup(userKeystroke) 57 | userKeystrokesHasKeydownEvent = true if isKeydownEvent 58 | if bindingKeystroke is userKeystroke 59 | return true 60 | else if isKeydownEvent 61 | return false 62 | null 63 | 64 | isPartialMatch = false 65 | bindingRemainderContainsOnlyKeyups = true 66 | bindingKeystrokeIndex = 0 67 | for bindingKeystroke in @keystrokeArray 68 | unless isPartialMatch 69 | doesMatch = matchesNextUserKeystroke(bindingKeystroke) 70 | if doesMatch is false 71 | return false 72 | else if doesMatch is null 73 | # Make sure userKeystrokes with only keyup events don't match everything 74 | if userKeystrokesHasKeydownEvent 75 | isPartialMatch = true 76 | else 77 | return false 78 | 79 | if isPartialMatch 80 | bindingRemainderContainsOnlyKeyups = false unless isKeyup(bindingKeystroke) 81 | 82 | # Bindings that match the beginning of the user's keystrokes are not a match. 83 | # e.g. This is not a match. It would have been a match on the previous keystroke: 84 | # bindingKeystrokes = ['ctrl-tab', '^tab'] 85 | # userKeystrokes = ['ctrl-tab', '^tab', '^ctrl'] 86 | return false if userKeystrokeIndex < userKeystrokes.length - 1 87 | 88 | if isPartialMatch and bindingRemainderContainsOnlyKeyups 89 | MATCH_TYPES.PENDING_KEYUP 90 | else if isPartialMatch 91 | MATCH_TYPES.PARTIAL 92 | else 93 | MATCH_TYPES.EXACT 94 | -------------------------------------------------------------------------------- /src/us-keymap.coffee: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A" 5 | }, 6 | "KeyB": { 7 | "unmodified": "b", 8 | "withShift": "B" 9 | }, 10 | "KeyC": { 11 | "unmodified": "c", 12 | "withShift": "C" 13 | }, 14 | "KeyD": { 15 | "unmodified": "d", 16 | "withShift": "D" 17 | }, 18 | "KeyE": { 19 | "unmodified": "e", 20 | "withShift": "E" 21 | }, 22 | "KeyF": { 23 | "unmodified": "f", 24 | "withShift": "F" 25 | }, 26 | "KeyG": { 27 | "unmodified": "g", 28 | "withShift": "G" 29 | }, 30 | "KeyH": { 31 | "unmodified": "h", 32 | "withShift": "H" 33 | }, 34 | "KeyI": { 35 | "unmodified": "i", 36 | "withShift": "I" 37 | }, 38 | "KeyJ": { 39 | "unmodified": "j", 40 | "withShift": "J" 41 | }, 42 | "KeyK": { 43 | "unmodified": "k", 44 | "withShift": "K" 45 | }, 46 | "KeyL": { 47 | "unmodified": "l", 48 | "withShift": "L" 49 | }, 50 | "KeyM": { 51 | "unmodified": "m", 52 | "withShift": "M" 53 | }, 54 | "KeyN": { 55 | "unmodified": "n", 56 | "withShift": "N" 57 | }, 58 | "KeyO": { 59 | "unmodified": "o", 60 | "withShift": "O" 61 | }, 62 | "KeyP": { 63 | "unmodified": "p", 64 | "withShift": "P" 65 | }, 66 | "KeyQ": { 67 | "unmodified": "q", 68 | "withShift": "Q" 69 | }, 70 | "KeyR": { 71 | "unmodified": "r", 72 | "withShift": "R" 73 | }, 74 | "KeyS": { 75 | "unmodified": "s", 76 | "withShift": "S" 77 | }, 78 | "KeyT": { 79 | "unmodified": "t", 80 | "withShift": "T" 81 | }, 82 | "KeyU": { 83 | "unmodified": "u", 84 | "withShift": "U" 85 | }, 86 | "KeyV": { 87 | "unmodified": "v", 88 | "withShift": "V" 89 | }, 90 | "KeyW": { 91 | "unmodified": "w", 92 | "withShift": "W" 93 | }, 94 | "KeyX": { 95 | "unmodified": "x", 96 | "withShift": "X" 97 | }, 98 | "KeyY": { 99 | "unmodified": "y", 100 | "withShift": "Y" 101 | }, 102 | "KeyZ": { 103 | "unmodified": "z", 104 | "withShift": "Z" 105 | }, 106 | "Digit1": { 107 | "unmodified": "1", 108 | "withShift": "!" 109 | }, 110 | "Digit2": { 111 | "unmodified": "2", 112 | "withShift": "@" 113 | }, 114 | "Digit3": { 115 | "unmodified": "3", 116 | "withShift": "#" 117 | }, 118 | "Digit4": { 119 | "unmodified": "4", 120 | "withShift": "$" 121 | }, 122 | "Digit5": { 123 | "unmodified": "5", 124 | "withShift": "%" 125 | }, 126 | "Digit6": { 127 | "unmodified": "6", 128 | "withShift": "^" 129 | }, 130 | "Digit7": { 131 | "unmodified": "7", 132 | "withShift": "&" 133 | }, 134 | "Digit8": { 135 | "unmodified": "8", 136 | "withShift": "*" 137 | }, 138 | "Digit9": { 139 | "unmodified": "9", 140 | "withShift": "(" 141 | }, 142 | "Digit0": { 143 | "unmodified": "0", 144 | "withShift": ")" 145 | }, 146 | "Space": { 147 | "unmodified": " ", 148 | "withShift": " " 149 | }, 150 | "Minus": { 151 | "unmodified": "-", 152 | "withShift": "_" 153 | }, 154 | "Equal": { 155 | "unmodified": "=", 156 | "withShift": "+" 157 | }, 158 | "BracketLeft": { 159 | "unmodified": "[", 160 | "withShift": "{" 161 | }, 162 | "BracketRight": { 163 | "unmodified": "]", 164 | "withShift": "}" 165 | }, 166 | "Backslash": { 167 | "unmodified": "\\", 168 | "withShift": "|" 169 | }, 170 | "Semicolon": { 171 | "unmodified": ";", 172 | "withShift": ":" 173 | }, 174 | "Quote": { 175 | "unmodified": "'", 176 | "withShift": "\"" 177 | }, 178 | "Backquote": { 179 | "unmodified": "`", 180 | "withShift": "~" 181 | }, 182 | "Comma": { 183 | "unmodified": ",", 184 | "withShift": "<" 185 | }, 186 | "Period": { 187 | "unmodified": ".", 188 | "withShift": ">" 189 | }, 190 | "Slash": { 191 | "unmodified": "/", 192 | "withShift": "?" 193 | }, 194 | "NumpadDivide": { 195 | "unmodified": "/", 196 | "withShift": "/" 197 | }, 198 | "NumpadMultiply": { 199 | "unmodified": "*", 200 | "withShift": "*" 201 | }, 202 | "NumpadSubtract": { 203 | "unmodified": "-", 204 | "withShift": "-" 205 | }, 206 | "NumpadAdd": { 207 | "unmodified": "+", 208 | "withShift": "+" 209 | }, 210 | "Numpad1": { 211 | "unmodified": "1", 212 | "withShift": "1" 213 | }, 214 | "Numpad2": { 215 | "unmodified": "2", 216 | "withShift": "2" 217 | }, 218 | "Numpad3": { 219 | "unmodified": "3", 220 | "withShift": "3" 221 | }, 222 | "Numpad4": { 223 | "unmodified": "4", 224 | "withShift": "4" 225 | }, 226 | "Numpad5": { 227 | "unmodified": "5", 228 | "withShift": "5" 229 | }, 230 | "Numpad6": { 231 | "unmodified": "6", 232 | "withShift": "6" 233 | }, 234 | "Numpad7": { 235 | "unmodified": "7", 236 | "withShift": "7" 237 | }, 238 | "Numpad8": { 239 | "unmodified": "8", 240 | "withShift": "8" 241 | }, 242 | "Numpad9": { 243 | "unmodified": "9", 244 | "withShift": "9" 245 | }, 246 | "Numpad0": { 247 | "unmodified": "0", 248 | "withShift": "0" 249 | }, 250 | "NumpadDecimal": { 251 | "unmodified": ".", 252 | "withShift": "." 253 | }, 254 | "IntlBackslash": { 255 | "unmodified": "§", 256 | "withShift": "±" 257 | }, 258 | "NumpadEqual": { 259 | "unmodified": "=", 260 | "withShift": "=" 261 | }, 262 | "AudioVolumeUp": { 263 | "unmodified": null, 264 | "withShift": "=" 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/slovak-cmd-keymap.coffee: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A" 5 | }, 6 | "KeyB": { 7 | "unmodified": "b", 8 | "withShift": "B" 9 | }, 10 | "KeyC": { 11 | "unmodified": "c", 12 | "withShift": "C" 13 | }, 14 | "KeyD": { 15 | "unmodified": "d", 16 | "withShift": "D" 17 | }, 18 | "KeyE": { 19 | "unmodified": "e", 20 | "withShift": "E" 21 | }, 22 | "KeyF": { 23 | "unmodified": "f", 24 | "withShift": "F" 25 | }, 26 | "KeyG": { 27 | "unmodified": "g", 28 | "withShift": "G" 29 | }, 30 | "KeyH": { 31 | "unmodified": "h", 32 | "withShift": "H" 33 | }, 34 | "KeyI": { 35 | "unmodified": "i", 36 | "withShift": "I" 37 | }, 38 | "KeyJ": { 39 | "unmodified": "j", 40 | "withShift": "J" 41 | }, 42 | "KeyK": { 43 | "unmodified": "k", 44 | "withShift": "K" 45 | }, 46 | "KeyL": { 47 | "unmodified": "l", 48 | "withShift": "L" 49 | }, 50 | "KeyM": { 51 | "unmodified": "m", 52 | "withShift": "M" 53 | }, 54 | "KeyN": { 55 | "unmodified": "n", 56 | "withShift": "N" 57 | }, 58 | "KeyO": { 59 | "unmodified": "o", 60 | "withShift": "O" 61 | }, 62 | "KeyP": { 63 | "unmodified": "p", 64 | "withShift": "P" 65 | }, 66 | "KeyQ": { 67 | "unmodified": "q", 68 | "withShift": "Q" 69 | }, 70 | "KeyR": { 71 | "unmodified": "r", 72 | "withShift": "R" 73 | }, 74 | "KeyS": { 75 | "unmodified": "s", 76 | "withShift": "S" 77 | }, 78 | "KeyT": { 79 | "unmodified": "t", 80 | "withShift": "T" 81 | }, 82 | "KeyU": { 83 | "unmodified": "u", 84 | "withShift": "U" 85 | }, 86 | "KeyV": { 87 | "unmodified": "v", 88 | "withShift": "V" 89 | }, 90 | "KeyW": { 91 | "unmodified": "w", 92 | "withShift": "W" 93 | }, 94 | "KeyX": { 95 | "unmodified": "x", 96 | "withShift": "X" 97 | }, 98 | "KeyY": { 99 | "unmodified": "z", 100 | "withShift": "Z" 101 | }, 102 | "KeyZ": { 103 | "unmodified": "y", 104 | "withShift": "Y" 105 | }, 106 | "Digit1": { 107 | "unmodified": "1", 108 | "withShift": "!" 109 | }, 110 | "Digit2": { 111 | "unmodified": "2", 112 | "withShift": "@" 113 | }, 114 | "Digit3": { 115 | "unmodified": "3", 116 | "withShift": "#" 117 | }, 118 | "Digit4": { 119 | "unmodified": "4", 120 | "withShift": "$" 121 | }, 122 | "Digit5": { 123 | "unmodified": "5", 124 | "withShift": "%" 125 | }, 126 | "Digit6": { 127 | "unmodified": "6", 128 | "withShift": "^" 129 | }, 130 | "Digit7": { 131 | "unmodified": "7", 132 | "withShift": "&" 133 | }, 134 | "Digit8": { 135 | "unmodified": "8", 136 | "withShift": "*" 137 | }, 138 | "Digit9": { 139 | "unmodified": "9", 140 | "withShift": "(" 141 | }, 142 | "Digit0": { 143 | "unmodified": "0", 144 | "withShift": ")" 145 | }, 146 | "Space": { 147 | "unmodified": " ", 148 | "withShift": " " 149 | }, 150 | "Minus": { 151 | "unmodified": "=", 152 | "withShift": null # TODO: What is this? 153 | }, 154 | "Equal": { 155 | "unmodified": "/", 156 | "withShift": null # TODO: What is this? 157 | }, 158 | "BracketLeft": { 159 | "unmodified": "[", 160 | "withShift": "{" 161 | }, 162 | "BracketRight": { 163 | "unmodified": "]", 164 | "withShift": "}" 165 | }, 166 | "Backslash": { 167 | "unmodified": "`", 168 | "withShift": null # TODO: What is this? 169 | }, 170 | "Semicolon": { 171 | "unmodified": ";", 172 | "withShift": ":" 173 | }, 174 | "Quote": { 175 | "unmodified": "'", 176 | "withShift": "\"" 177 | }, 178 | "Backquote": { 179 | "unmodified": "\\", 180 | "withShift": null # TODO: What is this? 181 | }, 182 | "Comma": { 183 | "unmodified": ",", 184 | "withShift": "<" 185 | }, 186 | "Period": { 187 | "unmodified": ".", 188 | "withShift": ">" 189 | }, 190 | "Slash": { 191 | "unmodified": "-", 192 | "withShift": null # TODO: What is this? 193 | }, 194 | "NumpadDivide": { 195 | "unmodified": "/", 196 | "withShift": "/" 197 | }, 198 | "NumpadMultiply": { 199 | "unmodified": "*", 200 | "withShift": "*" 201 | }, 202 | "NumpadSubtract": { 203 | "unmodified": "-", 204 | "withShift": "-" 205 | }, 206 | "NumpadAdd": { 207 | "unmodified": "+", 208 | "withShift": "+" 209 | }, 210 | "Numpad1": { 211 | "unmodified": "1", 212 | "withShift": "1" 213 | }, 214 | "Numpad2": { 215 | "unmodified": "2", 216 | "withShift": "2" 217 | }, 218 | "Numpad3": { 219 | "unmodified": "3", 220 | "withShift": "3" 221 | }, 222 | "Numpad4": { 223 | "unmodified": "4", 224 | "withShift": "4" 225 | }, 226 | "Numpad5": { 227 | "unmodified": "5", 228 | "withShift": "5" 229 | }, 230 | "Numpad6": { 231 | "unmodified": "6", 232 | "withShift": "6" 233 | }, 234 | "Numpad7": { 235 | "unmodified": "7", 236 | "withShift": "7" 237 | }, 238 | "Numpad8": { 239 | "unmodified": "8", 240 | "withShift": "8" 241 | }, 242 | "Numpad9": { 243 | "unmodified": "9", 244 | "withShift": "9" 245 | }, 246 | "Numpad0": { 247 | "unmodified": "0", 248 | "withShift": "0" 249 | }, 250 | "NumpadDecimal": { 251 | "unmodified": ".", 252 | "withShift": "." 253 | }, 254 | "IntlBackslash": { 255 | "unmodified": "§", 256 | "withShift": "±" 257 | }, 258 | "NumpadEqual": { 259 | "unmodified": "=", 260 | "withShift": "=" 261 | }, 262 | "AudioVolumeUp": { 263 | "unmodified": null, 264 | "withShift": "=" 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/slovak-qwerty-cmd-keymap.coffee: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A" 5 | }, 6 | "KeyB": { 7 | "unmodified": "b", 8 | "withShift": "B" 9 | }, 10 | "KeyC": { 11 | "unmodified": "c", 12 | "withShift": "C" 13 | }, 14 | "KeyD": { 15 | "unmodified": "d", 16 | "withShift": "D" 17 | }, 18 | "KeyE": { 19 | "unmodified": "e", 20 | "withShift": "E" 21 | }, 22 | "KeyF": { 23 | "unmodified": "f", 24 | "withShift": "F" 25 | }, 26 | "KeyG": { 27 | "unmodified": "g", 28 | "withShift": "G" 29 | }, 30 | "KeyH": { 31 | "unmodified": "h", 32 | "withShift": "H" 33 | }, 34 | "KeyI": { 35 | "unmodified": "i", 36 | "withShift": "I" 37 | }, 38 | "KeyJ": { 39 | "unmodified": "j", 40 | "withShift": "J" 41 | }, 42 | "KeyK": { 43 | "unmodified": "k", 44 | "withShift": "K" 45 | }, 46 | "KeyL": { 47 | "unmodified": "l", 48 | "withShift": "L" 49 | }, 50 | "KeyM": { 51 | "unmodified": "m", 52 | "withShift": "M" 53 | }, 54 | "KeyN": { 55 | "unmodified": "n", 56 | "withShift": "N" 57 | }, 58 | "KeyO": { 59 | "unmodified": "o", 60 | "withShift": "O" 61 | }, 62 | "KeyP": { 63 | "unmodified": "p", 64 | "withShift": "P" 65 | }, 66 | "KeyQ": { 67 | "unmodified": "q", 68 | "withShift": "Q" 69 | }, 70 | "KeyR": { 71 | "unmodified": "r", 72 | "withShift": "R" 73 | }, 74 | "KeyS": { 75 | "unmodified": "s", 76 | "withShift": "S" 77 | }, 78 | "KeyT": { 79 | "unmodified": "t", 80 | "withShift": "T" 81 | }, 82 | "KeyU": { 83 | "unmodified": "u", 84 | "withShift": "U" 85 | }, 86 | "KeyV": { 87 | "unmodified": "v", 88 | "withShift": "V" 89 | }, 90 | "KeyW": { 91 | "unmodified": "w", 92 | "withShift": "W" 93 | }, 94 | "KeyX": { 95 | "unmodified": "x", 96 | "withShift": "X" 97 | }, 98 | "KeyY": { 99 | "unmodified": "y", 100 | "withShift": "Y" 101 | }, 102 | "KeyZ": { 103 | "unmodified": "z", 104 | "withShift": "Z" 105 | }, 106 | "Digit1": { 107 | "unmodified": "1", 108 | "withShift": "!" 109 | }, 110 | "Digit2": { 111 | "unmodified": "2", 112 | "withShift": "@" 113 | }, 114 | "Digit3": { 115 | "unmodified": "3", 116 | "withShift": "#" 117 | }, 118 | "Digit4": { 119 | "unmodified": "4", 120 | "withShift": "$" 121 | }, 122 | "Digit5": { 123 | "unmodified": "5", 124 | "withShift": "%" 125 | }, 126 | "Digit6": { 127 | "unmodified": "6", 128 | "withShift": "^" 129 | }, 130 | "Digit7": { 131 | "unmodified": "7", 132 | "withShift": "&" 133 | }, 134 | "Digit8": { 135 | "unmodified": "8", 136 | "withShift": "*" 137 | }, 138 | "Digit9": { 139 | "unmodified": "9", 140 | "withShift": "(" 141 | }, 142 | "Digit0": { 143 | "unmodified": "0", 144 | "withShift": ")" 145 | }, 146 | "Space": { 147 | "unmodified": " ", 148 | "withShift": " " 149 | }, 150 | "Minus": { 151 | "unmodified": "=", 152 | "withShift": null # TODO: What is this? 153 | }, 154 | "Equal": { 155 | "unmodified": "/", 156 | "withShift": null # TODO: What is this? 157 | }, 158 | "BracketLeft": { 159 | "unmodified": "[", 160 | "withShift": "{" 161 | }, 162 | "BracketRight": { 163 | "unmodified": "]", 164 | "withShift": "}" 165 | }, 166 | "Backslash": { 167 | "unmodified": "`", 168 | "withShift": null # TODO: What is this? 169 | }, 170 | "Semicolon": { 171 | "unmodified": ";", 172 | "withShift": ":" 173 | }, 174 | "Quote": { 175 | "unmodified": "'", 176 | "withShift": "\"" 177 | }, 178 | "Backquote": { 179 | "unmodified": "\\", 180 | "withShift": null # TODO: What is this? 181 | }, 182 | "Comma": { 183 | "unmodified": ",", 184 | "withShift": "<" 185 | }, 186 | "Period": { 187 | "unmodified": ".", 188 | "withShift": ">" 189 | }, 190 | "Slash": { 191 | "unmodified": "-", 192 | "withShift": null # TODO: What is this? 193 | }, 194 | "NumpadDivide": { 195 | "unmodified": "/", 196 | "withShift": "/" 197 | }, 198 | "NumpadMultiply": { 199 | "unmodified": "*", 200 | "withShift": "*" 201 | }, 202 | "NumpadSubtract": { 203 | "unmodified": "-", 204 | "withShift": "-" 205 | }, 206 | "NumpadAdd": { 207 | "unmodified": "+", 208 | "withShift": "+" 209 | }, 210 | "Numpad1": { 211 | "unmodified": "1", 212 | "withShift": "1" 213 | }, 214 | "Numpad2": { 215 | "unmodified": "2", 216 | "withShift": "2" 217 | }, 218 | "Numpad3": { 219 | "unmodified": "3", 220 | "withShift": "3" 221 | }, 222 | "Numpad4": { 223 | "unmodified": "4", 224 | "withShift": "4" 225 | }, 226 | "Numpad5": { 227 | "unmodified": "5", 228 | "withShift": "5" 229 | }, 230 | "Numpad6": { 231 | "unmodified": "6", 232 | "withShift": "6" 233 | }, 234 | "Numpad7": { 235 | "unmodified": "7", 236 | "withShift": "7" 237 | }, 238 | "Numpad8": { 239 | "unmodified": "8", 240 | "withShift": "8" 241 | }, 242 | "Numpad9": { 243 | "unmodified": "9", 244 | "withShift": "9" 245 | }, 246 | "Numpad0": { 247 | "unmodified": "0", 248 | "withShift": "0" 249 | }, 250 | "NumpadDecimal": { 251 | "unmodified": ".", 252 | "withShift": "." 253 | }, 254 | "IntlBackslash": { 255 | "unmodified": "§", 256 | "withShift": "±" 257 | }, 258 | "NumpadEqual": { 259 | "unmodified": "=", 260 | "withShift": "=" 261 | }, 262 | "AudioVolumeUp": { 263 | "unmodified": null, 264 | "withShift": "=" 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/linux-dvorak.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A" 5 | }, 6 | "KeyB": { 7 | "unmodified": "x", 8 | "withShift": "X" 9 | }, 10 | "KeyC": { 11 | "unmodified": "j", 12 | "withShift": "J" 13 | }, 14 | "KeyD": { 15 | "unmodified": "e", 16 | "withShift": "E" 17 | }, 18 | "KeyE": { 19 | "unmodified": ".", 20 | "withShift": ">" 21 | }, 22 | "KeyF": { 23 | "unmodified": "u", 24 | "withShift": "U" 25 | }, 26 | "KeyG": { 27 | "unmodified": "i", 28 | "withShift": "I" 29 | }, 30 | "KeyH": { 31 | "unmodified": "d", 32 | "withShift": "D" 33 | }, 34 | "KeyI": { 35 | "unmodified": "c", 36 | "withShift": "C" 37 | }, 38 | "KeyJ": { 39 | "unmodified": "h", 40 | "withShift": "H" 41 | }, 42 | "KeyK": { 43 | "unmodified": "t", 44 | "withShift": "T" 45 | }, 46 | "KeyL": { 47 | "unmodified": "n", 48 | "withShift": "N" 49 | }, 50 | "KeyM": { 51 | "unmodified": "m", 52 | "withShift": "M" 53 | }, 54 | "KeyN": { 55 | "unmodified": "b", 56 | "withShift": "B" 57 | }, 58 | "KeyO": { 59 | "unmodified": "r", 60 | "withShift": "R" 61 | }, 62 | "KeyP": { 63 | "unmodified": "l", 64 | "withShift": "L" 65 | }, 66 | "KeyQ": { 67 | "unmodified": "'", 68 | "withShift": "\"" 69 | }, 70 | "KeyR": { 71 | "unmodified": "p", 72 | "withShift": "P" 73 | }, 74 | "KeyS": { 75 | "unmodified": "o", 76 | "withShift": "O" 77 | }, 78 | "KeyT": { 79 | "unmodified": "y", 80 | "withShift": "Y" 81 | }, 82 | "KeyU": { 83 | "unmodified": "g", 84 | "withShift": "G" 85 | }, 86 | "KeyV": { 87 | "unmodified": "k", 88 | "withShift": "K" 89 | }, 90 | "KeyW": { 91 | "unmodified": ",", 92 | "withShift": "<" 93 | }, 94 | "KeyX": { 95 | "unmodified": "q", 96 | "withShift": "Q" 97 | }, 98 | "KeyY": { 99 | "unmodified": "f", 100 | "withShift": "F" 101 | }, 102 | "KeyZ": { 103 | "unmodified": ";", 104 | "withShift": ":" 105 | }, 106 | "Digit1": { 107 | "unmodified": "1", 108 | "withShift": "!" 109 | }, 110 | "Digit2": { 111 | "unmodified": "2", 112 | "withShift": "@" 113 | }, 114 | "Digit3": { 115 | "unmodified": "3", 116 | "withShift": "#" 117 | }, 118 | "Digit4": { 119 | "unmodified": "4", 120 | "withShift": "$" 121 | }, 122 | "Digit5": { 123 | "unmodified": "5", 124 | "withShift": "%" 125 | }, 126 | "Digit6": { 127 | "unmodified": "6", 128 | "withShift": "^" 129 | }, 130 | "Digit7": { 131 | "unmodified": "7", 132 | "withShift": "&" 133 | }, 134 | "Digit8": { 135 | "unmodified": "8", 136 | "withShift": "*" 137 | }, 138 | "Digit9": { 139 | "unmodified": "9", 140 | "withShift": "(" 141 | }, 142 | "Digit0": { 143 | "unmodified": "0", 144 | "withShift": ")" 145 | }, 146 | "Space": { 147 | "unmodified": " ", 148 | "withShift": " " 149 | }, 150 | "Minus": { 151 | "unmodified": "[", 152 | "withShift": "{" 153 | }, 154 | "Equal": { 155 | "unmodified": "]", 156 | "withShift": "}" 157 | }, 158 | "BracketLeft": { 159 | "unmodified": "/", 160 | "withShift": "?" 161 | }, 162 | "BracketRight": { 163 | "unmodified": "=", 164 | "withShift": "+" 165 | }, 166 | "Backslash": { 167 | "unmodified": "\\", 168 | "withShift": "|" 169 | }, 170 | "Semicolon": { 171 | "unmodified": "s", 172 | "withShift": "S" 173 | }, 174 | "Quote": { 175 | "unmodified": "-", 176 | "withShift": "_" 177 | }, 178 | "Backquote": { 179 | "unmodified": "`", 180 | "withShift": "~" 181 | }, 182 | "Comma": { 183 | "unmodified": "w", 184 | "withShift": "W" 185 | }, 186 | "Period": { 187 | "unmodified": "v", 188 | "withShift": "V" 189 | }, 190 | "Slash": { 191 | "unmodified": "z", 192 | "withShift": "Z" 193 | }, 194 | "NumpadDivide": { 195 | "unmodified": "/", 196 | "withShift": "/" 197 | }, 198 | "NumpadMultiply": { 199 | "unmodified": "*", 200 | "withShift": "*" 201 | }, 202 | "NumpadSubtract": { 203 | "unmodified": "-", 204 | "withShift": "-" 205 | }, 206 | "NumpadAdd": { 207 | "unmodified": "+", 208 | "withShift": "+" 209 | }, 210 | "Numpad1": { 211 | "unmodified": null, 212 | "withShift": "1" 213 | }, 214 | "Numpad2": { 215 | "unmodified": null, 216 | "withShift": "2" 217 | }, 218 | "Numpad3": { 219 | "unmodified": null, 220 | "withShift": "3" 221 | }, 222 | "Numpad4": { 223 | "unmodified": null, 224 | "withShift": "4" 225 | }, 226 | "Numpad5": { 227 | "unmodified": null, 228 | "withShift": "5" 229 | }, 230 | "Numpad6": { 231 | "unmodified": null, 232 | "withShift": "6" 233 | }, 234 | "Numpad7": { 235 | "unmodified": null, 236 | "withShift": "7" 237 | }, 238 | "Numpad8": { 239 | "unmodified": null, 240 | "withShift": "8" 241 | }, 242 | "Numpad9": { 243 | "unmodified": null, 244 | "withShift": "9" 245 | }, 246 | "Numpad0": { 247 | "unmodified": null, 248 | "withShift": "0" 249 | }, 250 | "NumpadDecimal": { 251 | "unmodified": null, 252 | "withShift": "." 253 | }, 254 | "IntlBackslash": { 255 | "unmodified": "<", 256 | "withShift": ">" 257 | }, 258 | "NumpadEqual": { 259 | "unmodified": "=", 260 | "withShift": "=" 261 | }, 262 | "NumpadComma": { 263 | "unmodified": ".", 264 | "withShift": "." 265 | }, 266 | "NumpadParenLeft": { 267 | "unmodified": "(", 268 | "withShift": "(" 269 | }, 270 | "NumpadParenRight": { 271 | "unmodified": ")", 272 | "withShift": ")" 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/linux-swiss-german.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A" 5 | }, 6 | "KeyB": { 7 | "unmodified": "b", 8 | "withShift": "B" 9 | }, 10 | "KeyC": { 11 | "unmodified": "c", 12 | "withShift": "C" 13 | }, 14 | "KeyD": { 15 | "unmodified": "d", 16 | "withShift": "D" 17 | }, 18 | "KeyE": { 19 | "unmodified": "e", 20 | "withShift": "E" 21 | }, 22 | "KeyF": { 23 | "unmodified": "f", 24 | "withShift": "F" 25 | }, 26 | "KeyG": { 27 | "unmodified": "g", 28 | "withShift": "G" 29 | }, 30 | "KeyH": { 31 | "unmodified": "h", 32 | "withShift": "H" 33 | }, 34 | "KeyI": { 35 | "unmodified": "i", 36 | "withShift": "I" 37 | }, 38 | "KeyJ": { 39 | "unmodified": "j", 40 | "withShift": "J" 41 | }, 42 | "KeyK": { 43 | "unmodified": "k", 44 | "withShift": "K" 45 | }, 46 | "KeyL": { 47 | "unmodified": "l", 48 | "withShift": "L" 49 | }, 50 | "KeyM": { 51 | "unmodified": "m", 52 | "withShift": "M" 53 | }, 54 | "KeyN": { 55 | "unmodified": "n", 56 | "withShift": "N" 57 | }, 58 | "KeyO": { 59 | "unmodified": "o", 60 | "withShift": "O" 61 | }, 62 | "KeyP": { 63 | "unmodified": "p", 64 | "withShift": "P" 65 | }, 66 | "KeyQ": { 67 | "unmodified": "q", 68 | "withShift": "Q" 69 | }, 70 | "KeyR": { 71 | "unmodified": "r", 72 | "withShift": "R" 73 | }, 74 | "KeyS": { 75 | "unmodified": "s", 76 | "withShift": "S" 77 | }, 78 | "KeyT": { 79 | "unmodified": "t", 80 | "withShift": "T" 81 | }, 82 | "KeyU": { 83 | "unmodified": "u", 84 | "withShift": "U" 85 | }, 86 | "KeyV": { 87 | "unmodified": "v", 88 | "withShift": "V" 89 | }, 90 | "KeyW": { 91 | "unmodified": "w", 92 | "withShift": "W" 93 | }, 94 | "KeyX": { 95 | "unmodified": "x", 96 | "withShift": "X" 97 | }, 98 | "KeyY": { 99 | "unmodified": "z", 100 | "withShift": "Z" 101 | }, 102 | "KeyZ": { 103 | "unmodified": "y", 104 | "withShift": "Y" 105 | }, 106 | "Digit1": { 107 | "unmodified": "1", 108 | "withShift": "+" 109 | }, 110 | "Digit2": { 111 | "unmodified": "2", 112 | "withShift": "\"" 113 | }, 114 | "Digit3": { 115 | "unmodified": "3", 116 | "withShift": "*" 117 | }, 118 | "Digit4": { 119 | "unmodified": "4", 120 | "withShift": "�" 121 | }, 122 | "Digit5": { 123 | "unmodified": "5", 124 | "withShift": "%" 125 | }, 126 | "Digit6": { 127 | "unmodified": "6", 128 | "withShift": "&" 129 | }, 130 | "Digit7": { 131 | "unmodified": "7", 132 | "withShift": "/" 133 | }, 134 | "Digit8": { 135 | "unmodified": "8", 136 | "withShift": "(" 137 | }, 138 | "Digit9": { 139 | "unmodified": "9", 140 | "withShift": ")" 141 | }, 142 | "Digit0": { 143 | "unmodified": "0", 144 | "withShift": "=" 145 | }, 146 | "Space": { 147 | "unmodified": " ", 148 | "withShift": "�" 149 | }, 150 | "Minus": { 151 | "unmodified": "'", 152 | "withShift": "?" 153 | }, 154 | "Equal": { 155 | "unmodified": "^", 156 | "withShift": "`" 157 | }, 158 | "BracketLeft": { 159 | "unmodified": "�", 160 | "withShift": "�" 161 | }, 162 | "BracketRight": { 163 | "unmodified": "�", 164 | "withShift": "!" 165 | }, 166 | "Backslash": { 167 | "unmodified": "$", 168 | "withShift": "�" 169 | }, 170 | "Semicolon": { 171 | "unmodified": "�", 172 | "withShift": "�" 173 | }, 174 | "Quote": { 175 | "unmodified": "�", 176 | "withShift": "�" 177 | }, 178 | "Backquote": { 179 | "unmodified": "�", 180 | "withShift": "�" 181 | }, 182 | "Comma": { 183 | "unmodified": ",", 184 | "withShift": ";" 185 | }, 186 | "Period": { 187 | "unmodified": ".", 188 | "withShift": ":" 189 | }, 190 | "Slash": { 191 | "unmodified": "-", 192 | "withShift": "_" 193 | }, 194 | "NumpadDivide": { 195 | "unmodified": "/", 196 | "withShift": "/" 197 | }, 198 | "NumpadMultiply": { 199 | "unmodified": "*", 200 | "withShift": "*" 201 | }, 202 | "NumpadSubtract": { 203 | "unmodified": "-", 204 | "withShift": "-" 205 | }, 206 | "NumpadAdd": { 207 | "unmodified": "+", 208 | "withShift": "+" 209 | }, 210 | "Numpad1": { 211 | "unmodified": null, 212 | "withShift": "1" 213 | }, 214 | "Numpad2": { 215 | "unmodified": null, 216 | "withShift": "2" 217 | }, 218 | "Numpad3": { 219 | "unmodified": null, 220 | "withShift": "3" 221 | }, 222 | "Numpad4": { 223 | "unmodified": null, 224 | "withShift": "4" 225 | }, 226 | "Numpad5": { 227 | "unmodified": null, 228 | "withShift": "5" 229 | }, 230 | "Numpad6": { 231 | "unmodified": null, 232 | "withShift": "6" 233 | }, 234 | "Numpad7": { 235 | "unmodified": null, 236 | "withShift": "7" 237 | }, 238 | "Numpad8": { 239 | "unmodified": null, 240 | "withShift": "8" 241 | }, 242 | "Numpad9": { 243 | "unmodified": null, 244 | "withShift": "9" 245 | }, 246 | "Numpad0": { 247 | "unmodified": null, 248 | "withShift": "0" 249 | }, 250 | "NumpadDecimal": { 251 | "unmodified": null, 252 | "withShift": "," 253 | }, 254 | "IntlBackslash": { 255 | "unmodified": "<", 256 | "withShift": ">" 257 | }, 258 | "NumpadEqual": { 259 | "unmodified": "=", 260 | "withShift": "=" 261 | }, 262 | "NumpadComma": { 263 | "unmodified": ".", 264 | "withShift": "." 265 | }, 266 | "NumpadParenLeft": { 267 | "unmodified": "(", 268 | "withShift": "(" 269 | }, 270 | "NumpadParenRight": { 271 | "unmodified": ")", 272 | "withShift": ")" 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/windows-swedish.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A", 5 | "withAltGraph": null, 6 | "withAltGraphShift": null 7 | }, 8 | "KeyB": { 9 | "unmodified": "b", 10 | "withShift": "B", 11 | "withAltGraph": null, 12 | "withAltGraphShift": null 13 | }, 14 | "KeyC": { 15 | "unmodified": "c", 16 | "withShift": "C", 17 | "withAltGraph": null, 18 | "withAltGraphShift": null 19 | }, 20 | "KeyD": { 21 | "unmodified": "d", 22 | "withShift": "D", 23 | "withAltGraph": null, 24 | "withAltGraphShift": null 25 | }, 26 | "KeyE": { 27 | "unmodified": "e", 28 | "withShift": "E", 29 | "withAltGraph": null, 30 | "withAltGraphShift": null 31 | }, 32 | "KeyF": { 33 | "unmodified": "f", 34 | "withShift": "F", 35 | "withAltGraph": null, 36 | "withAltGraphShift": null 37 | }, 38 | "KeyG": { 39 | "unmodified": "g", 40 | "withShift": "G", 41 | "withAltGraph": null, 42 | "withAltGraphShift": null 43 | }, 44 | "KeyH": { 45 | "unmodified": "h", 46 | "withShift": "H", 47 | "withAltGraph": null, 48 | "withAltGraphShift": null 49 | }, 50 | "KeyI": { 51 | "unmodified": "i", 52 | "withShift": "I", 53 | "withAltGraph": null, 54 | "withAltGraphShift": null 55 | }, 56 | "KeyJ": { 57 | "unmodified": "j", 58 | "withShift": "J", 59 | "withAltGraph": null, 60 | "withAltGraphShift": null 61 | }, 62 | "KeyK": { 63 | "unmodified": "k", 64 | "withShift": "K", 65 | "withAltGraph": null, 66 | "withAltGraphShift": null 67 | }, 68 | "KeyL": { 69 | "unmodified": "l", 70 | "withShift": "L", 71 | "withAltGraph": null, 72 | "withAltGraphShift": null 73 | }, 74 | "KeyM": { 75 | "unmodified": "m", 76 | "withShift": "M", 77 | "withAltGraph": "µ", 78 | "withAltGraphShift": null 79 | }, 80 | "KeyN": { 81 | "unmodified": "n", 82 | "withShift": "N", 83 | "withAltGraph": null, 84 | "withAltGraphShift": null 85 | }, 86 | "KeyO": { 87 | "unmodified": "o", 88 | "withShift": "O", 89 | "withAltGraph": null, 90 | "withAltGraphShift": null 91 | }, 92 | "KeyP": { 93 | "unmodified": "p", 94 | "withShift": "P", 95 | "withAltGraph": null, 96 | "withAltGraphShift": null 97 | }, 98 | "KeyQ": { 99 | "unmodified": "q", 100 | "withShift": "Q", 101 | "withAltGraph": null, 102 | "withAltGraphShift": null 103 | }, 104 | "KeyR": { 105 | "unmodified": "r", 106 | "withShift": "R", 107 | "withAltGraph": null, 108 | "withAltGraphShift": null 109 | }, 110 | "KeyS": { 111 | "unmodified": "s", 112 | "withShift": "S", 113 | "withAltGraph": null, 114 | "withAltGraphShift": null 115 | }, 116 | "KeyT": { 117 | "unmodified": "t", 118 | "withShift": "T", 119 | "withAltGraph": null, 120 | "withAltGraphShift": null 121 | }, 122 | "KeyU": { 123 | "unmodified": "u", 124 | "withShift": "U", 125 | "withAltGraph": null, 126 | "withAltGraphShift": null 127 | }, 128 | "KeyV": { 129 | "unmodified": "v", 130 | "withShift": "V", 131 | "withAltGraph": null, 132 | "withAltGraphShift": null 133 | }, 134 | "KeyW": { 135 | "unmodified": "w", 136 | "withShift": "W", 137 | "withAltGraph": null, 138 | "withAltGraphShift": null 139 | }, 140 | "KeyX": { 141 | "unmodified": "x", 142 | "withShift": "X", 143 | "withAltGraph": null, 144 | "withAltGraphShift": null 145 | }, 146 | "KeyY": { 147 | "unmodified": "y", 148 | "withShift": "Y", 149 | "withAltGraph": null, 150 | "withAltGraphShift": null 151 | }, 152 | "KeyZ": { 153 | "unmodified": "z", 154 | "withShift": "Z", 155 | "withAltGraph": null, 156 | "withAltGraphShift": null 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "!", 161 | "withAltGraph": null, 162 | "withAltGraphShift": null 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "\"", 167 | "withAltGraph": "@", 168 | "withAltGraphShift": null 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "#", 173 | "withAltGraph": "£", 174 | "withAltGraphShift": null 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "¤", 179 | "withAltGraph": "$", 180 | "withAltGraphShift": null 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": null, 186 | "withAltGraphShift": null 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": "&", 191 | "withAltGraph": null, 192 | "withAltGraphShift": null 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "/", 197 | "withAltGraph": "{", 198 | "withAltGraphShift": null 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "(", 203 | "withAltGraph": "[", 204 | "withAltGraphShift": null 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": ")", 209 | "withAltGraph": "]", 210 | "withAltGraphShift": null 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": "=", 215 | "withAltGraph": "}", 216 | "withAltGraphShift": null 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": null, 222 | "withAltGraphShift": null 223 | }, 224 | "Minus": { 225 | "unmodified": "+", 226 | "withShift": "?", 227 | "withAltGraph": "\\", 228 | "withAltGraphShift": null 229 | }, 230 | "BracketLeft": { 231 | "unmodified": "å", 232 | "withShift": "Å", 233 | "withAltGraph": null, 234 | "withAltGraphShift": null 235 | }, 236 | "Backslash": { 237 | "unmodified": "'", 238 | "withShift": "*", 239 | "withAltGraph": null, 240 | "withAltGraphShift": null 241 | }, 242 | "Semicolon": { 243 | "unmodified": "ö", 244 | "withShift": "Ö", 245 | "withAltGraph": null, 246 | "withAltGraphShift": null 247 | }, 248 | "Quote": { 249 | "unmodified": "ä", 250 | "withShift": "Ä", 251 | "withAltGraph": null, 252 | "withAltGraphShift": null 253 | }, 254 | "Backquote": { 255 | "unmodified": "§", 256 | "withShift": "½", 257 | "withAltGraph": null, 258 | "withAltGraphShift": null 259 | }, 260 | "Comma": { 261 | "unmodified": ",", 262 | "withShift": ";", 263 | "withAltGraph": null, 264 | "withAltGraphShift": null 265 | }, 266 | "Period": { 267 | "unmodified": ".", 268 | "withShift": ":", 269 | "withAltGraph": null, 270 | "withAltGraphShift": null 271 | }, 272 | "Slash": { 273 | "unmodified": "-", 274 | "withShift": "_", 275 | "withAltGraph": null, 276 | "withAltGraphShift": null 277 | }, 278 | "NumpadMultiply": { 279 | "unmodified": "*", 280 | "withShift": "*", 281 | "withAltGraph": null, 282 | "withAltGraphShift": null 283 | }, 284 | "NumpadSubtract": { 285 | "unmodified": "-", 286 | "withShift": "-", 287 | "withAltGraph": null, 288 | "withAltGraphShift": null 289 | }, 290 | "NumpadAdd": { 291 | "unmodified": "+", 292 | "withShift": "+", 293 | "withAltGraph": null, 294 | "withAltGraphShift": null 295 | }, 296 | "IntlBackslash": { 297 | "unmodified": "<", 298 | "withShift": ">", 299 | "withAltGraph": "|", 300 | "withAltGraphShift": null 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/windows-us-international.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A", 5 | "withAltGraph": "á", 6 | "withAltGraphShift": "Á" 7 | }, 8 | "KeyB": { 9 | "unmodified": "b", 10 | "withShift": "B", 11 | "withAltGraph": null, 12 | "withAltGraphShift": null 13 | }, 14 | "KeyC": { 15 | "unmodified": "c", 16 | "withShift": "C", 17 | "withAltGraph": "©", 18 | "withAltGraphShift": "¢" 19 | }, 20 | "KeyD": { 21 | "unmodified": "d", 22 | "withShift": "D", 23 | "withAltGraph": "ð", 24 | "withAltGraphShift": "Ð" 25 | }, 26 | "KeyE": { 27 | "unmodified": "e", 28 | "withShift": "E", 29 | "withAltGraph": "é", 30 | "withAltGraphShift": "É" 31 | }, 32 | "KeyF": { 33 | "unmodified": "f", 34 | "withShift": "F", 35 | "withAltGraph": null, 36 | "withAltGraphShift": null 37 | }, 38 | "KeyG": { 39 | "unmodified": "g", 40 | "withShift": "G", 41 | "withAltGraph": null, 42 | "withAltGraphShift": null 43 | }, 44 | "KeyH": { 45 | "unmodified": "h", 46 | "withShift": "H", 47 | "withAltGraph": null, 48 | "withAltGraphShift": null 49 | }, 50 | "KeyI": { 51 | "unmodified": "i", 52 | "withShift": "I", 53 | "withAltGraph": "í", 54 | "withAltGraphShift": "Í" 55 | }, 56 | "KeyJ": { 57 | "unmodified": "j", 58 | "withShift": "J", 59 | "withAltGraph": null, 60 | "withAltGraphShift": null 61 | }, 62 | "KeyK": { 63 | "unmodified": "k", 64 | "withShift": "K", 65 | "withAltGraph": null, 66 | "withAltGraphShift": null 67 | }, 68 | "KeyL": { 69 | "unmodified": "l", 70 | "withShift": "L", 71 | "withAltGraph": "ø", 72 | "withAltGraphShift": "Ø" 73 | }, 74 | "KeyM": { 75 | "unmodified": "m", 76 | "withShift": "M", 77 | "withAltGraph": "µ", 78 | "withAltGraphShift": null 79 | }, 80 | "KeyN": { 81 | "unmodified": "n", 82 | "withShift": "N", 83 | "withAltGraph": "ñ", 84 | "withAltGraphShift": "Ñ" 85 | }, 86 | "KeyO": { 87 | "unmodified": "o", 88 | "withShift": "O", 89 | "withAltGraph": "ó", 90 | "withAltGraphShift": "Ó" 91 | }, 92 | "KeyP": { 93 | "unmodified": "p", 94 | "withShift": "P", 95 | "withAltGraph": "ö", 96 | "withAltGraphShift": "Ö" 97 | }, 98 | "KeyQ": { 99 | "unmodified": "q", 100 | "withShift": "Q", 101 | "withAltGraph": "ä", 102 | "withAltGraphShift": "Ä" 103 | }, 104 | "KeyR": { 105 | "unmodified": "r", 106 | "withShift": "R", 107 | "withAltGraph": "®", 108 | "withAltGraphShift": null 109 | }, 110 | "KeyS": { 111 | "unmodified": "s", 112 | "withShift": "S", 113 | "withAltGraph": "ß", 114 | "withAltGraphShift": "§" 115 | }, 116 | "KeyT": { 117 | "unmodified": "t", 118 | "withShift": "T", 119 | "withAltGraph": "þ", 120 | "withAltGraphShift": "Þ" 121 | }, 122 | "KeyU": { 123 | "unmodified": "u", 124 | "withShift": "U", 125 | "withAltGraph": "ú", 126 | "withAltGraphShift": "Ú" 127 | }, 128 | "KeyV": { 129 | "unmodified": "v", 130 | "withShift": "V", 131 | "withAltGraph": null, 132 | "withAltGraphShift": null 133 | }, 134 | "KeyW": { 135 | "unmodified": "w", 136 | "withShift": "W", 137 | "withAltGraph": "å", 138 | "withAltGraphShift": "Å" 139 | }, 140 | "KeyX": { 141 | "unmodified": "x", 142 | "withShift": "X", 143 | "withAltGraph": null, 144 | "withAltGraphShift": null 145 | }, 146 | "KeyY": { 147 | "unmodified": "y", 148 | "withShift": "Y", 149 | "withAltGraph": "ü", 150 | "withAltGraphShift": "Ü" 151 | }, 152 | "KeyZ": { 153 | "unmodified": "z", 154 | "withShift": "Z", 155 | "withAltGraph": "æ", 156 | "withAltGraphShift": "Æ" 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "!", 161 | "withAltGraph": "¡", 162 | "withAltGraphShift": "¹" 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "@", 167 | "withAltGraph": "²", 168 | "withAltGraphShift": null 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "#", 173 | "withAltGraph": "³", 174 | "withAltGraphShift": null 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "$", 179 | "withAltGraph": "¤", 180 | "withAltGraphShift": "£" 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": null, 186 | "withAltGraphShift": null 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": null, 191 | "withAltGraph": "^¼", 192 | "withAltGraphShift": null 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "&", 197 | "withAltGraph": "½", 198 | "withAltGraphShift": null 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "*", 203 | "withAltGraph": "¾", 204 | "withAltGraphShift": null 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": "(", 209 | "withAltGraph": "‘", 210 | "withAltGraphShift": null 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": ")", 215 | "withAltGraph": null, 216 | "withAltGraphShift": null 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": null, 222 | "withAltGraphShift": null 223 | }, 224 | "Minus": { 225 | "unmodified": "-", 226 | "withShift": "_", 227 | "withAltGraph": "¥", 228 | "withAltGraphShift": null 229 | }, 230 | "Equal": { 231 | "unmodified": "=", 232 | "withShift": "+", 233 | "withAltGraph": "×", 234 | "withAltGraphShift": "÷" 235 | }, 236 | "BracketLeft": { 237 | "unmodified": "[", 238 | "withShift": "{", 239 | "withAltGraph": "«", 240 | "withAltGraphShift": null 241 | }, 242 | "BracketRight": { 243 | "unmodified": "]", 244 | "withShift": "}", 245 | "withAltGraph": "»", 246 | "withAltGraphShift": null 247 | }, 248 | "Backslash": { 249 | "unmodified": "\\", 250 | "withShift": "|", 251 | "withAltGraph": "¬", 252 | "withAltGraphShift": "¦" 253 | }, 254 | "Semicolon": { 255 | "unmodified": ";", 256 | "withShift": ":", 257 | "withAltGraph": "¶", 258 | "withAltGraphShift": "°" 259 | }, 260 | "Quote": { 261 | "unmodified": null, 262 | "withShift": "'\"", 263 | "withAltGraph": "´", 264 | "withAltGraphShift": "¨" 265 | }, 266 | "Backquote": { 267 | "unmodified": null, 268 | "withShift": "`~", 269 | "withAltGraph": null, 270 | "withAltGraphShift": null 271 | }, 272 | "Comma": { 273 | "unmodified": ",", 274 | "withShift": "<", 275 | "withAltGraph": "ç", 276 | "withAltGraphShift": "Ç" 277 | }, 278 | "Period": { 279 | "unmodified": ".", 280 | "withShift": ">", 281 | "withAltGraph": null, 282 | "withAltGraphShift": null 283 | }, 284 | "Slash": { 285 | "unmodified": "/", 286 | "withShift": "?", 287 | "withAltGraph": "¿", 288 | "withAltGraphShift": null 289 | }, 290 | "NumpadMultiply": { 291 | "unmodified": "*", 292 | "withShift": "*", 293 | "withAltGraph": null, 294 | "withAltGraphShift": null 295 | }, 296 | "NumpadSubtract": { 297 | "unmodified": "-", 298 | "withShift": "-", 299 | "withAltGraph": null, 300 | "withAltGraphShift": null 301 | }, 302 | "NumpadAdd": { 303 | "unmodified": "+", 304 | "withShift": "+", 305 | "withAltGraph": null, 306 | "withAltGraphShift": null 307 | }, 308 | "IntlBackslash": { 309 | "unmodified": "\\", 310 | "withShift": "|", 311 | "withAltGraph": null, 312 | "withAltGraphShift": null 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/windows-swiss-german.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A", 5 | "withAltGraph": null, 6 | "withAltGraphShift": null 7 | }, 8 | "KeyB": { 9 | "unmodified": "b", 10 | "withShift": "B", 11 | "withAltGraph": null, 12 | "withAltGraphShift": null 13 | }, 14 | "KeyC": { 15 | "unmodified": "c", 16 | "withShift": "C", 17 | "withAltGraph": null, 18 | "withAltGraphShift": null 19 | }, 20 | "KeyD": { 21 | "unmodified": "d", 22 | "withShift": "D", 23 | "withAltGraph": null, 24 | "withAltGraphShift": null 25 | }, 26 | "KeyE": { 27 | "unmodified": "e", 28 | "withShift": "E", 29 | "withAltGraph": null, 30 | "withAltGraphShift": null 31 | }, 32 | "KeyF": { 33 | "unmodified": "f", 34 | "withShift": "F", 35 | "withAltGraph": null, 36 | "withAltGraphShift": null 37 | }, 38 | "KeyG": { 39 | "unmodified": "g", 40 | "withShift": "G", 41 | "withAltGraph": null, 42 | "withAltGraphShift": null 43 | }, 44 | "KeyH": { 45 | "unmodified": "h", 46 | "withShift": "H", 47 | "withAltGraph": null, 48 | "withAltGraphShift": null 49 | }, 50 | "KeyI": { 51 | "unmodified": "i", 52 | "withShift": "I", 53 | "withAltGraph": null, 54 | "withAltGraphShift": null 55 | }, 56 | "KeyJ": { 57 | "unmodified": "j", 58 | "withShift": "J", 59 | "withAltGraph": null, 60 | "withAltGraphShift": null 61 | }, 62 | "KeyK": { 63 | "unmodified": "k", 64 | "withShift": "K", 65 | "withAltGraph": null, 66 | "withAltGraphShift": null 67 | }, 68 | "KeyL": { 69 | "unmodified": "l", 70 | "withShift": "L", 71 | "withAltGraph": null, 72 | "withAltGraphShift": null 73 | }, 74 | "KeyM": { 75 | "unmodified": "m", 76 | "withShift": "M", 77 | "withAltGraph": null, 78 | "withAltGraphShift": null 79 | }, 80 | "KeyN": { 81 | "unmodified": "n", 82 | "withShift": "N", 83 | "withAltGraph": null, 84 | "withAltGraphShift": null 85 | }, 86 | "KeyO": { 87 | "unmodified": "o", 88 | "withShift": "O", 89 | "withAltGraph": null, 90 | "withAltGraphShift": null 91 | }, 92 | "KeyP": { 93 | "unmodified": "p", 94 | "withShift": "P", 95 | "withAltGraph": null, 96 | "withAltGraphShift": null 97 | }, 98 | "KeyQ": { 99 | "unmodified": "q", 100 | "withShift": "Q", 101 | "withAltGraph": null, 102 | "withAltGraphShift": null 103 | }, 104 | "KeyR": { 105 | "unmodified": "r", 106 | "withShift": "R", 107 | "withAltGraph": null, 108 | "withAltGraphShift": null 109 | }, 110 | "KeyS": { 111 | "unmodified": "s", 112 | "withShift": "S", 113 | "withAltGraph": null, 114 | "withAltGraphShift": null 115 | }, 116 | "KeyT": { 117 | "unmodified": "t", 118 | "withShift": "T", 119 | "withAltGraph": null, 120 | "withAltGraphShift": null 121 | }, 122 | "KeyU": { 123 | "unmodified": "u", 124 | "withShift": "U", 125 | "withAltGraph": null, 126 | "withAltGraphShift": null 127 | }, 128 | "KeyV": { 129 | "unmodified": "v", 130 | "withShift": "V", 131 | "withAltGraph": null, 132 | "withAltGraphShift": null 133 | }, 134 | "KeyW": { 135 | "unmodified": "w", 136 | "withShift": "W", 137 | "withAltGraph": null, 138 | "withAltGraphShift": null 139 | }, 140 | "KeyX": { 141 | "unmodified": "x", 142 | "withShift": "X", 143 | "withAltGraph": null, 144 | "withAltGraphShift": null 145 | }, 146 | "KeyY": { 147 | "unmodified": "z", 148 | "withShift": "Z", 149 | "withAltGraph": null, 150 | "withAltGraphShift": null 151 | }, 152 | "KeyZ": { 153 | "unmodified": "y", 154 | "withShift": "Y", 155 | "withAltGraph": null, 156 | "withAltGraphShift": null 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "+", 161 | "withAltGraph": "¦", 162 | "withAltGraphShift": null 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "\"", 167 | "withAltGraph": "@", 168 | "withAltGraphShift": null 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "*", 173 | "withAltGraph": "#", 174 | "withAltGraphShift": null 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "ç", 179 | "withAltGraph": "°", 180 | "withAltGraphShift": null 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": "§", 186 | "withAltGraphShift": null 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": "&", 191 | "withAltGraph": "¬", 192 | "withAltGraphShift": null 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "/", 197 | "withAltGraph": "|", 198 | "withAltGraphShift": null 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "(", 203 | "withAltGraph": "¢", 204 | "withAltGraphShift": null 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": ")", 209 | "withAltGraph": null, 210 | "withAltGraphShift": null 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": "=", 215 | "withAltGraph": null, 216 | "withAltGraphShift": null 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": null, 222 | "withAltGraphShift": null 223 | }, 224 | "Minus": { 225 | "unmodified": "'", 226 | "withShift": "?", 227 | "withAltGraph": null, 228 | "withAltGraphShift": null 229 | }, 230 | "Equal": { 231 | "unmodified": "´^", 232 | "withShift": null, 233 | "withAltGraph": "`~", 234 | "withAltGraphShift": null 235 | }, 236 | "BracketLeft": { 237 | "unmodified": "ü", 238 | "withShift": "è", 239 | "withAltGraph": "[", 240 | "withAltGraphShift": null 241 | }, 242 | "BracketRight": { 243 | "unmodified": null, 244 | "withShift": "¨!", 245 | "withAltGraph": "]", 246 | "withAltGraphShift": null 247 | }, 248 | "Backslash": { 249 | "unmodified": "$", 250 | "withShift": "£", 251 | "withAltGraph": "}", 252 | "withAltGraphShift": null 253 | }, 254 | "Semicolon": { 255 | "unmodified": "ö", 256 | "withShift": "é", 257 | "withAltGraph": null, 258 | "withAltGraphShift": null 259 | }, 260 | "Quote": { 261 | "unmodified": "ä", 262 | "withShift": "à", 263 | "withAltGraph": "{", 264 | "withAltGraphShift": null 265 | }, 266 | "Backquote": { 267 | "unmodified": "§", 268 | "withShift": "°", 269 | "withAltGraph": null, 270 | "withAltGraphShift": null 271 | }, 272 | "Comma": { 273 | "unmodified": ",", 274 | "withShift": ";", 275 | "withAltGraph": null, 276 | "withAltGraphShift": null 277 | }, 278 | "Period": { 279 | "unmodified": ".", 280 | "withShift": ":", 281 | "withAltGraph": null, 282 | "withAltGraphShift": null 283 | }, 284 | "Slash": { 285 | "unmodified": "-", 286 | "withShift": "_", 287 | "withAltGraph": null, 288 | "withAltGraphShift": null 289 | }, 290 | "NumpadMultiply": { 291 | "unmodified": "*", 292 | "withShift": "*", 293 | "withAltGraph": null, 294 | "withAltGraphShift": null 295 | }, 296 | "NumpadSubtract": { 297 | "unmodified": "-", 298 | "withShift": "-", 299 | "withAltGraph": null, 300 | "withAltGraphShift": null 301 | }, 302 | "NumpadAdd": { 303 | "unmodified": "+", 304 | "withShift": "+", 305 | "withAltGraph": null, 306 | "withAltGraphShift": null 307 | }, 308 | "IntlBackslash": { 309 | "unmodified": "<", 310 | "withShift": ">", 311 | "withAltGraph": "\\", 312 | "withAltGraphShift": null 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/mac-swedish.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A", 5 | "withAltGraph": "", 6 | "withAltGraphShift": "◊" 7 | }, 8 | "KeyB": { 9 | "unmodified": "b", 10 | "withShift": "B", 11 | "withAltGraph": "›", 12 | "withAltGraphShift": "»" 13 | }, 14 | "KeyC": { 15 | "unmodified": "c", 16 | "withShift": "C", 17 | "withAltGraph": "ç", 18 | "withAltGraphShift": "Ç" 19 | }, 20 | "KeyD": { 21 | "unmodified": "d", 22 | "withShift": "D", 23 | "withAltGraph": "∂", 24 | "withAltGraphShift": "∆" 25 | }, 26 | "KeyE": { 27 | "unmodified": "e", 28 | "withShift": "E", 29 | "withAltGraph": "é", 30 | "withAltGraphShift": "É" 31 | }, 32 | "KeyF": { 33 | "unmodified": "f", 34 | "withShift": "F", 35 | "withAltGraph": "ƒ", 36 | "withAltGraphShift": "∫" 37 | }, 38 | "KeyG": { 39 | "unmodified": "g", 40 | "withShift": "G", 41 | "withAltGraph": "¸", 42 | "withAltGraphShift": "¯" 43 | }, 44 | "KeyH": { 45 | "unmodified": "h", 46 | "withShift": "H", 47 | "withAltGraph": "˛", 48 | "withAltGraphShift": "˘" 49 | }, 50 | "KeyI": { 51 | "unmodified": "i", 52 | "withShift": "I", 53 | "withAltGraph": "ı", 54 | "withAltGraphShift": "ˆ" 55 | }, 56 | "KeyJ": { 57 | "unmodified": "j", 58 | "withShift": "J", 59 | "withAltGraph": "√", 60 | "withAltGraphShift": "¬" 61 | }, 62 | "KeyK": { 63 | "unmodified": "k", 64 | "withShift": "K", 65 | "withAltGraph": "ª", 66 | "withAltGraphShift": "º" 67 | }, 68 | "KeyL": { 69 | "unmodified": "l", 70 | "withShift": "L", 71 | "withAltGraph": "fi", 72 | "withAltGraphShift": "fl" 73 | }, 74 | "KeyM": { 75 | "unmodified": "m", 76 | "withShift": "M", 77 | "withAltGraph": "’", 78 | "withAltGraphShift": "”" 79 | }, 80 | "KeyN": { 81 | "unmodified": "n", 82 | "withShift": "N", 83 | "withAltGraph": "‘", 84 | "withAltGraphShift": "“" 85 | }, 86 | "KeyO": { 87 | "unmodified": "o", 88 | "withShift": "O", 89 | "withAltGraph": "œ", 90 | "withAltGraphShift": "Œ" 91 | }, 92 | "KeyP": { 93 | "unmodified": "p", 94 | "withShift": "P", 95 | "withAltGraph": "π", 96 | "withAltGraphShift": "∏" 97 | }, 98 | "KeyQ": { 99 | "unmodified": "q", 100 | "withShift": "Q", 101 | "withAltGraph": "•", 102 | "withAltGraphShift": "°" 103 | }, 104 | "KeyR": { 105 | "unmodified": "r", 106 | "withShift": "R", 107 | "withAltGraph": "®", 108 | "withAltGraphShift": "√" 109 | }, 110 | "KeyS": { 111 | "unmodified": "s", 112 | "withShift": "S", 113 | "withAltGraph": "ß", 114 | "withAltGraphShift": "∑" 115 | }, 116 | "KeyT": { 117 | "unmodified": "t", 118 | "withShift": "T", 119 | "withAltGraph": "†", 120 | "withAltGraphShift": "‡" 121 | }, 122 | "KeyU": { 123 | "unmodified": "u", 124 | "withShift": "U", 125 | "withAltGraph": "ü", 126 | "withAltGraphShift": "Ü" 127 | }, 128 | "KeyV": { 129 | "unmodified": "v", 130 | "withShift": "V", 131 | "withAltGraph": "‹", 132 | "withAltGraphShift": "«" 133 | }, 134 | "KeyW": { 135 | "unmodified": "w", 136 | "withShift": "W", 137 | "withAltGraph": "Ω", 138 | "withAltGraphShift": "˝" 139 | }, 140 | "KeyX": { 141 | "unmodified": "x", 142 | "withShift": "X", 143 | "withAltGraph": "≈", 144 | "withAltGraphShift": "ˇ" 145 | }, 146 | "KeyY": { 147 | "unmodified": "y", 148 | "withShift": "Y", 149 | "withAltGraph": "µ", 150 | "withAltGraphShift": "˜" 151 | }, 152 | "KeyZ": { 153 | "unmodified": "z", 154 | "withShift": "Z", 155 | "withAltGraph": "÷", 156 | "withAltGraphShift": "⁄" 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "!", 161 | "withAltGraph": "©", 162 | "withAltGraphShift": "¡" 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "\"", 167 | "withAltGraph": "™", 168 | "withAltGraphShift": "”" 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "#", 173 | "withAltGraph": "£", 174 | "withAltGraphShift": "¥" 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "€", 179 | "withAltGraph": "$", 180 | "withAltGraphShift": "¢" 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": "∞", 186 | "withAltGraphShift": "‰" 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": "&", 191 | "withAltGraph": "§", 192 | "withAltGraphShift": "¶" 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "/", 197 | "withAltGraph": "|", 198 | "withAltGraphShift": "\\" 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "(", 203 | "withAltGraph": "[", 204 | "withAltGraphShift": "{" 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": ")", 209 | "withAltGraph": "]", 210 | "withAltGraphShift": "}" 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": "=", 215 | "withAltGraph": "≈", 216 | "withAltGraphShift": "≠" 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": " ", 222 | "withAltGraphShift": " " 223 | }, 224 | "Minus": { 225 | "unmodified": "+", 226 | "withShift": "?", 227 | "withAltGraph": "±", 228 | "withAltGraphShift": "¿" 229 | }, 230 | "Equal": { 231 | "unmodified": "´", 232 | "withShift": "`", 233 | "withAltGraph": "´", 234 | "withAltGraphShift": "`" 235 | }, 236 | "BracketLeft": { 237 | "unmodified": "å", 238 | "withShift": "Å", 239 | "withAltGraph": "˙", 240 | "withAltGraphShift": "˚" 241 | }, 242 | "BracketRight": { 243 | "unmodified": "¨", 244 | "withShift": "^", 245 | "withAltGraph": "~", 246 | "withAltGraphShift": "^" 247 | }, 248 | "Backslash": { 249 | "unmodified": "'", 250 | "withShift": "*", 251 | "withAltGraph": "@", 252 | "withAltGraphShift": "’" 253 | }, 254 | "Semicolon": { 255 | "unmodified": "ö", 256 | "withShift": "Ö", 257 | "withAltGraph": "ø", 258 | "withAltGraphShift": "Ø" 259 | }, 260 | "Quote": { 261 | "unmodified": "ä", 262 | "withShift": "Ä", 263 | "withAltGraph": "æ", 264 | "withAltGraphShift": "Æ" 265 | }, 266 | "Backquote": { 267 | "unmodified": "<", 268 | "withShift": ">", 269 | "withAltGraph": "≤", 270 | "withAltGraphShift": "≥" 271 | }, 272 | "Comma": { 273 | "unmodified": ",", 274 | "withShift": ";", 275 | "withAltGraph": "‚", 276 | "withAltGraphShift": "„" 277 | }, 278 | "Period": { 279 | "unmodified": ".", 280 | "withShift": ":", 281 | "withAltGraph": "…", 282 | "withAltGraphShift": "·" 283 | }, 284 | "Slash": { 285 | "unmodified": "-", 286 | "withShift": "_", 287 | "withAltGraph": "–", 288 | "withAltGraphShift": "—" 289 | }, 290 | "NumpadDivide": { 291 | "unmodified": "/", 292 | "withShift": "/", 293 | "withAltGraph": "/", 294 | "withAltGraphShift": "/" 295 | }, 296 | "NumpadMultiply": { 297 | "unmodified": "*", 298 | "withShift": "*", 299 | "withAltGraph": "*", 300 | "withAltGraphShift": "*" 301 | }, 302 | "NumpadSubtract": { 303 | "unmodified": "-", 304 | "withShift": "-", 305 | "withAltGraph": "-", 306 | "withAltGraphShift": "-" 307 | }, 308 | "NumpadAdd": { 309 | "unmodified": "+", 310 | "withShift": "+", 311 | "withAltGraph": "+", 312 | "withAltGraphShift": "+" 313 | }, 314 | "Numpad1": { 315 | "unmodified": "1", 316 | "withShift": "1", 317 | "withAltGraph": "1", 318 | "withAltGraphShift": "1" 319 | }, 320 | "Numpad2": { 321 | "unmodified": "2", 322 | "withShift": "2", 323 | "withAltGraph": "2", 324 | "withAltGraphShift": "2" 325 | }, 326 | "Numpad3": { 327 | "unmodified": "3", 328 | "withShift": "3", 329 | "withAltGraph": "3", 330 | "withAltGraphShift": "3" 331 | }, 332 | "Numpad4": { 333 | "unmodified": "4", 334 | "withShift": "4", 335 | "withAltGraph": "4", 336 | "withAltGraphShift": "4" 337 | }, 338 | "Numpad5": { 339 | "unmodified": "5", 340 | "withShift": "5", 341 | "withAltGraph": "5", 342 | "withAltGraphShift": "5" 343 | }, 344 | "Numpad6": { 345 | "unmodified": "6", 346 | "withShift": "6", 347 | "withAltGraph": "6", 348 | "withAltGraphShift": "6" 349 | }, 350 | "Numpad7": { 351 | "unmodified": "7", 352 | "withShift": "7", 353 | "withAltGraph": "7", 354 | "withAltGraphShift": "7" 355 | }, 356 | "Numpad8": { 357 | "unmodified": "8", 358 | "withShift": "8", 359 | "withAltGraph": "8", 360 | "withAltGraphShift": "8" 361 | }, 362 | "Numpad9": { 363 | "unmodified": "9", 364 | "withShift": "9", 365 | "withAltGraph": "9", 366 | "withAltGraphShift": "9" 367 | }, 368 | "Numpad0": { 369 | "unmodified": "0", 370 | "withShift": "0", 371 | "withAltGraph": "0", 372 | "withAltGraphShift": "0" 373 | }, 374 | "NumpadDecimal": { 375 | "unmodified": ",", 376 | "withShift": ".", 377 | "withAltGraph": ",", 378 | "withAltGraphShift": "." 379 | }, 380 | "IntlBackslash": { 381 | "unmodified": "§", 382 | "withShift": "°", 383 | "withAltGraph": "¶", 384 | "withAltGraphShift": "•" 385 | }, 386 | "NumpadEqual": { 387 | "unmodified": "=", 388 | "withShift": "=", 389 | "withAltGraph": "=", 390 | "withAltGraphShift": "=" 391 | }, 392 | "AudioVolumeUp": { 393 | "unmodified": null, 394 | "withShift": "=", 395 | "withAltGraph": null, 396 | "withAltGraphShift": "=" 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/mac-russian-pc.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "ф", 4 | "withShift": "Ф", 5 | "withAltGraph": "d", 6 | "withAltGraphShift": "d" 7 | }, 8 | "KeyB": { 9 | "unmodified": "и", 10 | "withShift": "И", 11 | "withAltGraph": "і", 12 | "withAltGraphShift": "І" 13 | }, 14 | "KeyC": { 15 | "unmodified": "с", 16 | "withShift": "С", 17 | "withAltGraph": "c", 18 | "withAltGraphShift": "C" 19 | }, 20 | "KeyD": { 21 | "unmodified": "в", 22 | "withShift": "В", 23 | "withAltGraph": "ћ", 24 | "withAltGraphShift": "Ћ" 25 | }, 26 | "KeyE": { 27 | "unmodified": "у", 28 | "withShift": "У", 29 | "withAltGraph": "ў", 30 | "withAltGraphShift": "Ў" 31 | }, 32 | "KeyF": { 33 | "unmodified": "а", 34 | "withShift": "А", 35 | "withAltGraph": "÷", 36 | "withAltGraphShift": "±" 37 | }, 38 | "KeyG": { 39 | "unmodified": "п", 40 | "withShift": "П", 41 | "withAltGraph": "…", 42 | "withAltGraphShift": "√" 43 | }, 44 | "KeyH": { 45 | "unmodified": "р", 46 | "withShift": "Р", 47 | "withAltGraph": "•", 48 | "withAltGraphShift": "∞" 49 | }, 50 | "KeyI": { 51 | "unmodified": "ш", 52 | "withShift": "Ш", 53 | "withAltGraph": "ѕ", 54 | "withAltGraphShift": "Ѕ" 55 | }, 56 | "KeyJ": { 57 | "unmodified": "о", 58 | "withShift": "О", 59 | "withAltGraph": "∆", 60 | "withAltGraphShift": "µ" 61 | }, 62 | "KeyK": { 63 | "unmodified": "л", 64 | "withShift": "Л", 65 | "withAltGraph": "љ", 66 | "withAltGraphShift": "Љ" 67 | }, 68 | "KeyL": { 69 | "unmodified": "д", 70 | "withShift": "Д", 71 | "withAltGraph": "l", 72 | "withAltGraphShift": "L" 73 | }, 74 | "KeyM": { 75 | "unmodified": "ь", 76 | "withShift": "Ь", 77 | "withAltGraph": "m", 78 | "withAltGraphShift": "M" 79 | }, 80 | "KeyN": { 81 | "unmodified": "т", 82 | "withShift": "Т", 83 | "withAltGraph": "ƒ", 84 | "withAltGraphShift": "ƒ" 85 | }, 86 | "KeyO": { 87 | "unmodified": "щ", 88 | "withShift": "Щ", 89 | "withAltGraph": "'", 90 | "withAltGraphShift": "„" 91 | }, 92 | "KeyP": { 93 | "unmodified": "з", 94 | "withShift": "З", 95 | "withAltGraph": "‘", 96 | "withAltGraphShift": "’" 97 | }, 98 | "KeyQ": { 99 | "unmodified": "й", 100 | "withShift": "Й", 101 | "withAltGraph": "ј", 102 | "withAltGraphShift": "Ј" 103 | }, 104 | "KeyR": { 105 | "unmodified": "к", 106 | "withShift": "К", 107 | "withAltGraph": "ќ", 108 | "withAltGraphShift": "Ќ" 109 | }, 110 | "KeyS": { 111 | "unmodified": "ы", 112 | "withShift": "Ы", 113 | "withAltGraph": "z", 114 | "withAltGraphShift": "Z" 115 | }, 116 | "KeyT": { 117 | "unmodified": "е", 118 | "withShift": "Е", 119 | "withAltGraph": "†", 120 | "withAltGraphShift": "†" 121 | }, 122 | "KeyU": { 123 | "unmodified": "г", 124 | "withShift": "Г", 125 | "withAltGraph": "ѓ", 126 | "withAltGraphShift": "Ѓ" 127 | }, 128 | "KeyV": { 129 | "unmodified": "м", 130 | "withShift": "М", 131 | "withAltGraph": "v", 132 | "withAltGraphShift": "V" 133 | }, 134 | "KeyW": { 135 | "unmodified": "ц", 136 | "withShift": "Ц", 137 | "withAltGraph": "џ", 138 | "withAltGraphShift": "Џ" 139 | }, 140 | "KeyX": { 141 | "unmodified": "ч", 142 | "withShift": "Ч", 143 | "withAltGraph": "x", 144 | "withAltGraphShift": "X" 145 | }, 146 | "KeyY": { 147 | "unmodified": "н", 148 | "withShift": "Н", 149 | "withAltGraph": "њ", 150 | "withAltGraphShift": "Њ" 151 | }, 152 | "KeyZ": { 153 | "unmodified": "я", 154 | "withShift": "Я", 155 | "withAltGraph": "ђ", 156 | "withAltGraphShift": "Ђ" 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "!", 161 | "withAltGraph": "!", 162 | "withAltGraphShift": "™" 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "\"", 167 | "withAltGraph": "@", 168 | "withAltGraphShift": "§" 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "№", 173 | "withAltGraph": "#", 174 | "withAltGraphShift": "£" 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": ";", 179 | "withAltGraph": "$", 180 | "withAltGraphShift": "€" 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": "©", 186 | "withAltGraphShift": "®" 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": ":", 191 | "withAltGraph": "^", 192 | "withAltGraphShift": "¬" 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "?", 197 | "withAltGraph": "&", 198 | "withAltGraphShift": "¶" 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "*", 203 | "withAltGraph": "₽", 204 | "withAltGraphShift": "°" 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": "(", 209 | "withAltGraph": "(", 210 | "withAltGraphShift": "{" 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": ")", 215 | "withAltGraph": ")", 216 | "withAltGraphShift": "}" 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": " ", 222 | "withAltGraphShift": " " 223 | }, 224 | "Minus": { 225 | "unmodified": "-", 226 | "withShift": "_", 227 | "withAltGraph": "–", 228 | "withAltGraphShift": "—" 229 | }, 230 | "Equal": { 231 | "unmodified": "=", 232 | "withShift": "+", 233 | "withAltGraph": "≠", 234 | "withAltGraphShift": "≈" 235 | }, 236 | "BracketLeft": { 237 | "unmodified": "х", 238 | "withShift": "Х", 239 | "withAltGraph": "“", 240 | "withAltGraphShift": "”" 241 | }, 242 | "BracketRight": { 243 | "unmodified": "ъ", 244 | "withShift": "Ъ", 245 | "withAltGraph": "«", 246 | "withAltGraphShift": "»" 247 | }, 248 | "Backslash": { 249 | "unmodified": "\\", 250 | "withShift": "/", 251 | "withAltGraph": "\\", 252 | "withAltGraphShift": "|" 253 | }, 254 | "Semicolon": { 255 | "unmodified": "ж", 256 | "withShift": "Ж", 257 | "withAltGraph": "«", 258 | "withAltGraphShift": "»" 259 | }, 260 | "Quote": { 261 | "unmodified": "э", 262 | "withShift": "Э", 263 | "withAltGraph": "є", 264 | "withAltGraphShift": "Є" 265 | }, 266 | "Backquote": { 267 | "unmodified": "]", 268 | "withShift": "[", 269 | "withAltGraph": "`", 270 | "withAltGraphShift": "~" 271 | }, 272 | "Comma": { 273 | "unmodified": "б", 274 | "withShift": "Б", 275 | "withAltGraph": "≤", 276 | "withAltGraphShift": "<" 277 | }, 278 | "Period": { 279 | "unmodified": "ю", 280 | "withShift": "Ю", 281 | "withAltGraph": "≥", 282 | "withAltGraphShift": ">" 283 | }, 284 | "Slash": { 285 | "unmodified": ".", 286 | "withShift": ",", 287 | "withAltGraph": "ї", 288 | "withAltGraphShift": "Ї" 289 | }, 290 | "NumpadDivide": { 291 | "unmodified": "/", 292 | "withShift": "/", 293 | "withAltGraph": "/", 294 | "withAltGraphShift": "/" 295 | }, 296 | "NumpadMultiply": { 297 | "unmodified": "*", 298 | "withShift": "*", 299 | "withAltGraph": "*", 300 | "withAltGraphShift": "*" 301 | }, 302 | "NumpadSubtract": { 303 | "unmodified": "-", 304 | "withShift": "-", 305 | "withAltGraph": "-", 306 | "withAltGraphShift": "-" 307 | }, 308 | "NumpadAdd": { 309 | "unmodified": "+", 310 | "withShift": "+", 311 | "withAltGraph": "+", 312 | "withAltGraphShift": "+" 313 | }, 314 | "Numpad1": { 315 | "unmodified": "1", 316 | "withShift": "1", 317 | "withAltGraph": "1", 318 | "withAltGraphShift": "1" 319 | }, 320 | "Numpad2": { 321 | "unmodified": "2", 322 | "withShift": "2", 323 | "withAltGraph": "2", 324 | "withAltGraphShift": "2" 325 | }, 326 | "Numpad3": { 327 | "unmodified": "3", 328 | "withShift": "3", 329 | "withAltGraph": "3", 330 | "withAltGraphShift": "3" 331 | }, 332 | "Numpad4": { 333 | "unmodified": "4", 334 | "withShift": "4", 335 | "withAltGraph": "4", 336 | "withAltGraphShift": "4" 337 | }, 338 | "Numpad5": { 339 | "unmodified": "5", 340 | "withShift": "5", 341 | "withAltGraph": "5", 342 | "withAltGraphShift": "5" 343 | }, 344 | "Numpad6": { 345 | "unmodified": "6", 346 | "withShift": "6", 347 | "withAltGraph": "6", 348 | "withAltGraphShift": "6" 349 | }, 350 | "Numpad7": { 351 | "unmodified": "7", 352 | "withShift": "7", 353 | "withAltGraph": "7", 354 | "withAltGraphShift": "7" 355 | }, 356 | "Numpad8": { 357 | "unmodified": "8", 358 | "withShift": "8", 359 | "withAltGraph": "8", 360 | "withAltGraphShift": "8" 361 | }, 362 | "Numpad9": { 363 | "unmodified": "9", 364 | "withShift": "9", 365 | "withAltGraph": "9", 366 | "withAltGraphShift": "9" 367 | }, 368 | "Numpad0": { 369 | "unmodified": "0", 370 | "withShift": "0", 371 | "withAltGraph": "0", 372 | "withAltGraphShift": "0" 373 | }, 374 | "NumpadDecimal": { 375 | "unmodified": ",", 376 | "withShift": ",", 377 | "withAltGraph": ",", 378 | "withAltGraphShift": "," 379 | }, 380 | "IntlBackslash": { 381 | "unmodified": "ё", 382 | "withShift": "Ё", 383 | "withAltGraph": "§", 384 | "withAltGraphShift": "±" 385 | }, 386 | "NumpadEqual": { 387 | "unmodified": "=", 388 | "withShift": "=", 389 | "withAltGraph": "=", 390 | "withAltGraphShift": "=" 391 | }, 392 | "AudioVolumeUp": { 393 | "unmodified": null, 394 | "withShift": "=", 395 | "withAltGraph": null, 396 | "withAltGraphShift": "=" 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/mac-turkish.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "u", 4 | "withShift": "U", 5 | "withAltGraph": "", 6 | "withAltGraphShift": "Ë" 7 | }, 8 | "KeyB": { 9 | "unmodified": "ç", 10 | "withShift": "Ç", 11 | "withAltGraph": "¯", 12 | "withAltGraphShift": "˙" 13 | }, 14 | "KeyC": { 15 | "unmodified": "v", 16 | "withShift": "V", 17 | "withAltGraph": "√", 18 | "withAltGraphShift": "◊" 19 | }, 20 | "KeyD": { 21 | "unmodified": "e", 22 | "withShift": "E", 23 | "withAltGraph": null, 24 | "withAltGraphShift": "‰" 25 | }, 26 | "KeyE": { 27 | "unmodified": "ğ", 28 | "withShift": "Ğ", 29 | "withAltGraph": "#", 30 | "withAltGraphShift": "´" 31 | }, 32 | "KeyF": { 33 | "unmodified": "a", 34 | "withShift": "A", 35 | "withAltGraph": "å", 36 | "withAltGraphShift": "Å" 37 | }, 38 | "KeyG": { 39 | "unmodified": "ü", 40 | "withShift": "Ü", 41 | "withAltGraph": "@", 42 | "withAltGraphShift": "ª" 43 | }, 44 | "KeyH": { 45 | "unmodified": "t", 46 | "withShift": "T", 47 | "withAltGraph": "₺", 48 | "withAltGraphShift": "Ê" 49 | }, 50 | "KeyI": { 51 | "unmodified": "n", 52 | "withShift": "N", 53 | "withAltGraph": null, 54 | "withAltGraphShift": "ˆ" 55 | }, 56 | "KeyJ": { 57 | "unmodified": "k", 58 | "withShift": "K", 59 | "withAltGraph": null, 60 | "withAltGraphShift": "©" 61 | }, 62 | "KeyK": { 63 | "unmodified": "m", 64 | "withShift": "M", 65 | "withAltGraph": "µ", 66 | "withAltGraphShift": "˜" 67 | }, 68 | "KeyL": { 69 | "unmodified": "l", 70 | "withShift": "L", 71 | "withAltGraph": "¬", 72 | "withAltGraphShift": "£" 73 | }, 74 | "KeyM": { 75 | "unmodified": "s", 76 | "withShift": "S", 77 | "withAltGraph": "§", 78 | "withAltGraphShift": "~" 79 | }, 80 | "KeyN": { 81 | "unmodified": "z", 82 | "withShift": "Z", 83 | "withAltGraph": "Ω", 84 | "withAltGraphShift": "Û" 85 | }, 86 | "KeyO": { 87 | "unmodified": "h", 88 | "withShift": "H", 89 | "withAltGraph": null, 90 | "withAltGraphShift": "Ó" 91 | }, 92 | "KeyP": { 93 | "unmodified": "p", 94 | "withShift": "P", 95 | "withAltGraph": "π", 96 | "withAltGraphShift": "∏" 97 | }, 98 | "KeyQ": { 99 | "unmodified": "f", 100 | "withShift": "F", 101 | "withAltGraph": "ƒ", 102 | "withAltGraphShift": "Ï" 103 | }, 104 | "KeyR": { 105 | "unmodified": "ı", 106 | "withShift": "I", 107 | "withAltGraph": "^", 108 | "withAltGraphShift": "È" 109 | }, 110 | "KeyS": { 111 | "unmodified": "i", 112 | "withShift": "İ", 113 | "withAltGraph": null, 114 | "withAltGraphShift": "∞" 115 | }, 116 | "KeyT": { 117 | "unmodified": "o", 118 | "withShift": "O", 119 | "withAltGraph": "ø", 120 | "withAltGraphShift": "Ø" 121 | }, 122 | "KeyU": { 123 | "unmodified": "r", 124 | "withShift": "R", 125 | "withAltGraph": "®", 126 | "withAltGraphShift": "Â" 127 | }, 128 | "KeyV": { 129 | "unmodified": "c", 130 | "withShift": "C", 131 | "withAltGraph": "ç", 132 | "withAltGraphShift": "˚" 133 | }, 134 | "KeyW": { 135 | "unmodified": "g", 136 | "withShift": "G", 137 | "withAltGraph": "¶", 138 | "withAltGraphShift": "`" 139 | }, 140 | "KeyX": { 141 | "unmodified": "ö", 142 | "withShift": "Ö", 143 | "withAltGraph": "¨", 144 | "withAltGraphShift": "Ÿ" 145 | }, 146 | "KeyY": { 147 | "unmodified": "d", 148 | "withShift": "D", 149 | "withAltGraph": "∂", 150 | "withAltGraphShift": "Î" 151 | }, 152 | "KeyZ": { 153 | "unmodified": "j", 154 | "withShift": "J", 155 | "withAltGraph": "∆", 156 | "withAltGraphShift": "Ô" 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "!", 161 | "withAltGraph": "¡", 162 | "withAltGraphShift": "·" 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "*", 167 | "withAltGraph": "™", 168 | "withAltGraphShift": "€" 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "\"", 173 | "withAltGraph": "“", 174 | "withAltGraphShift": "”" 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "'", 179 | "withAltGraph": "‘", 180 | "withAltGraphShift": "’" 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "(", 185 | "withAltGraph": "[", 186 | "withAltGraphShift": "{" 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": "/", 191 | "withAltGraph": "|", 192 | "withAltGraphShift": "\\" 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": ")", 197 | "withAltGraph": "]", 198 | "withAltGraphShift": "}" 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "_", 203 | "withAltGraph": "•", 204 | "withAltGraphShift": "°" 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": "%", 209 | "withAltGraph": "«", 210 | "withAltGraphShift": "»" 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": ":", 215 | "withAltGraph": "º", 216 | "withAltGraphShift": "÷" 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": " ", 222 | "withAltGraphShift": " " 223 | }, 224 | "Minus": { 225 | "unmodified": "x", 226 | "withShift": "X", 227 | "withAltGraph": "≈", 228 | "withAltGraphShift": "Ù" 229 | }, 230 | "Equal": { 231 | "unmodified": "q", 232 | "withShift": "Q", 233 | "withAltGraph": "œ", 234 | "withAltGraphShift": "Œ" 235 | }, 236 | "BracketLeft": { 237 | "unmodified": ";", 238 | "withShift": "$", 239 | "withAltGraph": "…", 240 | "withAltGraphShift": "¢" 241 | }, 242 | "BracketRight": { 243 | "unmodified": ">", 244 | "withShift": "<", 245 | "withAltGraph": "≥", 246 | "withAltGraphShift": "≤" 247 | }, 248 | "Backslash": { 249 | "unmodified": "=", 250 | "withShift": "+", 251 | "withAltGraph": "≠", 252 | "withAltGraphShift": "±" 253 | }, 254 | "Semicolon": { 255 | "unmodified": "y", 256 | "withShift": "Y", 257 | "withAltGraph": "¥", 258 | "withAltGraphShift": "Á" 259 | }, 260 | "Quote": { 261 | "unmodified": "ş", 262 | "withShift": "Ş", 263 | "withAltGraph": "æ", 264 | "withAltGraphShift": "Æ" 265 | }, 266 | "Backquote": { 267 | "unmodified": "w", 268 | "withShift": "W", 269 | "withAltGraph": "∑", 270 | "withAltGraphShift": "„" 271 | }, 272 | "Comma": { 273 | "unmodified": "b", 274 | "withShift": "B", 275 | "withAltGraph": "∫", 276 | "withAltGraphShift": "ß" 277 | }, 278 | "Period": { 279 | "unmodified": ".", 280 | "withShift": "?", 281 | "withAltGraph": "&", 282 | "withAltGraphShift": "¿" 283 | }, 284 | "Slash": { 285 | "unmodified": ",", 286 | "withShift": "-", 287 | "withAltGraph": "–", 288 | "withAltGraphShift": "—" 289 | }, 290 | "NumpadDivide": { 291 | "unmodified": "/", 292 | "withShift": "/", 293 | "withAltGraph": "/", 294 | "withAltGraphShift": "/" 295 | }, 296 | "NumpadMultiply": { 297 | "unmodified": "*", 298 | "withShift": "*", 299 | "withAltGraph": "*", 300 | "withAltGraphShift": "*" 301 | }, 302 | "NumpadSubtract": { 303 | "unmodified": "-", 304 | "withShift": "-", 305 | "withAltGraph": "-", 306 | "withAltGraphShift": "-" 307 | }, 308 | "NumpadAdd": { 309 | "unmodified": "+", 310 | "withShift": "+", 311 | "withAltGraph": "+", 312 | "withAltGraphShift": "+" 313 | }, 314 | "Numpad1": { 315 | "unmodified": "1", 316 | "withShift": "1", 317 | "withAltGraph": "1", 318 | "withAltGraphShift": "1" 319 | }, 320 | "Numpad2": { 321 | "unmodified": "2", 322 | "withShift": "2", 323 | "withAltGraph": "2", 324 | "withAltGraphShift": "2" 325 | }, 326 | "Numpad3": { 327 | "unmodified": "3", 328 | "withShift": "3", 329 | "withAltGraph": "3", 330 | "withAltGraphShift": "3" 331 | }, 332 | "Numpad4": { 333 | "unmodified": "4", 334 | "withShift": "4", 335 | "withAltGraph": "4", 336 | "withAltGraphShift": "4" 337 | }, 338 | "Numpad5": { 339 | "unmodified": "5", 340 | "withShift": "5", 341 | "withAltGraph": "5", 342 | "withAltGraphShift": "5" 343 | }, 344 | "Numpad6": { 345 | "unmodified": "6", 346 | "withShift": "6", 347 | "withAltGraph": "6", 348 | "withAltGraphShift": "6" 349 | }, 350 | "Numpad7": { 351 | "unmodified": "7", 352 | "withShift": "7", 353 | "withAltGraph": "7", 354 | "withAltGraphShift": "7" 355 | }, 356 | "Numpad8": { 357 | "unmodified": "8", 358 | "withShift": "8", 359 | "withAltGraph": "8", 360 | "withAltGraphShift": "8" 361 | }, 362 | "Numpad9": { 363 | "unmodified": "9", 364 | "withShift": "9", 365 | "withAltGraph": "9", 366 | "withAltGraphShift": "9" 367 | }, 368 | "Numpad0": { 369 | "unmodified": "0", 370 | "withShift": "0", 371 | "withAltGraph": "0", 372 | "withAltGraphShift": "0" 373 | }, 374 | "NumpadDecimal": { 375 | "unmodified": ",", 376 | "withShift": ",", 377 | "withAltGraph": ",", 378 | "withAltGraphShift": "," 379 | }, 380 | "IntlBackslash": { 381 | "unmodified": "§", 382 | "withShift": "±", 383 | "withAltGraph": null, 384 | "withAltGraphShift": null 385 | }, 386 | "NumpadEqual": { 387 | "unmodified": "=", 388 | "withShift": "=", 389 | "withAltGraph": "=", 390 | "withAltGraphShift": "=" 391 | }, 392 | "AudioVolumeUp": { 393 | "unmodified": null, 394 | "withShift": "=", 395 | "withAltGraph": null, 396 | "withAltGraphShift": "=" 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/mac-swiss-german.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A", 5 | "withAltGraph": "å", 6 | "withAltGraphShift": "Å" 7 | }, 8 | "KeyB": { 9 | "unmodified": "b", 10 | "withShift": "B", 11 | "withAltGraph": "∫", 12 | "withAltGraphShift": null 13 | }, 14 | "KeyC": { 15 | "unmodified": "c", 16 | "withShift": "C", 17 | "withAltGraph": "©", 18 | "withAltGraphShift": null 19 | }, 20 | "KeyD": { 21 | "unmodified": "d", 22 | "withShift": "D", 23 | "withAltGraph": "∂", 24 | "withAltGraphShift": "fl" 25 | }, 26 | "KeyE": { 27 | "unmodified": "e", 28 | "withShift": "E", 29 | "withAltGraph": "€", 30 | "withAltGraphShift": "Ë" 31 | }, 32 | "KeyF": { 33 | "unmodified": "f", 34 | "withShift": "F", 35 | "withAltGraph": "ƒ", 36 | "withAltGraphShift": "‡" 37 | }, 38 | "KeyG": { 39 | "unmodified": "g", 40 | "withShift": "G", 41 | "withAltGraph": "@", 42 | "withAltGraphShift": "‚" 43 | }, 44 | "KeyH": { 45 | "unmodified": "h", 46 | "withShift": "H", 47 | "withAltGraph": "ª", 48 | "withAltGraphShift": "·" 49 | }, 50 | "KeyI": { 51 | "unmodified": "i", 52 | "withShift": "I", 53 | "withAltGraph": "¡", 54 | "withAltGraphShift": "ı" 55 | }, 56 | "KeyJ": { 57 | "unmodified": "j", 58 | "withShift": "J", 59 | "withAltGraph": "º", 60 | "withAltGraphShift": "˜" 61 | }, 62 | "KeyK": { 63 | "unmodified": "k", 64 | "withShift": "K", 65 | "withAltGraph": "∆", 66 | "withAltGraphShift": "¯" 67 | }, 68 | "KeyL": { 69 | "unmodified": "l", 70 | "withShift": "L", 71 | "withAltGraph": "¬", 72 | "withAltGraphShift": "ˆ" 73 | }, 74 | "KeyM": { 75 | "unmodified": "m", 76 | "withShift": "M", 77 | "withAltGraph": "µ", 78 | "withAltGraphShift": "˚" 79 | }, 80 | "KeyN": { 81 | "unmodified": "n", 82 | "withShift": "N", 83 | "withAltGraph": null, 84 | "withAltGraphShift": "˙" 85 | }, 86 | "KeyO": { 87 | "unmodified": "o", 88 | "withShift": "O", 89 | "withAltGraph": "ø", 90 | "withAltGraphShift": "Ø" 91 | }, 92 | "KeyP": { 93 | "unmodified": "p", 94 | "withShift": "P", 95 | "withAltGraph": "π", 96 | "withAltGraphShift": "∏" 97 | }, 98 | "KeyQ": { 99 | "unmodified": "q", 100 | "withShift": "Q", 101 | "withAltGraph": "œ", 102 | "withAltGraphShift": "Œ" 103 | }, 104 | "KeyR": { 105 | "unmodified": "r", 106 | "withShift": "R", 107 | "withAltGraph": "®", 108 | "withAltGraphShift": "È" 109 | }, 110 | "KeyS": { 111 | "unmodified": "s", 112 | "withShift": "S", 113 | "withAltGraph": "ß", 114 | "withAltGraphShift": "fi" 115 | }, 116 | "KeyT": { 117 | "unmodified": "t", 118 | "withShift": "T", 119 | "withAltGraph": "†", 120 | "withAltGraphShift": "Î" 121 | }, 122 | "KeyU": { 123 | "unmodified": "u", 124 | "withShift": "U", 125 | "withAltGraph": "°", 126 | "withAltGraphShift": "Ù" 127 | }, 128 | "KeyV": { 129 | "unmodified": "v", 130 | "withShift": "V", 131 | "withAltGraph": "√", 132 | "withAltGraphShift": "◊" 133 | }, 134 | "KeyW": { 135 | "unmodified": "w", 136 | "withShift": "W", 137 | "withAltGraph": "∑", 138 | "withAltGraphShift": "Á" 139 | }, 140 | "KeyX": { 141 | "unmodified": "x", 142 | "withShift": "X", 143 | "withAltGraph": "≈", 144 | "withAltGraphShift": "™" 145 | }, 146 | "KeyY": { 147 | "unmodified": "z", 148 | "withShift": "Z", 149 | "withAltGraph": "Ω", 150 | "withAltGraphShift": "Í" 151 | }, 152 | "KeyZ": { 153 | "unmodified": "y", 154 | "withShift": "Y", 155 | "withAltGraph": "¥", 156 | "withAltGraphShift": "Ÿ" 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "+", 161 | "withAltGraph": "±", 162 | "withAltGraphShift": "∞" 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "\"", 167 | "withAltGraph": "“", 168 | "withAltGraphShift": "”" 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "*", 173 | "withAltGraph": "#", 174 | "withAltGraphShift": "‹" 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "ç", 179 | "withAltGraph": "Ç", 180 | "withAltGraphShift": "⁄" 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": "[", 186 | "withAltGraphShift": "[" 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": "&", 191 | "withAltGraph": "]", 192 | "withAltGraphShift": "]" 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "/", 197 | "withAltGraph": "|", 198 | "withAltGraphShift": "\\" 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "(", 203 | "withAltGraph": "{", 204 | "withAltGraphShift": "Ò" 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": ")", 209 | "withAltGraph": "}", 210 | "withAltGraphShift": "Ô" 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": "=", 215 | "withAltGraph": "≠", 216 | "withAltGraphShift": "Ú" 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": " ", 222 | "withAltGraphShift": " " 223 | }, 224 | "Minus": { 225 | "unmodified": "'", 226 | "withShift": "?", 227 | "withAltGraph": "¿", 228 | "withAltGraphShift": "" 229 | }, 230 | "Equal": { 231 | "unmodified": null, 232 | "withShift": null, 233 | "withAltGraph": null, 234 | "withAltGraphShift": "^" 235 | }, 236 | "BracketLeft": { 237 | "unmodified": "ü", 238 | "withShift": "è", 239 | "withAltGraph": "§", 240 | "withAltGraphShift": "ÿ" 241 | }, 242 | "BracketRight": { 243 | "unmodified": null, 244 | "withShift": "!", 245 | "withAltGraph": "‘", 246 | "withAltGraphShift": "’" 247 | }, 248 | "Backslash": { 249 | "unmodified": "$", 250 | "withShift": "£", 251 | "withAltGraph": "¶", 252 | "withAltGraphShift": "•" 253 | }, 254 | "Semicolon": { 255 | "unmodified": "ö", 256 | "withShift": "é", 257 | "withAltGraph": "¢", 258 | "withAltGraphShift": "˘" 259 | }, 260 | "Quote": { 261 | "unmodified": "ä", 262 | "withShift": "à", 263 | "withAltGraph": "æ", 264 | "withAltGraphShift": "Æ" 265 | }, 266 | "Backquote": { 267 | "unmodified": "<", 268 | "withShift": ">", 269 | "withAltGraph": "≤", 270 | "withAltGraphShift": "≥" 271 | }, 272 | "Comma": { 273 | "unmodified": ",", 274 | "withShift": ";", 275 | "withAltGraph": "«", 276 | "withAltGraphShift": "»" 277 | }, 278 | "Period": { 279 | "unmodified": ".", 280 | "withShift": ":", 281 | "withAltGraph": "…", 282 | "withAltGraphShift": "÷" 283 | }, 284 | "Slash": { 285 | "unmodified": "-", 286 | "withShift": "_", 287 | "withAltGraph": "–", 288 | "withAltGraphShift": "—" 289 | }, 290 | "NumpadDivide": { 291 | "unmodified": "/", 292 | "withShift": "/", 293 | "withAltGraph": "/", 294 | "withAltGraphShift": "/" 295 | }, 296 | "NumpadMultiply": { 297 | "unmodified": "*", 298 | "withShift": "*", 299 | "withAltGraph": "*", 300 | "withAltGraphShift": "*" 301 | }, 302 | "NumpadSubtract": { 303 | "unmodified": "-", 304 | "withShift": "-", 305 | "withAltGraph": "-", 306 | "withAltGraphShift": "-" 307 | }, 308 | "NumpadAdd": { 309 | "unmodified": "+", 310 | "withShift": "+", 311 | "withAltGraph": "+", 312 | "withAltGraphShift": "+" 313 | }, 314 | "Numpad1": { 315 | "unmodified": "1", 316 | "withShift": "1", 317 | "withAltGraph": "1", 318 | "withAltGraphShift": "1" 319 | }, 320 | "Numpad2": { 321 | "unmodified": "2", 322 | "withShift": "2", 323 | "withAltGraph": "2", 324 | "withAltGraphShift": "2" 325 | }, 326 | "Numpad3": { 327 | "unmodified": "3", 328 | "withShift": "3", 329 | "withAltGraph": "3", 330 | "withAltGraphShift": "3" 331 | }, 332 | "Numpad4": { 333 | "unmodified": "4", 334 | "withShift": "4", 335 | "withAltGraph": "4", 336 | "withAltGraphShift": "4" 337 | }, 338 | "Numpad5": { 339 | "unmodified": "5", 340 | "withShift": "5", 341 | "withAltGraph": "5", 342 | "withAltGraphShift": "5" 343 | }, 344 | "Numpad6": { 345 | "unmodified": "6", 346 | "withShift": "6", 347 | "withAltGraph": "6", 348 | "withAltGraphShift": "6" 349 | }, 350 | "Numpad7": { 351 | "unmodified": "7", 352 | "withShift": "7", 353 | "withAltGraph": "7", 354 | "withAltGraphShift": "7" 355 | }, 356 | "Numpad8": { 357 | "unmodified": "8", 358 | "withShift": "8", 359 | "withAltGraph": "8", 360 | "withAltGraphShift": "8" 361 | }, 362 | "Numpad9": { 363 | "unmodified": "9", 364 | "withShift": "9", 365 | "withAltGraph": "9", 366 | "withAltGraphShift": "9" 367 | }, 368 | "Numpad0": { 369 | "unmodified": "0", 370 | "withShift": "0", 371 | "withAltGraph": "0", 372 | "withAltGraphShift": "0" 373 | }, 374 | "NumpadDecimal": { 375 | "unmodified": ".", 376 | "withShift": ",", 377 | "withAltGraph": ".", 378 | "withAltGraphShift": "." 379 | }, 380 | "IntlBackslash": { 381 | "unmodified": "§", 382 | "withShift": "°", 383 | "withAltGraph": "fi", 384 | "withAltGraphShift": "‰" 385 | }, 386 | "NumpadEqual": { 387 | "unmodified": "=", 388 | "withShift": "=", 389 | "withAltGraph": "=", 390 | "withAltGraphShift": "=" 391 | }, 392 | "AudioVolumeUp": { 393 | "unmodified": null, 394 | "withShift": "=", 395 | "withAltGraph": null, 396 | "withAltGraphShift": "=" 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/mac-dvorak-qwerty-cmd.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "a", 4 | "withShift": "A", 5 | "withAltGraph": "å", 6 | "withAltGraphShift": "Å" 7 | }, 8 | "KeyB": { 9 | "unmodified": "x", 10 | "withShift": "X", 11 | "withAltGraph": "≈", 12 | "withAltGraphShift": "˛" 13 | }, 14 | "KeyC": { 15 | "unmodified": "j", 16 | "withShift": "J", 17 | "withAltGraph": "∆", 18 | "withAltGraphShift": "Ô" 19 | }, 20 | "KeyD": { 21 | "unmodified": "e", 22 | "withShift": "E", 23 | "withAltGraph": null, 24 | "withAltGraphShift": "´" 25 | }, 26 | "KeyE": { 27 | "unmodified": ".", 28 | "withShift": ">", 29 | "withAltGraph": "≥", 30 | "withAltGraphShift": "˘" 31 | }, 32 | "KeyF": { 33 | "unmodified": "u", 34 | "withShift": "U", 35 | "withAltGraph": null, 36 | "withAltGraphShift": "¨" 37 | }, 38 | "KeyG": { 39 | "unmodified": "i", 40 | "withShift": "I", 41 | "withAltGraph": null, 42 | "withAltGraphShift": "ˆ" 43 | }, 44 | "KeyH": { 45 | "unmodified": "d", 46 | "withShift": "D", 47 | "withAltGraph": "∂", 48 | "withAltGraphShift": "Î" 49 | }, 50 | "KeyI": { 51 | "unmodified": "c", 52 | "withShift": "C", 53 | "withAltGraph": "ç", 54 | "withAltGraphShift": "Ç" 55 | }, 56 | "KeyJ": { 57 | "unmodified": "h", 58 | "withShift": "H", 59 | "withAltGraph": "˙", 60 | "withAltGraphShift": "Ó" 61 | }, 62 | "KeyK": { 63 | "unmodified": "t", 64 | "withShift": "T", 65 | "withAltGraph": "†", 66 | "withAltGraphShift": "ˇ" 67 | }, 68 | "KeyL": { 69 | "unmodified": "n", 70 | "withShift": "N", 71 | "withAltGraph": null, 72 | "withAltGraphShift": "˜" 73 | }, 74 | "KeyM": { 75 | "unmodified": "m", 76 | "withShift": "M", 77 | "withAltGraph": "µ", 78 | "withAltGraphShift": "Â" 79 | }, 80 | "KeyN": { 81 | "unmodified": "b", 82 | "withShift": "B", 83 | "withAltGraph": "∫", 84 | "withAltGraphShift": "ı" 85 | }, 86 | "KeyO": { 87 | "unmodified": "r", 88 | "withShift": "R", 89 | "withAltGraph": "®", 90 | "withAltGraphShift": "‰" 91 | }, 92 | "KeyP": { 93 | "unmodified": "l", 94 | "withShift": "L", 95 | "withAltGraph": "¬", 96 | "withAltGraphShift": "Ò" 97 | }, 98 | "KeyQ": { 99 | "unmodified": "'", 100 | "withShift": "\"", 101 | "withAltGraph": "æ", 102 | "withAltGraphShift": "Æ" 103 | }, 104 | "KeyR": { 105 | "unmodified": "p", 106 | "withShift": "P", 107 | "withAltGraph": "π", 108 | "withAltGraphShift": "∏" 109 | }, 110 | "KeyS": { 111 | "unmodified": "o", 112 | "withShift": "O", 113 | "withAltGraph": "ø", 114 | "withAltGraphShift": "Ø" 115 | }, 116 | "KeyT": { 117 | "unmodified": "y", 118 | "withShift": "Y", 119 | "withAltGraph": "¥", 120 | "withAltGraphShift": "Á" 121 | }, 122 | "KeyU": { 123 | "unmodified": "g", 124 | "withShift": "G", 125 | "withAltGraph": "©", 126 | "withAltGraphShift": "˝" 127 | }, 128 | "KeyV": { 129 | "unmodified": "k", 130 | "withShift": "K", 131 | "withAltGraph": "˚", 132 | "withAltGraphShift": "" 133 | }, 134 | "KeyW": { 135 | "unmodified": ",", 136 | "withShift": "<", 137 | "withAltGraph": "≤", 138 | "withAltGraphShift": "¯" 139 | }, 140 | "KeyX": { 141 | "unmodified": "q", 142 | "withShift": "Q", 143 | "withAltGraph": "œ", 144 | "withAltGraphShift": "Œ" 145 | }, 146 | "KeyY": { 147 | "unmodified": "f", 148 | "withShift": "F", 149 | "withAltGraph": "ƒ", 150 | "withAltGraphShift": "Ï" 151 | }, 152 | "KeyZ": { 153 | "unmodified": ";", 154 | "withShift": ":", 155 | "withAltGraph": "…", 156 | "withAltGraphShift": "Ú" 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "!", 161 | "withAltGraph": "¡", 162 | "withAltGraphShift": "⁄" 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "@", 167 | "withAltGraph": "™", 168 | "withAltGraphShift": "€" 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "#", 173 | "withAltGraph": "£", 174 | "withAltGraphShift": "‹" 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "$", 179 | "withAltGraph": "¢", 180 | "withAltGraphShift": "›" 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": "∞", 186 | "withAltGraphShift": "fi" 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": "^", 191 | "withAltGraph": "§", 192 | "withAltGraphShift": "fl" 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "&", 197 | "withAltGraph": "¶", 198 | "withAltGraphShift": "‡" 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "*", 203 | "withAltGraph": "•", 204 | "withAltGraphShift": "°" 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": "(", 209 | "withAltGraph": "ª", 210 | "withAltGraphShift": "·" 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": ")", 215 | "withAltGraph": "º", 216 | "withAltGraphShift": "‚" 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": " ", 222 | "withAltGraphShift": " " 223 | }, 224 | "Minus": { 225 | "unmodified": "[", 226 | "withShift": "{", 227 | "withAltGraph": "“", 228 | "withAltGraphShift": "”" 229 | }, 230 | "Equal": { 231 | "unmodified": "]", 232 | "withShift": "}", 233 | "withAltGraph": "‘", 234 | "withAltGraphShift": "’" 235 | }, 236 | "BracketLeft": { 237 | "unmodified": "/", 238 | "withShift": "?", 239 | "withAltGraph": "÷", 240 | "withAltGraphShift": "¿" 241 | }, 242 | "BracketRight": { 243 | "unmodified": "=", 244 | "withShift": "+", 245 | "withAltGraph": "≠", 246 | "withAltGraphShift": "±" 247 | }, 248 | "Backslash": { 249 | "unmodified": "\\", 250 | "withShift": "|", 251 | "withAltGraph": "«", 252 | "withAltGraphShift": "»" 253 | }, 254 | "Semicolon": { 255 | "unmodified": "s", 256 | "withShift": "S", 257 | "withAltGraph": "ß", 258 | "withAltGraphShift": "Í" 259 | }, 260 | "Quote": { 261 | "unmodified": "-", 262 | "withShift": "_", 263 | "withAltGraph": "–", 264 | "withAltGraphShift": "—" 265 | }, 266 | "Backquote": { 267 | "unmodified": "`", 268 | "withShift": "~", 269 | "withAltGraph": null, 270 | "withAltGraphShift": "`" 271 | }, 272 | "Comma": { 273 | "unmodified": "w", 274 | "withShift": "W", 275 | "withAltGraph": "∑", 276 | "withAltGraphShift": "„" 277 | }, 278 | "Period": { 279 | "unmodified": "v", 280 | "withShift": "V", 281 | "withAltGraph": "√", 282 | "withAltGraphShift": "◊" 283 | }, 284 | "Slash": { 285 | "unmodified": "z", 286 | "withShift": "Z", 287 | "withAltGraph": "Ω", 288 | "withAltGraphShift": "¸" 289 | }, 290 | "NumpadDivide": { 291 | "unmodified": "/", 292 | "withShift": "/", 293 | "withAltGraph": "/", 294 | "withAltGraphShift": "/" 295 | }, 296 | "NumpadMultiply": { 297 | "unmodified": "*", 298 | "withShift": "*", 299 | "withAltGraph": "*", 300 | "withAltGraphShift": "*" 301 | }, 302 | "NumpadSubtract": { 303 | "unmodified": "-", 304 | "withShift": "-", 305 | "withAltGraph": "-", 306 | "withAltGraphShift": "-" 307 | }, 308 | "NumpadAdd": { 309 | "unmodified": "+", 310 | "withShift": "+", 311 | "withAltGraph": "+", 312 | "withAltGraphShift": "+" 313 | }, 314 | "Numpad1": { 315 | "unmodified": "1", 316 | "withShift": "1", 317 | "withAltGraph": "1", 318 | "withAltGraphShift": "1" 319 | }, 320 | "Numpad2": { 321 | "unmodified": "2", 322 | "withShift": "2", 323 | "withAltGraph": "2", 324 | "withAltGraphShift": "2" 325 | }, 326 | "Numpad3": { 327 | "unmodified": "3", 328 | "withShift": "3", 329 | "withAltGraph": "3", 330 | "withAltGraphShift": "3" 331 | }, 332 | "Numpad4": { 333 | "unmodified": "4", 334 | "withShift": "4", 335 | "withAltGraph": "4", 336 | "withAltGraphShift": "4" 337 | }, 338 | "Numpad5": { 339 | "unmodified": "5", 340 | "withShift": "5", 341 | "withAltGraph": "5", 342 | "withAltGraphShift": "5" 343 | }, 344 | "Numpad6": { 345 | "unmodified": "6", 346 | "withShift": "6", 347 | "withAltGraph": "6", 348 | "withAltGraphShift": "6" 349 | }, 350 | "Numpad7": { 351 | "unmodified": "7", 352 | "withShift": "7", 353 | "withAltGraph": "7", 354 | "withAltGraphShift": "7" 355 | }, 356 | "Numpad8": { 357 | "unmodified": "8", 358 | "withShift": "8", 359 | "withAltGraph": "8", 360 | "withAltGraphShift": "8" 361 | }, 362 | "Numpad9": { 363 | "unmodified": "9", 364 | "withShift": "9", 365 | "withAltGraph": "9", 366 | "withAltGraphShift": "9" 367 | }, 368 | "Numpad0": { 369 | "unmodified": "0", 370 | "withShift": "0", 371 | "withAltGraph": "0", 372 | "withAltGraphShift": "0" 373 | }, 374 | "NumpadDecimal": { 375 | "unmodified": ".", 376 | "withShift": ".", 377 | "withAltGraph": ".", 378 | "withAltGraphShift": "." 379 | }, 380 | "IntlBackslash": { 381 | "unmodified": "§", 382 | "withShift": "±", 383 | "withAltGraph": "§", 384 | "withAltGraphShift": "±" 385 | }, 386 | "NumpadEqual": { 387 | "unmodified": "=", 388 | "withShift": "=", 389 | "withAltGraph": "=", 390 | "withAltGraphShift": "=" 391 | }, 392 | "AudioVolumeUp": { 393 | "unmodified": null, 394 | "withShift": "=", 395 | "withAltGraph": null, 396 | "withAltGraphShift": "=" 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/mac-greek.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "α", 4 | "withShift": "Α", 5 | "withAltGraph": "…", 6 | "withAltGraphShift": "ά" 7 | }, 8 | "KeyB": { 9 | "unmodified": "β", 10 | "withShift": "Β", 11 | "withAltGraph": null, 12 | "withAltGraphShift": null 13 | }, 14 | "KeyC": { 15 | "unmodified": "ψ", 16 | "withShift": "Ψ", 17 | "withAltGraph": "ç", 18 | "withAltGraphShift": "Χ" 19 | }, 20 | "KeyD": { 21 | "unmodified": "δ", 22 | "withShift": "Δ", 23 | "withAltGraph": "÷", 24 | "withAltGraphShift": null 25 | }, 26 | "KeyE": { 27 | "unmodified": "ε", 28 | "withShift": "Ε", 29 | "withAltGraph": "€", 30 | "withAltGraphShift": "Έ" 31 | }, 32 | "KeyF": { 33 | "unmodified": "φ", 34 | "withShift": "Φ", 35 | "withAltGraph": "≠", 36 | "withAltGraphShift": "Π" 37 | }, 38 | "KeyG": { 39 | "unmodified": "γ", 40 | "withShift": "Γ", 41 | "withAltGraph": "©", 42 | "withAltGraphShift": "Ϊ" 43 | }, 44 | "KeyH": { 45 | "unmodified": "η", 46 | "withShift": "Η", 47 | "withAltGraph": "½", 48 | "withAltGraphShift": "≠" 49 | }, 50 | "KeyI": { 51 | "unmodified": "ι", 52 | "withShift": "Ι", 53 | "withAltGraph": "†", 54 | "withAltGraphShift": "Ν" 55 | }, 56 | "KeyJ": { 57 | "unmodified": "ξ", 58 | "withShift": "Ξ", 59 | "withAltGraph": "≤", 60 | "withAltGraphShift": "§" 61 | }, 62 | "KeyK": { 63 | "unmodified": "κ", 64 | "withShift": "Κ", 65 | "withAltGraph": "≥", 66 | "withAltGraphShift": "°" 67 | }, 68 | "KeyL": { 69 | "unmodified": "λ", 70 | "withShift": "Λ", 71 | "withAltGraph": "¬", 72 | "withAltGraphShift": "·" 73 | }, 74 | "KeyM": { 75 | "unmodified": "μ", 76 | "withShift": "Μ", 77 | "withAltGraph": "’", 78 | "withAltGraphShift": "Ύ" 79 | }, 80 | "KeyN": { 81 | "unmodified": "ν", 82 | "withShift": "Ν", 83 | "withAltGraph": "‘", 84 | "withAltGraphShift": "Ό" 85 | }, 86 | "KeyO": { 87 | "unmodified": "ο", 88 | "withShift": "Ο", 89 | "withAltGraph": "œ", 90 | "withAltGraphShift": "Τ" 91 | }, 92 | "KeyP": { 93 | "unmodified": "π", 94 | "withShift": "Π", 95 | "withAltGraph": "≈", 96 | "withAltGraphShift": null 97 | }, 98 | "KeyQ": { 99 | "unmodified": ";", 100 | "withShift": ":", 101 | "withAltGraph": "·", 102 | "withAltGraphShift": "―" 103 | }, 104 | "KeyR": { 105 | "unmodified": "ρ", 106 | "withShift": "Ρ", 107 | "withAltGraph": "®", 108 | "withAltGraphShift": "Δ" 109 | }, 110 | "KeyS": { 111 | "unmodified": "σ", 112 | "withShift": "Σ", 113 | "withAltGraph": "ß", 114 | "withAltGraphShift": "ρ" 115 | }, 116 | "KeyT": { 117 | "unmodified": "τ", 118 | "withShift": "Τ", 119 | "withAltGraph": "™", 120 | "withAltGraphShift": null 121 | }, 122 | "KeyU": { 123 | "unmodified": "θ", 124 | "withShift": "Θ", 125 | "withAltGraph": "­", 126 | "withAltGraphShift": "Ξ" 127 | }, 128 | "KeyV": { 129 | "unmodified": "ω", 130 | "withShift": "Ω", 131 | "withAltGraph": "±", 132 | "withAltGraphShift": "Ά" 133 | }, 134 | "KeyW": { 135 | "unmodified": "ς", 136 | "withShift": null, 137 | "withAltGraph": "―", 138 | "withAltGraphShift": "Β" 139 | }, 140 | "KeyX": { 141 | "unmodified": "χ", 142 | "withShift": "Χ", 143 | "withAltGraph": null, 144 | "withAltGraphShift": null 145 | }, 146 | "KeyY": { 147 | "unmodified": "υ", 148 | "withShift": "Υ", 149 | "withAltGraph": "¥", 150 | "withAltGraphShift": null 151 | }, 152 | "KeyZ": { 153 | "unmodified": "ζ", 154 | "withShift": "Ζ", 155 | "withAltGraph": "§", 156 | "withAltGraphShift": null 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "!", 161 | "withAltGraph": "¹", 162 | "withAltGraphShift": "έ" 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "@", 167 | "withAltGraph": "²", 168 | "withAltGraphShift": "ί" 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "#", 173 | "withAltGraph": "³", 174 | "withAltGraphShift": "ή" 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "$", 179 | "withAltGraph": "£", 180 | "withAltGraphShift": "ό" 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": "§", 186 | "withAltGraphShift": "Ώ" 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": "^", 191 | "withAltGraph": "¶", 192 | "withAltGraphShift": "^" 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "&", 197 | "withAltGraph": "°", 198 | "withAltGraphShift": null 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "*", 203 | "withAltGraph": "•", 204 | "withAltGraphShift": "Ρ" 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": "(", 209 | "withAltGraph": "“", 210 | "withAltGraphShift": "Κ" 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": ")", 215 | "withAltGraph": "°", 216 | "withAltGraphShift": "ύ" 217 | }, 218 | "Space": { 219 | "unmodified": " ", 220 | "withShift": " ", 221 | "withAltGraph": " ", 222 | "withAltGraphShift": " " 223 | }, 224 | "Minus": { 225 | "unmodified": "-", 226 | "withShift": "_", 227 | "withAltGraph": "±", 228 | "withAltGraphShift": "_" 229 | }, 230 | "Equal": { 231 | "unmodified": "=", 232 | "withShift": "+", 233 | "withAltGraph": "½", 234 | "withAltGraphShift": "+" 235 | }, 236 | "BracketLeft": { 237 | "unmodified": "[", 238 | "withShift": "{", 239 | "withAltGraph": "«", 240 | "withAltGraphShift": "{" 241 | }, 242 | "BracketRight": { 243 | "unmodified": "]", 244 | "withShift": "}", 245 | "withAltGraph": "»", 246 | "withAltGraphShift": "}" 247 | }, 248 | "Backslash": { 249 | "unmodified": "\\", 250 | "withShift": "|", 251 | "withAltGraph": "¦", 252 | "withAltGraphShift": "Ζ" 253 | }, 254 | "Semicolon": { 255 | "unmodified": null, 256 | "withShift": null, 257 | "withAltGraph": null, 258 | "withAltGraphShift": ":" 259 | }, 260 | "Quote": { 261 | "unmodified": "'", 262 | "withShift": "\"", 263 | "withAltGraph": "'", 264 | "withAltGraphShift": "\"" 265 | }, 266 | "Backquote": { 267 | "unmodified": "`", 268 | "withShift": "~", 269 | "withAltGraph": "`", 270 | "withAltGraphShift": "Μ" 271 | }, 272 | "Comma": { 273 | "unmodified": ",", 274 | "withShift": "<", 275 | "withAltGraph": "«", 276 | "withAltGraphShift": null 277 | }, 278 | "Period": { 279 | "unmodified": ".", 280 | "withShift": ">", 281 | "withAltGraph": "»", 282 | "withAltGraphShift": "ώ" 283 | }, 284 | "Slash": { 285 | "unmodified": "/", 286 | "withShift": "?", 287 | "withAltGraph": "/", 288 | "withAltGraphShift": "?" 289 | }, 290 | "NumpadDivide": { 291 | "unmodified": "/", 292 | "withShift": "/", 293 | "withAltGraph": "/", 294 | "withAltGraphShift": "/" 295 | }, 296 | "NumpadMultiply": { 297 | "unmodified": "*", 298 | "withShift": "*", 299 | "withAltGraph": "*", 300 | "withAltGraphShift": "*" 301 | }, 302 | "NumpadSubtract": { 303 | "unmodified": "-", 304 | "withShift": "-", 305 | "withAltGraph": "-", 306 | "withAltGraphShift": "-" 307 | }, 308 | "NumpadAdd": { 309 | "unmodified": "+", 310 | "withShift": "+", 311 | "withAltGraph": "+", 312 | "withAltGraphShift": "+" 313 | }, 314 | "Numpad1": { 315 | "unmodified": "1", 316 | "withShift": "1", 317 | "withAltGraph": "1", 318 | "withAltGraphShift": "1" 319 | }, 320 | "Numpad2": { 321 | "unmodified": "2", 322 | "withShift": "2", 323 | "withAltGraph": "2", 324 | "withAltGraphShift": "2" 325 | }, 326 | "Numpad3": { 327 | "unmodified": "3", 328 | "withShift": "3", 329 | "withAltGraph": "3", 330 | "withAltGraphShift": "3" 331 | }, 332 | "Numpad4": { 333 | "unmodified": "4", 334 | "withShift": "4", 335 | "withAltGraph": "4", 336 | "withAltGraphShift": "4" 337 | }, 338 | "Numpad5": { 339 | "unmodified": "5", 340 | "withShift": "5", 341 | "withAltGraph": "5", 342 | "withAltGraphShift": "5" 343 | }, 344 | "Numpad6": { 345 | "unmodified": "6", 346 | "withShift": "6", 347 | "withAltGraph": "6", 348 | "withAltGraphShift": "6" 349 | }, 350 | "Numpad7": { 351 | "unmodified": "7", 352 | "withShift": "7", 353 | "withAltGraph": "7", 354 | "withAltGraphShift": "7" 355 | }, 356 | "Numpad8": { 357 | "unmodified": "8", 358 | "withShift": "8", 359 | "withAltGraph": "8", 360 | "withAltGraphShift": "8" 361 | }, 362 | "Numpad9": { 363 | "unmodified": "9", 364 | "withShift": "9", 365 | "withAltGraph": "9", 366 | "withAltGraphShift": "9" 367 | }, 368 | "Numpad0": { 369 | "unmodified": "0", 370 | "withShift": "0", 371 | "withAltGraph": "0", 372 | "withAltGraphShift": "0" 373 | }, 374 | "NumpadDecimal": { 375 | "unmodified": ".", 376 | "withShift": ".", 377 | "withAltGraph": ".", 378 | "withAltGraphShift": "." 379 | }, 380 | "IntlBackslash": { 381 | "unmodified": "§", 382 | "withShift": "±", 383 | "withAltGraph": null, 384 | "withAltGraphShift": null 385 | }, 386 | "NumpadEqual": { 387 | "unmodified": "=", 388 | "withShift": "=", 389 | "withAltGraph": "=", 390 | "withAltGraphShift": "=" 391 | }, 392 | "AudioVolumeUp": { 393 | "unmodified": null, 394 | "withShift": "=", 395 | "withAltGraph": null, 396 | "withAltGraphShift": "=" 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /spec/helpers/keymaps/mac-hebrew.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyA": { 3 | "unmodified": "ש", 4 | "withShift": "שׁ", 5 | "withAltGraph": "שׂ", 6 | "withAltGraphShift": "שׂ" 7 | }, 8 | "KeyB": { 9 | "unmodified": "נ", 10 | "withShift": null, 11 | "withAltGraph": "‘", 12 | "withAltGraphShift": "֜" 13 | }, 14 | "KeyC": { 15 | "unmodified": "ב", 16 | "withShift": "לֹ", 17 | "withAltGraph": "“", 18 | "withAltGraphShift": "֞" 19 | }, 20 | "KeyD": { 21 | "unmodified": "ג", 22 | "withShift": "„", 23 | "withAltGraph": "‏", 24 | "withAltGraphShift": null 25 | }, 26 | "KeyE": { 27 | "unmodified": "ק", 28 | "withShift": null, 29 | "withAltGraph": null, 30 | "withAltGraphShift": null 31 | }, 32 | "KeyF": { 33 | "unmodified": "כ", 34 | "withShift": null, 35 | "withAltGraph": "€", 36 | "withAltGraphShift": "€" 37 | }, 38 | "KeyG": { 39 | "unmodified": "ע", 40 | "withShift": null, 41 | "withAltGraph": "֞", 42 | "withAltGraphShift": null 43 | }, 44 | "KeyH": { 45 | "unmodified": "י", 46 | "withShift": null, 47 | "withAltGraph": "ײַ", 48 | "withAltGraphShift": "ײַ" 49 | }, 50 | "KeyI": { 51 | "unmodified": "ן", 52 | "withShift": null, 53 | "withAltGraph": null, 54 | "withAltGraphShift": null 55 | }, 56 | "KeyJ": { 57 | "unmodified": "ח", 58 | "withShift": null, 59 | "withAltGraph": "”", 60 | "withAltGraphShift": null 61 | }, 62 | "KeyK": { 63 | "unmodified": "ל", 64 | "withShift": "לֹ", 65 | "withAltGraph": "֜", 66 | "withAltGraphShift": null 67 | }, 68 | "KeyL": { 69 | "unmodified": "ך", 70 | "withShift": null, 71 | "withAltGraph": "’", 72 | "withAltGraphShift": null 73 | }, 74 | "KeyM": { 75 | "unmodified": "צ", 76 | "withShift": null, 77 | "withAltGraph": "שׁ", 78 | "withAltGraphShift": "שׁ" 79 | }, 80 | "KeyN": { 81 | "unmodified": "מ", 82 | "withShift": null, 83 | "withAltGraph": "’", 84 | "withAltGraphShift": "’" 85 | }, 86 | "KeyO": { 87 | "unmodified": "ם", 88 | "withShift": null, 89 | "withAltGraph": null, 90 | "withAltGraphShift": null 91 | }, 92 | "KeyP": { 93 | "unmodified": "פ", 94 | "withShift": null, 95 | "withAltGraph": null, 96 | "withAltGraphShift": null 97 | }, 98 | "KeyQ": { 99 | "unmodified": "/", 100 | "withShift": null, 101 | "withAltGraph": "…", 102 | "withAltGraphShift": "…" 103 | }, 104 | "KeyR": { 105 | "unmodified": "ר", 106 | "withShift": null, 107 | "withAltGraph": null, 108 | "withAltGraphShift": null 109 | }, 110 | "KeyS": { 111 | "unmodified": "ד", 112 | "withShift": null, 113 | "withAltGraph": "‎", 114 | "withAltGraphShift": null 115 | }, 116 | "KeyT": { 117 | "unmodified": "א", 118 | "withShift": null, 119 | "withAltGraph": null, 120 | "withAltGraphShift": null 121 | }, 122 | "KeyU": { 123 | "unmodified": "ו", 124 | "withShift": "וֹ", 125 | "withAltGraph": "וּ", 126 | "withAltGraphShift": "וּ" 127 | }, 128 | "KeyV": { 129 | "unmodified": "ה", 130 | "withShift": null, 131 | "withAltGraph": "”", 132 | "withAltGraphShift": "”" 133 | }, 134 | "KeyW": { 135 | "unmodified": "׳", 136 | "withShift": null, 137 | "withAltGraph": "ָ", 138 | "withAltGraphShift": "ָ" 139 | }, 140 | "KeyX": { 141 | "unmodified": "ס", 142 | "withShift": null, 143 | "withAltGraph": "—", 144 | "withAltGraphShift": "—" 145 | }, 146 | "KeyY": { 147 | "unmodified": "ט", 148 | "withShift": null, 149 | "withAltGraph": null, 150 | "withAltGraphShift": null 151 | }, 152 | "KeyZ": { 153 | "unmodified": "ז", 154 | "withShift": null, 155 | "withAltGraph": "–", 156 | "withAltGraphShift": "–" 157 | }, 158 | "Digit1": { 159 | "unmodified": "1", 160 | "withShift": "!", 161 | "withAltGraph": "ֲ", 162 | "withAltGraphShift": "ֲ" 163 | }, 164 | "Digit2": { 165 | "unmodified": "2", 166 | "withShift": "@", 167 | "withAltGraph": "ֳ", 168 | "withAltGraphShift": "ֳ" 169 | }, 170 | "Digit3": { 171 | "unmodified": "3", 172 | "withShift": "#", 173 | "withAltGraph": "ֱ", 174 | "withAltGraphShift": "ֱ" 175 | }, 176 | "Digit4": { 177 | "unmodified": "4", 178 | "withShift": "$", 179 | "withAltGraph": "ִ", 180 | "withAltGraphShift": "ִ" 181 | }, 182 | "Digit5": { 183 | "unmodified": "5", 184 | "withShift": "%", 185 | "withAltGraph": "ֵ", 186 | "withAltGraphShift": "ֵ" 187 | }, 188 | "Digit6": { 189 | "unmodified": "6", 190 | "withShift": "^", 191 | "withAltGraph": "ַ", 192 | "withAltGraphShift": "ַ" 193 | }, 194 | "Digit7": { 195 | "unmodified": "7", 196 | "withShift": "₪", 197 | "withAltGraph": "ָ", 198 | "withAltGraphShift": "ָ" 199 | }, 200 | "Digit8": { 201 | "unmodified": "8", 202 | "withShift": "*", 203 | "withAltGraph": "ֻ", 204 | "withAltGraphShift": "ֻ" 205 | }, 206 | "Digit9": { 207 | "unmodified": "9", 208 | "withShift": ")", 209 | "withAltGraph": "ֶ", 210 | "withAltGraphShift": "ֶ" 211 | }, 212 | "Digit0": { 213 | "unmodified": "0", 214 | "withShift": "(", 215 | "withAltGraph": "ְ", 216 | "withAltGraphShift": "ְ" 217 | }, 218 | "Escape": { 219 | "unmodified": null, 220 | "withShift": null, 221 | "withAltGraph": "‏", 222 | "withAltGraphShift": "‎" 223 | }, 224 | "Space": { 225 | "unmodified": " ", 226 | "withShift": " ", 227 | "withAltGraph": " ", 228 | "withAltGraphShift": " " 229 | }, 230 | "Minus": { 231 | "unmodified": "-", 232 | "withShift": "_", 233 | "withAltGraph": "-", 234 | "withAltGraphShift": "-" 235 | }, 236 | "Equal": { 237 | "unmodified": "=", 238 | "withShift": "+", 239 | "withAltGraph": "ֹ", 240 | "withAltGraphShift": "ֹ" 241 | }, 242 | "BracketLeft": { 243 | "unmodified": "]", 244 | "withShift": "}", 245 | "withAltGraph": null, 246 | "withAltGraphShift": null 247 | }, 248 | "BracketRight": { 249 | "unmodified": "[", 250 | "withShift": "{", 251 | "withAltGraph": null, 252 | "withAltGraphShift": null 253 | }, 254 | "Backslash": { 255 | "unmodified": "ֿ", 256 | "withShift": "|", 257 | "withAltGraph": "־", 258 | "withAltGraphShift": "־" 259 | }, 260 | "Semicolon": { 261 | "unmodified": "ף", 262 | "withShift": ":", 263 | "withAltGraph": ";", 264 | "withAltGraphShift": ";" 265 | }, 266 | "Quote": { 267 | "unmodified": ",", 268 | "withShift": "״", 269 | "withAltGraph": "ֲ", 270 | "withAltGraphShift": "ֲ" 271 | }, 272 | "Backquote": { 273 | "unmodified": ";", 274 | "withShift": "~", 275 | "withAltGraph": null, 276 | "withAltGraphShift": null 277 | }, 278 | "Comma": { 279 | "unmodified": "ת", 280 | "withShift": ">", 281 | "withAltGraph": "ּ", 282 | "withAltGraphShift": "ּ" 283 | }, 284 | "Period": { 285 | "unmodified": "ץ", 286 | "withShift": "<", 287 | "withAltGraph": null, 288 | "withAltGraphShift": null 289 | }, 290 | "Slash": { 291 | "unmodified": ".", 292 | "withShift": "?", 293 | "withAltGraph": null, 294 | "withAltGraphShift": null 295 | }, 296 | "NumpadDivide": { 297 | "unmodified": "/", 298 | "withShift": "/", 299 | "withAltGraph": "/", 300 | "withAltGraphShift": "/" 301 | }, 302 | "NumpadMultiply": { 303 | "unmodified": "*", 304 | "withShift": "*", 305 | "withAltGraph": "*", 306 | "withAltGraphShift": "*" 307 | }, 308 | "NumpadSubtract": { 309 | "unmodified": "-", 310 | "withShift": "-", 311 | "withAltGraph": "-", 312 | "withAltGraphShift": "-" 313 | }, 314 | "NumpadAdd": { 315 | "unmodified": "+", 316 | "withShift": "+", 317 | "withAltGraph": "+", 318 | "withAltGraphShift": "+" 319 | }, 320 | "Numpad1": { 321 | "unmodified": "1", 322 | "withShift": "1", 323 | "withAltGraph": "1", 324 | "withAltGraphShift": "1" 325 | }, 326 | "Numpad2": { 327 | "unmodified": "2", 328 | "withShift": "2", 329 | "withAltGraph": "2", 330 | "withAltGraphShift": "2" 331 | }, 332 | "Numpad3": { 333 | "unmodified": "3", 334 | "withShift": "3", 335 | "withAltGraph": "3", 336 | "withAltGraphShift": "3" 337 | }, 338 | "Numpad4": { 339 | "unmodified": "4", 340 | "withShift": "4", 341 | "withAltGraph": "4", 342 | "withAltGraphShift": "4" 343 | }, 344 | "Numpad5": { 345 | "unmodified": "5", 346 | "withShift": "5", 347 | "withAltGraph": "5", 348 | "withAltGraphShift": "5" 349 | }, 350 | "Numpad6": { 351 | "unmodified": "6", 352 | "withShift": "6", 353 | "withAltGraph": "6", 354 | "withAltGraphShift": "6" 355 | }, 356 | "Numpad7": { 357 | "unmodified": "7", 358 | "withShift": "7", 359 | "withAltGraph": "7", 360 | "withAltGraphShift": "7" 361 | }, 362 | "Numpad8": { 363 | "unmodified": "8", 364 | "withShift": "8", 365 | "withAltGraph": "8", 366 | "withAltGraphShift": "8" 367 | }, 368 | "Numpad9": { 369 | "unmodified": "9", 370 | "withShift": "9", 371 | "withAltGraph": "9", 372 | "withAltGraphShift": "9" 373 | }, 374 | "Numpad0": { 375 | "unmodified": "0", 376 | "withShift": "0", 377 | "withAltGraph": "0", 378 | "withAltGraphShift": "0" 379 | }, 380 | "NumpadDecimal": { 381 | "unmodified": ".", 382 | "withShift": ".", 383 | "withAltGraph": ".", 384 | "withAltGraphShift": "." 385 | }, 386 | "NumpadEqual": { 387 | "unmodified": "=", 388 | "withShift": "=", 389 | "withAltGraph": "=", 390 | "withAltGraphShift": "=" 391 | }, 392 | "AudioVolumeUp": { 393 | "unmodified": null, 394 | "withShift": "=", 395 | "withAltGraph": "=", 396 | "withAltGraphShift": "=" 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/helpers.coffee: -------------------------------------------------------------------------------- 1 | {calculateSpecificity} = require 'clear-cut' 2 | KeyboardLayout = require 'keyboard-layout' 3 | 4 | MODIFIERS = new Set(['ctrl', 'alt', 'shift', 'cmd']) 5 | ENDS_IN_MODIFIER_REGEX = /(ctrl|alt|shift|cmd)$/ 6 | WHITESPACE_REGEX = /\s+/ 7 | KEY_NAMES_BY_KEYBOARD_EVENT_CODE = { 8 | 'Space': 'space', 9 | 'Backspace': 'backspace' 10 | } 11 | NON_CHARACTER_KEY_NAMES_BY_KEYBOARD_EVENT_KEY = { 12 | 'Control': 'ctrl', 13 | 'Meta': 'cmd', 14 | 'ArrowDown': 'down', 15 | 'ArrowUp': 'up', 16 | 'ArrowLeft': 'left', 17 | 'ArrowRight': 'right' 18 | } 19 | NUMPAD_KEY_NAMES_BY_KEYBOARD_EVENT_CODE = { 20 | 'Numpad0': 'numpad0', 21 | 'Numpad1': 'numpad1', 22 | 'Numpad2': 'numpad2', 23 | 'Numpad3': 'numpad3', 24 | 'Numpad4': 'numpad4', 25 | 'Numpad5': 'numpad5', 26 | 'Numpad6': 'numpad6', 27 | 'Numpad7': 'numpad7', 28 | 'Numpad8': 'numpad8', 29 | 'Numpad9': 'numpad9' 30 | } 31 | 32 | LATIN_KEYMAP_CACHE = new WeakMap() 33 | isLatinKeymap = (keymap) -> 34 | return true unless keymap? 35 | 36 | isLatin = LATIN_KEYMAP_CACHE.get(keymap) 37 | if isLatin? 38 | isLatin 39 | else 40 | # To avoid exceptions, if the native keymap does not have entries for a key, 41 | # assume that key is latin. 42 | isLatin = 43 | (not keymap.KeyA? or isLatinCharacter(keymap.KeyA.unmodified)) and 44 | (not keymap.KeyS? or isLatinCharacter(keymap.KeyS.unmodified)) and 45 | (not keymap.KeyD? or isLatinCharacter(keymap.KeyD.unmodified)) and 46 | (not keymap.KeyF? or isLatinCharacter(keymap.KeyF.unmodified)) 47 | LATIN_KEYMAP_CACHE.set(keymap, isLatin) 48 | isLatin 49 | 50 | isASCIICharacter = (character) -> 51 | character? and character.length is 1 and character.charCodeAt(0) <= 127 52 | 53 | isLatinCharacter = (character) -> 54 | character? and character.length is 1 and character.charCodeAt(0) <= 0x024F 55 | 56 | isUpperCaseCharacter = (character) -> 57 | character? and character.length is 1 and character.toLowerCase() isnt character 58 | 59 | isLowerCaseCharacter = (character) -> 60 | character? and character.length is 1 and character.toUpperCase() isnt character 61 | 62 | usKeymap = null 63 | usCharactersForKeyCode = (code) -> 64 | usKeymap ?= require('./us-keymap') 65 | usKeymap[code] 66 | 67 | slovakCmdKeymap = null 68 | slovakQwertyCmdKeymap = null 69 | slovakCmdCharactersForKeyCode = (code, layout) -> 70 | slovakCmdKeymap ?= require('./slovak-cmd-keymap') 71 | slovakQwertyCmdKeymap ?= require('./slovak-qwerty-cmd-keymap') 72 | 73 | if layout is 'com.apple.keylayout.Slovak' 74 | slovakCmdKeymap[code] 75 | else 76 | slovakQwertyCmdKeymap[code] 77 | 78 | exports.normalizeKeystrokes = (keystrokes) -> 79 | normalizedKeystrokes = [] 80 | for keystroke in keystrokes.split(WHITESPACE_REGEX) 81 | if normalizedKeystroke = normalizeKeystroke(keystroke) 82 | normalizedKeystrokes.push(normalizedKeystroke) 83 | else 84 | return false 85 | normalizedKeystrokes.join(' ') 86 | 87 | normalizeKeystroke = (keystroke) -> 88 | if keyup = isKeyup(keystroke) 89 | keystroke = keystroke.slice(1) 90 | keys = parseKeystroke(keystroke) 91 | return false unless keys 92 | 93 | primaryKey = null 94 | modifiers = new Set 95 | 96 | for key, i in keys 97 | if MODIFIERS.has(key) 98 | modifiers.add(key) 99 | else 100 | # only the last key can be a non-modifier 101 | if i is keys.length - 1 102 | primaryKey = key 103 | else 104 | return false 105 | 106 | if keyup 107 | primaryKey = primaryKey.toLowerCase() if primaryKey? 108 | else 109 | modifiers.add('shift') if isUpperCaseCharacter(primaryKey) 110 | if modifiers.has('shift') and isLowerCaseCharacter(primaryKey) 111 | primaryKey = primaryKey.toUpperCase() 112 | 113 | keystroke = [] 114 | if not keyup or (keyup and not primaryKey?) 115 | keystroke.push('ctrl') if modifiers.has('ctrl') 116 | keystroke.push('alt') if modifiers.has('alt') 117 | keystroke.push('shift') if modifiers.has('shift') 118 | keystroke.push('cmd') if modifiers.has('cmd') 119 | keystroke.push(primaryKey) if primaryKey? 120 | keystroke = keystroke.join('-') 121 | keystroke = "^#{keystroke}" if keyup 122 | keystroke 123 | 124 | parseKeystroke = (keystroke) -> 125 | keys = [] 126 | keyStart = 0 127 | for character, index in keystroke when character is '-' 128 | if index > keyStart 129 | keys.push(keystroke.substring(keyStart, index)) 130 | keyStart = index + 1 131 | 132 | # The keystroke has a trailing - and is invalid 133 | return false if keyStart is keystroke.length 134 | keys.push(keystroke.substring(keyStart)) if keyStart < keystroke.length 135 | keys 136 | 137 | exports.keystrokeForKeyboardEvent = (event, customKeystrokeResolvers) -> 138 | {key, code, ctrlKey, altKey, shiftKey, metaKey} = event 139 | 140 | currentLayout = KeyboardLayout.getCurrentKeyboardLayout() 141 | 142 | if key is 'Dead' 143 | if process.platform is 'darwin' and characters = KeyboardLayout.getCurrentKeymap()?[event.code] 144 | if altKey and shiftKey and characters.withAltGraphShift? 145 | key = characters.withAltGraphShift 146 | else if altKey and characters.withAltGraph? 147 | key = characters.withAltGraph 148 | else if shiftKey and characters.withShift? 149 | key = characters.withShift 150 | else if characters.unmodified? 151 | key = characters.unmodified 152 | 153 | if NUMPAD_KEY_NAMES_BY_KEYBOARD_EVENT_CODE[code]? and event.getModifierState('NumLock') 154 | key = NUMPAD_KEY_NAMES_BY_KEYBOARD_EVENT_CODE[code] 155 | 156 | if KEY_NAMES_BY_KEYBOARD_EVENT_CODE[code]? 157 | key = KEY_NAMES_BY_KEYBOARD_EVENT_CODE[code] 158 | 159 | # Work around Chrome bugs on Linux 160 | if process.platform is 'linux' 161 | # Fix NumpadDecimal key value being '' with NumLock disabled. 162 | if code is 'NumpadDecimal' and not event.getModifierState('NumLock') 163 | key = 'delete' 164 | # Fix 'Unidentified' key value for '/' key on Brazillian keyboards 165 | if code is 'IntlRo' and key is 'Unidentified' and ctrlKey 166 | key = '/' 167 | 168 | isAltModifiedKey = false 169 | isNonCharacterKey = key.length > 1 170 | if isNonCharacterKey 171 | key = NON_CHARACTER_KEY_NAMES_BY_KEYBOARD_EVENT_KEY[key] ? key.toLowerCase() 172 | if key is "altgraph" and process.platform is "win32" 173 | key = "alt" 174 | else 175 | # Deal with caps-lock issues. Key bindings should always adjust the 176 | # capitalization of the key based on the shiftKey state and never the state 177 | # of the caps-lock key 178 | if shiftKey 179 | key = key.toUpperCase() 180 | else 181 | key = key.toLowerCase() 182 | 183 | if event.getModifierState('AltGraph') or (process.platform is 'darwin' and altKey) 184 | # All macOS layouts have an alt-modified character variant for every 185 | # single key. Therefore, if we always favored the alt variant, it would 186 | # become impossible to bind `alt-*` to anything. Since `alt-*` bindings 187 | # are rare and we bind very few by default on macOS, we will only shadow 188 | # an `alt-*` binding with an alt-modified character variant if it is a 189 | # basic ASCII character. 190 | if process.platform is 'darwin' and event.code 191 | nonAltModifiedKey = nonAltModifiedKeyForKeyboardEvent(event) 192 | if nonAltModifiedKey and (ctrlKey or metaKey or not isASCIICharacter(key)) 193 | key = nonAltModifiedKey 194 | else if key isnt nonAltModifiedKey 195 | altKey = false 196 | isAltModifiedKey = true 197 | # Windows layouts are more sparing in their use of AltGr-modified 198 | # characters, and the U.S. layout doesn't have any of them at all. That 199 | # means that if an AltGr variant character exists for the current 200 | # keystroke, it likely to be the intended character, and we always 201 | # interpret it as such rather than favoring a `ctrl-alt-*` binding 202 | # intepretation. 203 | else if process.platform is 'win32' and event.code 204 | nonAltModifiedKey = nonAltModifiedKeyForKeyboardEvent(event) 205 | if nonAltModifiedKey and (metaKey or not isASCIICharacter(key)) 206 | key = nonAltModifiedKey 207 | else if key isnt nonAltModifiedKey 208 | ctrlKey = false 209 | altKey = false 210 | isAltModifiedKey = true 211 | # Linux has a dedicated `AltGraph` key that is distinct from all other 212 | # modifiers, including LeftAlt. However, if AltGraph is used in 213 | # combination with other modifiers, we want to treat it as a modifier and 214 | # fall back to the non-alt-modified character. 215 | else if process.platform is 'linux' 216 | nonAltModifiedKey = nonAltModifiedKeyForKeyboardEvent(event) 217 | if nonAltModifiedKey and (ctrlKey or altKey or metaKey) 218 | key = nonAltModifiedKey 219 | altKey = event.getModifierState('AltGraph') 220 | isAltModifiedKey = not altKey 221 | 222 | # Ensure that shifted writing system characters are reported correctly 223 | if event.code and key.length is 1 224 | characters = 225 | # TODO: Remove the com.apple.keylayout.DVORAK-QWERTYCMD 226 | # case when we are using an Electron version based on Chromium M62 227 | # That issue was fixed in https://bugs.chromium.org/p/chromium/issues/detail?id=747358 228 | # Use US equivalent character for non-latin characters in keystrokes with modifiers 229 | # or when using the dvorak-qwertycmd layout and holding down the command key. 230 | # if (key.length is 1 and not isLatinKeymap(KeyboardLayout.getCurrentKeymap())) or 231 | if (not isLatinKeymap(KeyboardLayout.getCurrentKeymap())) or 232 | (metaKey and currentLayout.indexOf('DVORAK-QWERTYCMD') > -1) 233 | usCharactersForKeyCode(event.code) 234 | # As of Chromium ~62, KeyboardEvent.key is now sent in its un-shifted 235 | # for writing system characters (`8` vs `*`) so we need to manually 236 | # fetch the shifted version to maintain our former keystroke output 237 | else if not isAltModifiedKey 238 | KeyboardLayout.getCurrentKeymap()?[event.code] 239 | 240 | if characters 241 | if event.shiftKey 242 | key = characters.withShift 243 | else if characters.unmodified? 244 | key = characters.unmodified 245 | 246 | # Work around https://bugs.chromium.org/p/chromium/issues/detail?id=766800 247 | # TODO: Remove this workaround when we are using an Electron version based on chrome M62 248 | if metaKey and currentLayout is 'com.apple.keylayout.Slovak' or currentLayout is 'com.apple.keylayout.Slovak-QWERTY' 249 | if characters = slovakCmdCharactersForKeyCode(event.code, currentLayout) 250 | if event.shiftKey 251 | key = characters.withShift 252 | else 253 | key = characters.unmodified 254 | 255 | keystroke = '' 256 | if key is 'ctrl' or (ctrlKey and event.type isnt 'keyup') 257 | keystroke += 'ctrl' 258 | 259 | if key is 'alt' or (altKey and event.type isnt 'keyup') 260 | keystroke += '-' if keystroke.length > 0 261 | keystroke += 'alt' 262 | 263 | if key is 'shift' or (shiftKey and event.type isnt 'keyup' and (isNonCharacterKey or (isLatinCharacter(key) and isUpperCaseCharacter(key)))) 264 | keystroke += '-' if keystroke 265 | keystroke += 'shift' 266 | 267 | if key is 'cmd' or (metaKey and event.type isnt 'keyup') 268 | keystroke += '-' if keystroke 269 | keystroke += 'cmd' 270 | 271 | unless MODIFIERS.has(key) 272 | keystroke += '-' if keystroke 273 | keystroke += key 274 | 275 | keystroke = normalizeKeystroke("^#{keystroke}") if event.type is 'keyup' 276 | 277 | if customKeystrokeResolvers? 278 | for resolver in customKeystrokeResolvers 279 | customKeystroke = resolver({ 280 | keystroke, event, 281 | layoutName: KeyboardLayout.getCurrentKeyboardLayout(), 282 | keymap: KeyboardLayout.getCurrentKeymap() 283 | }) 284 | if customKeystroke 285 | keystroke = normalizeKeystroke(customKeystroke) 286 | 287 | keystroke 288 | 289 | nonAltModifiedKeyForKeyboardEvent = (event) -> 290 | if event.code and (characters = KeyboardLayout.getCurrentKeymap()?[event.code]) 291 | if event.shiftKey 292 | characters.withShift 293 | else 294 | characters.unmodified 295 | 296 | exports.MODIFIERS = MODIFIERS 297 | 298 | exports.characterForKeyboardEvent = (event) -> 299 | event.key if event.key.length is 1 and not (event.ctrlKey or event.metaKey) 300 | 301 | exports.calculateSpecificity = calculateSpecificity 302 | 303 | exports.isBareModifier = (keystroke) -> ENDS_IN_MODIFIER_REGEX.test(keystroke) 304 | 305 | exports.isModifierKeyup = (keystroke) -> isKeyup(keystroke) and ENDS_IN_MODIFIER_REGEX.test(keystroke) 306 | 307 | exports.isKeyup = isKeyup = (keystroke) -> keystroke.startsWith('^') and keystroke isnt '^' 308 | 309 | exports.keydownEvent = (key, options) -> 310 | return buildKeyboardEvent(key, 'keydown', options) 311 | 312 | exports.keyupEvent = (key, options) -> 313 | return buildKeyboardEvent(key, 'keyup', options) 314 | 315 | exports.getModifierKeys = (keystroke) -> 316 | keys = keystroke.split('-') 317 | mod_keys = [] 318 | for key in keys when MODIFIERS.has(key) 319 | mod_keys.push(key) 320 | mod_keys 321 | 322 | 323 | buildKeyboardEvent = (key, eventType, {ctrl, shift, alt, cmd, keyCode, target, location}={}) -> 324 | ctrlKey = ctrl ? false 325 | altKey = alt ? false 326 | shiftKey = shift ? false 327 | metaKey = cmd ? false 328 | bubbles = true 329 | cancelable = true 330 | 331 | event = new KeyboardEvent(eventType, { 332 | key, ctrlKey, altKey, shiftKey, metaKey, bubbles, cancelable 333 | }) 334 | 335 | if target? 336 | Object.defineProperty(event, 'target', get: -> target) 337 | Object.defineProperty(event, 'path', get: -> [target]) 338 | event 339 | -------------------------------------------------------------------------------- /src/keymap-manager.coffee: -------------------------------------------------------------------------------- 1 | CSON = require 'season' 2 | fs = require 'fs-plus' 3 | {isSelectorValid} = require 'clear-cut' 4 | path = require 'path' 5 | {File} = require 'pathwatcher' 6 | {Emitter, Disposable, CompositeDisposable} = require 'event-kit' 7 | {KeyBinding, MATCH_TYPES} = require './key-binding' 8 | CommandEvent = require './command-event' 9 | {normalizeKeystrokes, keystrokeForKeyboardEvent, isBareModifier, keydownEvent, keyupEvent, characterForKeyboardEvent, keystrokesMatch, isKeyup} = require './helpers' 10 | PartialKeyupMatcher = require './partial-keyup-matcher' 11 | 12 | Platforms = ['darwin', 'freebsd', 'linux', 'sunos', 'win32'] 13 | OtherPlatforms = Platforms.filter (platform) -> platform isnt process.platform 14 | 15 | # Extended: Allows commands to be associated with keystrokes in a 16 | # context-sensitive way. In Atom, you can access a global instance of this 17 | # object via `atom.keymaps`. 18 | # 19 | # Key bindings are plain JavaScript objects containing **CSS selectors** as 20 | # their top level keys, then **keystroke patterns** mapped to commands. 21 | # 22 | # ```cson 23 | # '.workspace': 24 | # 'ctrl-l': 'package:do-something' 25 | # 'ctrl-z': 'package:do-something-else' 26 | # '.mini.editor': 27 | # 'enter': 'core:confirm' 28 | # ``` 29 | # 30 | # When a keystroke sequence matches a binding in a given context, a custom DOM 31 | # event with a type based on the command is dispatched on the target of the 32 | # keyboard event. 33 | # 34 | # To match a keystroke sequence, the keymap starts at the target element for the 35 | # keyboard event. It looks for key bindings associated with selectors that match 36 | # the target element. If multiple match, the most specific is selected. If there 37 | # is a tie in specificity, the most recently added binding wins. If no bindings 38 | # are found for the events target, the search is repeated again for the target's 39 | # parent node and so on recursively until a binding is found or we traverse off 40 | # the top of the document. 41 | # 42 | # When a binding is found, its command event is always dispatched on the 43 | # original target of the keyboard event, even if the matching element is higher 44 | # up in the DOM. In addition, `.preventDefault()` is called on the keyboard 45 | # event to prevent the browser from taking action. `.preventDefault` is only 46 | # called if a matching binding is found. 47 | # 48 | # Command event objects have a non-standard method called `.abortKeyBinding()`. 49 | # If your command handler is invoked but you programmatically determine that no 50 | # action can be taken and you want to allow other bindings to be matched, call 51 | # `.abortKeyBinding()` on the event object. An example of where this is useful 52 | # is binding snippet expansion to `tab`. If `snippets:expand` is invoked when 53 | # the cursor does not follow a valid snippet prefix, we abort the binding and 54 | # allow `tab` to be handled by the default handler, which inserts whitespace. 55 | # 56 | # Multi-keystroke bindings are possible. If a sequence of one or more keystrokes 57 | # *partially* matches a multi-keystroke binding, the keymap enters a pending 58 | # state. The pending state is terminated on the next keystroke, or after 59 | # {::getPartialMatchTimeout} milliseconds has elapsed. When the pending state is 60 | # terminated via a timeout or a keystroke that leads to no matches, the longest 61 | # ambiguous bindings that caused the pending state are temporarily disabled and 62 | # the previous keystrokes are replayed. If there is ambiguity again during the 63 | # replay, the next longest bindings are disabled and the keystrokes are replayed 64 | # again. 65 | module.exports = 66 | class KeymapManager 67 | ### 68 | Section: Class Methods 69 | ### 70 | 71 | # Public: Create a keydown DOM event for testing purposes. 72 | # 73 | # * `key` The key or keyIdentifier of the event. For example, `'a'`, `'1'`, 74 | # `'escape'`, `'backspace'`, etc. 75 | # * `options` (optional) An {Object} containing any of the following: 76 | # * `ctrl` A {Boolean} indicating the ctrl modifier key 77 | # * `alt` A {Boolean} indicating the alt modifier key 78 | # * `shift` A {Boolean} indicating the shift modifier key 79 | # * `cmd` A {Boolean} indicating the cmd modifier key 80 | # * `which` A {Number} indicating `which` value of the event. See 81 | # the docs for KeyboardEvent for more information. 82 | # * `target` The target element of the event. 83 | @buildKeydownEvent: (key, options) -> keydownEvent(key, options) 84 | 85 | @buildKeyupEvent: (key, options) -> keyupEvent(key, options) 86 | 87 | ### 88 | Section: Properties 89 | ### 90 | 91 | partialMatchTimeout: 1000 92 | 93 | defaultTarget: null 94 | pendingPartialMatches: null 95 | pendingStateTimeoutHandle: null 96 | 97 | # Pending matches to bindings that begin with keydowns and end with a subset 98 | # of matching keyups 99 | pendingKeyupMatcher: new PartialKeyupMatcher() 100 | 101 | ### 102 | Section: Construction and Destruction 103 | ### 104 | 105 | # Public: Create a new KeymapManager. 106 | # 107 | # * `options` An {Object} containing properties to assign to the keymap. You 108 | # can pass custom properties to be used by extension methods. The 109 | # following properties are also supported: 110 | # * `defaultTarget` This will be used as the target of events whose target 111 | # is `document.body` to allow for a catch-all element when nothing is focused. 112 | constructor: (options={}) -> 113 | @[key] = value for key, value of options 114 | @watchSubscriptions = {} 115 | @customKeystrokeResolvers = [] 116 | @clear() 117 | 118 | # Public: Clear all registered key bindings and enqueued keystrokes. For use 119 | # in tests. 120 | clear: -> 121 | @emitter = new Emitter 122 | @keyBindings = [] 123 | @queuedKeyboardEvents = [] 124 | @queuedKeystrokes = [] 125 | @bindingsToDisable = [] 126 | 127 | # Public: Unwatch all watched paths. 128 | destroy: -> 129 | for filePath, subscription of @watchSubscriptions 130 | subscription.dispose() 131 | 132 | return 133 | 134 | ### 135 | Section: Event Subscription 136 | ### 137 | 138 | # Public: Invoke the given callback when one or more keystrokes completely 139 | # match a key binding. 140 | # 141 | # * `callback` {Function} to be called when keystrokes match a binding. 142 | # * `event` {Object} with the following keys: 143 | # * `keystrokes` {String} of keystrokes that matched the binding. 144 | # * `binding` {KeyBinding} that the keystrokes matched. 145 | # * `keyboardEventTarget` DOM element that was the target of the most 146 | # recent keyboard event. 147 | # 148 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 149 | onDidMatchBinding: (callback) -> 150 | @emitter.on 'did-match-binding', callback 151 | 152 | # Public: Invoke the given callback when one or more keystrokes partially 153 | # match a binding. 154 | # 155 | # * `callback` {Function} to be called when keystrokes partially match a 156 | # binding. 157 | # * `event` {Object} with the following keys: 158 | # * `keystrokes` {String} of keystrokes that matched the binding. 159 | # * `partiallyMatchedBindings` {KeyBinding}s that the keystrokes partially 160 | # matched. 161 | # * `keyboardEventTarget` DOM element that was the target of the most 162 | # recent keyboard event. 163 | # 164 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 165 | onDidPartiallyMatchBindings: (callback) -> 166 | @emitter.on 'did-partially-match-binding', callback 167 | 168 | # Public: Invoke the given callback when one or more keystrokes fail to match 169 | # any bindings. 170 | # 171 | # * `callback` {Function} to be called when keystrokes fail to match any 172 | # bindings. 173 | # * `event` {Object} with the following keys: 174 | # * `keystrokes` {String} of keystrokes that matched the binding. 175 | # * `keyboardEventTarget` DOM element that was the target of the most 176 | # recent keyboard event. 177 | # 178 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 179 | onDidFailToMatchBinding: (callback) -> 180 | @emitter.on 'did-fail-to-match-binding', callback 181 | 182 | # Invoke the given callback when a keymap file is reloaded. 183 | # 184 | # * `callback` {Function} to be called when a keymap file is reloaded. 185 | # * `event` {Object} with the following keys: 186 | # * `path` {String} representing the path of the reloaded keymap file. 187 | # 188 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 189 | onDidReloadKeymap: (callback) -> 190 | @emitter.on 'did-reload-keymap', callback 191 | 192 | # Invoke the given callback when a keymap file is unloaded. 193 | # 194 | # * `callback` {Function} to be called when a keymap file is unloaded. 195 | # * `event` {Object} with the following keys: 196 | # * `path` {String} representing the path of the unloaded keymap file. 197 | # 198 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 199 | onDidUnloadKeymap: (callback) -> 200 | @emitter.on 'did-unload-keymap', callback 201 | 202 | # Public: Invoke the given callback when a keymap file not able to be loaded. 203 | # 204 | # * `callback` {Function} to be called when a keymap file is unloaded. 205 | # * `error` {Object} with the following keys: 206 | # * `message` {String} the error message. 207 | # * `stack` {String} the error stack trace. 208 | # 209 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 210 | onDidFailToReadFile: (callback) -> 211 | @emitter.on 'did-fail-to-read-file', callback 212 | 213 | ### 214 | Section: Adding and Removing Bindings 215 | ### 216 | 217 | # Extended: Construct {KeyBinding}s from an object grouping them by CSS selector. 218 | # 219 | # * `source` A {String} (usually a path) uniquely identifying the given bindings 220 | # so they can be removed later. 221 | # * `bindings` An {Object} whose top-level keys point at sub-objects mapping 222 | # keystroke patterns to commands. 223 | # * `priority` A {Number} used to sort keybindings which have the same 224 | # specificity. Defaults to `0`. 225 | build: (source, keyBindingsBySelector, priority=0, throwOnInvalidSelector=true) -> 226 | bindings = [] 227 | for selector, keyBindings of keyBindingsBySelector 228 | # Verify selector is valid before registering any bindings 229 | if throwOnInvalidSelector and not isSelectorValid(selector.replace(/!important/g, '')) 230 | console.warn("Encountered an invalid selector adding key bindings from '#{source}': '#{selector}'") 231 | continue 232 | 233 | unless typeof keyBindings is 'object' 234 | console.warn("Encountered an invalid key binding when adding key bindings from '#{source}' '#{keyBindings}'") 235 | continue 236 | 237 | for keystrokes, command of keyBindings 238 | command = command?.toString?() ? '' 239 | 240 | if command.length is 0 241 | console.warn "Empty command for binding: `#{selector}` `#{keystrokes}` in #{source}" 242 | continue 243 | 244 | if normalizedKeystrokes = normalizeKeystrokes(keystrokes) 245 | bindings.push new KeyBinding(source, command, normalizedKeystrokes, selector, priority) 246 | else 247 | console.warn "Invalid keystroke sequence for binding: `#{keystrokes}: #{command}` in #{source}" 248 | bindings 249 | 250 | # Public: Add sets of key bindings grouped by CSS selector. 251 | # 252 | # * `source` A {String} (usually a path) uniquely identifying the given bindings 253 | # so they can be removed later. 254 | # * `bindings` An {Object} whose top-level keys point at sub-objects mapping 255 | # keystroke patterns to commands. 256 | # * `priority` A {Number} used to sort keybindings which have the same 257 | # specificity. Defaults to `0`. 258 | add: (source, keyBindingsBySelector, priority=0, throwOnInvalidSelector=true) -> 259 | addedKeyBindings = @build source, keyBindingsBySelector, priority, throwOnInvalidSelector 260 | @keyBindings.push addedKeyBindings... 261 | new Disposable => 262 | for keyBinding in addedKeyBindings 263 | index = @keyBindings.indexOf(keyBinding) 264 | @keyBindings.splice(index, 1) unless index is -1 265 | return 266 | 267 | removeBindingsFromSource: (source) -> 268 | @keyBindings = @keyBindings.filter (keyBinding) -> keyBinding.source isnt source 269 | undefined 270 | 271 | ### 272 | Section: Accessing Bindings 273 | ### 274 | 275 | # Public: Get all current key bindings. 276 | # 277 | # Returns an {Array} of {KeyBinding}s. 278 | getKeyBindings: -> 279 | @keyBindings.slice() 280 | 281 | # Public: Get the key bindings for a given command and optional target. 282 | # 283 | # * `params` An {Object} whose keys constrain the binding search: 284 | # * `keystrokes` A {String} representing one or more keystrokes, such as 285 | # 'ctrl-x ctrl-s' 286 | # * `command` A {String} representing the name of a command, such as 287 | # 'editor:backspace' 288 | # * `target` An optional DOM element constraining the search. If this 289 | # parameter is supplied, the call will only return bindings that 290 | # can be invoked by a KeyboardEvent originating from the target element. 291 | # 292 | # Returns an {Array} of key bindings. 293 | findKeyBindings: (params={}) -> 294 | {keystrokes, command, target, keyBindings} = params 295 | 296 | bindings = keyBindings ? @keyBindings 297 | 298 | if command? 299 | bindings = bindings.filter (binding) -> binding.command is command 300 | 301 | if keystrokes? 302 | bindings = bindings.filter (binding) -> binding.keystrokes is keystrokes 303 | 304 | if target? 305 | candidateBindings = bindings 306 | bindings = [] 307 | element = target 308 | while element? and element isnt document 309 | matchingBindings = candidateBindings 310 | .filter (binding) -> element.webkitMatchesSelector(binding.selector) 311 | .sort (a, b) -> a.compare(b) 312 | bindings.push(matchingBindings...) 313 | element = element.parentElement 314 | bindings 315 | 316 | 317 | ### 318 | Section: Managing Keymap Files 319 | ### 320 | 321 | # Public: Load the key bindings from the given path. 322 | # 323 | # * `path` A {String} containing a path to a file or a directory. If the path is 324 | # a directory, all files inside it will be loaded. 325 | # * `options` An {Object} containing the following optional keys: 326 | # * `watch` If `true`, the keymap will also reload the file at the given 327 | # path whenever it changes. This option cannot be used with directory paths. 328 | # * `priority` A {Number} used to sort keybindings which have the same 329 | # specificity. 330 | loadKeymap: (bindingsPath, options) -> 331 | checkIfDirectory = options?.checkIfDirectory ? true 332 | if checkIfDirectory and fs.isDirectorySync(bindingsPath) 333 | for filePath in fs.listSync(bindingsPath, ['.cson', '.json']) 334 | if @filePathMatchesPlatform(filePath) 335 | @loadKeymap(filePath, checkIfDirectory: false) 336 | else 337 | @add(bindingsPath, @readKeymap(bindingsPath, options?.suppressErrors), options?.priority) 338 | @watchKeymap(bindingsPath, options) if options?.watch 339 | 340 | undefined 341 | 342 | # Public: Cause the keymap to reload the key bindings file at the given path 343 | # whenever it changes. 344 | # 345 | # This method doesn't perform the initial load of the key bindings file. If 346 | # that's what you're looking for, call {::loadKeymap} with `watch: true`. 347 | # 348 | # * `path` A {String} containing a path to a file or a directory. If the path is 349 | # a directory, all files inside it will be loaded. 350 | # * `options` An {Object} containing the following optional keys: 351 | # * `priority` A {Number} used to sort keybindings which have the same 352 | # specificity. 353 | watchKeymap: (filePath, options) -> 354 | if not @watchSubscriptions[filePath]? or @watchSubscriptions[filePath].disposed 355 | file = new File(filePath) 356 | reloadKeymap = => @reloadKeymap(filePath, options) 357 | @watchSubscriptions[filePath] = new CompositeDisposable( 358 | file.onDidChange(reloadKeymap) 359 | file.onDidRename(reloadKeymap) 360 | file.onDidDelete(reloadKeymap) 361 | ) 362 | 363 | undefined 364 | 365 | # Called by the path watcher callback to reload a file at the given path. If 366 | # we can't read the file cleanly, we don't proceed with the reload. 367 | reloadKeymap: (filePath, options) -> 368 | if fs.isFileSync(filePath) 369 | bindings = @readKeymap(filePath, true) 370 | 371 | if typeof bindings isnt "undefined" 372 | @removeBindingsFromSource(filePath) 373 | @add(filePath, bindings, options?.priority) 374 | @emitter.emit 'did-reload-keymap', {path: filePath} 375 | else 376 | @removeBindingsFromSource(filePath) 377 | @emitter.emit 'did-unload-keymap', {path: filePath} 378 | 379 | readKeymap: (filePath, suppressErrors) -> 380 | if suppressErrors 381 | try 382 | CSON.readFileSync(filePath, allowDuplicateKeys: false) 383 | catch error 384 | console.warn("Failed to reload key bindings file: #{filePath}", error.stack ? error) 385 | @emitter.emit 'did-fail-to-read-file', error 386 | undefined 387 | else 388 | CSON.readFileSync(filePath, allowDuplicateKeys: false) 389 | 390 | # Determine if the given path should be loaded on this platform. If the 391 | # filename has the pattern '.cson' or 'foo..cson' and 392 | # does not match the current platform, returns false. Otherwise 393 | # returns true. 394 | filePathMatchesPlatform: (filePath) -> 395 | otherPlatforms = @getOtherPlatforms() 396 | for component in path.basename(filePath).split('.')[0...-1] 397 | return false if component in otherPlatforms 398 | true 399 | 400 | ### 401 | Section: Managing Keyboard Events 402 | ### 403 | 404 | # Public: Dispatch a custom event associated with the matching key binding for 405 | # the given `KeyboardEvent` if one can be found. 406 | # 407 | # If a matching binding is found on the event's target or one of its 408 | # ancestors, `.preventDefault()` is called on the keyboard event and the 409 | # binding's command is emitted as a custom event on the matching element. 410 | # 411 | # If the matching binding's command is 'native!', the method will terminate 412 | # without calling `.preventDefault()` on the keyboard event, allowing the 413 | # browser to handle it as normal. 414 | # 415 | # If the matching binding's command is 'unset!', the search will continue from 416 | # the current element's parent. 417 | # 418 | # If the matching binding's command is 'abort!', the search will terminate 419 | # without dispatching a command event. 420 | # 421 | # If the event's target is `document.body`, it will be treated as if its 422 | # target is `.defaultTarget` if that property is assigned on the keymap. 423 | # 424 | # * `event` A `KeyboardEvent` of type 'keydown' 425 | handleKeyboardEvent: (event, {replay, disabledBindings}={}) -> 426 | # Handling keyboard events is complicated and very nuanced. The complexity 427 | # is all due to supporting multi-stroke bindings. An example binding we'll 428 | # use throughout this very long comment: 429 | # 430 | # 'ctrl-a b c': 'my-sweet-command' // This is a binding 431 | # 432 | # This example means the user can type `ctrl-a` then `b` then `c`, and after 433 | # all of those keys are typed, it will dispatch the `my-sweet-command` 434 | # command. 435 | # 436 | # The KeymapManager has a couple member variables to deal with multi-stroke 437 | # bindings: `@queuedKeystrokes` and `@queuedKeyboardEvents`. They keep track 438 | # of the keystrokes the user has typed. When populated, the state variables 439 | # look something like: 440 | # 441 | # @queuedKeystrokes = ['ctrl-a', 'b', 'c'] 442 | # @queuedKeyboardEvents = [KeyboardEvent, KeyboardEvent, KeyboardEvent] 443 | # 444 | # Basically, this `handleKeyboardEvent` function will try to exactly match 445 | # the user's keystrokes to a binding. If it cant match exactly, it looks for 446 | # partial matches. So say, a user typed `ctrl-a` then `b`, but not `c` yet. 447 | # The `ctrl-a b c` binding would be partially matched: 448 | # 449 | # // The original binding: 'ctrl-a b c': 'my-sweet-command' 450 | # @queuedKeystrokes = ['ctrl-a', 'b'] // The user's keystrokes 451 | # @queuedKeyboardEvents = [KeyboardEvent, KeyboardEvent] 452 | # 453 | # When it finds partially matching bindings, it will put the KeymapManager 454 | # into a pending state via `enterPendingState` indicating that it is waiting 455 | # for either a timeout or more keystrokes to exactly match the partial 456 | # matches. In our example, it is waiting for the user to type `c` to 457 | # complete the partially matched `ctrl-a b c` binding. 458 | # 459 | # If a keystroke comes in that either matches a binding exactly, or yields 460 | # no partial matches, we will reset the state variables and exit pending 461 | # mode. If the keystroke yields no partial matches we will call 462 | # `terminatePendingState`. An extension of our last example: 463 | # 464 | # // Both of these will exit pending state for: 'ctrl-a b c': 'my-sweet-command' 465 | # @queuedKeystrokes = ['ctrl-a', 'b', 'c'] // User typed `c`. Exact match! Dispatch the command and clear state variables. Easy. 466 | # @queuedKeystrokes = ['ctrl-a', 'b', 'd'] // User typed `d`. No hope of matching, terminatePendingState(). Dragons. 467 | # 468 | # `terminatePendingState` is where things get crazy. Let's pretend the user 469 | # typed 3 total keystrokes: `ctrl-a`, `b`, then `d`. There are no exact 470 | # matches with these keystrokes given the original `'ctrl-a b c'` binding, 471 | # but other bindings might match a subset of the user's typed keystrokes. 472 | # Let's pretend we had more bindings defined: 473 | # 474 | # // The original binding; no match for ['ctrl-a', 'b', 'd']: 475 | # 'ctrl-a b c': 'my-sweet-command' 476 | # 477 | # // Bindings that all match a subset of ['ctrl-a', 'b', 'd']: 478 | # 'ctrl-a': 'ctrl-a-command' 479 | # 'b d': 'do-a-bd-deal' 480 | # 'd o g': 'wag-the-dog' 481 | # 482 | # With these example bindings, and the user's `['ctrl-a', 'b', 'd']` 483 | # keystrokes, we should dispatch commands `ctrl-a-command` and 484 | # `do-a-bd-deal`. 485 | # 486 | # After `['ctrl-a', 'b', 'd']` is typed by the user, `terminatePendingState` 487 | # is called, which will _disable_ the original unmatched `ctrl-a b c` 488 | # binding, empty the keystroke state variables, and _replay_ the key events 489 | # by running them through this `handleKeyboardEvent` function again. The 490 | # replay acts exactly as if a user were typing the keys, but with a disabled 491 | # binding. Because the original binding is disabled, the replayed keystrokes 492 | # will match other, shorter bindings, and in this case, dispatch commands 493 | # for our `ctrl-a` and then our `b d` bindings. 494 | # 495 | # Because the replay is calling this `handleKeyboardEvent` function again, 496 | # it can get into another pending state, and again call 497 | # `terminatePendingState`. The 2nd call to `terminatePendingState` might 498 | # disable other bindings, and do another replay, which might call this 499 | # function again ... and on and on. It will recurse until the KeymapManager 500 | # is no longer in a pending state with no partial matches from the most 501 | # recent event. 502 | # 503 | # Godspeed. 504 | 505 | # When a keyboard event is part of IME composition, the keyCode is always 506 | # 229, which is the "composition key code". This API is deprecated, but this 507 | # is the most simple and reliable way we found to ignore keystrokes that are 508 | # part of IME compositions. 509 | if event.keyCode is 229 and event.key isnt 'Dead' 510 | return 511 | 512 | # keystroke is the atom keybind syntax, e.g. 'ctrl-a' 513 | keystroke = @keystrokeForKeyboardEvent(event) 514 | 515 | # We dont care about bare modifier keys in the bindings. e.g. `ctrl y` isnt going to work. 516 | if event.type is 'keydown' and @queuedKeystrokes.length > 0 and isBareModifier(keystroke) 517 | event.preventDefault() 518 | return 519 | 520 | @queuedKeystrokes.push(keystroke) 521 | @queuedKeyboardEvents.push(event) 522 | keystrokes = @queuedKeystrokes.join(' ') 523 | 524 | # If the event's target is document.body, assign it to defaultTarget instead 525 | # to provide a catch-all element when nothing is focused. 526 | target = event.target 527 | target = @defaultTarget if event.target is document.body and @defaultTarget? 528 | 529 | # First screen for any bindings that match the current keystrokes, 530 | # regardless of their current selector. Matching strings is cheaper than 531 | # matching selectors. 532 | {partialMatchCandidates, pendingKeyupMatchCandidates, exactMatchCandidates} = @findMatchCandidates(@queuedKeystrokes, disabledBindings) 533 | dispatchedExactMatch = null 534 | partialMatches = @findPartialMatches(partialMatchCandidates, target) 535 | 536 | # If any partial match *was* pending but has now failed to match, add it to 537 | # the list of bindings to disable so we don't attempt to match it again 538 | # during a subsequent event replay by `terminatePendingState`. 539 | if @pendingPartialMatches? 540 | liveMatches = new Set(partialMatches.concat(exactMatchCandidates)) 541 | for binding in @pendingPartialMatches 542 | @bindingsToDisable.push(binding) unless liveMatches.has(binding) 543 | 544 | hasPartialMatches = partialMatches.length > 0 545 | shouldUsePartialMatches = hasPartialMatches 546 | 547 | if isKeyup(keystroke) 548 | exactMatchCandidates = exactMatchCandidates.concat(@pendingKeyupMatcher.getMatches(keystroke)) 549 | 550 | # Determine if the current keystrokes match any bindings *exactly*. If we 551 | # do find an exact match, the next step depends on whether we have any 552 | # partial matches. If we have no partial matches, we dispatch the command 553 | # immediately. Otherwise we break and allow ourselves to enter the pending 554 | # state with a timeout. 555 | if exactMatchCandidates.length > 0 556 | currentTarget = target 557 | eventHandled = false 558 | while not eventHandled and currentTarget? and currentTarget isnt document 559 | exactMatches = @findExactMatches(exactMatchCandidates, currentTarget) 560 | for exactMatchCandidate in exactMatches 561 | if exactMatchCandidate.command is 'native!' 562 | shouldUsePartialMatches = false 563 | # `break` breaks out of this loop, `eventHandled = true` breaks out of the parent loop 564 | eventHandled = true 565 | break 566 | 567 | if exactMatchCandidate.command is 'abort!' 568 | event.preventDefault() 569 | eventHandled = true 570 | break 571 | 572 | if exactMatchCandidate.command is 'unset!' 573 | break 574 | 575 | if hasPartialMatches 576 | # When there is a set of bindings like `'ctrl-y', 'ctrl-y ^ctrl'`, 577 | # and a `ctrl-y` comes in, this will allow the `ctrl-y` command to be 578 | # dispatched without waiting for any other keystrokes. 579 | allPartialMatchesContainKeyupRemainder = true 580 | for partialMatch in partialMatches 581 | if pendingKeyupMatchCandidates.indexOf(partialMatch) < 0 582 | allPartialMatchesContainKeyupRemainder = false 583 | # We found one partial match with unmatched keydowns. 584 | # We can stop looking. 585 | break 586 | # Don't dispatch this exact match. There are partial matches left 587 | # that have keydowns. 588 | break if allPartialMatchesContainKeyupRemainder is false 589 | else 590 | shouldUsePartialMatches = false 591 | 592 | if @dispatchCommandEvent(exactMatchCandidate.command, target, event) 593 | dispatchedExactMatch = exactMatchCandidate 594 | eventHandled = true 595 | for pendingKeyupMatch in pendingKeyupMatchCandidates 596 | @pendingKeyupMatcher.addPendingMatch(pendingKeyupMatch) 597 | break 598 | currentTarget = currentTarget.parentElement 599 | 600 | # Emit events. These are done on their own for clarity. 601 | 602 | if dispatchedExactMatch? 603 | @emitter.emit 'did-match-binding', { 604 | keystrokes, 605 | eventType: event.type, 606 | binding: dispatchedExactMatch, 607 | keyboardEventTarget: target 608 | } 609 | else if hasPartialMatches and shouldUsePartialMatches 610 | event.preventDefault() 611 | @emitter.emit 'did-partially-match-binding', { 612 | keystrokes, 613 | eventType: event.type, 614 | partiallyMatchedBindings: partialMatches, 615 | keyboardEventTarget: target 616 | } 617 | else if not dispatchedExactMatch? and not hasPartialMatches 618 | @emitter.emit 'did-fail-to-match-binding', { 619 | keystrokes, 620 | eventType: event.type, 621 | keyboardEventTarget: target 622 | } 623 | # Some of the queued keyboard events might have inserted characters had 624 | # we not prevented their default action. If we're replaying a keystroke 625 | # whose default action was prevented and no binding is matched, we'll 626 | # simulate the text input event that was previously prevented to insert 627 | # the missing characters. 628 | @simulateTextInput(event) if event.defaultPrevented and event.type is 'keydown' 629 | 630 | # Manage the keystroke queue state. State is updated separately for clarity. 631 | 632 | @bindingsToDisable.push(dispatchedExactMatch) if dispatchedExactMatch 633 | if hasPartialMatches and shouldUsePartialMatches 634 | enableTimeout = ( 635 | @pendingStateTimeoutHandle? or 636 | exactMatchCandidate? or 637 | characterForKeyboardEvent(@queuedKeyboardEvents[0])? 638 | ) 639 | enableTimeout = false if replay 640 | @enterPendingState(partialMatches, enableTimeout) 641 | else if not dispatchedExactMatch? and not hasPartialMatches and @pendingPartialMatches? 642 | # There are partial matches from a previous event, but none from this 643 | # event. This means the current event has removed any hope that the queued 644 | # key events will ever match any binding. So we will clear the state and 645 | # start over after replaying the events in `terminatePendingState`. 646 | @terminatePendingState() 647 | else 648 | @clearQueuedKeystrokes() 649 | 650 | # Public: Translate a keydown event to a keystroke string. 651 | # 652 | # * `event` A `KeyboardEvent` of type 'keydown' 653 | # 654 | # Returns a {String} describing the keystroke. 655 | keystrokeForKeyboardEvent: (event) -> 656 | keystrokeForKeyboardEvent(event, @customKeystrokeResolvers) 657 | 658 | # Public: Customize translation of raw keyboard events to keystroke strings. 659 | # This API is useful for working around Chrome bugs or changing how Atom 660 | # resolves certain key combinations. If multiple resolvers are installed, 661 | # the most recently-added resolver returning a string for a given keystroke 662 | # takes precedence. 663 | # 664 | # * `resolver` A {Function} that returns a keystroke {String} and is called 665 | # with an object containing the following keys: 666 | # * `keystroke` The currently resolved keystroke string. If your function 667 | # returns a falsy value, this is how Atom will resolve your keystroke. 668 | # * `event` The raw DOM 3 `KeyboardEvent` being resolved. See the DOM API 669 | # documentation for more details. 670 | # * `layoutName` The OS-specific name of the current keyboard layout. 671 | # * `keymap` An object mapping DOM 3 `KeyboardEvent.code` values to objects 672 | # with the typed character for that key in each modifier state, based on 673 | # the current operating system layout. 674 | # 675 | # Returns a {Disposable} that removes the added resolver. 676 | addKeystrokeResolver: (resolver) -> 677 | @customKeystrokeResolvers.push(resolver) 678 | new Disposable => 679 | index = @customKeystrokeResolvers.indexOf(resolver) 680 | @customKeystrokeResolvers.splice(index, 1) if index >= 0 681 | 682 | # Public: Get the number of milliseconds allowed before pending states caused 683 | # by partial matches of multi-keystroke bindings are terminated. 684 | # 685 | # Returns a {Number} 686 | getPartialMatchTimeout: -> 687 | @partialMatchTimeout 688 | 689 | ### 690 | Section: Private 691 | ### 692 | 693 | simulateTextInput: (keydownEvent) -> 694 | if character = characterForKeyboardEvent(keydownEvent) 695 | textInputEvent = document.createEvent("TextEvent") 696 | textInputEvent.initTextEvent("textInput", true, true, window, character) 697 | keydownEvent.path[0].dispatchEvent(textInputEvent) 698 | 699 | # For testing purposes 700 | getOtherPlatforms: -> OtherPlatforms 701 | 702 | # Finds all key bindings whose keystrokes match the given keystrokes. Returns 703 | # both partial and exact matches. 704 | findMatchCandidates: (keystrokeArray, disabledBindings) -> 705 | partialMatchCandidates = [] 706 | exactMatchCandidates = [] 707 | pendingKeyupMatchCandidates = [] 708 | disabledBindingSet = new Set(disabledBindings) 709 | 710 | for binding in @keyBindings when not disabledBindingSet.has(binding) 711 | doesMatch = binding.matchesKeystrokes(keystrokeArray) 712 | if doesMatch is MATCH_TYPES.EXACT 713 | exactMatchCandidates.push(binding) 714 | else if doesMatch is MATCH_TYPES.PARTIAL 715 | partialMatchCandidates.push(binding) 716 | else if doesMatch is MATCH_TYPES.PENDING_KEYUP 717 | partialMatchCandidates.push(binding) 718 | pendingKeyupMatchCandidates.push(binding) 719 | {partialMatchCandidates, pendingKeyupMatchCandidates, exactMatchCandidates} 720 | 721 | # Determine which of the given bindings have selectors matching the target or 722 | # one of its ancestors. This is used by {::handleKeyboardEvent} to determine 723 | # if there are any partial matches for the keyboard event. 724 | findPartialMatches: (partialMatchCandidates, target) -> 725 | partialMatches = [] 726 | ignoreKeystrokes = new Set 727 | 728 | partialMatchCandidates.forEach (binding) -> 729 | if binding.command is 'unset!' 730 | ignoreKeystrokes.add(binding.keystrokes) 731 | 732 | while partialMatchCandidates.length > 0 and target? and target isnt document 733 | partialMatchCandidates = partialMatchCandidates.filter (binding) -> 734 | if not ignoreKeystrokes.has(binding.keystrokes) and target.webkitMatchesSelector(binding.selector) 735 | partialMatches.push(binding) 736 | false 737 | else 738 | true 739 | target = target.parentElement 740 | partialMatches.sort (a, b) -> b.keystrokeCount - a.keystrokeCount 741 | 742 | # Find the matching bindings among the given candidates for the given target, 743 | # ordered by specificity. Does not traverse up the target's ancestors. This is 744 | # used by {::handleKeyboardEvent} to find a matching binding when there are no 745 | # partially-matching bindings. 746 | findExactMatches: (exactMatchCandidates, target) -> 747 | exactMatchCandidates 748 | .filter (binding) -> target.webkitMatchesSelector(binding.selector) 749 | .sort (a, b) -> a.compare(b) 750 | 751 | clearQueuedKeystrokes: -> 752 | @queuedKeyboardEvents = [] 753 | @queuedKeystrokes = [] 754 | @bindingsToDisable = [] 755 | 756 | enterPendingState: (pendingPartialMatches, enableTimeout) -> 757 | @cancelPendingState() if @pendingStateTimeoutHandle? 758 | @pendingPartialMatches = pendingPartialMatches 759 | if enableTimeout 760 | @pendingStateTimeoutHandle = setTimeout(@terminatePendingState.bind(this, true), @partialMatchTimeout) 761 | 762 | cancelPendingState: -> 763 | clearTimeout(@pendingStateTimeoutHandle) 764 | @pendingStateTimeoutHandle = null 765 | @pendingPartialMatches = null 766 | 767 | # This is called by {::handleKeyboardEvent} when no matching bindings are 768 | # found for the currently queued keystrokes or by the pending state timeout. 769 | # It disables the longest of the pending partially matching bindings, then 770 | # replays the queued keyboard events to allow any bindings with shorter 771 | # keystroke sequences to be matched unambiguously. 772 | # 773 | # Note that replaying events has a recursive behavior. Replaying will set 774 | # member state (e.g. @queuedKeyboardEvents) just like real events, and will 775 | # likely result in another call to this function. The replay process will 776 | # potentially replay the events (or a subset of events) several times, while 777 | # disabling bindings here and there. See any spec that handles multiple 778 | # keystrokes failures to match a binding. 779 | terminatePendingState: (fromTimeout) -> 780 | bindingsToDisable = @pendingPartialMatches.concat(@bindingsToDisable) 781 | eventsToReplay = @queuedKeyboardEvents 782 | 783 | @cancelPendingState() 784 | @clearQueuedKeystrokes() 785 | 786 | keyEventOptions = 787 | replay: true 788 | disabledBindings: bindingsToDisable 789 | 790 | for event in eventsToReplay 791 | keyEventOptions.disabledBindings = bindingsToDisable 792 | @handleKeyboardEvent(event, keyEventOptions) 793 | 794 | # We can safely re-enable the bindings when we no longer have any partial matches 795 | bindingsToDisable = null if bindingsToDisable? and not @pendingPartialMatches? 796 | 797 | if fromTimeout and @pendingPartialMatches? 798 | @terminatePendingState(true) 799 | 800 | return 801 | 802 | # After we match a binding, we call this method to dispatch a custom event 803 | # based on the binding's command. 804 | dispatchCommandEvent: (command, target, keyboardEvent) -> 805 | # Here we use prototype chain injection to add CommandEvent methods to this 806 | # custom event to support aborting key bindings and simulated bubbling for 807 | # detached targets. 808 | commandEvent = new CustomEvent(command, bubbles: true, cancelable: true) 809 | commandEvent.__proto__ = CommandEvent:: 810 | commandEvent.originalEvent = keyboardEvent 811 | 812 | if document.contains(target) 813 | target.dispatchEvent(commandEvent) 814 | else 815 | @simulateBubblingOnDetachedTarget(target, commandEvent) 816 | 817 | {keyBindingAborted} = commandEvent 818 | keyboardEvent.preventDefault() unless keyBindingAborted 819 | not keyBindingAborted 820 | 821 | # Chromium does not bubble events dispatched on detached targets, which makes 822 | # testing a pain in the ass. This method simulates bubbling manually. 823 | simulateBubblingOnDetachedTarget: (target, commandEvent) -> 824 | Object.defineProperty(commandEvent, 'target', get: -> target) 825 | Object.defineProperty(commandEvent, 'currentTarget', get: -> currentTarget) 826 | currentTarget = target 827 | while currentTarget? 828 | currentTarget.dispatchEvent(commandEvent) 829 | break if commandEvent.propagationStopped 830 | break if currentTarget is window 831 | currentTarget = currentTarget.parentNode ? window 832 | return 833 | --------------------------------------------------------------------------------