diff --git a/core/json_cache.js b/core/config_cache.js similarity index 73% rename from core/json_cache.js rename to core/config_cache.js index 0e9006cc..233157df 100644 --- a/core/json_cache.js +++ b/core/config_cache.js @@ -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(); \ No newline at end of file diff --git a/core/menu_module.js b/core/menu_module.js index c267f210..89a370c3 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -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); diff --git a/core/menu_util.js b/core/menu_util.js index d97760cc..c52f45ee 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -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") -} \ No newline at end of file +} diff --git a/core/theme.js b/core/theme.js index b56fde3d..93716bfe 100644 --- a/core/theme.js +++ b/core/theme.js @@ -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 { diff --git a/core/view_controller.js b/core/view_controller.js index 5e1f4622..b7cc43f8 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -562,6 +562,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { type : 'menus', client : self.client, configMci : formConfig.mci, + formId : formIdKey, }); } diff --git a/mods/menu.hjson b/mods/menu.hjson index c050f056..50c3ef80 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -440,8 +440,8 @@ }, "TL3" : { //"width" : 39, - "width" : 27, - "textOverflow" : "..." + //"width" : 27, + //"textOverflow" : "..." }, "TL5" : { "width" : 27 diff --git a/mods/themes/NU-MAYA/theme.hjson b/mods/themes/NU-MAYA/theme.hjson new file mode 100644 index 00000000..9e394cf2 --- /dev/null +++ b/mods/themes/NU-MAYA/theme.hjson @@ -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 } + } + } + } +} diff --git a/mods/themes/NU-MAYA/theme.json b/mods/themes/NU-MAYA/theme.json deleted file mode 100644 index 670e4c5f..00000000 --- a/mods/themes/NU-MAYA/theme.json +++ /dev/null @@ -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 } - } - } - } -} \ No newline at end of file