From 436c81204f1b8667ad6d1858d2d67b050f783095 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 16 Jan 2015 11:56:00 -0800 Subject: [PATCH] include our own emitKeypressEvents. fixes #92. --- lib/blessed.js | 2 +- lib/colors.js | 2 +- lib/events.js | 2 +- lib/keys.js | 302 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/program.js | 8 +- lib/tput.js | 2 +- lib/widget.js | 3 +- 7 files changed, 311 insertions(+), 10 deletions(-) create mode 100644 lib/keys.js diff --git a/lib/blessed.js b/lib/blessed.js index c3c1724..48e96b4 100644 --- a/lib/blessed.js +++ b/lib/blessed.js @@ -1,7 +1,7 @@ /** * Blessed * A curses-like library for node.js. - * Copyright (c) 2013, Christopher Jeffrey (MIT License). + * Copyright (c) 2013-2015, Christopher Jeffrey (MIT License). * https://github.com/chjj/blessed */ diff --git a/lib/colors.js b/lib/colors.js index 2e2ef63..6c2d60f 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -1,6 +1,6 @@ /** * colors.js - color-related functions for blessed. - * Copyright (c) 2013, Christopher Jeffrey (MIT License) + * Copyright (c) 2013-2015, Christopher Jeffrey (MIT License) * https://github.com/chjj/blessed */ diff --git a/lib/events.js b/lib/events.js index 88c7ac4..af5cb7a 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1,6 +1,6 @@ /** * events.js - event emitter for blessed - * Copyright (c) 2013, Christopher Jeffrey (MIT License) + * Copyright (c) 2013-2015, Christopher Jeffrey (MIT License) * https://github.com/chjj/blessed */ diff --git a/lib/keys.js b/lib/keys.js new file mode 100644 index 0000000..b331783 --- /dev/null +++ b/lib/keys.js @@ -0,0 +1,302 @@ +/** + * keys.js - emit key presses + * Copyright (c) 2010-2015, Joyent, Inc. and other contributors (MIT License) + * https://github.com/chjj/blessed + */ + +// Originally taken from the node.js tree: +// +// Copyright Joyent, Inc. and other Node contributors. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +var EventEmitter = require('events').EventEmitter; +var StringDecoder = require('string_decoder').StringDecoder; + +/** + * accepts a readable Stream instance and makes it emit "keypress" events + */ + +function emitKeypressEvents(stream) { + if (stream._keypressDecoder) return; + var StringDecoder = require('string_decoder').StringDecoder; // lazy load + stream._keypressDecoder = new StringDecoder('utf8'); + + function onData(b) { + if (EventEmitter.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 (EventEmitter.listenerCount(stream, 'keypress') > 0) { + stream.on('data', onData); + } else { + stream.on('newListener', onNewListener); + } +} +exports.emitKeypressEvents = emitKeypressEvents; + +/* + 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 metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/; +var metaKeyCodeRe = new RegExp('^' + metaKeyCodeReAnywhere.source + '$'); +var functionKeyCodeReAnywhere = + /(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; +var functionKeyCodeRe = new RegExp('^' + functionKeyCodeReAnywhere.source); + +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.length === 1 && 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; + } + + // Don't emit a key if no name was found + if (key.name === undefined) { + key = undefined; + } + + if (s.length === 1) { + ch = s; + } + + if (key || ch) { + stream.emit('keypress', ch, key); + } +} diff --git a/lib/program.js b/lib/program.js index ae9aa9b..9f2f709 100644 --- a/lib/program.js +++ b/lib/program.js @@ -1,6 +1,6 @@ /** * program.js - basic curses-like functionality for blessed. - * Copyright (c) 2013, Christopher Jeffrey (MIT License). + * Copyright (c) 2013-2015, Christopher Jeffrey (MIT License). * https://github.com/chjj/blessed */ @@ -234,7 +234,7 @@ Program.prototype.term = function(is) { }; Program.prototype.listen = function() { - var readline = require('readline') + var keys = require('./keys') , self = this; if (!this.input.isTTY || !this.output.isTTY) { @@ -247,7 +247,7 @@ Program.prototype.listen = function() { if (key.name === 'undefined' && (key.code === '[M' || key.code === '[I' || key.code === '[O')) { - // A mouse sequence. The readline module doesn't understand these. + // A mouse sequence. The `keys` module doesn't understand these. return; } @@ -279,7 +279,7 @@ Program.prototype.listen = function() { self.emit('data', data); }); - readline.emitKeypressEvents(this.input); + keys.emitKeypressEvents(this.input); this.on('newListener', function fn(type) { if (type === 'keypress' || type === 'mouse') { diff --git a/lib/tput.js b/lib/tput.js index 5e196bf..d3c7aac 100644 --- a/lib/tput.js +++ b/lib/tput.js @@ -1,6 +1,6 @@ /** * tput.js - parse and compile terminfo caps to javascript. - * Copyright (c) 2013, Christopher Jeffrey (MIT License) + * Copyright (c) 2013-2015, Christopher Jeffrey (MIT License) * https://github.com/chjj/blessed */ diff --git a/lib/widget.js b/lib/widget.js index 3641f8f..b2af209 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -1,8 +1,7 @@ /** * widget.js - high-level interface for blessed - * Copyright (c) 2013, Christopher Jeffrey (MIT License) + * Copyright (c) 2013-2015, Christopher Jeffrey (MIT License) * https://github.com/chjj/blessed - * Still under heavy development. */ /**