├── .editorconfig ├── .gitignore ├── demo ├── index.html └── index.js ├── license ├── package.json ├── readme.md ├── src ├── constants.ts ├── index.ts ├── maps.ts ├── types.ts └── utils.ts ├── test └── index.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.err 3 | *.log 4 | ._* 5 | .cache 6 | .fseventsd 7 | .DocumentRevisions* 8 | .DS_Store 9 | .TemporaryItems 10 | .Trashes 11 | Thumbs.db 12 | 13 | dist 14 | node_modules 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ShoSho 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import ShoSho from '../src'; 5 | 6 | /* HELPERS */ 7 | 8 | const yep = message => { 9 | return () => { 10 | console.log ( `✅ ${message}` ); 11 | return true; 12 | }; 13 | }; 14 | 15 | const nope = message => { 16 | return () => { 17 | console.log ( `❌ ${message}` ); 18 | return false; 19 | }; 20 | }; 21 | 22 | /* MAIN - LISTENING */ 23 | 24 | const shortcuts = new ShoSho (); 25 | 26 | shortcuts.register ( '0', yep ( '0' ) ); 27 | shortcuts.register ( '1', yep ( '1' ) ); 28 | shortcuts.register ( '2', yep ( '2' ) ); 29 | shortcuts.register ( '3', yep ( '3' ) ); 30 | shortcuts.register ( '4', yep ( '4' ) ); 31 | shortcuts.register ( '5', yep ( '5' ) ); 32 | shortcuts.register ( '6', yep ( '6' ) ); 33 | shortcuts.register ( '7', yep ( '7' ) ); 34 | shortcuts.register ( '8', yep ( '8' ) ); 35 | shortcuts.register ( '9', yep ( '9' ) ); 36 | 37 | shortcuts.register ( 'A', yep ( 'A' ) ); 38 | shortcuts.register ( 'B', yep ( 'B' ) ); 39 | shortcuts.register ( 'C', yep ( 'C' ) ); 40 | shortcuts.register ( 'D', yep ( 'D' ) ); 41 | shortcuts.register ( 'E', yep ( 'E' ) ); 42 | shortcuts.register ( 'F', yep ( 'F' ) ); 43 | shortcuts.register ( 'G', yep ( 'G' ) ); 44 | shortcuts.register ( 'H', yep ( 'H' ) ); 45 | shortcuts.register ( 'I', yep ( 'I' ) ); 46 | shortcuts.register ( 'J', yep ( 'J' ) ); 47 | shortcuts.register ( 'K', yep ( 'K' ) ); 48 | shortcuts.register ( 'L', yep ( 'L' ) ); 49 | shortcuts.register ( 'M', yep ( 'M' ) ); 50 | shortcuts.register ( 'N', yep ( 'N' ) ); 51 | shortcuts.register ( 'O', yep ( 'O' ) ); 52 | shortcuts.register ( 'P', yep ( 'P' ) ); 53 | shortcuts.register ( 'Q', yep ( 'Q' ) ); 54 | shortcuts.register ( 'R', yep ( 'R' ) ); 55 | shortcuts.register ( 'S', yep ( 'S' ) ); 56 | shortcuts.register ( 'T', yep ( 'T' ) ); 57 | shortcuts.register ( 'U', yep ( 'U' ) ); 58 | shortcuts.register ( 'V', yep ( 'V' ) ); 59 | shortcuts.register ( 'W', yep ( 'W' ) ); 60 | shortcuts.register ( 'X', yep ( 'X' ) ); 61 | shortcuts.register ( 'Y', yep ( 'Y' ) ); 62 | shortcuts.register ( 'Z', yep ( 'Z' ) ); 63 | 64 | shortcuts.register ( 'F1', yep ( 'F1' ) ); 65 | shortcuts.register ( 'F2', yep ( 'F2' ) ); 66 | shortcuts.register ( 'F3', yep ( 'F3' ) ); 67 | shortcuts.register ( 'F4', yep ( 'F4' ) ); 68 | shortcuts.register ( 'F5', yep ( 'F5' ) ); 69 | shortcuts.register ( 'F6', yep ( 'F6' ) ); 70 | shortcuts.register ( 'F7', yep ( 'F7' ) ); 71 | shortcuts.register ( 'F8', yep ( 'F8' ) ); 72 | shortcuts.register ( 'F9', yep ( 'F9' ) ); 73 | shortcuts.register ( 'F10', yep ( 'F10' ) ); 74 | shortcuts.register ( 'F11', yep ( 'F11' ) ); 75 | shortcuts.register ( 'F12', yep ( 'F12' ) ); 76 | shortcuts.register ( 'F13', yep ( 'F13' ) ); 77 | shortcuts.register ( 'F14', yep ( 'F14' ) ); 78 | shortcuts.register ( 'F15', yep ( 'F15' ) ); 79 | shortcuts.register ( 'F16', yep ( 'F16' ) ); 80 | shortcuts.register ( 'F17', yep ( 'F17' ) ); 81 | shortcuts.register ( 'F18', yep ( 'F18' ) ); 82 | shortcuts.register ( 'F19', yep ( 'F19' ) ); 83 | shortcuts.register ( 'F20', yep ( 'F20' ) ); 84 | shortcuts.register ( 'F21', yep ( 'F21' ) ); 85 | shortcuts.register ( 'F22', yep ( 'F22' ) ); 86 | shortcuts.register ( 'F23', yep ( 'F23' ) ); 87 | shortcuts.register ( 'F24', yep ( 'F24' ) ); 88 | 89 | shortcuts.register ( 'Numpad0', yep ( 'Numpad0' ) ); 90 | shortcuts.register ( 'Numpad1', yep ( 'Numpad1' ) ); 91 | shortcuts.register ( 'Numpad2', yep ( 'Numpad2' ) ); 92 | shortcuts.register ( 'Numpad3', yep ( 'Numpad3' ) ); 93 | shortcuts.register ( 'Numpad4', yep ( 'Numpad4' ) ); 94 | shortcuts.register ( 'Numpad5', yep ( 'Numpad5' ) ); 95 | shortcuts.register ( 'Numpad6', yep ( 'Numpad6' ) ); 96 | shortcuts.register ( 'Numpad7', yep ( 'Numpad7' ) ); 97 | shortcuts.register ( 'Numpad8', yep ( 'Numpad8' ) ); 98 | shortcuts.register ( 'Numpad9', yep ( 'Numpad9' ) ); 99 | 100 | shortcuts.register ( 'NumpadAdd', yep ( 'NumpadAdd' ) ); 101 | shortcuts.register ( 'NumpadComma', yep ( 'NumpadComma' ) ); 102 | shortcuts.register ( 'NumpadDecimal', yep ( 'NumpadDecimal' ) ); 103 | shortcuts.register ( 'NumpadDivide', yep ( 'NumpadDivide' ) ); 104 | shortcuts.register ( 'NumpadEnter', yep ( 'NumpadEnter' ) ); 105 | shortcuts.register ( 'NumpadEqual', yep ( 'NumpadEqual' ) ); 106 | shortcuts.register ( 'NumpadMultiply', yep ( 'NumpadMultiply' ) ); 107 | shortcuts.register ( 'NumpadSubtract', yep ( 'NumpadSubtract' ) ); 108 | 109 | shortcuts.register ( 'Left+Up', nope ( 'Left+Up' ) ); 110 | shortcuts.register ( 'Right+Up', nope ( 'Right+Up' ) ); 111 | shortcuts.register ( 'Left+Down', nope ( 'Left+Down' ) ); 112 | shortcuts.register ( 'Right+Down', nope ( 'Right+Down' ) ); 113 | shortcuts.register ( 'Left+Right', nope ( 'Left+Right' ) ); 114 | shortcuts.register ( 'Up+Down', nope ( 'Up+Down' ) ); 115 | 116 | shortcuts.register ( 'Down', yep ( 'Down' ) ); 117 | shortcuts.register ( 'Left', yep ( 'Left' ) ); 118 | shortcuts.register ( 'Right', yep ( 'Right' ) ); 119 | shortcuts.register ( 'Up', yep ( 'Up' ) ); 120 | 121 | shortcuts.register ( 'Backspace', yep ( 'Backspace' ) ); 122 | shortcuts.register ( 'CapsLock', yep ( 'CapsLock' ) ); 123 | shortcuts.register ( 'Delete', yep ( 'Delete' ) ); 124 | shortcuts.register ( 'End', yep ( 'End' ) ); 125 | shortcuts.register ( 'Enter', yep ( 'Enter' ) ); 126 | shortcuts.register ( 'Escape', yep ( 'Escape' ) ); 127 | shortcuts.register ( 'Home', yep ( 'Home' ) ); 128 | shortcuts.register ( 'Insert', yep ( 'Insert' ) ); 129 | shortcuts.register ( 'PageDown', yep ( 'PageDown' ) ); 130 | shortcuts.register ( 'PageUp', yep ( 'PageUp' ) ); 131 | shortcuts.register ( 'Space', yep ( 'Space' ) ); 132 | shortcuts.register ( 'Tab', yep ( 'Tab' ) ); 133 | 134 | shortcuts.register ( 'NumLock', yep ( 'NumLock' ) ); 135 | shortcuts.register ( 'ScrollLock', yep ( 'ScrollLock' ) ); 136 | 137 | // shortcuts.register ( 'AltLeft', nope ( 'AltLeft' ) ); 138 | // shortcuts.register ( 'AltRight', nope ( 'AltRight' ) ); 139 | // shortcuts.register ( 'Alt', nope ( 'Alt' ) ); 140 | // shortcuts.register ( 'OptionLeft', nope ( 'OptionLeft' ) ); 141 | // shortcuts.register ( 'OptionRight', nope ( 'OptionRight' ) ); 142 | // shortcuts.register ( 'Option', nope ( 'Option' ) ); 143 | // shortcuts.register ( 'CmdLeft', nope ( 'CmdLeft' ) ); 144 | // shortcuts.register ( 'CmdRight', nope ( 'CmdRight' ) ); 145 | // shortcuts.register ( 'Cmd', nope ( 'Cmd' ) ); 146 | // shortcuts.register ( 'CommandLeft', nope ( 'CommandLeft' ) ); 147 | // shortcuts.register ( 'CommandRight', nope ( 'CommandRight' ) ); 148 | // shortcuts.register ( 'Command', nope ( 'Command' ) ); 149 | // shortcuts.register ( 'MetaLeft', nope ( 'MetaLeft' ) ); 150 | // shortcuts.register ( 'MetaRight', nope ( 'MetaRight' ) ); 151 | // shortcuts.register ( 'Meta', nope ( 'Meta' ) ); 152 | // shortcuts.register ( 'CtrlLeft', nope ( 'CtrlLeft' ) ); 153 | // shortcuts.register ( 'CtrlRight', nope ( 'CtrlRight' ) ); 154 | // shortcuts.register ( 'Ctrl', nope ( 'Ctrl' ) ); 155 | // shortcuts.register ( 'ControlLeft', nope ( 'ControlLeft' ) ); 156 | // shortcuts.register ( 'ControlRight', nope ( 'ControlRight' ) ); 157 | // shortcuts.register ( 'Control', nope ( 'Control' ) ); 158 | // shortcuts.register ( 'ShiftLeft', nope ( 'ShiftLeft' ) ); 159 | // shortcuts.register ( 'ShiftRight', nope ( 'ShiftRight' ) ); 160 | // shortcuts.register ( 'Shift', nope ( 'Shift' ) ); 161 | // shortcuts.register ( 'CmdLeftOrCtrlLeft', nope ( 'CmdLeftOrCtrlLeft' ) ); 162 | // shortcuts.register ( 'CmdRightOrCtrlRight', nope ( 'CmdRightOrCtrlRight' ) ); 163 | // shortcuts.register ( 'CmdOrCtrl', nope ( 'CmdOrCtrl' ) ); 164 | // shortcuts.register ( 'CommandLeftOrControlLeft', nope ( 'CommandLeftOrControlLeft' ) ); 165 | // shortcuts.register ( 'CommandRightOrControlRight', nope ( 'CommandRightOrControlRight' ) ); 166 | // shortcuts.register ( 'CommandOrControl', nope ( 'CommandOrControl' ) ); 167 | 168 | shortcuts.register ( '!', nope ( 'ExclationMark' ) ); 169 | shortcuts.register ( '"', nope ( 'DoubleQuote' ) ); 170 | shortcuts.register ( '#', nope ( 'Hash' ) ); 171 | shortcuts.register ( '$', nope ( 'Dollar' ) ); 172 | shortcuts.register ( '%', nope ( 'Percent' ) ); 173 | shortcuts.register ( '&', nope ( 'Ampersand' ) ); 174 | shortcuts.register ( '\'', nope ( 'Quote' ) ); 175 | shortcuts.register ( '(', nope ( 'ParenthesisLeft' ) ); 176 | shortcuts.register ( ')', nope ( 'ParenthesisRight' ) ); 177 | shortcuts.register ( '*', nope ( 'Asterisk' ) ); 178 | shortcuts.register ( '+', nope ( 'Plus' ) ); 179 | shortcuts.register ( ',', nope ( 'Comma' ) ); 180 | shortcuts.register ( '-', nope ( 'Minus' ) ); 181 | shortcuts.register ( '.', nope ( 'Period' ) ); 182 | shortcuts.register ( '/', nope ( 'Slash' ) ); 183 | shortcuts.register ( ':', nope ( 'Colon' ) ); 184 | shortcuts.register ( ';', nope ( 'Semicolon' ) ); 185 | shortcuts.register ( '<', nope ( 'LessThan' ) ); 186 | shortcuts.register ( '=', nope ( 'Equal' ) ); 187 | shortcuts.register ( '>', nope ( 'GreaterThan' ) ); 188 | shortcuts.register ( '?', nope ( 'QuestionMark' ) ); 189 | shortcuts.register ( '@', nope ( 'At' ) ); 190 | shortcuts.register ( '[', nope ( 'BracketLeft' ) ); 191 | shortcuts.register ( '\\', nope ( 'Backslash' ) ); 192 | shortcuts.register ( ']', nope ( 'BracketRight' ) ); 193 | shortcuts.register ( '^', nope ( 'Caret' ) ); 194 | shortcuts.register ( '_', nope ( 'Underscore' ) ); 195 | shortcuts.register ( '`', nope ( 'Backquote' ) ); 196 | shortcuts.register ( '{', nope ( 'BraceLeft' ) ); 197 | shortcuts.register ( '|', nope ( 'Pipe' ) ); 198 | shortcuts.register ( '}', nope ( 'BraceRight' ) ); 199 | shortcuts.register ( '~', nope ( 'Tilde' ) ); 200 | 201 | shortcuts.register ( 'Shift+Plus', nope ( 'Shift+Plus (Alias)' ) ); 202 | shortcuts.register ( 'Shift+!', nope ( 'Shift+ExclationMark' ) ); 203 | shortcuts.register ( 'Shift+"', nope ( 'Shift+DoubleQuote' ) ); 204 | shortcuts.register ( 'Shift+#', nope ( 'Shift+Hash' ) ); 205 | shortcuts.register ( 'Shift+$', nope ( 'Shift+Dollar' ) ); 206 | shortcuts.register ( 'Shift+%', nope ( 'Shift+Percent' ) ); 207 | shortcuts.register ( 'Shift+&', nope ( 'Shift+Ampersand' ) ); 208 | shortcuts.register ( 'Shift+\'', nope ( 'Shift+Quote' ) ); 209 | shortcuts.register ( 'Shift+(', nope ( 'Shift+ParenthesisLeft' ) ); 210 | shortcuts.register ( 'Shift+)', nope ( 'Shift+ParenthesisRight' ) ); 211 | shortcuts.register ( 'Shift+*', nope ( 'Shift+Asterisk' ) ); 212 | shortcuts.register ( 'Shift++', nope ( 'Shift+Plus' ) ); 213 | shortcuts.register ( 'Shift+,', nope ( 'Shift+Comma' ) ); 214 | shortcuts.register ( 'Shift+-', nope ( 'Shift+Minus' ) ); 215 | shortcuts.register ( 'Shift+.', nope ( 'Shift+Period' ) ); 216 | shortcuts.register ( 'Shift+/', nope ( 'Shift+Slash' ) ); 217 | shortcuts.register ( 'Shift+:', nope ( 'Shift+Colon' ) ); 218 | shortcuts.register ( 'Shift+;', nope ( 'Shift+Semicolon' ) ); 219 | shortcuts.register ( 'Shift+<', nope ( 'Shift+LessThan' ) ); 220 | shortcuts.register ( 'Shift+=', nope ( 'Shift+Equal' ) ); 221 | shortcuts.register ( 'Shift+>', nope ( 'Shift+GreaterThan' ) ); 222 | shortcuts.register ( 'Shift+?', nope ( 'Shift+QuestionMark' ) ); 223 | shortcuts.register ( 'Shift+@', nope ( 'Shift+At' ) ); 224 | shortcuts.register ( 'Shift+[', nope ( 'Shift+BracketLeft' ) ); 225 | shortcuts.register ( 'Shift+\\', nope ( 'Shift+Backslash' ) ); 226 | shortcuts.register ( 'Shift+]', nope ( 'Shift+BracketRight' ) ); 227 | shortcuts.register ( 'Shift+^', nope ( 'Shift+Caret' ) ); 228 | shortcuts.register ( 'Shift+_', nope ( 'Shift+Underscore' ) ); 229 | shortcuts.register ( 'Shift+`', nope ( 'Shift+Backquote' ) ); 230 | shortcuts.register ( 'Shift+{', nope ( 'Shift+BraceLeft' ) ); 231 | shortcuts.register ( 'Shift+|', nope ( 'Shift+Pipe' ) ); 232 | shortcuts.register ( 'Shift+}', nope ( 'Shift+BraceRight' ) ); 233 | shortcuts.register ( 'Shift+~', nope ( 'Shift+Tilde' ) ); 234 | 235 | shortcuts.register ( 'Shift+1', nope ( 'Shift+1' ) ); 236 | shortcuts.register ( 'Shift+2', nope ( 'Shift+2' ) ); 237 | shortcuts.register ( 'Shift+3', nope ( 'Shift+3' ) ); 238 | shortcuts.register ( 'Shift+4', nope ( 'Shift+4' ) ); 239 | shortcuts.register ( 'Shift+5', nope ( 'Shift+5' ) ); 240 | shortcuts.register ( 'Shift+6', nope ( 'Shift+6' ) ); 241 | shortcuts.register ( 'Shift+7', nope ( 'Shift+7' ) ); 242 | shortcuts.register ( 'Shift+8', nope ( 'Shift+8' ) ); 243 | shortcuts.register ( 'Shift+9', nope ( 'Shift+9' ) ); 244 | shortcuts.register ( 'Shift+0', nope ( 'Shift+0' ) ); 245 | 246 | shortcuts.register ( 'Shift+A', nope ( 'Shift+A' ) ); 247 | shortcuts.register ( 'Shift+B', nope ( 'Shift+B' ) ); 248 | shortcuts.register ( 'Shift+C', nope ( 'Shift+C' ) ); 249 | shortcuts.register ( 'Shift+Z', nope ( 'Shift+Z' ) ); 250 | shortcuts.register ( 'Shift+Y', nope ( 'Shift+Y' ) ); 251 | shortcuts.register ( 'Shift+X', nope ( 'Shift+X' ) ); 252 | 253 | shortcuts.register ( 'ClickLeft', nope ( 'ClickLeft' ) ); 254 | shortcuts.register ( 'ClickRight', nope ( 'ClickRight' ) ); 255 | shortcuts.register ( 'ClickMiddle', nope ( 'ClickMiddle' ) ); 256 | shortcuts.register ( 'MouseLeft', nope ( 'MouseLeft' ) ); 257 | shortcuts.register ( 'MouseRight', nope ( 'MouseRight' ) ); 258 | shortcuts.register ( 'MouseMiddle', nope ( 'MouseMiddle' ) ); 259 | shortcuts.register ( 'Mouse0', nope ( 'Mouse0' ) ); 260 | shortcuts.register ( 'Mouse1', nope ( 'Mouse1' ) ); 261 | shortcuts.register ( 'Mouse2', nope ( 'Mouse2' ) ); 262 | shortcuts.register ( 'Mouse3', nope ( 'Mouse3' ) ); 263 | shortcuts.register ( 'Mouse4', nope ( 'Mouse4' ) ); 264 | shortcuts.register ( 'Mouse5', nope ( 'Mouse5' ) ); 265 | shortcuts.register ( 'Mouse6', nope ( 'Mouse6' ) ); 266 | shortcuts.register ( 'Mouse7', nope ( 'Mouse7' ) ); 267 | shortcuts.register ( 'Mouse8', nope ( 'Mouse8' ) ); 268 | shortcuts.register ( 'Mouse9', nope ( 'Mouse9' ) ); 269 | 270 | shortcuts.register ( 'Alt+F', yep ( 'Alt+F' ) ); 271 | shortcuts.register ( 'Cmd+K Cmd+A', yep ( 'Cmd+K Cmd+A' ) ); 272 | shortcuts.register ( 'Cmd+K B', yep ( 'Cmd+K B' ) ); 273 | shortcuts.register ( 'Cmd+K Alt+C', yep ( 'Cmd+K Alt+C' ) ); 274 | shortcuts.register ( 'Shift+ClickLeft', yep ( 'Shift+ClickLeft' ) ); 275 | shortcuts.register ( 'Cmd+ClickMiddle', yep ( 'Cmd+ClickMiddle' ) ); 276 | shortcuts.register ( 'Alt+ClickLeft Cmd+ClickMiddle', yep ( 'Alt+ClickLeft Cmd+ClickMiddle' ) ); 277 | 278 | shortcuts.register ( 'CmdLeft+CmdRight', yep ( 'CmdLeft+CmdRight' ) ); 279 | // shortcuts.register ( 'CmdLeft+CmdRight AltLeft+AltRight', yep ( 'CmdLeft+CmdRight AltLeft+AltRight' ) ); //TODO It'd be cool to support this too 280 | 281 | shortcuts.register ( 'Up Up Down Down Left Right Left Right B A', yep ( 'Up Up Down Down Left Right Left Right B A 🚀' ), { konami: true } ); 282 | 283 | // shortcuts.register ( 'Left+Up', nope ( 'Left+Up' ) ); 284 | // shortcuts.register ( 'Left+Down', nope ( 'Left+Down' ) ); 285 | // shortcuts.register ( 'Left+Right', nope ( 'Left+Right' ) ); 286 | // shortcuts.register ( 'Up+Down', nope ( 'Up+Down' ) ); 287 | // shortcuts.register ( 'Up+Right', nope ( 'Up+Right' ) ); 288 | // shortcuts.register ( 'Down+Right', nope ( 'Down+Right' ) ); 289 | 290 | shortcuts.register ( 'Ctrl+Cmd+Right', yep ( 'Ctrl+Cmd+Right' ) ); 291 | 292 | shortcuts.register ( '§', yep ( '§' ) ); 293 | shortcuts.register ( '±', yep ( '±' ) ); 294 | shortcuts.register ( 'Shift+§', nope ( 'Shift+§' ) ); 295 | shortcuts.register ( 'Shift+±', nope ( 'Shift+±' ) ); 296 | 297 | shortcuts.register ( 'Ctrl+Cmd+A', nope ( 'NOT DISPOSED!' ) )(); 298 | 299 | shortcuts.start (); 300 | 301 | // setTimeout ( () => { 302 | // shosho.stop (); 303 | // shosho.reset (); 304 | // }, 5000 ); 305 | 306 | /* MAIN - RECORDING */ 307 | 308 | shortcuts.register ( 'Ctrl+Cmd+R', () => { 309 | 310 | const dispose = ShoSho.record ( shortcut => { 311 | console.log ( '---' ); 312 | console.log ( shortcut ); 313 | console.log ( `"${shortcut}"` ); 314 | const trimmed = shortcut.replace ( /(^.*?)((?:Command(?:Left|Right)\+K )*\S+$)/, '$2' ); 315 | console.log ( trimmed ); 316 | const formatted = ShoSho.format ( trimmed, 'long-inflexible-nondirectional' ); 317 | console.log ( formatted ); 318 | }, { 319 | passthrough: false 320 | }); 321 | 322 | setTimeout ( dispose, 5000 ); 323 | 324 | return true; 325 | 326 | }); 327 | 328 | /* EXPORT */ 329 | 330 | globalThis.shosho = shortcuts; 331 | globalThis.S = shortcuts; 332 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present Fabio Spampinato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shosho", 3 | "repository": "github:fabiospampinato/shosho", 4 | "description": "A modern and powerful shortcuts management library.", 5 | "version": "1.4.3", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "exports": "./dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "scripts": { 11 | "clean": "tsex clean", 12 | "compile": "tsex compile", 13 | "compile:watch": "tsex compile --watch", 14 | "demo": "vite demo", 15 | "test": "tsex test", 16 | "test:watch": "tsex test --watch", 17 | "prepublishOnly": "tsex prepare" 18 | }, 19 | "keywords": [ 20 | "modern", 21 | "shortcuts", 22 | "management", 23 | "library" 24 | ], 25 | "devDependencies": { 26 | "fava": "^0.2.1", 27 | "tsex": "^3.0.1", 28 | "typescript": "^5.1.6", 29 | "vite": "^4.4.9" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ShoSho 2 | 3 | A modern and powerful shortcuts management library. 4 | 5 | ## Features 6 | 7 | - It is largely built on top of [`KeyboardEvent#code`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code) and [`KeyboardEvent#key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key), for great compatibility across browsers, platforms, languages and layouts. 8 | - It is _fast_. Almost always this library will do `O(1)` registrations, `O(1)` disposals, `O(1)` lookups and `O(1)` resetting. 9 | - It supports ~100 keyboard keys, including function keys and numpad keys. 10 | - It supports mouse buttons too, which can be mixed in shortcts with keyboard keys, like `Ctrl+ClickLeft`. 11 | - It supports simple shortcuts, like `Ctrl+F`, and shortcuts sequences, like `Ctrl+K Ctrl+F`. 12 | - It supports detecting which exact modifier key was pressed, like `ControlLeft` or `ControlRight`. 13 | - It supports common aliases for keys, like `Alt`/`Option`, or `Esc`/`Escape`. 14 | - It supports some character-based shortcuts, like `Shift+#`, rather than the equivalent key-based shortcut, which might be `Shift+3`. 15 | - It supports automatically using `Command` under macOS and `Control` elsewhere, by writing for example `CommandOrControl+F`. 16 | - It supports multi-trigger shortcuts, like `Up+Right` or `Down+Left`, except when the `Meta` key is pressed, which is buggy in browsers. 17 | - It supports Konami codes, a.k.a. cheatcode shortcuts, like `Up Up Down Down Left Right Left Right B A`. 18 | - It supports formatting shortcuts in 10 different styles, including formatting for [Electron](https://www.electronjs.org) and as symbols for hints/tooltips. 19 | - It supports recording a shortcut, for customizing shortcuts easily. 20 | 21 | ## Shortcut Syntax 22 | 23 | The following keys can be used when defining a shortcut: 24 | 25 | - **Modifiers**: Alt/Option, Cmd/Command/Meta, Ctrl/Control, Shift, CmdOrCtrl/CommandOrControl. 26 | - **Left modifiers**: AltLeft/OptionLeft, CmdLeft/CommandLeft/MetaLeft, CtrlLeft/ControLeft, CmdLeftOrCtrlLeft/CommandLeftOrControlLeft. 27 | - **Right modifiers**: AltRight/OptionRight, CmdRight/CommandRight/MetaRight, CtrlRight/ControRight, CmdRightOrCtrlRight/CommandRightOrControlRight. 28 | - **Digits**: 0-9. 29 | - **Alphabet letters**: A-Z. 30 | - **Function keys**: F1-F24. 31 | - **Numpad digits**: Numpad0-Numpad9. 32 | - **Numpad operators**: NumpadAdd, NumpadComma, NumpadDecimal, NumpadDivide, NumpadEnter, NumpadEqual, NumpadMultiply, NumpadSubtract. 33 | - **Special keys**: Backspace, Capslock, Del/Delete, Down, End, Enter/Return, Esc/Escape, Home, Insert, Left, PageDown, PageUp, Right, Space/Spacebar, Tab, Up, NumLock, ScrollLock. 34 | - **Punctuation keys**: !, @, #, $, %, ^, &, *, ( , ), _, {, }, |, :, ", <, >, ?, ~. 35 | - **Mouse buttons**: ClickLeft/MouseLeft, ClickMiddle/MouseMiddle, ClickRight/MouseRight, Mouse0-Mouse9. 36 | 37 | Please note that: 38 | 39 | - ℹ️ Other keys are not supported. 40 | - ℹ️ Shortcuts are case insensitive. 41 | - ℹ️ Keys in a single shortcut must be joined by a plus sign (e.g. Ctrl+A). 42 | - ℹ️ Sequences of shortcuts must be joined by a space (e.g. Ctrl+K Ctrl+B). 43 | - ⚠️ Punctuation keys should be avoided when possible, especially when used in combination with Shift/Alt, as they may lead to somewhat different results across different layouts. 44 | 45 | ## Install 46 | 47 | ```sh 48 | npm install --save shosho 49 | ``` 50 | 51 | ## Usage 52 | 53 | ```ts 54 | import ShoSho from 'shosho'; 55 | 56 | // Let's create a shortcuts manager 57 | 58 | const shortcuts = new ShoSho ({ 59 | capture: true, 60 | target: document, 61 | shouldHandleEvent ( event ) { 62 | // Return "true" if this event should be handled 63 | return true; 64 | } 65 | }); 66 | 67 | // Let's register some shortcuts. Shortcuts handlers must return "true" if they actually handled the shortcut 68 | 69 | shortcuts.register ( 'A', event => { // Single-key shortcut 70 | console.log ( 'Handling A' ); 71 | return true; 72 | }); 73 | 74 | shortcuts.register ( 'Ctrl+F', event => { // Simple modifier+trigger shortcut 75 | console.log ( 'Handling Ctrl+F' ); 76 | return true; 77 | }); 78 | 79 | shortcuts.register ( 'Ctrl+K Ctrl+F', event => { // Advanced sequence shortcut 80 | console.log ( 'Handling Ctrl+K Ctrl+F' ); 81 | return true; 82 | }); 83 | 84 | shortcuts.register ( 'ClickMiddle', event => { // Single-mouse-button shortcut 85 | console.log ( 'Skipping ClickMiddle actually' ); 86 | return false; // Returning "false" tells the library to try other potential handlers for this shortcut 87 | }); 88 | 89 | shortcuts.register ( 'CmdOrCtrl+ClickRight', event => { // Advanced mixed keyboard/mouse shortcut 90 | console.log ( 'Handling CmdOrCtrl+ClickRight' ); 91 | return true; 92 | }); 93 | 94 | // Let's register a Konami code, which basically is not affected by other registered shortcuts 95 | 96 | shortcuts.register ( 'Up Up Down Down Left Right Left Right B A', event => { // A Konami code 97 | console.log ( 'Secret shortcut triggered 🚀' ); 98 | }, { konami: true } ); // Registering it as a Konami code 99 | 100 | // Let's register a shortcut but then dispose of it 101 | 102 | const dispose = shortcuts.register ( 'Shift+1', event => { 103 | console.log ( 'Handling Shift+1' ); 104 | return true; 105 | }); 106 | 107 | dispose (); // Unregistering that shortcut with that handler 108 | 109 | // Let's trigger a shortcut programmatically, perhaps for debugging purposes 110 | 111 | const handled = shortcuts.trigger ( 'Ctrl+F' ); 112 | 113 | console.log ( handled ); // => true 114 | 115 | // Let's actually start listening for shortcuts 116 | 117 | shortcuts.start (); 118 | 119 | // Let's stop listening for shortcuts 120 | 121 | shortcuts.stop (); 122 | 123 | // Let's dispose of all registered shortcuts 124 | 125 | shortcuts.reset (); 126 | 127 | // Let's format a shortcut, with every supported format 128 | 129 | ShoSho.format ( 'ControlLeft+A', 'electron' ); // => 'Ctrl+A' 130 | ShoSho.format ( 'ControlLeft+A', 'symbols' ); // => '⌃A' 131 | ShoSho.format ( 'ControlLeft+A', 'long-flexible-directional' ); // => 'CommandOrControlLeft+A' 132 | ShoSho.format ( 'ControlLeft+A', 'long-flexible-nondirectional' ); // => 'CommandOrControl+A' 133 | ShoSho.format ( 'ControlLeft+A', 'long-inflexible-directional' ); // => 'ControlLeft+A' 134 | ShoSho.format ( 'ControlLeft+A', 'long-inflexible-nondirectional' ); // => 'Control+A' 135 | ShoSho.format ( 'ControlLeft+A', 'short-flexible-directional' ); // => 'CmdOrCtrlLeft+A' 136 | ShoSho.format ( 'ControlLeft+A', 'short-flexible-nondirectional' ); // => 'CmdOrCtrl+A' 137 | ShoSho.format ( 'ControlLeft+A', 'short-inflexible-directional' ); // => 'CtrlLeft+A' 138 | ShoSho.format ( 'ControlLeft+A', 'short-inflexible-nondirectional' ); // => 'Ctrl+A' 139 | 140 | // Let's check if something is a valid shortcut 141 | 142 | ShoSho.isShortcut ( 'Control+A' ); // => true 143 | ShoSho.isShortcut ( 'ControlA' ); // => false 144 | 145 | // Let's record a shortcut, which will require manual trimming and formatting 146 | 147 | const dispose = ShoSho.record ( shortcut => { 148 | 149 | console.log ( shortcut ); // => It could be 'A', 'CtrlLeft+A', 'CtrlLeft+A CtrlRight', 'CtrlLeft+A CtrlRight+B' and so on... 150 | 151 | const trimmed = shortcut.replace ( /(^.*?)((?:Control(?:Left|Right)\+K )*\S+$)/, '$2' ); // Allowing only 'ControlLeft+K' and 'ControlRight+K' to not be the last shortcut in the sequence 152 | const formatted = ShoSho.format ( trimmed, 'long-inflexible-nondirectional' ); // Ensuring the final shortcut is formatted exactly how we want it 153 | 154 | console.log ( formatted ); // => It could be 'K', 'CtrlLeft+K', 'CtrlLeft+K CtrlRight', 'Ctrlleft+K CtrlRight+A' and so on... 155 | 156 | }); 157 | 158 | // Let's stop recording 159 | 160 | dispose (); 161 | ``` 162 | 163 | ## License 164 | 165 | MIT © Fabio Spampinato 166 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | const DEFAULT_FORMAT_FORMAT = 'long-inflexible-nondirectional'; 5 | const DEFAULT_RECORD_FORMAT = 'long-inflexible-directional'; 6 | 7 | const MODIFIER_BITMASK = ( 1n << 15n ) - 1n; // Bitmask that includes all modifier keys and none of the triggers 8 | const TRIGGER_BITMASK = ( ( 1n << 160n ) - 1n ) ^ MODIFIER_BITMASK; // Bitmask that includes all trigger keys and none of the modifiers 9 | const UNSUPPORTED = TRIGGER_BITMASK; 10 | 11 | const PLUS_JOINER_RE = /(\S)\+/g; 12 | const WHITESPACE_JOINER_RE = /(\S)\s/g; 13 | const PLUSES_RE = /\+{2,}/gi; 14 | const WHITESPACE_RE = /\s+/gi; 15 | 16 | /* EXPORT */ 17 | 18 | export {DEFAULT_FORMAT_FORMAT, DEFAULT_RECORD_FORMAT}; 19 | export {MODIFIER_BITMASK, TRIGGER_BITMASK, UNSUPPORTED}; 20 | export {PLUS_JOINER_RE, WHITESPACE_JOINER_RE, PLUSES_RE, WHITESPACE_RE}; 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {DEFAULT_FORMAT_FORMAT, DEFAULT_RECORD_FORMAT} from './constants'; 5 | import {MODIFIER_BITMASK, TRIGGER_BITMASK, UNSUPPORTED} from './constants'; 6 | import {PLUS_JOINER_RE, PLUSES_RE, WHITESPACE_RE} from './constants'; 7 | import {CODE2ID, CODE_RISKY2ID, ID2FORMAT_TITLE, ID2FORMAT_ELECTRON, ID2FORMAT_SYMBOL, KEY_UNSUPPORTED2ID, KEY2ID, MOUSE2ID, NAME2ID, NAME_FORMATTING2ID, WHICH2ID} from './maps'; 8 | import {attempt, castArray, comparator, decompose, enumerate, first, isKeyboardEvent, isMac, isMouseEvent, isString, memoize, nope, or, orWith, takeRight, uniq, without, yep} from './utils'; 9 | import type {Checker, Disposer, Format, Handler, ChordNode, HandlerNode, HandlerOptions, Options, RecordHandler, RecordOptions} from './types'; 10 | 11 | /* HELPERS */ //TODO: Maybe move these elsewhere 12 | 13 | const has = ( id: bigint, key: bigint ): boolean => { 14 | return !!( id & key ); 15 | }; 16 | 17 | const id2trigger = ( id: bigint ): bigint => { 18 | return ( id & TRIGGER_BITMASK ); 19 | }; 20 | 21 | const shortcut2keys = memoize ( ( shortcut: string ): string[] => { 22 | const keys = shortcut.trim ().replace ( PLUSES_RE, '+Plus' ).replace ( PLUS_JOINER_RE, '$1 ' ).toLowerCase ().split ( WHITESPACE_RE ).filter ( Boolean ); 23 | return keys; 24 | }); 25 | 26 | const shortcut2ids = memoize ( ( shortcut: string ): bigint[][] => { 27 | const chords = shortcut.trim ().split ( WHITESPACE_RE ); 28 | const parts = chords.map ( chord2ids ); 29 | const ids = enumerate ( parts ); 30 | return ids; 31 | }); 32 | 33 | const shortcut2id_decomposed = memoize ( ( shortcut: string ): bigint[][] => { 34 | const chords = shortcut.trim ().split ( WHITESPACE_RE ); 35 | const keys = chords.map ( shortcut2keys ); 36 | const ids = keys.map ( keys => keys.map ( key => NAME_FORMATTING2ID[key] || UNSUPPORTED ) ); 37 | return ids; 38 | }); 39 | 40 | const chord2ids = memoize ( ( chord: string ): bigint[] => { 41 | const keys = shortcut2keys ( chord ); 42 | const parts = keys.map ( key => NAME2ID[key] || UNSUPPORTED ); 43 | const ids = enumerate ( parts ).map ( or ); 44 | return ids; 45 | }); 46 | 47 | const event2ids = ( event: Event ): bigint[] => { // Returning all possible detected ids, to support every scenario 48 | if ( isKeyboardEvent ( event ) ) { 49 | const codeId = CODE2ID[event.code] || 0n; 50 | if ( codeId ) return [codeId]; 51 | const keyId = KEY2ID[event.key] || 0n; 52 | const whichId = WHICH2ID[event.which] || 0n; 53 | const codeRiskyId = KEY_UNSUPPORTED2ID[event.key] ? 0n : CODE_RISKY2ID[event.code] || 0n; 54 | return [keyId, whichId, codeRiskyId]; 55 | } else if ( isMouseEvent ( event ) ) { 56 | return [MOUSE2ID[event.button] || 0n]; 57 | } else { 58 | return [0n]; 59 | } 60 | }; 61 | 62 | /* MAIN */ 63 | 64 | //TODO: Support character-based shortcuts (like Shift+#), by forking the current chords, i.e. properly 65 | //TODO: Support character-based shortcut triggering 66 | //TODO: Support deleting shortcuts with a filter, without the disposer function 67 | //TODO: Make sure there's always an Event object passed to handlers 68 | //TODO: Check if the event got stopped 69 | 70 | class ShoSho { 71 | 72 | /* VARIABLES */ 73 | 74 | private active: boolean; 75 | private chords: bigint[]; 76 | private chordsKonami: bigint[]; 77 | private depth: number; 78 | private depthKonami: number; 79 | private depthRecorder: number; 80 | private shouldHandle: Checker; 81 | private options: Options; 82 | private recorder?: RecordHandler; 83 | 84 | private tree: ChordNode = { 85 | children: {}, 86 | handlers: { 87 | parent: undefined, 88 | prev: undefined, 89 | next: undefined, 90 | handler: nope 91 | } 92 | }; 93 | 94 | private treeKonami: ChordNode = { 95 | children: {}, 96 | handlers: { 97 | parent: undefined, 98 | prev: undefined, 99 | next: undefined, 100 | handler: nope 101 | } 102 | }; 103 | 104 | /* CONSTRUCTOR */ 105 | 106 | constructor ( options: Options = {} ) { 107 | 108 | this.active = false; 109 | this.chords = [0n]; 110 | this.chordsKonami = [0n]; 111 | this.depth = 0; 112 | this.depthKonami = 0; 113 | this.depthRecorder = 0; 114 | this.shouldHandle = options.shouldHandleEvent || yep; 115 | this.options = options; 116 | 117 | } 118 | 119 | /* PRIVATE API */ 120 | 121 | private onBlur = (): void => { // If the tab loses focus we might not receive keyup/mouseup events 122 | 123 | const index = this.chords.length - 1; 124 | const indexKonami = this.chordsKonami.length - 1; 125 | 126 | this.chords[index] = 0n; 127 | this.chordsKonami[indexKonami] = 0n; 128 | 129 | }; 130 | 131 | private onNormalize = ( event: Event ): void => { // We might be missing some keydown/keyup events, safer to check again what the state of modifiers is 132 | 133 | if ( !isKeyboardEvent ( event ) && !isMouseEvent ( event ) ) return; 134 | 135 | const {altKey, ctrlKey, metaKey, shiftKey} = event; 136 | const code = isKeyboardEvent ( event ) ? event.code : ''; 137 | 138 | const index = this.chords.length - 1; 139 | const indexKonami = this.chordsKonami.length - 1; 140 | const chord = this.chords[index]; 141 | 142 | /* MISSING KEYDOWNS */ // We can't tell whether the left one or the right one was pressed, so we're taking a bit of a gamble on the left one //TODO: Maybe listen always to events to have that bit of data always available 143 | 144 | let addId = 0n; 145 | 146 | const hasAlt = has ( chord, CODE2ID.AltLeft ) || has ( chord, CODE2ID.AltRight ); 147 | const hasCtrl = has ( chord, CODE2ID.ControlLeft ) || has ( chord, CODE2ID.ControlRight ); 148 | const hasMeta = has ( chord, CODE2ID.MetaLeft ) || has ( chord, CODE2ID.MetaRight ); 149 | const hasShift = has ( chord, CODE2ID.ShiftLeft ) || has ( chord, CODE2ID.ShiftRight ); 150 | 151 | const isAlt = code === 'AltLeft' || code === 'AltRight'; 152 | const isCtrl = code === 'ControlLeft' || code === 'ControlRight'; 153 | const isMeta = code === 'MetaLeft' || code === 'MetaRight' || code === 'OSLeft' || code === 'OSRight'; 154 | const isShift = code === 'ShiftLeft' || code === 'ShiftRight'; 155 | 156 | if ( altKey && !hasAlt && !isAlt ) addId |= CODE2ID.AltLeft; 157 | if ( ctrlKey && !hasCtrl && !isCtrl ) addId |= CODE2ID.ControlLeft; 158 | if ( metaKey && !hasMeta && !isMeta ) addId |= CODE2ID.MetaLeft; 159 | if ( shiftKey && !hasShift && !isShift ) addId |= CODE2ID.ShiftLeft; 160 | 161 | if ( addId ) { 162 | 163 | this.chords[index] |= addId; 164 | this.chordsKonami[indexKonami] |= addId; 165 | 166 | } 167 | 168 | /* MISSING KEYUPS */ 169 | 170 | let deleteId = 0n; 171 | 172 | if ( !altKey ) deleteId |= CODE2ID.AltLeft | CODE2ID.AltRight; 173 | if ( !ctrlKey ) deleteId |= CODE2ID.ControlLeft | CODE2ID.ControlRight; 174 | if ( !metaKey ) deleteId |= CODE2ID.MetaLeft | CODE2ID.MetaRight; 175 | if ( !shiftKey ) deleteId |= CODE2ID.ShiftLeft | CODE2ID.ShiftRight; 176 | 177 | if ( deleteId ) { 178 | 179 | this.chords[index] &=~ deleteId; 180 | this.chordsKonami[indexKonami] &=~ deleteId; 181 | 182 | } 183 | 184 | }; 185 | 186 | private onRecord = ( ids: bigint[] ): void => { 187 | 188 | if ( !this.recorder ) return; 189 | 190 | const shortcut = first ( orWith ( this.chords, ids ).reverse ().map ( shortcut => ShoSho.format ( shortcut, DEFAULT_RECORD_FORMAT ) ) ); 191 | 192 | if ( !shortcut ) return; 193 | 194 | this.recorder ( shortcut ); 195 | 196 | }; 197 | 198 | private onDown = ( event: Event ): void => { 199 | 200 | if ( !this.shouldHandle ( event ) ) return; 201 | 202 | this.onNormalize ( event ); 203 | 204 | const index = this.chords.length - 1; 205 | const indexKonami = this.chordsKonami.length - 1; 206 | const chord = this.chords[index]; 207 | const chordKonami = this.chordsKonami[indexKonami]; 208 | const chordNoTrigger = chord & MODIFIER_BITMASK; 209 | const chordKonamiNoTrigger = chordKonami & MODIFIER_BITMASK; 210 | const chords = ( chord === chordNoTrigger ) ? [[chord, chordKonami]] : [[chord, chordKonami], [chordNoTrigger, chordKonamiNoTrigger]]; // Not being super strict about multi-key shortcuts here 211 | const ids = without ( uniq ( event2ids ( event ) ), 0n ); 212 | 213 | this.onRecord ( ids ); 214 | 215 | for ( let i = 0, l = ids.length; i < l; i++ ) { 216 | 217 | const id = ids[i]; 218 | const isLastId = ( i === l - 1 ); 219 | 220 | for ( let ci = 0, cl = chords.length; ci < cl; ci++ ) { 221 | 222 | const [chord, chordKonami] = chords[ci]; 223 | const isLastChord = ( ci === cl - 1 ); 224 | 225 | this.chords[index] = chord | id; 226 | this.chordsKonami[indexKonami] = chordKonami | id; 227 | 228 | const handled = !this.recorder && attempt ( () => this.trigger ( this.chords, event ), false ); 229 | const triggered = !!id2trigger ( this.chords[index] ); 230 | const isLast = handled || ( isLastId && isLastChord ); 231 | 232 | if ( !isLast ) continue; 233 | 234 | if ( handled ) { 235 | 236 | if ( !triggered || isMouseEvent ( event ) || chord & CODE2ID.MetaLeft || chord & CODE2ID.MetaRight ) { // Meta keys are buggy, they will swallow keyup events for the trigger 237 | 238 | this.chords = triggered ? [this.chords[index] &~ id] : [0n]; 239 | 240 | } 241 | 242 | event.preventDefault (); 243 | event.stopImmediatePropagation (); 244 | 245 | } else if ( triggered ) { 246 | 247 | this.chords.push ( this.chords[index] &~ id ); 248 | 249 | } 250 | 251 | if ( triggered ) { 252 | 253 | this.chordsKonami.push ( this.chordsKonami[indexKonami] &~ id ); 254 | 255 | } 256 | 257 | if ( this.recorder ) { 258 | 259 | if ( this.chords.length > this.depthRecorder ) { 260 | 261 | this.chords = takeRight ( this.chords, Math.max ( 1, this.depthRecorder ) ); 262 | 263 | } 264 | 265 | this.chordsKonami = [0n]; 266 | 267 | } else { 268 | 269 | if ( this.chords.length > this.depth ) { 270 | 271 | this.chords = takeRight ( this.chords, Math.max ( 1, this.depth ) ); 272 | 273 | } 274 | 275 | if ( this.chordsKonami.length > this.depthKonami ) { 276 | 277 | this.chordsKonami = takeRight ( this.chordsKonami, Math.max ( 1, this.depthKonami ) ); 278 | 279 | } 280 | 281 | } 282 | 283 | break; 284 | 285 | } 286 | 287 | } 288 | 289 | }; 290 | 291 | private onUp = ( event: Event ): void => { 292 | 293 | if ( !this.shouldHandle ( event ) ) return; 294 | 295 | const index = this.chords.length - 1; 296 | const indexKonami = this.chordsKonami.length - 1; 297 | const id = or ( event2ids ( event ) ); 298 | 299 | this.chords[index] &=~ id; 300 | this.chordsKonami[indexKonami] &=~ id; 301 | 302 | }; 303 | 304 | /* PUBLIC API */ 305 | 306 | record = ( handler: RecordHandler ): Disposer => { 307 | 308 | const isStartedAlready = this.active; 309 | 310 | this.chords = [0n]; 311 | this.chordsKonami = [0n]; 312 | this.recorder = handler; 313 | 314 | if ( !isStartedAlready ) { 315 | 316 | this.start (); 317 | 318 | } 319 | 320 | return (): void => { 321 | 322 | this.chords = [0n]; 323 | this.chordsKonami = [0n]; 324 | this.recorder = undefined; 325 | 326 | if ( !isStartedAlready ) { 327 | 328 | this.stop (); 329 | 330 | } 331 | 332 | }; 333 | 334 | }; 335 | 336 | register = ( shortcut: string | string[], handler: Handler, options: HandlerOptions = {} ): Disposer => { 337 | 338 | const chordseses = castArray ( shortcut ).map ( shortcut2ids ); 339 | const nodes: HandlerNode[] = []; 340 | const konami = !!options.konami; 341 | 342 | /* REGISTER */ 343 | 344 | for ( const chordses of chordseses ) { 345 | 346 | for ( const chords of chordses ) { 347 | 348 | let node = konami ? this.treeKonami : this.tree; 349 | let depth = 0; 350 | 351 | for ( const chord of chords ) { 352 | 353 | node = node.children[String ( chord )] ||= { 354 | children: {}, 355 | handlers: { 356 | parent: undefined, 357 | prev: undefined, 358 | next: undefined, 359 | handler: nope 360 | } 361 | }; 362 | 363 | depth += 1; 364 | 365 | } 366 | 367 | if ( konami ) { 368 | 369 | this.depthKonami = Math.max ( this.depthKonami, depth ); 370 | 371 | } else { 372 | 373 | this.depth = Math.max ( this.depth, depth ); 374 | 375 | } 376 | 377 | const handlers = node.handlers; 378 | 379 | node.handlers = handlers.next = { 380 | parent: node, 381 | prev: handlers, 382 | next: undefined, 383 | handler 384 | }; 385 | 386 | nodes.push ( node.handlers ); 387 | 388 | } 389 | 390 | } 391 | 392 | /* UNREGISTER */ 393 | 394 | return (): void => { 395 | 396 | for ( const node of nodes ) { 397 | 398 | const {prev, next} = node; 399 | 400 | if ( prev ) prev.next = next; 401 | if ( next ) next.prev = prev; 402 | if ( !next && node.parent ) node.parent.handlers = prev!; //TSC //UGLY: Write this better 403 | 404 | } 405 | 406 | }; 407 | 408 | }; 409 | 410 | reset = (): void => { 411 | 412 | this.tree.children = {}; 413 | this.treeKonami.children = {}; 414 | 415 | }; 416 | 417 | trigger = ( shortcut: string | bigint | bigint[], event?: Event ) : boolean => { 418 | 419 | const chords = isString ( shortcut ) ? first ( shortcut2ids ( shortcut ) ) : castArray ( shortcut ); 420 | const chordsKonami = this.chordsKonami; //TODO: Ugly, maybe there should just be two trigger functions, write this better 421 | 422 | if ( !chords ) return false; 423 | 424 | if ( this.depthKonami ) { // Triggering konami shortcuts, which are not stopped by, and don't stop, propagation 425 | 426 | for ( let i = 0, l = chordsKonami.length; i < l; i++ ) { // Trying all possible sequences 427 | 428 | const slice = i ? chordsKonami.slice ( i ) : chordsKonami; 429 | 430 | let node: ChordNode | undefined = this.treeKonami; 431 | 432 | for ( const chord of slice ) { 433 | 434 | node = node?.children[String ( chord )]; 435 | 436 | } 437 | 438 | let handler = node?.handlers; 439 | 440 | while ( handler ) { 441 | 442 | handler.handler ( event ); 443 | handler = handler.prev; 444 | 445 | } 446 | 447 | } 448 | 449 | } 450 | 451 | if ( this.depth ) { // Triggering regular shortcuts, which are stopped by, and stop, propagation 452 | 453 | for ( let i = 0, l = chords.length; i < l; i++ ) { // Trying all possible sequences 454 | 455 | const slice = i ? chords.slice ( i ) : chords; 456 | 457 | let node: ChordNode | undefined = this.tree; 458 | 459 | for ( const chord of slice ) { 460 | 461 | node = node?.children[String ( chord )]; 462 | 463 | } 464 | 465 | let handler = node?.handlers; 466 | 467 | while ( handler ) { 468 | 469 | if ( handler.handler ( event ) !== true ) { 470 | 471 | handler = handler.prev; 472 | 473 | } else { 474 | 475 | return true; 476 | 477 | } 478 | 479 | } 480 | 481 | } 482 | 483 | } 484 | 485 | return false; 486 | 487 | }; 488 | 489 | start = (): void => { 490 | 491 | if ( this.active ) return; 492 | 493 | this.active = true; 494 | 495 | const capture = this.options.capture ?? false; 496 | const root = this.options.target || document; 497 | 498 | root.addEventListener ( 'keydown', this.onDown, { capture } ); 499 | root.addEventListener ( 'keyup', this.onUp, { capture } ); 500 | root.addEventListener ( 'click', this.onDown, { capture } ); 501 | root.addEventListener ( 'auxclick', this.onDown, { capture } ); 502 | root.addEventListener ( 'contextmenu', this.onDown, { capture } ); 503 | 504 | window.addEventListener ( 'blur', this.onBlur ); 505 | 506 | }; 507 | 508 | stop = (): void => { 509 | 510 | if ( !this.active ) return; 511 | 512 | this.active = false; 513 | 514 | const capture = this.options.capture ?? false; 515 | const root = this.options.target || document; 516 | 517 | root.removeEventListener ( 'keydown', this.onDown, { capture } ); 518 | root.removeEventListener ( 'keyup', this.onUp, { capture } ); 519 | root.removeEventListener ( 'click', this.onDown, { capture } ); 520 | root.removeEventListener ( 'auxclick', this.onDown, { capture } ); 521 | root.removeEventListener ( 'contextmenu', this.onDown, { capture } ); 522 | 523 | window.removeEventListener ( 'blur', this.onBlur ); 524 | 525 | }; 526 | 527 | /* STATIC API */ 528 | 529 | static format = ( shortcut: string | bigint | bigint[], format: Format = DEFAULT_FORMAT_FORMAT ): string => { 530 | 531 | const formatMap = ( format === 'electron' ) ? ID2FORMAT_ELECTRON : ( format === 'symbols' ) ? ID2FORMAT_SYMBOL : ID2FORMAT_TITLE; 532 | const formatKeysJoiner = ( format === 'symbols' ) ? '' : '+'; 533 | const formatShortcutsJoiner = ' '; 534 | 535 | let outputIds = isString ( shortcut ) ? shortcut2id_decomposed ( shortcut ) : castArray ( shortcut ).map ( chord => ( chord === UNSUPPORTED ) ? [] : decompose ( chord ) ); 536 | let output = outputIds.map ( keys => keys.sort ( comparator ).map ( key => formatMap[key] || '§' ).join ( formatKeysJoiner ) ).join ( formatShortcutsJoiner ); 537 | 538 | if ( format.includes ( '-nondirectional' ) ) { 539 | 540 | /* SPARING LEFT+RIGHT MODIFIERS IN A SHORTCUT */ 541 | 542 | output = output.replace ( /(CommandOrControl|Control|Command|Alt|Shift)Left(\S+)\1Right/g, '$1§eft$2$1§ight' ); 543 | output = output.replace ( /(CommandOrControl|Control|Command|Alt|Shift)Right(\S+)\1Left/g, '$1§ight$2$1§eft' ); 544 | 545 | /* REMOVING LEFT/RIGHT SUFFIXES FROM MODIFIERS */ 546 | 547 | output = output.replace ( /(Control|Command|Alt|Shift)(Left|Right)/g, '$1' ); 548 | 549 | /* RESTORING LEFT+RIGHT MODIFIERS IN A SHORTCUT */ 550 | 551 | output = output.replaceAll ( '§eft', 'Left' ); 552 | output = output.replaceAll ( '§ight', 'Right' ); 553 | 554 | } 555 | 556 | if ( format.includes ( '-inflexible' ) ) { 557 | 558 | /* RESOLVING FLEXIBLE MODIFIERS */ 559 | 560 | const replacement = isMac () ? 'Command' : 'Control'; 561 | 562 | output = output.replace ( /Command(Left|Right)?OrControl/g, replacement ); 563 | 564 | } 565 | 566 | if ( format.includes ( '-flexible' ) ) { 567 | 568 | /* SPARING CONTROL+COMMAND IN A SHORTCUT */ 569 | 570 | output = output.replace ( /Control(\S+)Command/g, '§ontrol$1§ommand' ); 571 | output = output.replace ( /Command(\S+)Control/g, '§ommand$1§ontrol' ); 572 | 573 | /* MAKING OTHERS CONTROLS/COMMANDS FLEXIBLE */ 574 | 575 | output = output.replace ( /(^|\W)Control/g, '$1CommandOrControl' ); 576 | output = output.replace ( /Command(?!Or|Left|Right)/g, 'CommandOrControl' ); 577 | output = output.replace ( /CommandLeft(?!Or)/g, 'CommandOrControlLeft' ); 578 | output = output.replace ( /CommandRight(?!Or)/g, 'CommandOrControlRight' ); 579 | 580 | /* RESTORING CONTROL+COMMAND IN A SHORTCUT */ 581 | 582 | output = output.replaceAll ( '§ontrol', 'Control' ); 583 | output = output.replaceAll ( '§ommand', 'Command' ); 584 | 585 | } 586 | 587 | if ( format.includes ( 'short-' ) ) { 588 | 589 | /* SHORTHENING MODIFIERS */ 590 | 591 | output = output.replaceAll ( 'Command', 'Cmd' ); 592 | output = output.replaceAll ( 'Control', 'Ctrl' ); 593 | 594 | } 595 | 596 | return output; 597 | 598 | }; 599 | 600 | static isShortcut = memoize ( ( shortcut: string ): boolean => { 601 | 602 | const keys = shortcut2keys ( shortcut ); 603 | const isValid = keys.every ( key => key in NAME2ID ); 604 | 605 | return isValid; 606 | 607 | }); 608 | 609 | static record = ( handler: RecordHandler, options: RecordOptions = {} ): Disposer => { 610 | 611 | const shouldHandle = options.shouldHandleEvent || yep; 612 | 613 | const shortcuts = new ShoSho ({ 614 | capture: true, 615 | target: window, 616 | shouldHandleEvent: event => { 617 | if ( !shouldHandle ( event ) ) return false; 618 | if ( !options.passthrough ) { 619 | event.preventDefault (); 620 | event.stopImmediatePropagation (); 621 | } 622 | return true; 623 | } 624 | }); 625 | 626 | shortcuts.depthRecorder = options.limit ?? 10; 627 | 628 | return shortcuts.record ( handler ); 629 | 630 | }; 631 | 632 | } 633 | 634 | /* EXPORT */ 635 | 636 | export default ShoSho; 637 | export type {Format, Handler, HandlerOptions, Options, RecordHandler, RecordOptions}; 638 | -------------------------------------------------------------------------------- /src/maps.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {isMac} from './utils'; 5 | 6 | /* MODIFIERS */ 7 | 8 | const ControlLeft = 1n << 0n; 9 | const ControlRight = 1n << 1n; 10 | const MetaLeft = 1n << 2n; 11 | const MetaRight = 1n << 3n; 12 | const OSLeft = 1n << 2n; 13 | const OSRight = 1n << 3n; 14 | const AltLeft = 1n << 4n; 15 | const AltRight = 1n << 5n; 16 | const ShiftLeft = 1n << 6n; 17 | const ShiftRight = 1n << 7n; 18 | 19 | /* VIRTUAL MODIFIERS */ 20 | 21 | const Control = 1n << 8n; 22 | const Command = 1n << 9n; 23 | const Alt = 1n << 10n; 24 | const Shift = 1n << 11n; 25 | const CommandOrControl = 1n << 12n; 26 | const CommandOrControlLeft = 1n << 13n; 27 | const CommandOrControlRight = 1n << 14n; 28 | 29 | /* DIGITS */ 30 | 31 | const Digit0 = 1n << 15n; 32 | const Digit1 = 1n << 16n; 33 | const Digit2 = 1n << 17n; 34 | const Digit3 = 1n << 18n; 35 | const Digit4 = 1n << 19n; 36 | const Digit5 = 1n << 20n; 37 | const Digit6 = 1n << 21n; 38 | const Digit7 = 1n << 22n; 39 | const Digit8 = 1n << 23n; 40 | const Digit9 = 1n << 24n; 41 | 42 | /* ALPHABET */ 43 | 44 | const KeyA = 1n << 25n; 45 | const KeyB = 1n << 26n; 46 | const KeyC = 1n << 27n; 47 | const KeyD = 1n << 28n; 48 | const KeyE = 1n << 29n; 49 | const KeyF = 1n << 30n; 50 | const KeyG = 1n << 31n; 51 | const KeyH = 1n << 32n; 52 | const KeyI = 1n << 33n; 53 | const KeyJ = 1n << 34n; 54 | const KeyK = 1n << 35n; 55 | const KeyL = 1n << 36n; 56 | const KeyM = 1n << 37n; 57 | const KeyN = 1n << 38n; 58 | const KeyO = 1n << 39n; 59 | const KeyP = 1n << 40n; 60 | const KeyQ = 1n << 41n; 61 | const KeyR = 1n << 42n; 62 | const KeyS = 1n << 43n; 63 | const KeyT = 1n << 44n; 64 | const KeyU = 1n << 45n; 65 | const KeyV = 1n << 46n; 66 | const KeyW = 1n << 47n; 67 | const KeyX = 1n << 48n; 68 | const KeyY = 1n << 49n; 69 | const KeyZ = 1n << 50n; 70 | 71 | /* FUNCTION KEYS */ 72 | 73 | const F1 = 1n << 51n; 74 | const F2 = 1n << 52n; 75 | const F3 = 1n << 53n; 76 | const F4 = 1n << 54n; 77 | const F5 = 1n << 55n; 78 | const F6 = 1n << 56n; 79 | const F7 = 1n << 57n; 80 | const F8 = 1n << 58n; 81 | const F9 = 1n << 59n; 82 | const F10 = 1n << 60n; 83 | const F11 = 1n << 61n; 84 | const F12 = 1n << 62n; 85 | const F13 = 1n << 63n; 86 | const F14 = 1n << 64n; 87 | const F15 = 1n << 65n; 88 | const F16 = 1n << 66n; 89 | const F17 = 1n << 67n; 90 | const F18 = 1n << 68n; 91 | const F19 = 1n << 69n; 92 | const F20 = 1n << 70n; 93 | const F21 = 1n << 71n; 94 | const F22 = 1n << 72n; 95 | const F23 = 1n << 73n; 96 | const F24 = 1n << 74n; 97 | 98 | /* NUMPAD DIGITS */ 99 | 100 | const Numpad0 = 1n << 75n; 101 | const Numpad1 = 1n << 76n; 102 | const Numpad2 = 1n << 77n; 103 | const Numpad3 = 1n << 78n; 104 | const Numpad4 = 1n << 79n; 105 | const Numpad5 = 1n << 80n; 106 | const Numpad6 = 1n << 81n; 107 | const Numpad7 = 1n << 82n; 108 | const Numpad8 = 1n << 83n; 109 | const Numpad9 = 1n << 84n; 110 | 111 | /* NUMPAD OPERATORS */ 112 | 113 | const NumpadAdd = 1n << 85n; 114 | const NumpadComma = 1n << 86n; 115 | const NumpadDecimal = 1n << 87n; 116 | const NumpadDivide = 1n << 88n; 117 | const NumpadEnter = 1n << 89n; 118 | const NumpadEqual = 1n << 90n; 119 | const NumpadMultiply = 1n << 91n; 120 | const NumpadSubtract = 1n << 92n; 121 | 122 | /* SPECIAL KEYS */ 123 | 124 | const ArrowDown = 1n << 93n; 125 | const ArrowLeft = 1n << 94n; 126 | const ArrowRight = 1n << 95n; 127 | const ArrowUp = 1n << 96n; 128 | const Backspace = 1n << 97n; 129 | const CapsLock = 1n << 98n; 130 | const Delete = 1n << 99n; 131 | const End = 1n << 100n; 132 | const Enter = 1n << 101n; 133 | const Escape = 1n << 102n; 134 | const Home = 1n << 103n; 135 | const Insert = 1n << 104n; 136 | const PageDown = 1n << 105n; 137 | const PageUp = 1n << 106n; 138 | const Space = 1n << 107n; 139 | const Tab = 1n << 108n; 140 | 141 | /* PUNCTUATION KEYS */ 142 | 143 | const Backquote = 1n << 109n; 144 | const Backslash = 1n << 110n; 145 | const BracketLeft = 1n << 111n; 146 | const BracketRight = 1n << 112n; 147 | const Comma = 1n << 113n; 148 | const Equal = 1n << 114n; 149 | const IntlBackslash = 1n << 115n; 150 | const Minus = 1n << 116n; 151 | const Period = 1n << 117n; 152 | const Quote = 1n << 118n; 153 | const Semicolon = 1n << 119n; 154 | const Slash = 1n << 120n; 155 | 156 | /* OTHER KEYS */ 157 | 158 | const NumLock = 1n << 121n; 159 | const ScrollLock = 1n << 122n; 160 | 161 | /* CUSTOM KEYBOARD KEYS */ 162 | 163 | const Ampersand = 1n << 123n; 164 | const Asterisk = 1n << 124n; 165 | const At = 1n << 125n; 166 | const BraceLeft = 1n << 126n; 167 | const BraceRight = 1n << 127n; 168 | const Caret = 1n << 128n; 169 | const Colon = 1n << 129n; 170 | const Dollar = 1n << 130n; 171 | const DoubleQuote = 1n << 131n; 172 | const ExclamationMark = 1n << 132n; 173 | const GreaterThan = 1n << 133n; 174 | const Hash = 1n << 134n; 175 | const LessThan = 1n << 135n; 176 | const ParenthesisLeft = 1n << 136n; 177 | const ParenthesisRight = 1n << 137n; 178 | const Percent = 1n << 138n; 179 | const Pipe = 1n << 139n; 180 | const Plus = 1n << 140n; 181 | const QuestionMark = 1n << 141n; 182 | const Tilde = 1n << 142n; 183 | const Underscore = 1n << 143n; 184 | 185 | /* CUSTOM MOUSE KEYS */ 186 | 187 | const Mouse0 = 1n << 144n; 188 | const Mouse1 = 1n << 145n; 189 | const Mouse2 = 1n << 146n; 190 | const Mouse3 = 1n << 147n; 191 | const Mouse4 = 1n << 148n; 192 | const Mouse5 = 1n << 149n; 193 | const Mouse6 = 1n << 150n; 194 | const Mouse7 = 1n << 151n; 195 | const Mouse8 = 1n << 152n; 196 | const Mouse9 = 1n << 153n; 197 | 198 | /* UNSUPPORTED KEYS */ 199 | 200 | const Section = 1n << 154n; 201 | const PlusMinus = 1n << 155n; 202 | 203 | /* MAIN */ 204 | 205 | const CODE2ID = { 206 | /* MODIFIERS */ 207 | ControlLeft, 208 | ControlRight, 209 | MetaLeft, 210 | MetaRight, 211 | OSLeft, 212 | OSRight, 213 | AltLeft, 214 | AltRight, 215 | ShiftLeft, 216 | ShiftRight, 217 | /* FUNCTION KEYS */ 218 | F1, 219 | F2, 220 | F3, 221 | F4, 222 | F5, 223 | F6, 224 | F7, 225 | F8, 226 | F9, 227 | F10, 228 | F11, 229 | F12, 230 | F13, 231 | F14, 232 | F15, 233 | F16, 234 | F17, 235 | F18, 236 | F19, 237 | F20, 238 | F21, 239 | F22, 240 | F23, 241 | F24, 242 | /* NUMPAD DIGITS */ 243 | Numpad0, 244 | Numpad1, 245 | Numpad2, 246 | Numpad3, 247 | Numpad4, 248 | Numpad5, 249 | Numpad6, 250 | Numpad7, 251 | Numpad8, 252 | Numpad9, 253 | /* NUMPAD OPERATORS */ 254 | NumpadAdd, 255 | NumpadComma, 256 | NumpadDecimal, 257 | NumpadDivide, 258 | NumpadEnter, 259 | NumpadEqual, 260 | NumpadMultiply, 261 | NumpadSubtract, 262 | /* SPECIAL KEYS */ 263 | ArrowDown, 264 | ArrowLeft, 265 | ArrowRight, 266 | ArrowUp, 267 | Backspace, 268 | CapsLock, 269 | Delete, 270 | End, 271 | Enter, 272 | Escape, 273 | Home, 274 | Insert, 275 | PageDown, 276 | PageUp, 277 | Space, 278 | Tab, 279 | /* OTHER KEYS */ 280 | NumLock, 281 | ScrollLock 282 | }; 283 | 284 | const CODE_RISKY2ID = { 285 | /* PUNCTUATION */ 286 | Backquote, 287 | Backslash, 288 | BracketLeft, 289 | BracketRight, 290 | Comma, 291 | Equal, 292 | IntlBackslash, 293 | Minus, 294 | Period, 295 | Quote, 296 | Semicolon, 297 | Slash 298 | }; 299 | 300 | const ID2FORMAT_TITLE = { 301 | /* MODIFIERS */ 302 | [ControlLeft]: 'ControlLeft', 303 | [ControlRight]: 'ControlRight', 304 | [MetaLeft]: 'CommandLeft', 305 | [MetaRight]: 'CommandRight', 306 | [OSLeft]: 'CommandLeft', 307 | [OSRight]: 'CommandRight', 308 | [AltLeft]: 'AltLeft', 309 | [AltRight]: 'AltRight', 310 | [ShiftLeft]: 'ShiftLeft', 311 | [ShiftRight]: 'ShiftRight', 312 | /* VIRTUAL MODIFIERS */ 313 | [Control]: 'Control', 314 | [Command]: 'Command', 315 | [Alt]: 'Alt', 316 | [Shift]: 'Shift', 317 | [CommandOrControl]: 'CommandOrControl', 318 | [CommandOrControlLeft]: 'CommandOrControlLeft', 319 | [CommandOrControlRight]: 'CommandOrControlRight', 320 | /* DIGITS */ 321 | [Digit0]: '0', 322 | [Digit1]: '1', 323 | [Digit2]: '2', 324 | [Digit3]: '3', 325 | [Digit4]: '4', 326 | [Digit5]: '5', 327 | [Digit6]: '6', 328 | [Digit7]: '7', 329 | [Digit8]: '8', 330 | [Digit9]: '9', 331 | /* ALPHABET */ 332 | [KeyA]: 'A', 333 | [KeyB]: 'B', 334 | [KeyC]: 'C', 335 | [KeyD]: 'D', 336 | [KeyE]: 'E', 337 | [KeyF]: 'F', 338 | [KeyG]: 'G', 339 | [KeyH]: 'H', 340 | [KeyI]: 'I', 341 | [KeyJ]: 'J', 342 | [KeyK]: 'K', 343 | [KeyL]: 'L', 344 | [KeyM]: 'M', 345 | [KeyN]: 'N', 346 | [KeyO]: 'O', 347 | [KeyP]: 'P', 348 | [KeyQ]: 'Q', 349 | [KeyR]: 'R', 350 | [KeyS]: 'S', 351 | [KeyT]: 'T', 352 | [KeyU]: 'U', 353 | [KeyV]: 'V', 354 | [KeyW]: 'W', 355 | [KeyX]: 'X', 356 | [KeyY]: 'Y', 357 | [KeyZ]: 'Z', 358 | /* FUNCTION KEYS */ 359 | [F1]: 'F1', 360 | [F2]: 'F2', 361 | [F3]: 'F3', 362 | [F4]: 'F4', 363 | [F5]: 'F5', 364 | [F6]: 'F6', 365 | [F7]: 'F7', 366 | [F8]: 'F8', 367 | [F9]: 'F9', 368 | [F10]: 'F10', 369 | [F11]: 'F11', 370 | [F12]: 'F12', 371 | [F13]: 'F13', 372 | [F14]: 'F14', 373 | [F15]: 'F15', 374 | [F16]: 'F16', 375 | [F17]: 'F17', 376 | [F18]: 'F18', 377 | [F19]: 'F19', 378 | [F20]: 'F20', 379 | [F21]: 'F21', 380 | [F22]: 'F22', 381 | [F23]: 'F23', 382 | [F24]: 'F24', 383 | /* NUMPAD DIGITS */ 384 | [Numpad0]: 'Numpad0', 385 | [Numpad1]: 'Numpad1', 386 | [Numpad2]: 'Numpad2', 387 | [Numpad3]: 'Numpad3', 388 | [Numpad4]: 'Numpad4', 389 | [Numpad5]: 'Numpad5', 390 | [Numpad6]: 'Numpad6', 391 | [Numpad7]: 'Numpad7', 392 | [Numpad8]: 'Numpad8', 393 | [Numpad9]: 'Numpad9', 394 | /* NUMPAD OPERATORS */ 395 | [NumpadAdd]: 'NumpadAdd', 396 | [NumpadComma]: 'NumpadComma', 397 | [NumpadDecimal]: 'NumpadDecimal', 398 | [NumpadDivide]: 'NumpadDivide', 399 | [NumpadEnter]: 'NumpadEnter', 400 | [NumpadEqual]: 'NumpadEqual', 401 | [NumpadMultiply]: 'NumpadMultiply', 402 | [NumpadSubtract]: 'NumpadSubtract', 403 | /* SPECIAL KEYS */ 404 | [ArrowDown]: 'Down', 405 | [ArrowLeft]: 'Left', 406 | [ArrowRight]: 'Right', 407 | [ArrowUp]: 'Up', 408 | [Backspace]: 'Backspace', 409 | [CapsLock]: 'CapsLock', 410 | [Delete]: 'Delete', 411 | [End]: 'End', 412 | [Enter]: 'Enter', 413 | [Escape]: 'Esc', 414 | [Home]: 'Home', 415 | [Insert]: 'Insert', 416 | [PageDown]: 'PageDown', 417 | [PageUp]: 'PageUp', 418 | [Space]: 'Space', 419 | [Tab]: 'Tab', 420 | /* PUNCTUATION KEYS */ 421 | [Backquote]: '`', 422 | [Backslash]: '\\', 423 | [BracketLeft]: '[', 424 | [BracketRight]: ']', 425 | [Comma]: ',', 426 | [Equal]: '=', 427 | [Minus]: '-', 428 | [Period]: '.', 429 | [Quote]: '\'', 430 | [Semicolon]: ';', 431 | [Slash]: '/', 432 | /* OTHER KEYS */ 433 | [NumLock]: 'NumLock', 434 | [ScrollLock]: 'ScrollLock', 435 | /* CUSTOM KEYBOARD KEYS */ 436 | [Ampersand]: '&', 437 | [Asterisk]: '*', 438 | [At]: '@', 439 | [BraceLeft]: '{', 440 | [BraceRight]: '}', 441 | [Caret]: '^', 442 | [Colon]: ':', 443 | [Dollar]: '$', 444 | [DoubleQuote]: '"', 445 | [ExclamationMark]: '!', 446 | [GreaterThan]: '>', 447 | [Hash]: '#', 448 | [LessThan]: '<', 449 | [ParenthesisLeft]: '(', 450 | [ParenthesisRight]: ')', 451 | [Percent]: '%', 452 | [Pipe]: '|', 453 | [Plus]: '+', 454 | [QuestionMark]: '?', 455 | [Tilde]: '~', 456 | [Underscore]: '_', 457 | /* CUSTOM MOUSE KEYS */ 458 | [Mouse0]: 'LeftClick', 459 | [Mouse1]: 'MiddleClick', 460 | [Mouse2]: 'RightClick', 461 | [Mouse3]: 'Mouse3', 462 | [Mouse4]: 'Mouse4', 463 | [Mouse5]: 'Mouse5', 464 | [Mouse6]: 'Mouse6', 465 | [Mouse7]: 'Mouse7', 466 | [Mouse8]: 'Mouse8', 467 | [Mouse9]: 'Mouse9', 468 | /* UNSUPPORTED KEYS */ 469 | [Section]: '§', 470 | [PlusMinus]: '±' 471 | }; 472 | 473 | const ID2FORMAT_ELECTRON = { 474 | /* MODIFIERS */ 475 | [ControlLeft]: 'Ctrl', 476 | [ControlRight]: 'Ctrl', 477 | [MetaLeft]: 'Cmd', 478 | [MetaRight]: 'Cmd', 479 | [OSLeft]: 'Cmd', 480 | [OSRight]: 'Cmd', 481 | [AltLeft]: 'Alt', 482 | [AltRight]: 'Alt', 483 | [ShiftLeft]: 'Shift', 484 | [ShiftRight]: 'Shift', 485 | /* VIRTUAL MODIFIERS */ 486 | [Control]: 'Ctrl', 487 | [Command]: 'Cmd', 488 | [Alt]: 'Alt', 489 | [Shift]: 'Shift', 490 | [CommandOrControl]: 'CmdOrCtrl', 491 | [CommandOrControlLeft]: 'CmdOrCtrl', 492 | [CommandOrControlRight]: 'CmdOrCtrl', 493 | /* DIGITS */ 494 | [Digit0]: '0', 495 | [Digit1]: '1', 496 | [Digit2]: '2', 497 | [Digit3]: '3', 498 | [Digit4]: '4', 499 | [Digit5]: '5', 500 | [Digit6]: '6', 501 | [Digit7]: '7', 502 | [Digit8]: '8', 503 | [Digit9]: '9', 504 | /* ALPHABET */ 505 | [KeyA]: 'A', 506 | [KeyB]: 'B', 507 | [KeyC]: 'C', 508 | [KeyD]: 'D', 509 | [KeyE]: 'E', 510 | [KeyF]: 'F', 511 | [KeyG]: 'G', 512 | [KeyH]: 'H', 513 | [KeyI]: 'I', 514 | [KeyJ]: 'J', 515 | [KeyK]: 'K', 516 | [KeyL]: 'L', 517 | [KeyM]: 'M', 518 | [KeyN]: 'N', 519 | [KeyO]: 'O', 520 | [KeyP]: 'P', 521 | [KeyQ]: 'Q', 522 | [KeyR]: 'R', 523 | [KeyS]: 'S', 524 | [KeyT]: 'T', 525 | [KeyU]: 'U', 526 | [KeyV]: 'V', 527 | [KeyW]: 'W', 528 | [KeyX]: 'X', 529 | [KeyY]: 'Y', 530 | [KeyZ]: 'Z', 531 | /* FUNCTION KEYS */ 532 | [F1]: 'F1', 533 | [F2]: 'F2', 534 | [F3]: 'F3', 535 | [F4]: 'F4', 536 | [F5]: 'F5', 537 | [F6]: 'F6', 538 | [F7]: 'F7', 539 | [F8]: 'F8', 540 | [F9]: 'F9', 541 | [F10]: 'F10', 542 | [F11]: 'F11', 543 | [F12]: 'F12', 544 | [F13]: 'F13', 545 | [F14]: 'F14', 546 | [F15]: 'F15', 547 | [F16]: 'F16', 548 | [F17]: 'F17', 549 | [F18]: 'F18', 550 | [F19]: 'F19', 551 | [F20]: 'F20', 552 | [F21]: 'F21', 553 | [F22]: 'F22', 554 | [F23]: 'F23', 555 | [F24]: 'F24', 556 | /* NUMPAD DIGITS */ 557 | [Numpad0]: 'Num0', 558 | [Numpad1]: 'Num1', 559 | [Numpad2]: 'Num2', 560 | [Numpad3]: 'Num3', 561 | [Numpad4]: 'Num4', 562 | [Numpad5]: 'Num5', 563 | [Numpad6]: 'Num6', 564 | [Numpad7]: 'Num7', 565 | [Numpad8]: 'Num8', 566 | [Numpad9]: 'Num9', 567 | /* NUMPAD OPERATORS */ 568 | [NumpadAdd]: 'NumAdd', 569 | [NumpadComma]: '§', 570 | [NumpadDecimal]: 'NumDec', 571 | [NumpadDivide]: 'NumDiv', 572 | [NumpadEnter]: '§', 573 | [NumpadEqual]: '§', 574 | [NumpadMultiply]: 'NumMult', 575 | [NumpadSubtract]: 'NumSub', 576 | /* SPECIAL KEYS */ 577 | [ArrowDown]: 'Down', 578 | [ArrowLeft]: 'Left', 579 | [ArrowRight]: 'Right', 580 | [ArrowUp]: 'Up', 581 | [Backspace]: 'Backspace', 582 | [CapsLock]: 'CapsLock', 583 | [Delete]: 'Delete', 584 | [End]: 'End', 585 | [Enter]: 'Enter', 586 | [Escape]: 'Esc', 587 | [Home]: 'Home', 588 | [Insert]: 'Insert', 589 | [PageDown]: 'PageDown', 590 | [PageUp]: 'PageUp', 591 | [Space]: 'Space', 592 | [Tab]: 'Tab', 593 | /* PUNCTUATION KEYS */ 594 | [Backquote]: '`', 595 | [Backslash]: '\\', 596 | [BracketLeft]: '[', 597 | [BracketRight]: ']', 598 | [Comma]: ',', 599 | [Equal]: '=', 600 | [Minus]: '-', 601 | [Period]: '.', 602 | [Quote]: '\'', 603 | [Semicolon]: ';', 604 | [Slash]: '/', 605 | /* OTHER KEYS */ 606 | [NumLock]: 'NumLock', 607 | [ScrollLock]: 'ScrollLock', 608 | /* CUSTOM KEYBOARD KEYS */ 609 | [Ampersand]: '&', 610 | [Asterisk]: '*', 611 | [At]: '@', 612 | [BraceLeft]: '{', 613 | [BraceRight]: '}', 614 | [Caret]: '^', 615 | [Colon]: ':', 616 | [Dollar]: '$', 617 | [DoubleQuote]: '"', 618 | [ExclamationMark]: '!', 619 | [GreaterThan]: '>', 620 | [Hash]: '#', 621 | [LessThan]: '<', 622 | [ParenthesisLeft]: '(', 623 | [ParenthesisRight]: ')', 624 | [Percent]: '%', 625 | [Pipe]: '|', 626 | [Plus]: '+', 627 | [QuestionMark]: '?', 628 | [Tilde]: '~', 629 | [Underscore]: '_', 630 | /* CUSTOM MOUSE KEYS */ 631 | [Mouse0]: '§', 632 | [Mouse1]: '§', 633 | [Mouse2]: '§', 634 | [Mouse3]: '§', 635 | [Mouse4]: '§', 636 | [Mouse5]: '§', 637 | [Mouse6]: '§', 638 | [Mouse7]: '§', 639 | [Mouse8]: '§', 640 | [Mouse9]: '§', 641 | /* UNSUPPORTED KEYS */ 642 | [Section]: '§', 643 | [PlusMinus]: '±' 644 | }; 645 | 646 | const ID2FORMAT_SYMBOL = { 647 | /* MODIFIERS */ 648 | [ControlLeft]: '⌃', 649 | [ControlRight]: '⌃', 650 | [MetaLeft]: '⌘', 651 | [MetaRight]: '⌘', 652 | [OSLeft]: '⌘', 653 | [OSRight]: '⌘', 654 | [AltLeft]: '⌥', 655 | [AltRight]: '⌥', 656 | [ShiftLeft]: '⇧', 657 | [ShiftRight]: '⇧', 658 | /* VIRTUAL MODIFIERS */ 659 | [Control]: '⌃', 660 | [Command]: '⌘', 661 | [Alt]: '⌥', 662 | [Shift]: '⇧', 663 | [CommandOrControl]: isMac () ? '⌘' : '⌃', 664 | [CommandOrControlLeft]: isMac () ? '⌘' : '⌃', 665 | [CommandOrControlRight]: isMac () ? '⌘' : '⌃', 666 | /* DIGITS */ 667 | [Digit0]: '0', 668 | [Digit1]: '1', 669 | [Digit2]: '2', 670 | [Digit3]: '3', 671 | [Digit4]: '4', 672 | [Digit5]: '5', 673 | [Digit6]: '6', 674 | [Digit7]: '7', 675 | [Digit8]: '8', 676 | [Digit9]: '9', 677 | /* ALPHABET */ 678 | [KeyA]: 'A', 679 | [KeyB]: 'B', 680 | [KeyC]: 'C', 681 | [KeyD]: 'D', 682 | [KeyE]: 'E', 683 | [KeyF]: 'F', 684 | [KeyG]: 'G', 685 | [KeyH]: 'H', 686 | [KeyI]: 'I', 687 | [KeyJ]: 'J', 688 | [KeyK]: 'K', 689 | [KeyL]: 'L', 690 | [KeyM]: 'M', 691 | [KeyN]: 'N', 692 | [KeyO]: 'O', 693 | [KeyP]: 'P', 694 | [KeyQ]: 'Q', 695 | [KeyR]: 'R', 696 | [KeyS]: 'S', 697 | [KeyT]: 'T', 698 | [KeyU]: 'U', 699 | [KeyV]: 'V', 700 | [KeyW]: 'W', 701 | [KeyX]: 'X', 702 | [KeyY]: 'Y', 703 | [KeyZ]: 'Z', 704 | /* FUNCTION KEYS */ 705 | [F1]: 'F1', 706 | [F2]: 'F2', 707 | [F3]: 'F3', 708 | [F4]: 'F4', 709 | [F5]: 'F5', 710 | [F6]: 'F6', 711 | [F7]: 'F7', 712 | [F8]: 'F8', 713 | [F9]: 'F9', 714 | [F10]: 'F10', 715 | [F11]: 'F11', 716 | [F12]: 'F12', 717 | [F13]: 'F13', 718 | [F14]: 'F14', 719 | [F15]: 'F15', 720 | [F16]: 'F16', 721 | [F17]: 'F17', 722 | [F18]: 'F18', 723 | [F19]: 'F19', 724 | [F20]: 'F20', 725 | [F21]: 'F21', 726 | [F22]: 'F22', 727 | [F23]: 'F23', 728 | [F24]: 'F24', 729 | /* NUMPAD DIGITS */ 730 | [Numpad0]: 'Numpad0', 731 | [Numpad1]: 'Numpad1', 732 | [Numpad2]: 'Numpad2', 733 | [Numpad3]: 'Numpad3', 734 | [Numpad4]: 'Numpad4', 735 | [Numpad5]: 'Numpad5', 736 | [Numpad6]: 'Numpad6', 737 | [Numpad7]: 'Numpad7', 738 | [Numpad8]: 'Numpad8', 739 | [Numpad9]: 'Numpad9', 740 | /* NUMPAD OPERATORS */ 741 | [NumpadAdd]: 'NumpadAdd', 742 | [NumpadComma]: 'NumpadComma', 743 | [NumpadDecimal]: 'NumpadDecimal', 744 | [NumpadDivide]: 'NumpadDivide', 745 | [NumpadEnter]: 'NumpadEnter', 746 | [NumpadEqual]: 'NumpadEqual', 747 | [NumpadMultiply]: 'NumpadMultiply', 748 | [NumpadSubtract]: 'NumpadSubtract', 749 | /* SPECIAL KEYS */ 750 | [ArrowDown]: '↓', 751 | [ArrowLeft]: '←', 752 | [ArrowRight]: '→', 753 | [ArrowUp]: '↑', 754 | [Backspace]: '⌫', 755 | [CapsLock]: '⇪', 756 | [Delete]: '⌦', 757 | [End]: '↘', 758 | [Enter]: '⏎', 759 | [Escape]: '⎋', 760 | [Home]: '↖', 761 | [Insert]: '⎀', 762 | [PageDown]: '⇟', 763 | [PageUp]: '⇞', 764 | [Space]: '␣', 765 | [Tab]: '⇥', 766 | /* PUNCTUATION KEYS */ 767 | [Backquote]: '`', 768 | [Backslash]: '\\', 769 | [BracketLeft]: '[', 770 | [BracketRight]: ']', 771 | [Comma]: ',', 772 | [Equal]: '=', 773 | [Minus]: '-', 774 | [Period]: '.', 775 | [Quote]: '\'', 776 | [Semicolon]: ';', 777 | [Slash]: '/', 778 | /* OTHER KEYS */ 779 | [NumLock]: 'NumLock', 780 | [ScrollLock]: 'ScrollLock', 781 | /* CUSTOM KEYBOARD KEYS */ 782 | [Ampersand]: '&', 783 | [Asterisk]: '*', 784 | [At]: '@', 785 | [BraceLeft]: '{', 786 | [BraceRight]: '}', 787 | [Caret]: '^', 788 | [Colon]: ':', 789 | [Dollar]: '$', 790 | [DoubleQuote]: '"', 791 | [ExclamationMark]: '!', 792 | [GreaterThan]: '>', 793 | [Hash]: '#', 794 | [LessThan]: '<', 795 | [ParenthesisLeft]: '(', 796 | [ParenthesisRight]: ')', 797 | [Percent]: '%', 798 | [Pipe]: '|', 799 | [Plus]: '+', 800 | [QuestionMark]: '?', 801 | [Tilde]: '~', 802 | [Underscore]: '_', 803 | /* CUSTOM MOUSE KEYS */ 804 | [Mouse0]: 'LeftClick', 805 | [Mouse1]: 'MiddleClick', 806 | [Mouse2]: 'RightClick', 807 | [Mouse3]: 'Mouse3', 808 | [Mouse4]: 'Mouse4', 809 | [Mouse5]: 'Mouse5', 810 | [Mouse6]: 'Mouse6', 811 | [Mouse7]: 'Mouse7', 812 | [Mouse8]: 'Mouse8', 813 | [Mouse9]: 'Mouse9', 814 | /* UNSUPPORTED KEYS */ 815 | [Section]: '§', 816 | [PlusMinus]: '±' 817 | }; 818 | 819 | const KEY_DIGITS2ID = { 820 | /* DIGITS */ 821 | '0': Digit0, 822 | '1': Digit1, 823 | '2': Digit2, 824 | '3': Digit3, 825 | '4': Digit4, 826 | '5': Digit5, 827 | '6': Digit6, 828 | '7': Digit7, 829 | '8': Digit8, 830 | '9': Digit9 831 | }; 832 | 833 | const KEY_ALPHABET2ID = { 834 | /* ALPHABET UPPERCASE */ 835 | 'A': KeyA, 836 | 'B': KeyB, 837 | 'C': KeyC, 838 | 'D': KeyD, 839 | 'E': KeyE, 840 | 'F': KeyF, 841 | 'G': KeyG, 842 | 'H': KeyH, 843 | 'I': KeyI, 844 | 'J': KeyJ, 845 | 'K': KeyK, 846 | 'L': KeyL, 847 | 'M': KeyM, 848 | 'N': KeyN, 849 | 'O': KeyO, 850 | 'P': KeyP, 851 | 'Q': KeyQ, 852 | 'R': KeyR, 853 | 'S': KeyS, 854 | 'T': KeyT, 855 | 'U': KeyU, 856 | 'V': KeyV, 857 | 'W': KeyW, 858 | 'X': KeyX, 859 | 'Y': KeyY, 860 | 'Z': KeyZ, 861 | /* ALPHABET LOWERCASE */ 862 | 'a': KeyA, 863 | 'b': KeyB, 864 | 'c': KeyC, 865 | 'd': KeyD, 866 | 'e': KeyE, 867 | 'f': KeyF, 868 | 'g': KeyG, 869 | 'h': KeyH, 870 | 'i': KeyI, 871 | 'j': KeyJ, 872 | 'k': KeyK, 873 | 'l': KeyL, 874 | 'm': KeyM, 875 | 'n': KeyN, 876 | 'o': KeyO, 877 | 'p': KeyP, 878 | 'q': KeyQ, 879 | 'r': KeyR, 880 | 's': KeyS, 881 | 't': KeyT, 882 | 'u': KeyU, 883 | 'v': KeyV, 884 | 'w': KeyW, 885 | 'x': KeyX, 886 | 'y': KeyY, 887 | 'z': KeyZ 888 | }; 889 | 890 | const KEY_PUNCTUATION2ID = { 891 | /* PUNCTUATION KEYS */ 892 | '`': Backquote, 893 | '\\': Backslash, 894 | '[': BracketLeft, 895 | ']': BracketRight, 896 | ',': Comma, 897 | '=': Equal, 898 | '-': Minus, 899 | '.': Period, 900 | '\'': Quote, 901 | ';': Semicolon, 902 | '/': Slash, 903 | /* OTHER PUNCTUATION KEYS */ 904 | '&': Ampersand, 905 | '*': Asterisk, 906 | '@': At, 907 | '{': BraceLeft, 908 | '}': BraceRight, 909 | '^': Caret, 910 | ':': Colon, 911 | '$': Dollar, 912 | '"': DoubleQuote, 913 | '!': ExclamationMark, 914 | '>': GreaterThan, 915 | '#': Hash, 916 | '<': LessThan, 917 | '(': ParenthesisLeft, 918 | ')': ParenthesisRight, 919 | '%': Percent, 920 | '|': Pipe, 921 | '+': Plus, 922 | '?': QuestionMark, 923 | '~': Tilde, 924 | '_': Underscore 925 | }; 926 | 927 | const KEY_UNSUPPORTED2ID = { 928 | '§': Section, 929 | '±': PlusMinus 930 | }; 931 | 932 | const KEY2ID = { 933 | ...KEY_DIGITS2ID, 934 | ...KEY_ALPHABET2ID, 935 | ...KEY_PUNCTUATION2ID, 936 | ...KEY_UNSUPPORTED2ID 937 | }; 938 | 939 | const MOUSE2ID = { 940 | 0: Mouse0, 941 | 1: Mouse1, 942 | 2: Mouse2, 943 | 3: Mouse3, 944 | 4: Mouse4, 945 | 5: Mouse5, 946 | 6: Mouse6, 947 | 7: Mouse7, 948 | 8: Mouse8, 949 | 9: Mouse9 950 | }; 951 | 952 | const NAME2ID = { 953 | /* MODIFIERS */ 954 | altleft: AltLeft, 955 | altright: AltRight, 956 | alt: [AltLeft, AltRight], 957 | optionleft: AltLeft, 958 | optionright: AltRight, 959 | option: [AltLeft, AltRight], 960 | cmdleft: MetaLeft, 961 | cmdright: MetaRight, 962 | cmd: [MetaLeft, MetaRight], 963 | commandleft: MetaLeft, 964 | commandright: MetaRight, 965 | command: [MetaLeft, MetaRight], 966 | metaleft: MetaLeft, 967 | metaright: MetaRight, 968 | meta: [MetaLeft, MetaRight], 969 | ctrlleft: ControlLeft, 970 | ctrlright: ControlRight, 971 | ctrl: [ControlLeft, ControlRight], 972 | controlleft: ControlLeft, 973 | controlright: ControlRight, 974 | control: [ControlLeft, ControlRight], 975 | shiftleft: ShiftLeft, 976 | shiftright: ShiftRight, 977 | shift: [ShiftLeft, ShiftRight], 978 | /* VIRTUAL MODIFIERS */ 979 | cmdleftorctrlleft: isMac () ? MetaLeft : ControlLeft, 980 | cmdrightorctrlright: isMac () ? MetaRight : ControlRight, 981 | cmdorctrlleft: isMac () ? MetaLeft : ControlLeft, 982 | cmdorctrlright: isMac () ? MetaRight : ControlRight, 983 | cmdorctrl: isMac () ? [MetaLeft, MetaRight] : [ControlLeft, ControlRight], 984 | commandleftorcontrolleft: isMac () ? MetaLeft : ControlLeft, 985 | commandrightorcontrolright: isMac () ? MetaRight : ControlRight, 986 | commandorcontrolleft: isMac () ? MetaLeft : ControlLeft, 987 | commandorcontrolright: isMac () ? MetaRight : ControlRight, 988 | commandorcontrol: isMac () ? [MetaLeft, MetaRight] : [ControlLeft, ControlRight], 989 | /* MOUSE */ 990 | clickleft: Mouse0, 991 | clickmiddle: Mouse1, 992 | clickright: Mouse2, 993 | leftclick: Mouse0, 994 | middleclick: Mouse1, 995 | rightclick: Mouse2, 996 | leftmouse: Mouse0, 997 | middlemouse: Mouse1, 998 | rightmouse: Mouse2, 999 | mouseleft: Mouse0, 1000 | mousemiddle: Mouse1, 1001 | mouseright: Mouse2, 1002 | mouse0: Mouse0, 1003 | mouse1: Mouse1, 1004 | mouse2: Mouse2, 1005 | mouse3: Mouse3, 1006 | mouse4: Mouse4, 1007 | mouse5: Mouse5, 1008 | mouse6: Mouse6, 1009 | mouse7: Mouse7, 1010 | mouse8: Mouse8, 1011 | mouse9: Mouse9, 1012 | /* SPECIAL CHARACTERS */ 1013 | backspace: Backspace, 1014 | capslock: CapsLock, 1015 | del: Delete, 1016 | delete: Delete, 1017 | down: ArrowDown, 1018 | end: End, 1019 | enter: Enter, 1020 | return: Enter, 1021 | esc: Escape, 1022 | escape: Escape, 1023 | home: Home, 1024 | insert: Insert, 1025 | left: ArrowLeft, 1026 | pagedown: PageDown, 1027 | pageup: PageUp, 1028 | right: ArrowRight, 1029 | space: Space, 1030 | spacebar: Space, 1031 | tab: Tab, 1032 | up: ArrowUp, 1033 | /* DIGITS & ALPHABET & PUNCTUATION KEYS */ 1034 | ...KEY_DIGITS2ID, 1035 | ...KEY_ALPHABET2ID, 1036 | ...KEY_PUNCTUATION2ID, 1037 | plus: Plus, 1038 | /* FUNCTION KEYS */ 1039 | f1: F1, 1040 | f2: F2, 1041 | f3: F3, 1042 | f4: F4, 1043 | f5: F5, 1044 | f6: F6, 1045 | f7: F7, 1046 | f8: F8, 1047 | f9: F9, 1048 | f10: F10, 1049 | f11: F11, 1050 | f12: F12, 1051 | f13: F13, 1052 | f14: F14, 1053 | f15: F15, 1054 | f16: F16, 1055 | f17: F17, 1056 | f18: F18, 1057 | f19: F19, 1058 | f20: F20, 1059 | f21: F21, 1060 | f22: F22, 1061 | f23: F23, 1062 | f24: F24, 1063 | /* NUMPAD DIGITS */ 1064 | numpad0: Numpad0, 1065 | numpad1: Numpad1, 1066 | numpad2: Numpad2, 1067 | numpad3: Numpad3, 1068 | numpad4: Numpad4, 1069 | numpad5: Numpad5, 1070 | numpad6: Numpad6, 1071 | numpad7: Numpad7, 1072 | numpad8: Numpad8, 1073 | numpad9: Numpad9, 1074 | /* NUMPAD OPERATORS */ 1075 | numpadadd: NumpadAdd, 1076 | numpadcomma: NumpadComma, 1077 | numpaddecimal: NumpadDecimal, 1078 | numpaddivide: NumpadDivide, 1079 | numpadenter: NumpadEnter, 1080 | numpadequal: NumpadEqual, 1081 | numpadmultiply: NumpadMultiply, 1082 | numpadsubtract: NumpadSubtract, 1083 | /* OTHERS */ 1084 | numlock: NumLock, 1085 | scrolllock: ScrollLock 1086 | }; 1087 | 1088 | const NAME_FORMATTING2ID = { 1089 | /* REGULARS */ 1090 | ...NAME2ID, 1091 | /* VIRTUAL MODIFIERS */ 1092 | ctrl: Control, 1093 | control: Control, 1094 | cmd: Command, 1095 | command: Command, 1096 | alt: Alt, 1097 | shift: Shift, 1098 | cmdleftorctrlleft: CommandOrControlLeft, 1099 | cmdrightorctrlright: CommandOrControlRight, 1100 | cmdorctrlleft: CommandOrControlLeft, 1101 | cmdorctrlright: CommandOrControlRight, 1102 | cmdorctrl: CommandOrControl, 1103 | commandleftorcontrolleft: CommandOrControlLeft, 1104 | commandrightorcontrolright: CommandOrControlRight, 1105 | commandorcontrolleft: CommandOrControlLeft, 1106 | commandorcontrolright: CommandOrControlRight, 1107 | commandorcontrol: CommandOrControl 1108 | }; 1109 | 1110 | const WHICH2ID = { 1111 | /* DIGITS */ 1112 | 48: Digit0, 1113 | 49: Digit1, 1114 | 50: Digit2, 1115 | 51: Digit3, 1116 | 52: Digit4, 1117 | 53: Digit5, 1118 | 54: Digit6, 1119 | 55: Digit7, 1120 | 56: Digit8, 1121 | 57: Digit9, 1122 | /* ALPHABET UPPERCASE */ 1123 | 65: KeyA, 1124 | 66: KeyB, 1125 | 67: KeyC, 1126 | 68: KeyD, 1127 | 69: KeyE, 1128 | 70: KeyF, 1129 | 71: KeyG, 1130 | 72: KeyH, 1131 | 73: KeyI, 1132 | 74: KeyJ, 1133 | 75: KeyK, 1134 | 76: KeyL, 1135 | 77: KeyM, 1136 | 78: KeyN, 1137 | 79: KeyO, 1138 | 80: KeyP, 1139 | 81: KeyQ, 1140 | 82: KeyR, 1141 | 83: KeyS, 1142 | 84: KeyT, 1143 | 85: KeyU, 1144 | 86: KeyV, 1145 | 87: KeyW, 1146 | 88: KeyX, 1147 | 89: KeyY, 1148 | 90: KeyZ, 1149 | /* ALPHABET LOWERCASE */ 1150 | 97: KeyA, 1151 | 98: KeyB, 1152 | 99: KeyC, 1153 | 100: KeyD, 1154 | 101: KeyE, 1155 | 102: KeyF, 1156 | 103: KeyG, 1157 | 104: KeyH, 1158 | 105: KeyI, 1159 | 106: KeyJ, 1160 | 107: KeyK, 1161 | 108: KeyL, 1162 | 109: KeyM, 1163 | 110: KeyN, 1164 | 111: KeyO, 1165 | 112: KeyP, 1166 | 113: KeyQ, 1167 | 114: KeyR, 1168 | 115: KeyS, 1169 | 116: KeyT, 1170 | 117: KeyU, 1171 | 118: KeyV, 1172 | 119: KeyW, 1173 | 120: KeyX, 1174 | 121: KeyY, 1175 | 122: KeyZ, 1176 | /* PUNCTUATION KEYS */ 1177 | 96: Backquote, 1178 | 92: Backslash, 1179 | 91: BracketLeft, 1180 | 93: BracketRight, 1181 | 44: Comma, 1182 | 61: Equal, 1183 | 45: Minus, 1184 | 46: Period, 1185 | 39: Quote, 1186 | 59: Semicolon, 1187 | 47: Slash, 1188 | /* OTHER PUNCTUATION KEYS */ 1189 | 38: Ampersand, 1190 | 42: Asterisk, 1191 | 64: At, 1192 | 123: BraceLeft, 1193 | 125: BraceRight, 1194 | 94: Caret, 1195 | 58: Colon, 1196 | 36: Dollar, 1197 | 34: DoubleQuote, 1198 | 33: ExclamationMark, 1199 | 62: GreaterThan, 1200 | 35: Hash, 1201 | 60: LessThan, 1202 | 40: ParenthesisLeft, 1203 | 41: ParenthesisRight, 1204 | 37: Percent, 1205 | 124: Pipe, 1206 | 43: Plus, 1207 | 63: QuestionMark, 1208 | 126: Tilde, 1209 | 95: Underscore 1210 | }; 1211 | 1212 | /* EXPORT */ 1213 | 1214 | export {CODE2ID, CODE_RISKY2ID, ID2FORMAT_TITLE, ID2FORMAT_ELECTRON, ID2FORMAT_SYMBOL, KEY_DIGITS2ID, KEY_ALPHABET2ID, KEY_PUNCTUATION2ID, KEY_UNSUPPORTED2ID, KEY2ID, MOUSE2ID, NAME2ID, NAME_FORMATTING2ID, WHICH2ID}; 1215 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | type Checker = { 5 | ( event: Event ): boolean 6 | }; 7 | 8 | type Disposer = { 9 | (): void 10 | }; 11 | 12 | type Format = ( 13 | 'electron' | 14 | 'symbols' | 15 | 'long-flexible-directional' | 16 | 'long-flexible-nondirectional' | 17 | 'long-inflexible-directional' | 18 | 'long-inflexible-nondirectional' | 19 | 'short-flexible-directional' | 20 | 'short-flexible-nondirectional' | 21 | 'short-inflexible-directional' | 22 | 'short-inflexible-nondirectional' 23 | ); 24 | 25 | type Handler = { 26 | ( event?: Event ): boolean | void 27 | }; 28 | 29 | type ChordNode = { 30 | children: Partial>, 31 | handlers: HandlerNode // Pointing to the last item in the linked list 32 | }; 33 | 34 | type HandlerNode = { 35 | parent: ChordNode | undefined, 36 | prev: HandlerNode | undefined, 37 | next: HandlerNode | undefined, 38 | handler: Handler 39 | }; 40 | 41 | type HandlerOptions = { 42 | konami?: boolean 43 | }; 44 | 45 | type Options = { 46 | capture?: boolean, 47 | target?: Window | Document | HTMLElement | SVGElement | MathMLElement, 48 | shouldHandleEvent?: ( event: Event ) => boolean 49 | }; 50 | 51 | type RecordHandler = { 52 | ( shortcut: string ): void 53 | }; 54 | 55 | type RecordOptions = { 56 | limit?: number, 57 | passthrough?: boolean, 58 | shouldHandleEvent?: ( event: Event ) => boolean 59 | }; 60 | 61 | /* EXPORT */ 62 | 63 | export type {Checker, Disposer, Format, Handler, ChordNode, HandlerNode, HandlerOptions, Options, RecordHandler, RecordOptions}; 64 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | const attempt = ( fn: () => T, fallback: T ): T => { 5 | 6 | try { 7 | 8 | return fn (); 9 | 10 | } catch { 11 | 12 | return fallback; 13 | 14 | } 15 | 16 | }; 17 | 18 | const castArray = ( value: T | T[] ): T[] => { 19 | 20 | return isArray ( value ) ? value : [value]; 21 | 22 | }; 23 | 24 | const comparator = ( a: T, b: T ): -1 | 0 | 1 => { 25 | 26 | if ( a < b ) return -1; 27 | 28 | if ( a > b ) return 1; 29 | 30 | return 0; 31 | 32 | }; 33 | 34 | const decompose = ( value: bigint ): bigint[] => { // Returns all the primitive components of a binary bigint 35 | 36 | const components: bigint[] = []; 37 | 38 | for ( let i = 0n; value > 0n; i += 1n, value >>= 1n ) { 39 | 40 | if ( value & 1n ) { 41 | 42 | components.push ( 1n << i ); 43 | 44 | } 45 | 46 | } 47 | 48 | return components; 49 | 50 | }; 51 | 52 | const enumerate = ( parts: (bigint | bigint[])[] ): bigint[][] => { // Exhaustively listing all possible paths from the start to the end 53 | 54 | if ( parts.length === 0 ) { 55 | 56 | return []; 57 | 58 | } else if ( parts.length === 1 ) { 59 | 60 | const part = parts[0]; 61 | 62 | if ( isArray ( part ) ) { 63 | 64 | if ( part.length <= 1 ) { 65 | 66 | return [part]; 67 | 68 | } else { 69 | 70 | return part.map ( value => [value] ); 71 | 72 | } 73 | 74 | } else { 75 | 76 | return [[part]]; 77 | 78 | } 79 | 80 | } 81 | 82 | const normalized = parts.map ( castArray ); 83 | const heads = normalized[0]; 84 | const tails = enumerate ( normalized.slice ( 1 ) ); 85 | const values: bigint[][] = []; 86 | 87 | for ( const head of heads ) { 88 | 89 | if ( tails.length ) { 90 | 91 | for ( const tail of tails ) { 92 | 93 | values.push ([ head, ...tail ]); 94 | 95 | } 96 | 97 | } else { 98 | 99 | values.push ([ head ]); 100 | 101 | } 102 | 103 | } 104 | 105 | return values; 106 | 107 | }; 108 | 109 | const first = ( values: T[] ): T | undefined => { 110 | 111 | return values[0]; 112 | 113 | }; 114 | 115 | const isArray = ( value: unknown ): value is unknown[] => { 116 | 117 | return Array.isArray ( value ); 118 | 119 | }; 120 | 121 | const isKeyboardEvent = ( value: unknown ): value is KeyboardEvent => { 122 | 123 | return value instanceof KeyboardEvent; 124 | 125 | }; 126 | 127 | const isMac = ((): () => boolean => { 128 | 129 | const isMac = ((): boolean => { 130 | 131 | if ( typeof navigator === 'object' ) { 132 | 133 | return /mac|ipod|iphone|ipad/i.test ( navigator.platform ); 134 | 135 | } 136 | 137 | if ( typeof globalThis['process'] === 'object' ) { 138 | 139 | return globalThis['process']['platform'] === 'darwin'; 140 | 141 | } 142 | 143 | return false; 144 | 145 | })(); 146 | 147 | return () => isMac; 148 | 149 | })(); 150 | 151 | const isMouseEvent = ( value: unknown ): value is MouseEvent => { 152 | 153 | return value instanceof MouseEvent; 154 | 155 | }; 156 | 157 | const isString = ( value: unknown ): value is string => { 158 | 159 | return typeof value === 'string'; 160 | 161 | }; 162 | 163 | const isUndefined = ( value: unknown ): value is undefined => { 164 | 165 | return value === undefined; 166 | 167 | }; 168 | 169 | const last = ( values: T[] ): T | undefined => { 170 | 171 | return values[values.length - 1]; 172 | 173 | }; 174 | 175 | const memoize = ( fn: ( arg: T ) => R ): (( arg: T ) => R) => { 176 | 177 | const cache = new Map (); 178 | 179 | return ( arg: T ): R => { 180 | 181 | const resultCached = cache.get ( arg ); 182 | 183 | if ( !isUndefined ( resultCached ) || cache.has ( arg ) ) return resultCached; 184 | 185 | const result = fn ( arg ); 186 | 187 | cache.set ( arg, result ); 188 | 189 | return result; 190 | 191 | }; 192 | 193 | }; 194 | 195 | const nope = (): false => { 196 | 197 | return false; 198 | 199 | }; 200 | 201 | const or = ( values: bigint[] ): bigint => { 202 | 203 | return values.reduce ( ( acc, value ) => acc | value, 0n ); 204 | 205 | }; 206 | 207 | const orWith = ( values: bigint[], otherValues: bigint[] ): bigint[][] => { 208 | 209 | return otherValues.map ( other => { 210 | 211 | const head = values.slice ( 0, -1 ); 212 | const tail = last ( values ) || 0n; 213 | 214 | return [...head, tail | other]; 215 | 216 | }); 217 | 218 | }; 219 | 220 | const take = ( arr: T[], nr: number ): T[] => { 221 | 222 | return arr.slice ( 0, Math.max ( 0, nr ) ); 223 | 224 | }; 225 | 226 | const takeRight = ( arr: T[], nr: number ): T[] => { 227 | 228 | return arr.slice ( arr.length - Math.max ( 0, nr ), arr.length ); 229 | 230 | }; 231 | 232 | const uniq = ( values: T[] ): T[] => { 233 | 234 | if ( values.length <= 1 ) return values; 235 | 236 | return Array.from ( new Set ( values ) ); 237 | 238 | }; 239 | 240 | const without = ( values: T[], except: T ): T[] => { 241 | 242 | if ( !values.includes ( except ) ) return values; 243 | 244 | return values.filter ( value => value !== except ); 245 | 246 | }; 247 | 248 | const yep = (): true => { 249 | 250 | return true; 251 | 252 | }; 253 | 254 | /* EXPORT */ 255 | 256 | export {attempt, castArray, comparator, decompose, enumerate, first, isArray, isKeyboardEvent, isMac, isMouseEvent, isString, last, memoize, nope, or, orWith, take, takeRight, uniq, without, yep}; 257 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {describe} from 'fava'; 5 | import {NAME2ID} from '../dist/maps.js'; 6 | import ShoSho from '../dist/index.js'; 7 | 8 | /* MAIN */ 9 | 10 | describe ( 'ShoSho', () => { 11 | 12 | describe ( 'format', it => { 13 | 14 | it ( 'supports formatting shortcuts in different formats', t => { 15 | 16 | const tests = [ 17 | // Empty 18 | ['electron', '', ''], 19 | ['symbols', '', ''], 20 | ['long-flexible-directional', '', ''], 21 | ['long-flexible-nondirectional', '', ''], 22 | ['long-inflexible-directional', '', ''], 23 | ['long-inflexible-nondirectional', '', ''], 24 | ['short-flexible-directional', '', ''], 25 | ['short-flexible-nondirectional', '', ''], 26 | ['short-inflexible-directional', '', ''], 27 | ['short-inflexible-nondirectional', '', ''], 28 | // Control+A 29 | ['electron', 'Control+A', 'Ctrl+A'], 30 | ['symbols', 'Control+A', '⌃A'], 31 | ['long-flexible-directional', 'Control+A', 'CommandOrControl+A'], 32 | ['long-flexible-nondirectional', 'Control+A', 'CommandOrControl+A'], 33 | ['long-inflexible-directional', 'Control+A', 'Control+A'], 34 | ['long-inflexible-nondirectional', 'Control+A', 'Control+A'], 35 | ['short-flexible-directional', 'Control+A', 'CmdOrCtrl+A'], 36 | ['short-flexible-nondirectional', 'Control+A', 'CmdOrCtrl+A'], 37 | ['short-inflexible-directional', 'Control+A', 'Ctrl+A'], 38 | ['short-inflexible-nondirectional', 'Control+A', 'Ctrl+A'], 39 | // ControlLeft+A 40 | ['electron', 'ControlLeft+A', 'Ctrl+A'], 41 | ['symbols', 'ControlLeft+A', '⌃A'], 42 | ['long-flexible-directional', 'ControlLeft+A', 'CommandOrControlLeft+A'], 43 | ['long-flexible-nondirectional', 'ControlLeft+A', 'CommandOrControl+A'], 44 | ['long-inflexible-directional', 'ControlLeft+A', 'ControlLeft+A'], 45 | ['long-inflexible-nondirectional', 'ControlLeft+A', 'Control+A'], 46 | ['short-flexible-directional', 'ControlLeft+A', 'CmdOrCtrlLeft+A'], 47 | ['short-flexible-nondirectional', 'ControlLeft+A', 'CmdOrCtrl+A'], 48 | ['short-inflexible-directional', 'ControlLeft+A', 'CtrlLeft+A'], 49 | ['short-inflexible-nondirectional', 'ControlLeft+A', 'Ctrl+A'], 50 | // ControlRight+A 51 | ['electron', 'ControlRight+A', 'Ctrl+A'], 52 | ['symbols', 'ControlRight+A', '⌃A'], 53 | ['long-flexible-directional', 'ControlRight+A', 'CommandOrControlRight+A'], 54 | ['long-flexible-nondirectional', 'ControlRight+A', 'CommandOrControl+A'], 55 | ['long-inflexible-directional', 'ControlRight+A', 'ControlRight+A'], 56 | ['long-inflexible-nondirectional', 'ControlRight+A', 'Control+A'], 57 | ['short-flexible-directional', 'ControlRight+A', 'CmdOrCtrlRight+A'], 58 | ['short-flexible-nondirectional', 'ControlRight+A', 'CmdOrCtrl+A'], 59 | ['short-inflexible-directional', 'ControlRight+A', 'CtrlRight+A'], 60 | ['short-inflexible-nondirectional', 'ControlRight+A', 'Ctrl+A'], 61 | // CommandLeft+A 62 | ['electron', 'CommandLeft+A', 'Cmd+A'], 63 | ['symbols', 'CommandLeft+A', '⌘A'], 64 | ['long-flexible-directional', 'CommandLeft+A', 'CommandOrControlLeft+A'], 65 | ['long-flexible-nondirectional', 'CommandLeft+A', 'CommandOrControl+A'], 66 | ['long-inflexible-directional', 'CommandLeft+A', 'CommandLeft+A'], 67 | ['long-inflexible-nondirectional', 'CommandLeft+A', 'Command+A'], 68 | ['short-flexible-directional', 'CommandLeft+A', 'CmdOrCtrlLeft+A'], 69 | ['short-flexible-nondirectional', 'CommandLeft+A', 'CmdOrCtrl+A'], 70 | ['short-inflexible-directional', 'CommandLeft+A', 'CmdLeft+A'], 71 | ['short-inflexible-nondirectional', 'CommandLeft+A', 'Cmd+A'], 72 | // CommandRight+A 73 | ['electron', 'CommandRight+A', 'Cmd+A'], 74 | ['symbols', 'CommandRight+A', '⌘A'], 75 | ['long-flexible-directional', 'CommandRight+A', 'CommandOrControlRight+A'], 76 | ['long-flexible-nondirectional', 'CommandRight+A', 'CommandOrControl+A'], 77 | ['long-inflexible-directional', 'CommandRight+A', 'CommandRight+A'], 78 | ['long-inflexible-nondirectional', 'CommandRight+A', 'Command+A'], 79 | ['short-flexible-directional', 'CommandRight+A', 'CmdOrCtrlRight+A'], 80 | ['short-flexible-nondirectional', 'CommandRight+A', 'CmdOrCtrl+A'], 81 | ['short-inflexible-directional', 'CommandRight+A', 'CmdRight+A'], 82 | ['short-inflexible-nondirectional', 'CommandRight+A', 'Cmd+A'], 83 | // CommandOrControlLeft+A 84 | ['electron', 'CommandOrControlLeft+A', 'CmdOrCtrl+A'], 85 | ['symbols', 'CommandOrControlLeft+A', '⌘A'], 86 | ['long-flexible-directional', 'CommandOrControlLeft+A', 'CommandOrControlLeft+A'], 87 | ['long-flexible-nondirectional', 'CommandOrControlLeft+A', 'CommandOrControl+A'], 88 | ['long-inflexible-directional', 'CommandOrControlLeft+A', 'CommandLeft+A'], 89 | ['long-inflexible-nondirectional', 'CommandOrControlLeft+A', 'Command+A'], 90 | ['short-flexible-directional', 'CommandOrControlLeft+A', 'CmdOrCtrlLeft+A'], 91 | ['short-flexible-nondirectional', 'CommandOrControlLeft+A', 'CmdOrCtrl+A'], 92 | ['short-inflexible-directional', 'CommandOrControlLeft+A', 'CmdLeft+A'], 93 | ['short-inflexible-nondirectional', 'CommandOrControlLeft+A', 'Cmd+A'], 94 | // CommandRightOrControlRight+A 95 | ['electron', 'CmdRightOrCtrlRight+A', 'CmdOrCtrl+A'], 96 | ['symbols', 'CmdRightOrCtrlRight+A', '⌘A'], 97 | ['long-flexible-directional', 'CmdRightOrCtrlRight+A', 'CommandOrControlRight+A'], 98 | ['long-flexible-nondirectional', 'CmdRightOrCtrlRight+A', 'CommandOrControl+A'], 99 | ['long-inflexible-directional', 'CmdRightOrCtrlRight+A', 'CommandRight+A'], 100 | ['long-inflexible-nondirectional', 'CmdRightOrCtrlRight+A', 'Command+A'], 101 | ['short-flexible-directional', 'CmdRightOrCtrlRight+A', 'CmdOrCtrlRight+A'], 102 | ['short-flexible-nondirectional', 'CmdRightOrCtrlRight+A', 'CmdOrCtrl+A'], 103 | ['short-inflexible-directional', 'CmdRightOrCtrlRight+A', 'CmdRight+A'], 104 | ['short-inflexible-nondirectional', 'CmdRightOrCtrlRight+A', 'Cmd+A'], 105 | // Ctrl+Cmd+A Ctrl 106 | ['electron', 'Ctrl+Cmd+A Ctrl', 'Ctrl+Cmd+A Ctrl'], 107 | ['symbols', 'Ctrl+Cmd+A Ctrl', '⌃⌘A ⌃'], 108 | ['long-flexible-directional', 'Ctrl+Cmd+A Ctrl', 'Control+Command+A CommandOrControl'], 109 | ['long-flexible-nondirectional', 'Ctrl+Cmd+A Ctrl', 'Control+Command+A CommandOrControl'], 110 | ['long-inflexible-directional', 'Ctrl+Cmd+A Ctrl', 'Control+Command+A Control'], 111 | ['long-inflexible-nondirectional', 'Ctrl+Cmd+A Ctrl', 'Control+Command+A Control'], 112 | ['short-flexible-directional', 'Ctrl+Cmd+A Ctrl', 'Ctrl+Cmd+A CmdOrCtrl'], 113 | ['short-flexible-nondirectional', 'Ctrl+Cmd+A Ctrl', 'Ctrl+Cmd+A CmdOrCtrl'], 114 | ['short-inflexible-directional', 'Ctrl+Cmd+A Ctrl', 'Ctrl+Cmd+A Ctrl'], 115 | ['short-inflexible-nondirectional', 'Ctrl+Cmd+A Ctrl', 'Ctrl+Cmd+A Ctrl'], 116 | // Cmd+Ctrl+A Cmd 117 | ['electron', 'Cmd+Ctrl+A Cmd', 'Ctrl+Cmd+A Cmd'], 118 | ['symbols', 'Cmd+Ctrl+A Cmd', '⌃⌘A ⌘'], 119 | ['long-flexible-directional', 'Cmd+Ctrl+A Cmd', 'Control+Command+A CommandOrControl'], 120 | ['long-flexible-nondirectional', 'Cmd+Ctrl+A Cmd', 'Control+Command+A CommandOrControl'], 121 | ['long-inflexible-directional', 'Cmd+Ctrl+A Cmd', 'Control+Command+A Command'], 122 | ['long-inflexible-nondirectional', 'Cmd+Ctrl+A Cmd', 'Control+Command+A Command'], 123 | ['short-flexible-directional', 'Cmd+Ctrl+A Cmd', 'Ctrl+Cmd+A CmdOrCtrl'], 124 | ['short-flexible-nondirectional', 'Cmd+Ctrl+A Cmd', 'Ctrl+Cmd+A CmdOrCtrl'], 125 | ['short-inflexible-directional', 'Cmd+Ctrl+A Cmd', 'Ctrl+Cmd+A Cmd'], 126 | ['short-inflexible-nondirectional', 'Cmd+Ctrl+A Cmd', 'Ctrl+Cmd+A Cmd'], 127 | // CmdLeft+CmdRight 128 | ['electron', 'CmdLeft+CmdRight', 'Cmd+Cmd'], 129 | ['symbols', 'CmdLeft+CmdRight', '⌘⌘'], 130 | ['long-flexible-directional', 'CmdLeft+CmdRight', 'CommandOrControlLeft+CommandOrControlRight'], 131 | ['long-flexible-nondirectional', 'CmdLeft+CmdRight', 'CommandOrControlLeft+CommandOrControlRight'], 132 | ['long-inflexible-directional', 'CmdLeft+CmdRight', 'CommandLeft+CommandRight'], 133 | ['long-inflexible-nondirectional', 'CmdLeft+CmdRight', 'CommandLeft+CommandRight'], 134 | ['short-flexible-directional', 'CmdLeft+CmdRight', 'CmdOrCtrlLeft+CmdOrCtrlRight'], 135 | ['short-flexible-nondirectional', 'CmdLeft+CmdRight', 'CmdOrCtrlLeft+CmdOrCtrlRight'], 136 | ['short-inflexible-directional', 'CmdLeft+CmdRight', 'CmdLeft+CmdRight'], 137 | ['short-inflexible-nondirectional', 'CmdLeft+CmdRight', 'CmdLeft+CmdRight'], 138 | // CmdLeft+ShiftRight 139 | ['electron', 'CmdLeft+ShiftRight', 'Cmd+Shift'], 140 | ['symbols', 'CmdLeft+ShiftRight', '⌘⇧'], 141 | ['long-flexible-directional', 'CmdLeft+ShiftRight', 'CommandOrControlLeft+ShiftRight'], 142 | ['long-flexible-nondirectional', 'CmdLeft+ShiftRight', 'CommandOrControl+Shift'], 143 | ['long-inflexible-directional', 'CmdLeft+ShiftRight', 'CommandLeft+ShiftRight'], 144 | ['long-inflexible-nondirectional', 'CmdLeft+ShiftRight', 'Command+Shift'], 145 | ['short-flexible-directional', 'CmdLeft+ShiftRight', 'CmdOrCtrlLeft+ShiftRight'], 146 | ['short-flexible-nondirectional', 'CmdLeft+ShiftRight', 'CmdOrCtrl+Shift'], 147 | ['short-inflexible-directional', 'CmdLeft+ShiftRight', 'CmdLeft+ShiftRight'], 148 | ['short-inflexible-nondirectional', 'CmdLeft+ShiftRight', 'Cmd+Shift'], 149 | // Left+Right modifiers 150 | ['short-inflexible-nondirectional', 'CtrlLeft+CtrlRight', 'CtrlLeft+CtrlRight'], 151 | ['short-inflexible-nondirectional', 'CmdLeft+CtrlRight', 'Ctrl+Cmd'], 152 | ['short-inflexible-nondirectional', 'AltLeft+CtrlRight', 'Ctrl+Alt'], 153 | ['short-inflexible-nondirectional', 'ShiftLeft+CtrlRight', 'Ctrl+Shift'], 154 | ['short-inflexible-nondirectional', 'CtrlLeft+CmdRight', 'Ctrl+Cmd'], 155 | ['short-inflexible-nondirectional', 'CmdLeft+CmdRight', 'CmdLeft+CmdRight'], 156 | ['short-inflexible-nondirectional', 'AltLeft+CmdRight', 'Cmd+Alt'], 157 | ['short-inflexible-nondirectional', 'ShiftLeft+CmdRight', 'Cmd+Shift'], 158 | ['short-inflexible-nondirectional', 'CtrlLeft+AltRight', 'Ctrl+Alt'], 159 | ['short-inflexible-nondirectional', 'CmdLeft+AltRight', 'Cmd+Alt'], 160 | ['short-inflexible-nondirectional', 'AltLeft+AltRight', 'AltLeft+AltRight'], 161 | ['short-inflexible-nondirectional', 'ShiftLeft+AltRight', 'Alt+Shift'], 162 | ['short-inflexible-nondirectional', 'CtrlLeft+ShiftRight', 'Ctrl+Shift'], 163 | ['short-inflexible-nondirectional', 'CmdLeft+ShiftRight', 'Cmd+Shift'], 164 | ['short-inflexible-nondirectional', 'AltLeft+ShiftRight', 'Alt+Shift'], 165 | ['short-inflexible-nondirectional', 'ShiftLeft+ShiftRight', 'ShiftLeft+ShiftRight'], 166 | // Right+Left modifiers 167 | ['short-inflexible-nondirectional', 'CtrlRight+CtrlLeft', 'CtrlLeft+CtrlRight'], 168 | ['short-inflexible-nondirectional', 'CmdRight+CtrlLeft', 'Ctrl+Cmd'], 169 | ['short-inflexible-nondirectional', 'AltRight+CtrlLeft', 'Ctrl+Alt'], 170 | ['short-inflexible-nondirectional', 'ShiftRight+CtrlLeft', 'Ctrl+Shift'], 171 | ['short-inflexible-nondirectional', 'CtrlRight+CmdLeft', 'Ctrl+Cmd'], 172 | ['short-inflexible-nondirectional', 'CmdRight+CmdLeft', 'CmdLeft+CmdRight'], 173 | ['short-inflexible-nondirectional', 'AltRight+CmdLeft', 'Cmd+Alt'], 174 | ['short-inflexible-nondirectional', 'ShiftRight+CmdLeft', 'Cmd+Shift'], 175 | ['short-inflexible-nondirectional', 'CtrlRight+AltLeft', 'Ctrl+Alt'], 176 | ['short-inflexible-nondirectional', 'CmdRight+AltLeft', 'Cmd+Alt'], 177 | ['short-inflexible-nondirectional', 'AltRight+AltLeft', 'AltLeft+AltRight'], 178 | ['short-inflexible-nondirectional', 'ShiftRight+AltLeft', 'Alt+Shift'], 179 | ['short-inflexible-nondirectional', 'CtrlRight+ShiftLeft', 'Ctrl+Shift'], 180 | ['short-inflexible-nondirectional', 'CmdRight+ShiftLeft', 'Cmd+Shift'], 181 | ['short-inflexible-nondirectional', 'AltRight+ShiftLeft', 'Alt+Shift'], 182 | ['short-inflexible-nondirectional', 'ShiftRight+ShiftLeft', 'ShiftLeft+ShiftRight'], 183 | // Others... 184 | ['electron', 'Alt+Cmd+Ctrl+Shift', 'Ctrl+Cmd+Alt+Shift'], 185 | ['electron', 'Shift+Ctrl+Cmd+Alt', 'Ctrl+Cmd+Alt+Shift'], 186 | ['short-inflexible-nondirectional', 'CommandOrControlLeft+CommandOrControlRight', 'CmdLeft+CmdRight'], 187 | ['electron', '+', '+'] 188 | ]; 189 | 190 | for ( const [format, input, output] of tests ) { 191 | 192 | t.is ( ShoSho.format ( input, format ), output ); 193 | 194 | } 195 | 196 | }); 197 | 198 | }); 199 | 200 | describe ( 'isShortcut', it => { 201 | 202 | it ( 'supports detecting valid shortcuts', t => { 203 | 204 | for ( const key in NAME2ID ) { 205 | 206 | t.true ( ShoSho.isShortcut ( key ) ); 207 | t.true ( ShoSho.isShortcut ( key.toUpperCase () ) ); 208 | t.true ( ShoSho.isShortcut ( key.toLowerCase () ) ); 209 | 210 | } 211 | 212 | for ( const key in NAME2ID ) { 213 | 214 | t.false ( ShoSho.isShortcut ( `${key}${key}` ) ); 215 | 216 | } 217 | 218 | t.true ( ShoSho.isShortcut ( 'Cmd' ) ); 219 | t.true ( ShoSho.isShortcut ( 'Command' ) ); 220 | t.true ( ShoSho.isShortcut ( 'Command+A' ) ); 221 | t.true ( ShoSho.isShortcut ( 'Command A' ) ); 222 | t.true ( ShoSho.isShortcut ( 'Command+A B' ) ); 223 | t.true ( ShoSho.isShortcut ( 'CommandLeft+A' ) ); 224 | t.true ( ShoSho.isShortcut ( 'CommandRight+A' ) ); 225 | t.true ( ShoSho.isShortcut ( '+' ) ); 226 | 227 | t.false ( ShoSho.isShortcut ( 'Comma' ) ); 228 | t.false ( ShoSho.isShortcut ( 'AB' ) ); 229 | t.false ( ShoSho.isShortcut ( 'CommandA' ) ); 230 | 231 | }); 232 | 233 | }); 234 | 235 | }); 236 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsex/tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false 5 | } 6 | } 7 | --------------------------------------------------------------------------------