2014-10-17 02:21:06 +00:00
|
|
|
/* jslint node: true */
|
|
|
|
'use strict';
|
|
|
|
|
2016-08-04 01:38:06 +00:00
|
|
|
// ENiGMA½
|
2014-10-17 02:21:06 +00:00
|
|
|
var conf = require('./config.js');
|
|
|
|
var miscUtil = require('./misc_util.js');
|
|
|
|
var ansi = require('./ansi_term.js');
|
|
|
|
var aep = require('./ansi_escape_parser.js');
|
2016-02-03 04:35:59 +00:00
|
|
|
var sauce = require('./sauce.js');
|
2014-10-17 02:21:06 +00:00
|
|
|
|
2016-08-04 01:38:06 +00:00
|
|
|
// deps
|
|
|
|
var fs = require('fs');
|
|
|
|
var paths = require('path');
|
|
|
|
var assert = require('assert');
|
|
|
|
var iconv = require('iconv-lite');
|
2015-04-19 08:13:13 +00:00
|
|
|
var _ = require('lodash');
|
|
|
|
|
2014-10-17 02:21:06 +00:00
|
|
|
exports.getArt = getArt;
|
|
|
|
exports.getArtFromPath = getArtFromPath;
|
|
|
|
exports.display = display;
|
|
|
|
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
|
|
|
|
|
|
|
// :TODO: Return MCI code information
|
|
|
|
// :TODO: process SAUCE comments
|
|
|
|
// :TODO: return font + font mapped information from SAUCE
|
|
|
|
|
|
|
|
var SUPPORTED_ART_TYPES = {
|
|
|
|
// :TODO: the defualt encoding are really useless if they are all the same ...
|
|
|
|
// perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf
|
|
|
|
'.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a },
|
|
|
|
'.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a },
|
|
|
|
'.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a },
|
|
|
|
'.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a },
|
2015-11-01 20:32:52 +00:00
|
|
|
|
|
|
|
'.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a },
|
|
|
|
'.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a },
|
2014-10-17 02:21:06 +00:00
|
|
|
// :TODO: extentions for wwiv, renegade, celerity, syncronet, ...
|
|
|
|
// :TODO: extension for atari
|
|
|
|
// :TODO: extension for topaz ansi/ascii.
|
|
|
|
};
|
|
|
|
|
2015-04-17 04:29:53 +00:00
|
|
|
function getFontNameFromSAUCE(sauce) {
|
|
|
|
if(sauce.Character) {
|
|
|
|
return sauce.Character.fontName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-17 02:21:06 +00:00
|
|
|
function sliceAtEOF(data, eofMarker) {
|
|
|
|
var eof = data.length;
|
|
|
|
// :TODO: max scan back or other beter way of doing this?!
|
|
|
|
for(var i = data.length - 1; i > 0; i--) {
|
|
|
|
if(data[i] === eofMarker) {
|
|
|
|
eof = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return data.slice(0, eof);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getArtFromPath(path, options, cb) {
|
|
|
|
fs.readFile(path, function onData(err, data) {
|
|
|
|
if(err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Convert from encodedAs -> j
|
|
|
|
//
|
|
|
|
var ext = paths.extname(path).toLowerCase();
|
|
|
|
var encoding = options.encodedAs || defaultEncodingFromExtension(ext);
|
2015-04-17 04:29:53 +00:00
|
|
|
|
2014-10-17 02:21:06 +00:00
|
|
|
// :TODO: how are BOM's currently handled if present? Are they removed? Do we need to?
|
|
|
|
|
|
|
|
function sliceOfData() {
|
|
|
|
if(options.fullFile === true) {
|
|
|
|
return iconv.decode(data, encoding);
|
|
|
|
} else {
|
|
|
|
var eofMarker = defaultEofFromExtension(ext);
|
|
|
|
return iconv.decode(sliceAtEOF(data, eofMarker), encoding);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getResult(sauce) {
|
|
|
|
var result = {
|
|
|
|
data : sliceOfData(),
|
|
|
|
fromPath : path,
|
|
|
|
};
|
|
|
|
|
|
|
|
if(sauce) {
|
|
|
|
result.sauce = sauce;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
2015-07-29 04:31:28 +00:00
|
|
|
}
|
2014-10-17 02:21:06 +00:00
|
|
|
|
|
|
|
if(options.readSauce === true) {
|
2016-02-03 04:35:59 +00:00
|
|
|
sauce.readSAUCE(data, function onSauce(err, sauce) {
|
2014-10-17 02:21:06 +00:00
|
|
|
if(err) {
|
|
|
|
cb(null, getResult());
|
|
|
|
} else {
|
|
|
|
//
|
|
|
|
// If a encoding was not provided & we have a mapping from
|
|
|
|
// the information provided by SAUCE, use that.
|
|
|
|
//
|
|
|
|
if(!options.encodedAs) {
|
2015-04-17 04:29:53 +00:00
|
|
|
/*
|
2014-10-17 02:21:06 +00:00
|
|
|
if(sauce.Character && sauce.Character.fontName) {
|
|
|
|
var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName];
|
|
|
|
if(enc) {
|
|
|
|
encoding = enc;
|
|
|
|
}
|
|
|
|
}
|
2015-04-17 04:29:53 +00:00
|
|
|
*/
|
2014-10-17 02:21:06 +00:00
|
|
|
}
|
|
|
|
cb(null, getResult(sauce));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
cb(null, getResult());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function getArt(name, options, cb) {
|
|
|
|
var ext = paths.extname(name);
|
|
|
|
|
|
|
|
options.basePath = miscUtil.valueWithDefault(options.basePath, conf.config.paths.art);
|
|
|
|
options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);
|
|
|
|
|
|
|
|
// :TODO: make use of asAnsi option and convert from supported -> ansi
|
|
|
|
|
|
|
|
if('' !== ext) {
|
|
|
|
options.types = [ ext.toLowerCase() ];
|
|
|
|
} else {
|
2015-05-14 22:49:19 +00:00
|
|
|
if(_.isUndefined(options.types)) {
|
2014-10-17 02:21:06 +00:00
|
|
|
options.types = Object.keys(SUPPORTED_ART_TYPES);
|
2015-05-14 22:49:19 +00:00
|
|
|
} else if(_.isString(options.types)) {
|
2014-10-17 02:21:06 +00:00
|
|
|
options.types = [ options.types.toLowerCase() ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If an extension is provided, just read the file now
|
|
|
|
if('' !== ext) {
|
|
|
|
var directPath = paths.join(options.basePath, name);
|
|
|
|
getArtFromPath(directPath, options, cb);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.readdir(options.basePath, function onFiles(err, files) {
|
|
|
|
if(err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var filtered = files.filter(function onFile(file) {
|
|
|
|
//
|
|
|
|
// Ignore anything not allowed in |options.types|
|
|
|
|
//
|
|
|
|
var fext = paths.extname(file);
|
|
|
|
if(options.types.indexOf(fext.toLowerCase()) < 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
var bn = paths.basename(file, fext).toLowerCase();
|
|
|
|
if(options.random) {
|
|
|
|
var suppliedBn = paths.basename(name, fext).toLowerCase();
|
|
|
|
//
|
|
|
|
// Random selection enabled. We'll allow for
|
|
|
|
// basename1.ext, basename2.ext, ...
|
|
|
|
//
|
|
|
|
if(bn.indexOf(suppliedBn) !== 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
var num = bn.substr(suppliedBn.length);
|
|
|
|
if(num.length > 0) {
|
|
|
|
if(isNaN(parseInt(num, 10))) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
//
|
|
|
|
// We've already validated the extension (above). Must be an exact
|
|
|
|
// match to basename here
|
|
|
|
//
|
|
|
|
if(bn != paths.basename(name, fext).toLowerCase()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
2016-08-04 01:38:06 +00:00
|
|
|
|
2014-10-17 02:21:06 +00:00
|
|
|
if(filtered.length > 0) {
|
|
|
|
//
|
|
|
|
// We should now have:
|
|
|
|
// - Exactly (1) item in |filtered| if non-random
|
|
|
|
// - 1:n items in |filtered| to choose from if random
|
|
|
|
//
|
|
|
|
var readPath;
|
|
|
|
if(options.random) {
|
|
|
|
readPath = paths.join(options.basePath, filtered[Math.floor(Math.random() * filtered.length)]);
|
|
|
|
} else {
|
|
|
|
assert(1 === filtered.length);
|
|
|
|
readPath = paths.join(options.basePath, filtered[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
getArtFromPath(readPath, options, cb);
|
|
|
|
} else {
|
2016-08-04 01:38:06 +00:00
|
|
|
return cb(new Error(`No matching art for supplied criteria: ${name}`));
|
2014-10-17 02:21:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// :TODO: need a showArt()
|
|
|
|
// - center (if term width > 81)
|
|
|
|
// - interruptable
|
|
|
|
// - pausable: by user key and/or by page size (e..g term height)
|
|
|
|
|
|
|
|
|
|
|
|
function defaultEncodingFromExtension(ext) {
|
|
|
|
return SUPPORTED_ART_TYPES[ext.toLowerCase()].defaultEncoding;
|
|
|
|
}
|
|
|
|
|
|
|
|
function defaultEofFromExtension(ext) {
|
|
|
|
return SUPPORTED_ART_TYPES[ext.toLowerCase()].eof;
|
|
|
|
}
|
|
|
|
|
|
|
|
// :TODO: change to display(art, options, cb)
|
|
|
|
// cb(err, mci)
|
|
|
|
|
2014-11-05 06:50:42 +00:00
|
|
|
function display(options, cb) {
|
2015-05-03 23:35:55 +00:00
|
|
|
assert(_.isObject(options));
|
|
|
|
assert(_.isObject(options.client));
|
|
|
|
assert(!_.isUndefined(options.art));
|
2014-11-05 06:50:42 +00:00
|
|
|
|
|
|
|
if(0 === options.art.length) {
|
|
|
|
cb(new Error('Empty art'));
|
2014-10-17 02:21:06 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-05-03 23:35:55 +00:00
|
|
|
// pause = none/off | end | termHeight | [ "key1", "key2", ... ]
|
|
|
|
|
2014-10-17 02:21:06 +00:00
|
|
|
var cancelKeys = miscUtil.valueWithDefault(options.cancelKeys, []);
|
|
|
|
var pauseKeys = miscUtil.valueWithDefault(options.pauseKeys, []);
|
|
|
|
var pauseAtTermHeight = miscUtil.valueWithDefault(options.pauseAtTermHeight, false);
|
2014-11-05 06:50:42 +00:00
|
|
|
var mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ' ');
|
2015-04-19 08:13:13 +00:00
|
|
|
|
|
|
|
var iceColors = options.iceColors;
|
|
|
|
if(_.isUndefined(options.iceColors)) {
|
|
|
|
// detect from SAUCE, if present
|
|
|
|
iceColors = false;
|
|
|
|
if(_.isObject(options.sauce) && _.isNumber(options.sauce.ansiFlags)) {
|
|
|
|
if(options.sauce.ansiFlags & (1 << 0)) {
|
|
|
|
iceColors = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//var iceColors = miscUtil.valueWithDefault(options.iceColors, false);
|
2014-10-17 02:21:06 +00:00
|
|
|
|
|
|
|
// :TODO: support pause/cancel & pause @ termHeight
|
|
|
|
var canceled = false;
|
|
|
|
|
|
|
|
var parser = new aep.ANSIEscapeParser({
|
|
|
|
mciReplaceChar : mciReplaceChar,
|
|
|
|
termHeight : options.client.term.termHeight,
|
|
|
|
termWidth : options.client.term.termWidth,
|
2015-09-27 21:35:24 +00:00
|
|
|
trailingLF : options.trailingLF,
|
2014-10-17 02:21:06 +00:00
|
|
|
});
|
|
|
|
|
2015-04-30 20:39:03 +00:00
|
|
|
var mciMap = {};
|
2014-10-17 02:21:06 +00:00
|
|
|
var mciPosQueue = [];
|
|
|
|
var parseComplete = false;
|
|
|
|
|
2014-10-28 03:58:34 +00:00
|
|
|
var generatedId = 100;
|
|
|
|
|
2015-05-03 23:35:55 +00:00
|
|
|
var cprListener = function(pos) {
|
2014-10-30 04:23:44 +00:00
|
|
|
if(mciPosQueue.length > 0) {
|
|
|
|
var forMapItem = mciPosQueue.shift();
|
2015-04-30 20:39:03 +00:00
|
|
|
mciMap[forMapItem].position = pos;
|
2014-10-30 04:23:44 +00:00
|
|
|
|
|
|
|
if(parseComplete && 0 === mciPosQueue.length) {
|
|
|
|
completed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function completed() {
|
2015-05-03 23:35:55 +00:00
|
|
|
options.client.removeListener('cursor position report', cprListener);
|
2014-10-30 04:23:44 +00:00
|
|
|
parser.removeAllListeners(); // :TODO: Necessary???
|
2014-11-10 04:24:09 +00:00
|
|
|
|
|
|
|
if(iceColors) {
|
2014-11-13 06:16:47 +00:00
|
|
|
// options.client.term.write(ansi.blinkNormal());
|
2014-11-10 04:24:09 +00:00
|
|
|
}
|
|
|
|
|
2015-06-26 04:34:33 +00:00
|
|
|
var extraInfo = {
|
2015-07-24 04:23:44 +00:00
|
|
|
height : parser.row - 1,
|
2015-06-26 04:34:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
cb(null, mciMap, extraInfo);
|
2014-10-30 04:23:44 +00:00
|
|
|
}
|
|
|
|
|
2015-05-03 23:35:55 +00:00
|
|
|
options.client.on('cursor position report', cprListener);
|
|
|
|
|
2015-05-04 03:36:53 +00:00
|
|
|
options.pause = 'termHeight'; // :TODO: remove!!
|
|
|
|
var nextPauseTermHeight = options.client.term.termHeight;
|
|
|
|
var continous = false;
|
2015-05-03 23:35:55 +00:00
|
|
|
|
2015-07-21 04:56:48 +00:00
|
|
|
/*
|
2015-05-03 23:35:55 +00:00
|
|
|
parser.on('row update', function rowUpdate(row) {
|
2015-05-04 03:36:53 +00:00
|
|
|
if(row >= nextPauseTermHeight) {
|
|
|
|
if(!continous && 'termHeight' === options.pause) {
|
2015-06-05 22:20:26 +00:00
|
|
|
// :TODO: Must use new key type (ch, key)
|
2015-05-03 23:35:55 +00:00
|
|
|
options.client.waitForKeyPress(function kp(k) {
|
2015-05-04 03:36:53 +00:00
|
|
|
// :TODO: Allow for configurable key(s) here; or none
|
|
|
|
if('C' === k || 'c' == k) {
|
|
|
|
continous = true;
|
|
|
|
}
|
2015-05-03 23:35:55 +00:00
|
|
|
parser.parse();
|
|
|
|
});
|
|
|
|
parser.stop();
|
|
|
|
}
|
2015-05-04 03:36:53 +00:00
|
|
|
nextPauseTermHeight += options.client.term.termHeight;
|
2015-05-03 23:35:55 +00:00
|
|
|
}
|
|
|
|
});
|
2015-07-21 04:56:48 +00:00
|
|
|
*/
|
2014-10-30 04:23:44 +00:00
|
|
|
|
2015-04-30 20:39:03 +00:00
|
|
|
parser.on('mci', function mciEncountered(mciInfo) {
|
|
|
|
|
2015-05-01 04:29:24 +00:00
|
|
|
/*
|
|
|
|
if('PA' === mciInfo.mci) {
|
|
|
|
// :TODO: can't do this until this thing is pausable anyway...
|
|
|
|
options.client.waitForKeyPress(function kp(k) {
|
|
|
|
console.log('got a key: ' + k);
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
2015-04-19 08:13:13 +00:00
|
|
|
// :TODO: ensure generatedId's do not conflict with any |id|
|
2015-05-03 23:35:55 +00:00
|
|
|
// :TODO: Bug here - should only generate & increment ID's for the initial entry, not the "focus" version
|
2015-05-01 04:29:24 +00:00
|
|
|
var id = !_.isNumber(mciInfo.id) ? generatedId++ : mciInfo.id;
|
2015-04-30 20:39:03 +00:00
|
|
|
var mapKey = mciInfo.mci + id;
|
|
|
|
var mapEntry = mciMap[mapKey];
|
|
|
|
if(mapEntry) {
|
|
|
|
mapEntry.focusSGR = mciInfo.SGR;
|
|
|
|
mapEntry.focusArgs = mciInfo.args;
|
2014-10-17 02:21:06 +00:00
|
|
|
} else {
|
2015-04-30 20:39:03 +00:00
|
|
|
mciMap[mapKey] = {
|
|
|
|
args : mciInfo.args,
|
|
|
|
SGR : mciInfo.SGR,
|
|
|
|
code : mciInfo.mci,
|
|
|
|
id : id,
|
2014-10-17 02:21:06 +00:00
|
|
|
};
|
|
|
|
|
2015-04-30 20:39:03 +00:00
|
|
|
mciPosQueue.push(mapKey);
|
2014-10-17 02:21:06 +00:00
|
|
|
|
2015-07-20 03:49:48 +00:00
|
|
|
options.client.term.write(ansi.queryPos(), false);
|
2014-10-17 02:21:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2016-05-24 02:45:18 +00:00
|
|
|
/*
|
2014-10-17 02:21:06 +00:00
|
|
|
parser.on('chunk', function onChunk(chunk) {
|
2015-04-30 20:39:03 +00:00
|
|
|
options.client.term.write(chunk, false);
|
2014-10-17 02:21:06 +00:00
|
|
|
});
|
2016-05-24 02:45:18 +00:00
|
|
|
*/
|
|
|
|
parser.on('literal', literal => {
|
|
|
|
options.client.term.write(literal, false);
|
|
|
|
});
|
|
|
|
|
|
|
|
parser.on('control', control => {
|
|
|
|
options.client.term.write(control, false);
|
|
|
|
});
|
|
|
|
|
2014-10-17 02:21:06 +00:00
|
|
|
|
|
|
|
parser.on('complete', function onComplete() {
|
|
|
|
parseComplete = true;
|
|
|
|
|
|
|
|
if(0 === mciPosQueue.length) {
|
2014-10-30 04:23:44 +00:00
|
|
|
completed();
|
2014-10-17 02:21:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2015-04-17 04:29:53 +00:00
|
|
|
// :TODO: If options.font, set the font via ANSI
|
|
|
|
// ...this should come from sauce, be passed in, or defaulted
|
|
|
|
var ansiFont = '';
|
|
|
|
if(options.font) {
|
2015-04-24 05:00:48 +00:00
|
|
|
ansiFont = ansi.setSyncTERMFont(options.font);
|
2015-04-17 04:29:53 +00:00
|
|
|
} else if(options.sauce) {
|
|
|
|
var fontName = getFontNameFromSAUCE(options.sauce);
|
2015-04-24 05:00:48 +00:00
|
|
|
if(fontName) {
|
|
|
|
fontName = ansi.getSyncTERMFontFromAlias(fontName);
|
|
|
|
}
|
2015-04-17 04:29:53 +00:00
|
|
|
|
2015-11-22 00:01:21 +00:00
|
|
|
// Don't set default (cp437) from SAUCE
|
|
|
|
if(fontName && 'cp437' !== fontName) {
|
2015-04-24 05:00:48 +00:00
|
|
|
ansiFont = ansi.setSyncTERMFont(fontName);
|
2015-04-17 04:29:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(ansiFont.length > 1) {
|
2015-07-20 03:49:48 +00:00
|
|
|
options.client.term.write(ansiFont, false);
|
2015-04-17 04:29:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-11-10 04:24:09 +00:00
|
|
|
if(iceColors) {
|
2015-07-20 03:49:48 +00:00
|
|
|
options.client.term.write(ansi.blinkToBrightIntensity(), false);
|
2014-11-10 04:24:09 +00:00
|
|
|
}
|
|
|
|
|
2015-05-03 23:35:55 +00:00
|
|
|
parser.reset(options.art);
|
|
|
|
parser.parse();
|
2014-10-17 02:21:06 +00:00
|
|
|
}
|