/* jslint node: true */ 'use strict'; 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'); var Log = require('./logger.js').log; var user = require('./user.js'); var moduleUtil = require('./module_util.js'); var menuUtil = require('./menu_util.js'); 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; // :TODO: Move all of the key stuff to it's own module // // Resources & Standards: // * http://www.ansi-bbs.org/ansi-bbs-core-server.html // var ANSI_KEY_NAME_MAP = { 0x08 : 'backspace', // BS 0x09 : 'tab', // 0x7f : 'del', 0x1b : 'esc', 0x0d : 'enter', 0x19 : 'end of medium', // EM / CTRL-Y }; var ANSI_KEY_CSI_NAME_MAP = { 0x40 : 'insert', // @ 0x41 : 'up arrow', // A 0x42 : 'down arrow', // B 0x43 : 'right arrow', // C 0x44 : 'left arrow', // D 0x48 : 'home', // H 0x4b : 'end', // K 0x56 : 'page up', // V 0x55 : 'page down', // U }; var ANSI_F_KEY_NAME_MAP_1 = { 0x50 : 'F1', 0x51 : 'F2', 0x52 : 'F3', 0x53 : 'F4', 0x74 : 'F5', }; var ANSI_F_KEY_NAME_MAP_2 = { // rxvt 11 : 'F1', 12 : 'F2', 13 : 'F3', 14 : 'F4', 15 : 'F5', // SyncTERM 17 : 'F6', 18 : 'F7', 19 : 'F8', 20 : 'F9', 21 : 'F10', 23 : 'F11', 24 : 'F12', }; // :TODO: put this in a common area!!!! function getIntArgArray(array) { var i = array.length; while(i--) { array[i] = parseInt(array[i], 10); } return array; } function Client(input, output) { stream.call(this); var self = this; this.input = input; this.output = output; this.term = new term.ClientTerminal(this.output); this.user = new user.User(); this.currentTheme = { info : { name : 'N/A', description : 'None' } }; // // Peek at |data| and emit for any specialized handling // such as ANSI control codes or user/keyboard input // self.on('data', 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() { if(self.currentMenuModule) { self.currentMenuModule.leave(); self.currentMenuModule = null; } }; } require('util').inherits(Client, stream); Client.prototype.end = function () { this.detachCurrentMenuModule(); return this.output.end.apply(this.output, arguments); }; Client.prototype.destroy = function () { return this.output.destroy.apply(this.output, arguments); }; Client.prototype.destroySoon = function () { return this.output.destroySoon.apply(this.output, arguments); }; Client.prototype.waitForKeyPress = function(cb) { this.once('key press', function onKeyPress(kp) { cb(kp); }); }; Client.prototype.address = function() { return this.input.address(); }; Client.prototype.gotoMenuModule = function(options, cb) { var self = this; assert(options.name); // Assign a default missing module handler callback if none was provided cb = miscUtil.valueWithDefault(cb, self.defaultHandlerMissingMod()); self.detachCurrentMenuModule(); var loadOptions = { name : options.name, client : self, args : options.args }; menuUtil.loadMenu(loadOptions, function onMenuModuleLoaded(err, modInst) { if(err) { cb(err); } else { Log.debug( { menuName : options.name }, 'Goto menu module'); modInst.enter(self); self.currentMenuModule = modInst; } }); }; Client.prototype.fallbackMenuModule = function(cb) { }; /////////////////////////////////////////////////////////////////////////////// // Default error handlers /////////////////////////////////////////////////////////////////////////////// // :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something Client.prototype.defaultHandlerMissingMod = function(err) { var self = this; function handler(err) { Log.error(err); self.term.write(ansi.resetScreen()); self.term.write('An unrecoverable error has been encountered!\n'); self.term.write('This has been logged for your SysOp to review.\n'); self.term.write('\nGoodbye!\n'); //self.term.write(err); //if(miscUtil.isDevelopment() && err.stack) { // self.term.write('\n' + err.stack + '\n'); //} self.end(); } return handler; };