* Some SyncTERM / EtherTerm key support for new key system

* Break long words for word wrap if required
* Lots of cursor movement improvements for MultiLineEditText2
* Code cleanup
This commit is contained in:
Bryan Ashby 2015-06-06 00:33:59 -06:00
parent f2a61828aa
commit feab2e0233
6 changed files with 99 additions and 197 deletions

View File

@ -19,14 +19,12 @@ function ButtonView(options) {
util.inherits(ButtonView, TextView); util.inherits(ButtonView, TextView);
ButtonView.prototype.onKeyPress = function(key, isSpecial) { ButtonView.prototype.onKeyPress = function(ch, key) {
ButtonView.super_.prototype.onKeyPress.call(this, key, isSpecial); if(' ' === ch) {
// allow spacebar to 'click' buttons
// :TODO: need to check configurable mapping here
if(' ' === key) {
this.emit('action', 'accept'); this.emit('action', 'accept');
} }
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
ButtonView.prototype.getData = function() { ButtonView.prototype.getData = function() {

View File

@ -116,7 +116,7 @@ var RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'
var RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ var RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [
'(\\d+)(?:;(\\d+))?([~^$])', '(\\d+)(?:;(\\d+))?([~^$])',
'(?:M([@ #!a`])(.)(.))', // mouse stuff '(?:M([@ #!a`])(.)(.))', // mouse stuff
'(?:1;)?(\\d+)?([a-zA-Z])' '(?:1;)?(\\d+)?([a-zA-Z@])'
].join('|') + ')'); ].join('|') + ')');
var RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); var RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source);
@ -234,9 +234,6 @@ function Client(input, output) {
'[8~' : { name : 'end' }, '[8~' : { name : 'end' },
// rxvt with modifiers // rxvt with modifiers
/* rxvt keys with modifiers */
'[a' : { name : 'up arrow', shift : true }, '[a' : { name : 'up arrow', shift : true },
'[b' : { name : 'down arrow', shift : true }, '[b' : { name : 'down arrow', shift : true },
'[c' : { name : 'right arrow', shift : true }, '[c' : { name : 'right arrow', shift : true },
@ -263,8 +260,9 @@ function Client(input, output) {
'[7^' : { name : 'home', ctrl : true }, '[7^' : { name : 'home', ctrl : true },
'[8^' : { name : 'end', ctrl : true }, '[8^' : { name : 'end', ctrl : true },
// SyncTERM // SyncTERM / EtherTerm
'[K' : { name : 'end' }, '[K' : { name : 'end' },
'[@' : { name : 'insert' },
// other // other
'[Z' : { name : 'tab', shift : true }, '[Z' : { name : 'tab', shift : true },
@ -294,8 +292,6 @@ function Client(input, output) {
buf = buf.concat(data.split('')); // remainder buf = buf.concat(data.split('')); // remainder
console.log(buf)
buf.forEach(function bufPart(s) { buf.forEach(function bufPart(s) {
var key = { var key = {
seq : s, seq : s,
@ -382,88 +378,13 @@ function Client(input, output) {
} }
if(key || ch) { if(key || ch) {
Log.trace( { key : key, ch : ch }, 'User keyboard input');
self.emit('key press', ch, key); self.emit('key press', ch, key);
} }
}); });
}); });
//
// Peek at |data| and emit for any specialized handling
// such as ANSI control codes or user/keyboard input
//
self.on('dataXX', function onData(data) {
var len = data.length;
var c;
var name;
if(1 === len) {
c = data[0];
if(0x00 === c) {
// ignore single NUL
return;
}
name = ANSI_KEY_NAME_MAP[c];
if(name) {
self.emit('special key', name);
self.emit('key press', data, true);
} else {
self.emit('key press', data, false);
}
}
if(0x1b !== data[0]) {
return;
}
if(3 === len) {
if(0x5b === data[1]) {
name = ANSI_KEY_CSI_NAME_MAP[data[2]];
if(name) {
self.emit('special key', name);
self.emit('key press', data, true);
}
} else if(0x4f === data[1]) {
name = ANSI_F_KEY_NAME_MAP_1[data[2]];
if(name) {
self.emit('special key', name);
self.emit('key press', data, true);
}
}
} else if(5 === len && 0x5b === data[1] && 0x7e === data[4]) {
var code = parseInt(data.slice(2,4), 10);
if(!isNaN(code)) {
name = ANSI_F_KEY_NAME_MAP_2[code];
if(name) {
self.emit('special key', name);
self.emit('key press', data, true);
}
}
} else if(len > 3) {
// :TODO: Implement various responses to DSR's & such
// See e.g. http://www.vt100.net/docs/vt100-ug/chapter3.html
var dsrResponseRe = /\u001b\[([0-9\;]+)([R])/g;
var match;
var args;
do {
match = dsrResponseRe.exec(data);
if(null !== match) {
switch(match[2]) {
case 'R' :
args = getIntArgArray(match[1].split(';'));
if(2 === args.length) {
self.emit('cursor position report', args);
}
break;
}
}
} while(0 !== dsrResponseRe.lastIndex);
}
});
self.detachCurrentMenuModule = function() { self.detachCurrentMenuModule = function() {
if(self.currentMenuModule) { if(self.currentMenuModule) {
self.currentMenuModule.leave(); self.currentMenuModule.leave();

View File

@ -89,6 +89,13 @@ function MultiLineEditTextView2(options) {
return index; return index;
}; };
this.getRemainingLinesBelowRow = function(row) {
if(!_.isNumber(row)) {
row = self.cursorPos.row;
}
return self.textLines.length - (self.topVisibleIndex + row) - 1;
};
this.redrawVisibleArea = function() { this.redrawVisibleArea = function() {
assert(self.topVisibleIndex < self.textLines.length); assert(self.topVisibleIndex < self.textLines.length);
@ -137,10 +144,16 @@ function MultiLineEditTextView2(options) {
self.textLines[index].text, col, c); self.textLines[index].text, col, c);
}; };
this.getRemainingTabWidth = function(col) {
if(!_.isNumber(col)) {
col = self.cursorPos.col;
}
return self.tabWidth - (col % self.tabWidth);
};
this.expandTab = function(col, expandChar) { this.expandTab = function(col, expandChar) {
expandChar = expandChar || ' '; expandChar = expandChar || ' ';
var count = self.tabWidth - (col % self.tabWidth); return new Array(self.getRemainingTabWidth(col)).join(expandChar);
return new Array(count).join(expandChar);
}; };
this.wordWrapSingleLine = function(s, width) { this.wordWrapSingleLine = function(s, width) {
@ -153,7 +166,10 @@ function MultiLineEditTextView2(options) {
// * Tabs in Sublime Text 3 are also treated as a word, so, e.g. // * Tabs in Sublime Text 3 are also treated as a word, so, e.g.
// "\t" may resolve to " " and must fit within the space. // "\t" may resolve to " " and must fit within the space.
// //
// note: we cannot simply use \s below as it includes \t // * If a word is ultimately too long to fit, break it up until it does.
//
// RegExp below is JavaScript '\s' minus the '\t'
//
var re = new RegExp( var re = new RegExp(
'\t|[ \f\n\r\v\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006' + '\t|[ \f\n\r\v\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006' +
'\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+', 'g'); '\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+', 'g');
@ -164,11 +180,13 @@ function MultiLineEditTextView2(options) {
var word; var word;
function addWord() { function addWord() {
if(wrapped[i].length + word.length > self.dimens.width) { word.match(new RegExp('.{0,' + self.dimens.width + '}', 'g')).forEach(function wrd(w) {
wrapped[++i] = word; if(wrapped[i].length + w.length > self.dimens.width) {
} else { wrapped[++i] = w;
wrapped[i] += word; } else {
} wrapped[i] += w;
}
});
} }
do { do {
@ -317,7 +335,7 @@ function MultiLineEditTextView2(options) {
self.client.term.write(ansi.left()); self.client.term.write(ansi.left());
// :TODO: handle landing on a tab // :TODO: handle landing on a tab
} else { } else {
// :TODO: goto previous line if possible and scroll if needed self.cursorEndOfPreviousLine();
} }
}; };
@ -327,10 +345,9 @@ function MultiLineEditTextView2(options) {
self.cursorPos.col++; self.cursorPos.col++;
self.client.term.write(ansi.right()); self.client.term.write(ansi.right());
// :TODO: handle landing on a tab self.adjustCursorToNextTab('right');
} else { } else {
// :TODO: goto next line; scroll if needed, etc. self.cursorBeginOfNextLine();
} }
}; };
@ -362,18 +379,40 @@ function MultiLineEditTextView2(options) {
}; };
this.adjustCursorIfPastEndOfLine = function(alwaysUpdateCursor) { this.adjustCursorIfPastEndOfLine = function(forceUpdate) {
var eolColumn = self.getTextEndOfLineColumn(); var eolColumn = self.getTextEndOfLineColumn();
if(self.cursorPos.col > eolColumn) { if(self.cursorPos.col > eolColumn) {
self.cursorPos.col = eolColumn; self.cursorPos.col = eolColumn;
alwaysUpdateCursor = true; forceUpdate = true;
} }
if(alwaysUpdateCursor) { if(forceUpdate) {
self.moveClientCusorToCursorPos(); self.moveClientCusorToCursorPos();
} }
}; };
this.adjustCursorToNearestTab = function() {
//
// When pressing up or down and landing on a tab, jump
// to the nearest tabstop -- right or left.
//
};
this.adjustCursorToNextTab = function(direction) {
if('\t' === self.getText()[self.cursorPos.col]) {
//
// When pressing right or left, jump to the next
// tabstop in that direction.
//
if('right' === direction) {
var move = self.getRemainingTabWidth() - 1;
self.cursorPos.col += move;
self.client.term.write(ansi.right(move));
}
}
};
this.cursorStartOfDocument = function() { this.cursorStartOfDocument = function() {
self.topVisibleIndex = 0; self.topVisibleIndex = 0;
self.cursorPos = { row : 0, col : 0 }; self.cursorPos = { row : 0, col : 0 };
@ -391,12 +430,38 @@ function MultiLineEditTextView2(options) {
self.moveClientCusorToCursorPos(); self.moveClientCusorToCursorPos();
}; };
this.cursorBeginOfNextLine = function() {
// e.g. when scrolling right past eol
var linesBelow = self.getRemainingLinesBelowRow();
if(linesBelow > 0) {
var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1;
if(self.cursorPos.row < lastVisibleRow) {
self.cursorPos.row++;
} else {
self.scrollDocumentUp();
}
self.keyPressHome(); // same as pressing 'home'
}
};
this.cursorEndOfPreviousLine = function() {
if(self.topVisibleIndex > 0) {
if(self.cursorPos.row > 0) {
self.cursorPos.row--;
} else {
self.scrollDocumentDown();
}
self.keyPressEnd(); // same as pressing 'end'
}
};
this.scrollDocumentUp = function() { this.scrollDocumentUp = function() {
// //
// Note: We scroll *up* when the cursor goes *down* beyond // Note: We scroll *up* when the cursor goes *down* beyond
// the visible area! // the visible area!
// //
var linesBelow = self.textLines.length - (self.topVisibleIndex + self.cursorPos.row) - 1; var linesBelow = self.getRemainingLinesBelowRow();
if(linesBelow > 0) { if(linesBelow > 0) {
self.topVisibleIndex++; self.topVisibleIndex++;
self.redraw(); self.redraw();
@ -426,8 +491,7 @@ MultiLineEditTextView2.prototype.redraw = function() {
MultiLineEditTextView2.prototype.setText = function(text) { MultiLineEditTextView2.prototype.setText = function(text) {
this.textLines = []; this.textLines = [];
//text = 'Supper fluffy bunny test thing\nHello, everyone!\n\nStuff and thing and what nots\r\na\tb\tc\td\te'; //text = "Tab:\r\n\tA\tB\tC\tD\tE\tF\tG\tH\tI\tJ\tK\tL\tM\tN\tO\tP\nA reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally long word!!!";
//text = "You. Now \ttomorrow \tthere'll \tbe \ttwo \tsessions, \tof\t course, morning and afternoon.";
this.insertText(text);//, 0, 0); this.insertText(text);//, 0, 0);
this.cursorEndOfDocument(); this.cursorEndOfDocument();
@ -462,37 +526,3 @@ MultiLineEditTextView2.prototype.onKeyPress = function(ch, key) {
MultiLineEditTextView2.super_.prototype.onKeyPress.call(this, ch, key); MultiLineEditTextView2.super_.prototype.onKeyPress.call(this, ch, key);
} }
}; };
/*
MultiLineEditTextView2.prototype.onKeyPress = function(key, isSpecial) {
if(isSpecial) {
return;
}
this.keyPressCharacter(key);
MultiLineEditTextView2.super_.prototype.onKeyPress.call(this, key, isSpecial);
};
MultiLineEditTextView2.prototype.onSpecialKeyPress = function(keyName) {
var self = this;
console.log(keyName);
var handled = false;
HANDLED_SPECIAL_KEYS.forEach(function key(arrowKey) {
if(self.isSpecialKeyMapped(arrowKey, keyName)) {
self[_.camelCase('keyPress ' + arrowKey)]();
handled = true;
}
});
if(!handled) {
MultiLineEditTextView2.super_.prototype.onSpecialKeyPress.call(this, keyName);
}
};
*/

View File

@ -5,6 +5,7 @@ var events = require('events');
var util = require('util'); var util = require('util');
var assert = require('assert'); var assert = require('assert');
var ansi = require('./ansi_term.js'); var ansi = require('./ansi_term.js');
var _ = require('lodash'); var _ = require('lodash');
exports.View = View; exports.View = View;
@ -79,7 +80,7 @@ function View(options) {
} }
this.isSpecialKeyMapped = function(keySet, keyName) { this.isSpecialKeyMapped = function(keySet, keyName) {
return this.specialKeyMap[keySet].indexOf(keyName) > -1; return _.has(this.specialKeyMap, keySet) && this.specialKeyMap[keySet].indexOf(keyName) > -1;
}; };
this.getANSIColor = function(color) { this.getANSIColor = function(color) {
@ -177,25 +178,6 @@ View.prototype.setFocus = function(focused) {
this.hasFocus = focused; this.hasFocus = focused;
this.restoreCursor(); this.restoreCursor();
}; };
/*
View.prototype.onKeyPress = function(key, isSpecial) {
assert(this.hasFocus, 'View does not have focus');
assert(this.acceptsInput, 'View does not accept input');
assert(1 === key.length);
};
View.prototype.onSpecialKeyPress = function(keyName) {
assert(this.hasFocus, 'View does not have focus');
assert(this.acceptsInput, 'View does not accept input');
assert(this.specialKeyMap, 'No special key map defined');
if(this.isSpecialKeyMapped('accept', keyName)) {
this.emit('action', 'accept');
} else if(this.isSpecialKeyMapped('next', keyName)) {
this.emit('action', 'next');
}
};
*/
View.prototype.onKeyPress = function(ch, key) { View.prototype.onKeyPress = function(ch, key) {
assert(this.hasFocus, 'View does not have focus'); assert(this.hasFocus, 'View does not have focus');

View File

@ -40,9 +40,6 @@ function ViewController(options) {
// Process key presses treating form submit mapped // Process key presses treating form submit mapped
// keys special. Everything else is forwarded on to // keys special. Everything else is forwarded on to
// the focused View, if any. // // the focused View, if any. //
console.log('ch=' + ch + ' / ' + JSON.stringify(key));
if(key) { if(key) {
var submitViewId = self.submitKeyMap[key.name]; var submitViewId = self.submitKeyMap[key.name];
if(submitViewId) { if(submitViewId) {
@ -57,32 +54,6 @@ function ViewController(options) {
} }
}; };
/*
this.clientKeyPressHandler = function(key, isSpecial) {
if(isSpecial) {
return;
}
if(self.focusedView && self.focusedView.acceptsInput) {
key = 'string' === typeof key ? key : key.toString();
self.focusedView.onKeyPress(key, isSpecial);
}
};
this.clientSpecialKeyHandler = function(keyName) {
var submitViewId = self.submitKeyMap[keyName];
if(submitViewId) {
self.switchFocus(submitViewId);
self.submitForm();
} else {
if(self.focusedView && self.focusedView.acceptsInput) {
self.focusedView.onSpecialKeyPress(keyName);
}
}
};
*/
this.viewActionListener = function(action) { this.viewActionListener = function(action) {
switch(action) { switch(action) {
case 'next' : case 'next' :

View File

@ -162,7 +162,7 @@
"text" : "Apply" "text" : "Apply"
}, },
"BT13" : { "BT13" : {
"submit" : [ "esc" ], "submit" : [ "escape" ],
"text" : "Cancel" "text" : "Cancel"
} }
}, },
@ -282,7 +282,7 @@
"BT5" : { "BT5" : {
"width" : 8, "width" : 8,
"text" : "< Back", "text" : "< Back",
"submit" : [ "esc" ] "submit" : [ "escape" ]
} }
}, },
"submit" : { "submit" : {
@ -317,7 +317,7 @@
}, },
"BT8" : { "BT8" : {
"text" : "< Back", "text" : "< Back",
"submit" : [ "esc" ] "submit" : [ "escape" ]
} }
}, },
"submit" : { "submit" : {
@ -346,7 +346,7 @@
}, },
"BT5" : { "BT5" : {
"text" : "< Back", "text" : "< Back",
"submit" : [ "esc" ] "submit" : [ "escape" ]
} }
}, },
"submit" : { "submit" : {
@ -376,7 +376,7 @@
}, },
"BT5" : { "BT5" : {
"text" : "< Back", "text" : "< Back",
"submit" : [ "esc" ] "submit" : [ "escape" ]
} }
}, },
"submit" : { "submit" : {
@ -459,7 +459,7 @@
}, },
"ET5" : { "ET5" : {
"password" : true, "password" : true,
"submit" : [ "esc" ], "submit" : [ "escape" ],
"fillChar" : "#" "fillChar" : "#"
}, },
"TM6" : { "TM6" : {