diff --git a/core/client.js b/core/client.js index 53a3c337..40e1e5af 100644 --- a/core/client.js +++ b/core/client.js @@ -1,6 +1,36 @@ /* jslint node: true */ 'use strict'; +/* + Portions of this code for key handling heavily inspired from the following: + https://github.com/chjj/blessed/blob/master/lib/keys.js + + MIT license is as follows: + -------------------------- + The MIT License (MIT) + + Copyright (c) + + 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 stream = require('stream'); var assert = require('assert'); @@ -80,6 +110,14 @@ 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('|')); + + function Client(input, output) { stream.call(this); @@ -91,6 +129,117 @@ function Client(input, output) { this.user = new user.User(); this.currentTheme = { info : { name : 'N/A', description : 'None' } }; + // + // Peek at incoming |data| and emit events for any special + // handling that may include: + // * Keyboard input + // * ANSI CSR's and the like + // + // References: + // * http://www.ansi-bbs.org/ansi-bbs-core-server.html + // + // Implementation inspired from 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.getKeyComponentsFromCode = function(code) { + return { + 'OP' : { name : 'f1' }, + }[code]; + }; + + this.on('dataXXXX', function clientData(data) { + var len = data.length; + + // create a uniform format that can be parsed below + if(data[0] > 127 && undefined === data[1]) { + data[0] -= 128; + data = '\u001b' + data.toString('utf-8'); + } else { + data = data.toString('utf-8'); + } + + if(self.isMouseInput(data)) { + return; + } + + var buf = []; + var m; + while((m = REGEXP_ANSI_KEYCODE_ANYWHERE.exec(data))) { + buf = buf.concat(data.slice(0, m.index).split('')); + buf.push(m[0]); + data = data.slice(m.index + m[0].length); + } + + buf = buf.concat(data.split('')); // remainder + + buf.forEach(function bufPart(s) { + var key = { + seq : s, + name : undefined, + ctrl : false, + meta : false, + shift : false, + }; + + var parts; + + if('\r' === s) { + key.name = 'return'; + } else if('\n' === s) { + key.name = 'line feed'; + } else if('\t' === s) { + key.name = 'tab'; + } else if ('\b' === s || '\x7f' === s || '\x1b\x7f' === s || '\x1b\b' === s) { + // backspace, CTRL-H + key.name = 'backspace'; + key.meta = ('\x1b' === s.charAt(0)); + } else if('\x1b' === s || '\x1b\x1b' === s) { + key.name = 'escape'; + key.meta = (2 === s.length); + } else if (' ' === s || '\x1b ' === s) { + // rather annoying that space can come in other than just " " + key.name = 'space'; + key.meta = (2 === s.length); + } else if(1 === s.length && s <= '\x1a') { + // CTRL- + key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); + key.ctrl = true; + } else if(1 === s.length && s >= 'a' && s <= 'z') { + // normal, lowercased letter + key.name = s; + } else if(1 === s.length && s >= 'A' && s <= 'Z') { + key.name = s.toLowerCase(); + key.shift = true; + } else if ((parts = REGEXP_ANSI_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))) { + var code = + (parts[1] || '') + (parts[2] || '') + + (parts[4] || '') + (parts[9] || ''); + var modifier = (parts[3] || parts[8] || 1) - 1; + + key.ctrl = !!(modifier & 4); + key.meta = !!(modifier & 10); + key.shift = !!(modifier & 1); + key.code = code; + } + + }); + }); + // // Peek at |data| and emit for any specialized handling // such as ANSI control codes or user/keyboard input diff --git a/core/multi_line_edit_text_view2.js b/core/multi_line_edit_text_view2.js index 4e4ffc4f..04a2eb8b 100644 --- a/core/multi_line_edit_text_view2.js +++ b/core/multi_line_edit_text_view2.js @@ -235,6 +235,15 @@ function MultiLineEditTextView2(options) { } }; + this.getAbsolutePosition = function(row, col) { + return { row : self.position.row + self.cursorPos.row, col : self.position.col + self.cursorPos.col }; + }; + + this.moveClientCusorToCursorPos = function() { + var absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); + self.client.term.write(ansi.goto(absPos.row, absPos.col)); + }; + this.keyPressCharacter = function(c, row, col) { var index = self.getTextLinesIndex(row); @@ -256,16 +265,7 @@ function MultiLineEditTextView2(options) { }; - this.getAbsolutePosition = function(row, col) { - return { row : self.position.row + self.cursorPos.row, col : self.position.col + self.cursorPos.col }; - }; - - this.moveClientCusorToCursorPos = function() { - var absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); - self.client.term.write(ansi.goto(absPos.row, absPos.col)); - }; - - this.cursorUp = function() { + this.keyPressUp = function() { if(self.cursorPos.row > 0) { self.cursorPos.row--; self.client.term.write(ansi.up()); @@ -278,7 +278,7 @@ function MultiLineEditTextView2(options) { } }; - this.cursorDown = function() { + this.keyPressDown = function() { var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; if(self.cursorPos.row < lastVisibleRow) { self.cursorPos.row++; @@ -293,7 +293,7 @@ function MultiLineEditTextView2(options) { } }; - this.cursorLeft = function() { + this.keyPressLeft = function() { if(self.cursorPos.col > 0) { self.cursorPos.col--; self.client.term.write(ansi.left()); @@ -303,7 +303,7 @@ function MultiLineEditTextView2(options) { } }; - this.cursorRight = function() { + this.keyPressRight = function() { var eolColumn = self.getTextEndOfLineColumn(); if(self.cursorPos.col < eolColumn) { self.cursorPos.col++; @@ -316,7 +316,7 @@ function MultiLineEditTextView2(options) { } }; - this.cursorHome = function() { + this.keyPressHome = function() { var firstNonWhitespace = self.getVisibleText().search(/\S/); if(-1 !== firstNonWhitespace) { self.cursorPos.col = firstNonWhitespace; @@ -327,16 +327,20 @@ function MultiLineEditTextView2(options) { self.moveClientCusorToCursorPos(); }; - this.cursorEnd = function() { + this.keyPressEnd = function() { self.cursorPos.col = self.getTextEndOfLineColumn(); self.moveClientCusorToCursorPos(); }; - this.cursorPageUp = function() { + this.keyPressPageUp = function() { }; - this.cursorPageDown = function() { + this.keyPressPageDown = function() { + + }; + + this.keyPressLineFeed = function() { }; @@ -422,9 +426,11 @@ MultiLineEditTextView2.prototype.onKeyPress = function(key, isSpecial) { MultiLineEditTextView2.super_.prototype.onKeyPress.call(this, key, isSpecial); }; -var CURSOR_KEYS = [ - 'up', 'down', 'left', 'right', 'home', 'end', - 'pageUp', 'pageDown' +var HANDLED_SPECIAL_KEYS = [ + 'up', 'down', 'left', 'right', + 'home', 'end', + 'pageUp', 'pageDown', + 'lineFeed', ]; MultiLineEditTextView2.prototype.onSpecialKeyPress = function(keyName) { @@ -450,20 +456,15 @@ MultiLineEditTextView2.prototype.onSpecialKeyPress = function(keyName) { UP/^E LEFT/^S PGUP/^R HOME/^F DOWN/^X RIGHT/^D PGDN/^C END/^G */ - // :TODO: Make these keyPressXXXXXXX, e.g. keyPressUp(), keyPressLeft(), ... - - CURSOR_KEYS.forEach(function key(arrowKey) { + var handled = false; + HANDLED_SPECIAL_KEYS.forEach(function key(arrowKey) { if(self.isSpecialKeyMapped(arrowKey, keyName)) { - //self[makeKeyHandler('cursor', arrowKey)](); - self['cursor' + arrowKey.substring(0,1).toUpperCase() + arrowKey.replace(/\s/g, '').substring(1)](); + self[_.camelCase('keyPress ' + arrowKey)](); + handled = true; } }); - // TEMP HACK FOR TESTING ----- - if(self.isSpecialKeyMapped('lineFeed', keyName)) { - //self.cursorStartOfDocument(); - self.cursorStartOfDocument(); + if(!handled) { + MultiLineEditTextView2.super_.prototype.onSpecialKeyPress.call(this, keyName); } - - //MultiLineEditTextView2.super_.prototype.onSpecialKeyPress.call(this, keyName); };