From be6af161ec19776680618f5f27903d2ffa6b602c Mon Sep 17 00:00:00 2001 From: Andrew Pamment Date: Mon, 27 Jun 2016 17:29:17 +1000 Subject: [PATCH] Added ERC Module --- core/multi_line_edit_text_view.js | 90 +++++++++------ mods/art/erc.ans | Bin 0 -> 376 bytes mods/erc_client.js | 184 ++++++++++++++++++++++++++++++ mods/menu.hjson | 102 +++++++++++++---- 4 files changed, 318 insertions(+), 58 deletions(-) create mode 100644 mods/art/erc.ans create mode 100644 mods/erc_client.js diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 63c5e20b..c06a1f2c 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -34,7 +34,7 @@ var _ = require('lodash'); // // Editors - BBS // * https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp -// +// // // Editors - Other // * http://joe-editor.sourceforge.net/ @@ -55,12 +55,12 @@ var _ = require('lodash'); // // To-Do -// +// // * Index pos % for emit scroll events // * Some of this shoudl be async'd where there is lots of processing (e.g. word wrap) // * Fix backspace when col=0 (e.g. bs to prev line) // * Add back word delete -// * +// * var SPECIAL_KEY_MAP_DEFAULT = { @@ -114,6 +114,11 @@ function MultiLineEditTextView(options) { this.topVisibleIndex = 0; this.mode = options.mode || 'edit'; // edit | preview | read-only + if (this.mode == 'edit') { + this.autoScroll = options.autoScroll || 'true'; + } else { + this.autoScroll = options.autoScroll || 'false'; + } // // cursorPos represents zero-based row, col positions // within the editor itself @@ -179,7 +184,7 @@ function MultiLineEditTextView(options) { this.eraseRows = function(startRow, endRow) { self.client.term.rawWrite(self.getSGRFor('text') + ansi.hideCursor()); - + var absPos = self.getAbsolutePosition(startRow, 0); var absPosEnd = self.getAbsolutePosition(endRow, 0); var eraseFiller = new Array(self.dimens.width).join(' '); @@ -216,7 +221,7 @@ function MultiLineEditTextView(options) { if(!_.isNumber(index)) { index = self.getTextLinesIndex(); } - return self.textLines[index].text.replace(/\t/g, ' '); + return self.textLines[index].text.replace(/\t/g, ' '); }; this.getText = function(index) { @@ -266,19 +271,19 @@ function MultiLineEditTextView(options) { } return lines; }; - + this.getOutputText = function(startIndex, endIndex, eolMarker) { let lines = self.getTextLines(startIndex, endIndex); let text = ''; var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); - + lines.forEach(line => { text += line.text.replace(re, '\t'); if(eolMarker && line.eol) { text += eolMarker; - } + } }); - + return text; } @@ -302,7 +307,7 @@ function MultiLineEditTextView(options) { /* this.editTextAtPosition = function(editAction, text, index, col) { switch(editAction) { - case 'insert' : + case 'insert' : self.insertCharactersInText(text, index, col); break; @@ -329,7 +334,7 @@ function MultiLineEditTextView(options) { newLines[newLines.length - 1].eol = true; Array.prototype.splice.apply( - self.textLines, + self.textLines, [ index, (nextEolIndex - index) + 1 ].concat(newLines)); return wrapped.firstWrapRange; @@ -337,7 +342,7 @@ function MultiLineEditTextView(options) { this.removeCharactersFromText = function(index, col, operation, count) { if('right' === operation) { - self.textLines[index].text = + self.textLines[index].text = self.textLines[index].text.slice(col, count) + self.textLines[index].text.slice(col + count); @@ -354,11 +359,11 @@ function MultiLineEditTextView(options) { } else if ('backspace' === operation) { // :TODO: method for splicing text self.textLines[index].text = - self.textLines[index].text.slice(0, col - (count - 1)) + + self.textLines[index].text.slice(0, col - (count - 1)) + self.textLines[index].text.slice(col + 1); self.cursorPos.col -= (count - 1); - + self.updateTextWordWrap(index); self.redrawRows(self.cursorPos.row, self.dimens.height); @@ -405,9 +410,9 @@ function MultiLineEditTextView(options) { this.insertCharactersInText = function(c, index, col) { self.textLines[index].text = [ - self.textLines[index].text.slice(0, col), - c, - self.textLines[index].text.slice(col) + self.textLines[index].text.slice(0, col), + c, + self.textLines[index].text.slice(col) ].join(''); //self.cursorPos.col++; @@ -443,13 +448,13 @@ function MultiLineEditTextView(options) { // absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); self.client.term.write( - ansi.hideCursor() + - self.getSGRFor('text') + + ansi.hideCursor() + + self.getSGRFor('text') + self.getRenderText(index).slice(self.cursorPos.col - c.length) + ansi.goto(absPos.row, absPos.col) + ansi.showCursor(), false ); - } + } }; this.getRemainingTabWidth = function(col) { @@ -541,7 +546,7 @@ function MultiLineEditTextView(options) { .split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); var wrapped; - + for(var i = 0; i < text.length; ++i) { wrapped = self.wordWrapSingleLine( text[i], // input @@ -556,7 +561,7 @@ function MultiLineEditTextView(options) { }; this.getAbsolutePosition = function(row, col) { - return { + return { row : self.position.row + row, col : self.position.col + col, }; @@ -610,7 +615,7 @@ function MultiLineEditTextView(options) { this.keyPressDown = function() { var lastVisibleRow = Math.min( - self.dimens.height, + self.dimens.height, (self.textLines.length - self.topVisibleIndex)) - 1; if(self.cursorPos.row < lastVisibleRow) { @@ -714,7 +719,7 @@ function MultiLineEditTextView(options) { var nextEolIndex = self.getNextEndOfLineIndex(index); var text = self.getContiguousText(index, nextEolIndex); var newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; - + newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); for(var i = 1; i < newLines.length; ++i) { newLines[i] = { text : newLines[i] }; @@ -722,7 +727,7 @@ function MultiLineEditTextView(options) { newLines[newLines.length - 1].eol = true; Array.prototype.splice.apply( - self.textLines, + self.textLines, [ index, (nextEolIndex - index) + 1 ].concat(newLines)); // redraw from current row to end of visible area @@ -844,9 +849,9 @@ function MultiLineEditTextView(options) { self.client.term.rawWrite(ansi.left(move)); break; - case 'up' : + case 'up' : case 'down' : - // + // // Jump to the tabstop nearest the cursor // var newCol = self.tabStops.reduce(function r(prev, curr) { @@ -890,7 +895,7 @@ function MultiLineEditTextView(options) { 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) { @@ -1007,9 +1012,9 @@ MultiLineEditTextView.prototype.setText = function(text) { MultiLineEditTextView.prototype.addText = function(text) { this.insertRawText(text); - if(this.isEditMode()) { + if(this.autoScroll) { this.cursorEndOfDocument(); - } else if(this.isPreviewMode()) { + } else { this.cursorStartOfDocument(); } }; @@ -1027,7 +1032,7 @@ MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { }; var HANDLED_SPECIAL_KEYS = [ - 'up', 'down', 'left', 'right', + 'up', 'down', 'left', 'right', 'home', 'end', 'page up', 'page down', 'line feed', @@ -1045,7 +1050,7 @@ MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { var self = this; var handled; - if(key) { + if(key) { HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) { if(self.isKeyMapped(specialKey, key.name)) { @@ -1068,6 +1073,22 @@ MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { } }; +MultiLineEditTextView.prototype.scrollUp = function() { + this.scrollDocumentUp(); +} + +MultiLineEditTextView.prototype.scrollDown = function() { + this.scrollDocumentDown(); +} + +MultiLineEditTextView.prototype.deleteLine = function(line) { + this.textLines.splice(line, 1); +} + +MultiLineEditTextView.prototype.getLineCount = function() { + return this.textLines.length; +} + MultiLineEditTextView.prototype.getTextEditMode = function() { return this.overtypeMode ? 'overtype' : 'insert'; }; @@ -1075,11 +1096,10 @@ MultiLineEditTextView.prototype.getTextEditMode = function() { MultiLineEditTextView.prototype.getEditPosition = function() { var currentIndex = this.getTextLinesIndex() + 1; - return { - row : this.getTextLinesIndex(this.cursorPos.row), + return { + row : this.getTextLinesIndex(this.cursorPos.row), col : this.cursorPos.col, percent : Math.floor(((currentIndex / this.textLines.length) * 100)), below : this.getRemainingLinesBelowRow(), }; }; - diff --git a/mods/art/erc.ans b/mods/art/erc.ans new file mode 100644 index 0000000000000000000000000000000000000000..d2f336d2255ffac5d5eed3b240d40fe2d6e267ba GIT binary patch literal 376 zcmb1+Hn27^ur@Z$y-j5x9c^r$tLhtK$b}66Wr0>oN1K>h8yn=N1|=&vXC#&=IOk-h z=9MVu7nWw0D3s(YfN3jDpgMBgC>?ER4RnN^G|-bk>l7ePat$%&k_vVVb#^r{P@o<# jGB7kVFf%gaWn^GrWDH=CU;qPQPbXi6Fn31?4^9FA1c{gz literal 0 HcmV?d00001 diff --git a/mods/erc_client.js b/mods/erc_client.js new file mode 100644 index 00000000..61e2881e --- /dev/null +++ b/mods/erc_client.js @@ -0,0 +1,184 @@ +/* jslint node: true */ +'use strict'; +var MenuModule = require('../core/menu_module.js').MenuModule; + +const async = require('async'); +const _ = require('lodash'); +const net = require('net'); + +const packageJson = require('../package.json'); + +/* + Expected configuration block: + + ercClient: { + art: erc + module: erc_client + config: { + host: 192.168.1.171 + port: 5001 + bbsTag: SUPER + } + + form: { + 0: { + mci: { + MT1: { + width: 79 + height: 21 + mode: preview + autoScroll: true + } + ET3: { + autoScale: false + width: 77 + argName: chattxt + focus: true + submit: true + } + } + + submit: { + *: [ + { + value: { chattxt: null } + action: @method:processInput + } + ] + } + actionKeys: [ + { + keys: [ "tab" ] + } + { + keys: [ "up arrow" ] + action: @method:scrollDown + } + { + keys: [ "down arrow" ] + action: @method:scrollUp + } + ] + } + } + } +*/ + +exports.getModule = ErcClientModule; + +exports.moduleInfo = { + name : 'ENiGMA Relay Chat Client', + desc : 'Chat with other ENiGMA BBSes', + author : 'Andrew Pamment', +}; + +var MciViewIds = { + chatDisplay : 1, + inputArea : 3, +}; + +function ErcClientModule(options) { + MenuModule.call(this, options); + + var self = this; + this.config = options.menuConfig.config; + this.chatConnection = null; + this.finishedLoading = function() { + async.series( + [ + function validateConfig(callback) { + if(_.isString(self.config.host) && + _.isNumber(self.config.port) && + _.isString(self.config.bbsTag)) + { + callback(null); + } else { + callback(new Error('Configuration is missing required option(s)')); + } + }, + function connectToServer(callback) { + const connectOpts = { + port : self.config.port, + host : self.config.host, + }; + + + var chatMessageView = self.viewControllers.menu.getView(MciViewIds.chatDisplay); + chatMessageView.setText("Connecting to server..."); + chatMessageView.redraw(); + self.viewControllers.menu.switchFocus(MciViewIds.inputArea); + self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); + + self.chatConnection.on('data', data => { + var chatMessageView = self.viewControllers.menu.getView(MciViewIds.chatDisplay); + + if (data.toString().substring(0, 12) == "ERCHANDSHAKE") { + self.chatConnection.write("ERCMAGIC|" + self.config.bbsTag + "|" + self.client.user.username + "\r\n"); + } else { + chatMessageView.addText(data.toString()); + if (chatMessageView.getLineCount() > 30) { + chatMessageView.deleteLine(0); + chatMessageView.scrollDown(); + } + chatMessageView.redraw(); + self.viewControllers.menu.switchFocus(MciViewIds.inputArea); + } + }); + + self.chatConnection.once('end', () => { + return callback(null); + }); + + self.chatConnection.once('error', err => { + self.client.log.info(`Telnet bridge connection error: ${err.message}`); + }); + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'Telnet connection error'); + } + + self.prevMenu(); + } + ); + }; + + + this.menuMethods = { + processInput : function(data, cb) { + let chatInput = self.viewControllers.menu.getView(MciViewIds.inputArea); + let chatData = chatInput.getData(); + if (chatData[0] === '/') { + if (chatData[1] === 'q' || chatInput[1] === 'Q') { + self.chatConnection.end(); + } + } else { + self.chatConnection.write(chatData + "\r\n"); + chatInput.clearText(); + } + }, + scrollUp : function(data, cb) { + let chatInput = self.viewControllers.menu.getView(MciViewIds.inputArea); + let chatMessageView = self.viewControllers.menu.getView(MciViewIds.chatDisplay); + chatMessageView.scrollUp(); + chatMessageView.redraw(); + chatInput.setFocus(true); + }, + scrollDown : function(data, cb) { + let chatInput = self.viewControllers.menu.getView(MciViewIds.inputArea); + let chatMessageView = self.viewControllers.menu.getView(MciViewIds.chatDisplay); + chatMessageView.scrollDown(); + chatMessageView.redraw(); + chatInput.setFocus(true); + } + }; + + +} + +require('util').inherits(ErcClientModule, MenuModule); + +ErcClientModule.prototype.mciReady = function(mciData, cb) { + this.standardMCIReadyHandler(mciData, cb); +}; diff --git a/mods/menu.hjson b/mods/menu.hjson index 5618f111..00e67b72 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -1,4 +1,4 @@ -{ +{ /* ENiGMA½ Menu Configuration @@ -29,7 +29,7 @@ // // Another SSH specialization: If the user logs in with a new user - // name (e.g. "new", "apply", ...) they will be directed to the + // name (e.g. "new", "apply", ...) they will be directed to the // application process. // sshConnectedNewUser: { @@ -157,7 +157,7 @@ } } - logoff: { + logoff: { art: LOGOFF desc: Logging Off next: @systemMethod:logoff @@ -264,7 +264,7 @@ action: @systemMethod:prevMenu } ] - } + } } } @@ -361,7 +361,7 @@ action: @systemMethod:prevMenu } ] - } + } } } @@ -375,10 +375,10 @@ status: Feedback to SysOp module: msg_area_post_fse next: [ - { + { acs: AS2 next: fullLoginSequenceLoginArt - } + } { next: newUserInactiveDone } @@ -510,7 +510,7 @@ module: last_callers art: LASTCALL options: { pause: true } - next: fullLoginSequenceWhosOnline + next: fullLoginSequenceWhosOnline } fullLoginSequenceWhosOnline: { desc: Who's Online @@ -644,6 +644,10 @@ value: { command: "K" } action: @menu:mainMenuFeedbackToSysOp } + { + value: { command: "CHAT"} + action: @menu:ercClient + } { value: 1 action: @menu:mainMenu @@ -665,7 +669,7 @@ mainMenuUserStats: { desc: User Stats art: STATUS - options: { pause: true } + options: { pause: true } } mainMenuSystemStats: { desc: System Stats @@ -907,6 +911,58 @@ } } + ercClient: { + art: erc + module: erc_client + config: { + host: 192.168.1.171 + port: 5001 + bbsTag: SUPER + } + + form: { + 0: { + mci: { + MT1: { + width: 79 + height: 21 + mode: preview + autoScroll: true + } + ET3: { + autoScale: false + width: 77 + argName: chattxt + focus: true + submit: true + } + } + + submit: { + *: [ + { + value: { chattxt: null } + action: @method:processInput + } + ] + } + actionKeys: [ + { + keys: [ "tab" ] + } + { + keys: [ "up arrow" ] + action: @method:scrollDown + } + { + keys: [ "down arrow" ] + action: @method:scrollUp + } + ] + } + } + } + /////////////////////////////////////////////////////////////////////// // Doors Menu /////////////////////////////////////////////////////////////////////// @@ -948,12 +1004,12 @@ doorPimpWars: { desc: Playing PimpWars - module: abracadabra + module: abracadabra config: { name: PimpWars dropFileType: DORINFO cmd: /home/nuskooler/DOS/scripts/pimpwars.sh - args: [ + args: [ "{node}", "{dropFile}", "{srvPort}", @@ -966,12 +1022,12 @@ doorDarkLands: { desc: Playing Dark Lands - module: abracadabra + module: abracadabra config: { name: DARKLANDS dropFileType: DOOR cmd: /home/nuskooler/dev/enigma-bbs/doors/darklands/start.sh - args: [ + args: [ "{node}", "{dropFile}", "{srvPort}", @@ -981,7 +1037,7 @@ io: socket } } - + doorLORD: { desc: Playing L.O.R.D. module: abracadabra @@ -1056,7 +1112,7 @@ { value: 1 action: @menu:messageArea - } + } ] } @@ -1244,7 +1300,7 @@ { keys: [ "n", "shift + n" ] action: @method:nextMessage - } + } { keys: [ "r", "shift + r" ] action: @method:replyMessage @@ -1259,7 +1315,7 @@ { keys: [ "?" ] action: @method:viewModeMenuHelp - } + } { keys: [ "down arrow", "up arrow", "page up", "page down" ] action: @method:movementKeyPressed @@ -1295,7 +1351,7 @@ validate: @systemMethod:validateNonEmpty } ET3: { - argName: subject + argName: subject maxLength: 72 submit: true validate: @systemMethod:validateNonEmpty @@ -1395,7 +1451,7 @@ width: 79 height: 4 argName: quote - } + } } submit: { @@ -1552,7 +1608,7 @@ "mci" : { "VM1" : { "items" : [ - "Single Line Text Editing Views", + "Single Line Text Editing Views", "Spinner & Toggle Views", "Mask Edit Views", "Multi Line Text Editor", @@ -1735,7 +1791,7 @@ "form" : { "0" : { "BTMT" : { - "mci" : { + "mci" : { "MT1" : { "width" : 70, "height" : 17, @@ -2019,6 +2075,6 @@ } } } - } + } } -} \ No newline at end of file +}