Added ERC Module

This commit is contained in:
Andrew Pamment 2016-06-27 17:29:17 +10:00
parent b6cada6f3c
commit be6af161ec
4 changed files with 318 additions and 58 deletions

View file

@ -34,7 +34,7 @@ var _ = require('lodash');
// //
// Editors - BBS // Editors - BBS
// * https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp // * https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp
// //
// //
// Editors - Other // Editors - Other
// * http://joe-editor.sourceforge.net/ // * http://joe-editor.sourceforge.net/
@ -55,12 +55,12 @@ var _ = require('lodash');
// //
// To-Do // To-Do
// //
// * Index pos % for emit scroll events // * Index pos % for emit scroll events
// * Some of this shoudl be async'd where there is lots of processing (e.g. word wrap) // * 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) // * Fix backspace when col=0 (e.g. bs to prev line)
// * Add back word delete // * Add back word delete
// * // *
var SPECIAL_KEY_MAP_DEFAULT = { var SPECIAL_KEY_MAP_DEFAULT = {
@ -114,6 +114,11 @@ function MultiLineEditTextView(options) {
this.topVisibleIndex = 0; this.topVisibleIndex = 0;
this.mode = options.mode || 'edit'; // edit | preview | read-only 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 // cursorPos represents zero-based row, col positions
// within the editor itself // within the editor itself
@ -179,7 +184,7 @@ function MultiLineEditTextView(options) {
this.eraseRows = function(startRow, endRow) { this.eraseRows = function(startRow, endRow) {
self.client.term.rawWrite(self.getSGRFor('text') + ansi.hideCursor()); self.client.term.rawWrite(self.getSGRFor('text') + ansi.hideCursor());
var absPos = self.getAbsolutePosition(startRow, 0); var absPos = self.getAbsolutePosition(startRow, 0);
var absPosEnd = self.getAbsolutePosition(endRow, 0); var absPosEnd = self.getAbsolutePosition(endRow, 0);
var eraseFiller = new Array(self.dimens.width).join(' '); var eraseFiller = new Array(self.dimens.width).join(' ');
@ -216,7 +221,7 @@ function MultiLineEditTextView(options) {
if(!_.isNumber(index)) { if(!_.isNumber(index)) {
index = self.getTextLinesIndex(); index = self.getTextLinesIndex();
} }
return self.textLines[index].text.replace(/\t/g, ' '); return self.textLines[index].text.replace(/\t/g, ' ');
}; };
this.getText = function(index) { this.getText = function(index) {
@ -266,19 +271,19 @@ function MultiLineEditTextView(options) {
} }
return lines; return lines;
}; };
this.getOutputText = function(startIndex, endIndex, eolMarker) { this.getOutputText = function(startIndex, endIndex, eolMarker) {
let lines = self.getTextLines(startIndex, endIndex); let lines = self.getTextLines(startIndex, endIndex);
let text = ''; let text = '';
var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g');
lines.forEach(line => { lines.forEach(line => {
text += line.text.replace(re, '\t'); text += line.text.replace(re, '\t');
if(eolMarker && line.eol) { if(eolMarker && line.eol) {
text += eolMarker; text += eolMarker;
} }
}); });
return text; return text;
} }
@ -302,7 +307,7 @@ function MultiLineEditTextView(options) {
/* /*
this.editTextAtPosition = function(editAction, text, index, col) { this.editTextAtPosition = function(editAction, text, index, col) {
switch(editAction) { switch(editAction) {
case 'insert' : case 'insert' :
self.insertCharactersInText(text, index, col); self.insertCharactersInText(text, index, col);
break; break;
@ -329,7 +334,7 @@ function MultiLineEditTextView(options) {
newLines[newLines.length - 1].eol = true; newLines[newLines.length - 1].eol = true;
Array.prototype.splice.apply( Array.prototype.splice.apply(
self.textLines, self.textLines,
[ index, (nextEolIndex - index) + 1 ].concat(newLines)); [ index, (nextEolIndex - index) + 1 ].concat(newLines));
return wrapped.firstWrapRange; return wrapped.firstWrapRange;
@ -337,7 +342,7 @@ function MultiLineEditTextView(options) {
this.removeCharactersFromText = function(index, col, operation, count) { this.removeCharactersFromText = function(index, col, operation, count) {
if('right' === operation) { 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) +
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) { } else if ('backspace' === operation) {
// :TODO: method for splicing text // :TODO: method for splicing text
self.textLines[index].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.textLines[index].text.slice(col + 1);
self.cursorPos.col -= (count - 1); self.cursorPos.col -= (count - 1);
self.updateTextWordWrap(index); self.updateTextWordWrap(index);
self.redrawRows(self.cursorPos.row, self.dimens.height); self.redrawRows(self.cursorPos.row, self.dimens.height);
@ -405,9 +410,9 @@ function MultiLineEditTextView(options) {
this.insertCharactersInText = function(c, index, col) { this.insertCharactersInText = function(c, index, col) {
self.textLines[index].text = [ self.textLines[index].text = [
self.textLines[index].text.slice(0, col), self.textLines[index].text.slice(0, col),
c, c,
self.textLines[index].text.slice(col) self.textLines[index].text.slice(col)
].join(''); ].join('');
//self.cursorPos.col++; //self.cursorPos.col++;
@ -443,13 +448,13 @@ function MultiLineEditTextView(options) {
// //
absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col);
self.client.term.write( self.client.term.write(
ansi.hideCursor() + ansi.hideCursor() +
self.getSGRFor('text') + self.getSGRFor('text') +
self.getRenderText(index).slice(self.cursorPos.col - c.length) + self.getRenderText(index).slice(self.cursorPos.col - c.length) +
ansi.goto(absPos.row, absPos.col) + ansi.goto(absPos.row, absPos.col) +
ansi.showCursor(), false ansi.showCursor(), false
); );
} }
}; };
this.getRemainingTabWidth = function(col) { this.getRemainingTabWidth = function(col) {
@ -541,7 +546,7 @@ function MultiLineEditTextView(options) {
.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); .split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
var wrapped; var wrapped;
for(var i = 0; i < text.length; ++i) { for(var i = 0; i < text.length; ++i) {
wrapped = self.wordWrapSingleLine( wrapped = self.wordWrapSingleLine(
text[i], // input text[i], // input
@ -556,7 +561,7 @@ function MultiLineEditTextView(options) {
}; };
this.getAbsolutePosition = function(row, col) { this.getAbsolutePosition = function(row, col) {
return { return {
row : self.position.row + row, row : self.position.row + row,
col : self.position.col + col, col : self.position.col + col,
}; };
@ -610,7 +615,7 @@ function MultiLineEditTextView(options) {
this.keyPressDown = function() { this.keyPressDown = function() {
var lastVisibleRow = Math.min( var lastVisibleRow = Math.min(
self.dimens.height, self.dimens.height,
(self.textLines.length - self.topVisibleIndex)) - 1; (self.textLines.length - self.topVisibleIndex)) - 1;
if(self.cursorPos.row < lastVisibleRow) { if(self.cursorPos.row < lastVisibleRow) {
@ -714,7 +719,7 @@ function MultiLineEditTextView(options) {
var nextEolIndex = self.getNextEndOfLineIndex(index); var nextEolIndex = self.getNextEndOfLineIndex(index);
var text = self.getContiguousText(index, nextEolIndex); var text = self.getContiguousText(index, nextEolIndex);
var newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; var newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped;
newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } );
for(var i = 1; i < newLines.length; ++i) { for(var i = 1; i < newLines.length; ++i) {
newLines[i] = { text : newLines[i] }; newLines[i] = { text : newLines[i] };
@ -722,7 +727,7 @@ function MultiLineEditTextView(options) {
newLines[newLines.length - 1].eol = true; newLines[newLines.length - 1].eol = true;
Array.prototype.splice.apply( Array.prototype.splice.apply(
self.textLines, self.textLines,
[ index, (nextEolIndex - index) + 1 ].concat(newLines)); [ index, (nextEolIndex - index) + 1 ].concat(newLines));
// redraw from current row to end of visible area // redraw from current row to end of visible area
@ -844,9 +849,9 @@ function MultiLineEditTextView(options) {
self.client.term.rawWrite(ansi.left(move)); self.client.term.rawWrite(ansi.left(move));
break; break;
case 'up' : case 'up' :
case 'down' : case 'down' :
// //
// Jump to the tabstop nearest the cursor // Jump to the tabstop nearest the cursor
// //
var newCol = self.tabStops.reduce(function r(prev, curr) { var newCol = self.tabStops.reduce(function r(prev, curr) {
@ -890,7 +895,7 @@ function MultiLineEditTextView(options) {
this.cursorBeginOfNextLine = function() { this.cursorBeginOfNextLine = function() {
// e.g. when scrolling right past eol // e.g. when scrolling right past eol
var linesBelow = self.getRemainingLinesBelowRow(); var linesBelow = self.getRemainingLinesBelowRow();
if(linesBelow > 0) { if(linesBelow > 0) {
var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1;
if(self.cursorPos.row < lastVisibleRow) { if(self.cursorPos.row < lastVisibleRow) {
@ -1007,9 +1012,9 @@ MultiLineEditTextView.prototype.setText = function(text) {
MultiLineEditTextView.prototype.addText = function(text) { MultiLineEditTextView.prototype.addText = function(text) {
this.insertRawText(text); this.insertRawText(text);
if(this.isEditMode()) { if(this.autoScroll) {
this.cursorEndOfDocument(); this.cursorEndOfDocument();
} else if(this.isPreviewMode()) { } else {
this.cursorStartOfDocument(); this.cursorStartOfDocument();
} }
}; };
@ -1027,7 +1032,7 @@ MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {
}; };
var HANDLED_SPECIAL_KEYS = [ var HANDLED_SPECIAL_KEYS = [
'up', 'down', 'left', 'right', 'up', 'down', 'left', 'right',
'home', 'end', 'home', 'end',
'page up', 'page down', 'page up', 'page down',
'line feed', 'line feed',
@ -1045,7 +1050,7 @@ MultiLineEditTextView.prototype.onKeyPress = function(ch, key) {
var self = this; var self = this;
var handled; var handled;
if(key) { if(key) {
HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) { HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) {
if(self.isKeyMapped(specialKey, key.name)) { 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() { MultiLineEditTextView.prototype.getTextEditMode = function() {
return this.overtypeMode ? 'overtype' : 'insert'; return this.overtypeMode ? 'overtype' : 'insert';
}; };
@ -1075,11 +1096,10 @@ MultiLineEditTextView.prototype.getTextEditMode = function() {
MultiLineEditTextView.prototype.getEditPosition = function() { MultiLineEditTextView.prototype.getEditPosition = function() {
var currentIndex = this.getTextLinesIndex() + 1; var currentIndex = this.getTextLinesIndex() + 1;
return { return {
row : this.getTextLinesIndex(this.cursorPos.row), row : this.getTextLinesIndex(this.cursorPos.row),
col : this.cursorPos.col, col : this.cursorPos.col,
percent : Math.floor(((currentIndex / this.textLines.length) * 100)), percent : Math.floor(((currentIndex / this.textLines.length) * 100)),
below : this.getRemainingLinesBelowRow(), below : this.getRemainingLinesBelowRow(),
}; };
}; };

BIN
mods/art/erc.ans Normal file

Binary file not shown.

184
mods/erc_client.js Normal file
View file

@ -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);
};

View file

@ -1,4 +1,4 @@
{ {
/* /*
ENiGMA½ Menu Configuration ENiGMA½ Menu Configuration
@ -29,7 +29,7 @@
// //
// Another SSH specialization: If the user logs in with a new user // 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. // application process.
// //
sshConnectedNewUser: { sshConnectedNewUser: {
@ -157,7 +157,7 @@
} }
} }
logoff: { logoff: {
art: LOGOFF art: LOGOFF
desc: Logging Off desc: Logging Off
next: @systemMethod:logoff next: @systemMethod:logoff
@ -264,7 +264,7 @@
action: @systemMethod:prevMenu action: @systemMethod:prevMenu
} }
] ]
} }
} }
} }
@ -361,7 +361,7 @@
action: @systemMethod:prevMenu action: @systemMethod:prevMenu
} }
] ]
} }
} }
} }
@ -375,10 +375,10 @@
status: Feedback to SysOp status: Feedback to SysOp
module: msg_area_post_fse module: msg_area_post_fse
next: [ next: [
{ {
acs: AS2 acs: AS2
next: fullLoginSequenceLoginArt next: fullLoginSequenceLoginArt
} }
{ {
next: newUserInactiveDone next: newUserInactiveDone
} }
@ -510,7 +510,7 @@
module: last_callers module: last_callers
art: LASTCALL art: LASTCALL
options: { pause: true } options: { pause: true }
next: fullLoginSequenceWhosOnline next: fullLoginSequenceWhosOnline
} }
fullLoginSequenceWhosOnline: { fullLoginSequenceWhosOnline: {
desc: Who's Online desc: Who's Online
@ -644,6 +644,10 @@
value: { command: "K" } value: { command: "K" }
action: @menu:mainMenuFeedbackToSysOp action: @menu:mainMenuFeedbackToSysOp
} }
{
value: { command: "CHAT"}
action: @menu:ercClient
}
{ {
value: 1 value: 1
action: @menu:mainMenu action: @menu:mainMenu
@ -665,7 +669,7 @@
mainMenuUserStats: { mainMenuUserStats: {
desc: User Stats desc: User Stats
art: STATUS art: STATUS
options: { pause: true } options: { pause: true }
} }
mainMenuSystemStats: { mainMenuSystemStats: {
desc: System Stats 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 // Doors Menu
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
@ -948,12 +1004,12 @@
doorPimpWars: { doorPimpWars: {
desc: Playing PimpWars desc: Playing PimpWars
module: abracadabra module: abracadabra
config: { config: {
name: PimpWars name: PimpWars
dropFileType: DORINFO dropFileType: DORINFO
cmd: /home/nuskooler/DOS/scripts/pimpwars.sh cmd: /home/nuskooler/DOS/scripts/pimpwars.sh
args: [ args: [
"{node}", "{node}",
"{dropFile}", "{dropFile}",
"{srvPort}", "{srvPort}",
@ -966,12 +1022,12 @@
doorDarkLands: { doorDarkLands: {
desc: Playing Dark Lands desc: Playing Dark Lands
module: abracadabra module: abracadabra
config: { config: {
name: DARKLANDS name: DARKLANDS
dropFileType: DOOR dropFileType: DOOR
cmd: /home/nuskooler/dev/enigma-bbs/doors/darklands/start.sh cmd: /home/nuskooler/dev/enigma-bbs/doors/darklands/start.sh
args: [ args: [
"{node}", "{node}",
"{dropFile}", "{dropFile}",
"{srvPort}", "{srvPort}",
@ -981,7 +1037,7 @@
io: socket io: socket
} }
} }
doorLORD: { doorLORD: {
desc: Playing L.O.R.D. desc: Playing L.O.R.D.
module: abracadabra module: abracadabra
@ -1056,7 +1112,7 @@
{ {
value: 1 value: 1
action: @menu:messageArea action: @menu:messageArea
} }
] ]
} }
@ -1244,7 +1300,7 @@
{ {
keys: [ "n", "shift + n" ] keys: [ "n", "shift + n" ]
action: @method:nextMessage action: @method:nextMessage
} }
{ {
keys: [ "r", "shift + r" ] keys: [ "r", "shift + r" ]
action: @method:replyMessage action: @method:replyMessage
@ -1259,7 +1315,7 @@
{ {
keys: [ "?" ] keys: [ "?" ]
action: @method:viewModeMenuHelp action: @method:viewModeMenuHelp
} }
{ {
keys: [ "down arrow", "up arrow", "page up", "page down" ] keys: [ "down arrow", "up arrow", "page up", "page down" ]
action: @method:movementKeyPressed action: @method:movementKeyPressed
@ -1295,7 +1351,7 @@
validate: @systemMethod:validateNonEmpty validate: @systemMethod:validateNonEmpty
} }
ET3: { ET3: {
argName: subject argName: subject
maxLength: 72 maxLength: 72
submit: true submit: true
validate: @systemMethod:validateNonEmpty validate: @systemMethod:validateNonEmpty
@ -1395,7 +1451,7 @@
width: 79 width: 79
height: 4 height: 4
argName: quote argName: quote
} }
} }
submit: { submit: {
@ -1552,7 +1608,7 @@
"mci" : { "mci" : {
"VM1" : { "VM1" : {
"items" : [ "items" : [
"Single Line Text Editing Views", "Single Line Text Editing Views",
"Spinner & Toggle Views", "Spinner & Toggle Views",
"Mask Edit Views", "Mask Edit Views",
"Multi Line Text Editor", "Multi Line Text Editor",
@ -1735,7 +1791,7 @@
"form" : { "form" : {
"0" : { "0" : {
"BTMT" : { "BTMT" : {
"mci" : { "mci" : {
"MT1" : { "MT1" : {
"width" : 70, "width" : 70,
"height" : 17, "height" : 17,
@ -2019,6 +2075,6 @@
} }
} }
} }
} }
} }
} }