From dd0568f20763b74a19453d5a4ba9c830ad6a83e7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 4 Jun 2015 22:29:14 -0600 Subject: [PATCH] * Most of new key/DSR implementation in place... a bit more to go with separation of ch vs key & cleaing up the two handlers -> one onKeyPress --- core/client.js | 184 ++++++++++++++++++++++++----- core/multi_line_edit_text_view2.js | 2 +- core/view.js | 2 +- core/view_controller.js | 31 ++++- 4 files changed, 185 insertions(+), 34 deletions(-) diff --git a/core/client.js b/core/client.js index 40e1e5af..14157a9f 100644 --- a/core/client.js +++ b/core/client.js @@ -30,10 +30,6 @@ THE SOFTWARE. -------------------------- */ - -var stream = require('stream'); -var assert = require('assert'); - var term = require('./client_term.js'); var miscUtil = require('./misc_util.js'); var ansi = require('./ansi_term.js'); @@ -42,6 +38,10 @@ var user = require('./user.js'); var moduleUtil = require('./module_util.js'); var menuUtil = require('./menu_util.js'); +var stream = require('stream'); +var assert = require('assert'); +var _ = require('lodash'); + exports.Client = Client; //var ANSI_CONTROL_REGEX = /(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/g; @@ -110,12 +110,24 @@ function getIntArgArray(array) { return array; } -var REGEXP_ANSI_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; -var REGEXP_ANSI_KEYCODE = new RegExp('^' + REGEXP_ANSI_KEYCODE_ANYWHERE.source + '$'); -var REGEXP_ANSI_KEYCODE_FUNC_ANYWHERE = /(?:\u001b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:M([@ #!a`])(.)(.))|(?:1;)?(\d+)?([a-zA-Z]))/; -var REGEXP_ANSI_KEYCODE_FUNC = new RegExp('^' + REGEXP_ANSI_KEYCODE_FUNC_ANYWHERE.source); -var REGEXP_ANSI_KEYCODE_ESC = new RegExp( - [ REGEXP_ANSI_KEYCODE_FUNC_ANYWHERE.source, REGEXP_ANSI_KEYCODE_ANYWHERE.source, /\u001b./.source].join('|')); +var RE_DSR_RESPONSE = /(?:\u001b\[)([0-9\;]+)([R])/; + +var RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; +var RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); +var RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ + '(\\d+)(?:;(\\d+))?([~^$])', + '(?:M([@ #!a`])(.)(.))', // mouse stuff + '(?:1;)?(\\d+)?([a-zA-Z])' + ].join('|') + ')'); + +var RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); +var RE_ESC_CODE_ANYWHERE = new RegExp( [ + RE_FUNCTION_KEYCODE_ANYWHERE.source, + RE_META_KEYCODE_ANYWHERE.source, + RE_DSR_RESPONSE.source, + /\u001b./.source + ].join('|')); + function Client(input, output) { @@ -138,28 +150,120 @@ function Client(input, output) { // References: // * http://www.ansi-bbs.org/ansi-bbs-core-server.html // - // Implementation inspired from https://github.com/chjj/blessed/blob/master/lib/keys.js + // Implementation inspired from Christopher Jeffrey's Blessing library: + // https://github.com/chjj/blessed/blob/master/lib/keys.js // // :TODO: this is a WIP v2 of onData() - this.isMouseInput = function(s) { - return /\x1b\[M/.test(s) || - /\u001b\[M([\x00\u0020-\uffff]{3})/.test(s) || - /\u001b\[(\d+;\d+;\d+)M/.test(s) || - /\u001b\[<(\d+;\d+;\d+)([mM])/.test(s) || - /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(s) || - /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(s) || - /\u001b\[(O|I)/.test(s); + this.isMouseInput = function(data) { + return /\x1b\[M/.test(data) || + /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || + /\u001b\[(\d+;\d+;\d+)M/.test(data) || + /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || + /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || + /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || + /\u001b\[(O|I)/.test(data); }; this.getKeyComponentsFromCode = function(code) { return { + // xterm/gnome 'OP' : { name : 'f1' }, + 'OQ' : { name : 'f2' }, + 'OR' : { name : 'f3' }, + 'OS' : { name : 'f4' }, + + 'OA' : { name : 'up arrow' }, + 'OB' : { name : 'down arrow' }, + 'OC' : { name : 'right arrow' }, + 'OD' : { name : 'left arrow' }, + 'OE' : { name : 'clear' }, + 'OF' : { name : 'end' }, + 'OH' : { name : 'home' }, + + // xterm/rxvt + '[11~' : { name : 'f1' }, + '[12~' : { name : 'f2' }, + '[13~' : { name : 'f3' }, + '[14~' : { name : 'f4' }, + + '[1~' : { name : 'home' }, + '[2~' : { name : 'insert' }, + '[3~' : { name : 'delete' }, + '[4~' : { name : 'end' }, + '[5~' : { name : 'page up' }, + '[6~' : { name : 'page down' }, + + // Cygwin & libuv + '[[A' : { name : 'f1' }, + '[[B' : { name : 'f2' }, + '[[C' : { name : 'f3' }, + '[[D' : { name : 'f4' }, + '[[E' : { name : 'f5' }, + + // Common impls + '[15~' : { name : 'f5' }, + '[17~' : { name : 'f6' }, + '[18~' : { name : 'f7' }, + '[19~' : { name : 'f8' }, + '[20~' : { name : 'f9' }, + '[21~' : { name : 'f10' }, + '[23~' : { name : 'f11' }, + '[24~' : { name : 'f12' }, + + // xterm + '[A' : { name : 'up arrow' }, + '[B' : { name : 'down arrow' }, + '[C' : { name : 'right arrow' }, + '[D' : { name : 'left arrow' }, + '[E' : { name : 'clear' }, + '[F' : { name : 'end' }, + '[H' : { name : 'home' }, + + // PuTTY + '[[5~' : { name : 'page up' }, + '[[6~' : { name : 'page down' }, + + // rvxt + '[7~' : { name : 'home' }, + '[8~' : { name : 'end' }, + + // rxvt with modifiers + + + /* rxvt keys with modifiers */ + '[a' : { name : 'up arrow', shift : true }, + '[b' : { name : 'down arrow', shift : true }, + '[c' : { name : 'right arrow', shift : true }, + '[d' : { name : 'left arrow', shift : true }, + '[e' : { name : 'clear', shift : true }, + + '[2$' : { name : 'insert', shift : true }, + '[3$' : { name : 'delete', shift : true }, + '[5$' : { name : 'page up', shift : true }, + '[6$' : { name : 'page down', shift : true }, + '[7$' : { name : 'home', shift : true }, + '[8$' : { name : 'end', shift : true }, + + 'Oa' : { name : 'up arrow', ctrl : true }, + 'Ob' : { name : 'down arrow', ctrl : true }, + 'Oc' : { name : 'right arrow', ctrl : true }, + 'Od' : { name : 'left arrow', ctrl : true }, + 'Oe' : { name : 'clear', ctrl : true }, + + '[2^' : { name : 'insert', ctrl : true }, + '[3^' : { name : 'delete', ctrl : true }, + '[5^' : { name : 'page up', ctrl : true }, + '[6^' : { name : 'page down', ctrl : true }, + '[7^' : { name : 'home', ctrl : true }, + '[8^' : { name : 'end', ctrl : true }, + + // other + '[Z' : { name : 'tab', shift : true }, }[code]; }; - this.on('dataXXXX', function clientData(data) { - var len = data.length; - + this.on('data', function clientData(data) { + // create a uniform format that can be parsed below if(data[0] > 127 && undefined === data[1]) { data[0] -= 128; @@ -174,7 +278,7 @@ function Client(input, output) { var buf = []; var m; - while((m = REGEXP_ANSI_KEYCODE_ANYWHERE.exec(data))) { + while((m = RE_ESC_CODE_ANYWHERE.exec(data))) { buf = buf.concat(data.slice(0, m.index).split('')); buf.push(m[0]); data = data.slice(m.index + m[0].length); @@ -193,7 +297,14 @@ function Client(input, output) { var parts; - if('\r' === s) { + if((parts = RE_DSR_RESPONSE.exec(s))) { + if('R' === parts[2]) { + var cprArgs = getIntArgArray(parts[1].split(';')); + if(2 === cprArgs.length) { + self.emit('cursor position report', cprArgs); + } + } + } else if('\r' === s) { key.name = 'return'; } else if('\n' === s) { key.name = 'line feed'; @@ -220,12 +331,12 @@ function Client(input, output) { } else if(1 === s.length && s >= 'A' && s <= 'Z') { key.name = s.toLowerCase(); key.shift = true; - } else if ((parts = REGEXP_ANSI_KEYCODE.exec(s))) { + } else if ((parts = RE_META_KEYCODE.exec(s))) { // meta with character key key.name = parts[1].toLowerCase(); key.meta = true; key.shift = /^[A-Z]$/.test(parts[1]); - } else if((parts = REGEXP_ANSI_KEYCODE_FUNC.exec(s))) { + } else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { var code = (parts[1] || '') + (parts[2] || '') + (parts[4] || '') + (parts[9] || ''); @@ -235,8 +346,25 @@ function Client(input, output) { key.meta = !!(modifier & 10); key.shift = !!(modifier & 1); key.code = code; + + _.assign(key, self.getKeyComponentsFromCode(code)); } + var ch; + if(1 === s.length) { + ch = s; + } else if('space' === key.name) { + // stupid hack to always get space as a regular char + ch = ' '; + } + + if(_.isUndefined(key.name)) { + key = undefined; + } + + if(key || ch) { + self.emit('key press', ch, key); + } }); }); @@ -244,13 +372,11 @@ function Client(input, output) { // Peek at |data| and emit for any specialized handling // such as ANSI control codes or user/keyboard input // - self.on('data', function onData(data) { + self.on('dataXX', function onData(data) { var len = data.length; var c; var name; - console.log(data) - if(1 === len) { c = data[0]; diff --git a/core/multi_line_edit_text_view2.js b/core/multi_line_edit_text_view2.js index 04a2eb8b..7bd95b23 100644 --- a/core/multi_line_edit_text_view2.js +++ b/core/multi_line_edit_text_view2.js @@ -11,7 +11,7 @@ var assert = require('assert'); var _ = require('lodash'); var SPECIAL_KEY_MAP_DEFAULT = { - lineFeed : [ 'enter' ], + lineFeed : [ 'return' ], exit : [ 'esc' ], backspace : [ 'backspace' ], del : [ 'del' ], diff --git a/core/view.js b/core/view.js index 78c9f82a..9cade589 100644 --- a/core/view.js +++ b/core/view.js @@ -11,7 +11,7 @@ exports.View = View; exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT; var VIEW_SPECIAL_KEY_MAP_DEFAULT = { - accept : [ 'enter' ], + accept : [ 'return' ], exit : [ 'esc' ], backspace : [ 'backspace', 'del' ], del : [ 'del' ], diff --git a/core/view_controller.js b/core/view_controller.js index f37469d1..29d6d11f 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -35,6 +35,30 @@ function ViewController(options) { this.mciViewFactory = new MCIViewFactory(this.client); this.submitKeyMap = {}; + this.clientKeyPressHandler = function(ch, specialKey) { + console.log('ch=' + ch + ' / ' + JSON.stringify(specialKey)); + // :TODO: pass actual key object along here + if(specialKey) { + var submitViewId = self.submitKeyMap[specialKey.name]; + if(submitViewId) { + self.switchFocus(submitViewId); + self.submitForm(); + } else { + // :TODO: pass actual key here + if(self.focusedView && self.focusedView.acceptsInput) { + self.focusedView.onSpecialKeyPress(specialKey.name); + } + } + } else { + assert(_.isString(ch)); + + if(self.focusedView && self.focusedView.acceptsInput) { + self.focusedView.onKeyPress(ch); + } + } + }; + + /* this.clientKeyPressHandler = function(key, isSpecial) { if(isSpecial) { return; @@ -58,6 +82,7 @@ function ViewController(options) { } } }; + */ this.viewActionListener = function(action) { switch(action) { @@ -296,7 +321,7 @@ ViewController.prototype.attachClientEvents = function() { } this.client.on('key press', this.clientKeyPressHandler); - this.client.on('special key', this.clientSpecialKeyHandler); + //this.client.on('special key', this.clientSpecialKeyHandler); this.attached = true; }; @@ -307,7 +332,7 @@ ViewController.prototype.detachClientEvents = function() { } this.client.removeListener('key press', this.clientKeyPressHandler); - this.client.removeListener('special key', this.clientSpecialKeyHandler); + //this.client.removeListener('special key', this.clientSpecialKeyHandler); for(var id in this.views) { this.views[id].removeAllListeners(); @@ -342,7 +367,7 @@ ViewController.prototype.switchFocus = function(id) { var view = this.getView(id); if(view && view.acceptsFocus) { - this.switchFocusEvent('enter', view); + this.switchFocusEvent('return', view); this.focusedView = view; this.focusedView.setFocus(true);