├── .editorconfig ├── .gitignore ├── example.js ├── index.js ├── license.md ├── package.json └── readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | # Use tabs in JavaScript and JSON. 11 | [**.{js, json}] 12 | indent_style = tab 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | .nvm-version 5 | node_modules 6 | npm-debug.log 7 | pnpm-debug.log 8 | 9 | package-lock.json 10 | shrinkwrap.yaml 11 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const listen = require('.') 4 | 5 | const stopListening = listen(process.stdin, (key) => { 6 | if (key.ctrl && key.name === 'c') { 7 | stopListening() // stop on Ctrl + C 8 | } else console.log(key) 9 | }) 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const StringDecoder = require('string_decoder').StringDecoder 4 | 5 | // Stolen from the `keypress` module. 6 | // https://github.com/TooTallNate/keypress/blob/476a519/index.js#L174-L408 7 | const parse = (s, enc) => { 8 | var ch, parts, key = { 9 | name: undefined 10 | , ctrl: false 11 | , meta: false 12 | , shift: false 13 | , sequence: s 14 | } 15 | 16 | if (Buffer.isBuffer(s)) { 17 | if (s[0] > 127 && s[1] === undefined) { 18 | s[0] -= 128; 19 | s = '\x1b' + s.toString(enc || 'utf-8') 20 | } else s = s.toString(enc || 'utf-8') 21 | } 22 | 23 | if (s === '\r') { // carriage return 24 | key.name = 'return' 25 | } else if (s === '\n') { // enter, should have been called linefeed 26 | key.name = 'enter' 27 | } else if (s === '\t') { // tab 28 | key.name = 'tab' 29 | } else if (s === '\b' || s === '\x7f' || 30 | s === '\x1b\x7f' || s === '\x1b\b') { // backspace or ctrl+h 31 | key.name = 'backspace' 32 | key.meta = (s.charAt(0) === '\x1b') 33 | } else if (s === '\x1b' || s === '\x1b\x1b') { // escape key 34 | key.name = 'escape' 35 | key.meta = (s.length === 2) 36 | } else if (s === ' ' || s === '\x1b ') { // space 37 | key.name = 'space'; 38 | key.meta = (s.length === 2); 39 | } else if (s <= '\x1a') { // ctrl+letter 40 | key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1) 41 | key.ctrl = true 42 | } else if (s.length === 1 && s >= 'a' && s <= 'z') { // lowercase letter 43 | key.name = s 44 | } else if (s.length === 1 && s >= 'A' && s <= 'Z') { // shift+letter 45 | key.name = s.toLowerCase() 46 | key.shift = true 47 | } else if (parts = metaKeyCodeRe.exec(s)) { // meta+character key 48 | key.name = parts[1].toLowerCase() 49 | key.meta = true 50 | key.shift = /^[A-Z]$/.test(parts[1]) 51 | } else if (parts = functionKeyCodeRe.exec(s)) { // ansi escape sequence 52 | // reassemble the key code leaving out leading \x1b's, 53 | // the modifier key bitflag and any meaningless "1;" sequence 54 | let code = (parts[1] || '') + (parts[2] || '') + (parts[4] || '') + (parts[6] || '') 55 | let modifier = (parts[3] || parts[5] || 1) - 1 56 | 57 | // Parse the key modifier 58 | key.ctrl = !!(modifier & 4) 59 | key.meta = !!(modifier & 10) 60 | key.shift = !!(modifier & 1) 61 | key.code = code 62 | 63 | // Parse the key itself 64 | switch (code) { 65 | /* xterm/gnome ESC O letter */ 66 | case 'OP': key.name = 'f1'; break 67 | case 'OQ': key.name = 'f2'; break 68 | case 'OR': key.name = 'f3'; break 69 | case 'OS': key.name = 'f4'; break 70 | 71 | /* xterm/rxvt ESC [ number ~ */ 72 | case '[11~': key.name = 'f1'; break 73 | case '[12~': key.name = 'f2'; break 74 | case '[13~': key.name = 'f3'; break 75 | case '[14~': key.name = 'f4'; break 76 | 77 | /* from Cygwin and used in libuv */ 78 | case '[[A': key.name = 'f1'; break 79 | case '[[B': key.name = 'f2'; break 80 | case '[[C': key.name = 'f3'; break 81 | case '[[D': key.name = 'f4'; break 82 | case '[[E': key.name = 'f5'; break 83 | 84 | /* common */ 85 | case '[15~': key.name = 'f5'; break 86 | case '[17~': key.name = 'f6'; break 87 | case '[18~': key.name = 'f7'; break 88 | case '[19~': key.name = 'f8'; break 89 | case '[20~': key.name = 'f9'; break 90 | case '[21~': key.name = 'f10'; break 91 | case '[23~': key.name = 'f11'; break 92 | case '[24~': key.name = 'f12'; break 93 | 94 | /* xterm ESC [ letter */ 95 | case '[A': key.name = 'up'; break 96 | case '[B': key.name = 'down'; break 97 | case '[C': key.name = 'right'; break 98 | case '[D': key.name = 'left'; break 99 | case '[E': key.name = 'clear'; break 100 | case '[F': key.name = 'end'; break 101 | case '[H': key.name = 'home'; break 102 | 103 | /* xterm/gnome ESC O letter */ 104 | case 'OA': key.name = 'up'; break 105 | case 'OB': key.name = 'down'; break 106 | case 'OC': key.name = 'right'; break 107 | case 'OD': key.name = 'left'; break 108 | case 'OE': key.name = 'clear'; break 109 | case 'OF': key.name = 'end'; break 110 | case 'OH': key.name = 'home'; break 111 | 112 | /* xterm/rxvt ESC [ number ~ */ 113 | case '[1~': key.name = 'home'; break 114 | case '[2~': key.name = 'insert'; break 115 | case '[3~': key.name = 'delete'; break 116 | case '[4~': key.name = 'end'; break 117 | case '[5~': key.name = 'pageup'; break 118 | case '[6~': key.name = 'pagedown'; break 119 | 120 | /* putty */ 121 | case '[[5~': key.name = 'pageup'; break 122 | case '[[6~': key.name = 'pagedown'; break 123 | 124 | /* rxvt */ 125 | case '[7~': key.name = 'home'; break 126 | case '[8~': key.name = 'end'; break 127 | 128 | /* rxvt keys with modifiers */ 129 | case '[a': key.name = 'up'; key.shift = true; break 130 | case '[b': key.name = 'down'; key.shift = true; break 131 | case '[c': key.name = 'right'; key.shift = true; break 132 | case '[d': key.name = 'left'; key.shift = true; break 133 | case '[e': key.name = 'clear'; key.shift = true; break 134 | 135 | case '[2$': key.name = 'insert'; key.shift = true; break 136 | case '[3$': key.name = 'delete'; key.shift = true; break 137 | case '[5$': key.name = 'pageup'; key.shift = true; break 138 | case '[6$': key.name = 'pagedown'; key.shift = true; break 139 | case '[7$': key.name = 'home'; key.shift = true; break 140 | case '[8$': key.name = 'end'; key.shift = true; break 141 | 142 | case 'Oa': key.name = 'up'; key.ctrl = true; break 143 | case 'Ob': key.name = 'down'; key.ctrl = true; break 144 | case 'Oc': key.name = 'right'; key.ctrl = true; break 145 | case 'Od': key.name = 'left'; key.ctrl = true; break 146 | case 'Oe': key.name = 'clear'; key.ctrl = true; break 147 | 148 | case '[2^': key.name = 'insert'; key.ctrl = true; break 149 | case '[3^': key.name = 'delete'; key.ctrl = true; break 150 | case '[5^': key.name = 'pageup'; key.ctrl = true; break 151 | case '[6^': key.name = 'pagedown'; key.ctrl = true; break 152 | case '[7^': key.name = 'home'; key.ctrl = true; break 153 | case '[8^': key.name = 'end'; key.ctrl = true; break 154 | 155 | /* misc. */ 156 | case '[Z': key.name = 'tab'; key.shift = true; break 157 | default: key.name = 'undefined'; break 158 | } 159 | } else if (s.length > 1 && s[0] !== '\x1b') { 160 | // Got a longer-than-one string of characters. 161 | // Probably a paste, since it wasn't a control sequence. 162 | return Array.prototype.map.call(s, (c) => parse(c, enc)) 163 | } 164 | 165 | if (s.length === 1) ch = s 166 | key.raw = s 167 | return key 168 | } 169 | 170 | // Regexes used for ansi escape code splitting 171 | const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/ 172 | const functionKeyCodeRe = 173 | /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/ 174 | 175 | const listenForKeys = (stream, cb) => { 176 | if (!stream || 'boolean' !== typeof stream.isRaw) { 177 | throw new Error('Invalid stream passed.') 178 | } 179 | if ('function' !== typeof cb) { 180 | throw new Error('Invalid callback passed.') 181 | } 182 | 183 | const decoder = new StringDecoder('utf8') 184 | const onData = (raw) => { 185 | const data = decoder.write(raw) 186 | const keys = parse(data, stream.encoding) 187 | 188 | if (Array.isArray(keys)) { 189 | for (let key of keys) cb(key) 190 | } else cb(keys) 191 | } 192 | 193 | const oldRawMode = stream.isRaw 194 | stream.setRawMode(true) 195 | stream.on('data', onData) 196 | stream.resume() 197 | 198 | const stopListening = () => { 199 | stream.setRawMode(oldRawMode) 200 | stream.pause() 201 | stream.removeListener('data', onData) 202 | } 203 | return stopListening 204 | } 205 | 206 | listenForKeys.parse = parse 207 | module.exports = listenForKeys 208 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Jannis R 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@derhuerst/cli-on-key", 3 | "description": "Read command line key presses from process.stdin.", 4 | "version": "0.1.0", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "keywords": [ 10 | "command line", 11 | "cli", 12 | "key", 13 | "keypress", 14 | "stdin", 15 | "event" 16 | ], 17 | "author": "Jannis R ", 18 | "homepage": "https://github.com/derhuerst/cli-on-key", 19 | "repository": "derhuerst/cli-on-key", 20 | "bugs": "https://github.com/derhuerst/cli-on-key/issues", 21 | "license": "ISC", 22 | "engines": { 23 | "node": ">=6" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cli-on-key 2 | 3 | **Read command line key presses from process.stdin.** Based on [`TooTallNate/keypress`](https://github.com/TooTallNate/keypress). 4 | 5 | [![npm version](https://img.shields.io/npm/v/cli-on-key.svg)](https://www.npmjs.com/package/derhuerst/cli-on-key) 6 | ![ISC-licensed](https://img.shields.io/github/license/derhuerst/cli-on-key.svg) 7 | [![chat with me on Gitter](https://img.shields.io/badge/chat%20with%20me-on%20gitter-512e92.svg)](https://gitter.im/derhuerst) 8 | [![support me on Patreon](https://img.shields.io/badge/support%20me-on%20patreon-fa7664.svg)](https://patreon.com/derhuerst) 9 | 10 | 11 | ## Installing 12 | 13 | ```shell 14 | npm install @derhuerst/cli-on-key 15 | ``` 16 | 17 | 18 | ## Usage 19 | 20 | `listen` will switch the stream to [raw mode](https://nodejs.org/api/tty.html#tty_readstream_israw), `stopListening` will revert to the previous value. 21 | 22 | ```js 23 | const listen = require('cli-on-key') 24 | 25 | const stopListening = listen(process.stdin, (key) => { 26 | if (key.ctrl && key.name === 'c') { 27 | stopListening() // stop on Ctrl + C 28 | } else console.log(key) 29 | }) 30 | ``` 31 | 32 | 33 | ## Contributing 34 | 35 | If you have a question or have difficulties using `cli-on-key`, please double-check your code and setup first. If you think you have found a bug or want to propose a feature, refer to [the issues page](https://github.com/derhuerst/cli-on-key/issues). 36 | --------------------------------------------------------------------------------