* Introduce MCI cache: Art files are hashed and MCI info (per client) is cached. Term resizes invalidate the cache
* Bring in farmhash for art file hashing (and perhaps other uses soon) * Re-write of art.js display(). Better system, use MCI cache, etc. * Update package.json engines node req.
This commit is contained in:
parent
5112506e14
commit
e7ba6c406e
154
core/art.js
154
core/art.js
|
@ -7,6 +7,7 @@ var miscUtil = require('./misc_util.js');
|
||||||
var ansi = require('./ansi_term.js');
|
var ansi = require('./ansi_term.js');
|
||||||
var aep = require('./ansi_escape_parser.js');
|
var aep = require('./ansi_escape_parser.js');
|
||||||
var sauce = require('./sauce.js');
|
var sauce = require('./sauce.js');
|
||||||
|
const farmhash = require('farmhash');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
@ -225,10 +226,157 @@ function defaultEofFromExtension(ext) {
|
||||||
return SUPPORTED_ART_TYPES[ext.toLowerCase()].eof;
|
return SUPPORTED_ART_TYPES[ext.toLowerCase()].eof;
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: change to display(art, options, cb)
|
// :TODO: Implement the following
|
||||||
// cb(err, mci)
|
// * Pause (disabled | termHeight | keyPress )
|
||||||
|
// * Cancel (disabled | <keys> )
|
||||||
|
// * Resume from pause -> continous (disabled | <keys>)
|
||||||
|
function display(client, art, options, cb) {
|
||||||
|
if(_.isFunction(options) && !cb) {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
function display(options, cb) {
|
if(!art || !art.length) {
|
||||||
|
return cb(new Error('Empty art'));
|
||||||
|
}
|
||||||
|
|
||||||
|
options.mciReplaceChar = options.mciReplaceChar || ' ';
|
||||||
|
options.disableMciCache = options.disableMciCache || false;
|
||||||
|
|
||||||
|
if(!_.isBoolean(options.iceColors)) {
|
||||||
|
// try to detect from SAUCE
|
||||||
|
if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) {
|
||||||
|
options.iceColors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ansiParser = new aep.ANSIEscapeParser({
|
||||||
|
mciReplaceChar : options.mciReplaceChar,
|
||||||
|
termHeight : client.term.termHeight,
|
||||||
|
termWidth : client.term.termWidth,
|
||||||
|
trailingLF : options.trailingLF,
|
||||||
|
});
|
||||||
|
|
||||||
|
let parseComplete = false;
|
||||||
|
let cprListener;
|
||||||
|
let mciMap;
|
||||||
|
const mciCprQueue = [];
|
||||||
|
let artHash;
|
||||||
|
|
||||||
|
function completed() {
|
||||||
|
if(cprListener) {
|
||||||
|
client.removeListener('cursor position report', cprListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!options.disableMciCache) {
|
||||||
|
// cache our MCI findings...
|
||||||
|
client.mciCache[artHash] = mciMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
ansiParser.removeAllListeners(); // :TODO: Necessary???
|
||||||
|
|
||||||
|
const extraInfo = {
|
||||||
|
height : ansiParser.row - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return cb(null, mciMap, extraInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(!options.disableMciCache) {
|
||||||
|
artHash = farmhash.hash32(art);
|
||||||
|
|
||||||
|
// see if we have a mciMap cached for this art
|
||||||
|
if(client.mciCache) {
|
||||||
|
mciMap = client.mciCache[artHash];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!mciMap) {
|
||||||
|
// no cached MCI info
|
||||||
|
mciMap = {};
|
||||||
|
|
||||||
|
cprListener = function(pos) {
|
||||||
|
if(mciCprQueue.length > 0) {
|
||||||
|
mciMap[mciCprQueue.shift()].position = pos;
|
||||||
|
|
||||||
|
if(parseComplete && 0 === mciCprQueue.length) {
|
||||||
|
return completed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.on('cursor position report', cprListener);
|
||||||
|
|
||||||
|
let generatedId = 100;
|
||||||
|
|
||||||
|
ansiParser.on('mci', mciInfo => {
|
||||||
|
// :TODO: ensure generatedId's do not conflict with any existing |id|
|
||||||
|
const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
|
||||||
|
const mapKey = `${mciInfo.mci}${id}`;
|
||||||
|
const mapEntry = mciMap[mapKey];
|
||||||
|
|
||||||
|
if(mapEntry) {
|
||||||
|
mapEntry.focusSGR = mciInfo.SGR;
|
||||||
|
mapEntry.focusArgs = mciInfo.args;
|
||||||
|
} else {
|
||||||
|
mciMap[mapKey] = {
|
||||||
|
args : mciInfo.args,
|
||||||
|
SGR : mciInfo.SGR,
|
||||||
|
code : mciInfo.mci,
|
||||||
|
id : id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!mciInfo.id) {
|
||||||
|
++generatedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
mciCprQueue.push(mapKey);
|
||||||
|
client.term.write(ansi.queryPos(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ansiParser.on('literal', literal => client.term.write(literal, false) );
|
||||||
|
ansiParser.on('control', control => client.term.write(control, false) );
|
||||||
|
|
||||||
|
ansiParser.on('complete', () => {
|
||||||
|
parseComplete = true;
|
||||||
|
|
||||||
|
if(0 === mciCprQueue.length) {
|
||||||
|
return completed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ansiFontSeq;
|
||||||
|
if(options.font) {
|
||||||
|
ansiFontSeq = ansi.setSyncTermFontWithAlias(options.font);
|
||||||
|
} else if(options.sauce) {
|
||||||
|
let fontName = getFontNameFromSAUCE(options.sauce);
|
||||||
|
if(fontName) {
|
||||||
|
fontName = ansi.getSyncTERMFontFromAlias(fontName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't set default (CP437) from SAUCE
|
||||||
|
if(fontName && 'cp437' !== fontName) {
|
||||||
|
ansiFontSeq = ansi.setSyncTERMFont(fontName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ansiFontSeq) {
|
||||||
|
client.term.write(ansiFontSeq, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.iceColors) {
|
||||||
|
client.term.write(ansi.blinkToBrightIntensity(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ansiParser.reset(art);
|
||||||
|
ansiParser.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayBACKUP(options, cb) {
|
||||||
assert(_.isObject(options));
|
assert(_.isObject(options));
|
||||||
assert(_.isObject(options.client));
|
assert(_.isObject(options.client));
|
||||||
assert(!_.isUndefined(options.art));
|
assert(!_.isUndefined(options.art));
|
||||||
|
|
|
@ -82,6 +82,11 @@ function Client(input, output) {
|
||||||
this.lastKeyPressMs = Date.now();
|
this.lastKeyPressMs = Date.now();
|
||||||
this.menuStack = new MenuStack(this);
|
this.menuStack = new MenuStack(this);
|
||||||
this.acs = new ACS(this);
|
this.acs = new ACS(this);
|
||||||
|
this.mciCache = {};
|
||||||
|
|
||||||
|
this.clearMciCache = function() {
|
||||||
|
this.mciCache = {};
|
||||||
|
};
|
||||||
|
|
||||||
Object.defineProperty(this, 'node', {
|
Object.defineProperty(this, 'node', {
|
||||||
get : function() {
|
get : function() {
|
||||||
|
|
|
@ -150,11 +150,13 @@ ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) {
|
||||||
|
|
||||||
ClientTerminal.prototype.rawWrite = function(s, cb) {
|
ClientTerminal.prototype.rawWrite = function(s, cb) {
|
||||||
if(this.output) {
|
if(this.output) {
|
||||||
this.output.write(s, function written(err) {
|
this.output.write(s, err => {
|
||||||
if(_.isFunction(cb)) {
|
if(cb) {
|
||||||
cb(err);
|
return cb(err);
|
||||||
} else if(err) {
|
}
|
||||||
Log.warn('Failed writing to socket: ' + err.toString());
|
|
||||||
|
if(err) {
|
||||||
|
Log.warn( { error : err.message }, 'Failed writing to socket');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,6 +149,8 @@ function SSHClient(clientConn) {
|
||||||
if(termHeight > 0 && termWidth > 0) {
|
if(termHeight > 0 && termWidth > 0) {
|
||||||
self.term.termHeight = termHeight;
|
self.term.termHeight = termHeight;
|
||||||
self.term.termWidth = termWidth;
|
self.term.termWidth = termWidth;
|
||||||
|
|
||||||
|
self.clearMciCache(); // term size changes = invalidate cache
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) {
|
if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) {
|
||||||
|
|
|
@ -599,9 +599,11 @@ TelnetClient.prototype.handleSbCommand = function(evt) {
|
||||||
self.setTermType(evt.envVars[name]);
|
self.setTermType(evt.envVars[name]);
|
||||||
} else if('COLUMNS' === name && 0 === self.term.termWidth) {
|
} else if('COLUMNS' === name && 0 === self.term.termWidth) {
|
||||||
self.term.termWidth = parseInt(evt.envVars[name]);
|
self.term.termWidth = parseInt(evt.envVars[name]);
|
||||||
|
self.clearMciCache(); // term size changes = invalidate cache
|
||||||
self.log.debug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated');
|
self.log.debug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated');
|
||||||
} else if('ROWS' === name && 0 === self.term.termHeight) {
|
} else if('ROWS' === name && 0 === self.term.termHeight) {
|
||||||
self.term.termHeight = parseInt(evt.envVars[name]);
|
self.term.termHeight = parseInt(evt.envVars[name]);
|
||||||
|
self.clearMciCache(); // term size changes = invalidate cache
|
||||||
self.log.debug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated');
|
self.log.debug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated');
|
||||||
} else {
|
} else {
|
||||||
if(name in self.term.env) {
|
if(name in self.term.env) {
|
||||||
|
@ -636,6 +638,8 @@ TelnetClient.prototype.handleSbCommand = function(evt) {
|
||||||
self.term.env.ROWS = evt.height;
|
self.term.env.ROWS = evt.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.clearMciCache(); // term size changes = invalidate cache
|
||||||
|
|
||||||
self.log.debug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated');
|
self.log.debug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated');
|
||||||
} else {
|
} else {
|
||||||
self.log(evt, 'SB');
|
self.log(evt, 'SB');
|
||||||
|
|
|
@ -455,6 +455,7 @@ function displayThemeArt(options, cb) {
|
||||||
cb(err);
|
cb(err);
|
||||||
} else {
|
} else {
|
||||||
// :TODO: just use simple merge of options -> displayOptions
|
// :TODO: just use simple merge of options -> displayOptions
|
||||||
|
/*
|
||||||
var dispOptions = {
|
var dispOptions = {
|
||||||
art : artInfo.data,
|
art : artInfo.data,
|
||||||
sauce : artInfo.sauce,
|
sauce : artInfo.sauce,
|
||||||
|
@ -466,6 +467,16 @@ function displayThemeArt(options, cb) {
|
||||||
art.display(dispOptions, function displayed(err, mciMap, extraInfo) {
|
art.display(dispOptions, function displayed(err, mciMap, extraInfo) {
|
||||||
cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
|
cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
const displayOpts = {
|
||||||
|
sauce : artInfo.sauce,
|
||||||
|
font : options.font,
|
||||||
|
trailingLF : options.trailingLF,
|
||||||
|
};
|
||||||
|
|
||||||
|
art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => {
|
||||||
|
return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,9 +31,10 @@
|
||||||
"string-format": "davidchambers/string-format#mini-language",
|
"string-format": "davidchambers/string-format#mini-language",
|
||||||
"temp": "^0.8.3",
|
"temp": "^0.8.3",
|
||||||
"inquirer" : "^1.1.0",
|
"inquirer" : "^1.1.0",
|
||||||
"fs-extra" : "0.26.x"
|
"fs-extra" : "0.26.x",
|
||||||
|
"farmhash" : "^1.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.2"
|
"node": ">=4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue