* JSONCache -> ConfigCache

* ConfigCache used for theme.hjson
* Reformatted theme.hjson JSON to HJSON
* Specific form ID used when applying themes if available, else generic match used
* Pass extraArgs when processing 'next'
This commit is contained in:
Bryan Ashby 2015-09-09 21:31:04 -06:00
parent cc6d214882
commit 481c3171f4
8 changed files with 233 additions and 175 deletions

View File

@ -7,16 +7,16 @@ var Log = require('./logger.js').log;
var paths = require('path');
var fs = require('fs');
var Gaze = require('gaze').Gaze;
var stripJsonComments = require('strip-json-comments');
var events = require('events');
var util = require('util');
var assert = require('assert');
var hjson = require('hjson');
module.exports = exports = new JSONCache();
function JSONCache() {
function ConfigCache() {
events.EventEmitter.call(this);
var self = this;
this.cache = {}; // filePath -> JSON
this.cache = {}; // filePath -> HJSON
this.gaze = new Gaze();
this.reCacheJSONFromFile = function(filePath, cb) {
@ -45,14 +45,18 @@ function JSONCache() {
self.reCacheJSONFromFile(filePath, function reCached(err) {
if(err) {
Log.error( { error : err, filePath : filePath } , 'Error recaching JSON');
} else {
self.emit('recached', filePath);
}
});
});
}
JSONCache.prototype.getJSON = function(fileName, cb) {
util.inherits(ConfigCache, events.EventEmitter);
ConfigCache.prototype.getConfig = function(filePath, cb) {
var self = this;
var filePath = paths.join(Config.paths.mods, fileName);
if(filePath in this.cache) {
cb(null, this.cache[filePath], false);
@ -65,3 +69,9 @@ JSONCache.prototype.getJSON = function(fileName, cb) {
});
}
};
ConfigCache.prototype.getModConfig = function(fileName, cb) {
this.getConfig(paths.join(Config.paths.mods, fileName), cb);
};
module.exports = exports = new ConfigCache();

View File

@ -154,8 +154,15 @@ function MenuModule(options) {
this.nextMenu = function() {
if(!_.isObject(self.menuConfig.form) && !_.isString(self.menuConfig.prompt) &&
_.isString(self.menuConfig.next))
(_.isString(self.menuConfig.next) || _.isObject(self.menuConfig.next)))
{
/*
next : "spec"
next: {
"asset" : "spec",
"extraArgs" : ...
}
*/
if(self.hasNextTimeout()) {
setTimeout(function nextTimeout() {
menuUtil.handleNext(self.client, self.menuConfig.next);

View File

@ -8,7 +8,7 @@ var conf = require('./config.js'); // :TODO: remove me!
var Config = require('./config.js').config;
var asset = require('./asset.js');
var theme = require('./theme.js');
var jsonCache = require('./json_cache.js');
var configCache = require('./config_cache.js');
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
var fs = require('fs');
@ -29,7 +29,7 @@ function getMenuConfig(name, cb) {
async.waterfall(
[
function loadMenuJSON(callback) {
jsonCache.getJSON('menu.hjson', function loaded(err, menuJson) {
configCache.getModConfig('menu.hjson', function loaded(err, menuJson) {
callback(err, menuJson);
});
},
@ -43,7 +43,7 @@ function getMenuConfig(name, cb) {
},
function loadPromptJSON(callback) {
if(_.isString(menuConfig.prompt)) {
jsonCache.getJSON('prompt.hjson', function loaded(err, promptJson, reCached) {
configCache.getModConfig('prompt.hjson', function loaded(err, promptJson, reCached) {
callback(err, promptJson);
});
} else {
@ -166,14 +166,14 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
}
// :TODO: Most of this should be moved elsewhere .... DRY...
function callModuleMenuMethod(client, asset, path, formData) {
function callModuleMenuMethod(client, asset, path, formData, extraArgs) {
if('' === paths.extname(path)) {
path += '.js';
}
try {
var methodMod = require(path);
methodMod[asset.asset](client.currentMenuModule, formData || { }, conf.extraArgs);
methodMod[asset.asset](client.currentMenuModule, formData || { }, extraArgs);
} catch(e) {
client.log.error( { error : e.toString(), methodName : asset.asset }, 'Failed to execute asset method');
}
@ -190,12 +190,12 @@ function handleAction(client, formData, conf) {
case 'method' :
case 'systemMethod' :
if(_.isString(actionAsset.location)) {
callModuleMenuMethod(client, actionAsset, paths.join(Config.paths.mods, actionAsset.location, formData));
callModuleMenuMethod(client, actionAsset, paths.join(Config.paths.mods, actionAsset.location, formData, conf.extraArgs));
} else {
if('systemMethod' === actionAsset.type) {
// :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. ()
// :TODO: Probably better as system_method.js
callModuleMenuMethod(client, actionAsset, paths.join(__dirname, 'system_menu_method.js'), formData);
callModuleMenuMethod(client, actionAsset, paths.join(__dirname, 'system_menu_method.js'), formData, conf.extraArgs);
} else {
// local to current module
var currentModule = client.currentMenuModule;
@ -212,32 +212,35 @@ function handleAction(client, formData, conf) {
}
}
function handleNext(client, nextSpec) {
function handleNext(client, nextSpec, conf) {
assert(_.isString(nextSpec));
var nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu');
conf = conf || {};
var extraArgs = conf.extraArgs || {};
switch(nextAsset.type) {
case 'method' :
case 'systemMethod' :
if(_.isString(nextAsset.location)) {
callModuleMenuMethod(client, nextAsset, paths.join(Config.paths.mods, actionAsset.location));
callModuleMenuMethod(client, nextAsset, paths.join(Config.paths.mods, actionAsset.location), {}, extraArgs);
} else {
if('systemMethod' === nextAsset.type) {
// :TODO: see other notes about system_menu_method.js here
callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'));
callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs);
} else {
// local to current module
var currentModule = client.currentMenuModule;
if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) {
currentModule.menuMethods[actionAsset.asset]( { }, { } );
currentModule.menuMethods[actionAsset.asset]( { }, extraArgs );
}
}
}
break;
case 'menu' :
client.gotoMenuModule( { name : nextAsset.asset } );
client.gotoMenuModule( { name : nextAsset.asset, extraArgs : extraArgs } );
break;
default :
@ -246,17 +249,24 @@ function handleNext(client, nextSpec) {
}
}
// :TODO: This should be in theme.js
// :TODO: Need to take (optional) form ID to search for (e.g. for multi-form menus)
// :TODO: custom art needs a way to be themed -- e.g. config.art.someArtThing
// :TODO: custom art needs a way to be themed -- e.g. config.art.someArtThing -- what does this mean exactly?
// :TODO: Seems better in theme.js, but that includes ViewController...which would then include theme.js
function applyThemeCustomization(options) {
//
// options.name : menu/prompt name
// options.configMci : menu or prompt config (menu.json / prompt.json) specific mci section
// options.client : client
// options.type : menu|prompt
// options.formId : (optional) form ID in cases where multiple forms may exist wanting their own customization
//
// In the case of formId, the theme must include the ID as well, e.g.:
// {
// ...
// "2" : {
// "TL1" : { ... }
// }
// }
//
assert(_.isString(options.name));
assert("menus" === options.type || "prompts" === options.type);
@ -268,10 +278,16 @@ function applyThemeCustomization(options) {
if(_.has(options.client.currentTheme, [ 'customization', options.type, options.name ])) {
var themeConfig = options.client.currentTheme.customization[options.type][options.name];
if(options.formId && _.has(themeConfig, options.formId.toString())) {
// form ID found - use exact match
themeConfig = themeConfig[options.formId];
}
Object.keys(themeConfig).forEach(function mciEntry(mci) {
_.defaults(options.configMci[mci], themeConfig[mci]);
});
}
// :TODO: apply generic stuff, e.g. "VM" (vs "VM1")
}
}

View File

@ -6,7 +6,7 @@ var art = require('./art.js');
var ansi = require('./ansi_term.js');
var miscUtil = require('./misc_util.js');
var Log = require('./logger.js').log;
var jsonCache = require('./json_cache.js');
var configCache = require('./config_cache.js');
var asset = require('./asset.js');
var ViewController = require('./view_controller.js').ViewController;
@ -25,77 +25,73 @@ exports.displayThemeArt = displayThemeArt;
exports.displayThemedPause = displayThemedPause;
exports.displayThemedAsset = displayThemedAsset;
// :TODO: use JSONCache here... may need to fancy it up a bit in order to have events for after re-cache, e.g. to update helpers below:
function loadTheme(themeID, cb) {
var path = paths.join(Config.paths.themes, themeID, 'theme.json');
function refreshThemeHelpers(theme) {
//
// Create some handy helpers
//
theme.helpers = {
getPasswordChar : function() {
var pwChar = Config.defaults.passwordChar;
if(_.has(theme, 'customization.defaults.general')) {
var themePasswordChar = theme.customization.defaults.general.passwordChar;
if(_.isString(themePasswordChar)) {
pwChar = themePasswordChar.substr(0, 1);
} else if(_.isNumber(themePasswordChar)) {
pwChar = String.fromCharCode(themePasswordChar);
}
}
return pwChar;
},
getDateFormat : function(style) {
style = style || 'short';
fs.readFile(path, { encoding : 'utf8' }, function onData(err, data) {
var format = Config.defaults.dateFormat[style] || 'MM/DD/YYYY';
if(_.has(theme, 'customization.defaults.dateFormat')) {
return theme.customization.defaults.dateFormat[style] || format;
}
return format;
},
getTimeFormat : function(style) {
style = style || 'short';
var format = Config.defaults.timeFormat[style] || 'h:mm a';
if(_.has(theme, 'customization.defaults.timeFormat')) {
return theme.customization.defaults.timeFormat[style] || format;
}
return format;
},
getDateTimeFormat : function(style) {
style = style || 'short';
var format = Config.defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a';
if(_.has(theme, 'customization.defaults.dateTimeFormat')) {
return theme.customization.defaults.dateTimeFormat[style] || format;
}
return format;
}
}
}
function loadTheme(themeID, cb) {
var path = paths.join(Config.paths.themes, themeID, 'theme.hjson');
configCache.getConfig(path, function loaded(err, theme) {
if(err) {
cb(err);
} else {
try {
var theme = JSON.parse(stripJsonComments(data));
if(!_.isObject(theme.info)) {
cb(new Error('Invalid theme JSON'));
return;
}
assert(!_.isObject(theme.helpers)); // we create this on the fly!
//
// Create some handy helpers
//
theme.helpers = {
getPasswordChar : function() {
var pwChar = Config.defaults.passwordChar;
if(_.has(theme, 'customization.defaults.general')) {
var themePasswordChar = theme.customization.defaults.general.passwordChar;
if(_.isString(themePasswordChar)) {
pwChar = themePasswordChar.substr(0, 1);
} else if(_.isNumber(themePasswordChar)) {
pwChar = String.fromCharCode(themePasswordChar);
}
}
return pwChar;
},
getDateFormat : function(style) {
style = style || 'short';
var format = Config.defaults.dateFormat[style] || 'MM/DD/YYYY';
if(_.has(theme, 'customization.defaults.dateFormat')) {
return theme.customization.defaults.dateFormat[style] || format;
}
return format;
},
getTimeFormat : function(style) {
style = style || 'short';
var format = Config.defaults.timeFormat[style] || 'h:mm a';
if(_.has(theme, 'customization.defaults.timeFormat')) {
return theme.customization.defaults.timeFormat[style] || format;
}
return format;
},
getDateTimeFormat : function(style) {
style = style || 'short';
var format = Config.defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a';
if(_.has(theme, 'customization.defaults.dateTimeFormat')) {
return theme.customization.defaults.dateTimeFormat[style] || format;
}
return format;
}
};
cb(null, theme);
} catch(e) {
cb(err);
if(!_.isObject(theme.info)) {
cb(new Error('Invalid theme or missing \'info\' section'));
return;
}
refreshThemeHelpers(theme);
cb(null, theme, path);
}
});
}
@ -118,9 +114,20 @@ function initAvailableThemes(cb) {
},
function populateAvailable(filtered, callback) {
filtered.forEach(function onTheme(themeId) {
loadTheme(themeId, function themeLoaded(err, theme) {
loadTheme(themeId, function themeLoaded(err, theme, themePath) {
if(!err) {
availableThemes[themeId] = theme;
configCache.on('recached', function recached(path) {
if(themePath === path) {
loadTheme(themeId, function reloaded(err, reloadedTheme) {
Log.debug( { info : theme.info }, 'Theme recached' );
availableThemes[themeId] = reloadedTheme;
});
}
});
Log.debug( { info : theme.info }, 'Theme loaded');
}
});
@ -227,7 +234,7 @@ function displayThemedPause(options, cb) {
async.series(
[
function loadPromptJSON(callback) {
jsonCache.getJSON('prompt.hjson', function loaded(err, promptJson) {
configCache.getModConfig('prompt.hjson', function loaded(err, promptJson) {
if(err) {
callback(err);
} else {

View File

@ -562,6 +562,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) {
type : 'menus',
client : self.client,
configMci : formConfig.mci,
formId : formIdKey,
});
}

View File

@ -440,8 +440,8 @@
},
"TL3" : {
//"width" : 39,
"width" : 27,
"textOverflow" : "..."
//"width" : 27,
//"textOverflow" : "..."
},
"TL5" : {
"width" : 27

View File

@ -0,0 +1,96 @@
{
info: {
name: Nu Mayan
author: NuSkooler
}
customization: {
defaults: {
general: {
passwordChar: φ
}
dateFormat: {
short: YYYY-MMM-DD
}
timeFormat: {
short: h:mm:ss a
}
mci: {
TM: {
styleSGR1: |00|30|01
}
}
}
menus: {
matrix: {
VM1: {
itemSpacing: 1
justify: center
width: 12
focusTextStyle: l33t
}
}
apply: {
ET1: { width: 21 }
ET2: { width: 21 }
ME3: {
styleSGR1: |00|30|01
styleSGR2: |00|37
fillChar: "#"
}
ET4: { width: 1 }
ET5: { width: 21 }
ET6: { width: 21 }
ET7: { width: 21 }
ET8: { width: 21 }
ET9: { width: 21 }
ET10: { width: 21 }
}
lastCallers: {
TL1: {
resizable: false
width: 16
textOverflow: ...
}
TL2: {
resizable: false
width: 19
textOverflow: ...
}
TL3: {
resizable: false
width: 17
textOverflow: ...
}
}
messageAreaViewPost: {
0: {
TL3: {
width: 27
textOverflow: ...
}
}
}
}
prompts: {
userCredentials: {
ET1: { width: 21 }
ET2: { width: 21 }
}
}
}
}

View File

@ -1,79 +0,0 @@
{
"info" : {
"name" : "Nu Mayan",
"author" : "NuSkooler"
},
"customization" : {
"defaults" : {
"general" : {
"passwordChar" : "φ"
},
"dateFormat" : {
"short" : "YYYY-MMM-DD"
},
"timeFormat" : {
"short" : "h:mm:ss a"
},
"mci" : {
"TM" : {
"styleSGR1" : "|00|30|01"
}
}
},
"menus" : {
"matrix" : {
"VM1" : {
"itemSpacing" : 1,
"justify" : "center",
"width" : 12,
"focusTextStyle" : "l33t"
}
},
"apply" : {
"ET1" : { "width" : 21 },
"ET2" : { "width" : 21 },
//"ET3" : { "width" : 21 },
"ME3" : {
"styleSGR1" : "|00|30|01",
"styleSGR2" : "|00|37",
"fillChar" : "#"
},
"ET4" : { "width" : 1 },
"ET5" : { "width" : 21 },
"ET6" : { "width" : 21 },
"ET7" : { "width" : 21 },
"ET8" : { "width" : 21 },
"ET9" : { "width" : 21 },
"ET10" : { "width" : 21 }
},
"lastCallers" : {
"TL1" : {
"resizable" : false,
"width" : 16,
"textOverflow" : "..."
},
"TL2" : {
"resizable" : false,
"width" : 19,
"textOverflow" : "..."
},
"TL3" : {
"resizable" : false,
"width" : 17,
"textOverflow" : "..."
}
},
"messageAreaViewPost" : {
"TL3" : {
"width" : 25
}
}
},
"prompts" : {
"userCredentials" : {
"ET1" : { "width" : 21 },
"ET2" : { "width" : 21 }
}
}
}
}