/** * Module dependencies. */ var EventEmitter = require('events').EventEmitter; /** * Module exports. */ var exports = module.exports = keypress; /** * This module offers the internal "keypress" functionality from node-core's * `readline` module, for your own programs and modules to use. * * The `keypress` function accepts a readable Stream instance and makes it * emit "keypress" events. * * Usage: * * ``` js * require('keypress')(process.stdin); * * process.stdin.on('keypress', function (ch, key) { * console.log(ch, key); * if (key.ctrl && key.name == 'c') { * process.stdin.pause(); * } * }); * proces.stdin.resume(); * ``` * * @param {Stream} stream * @api public */ function keypress(stream) { if (isEmittingKeypress(stream)) return; var StringDecoder = require('string_decoder').StringDecoder; // lazy load stream._keypressDecoder = new StringDecoder('utf8'); function onData(b) { if (listenerCount(stream, 'keypress') > 0) { var r = stream._keypressDecoder.write(b); if (r) emitKey(stream, r); } else { // Nobody's watching anyway stream.removeListener('data', onData); stream.on('newListener', onNewListener); } } function onNewListener(event) { if (event == 'keypress') { stream.on('data', onData); stream.removeListener('newListener', onNewListener); } } if (listenerCount(stream, 'keypress') > 0) { stream.on('data', onData); } else { stream.on('newListener', onNewListener); } } /** * Returns `true` if the stream is already emitting "keypress" events. * `false` otherwise. * * @param {Stream} stream readable stream * @return {Boolean} `true` if the stream is emitting "keypress" events * @api private */ function isEmittingKeypress(stream) { var rtn = !!stream._keypressDecoder; if (!rtn) { // XXX: for older versions of node (v0.6.x, v0.8.x) we want to remove the // existing "data" and "newListener" keypress events since they won't include // this `keypress` module extensions (like "mousepress" events). stream.listeners('data').slice(0).forEach(function(l) { if (l.name == 'onData' && /emitKey/.test(l.toString())) { stream.removeListener('data', l); } }); stream.listeners('newListener').slice(0).forEach(function(l) { if (l.name == 'onNewListener' && /keypress/.test(l.toString())) { stream.removeListener('newListener', l); } }); } return rtn; } /** * Enables "mousepress" events on the *input* stream. Note that `stream` must be * an *output* stream (i.e. a Writable Stream instance), usually `process.stdout`. * * @param {Stream} stream writable stream instance * @api public */ exports.enableMouse = function (stream) { stream.write('\x1b[?1000h'); }; /** * Disables "mousepress" events from being sent to the *input* stream. * Note that `stream` must be an *output* stream (i.e. a Writable Stream instance), * usually `process.stdout`. * * @param {Stream} stream writable stream instance * @api public */ exports.disableMouse = function (stream) { stream.write('\x1b[?1000l'); }; /** * `EventEmitter.listenerCount()` polyfill, for backwards compat. * * @param {Emitter} emitter event emitter instance * @param {String} event event name * @return {Number} number of listeners for `event` * @api public */ var listenerCount = EventEmitter.listenerCount; if (!listenerCount) { listenerCount = function(emitter, event) { return emitter.listeners(event).length; }; } /////////////////////////////////////////////////////////////////////// // Below this function is code from node-core's `readline.js` module // /////////////////////////////////////////////////////////////////////// /* Some patterns seen in terminal key escape codes, derived from combos seen at http://www.midnight-commander.org/browser/lib/tty/key.c ESC letter ESC [ letter ESC [ modifier letter ESC [ 1 ; modifier letter ESC [ num char ESC [ num ; modifier char ESC O letter ESC O modifier letter ESC O 1 ; modifier letter ESC N letter ESC [ [ num ; modifier char ESC [ [ 1 ; modifier letter ESC ESC [ num char ESC ESC O letter - char is usually ~ but $ and ^ also happen with rxvt - modifier is 1 + (shift * 1) + (left_alt * 2) + (ctrl * 4) + (right_alt * 8) - two leading ESCs apparently mean the same as one leading ESC */ // Regexes used for ansi escape code splitting var metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/; var functionKeyCodeRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; function emitKey(stream, s) { var ch, key = { name: undefined, ctrl: false, meta: false, shift: false }, parts; if (Buffer.isBuffer(s)) { if (s[0] > 127 && s[1] === undefined) { s[0] -= 128; s = '\x1b' + s.toString(stream.encoding || 'utf-8'); } else { s = s.toString(stream.encoding || 'utf-8'); } } key.sequence = s; if (s === '\r') { // carriage return key.name = 'return'; } else if (s === '\n') { // enter, should have been called linefeed key.name = 'enter'; } else if (s === '\t') { // tab key.name = 'tab'; } else if (s === '\b' || s === '\x7f' || s === '\x1b\x7f' || s === '\x1b\b') { // backspace or ctrl+h key.name = 'backspace'; key.meta = (s.charAt(0) === '\x1b'); } else if (s === '\x1b' || s === '\x1b\x1b') { // escape key key.name = 'escape'; key.meta = (s.length === 2); } else if (s === ' ' || s === '\x1b ') { key.name = 'space'; key.meta = (s.length === 2); } else if (s <= '\x1a') { // ctrl+letter key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); key.ctrl = true; } else if (s.length === 1 && s >= 'a' && s <= 'z') { // lowercase letter key.name = s; } else if (s.length === 1 && s >= 'A' && s <= 'Z') { // shift+letter key.name = s.toLowerCase(); key.shift = true; } else if (parts = metaKeyCodeRe.exec(s)) { // meta+character key key.name = parts[1].toLowerCase(); key.meta = true; key.shift = /^[A-Z]$/.test(parts[1]); } else if (parts = functionKeyCodeRe.exec(s)) { // ansi escape sequence // reassemble the key code leaving out leading \x1b's, // the modifier key bitflag and any meaningless "1;" sequence var code = (parts[1] || '') + (parts[2] || '') + (parts[4] || '') + (parts[6] || ''), modifier = (parts[3] || parts[5] || 1) - 1; // Parse the key modifier key.ctrl = !!(modifier & 4); key.meta = !!(modifier & 10); key.shift = !!(modifier & 1); key.code = code; // Parse the key itself switch (code) { /* xterm/gnome ESC O letter */ case 'OP': key.name = 'f1'; break; case 'OQ': key.name = 'f2'; break; case 'OR': key.name = 'f3'; break; case 'OS': key.name = 'f4'; break; /* xterm/rxvt ESC [ number ~ */ case '[11~': key.name = 'f1'; break; case '[12~': key.name = 'f2'; break; case '[13~': key.name = 'f3'; break; case '[14~': key.name = 'f4'; break; /* from Cygwin and used in libuv */ case '[[A': key.name = 'f1'; break; case '[[B': key.name = 'f2'; break; case '[[C': key.name = 'f3'; break; case '[[D': key.name = 'f4'; break; case '[[E': key.name = 'f5'; break; /* common */ case '[15~': key.name = 'f5'; break; case '[17~': key.name = 'f6'; break; case '[18~': key.name = 'f7'; break; case '[19~': key.name = 'f8'; break; case '[20~': key.name = 'f9'; break; case '[21~': key.name = 'f10'; break; case '[23~': key.name = 'f11'; break; case '[24~': key.name = 'f12'; break; /* xterm ESC [ letter */ case '[A': key.name = 'up'; break; case '[B': key.name = 'down'; break; case '[C': key.name = 'right'; break; case '[D': key.name = 'left'; break; case '[E': key.name = 'clear'; break; case '[F': key.name = 'end'; break; case '[H': key.name = 'home'; break; /* xterm/gnome ESC O letter */ case 'OA': key.name = 'up'; break; case 'OB': key.name = 'down'; break; case 'OC': key.name = 'right'; break; case 'OD': key.name = 'left'; break; case 'OE': key.name = 'clear'; break; case 'OF': key.name = 'end'; break; case 'OH': key.name = 'home'; break; /* xterm/rxvt ESC [ number ~ */ case '[1~': key.name = 'home'; break; case '[2~': key.name = 'insert'; break; case '[3~': key.name = 'delete'; break; case '[4~': key.name = 'end'; break; case '[5~': key.name = 'pageup'; break; case '[6~': key.name = 'pagedown'; break; /* putty */ case '[[5~': key.name = 'pageup'; break; case '[[6~': key.name = 'pagedown'; break; /* rxvt */ case '[7~': key.name = 'home'; break; case '[8~': key.name = 'end'; break; /* rxvt keys with modifiers */ case '[a': key.name = 'up'; key.shift = true; break; case '[b': key.name = 'down'; key.shift = true; break; case '[c': key.name = 'right'; key.shift = true; break; case '[d': key.name = 'left'; key.shift = true; break; case '[e': key.name = 'clear'; key.shift = true; break; case '[2$': key.name = 'insert'; key.shift = true; break; case '[3$': key.name = 'delete'; key.shift = true; break; case '[5$': key.name = 'pageup'; key.shift = true; break; case '[6$': key.name = 'pagedown'; key.shift = true; break; case '[7$': key.name = 'home'; key.shift = true; break; case '[8$': key.name = 'end'; key.shift = true; break; case 'Oa': key.name = 'up'; key.ctrl = true; break; case 'Ob': key.name = 'down'; key.ctrl = true; break; case 'Oc': key.name = 'right'; key.ctrl = true; break; case 'Od': key.name = 'left'; key.ctrl = true; break; case 'Oe': key.name = 'clear'; key.ctrl = true; break; case '[2^': key.name = 'insert'; key.ctrl = true; break; case '[3^': key.name = 'delete'; key.ctrl = true; break; case '[5^': key.name = 'pageup'; key.ctrl = true; break; case '[6^': key.name = 'pagedown'; key.ctrl = true; break; case '[7^': key.name = 'home'; key.ctrl = true; break; case '[8^': key.name = 'end'; key.ctrl = true; break; /* misc. */ case '[Z': key.name = 'tab'; key.shift = true; break; default: key.name = 'undefined'; break; } } else if (s.length > 1 && s[0] !== '\x1b') { // Got a longer-than-one string of characters. // Probably a paste, since it wasn't a control sequence. Array.prototype.forEach.call(s, function(c) { emitKey(stream, c); }); return; } // XXX: this "mouse" parsing code is NOT part of the node-core standard // `readline.js` module, and is a `keypress` module non-standard extension. if (key.code == '[M') { key.name = 'mouse'; var s = key.sequence; var b = s.charCodeAt(3); key.x = s.charCodeAt(4) - 040; key.y = s.charCodeAt(5) - 040; key.scroll = 0; key.ctrl = !!(1<<4 & b); key.meta = !!(1<<3 & b); key.shift = !!(1<<2 & b); key.release = (3 & b) === 3; if (1<<6 & b) { //scroll key.scroll = 1 & b ? 1 : -1; } if (!key.release && !key.scroll) { key.button = b & 3; } } // Don't emit a key if no name was found if (key.name === undefined) { key = undefined; } if (s.length === 1) { ch = s; } if (key && key.name == 'mouse') { stream.emit('mousepress', key); } else if (key || ch) { stream.emit('keypress', ch, key); } }