diff --git a/WHATSNEW.md b/WHATSNEW.md index 35c26086..21d43083 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -4,6 +4,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 0.0.12-beta * The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](/docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276). * The default configuration has been moved to [config_default.js](/core/config_default.js). +* A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `include` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`. +* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `include`ed in `menu.hjson`. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/achievement.js b/core/achievement.js index 87f71914..3c58d4d3 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -162,15 +162,14 @@ class Achievements { } }; - const configOptions = { + this.config = new ConfigLoader({ onReload : err => { if (!err) { configLoaded(); } - }, - }; + } + }); - this.config = new ConfigLoader(configOptions); this.config.init(configPath, err => { if (err) { return cb(err); diff --git a/core/bbs.js b/core/bbs.js index 0bf8f8aa..074a742b 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -212,15 +212,9 @@ function initialize(cb) { function initStatLog(callback) { return require('./stat_log.js').init(callback); }, - function initConfigs(callback) { - return require('./config_util.js').init(callback); - }, - function initThemes(callback) { - // Have to pull in here so it's after Config init - require('./theme.js').initAvailableThemes( (err, themeCount) => { - logger.log.info({ themeCount }, 'Themes initialized'); - return callback(err); - }); + function initMenusAndThemes(callback) { + const { ThemeManager } = require('./theme'); + return ThemeManager.create(callback); }, function loadSysOpInformation(callback) { // diff --git a/core/client.js b/core/client.js index b8b08c34..8d17f953 100644 --- a/core/client.js +++ b/core/client.js @@ -83,7 +83,7 @@ function Client(/*input, output*/) { const self = this; this.user = new User(); - this.currentTheme = { info : { name : 'N/A', description : 'None' } }; + this.currentThemeConfig = { info : { name : 'N/A', description : 'None' } }; this.lastActivityTime = Date.now(); this.menuStack = new MenuStack(this); this.acs = new ACS( { client : this, user : this.user } ); @@ -94,6 +94,26 @@ function Client(/*input, output*/) { this.mciCache = {}; }; + Object.defineProperty(this, 'currentTheme', { + get : () => { + if (this.currentThemeConfig) { + return this.currentThemeConfig.get(); + } else { + return { + info : { + name : 'N/A', + author : 'N/A', + description : 'N/A', + group : 'N/A', + } + }; + } + }, + set : (theme) => { + this.currentThemeConfig = theme; + } + }); + Object.defineProperty(this, 'node', { get : function() { return self.session.id; @@ -120,7 +140,7 @@ function Client(/*input, output*/) { this.themeChangedListener = function( { themeId } ) { if(_.get(self.currentTheme, 'info.themeId') === themeId) { - self.currentTheme = require('./theme.js').getAvailableThemes().get(themeId); + self.currentThemeConfig = require('./theme.js').getAvailableThemes().get(themeId); } }; diff --git a/core/config_default.js b/core/config_default.js index 063f3899..82d3eb1d 100644 --- a/core/config_default.js +++ b/core/config_default.js @@ -14,7 +14,6 @@ module.exports = () => { closedSystem : false, // is the system closed to new users? menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path - promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path achievementFile : 'achievements.hjson', }, diff --git a/core/config_util.js b/core/config_util.js index d64c7a24..63e46f62 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -1,17 +1,12 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').get; -const ConfigCache = require('./config_cache.js'); -const Events = require('./events.js'); +const Config = require('./config.js').get; // deps -const paths = require('path'); -const async = require('async'); +const paths = require('path'); -exports.init = init; -exports.getConfigPath = getConfigPath; -exports.getFullConfig = getFullConfig; +exports.getConfigPath = getConfigPath; function getConfigPath(filePath) { // |filePath| is assumed to be in the config path if it's only a file name @@ -20,48 +15,3 @@ function getConfigPath(filePath) { } return filePath; } - -function init(cb) { - // pre-cache menu.hjson and prompt.hjson + establish events - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - if(reCachedPath === getConfigPath(Config().general.menuFile)) { - Events.emit(Events.getSystemEvents().MenusChanged); - } else if(reCachedPath === getConfigPath(Config().general.promptFile)) { - Events.emit(Events.getSystemEvents().PromptsChanged); - } - }; - - const config = Config(); - async.series( - [ - function menu(callback) { - return ConfigCache.getConfigWithOptions( - { - filePath : getConfigPath(config.general.menuFile), - callback : changed, - }, - callback - ); - }, - function prompt(callback) { - return ConfigCache.getConfigWithOptions( - { - filePath : getConfigPath(config.general.promptFile), - callback : changed, - }, - callback - ); - } - ], - err => { - return cb(err); - } - ); -} - -function getFullConfig(filePath, cb) { - ConfigCache.getConfig(getConfigPath(filePath), (err, config) => { - return cb(err, config); - }); -} diff --git a/core/menu_util.js b/core/menu_util.js index a429415b..923d907d 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -24,10 +24,11 @@ function getMenuConfig(client, name, cb) { async.waterfall( [ function locateMenuConfig(callback) { - if(_.has(client.currentTheme, [ 'menus', name ])) { - const menuConfig = client.currentTheme.menus[name]; + const menuConfig = _.get(client.currentTheme, [ 'menus', name ]); + if (menuConfig) { return callback(null, menuConfig); } + return callback(Errors.DoesNotExist(`No menu entry for "${name}"`)); }, function locatePromptConfig(menuConfig, callback) { diff --git a/core/theme.js b/core/theme.js index c477f92e..557377b7 100644 --- a/core/theme.js +++ b/core/theme.js @@ -5,16 +5,16 @@ const Config = require('./config.js').get; const art = require('./art.js'); const ansi = require('./ansi_term.js'); const Log = require('./logger.js').log; -const ConfigCache = require('./config_cache.js'); -const getFullConfig = require('./config_util.js').getFullConfig; const asset = require('./asset.js'); const ViewController = require('./view_controller.js').ViewController; const Errors = require('./enig_error.js').Errors; -const ErrorReasons = require('./enig_error.js').ErrorReasons; const Events = require('./events.js'); const AnsiPrep = require('./ansi_prep.js'); const UserProps = require('./user_property.js'); +const ConfigLoader = require('./config_loader'); +const { getConfigPath } = require('./config_util'); + // deps const fs = require('graceful-fs'); const paths = require('path'); @@ -26,213 +26,262 @@ exports.getThemeArt = getThemeArt; exports.getAvailableThemes = getAvailableThemes; exports.getRandomTheme = getRandomTheme; exports.setClientTheme = setClientTheme; -exports.initAvailableThemes = initAvailableThemes; exports.displayPreparedArt = displayPreparedArt; exports.displayThemeArt = displayThemeArt; exports.displayThemedPause = displayThemedPause; exports.displayThemedPrompt = displayThemedPrompt; exports.displayThemedAsset = displayThemedAsset; -function refreshThemeHelpers(theme) { - // - // Create some handy helpers - // - theme.helpers = { - getPasswordChar : function() { - let pwChar = _.get( - theme, - 'customization.defaults.passwordChar', - Config().theme.passwordChar - ); +// global instance of ThemeManager; see ThemeManager.create() +let themeManagerInstance; - if(_.isString(pwChar)) { - pwChar = pwChar.substr(0, 1); - } else if(_.isNumber(pwChar)) { - pwChar = String.fromCharCode(pwChar); +exports.ThemeManager = class ThemeManager { + constructor() { + this.availableThemes = new Map(); + } + + static create(cb) { + themeManagerInstance = new ThemeManager(); + themeManagerInstance.init(err => { + if (!err) { + themeManagerInstance.getAvailableThemes().forEach( (themeConfig, themeId) => { + const { name, author, group } = themeConfig.get().info; + Log.info( + { themeId, themeName : name, author, group }, + 'Theme loaded' + ); + }); } - return pwChar; - }, - getDateFormat : function(style = 'short') { - const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY'; - return _.get(theme, `customization.defaults.dateFormat.${style}`, format); - }, - getTimeFormat : function(style = 'short') { - const format = Config().theme.timeFormat[style] || 'h:mm a'; - return _.get(theme, `customization.defaults.timeFormat.${style}`, format); - }, - getDateTimeFormat : function(style = 'short') { - const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; - return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); - } - }; -} - -function loadTheme(themeId, cb) { - const path = paths.join(Config().paths.themes, themeId, 'theme.hjson'); - - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - if(reCachedPath === path) { - reloadTheme(themeId); - } - }; - - const getOpts = { - filePath : path, - forceReCache : true, - callback : changed, - }; - - ConfigCache.getConfigWithOptions(getOpts, (err, theme) => { - if(err) { return cb(err); - } + }); + } + getAvailableThemes() { + return this.availableThemes; + } + + init(cb) { + this.menuConfig = new ConfigLoader({ + onReload : err => { + if (!err) { + // menu.hjson/includes have changed; this could affect + // all themes, so they must be reloaded + Events.emit(Events.getSystemEvents().MenusChanged); + + this._reloadAllThemes(); + } + }, + }); + + this.menuConfig.init(getConfigPath(Config().general.menuFile), err => { + if (err) { + return cb(err); + } + + return this._loadThemes(cb); + }); + } + + _loadThemes(cb) { + const themeDir = Config().paths.themes; + + fs.readdir(themeDir, (err, files) => { + if (err) { + return cb(err); + } + + async.filter(files, (filename, nextFilename) => { + const fullPath = paths.join(themeDir, filename); + fs.stat(fullPath, (err, stats) => { + if (err) { + return nextFilename(err); + } + + return nextFilename(null, stats.isDirectory()); + }); + }, + (err, themeIds) => { + if (err) { + return cb(err); + } + + async.each(themeIds, (themeId, nextThemeId) => { + return this._loadTheme(themeId, nextThemeId); + }, + err => { + return cb(err); + }); + }); + }); + } + + _loadTheme(themeId, cb) { + const themeConfig = new ConfigLoader({ + onReload : err => { + if (!err) { + // this particular theme has changed + this._themeLoaded(themeId, err => { + if (!err) { + Events.emit( + Events.getSystemEvents().ThemeChanged, + { themeId } + ); + } + }); + } + } + }); + + const themeConfigPath = paths.join(Config().paths.themes, themeId, 'theme.hjson'); + + themeConfig.init(themeConfigPath, err => { + if (err) { + return cb(err); + } + + this._themeLoaded(themeId, themeConfig); + return cb(null); + }); + } + + _themeLoaded(themeId, themeConfig) { + const theme = themeConfig.get(); + + // do some basic validation + // :TODO: schema validation here if(!_.isObject(theme.info) || !_.isString(theme.info.name) || !_.isString(theme.info.author)) { - return cb(Errors.Invalid('Invalid or missing "info" section')); + return Log.warn({ themeId }, 'Theme contains invalid or missing "info" section'); } if(false === _.get(theme, 'info.enabled')) { - return cb(Errors.General('Theme is not enabled', ErrorReasons.ErrNotEnabled)); + Log.info({ themeId }, 'Theme is disabled'); + return this.availableThemes.delete(themeId); } - refreshThemeHelpers(theme); + // merge menu.hjson+theme.hjson/etc. to the final usable theme + this._finalizeTheme(themeConfig); - return cb(null, theme, path); - }); -} + // Theme is valid and enabled; update it in available themes + this.availableThemes.set(themeId, themeConfig); -const availableThemes = new Map(); - -const IMMUTABLE_MCI_PROPERTIES = [ - 'maxLength', 'argName', 'submit', 'validate' -]; - -function getMergedTheme(menuConfig, promptConfig, theme) { - assert(_.isObject(menuConfig)); - assert(_.isObject(theme)); - - // :TODO: merge in defaults (customization.defaults{} ) - // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") - - // - // Create a *clone* of menuConfig (menu.hjson) then bring in - // promptConfig (prompt.hjson) - // - const mergedTheme = _.cloneDeep(menuConfig); - - if(_.isObject(promptConfig.prompts)) { - mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); + Events.emit( + Events.getSystemEvents().ThemeChanged, + { themeId } + ); } - // - // Add in data we won't be altering directly from the theme - // - mergedTheme.info = theme.info; - mergedTheme.helpers = theme.helpers; - mergedTheme.achievements = _.get(theme, 'customization.achievements'); + _finalizeTheme(themeConfig) { + // These TODOs are left over from the old system - decide what/if to do with them: + // :TODO: merge in defaults (customization.defaults{} ) + // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") - // - // merge customizer to disallow immutable MCI properties - // - const mciCustomizer = function(objVal, srcVal, key) { - return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; - }; + // start out with menu.hjson + const mergedTheme = _.cloneDeep(this.menuConfig.get()); - function getFormKeys(fromObj) { - // remove all non-numbers - return _.remove(_.keys(fromObj), k => !isNaN(k)); - } + const theme = themeConfig.get(); - function mergeMciProperties(dest, src) { - Object.keys(src).forEach(mci => { - if(dest[mci]) { - _.mergeWith(dest[mci], src[mci], mciCustomizer); - } else { - // theme contains MCI not in menu; bring in as-is - dest[mci] = src[mci]; - } - }); - } + // some data brought directly over + mergedTheme.info = theme.info; + mergedTheme.achievements = _.get(theme, 'customization.achievements'); - function applyThemeMciBlock(dest, src, formKey) { - if(_.isObject(src.mci)) { - mergeMciProperties(dest, src.mci); - } else { - if(_.has(src, [ formKey, 'mci' ])) { - mergeMciProperties(dest, src[formKey].mci); - } - } - } + // Create some helpers for this theme + this._setThemeHelpers(mergedTheme); - // - // menu.hjson can have a couple different structures: - // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block - // (this allows multiple layout types defined by one menu for example) - // - // 2) Non-explicit declaration: 'mci' directly under 'form:' - // - // theme.hjson has it's own mix: - // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) - // - // 2) Non-explicit: 'mci' directly under an entry - // - // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up - // with menu.hjson in #1. - // - // * When theming an explicit menu.hjson entry (1), we will use a matching explicit - // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" - // and fall back to generic if a match is not found. - // - // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming - // there is a generic 'mci' block. - // - function applyToForm(form, menuTheme, formKey) { - if(_.isObject(form.mci)) { - // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID - applyThemeMciBlock(form.mci, menuTheme, formKey); - } else { - // remove anything not uppercase - const menuMciCodeKeys = _.remove(_.keys(form), k => k === k.toUpperCase()); + // merge customizer to disallow immutable MCI properties + const ImmutableMCIProperties = [ + 'maxLength', 'argName', 'submit', 'validate' + ]; - menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { - let applyFrom; - if(_.has(menuTheme, [ mciKey, 'mci' ])) { - applyFrom = menuTheme[mciKey]; + const mciCustomizer = (objVal, srcVal, key) => { + return ImmutableMCIProperties.indexOf(key) > -1 ? objVal : srcVal; + }; + + const getFormKeys = (obj) => { + // remove all non-numbers + return _.remove(Object.keys(obj), k => !isNaN(k)); + }; + + const mergeMciProperties = (dst, src) => { + Object.keys(src).forEach(mci => { + if (dst[mci]) { + _.mergeWith(dst[mci], src[mci], mciCustomizer); } else { - applyFrom = menuTheme; + // theme contains a MCI that's not found in menu + dst[mci] = src[mci]; } - - applyThemeMciBlock(form[mciKey].mci, applyFrom, formKey); }); - } - } + }; - [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { - _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { - let createdFormSection = false; - const mergedThemeMenu = mergedTheme[sectionName][menuName]; + const applyThemeMciBlock = (dst, src, formKey) => { + if(_.isObject(src.mci)) { + mergeMciProperties(dst, src.mci); + } else if (_.has(src, [ formKey, 'mci' ])) { + mergeMciProperties(dst, src[formKey].mci); + } + }; - if(_.has(theme, [ 'customization', sectionName, menuName ])) { - const menuTheme = theme.customization[sectionName][menuName]; + // + // menu.hjson can have a couple different structures: + // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block + // (this allows multiple layout types defined by one menu for example) + // + // 2) Non-explicit declaration: 'mci' directly under 'form:' + // + // theme.hjson has it's own mix: + // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) + // + // 2) Non-explicit: 'mci' directly under an entry + // + // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up + // with menu.hjson in #1. + // + // * When theming an explicit menu.hjson entry (1), we will use a matching explicit + // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" + // and fall back to generic if a match is not found. + // + // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming + // there is a generic 'mci' block. + // + const applyToForm = (form, menuTheme, formKey) => { + if (_.isObject(form.mci)) { + // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID + applyThemeMciBlock(form.mci, menuTheme, formKey); + } else { + // remove anything not uppercase + const menuMciCodeKeys = _.remove(_.keys(form), k => k === k.toUpperCase()); - // config block is direct assign/overwrite - // :TODO: should probably be _.merge() - if(menuTheme.config) { - mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); - } + menuMciCodeKeys.forEach(mciKey => { + const src = _.has(menuTheme, [ mciKey, 'mci' ]) ? + menuTheme[mciKey] : + menuTheme; - if('menus' === sectionName) { - if(_.isObject(mergedThemeMenu.form)) { - getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { - applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); - }); - } else { - if(_.isObject(menuTheme.mci)) { + applyThemeMciBlock(form[mciKey].mci, src, formKey); + }); + } + }; + + [ 'menus', 'prompts'].forEach(sectionName => { + Object.keys(mergedTheme[sectionName]).forEach(entryName => { + let createdFormSection = false; + const mergedThemeMenu = mergedTheme[sectionName][entryName]; + + const menuTheme = _.get(theme, [ 'customization', sectionName, entryName ]); + if (menuTheme) { + if (menuTheme.config) { + // :TODO: should this be _.merge() ? + mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); + } + + if('menus' === sectionName) { + if(_.isObject(mergedThemeMenu.form)) { + getFormKeys(mergedThemeMenu.form).forEach(formKey => { + applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); + }); + } else if(_.isObject(menuTheme.mci)) { // // Not specified at menu level means we apply anything from the // theme to form.0.mci{} @@ -241,158 +290,84 @@ function getMergedTheme(menuConfig, promptConfig, theme) { mergeMciProperties(mergedThemeMenu.form[0], menuTheme); createdFormSection = true; } + } else if('prompts' === sectionName) { + // no 'form' or form keys for prompts -- direct to mci + applyToForm(mergedThemeMenu, menuTheme); } - } else if('prompts' === sectionName) { - // no 'form' or form keys for prompts -- direct to mci - applyToForm(mergedThemeMenu, menuTheme); } - } - // - // Finished merging for this menu/prompt - // - // If the following conditions are true, set runtime.autoNext to true: - // * This is a menu - // * There is/was no explicit 'form' section - // * There is no 'prompt' specified - // - if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) && - (createdFormSection || !_.isObject(mergedThemeMenu.form))) - { - mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); - } + // + // Finished merging for this menu/prompt + // + // If the following conditions are true, set runtime.autoNext to true: + // * This is a menu + // * There is/was no explicit 'form' section + // * There is no 'prompt' specified + // + if('menus' === sectionName && + !_.isString(mergedThemeMenu.prompt) && + (createdFormSection || !_.isObject(mergedThemeMenu.form))) + { + mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); + } + }); }); - }); + themeConfig.current = mergedTheme; + } - return mergedTheme; -} + _setThemeHelpers(theme) { + theme.helpers = { + getPasswordChar : function() { + let pwChar = _.get( + theme, + 'customization.defaults.passwordChar', + Config().theme.passwordChar + ); -function reloadTheme(themeId) { - const config = Config(); - async.waterfall( - [ - function loadMenuConfig(callback) { - getFullConfig(config.general.menuFile, (err, menuConfig) => { - return callback(err, menuConfig); - }); + if(_.isString(pwChar)) { + pwChar = pwChar.substr(0, 1); + } else if(_.isNumber(pwChar)) { + pwChar = String.fromCharCode(pwChar); + } + + return pwChar; }, - function loadPromptConfig(menuConfig, callback) { - getFullConfig(config.general.promptFile, (err, promptConfig) => { - return callback(err, menuConfig, promptConfig); - }); + getDateFormat : function(style = 'short') { + const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY'; + return _.get(theme, `customization.defaults.dateFormat.${style}`, format); }, - function loadIt(menuConfig, promptConfig, callback) { - loadTheme(themeId, (err, theme) => { - if(err) { - if(ErrorReasons.NotEnabled !== err.reasonCode) { - Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); - return; - } - return callback(err); - } - - Object.assign(theme.info, { themeId } ); - availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); - - Events.emit( - Events.getSystemEvents().ThemeChanged, - { themeId } - ); - - return callback(null, theme); - }); + getTimeFormat : function(style = 'short') { + const format = Config().theme.timeFormat[style] || 'h:mm a'; + return _.get(theme, `customization.defaults.timeFormat.${style}`, format); + }, + getDateTimeFormat : function(style = 'short') { + const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; + return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); } - ], - (err, theme) => { - if(err) { - Log.warn( { themeId, error : err.message }, 'Failed to reload theme'); - } else { - Log.debug( { info : theme.info }, 'Theme recached' ); - } - } - ); -} + }; + } -function reloadAllThemes() -{ - async.each([ ...availableThemes.keys() ], themeId => reloadTheme(themeId)); -} - -function initAvailableThemes(cb) { - const config = Config(); - async.waterfall( - [ - function loadMenuConfig(callback) { - getFullConfig(config.general.menuFile, (err, menuConfig) => { - return callback(err, menuConfig); - }); - }, - function loadPromptConfig(menuConfig, callback) { - getFullConfig(config.general.promptFile, (err, promptConfig) => { - return callback(err, menuConfig, promptConfig); - }); - }, - function getThemeDirectories(menuConfig, promptConfig, callback) { - fs.readdir(config.paths.themes, (err, files) => { - if(err) { - return callback(err); - } - - return callback( - null, - menuConfig, - promptConfig, - files.filter( f => { - // sync normally not allowed -- initAvailableThemes() is a startup-only method, however - return fs.statSync(paths.join(config.paths.themes, f)).isDirectory(); - }) - ); - }); - }, - function populateAvailable(menuConfig, promptConfig, themeDirectories, callback) { - async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID - loadTheme(themeId, (err, theme) => { - if(err) { - if(ErrorReasons.NotEnabled !== err.reasonCode) { - Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); - } - - return nextThemeDir(null); // try next - } - - Object.assign(theme.info, { themeId } ); - availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); - return nextThemeDir(null); - }); - }, err => { - return callback(err); - }); - }, - function initEvents(callback) { - Events.on(Events.getSystemEvents().MenusChanged, () => { - return reloadAllThemes(); - }); - Events.on(Events.getSystemEvents().PromptsChanged, () => { - return reloadAllThemes(); - }); - - return callback(null); - } - ], - err => { - return cb(err, availableThemes.size); - } - ); -} + _reloadAllThemes() { + async.each([ ...this.availableThemes.keys() ], (themeId, nextThemeId) => { + this._loadTheme(themeId, err => { + if (!err) { + Log.info({ themeId }, 'Theme reloaded'); + } + return nextThemeId(null); // always proceed + }); + }); + } +}; function getAvailableThemes() { - return availableThemes; + return themeManagerInstance.getAvailableThemes(); } function getRandomTheme() { - if(availableThemes.size > 0) { - const themeIds = [ ...availableThemes.keys() ]; + const avail = getAvailableThemes(); + if(avail.size > 0) { + const themeIds = [ ...avail.keys() ]; return themeIds[Math.floor(Math.random() * themeIds.length)]; } } diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 6b335617..941637c3 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -16,7 +16,6 @@ - [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %}) - [HJSON Config Files]({{ site.baseurl }}{% link configuration/hjson.md %}) - [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) - - [Prompts]({{ site.baseurl }}{% link configuration/prompt-hjson.md %}) - [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %}) - [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %}) - [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %}) diff --git a/docs/art/themes.md b/docs/art/themes.md index 577a95fe..4da7c310 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -38,7 +38,7 @@ The `customization` block in is itself broken up into major parts: |-------------|---------------------------------------------------| | `defaults` | Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. | | `menus` | The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. | -| `prompts` | Similar to `menus`, this file themes prompts found in `prompts.hjson`. | +| `prompts` | Similar to `menus`, this section themes `prompts`. | #### Defaults | Item | Description | @@ -46,7 +46,7 @@ The `customization` block in is itself broken up into major parts: | `passwordChar` | Character to display in password fields. Defaults to `*` | | `dateFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for dates. | | `timeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for times. | -| `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. | +| `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. | Example: ```hjson diff --git a/docs/configuration/colour-codes.md b/docs/configuration/colour-codes.md index 9ad9abdd..e2355f01 100644 --- a/docs/configuration/colour-codes.md +++ b/docs/configuration/colour-codes.md @@ -2,9 +2,7 @@ layout: page title: Colour Codes --- -ENiGMA½ supports Renegade-style pipe colour codes for formatting strings. You'll see them used in [`config.hjson`](config-hjson), -[`prompt.hjson`](prompt-hjson), [`menu.hjson`](menu-hjson), and can also be used in places like the oneliner, rumour mod, -full screen editor etc. +ENiGMA½ supports Renegade-style pipe colour codes for formatting strings. You'll see them used throughout your configuration, and can also be used in places like the oneliner, rumour mod, full screen editor etc. ## Usage When ENiGMA½ encounters colour codes in strings, they'll be processed in order and combined where possible. @@ -18,8 +16,8 @@ For example: ## Colour Code Reference -:warning: Colour codes |24 to |31 are considered "blinking" or "iCE" colour codes. On terminals that support them they'll +:warning: Colour codes |24 to |31 are considered "blinking" or "iCE" colour codes. On terminals that support them they'll be shown as the correct colours - for terminals that don't, or are that are set to "blinking" mode - they'll blink! -![Regegade style colour codes](../assets/images/colour-codes.png "Colour Codes") +![Renegade style colour codes](../assets/images/colour-codes.png "Colour Codes") diff --git a/docs/configuration/creating-config.md b/docs/configuration/creating-config.md index 5d845d5e..068ad5fa 100644 --- a/docs/configuration/creating-config.md +++ b/docs/configuration/creating-config.md @@ -10,5 +10,5 @@ Your initial configuration skeleton can be created using the `oputil.js` command ./oputil.js config new ``` -You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce `config/-menu.hjson` and `config/-prompt.hjson` files (where `` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) and [Prompt HJSON](prompt-hjson.md) for more information. +You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce `config/-menu.hjson` and `config/-prompt.hjson` files (where `` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) for more information. diff --git a/docs/configuration/hjson.md b/docs/configuration/hjson.md index 74b6eb01..e812479f 100644 --- a/docs/configuration/hjson.md +++ b/docs/configuration/hjson.md @@ -3,7 +3,7 @@ layout: page title: HJSON Config Files --- ## JSON for Humans! -HJSON is the configuration file format used by ENiGMA½ for [System Configuration](config-hjson.md), [Menus](menu-hjson.md), [Prompts](prompt-hjson.md), etc. [HJSON](https://hjson.org/) is is [JSON](https://json.org/) for humans! +HJSON is the configuration file format used by ENiGMA½ for [System Configuration](config-hjson.md), [Menus](menu-hjson.md), etc. [HJSON](https://hjson.org/) is is [JSON](https://json.org/) for humans! For those completely unfamiliar, JSON stands for JavaScript Object Notation. But don't let that scare you! JSON is simply a text file format with a bit of structure ― kind of like a fancier INI file. HJSON on the other hand as mentioned previously, is JSON for humans. That is, it has the following features and more: @@ -18,7 +18,6 @@ Through the documentation, some terms regarding HJSON and configuration files wi * `config.hjson`: Refers to `/path/to/enigma-bbs/config/config.hjson`. See [System Configuration](config-hjson.md). * `menu.hjson`: Refers to `/path/to/enigma-bbs/config/-menu.hjson`. See [Menus](menu-hjson.md). -* `prompt.hjson`: Refers to `/path/to/enigma-bbs/config/-prompt.hjson`. See [Prompts](prompt-hjson.md). * Configuration *key*: Elements in HJSON are name-value pairs where the name is the *key*. For example, provided `foo: bar`, `foo` is the key. * Configuration *section* or *block* (also commonly called an "Object" in code): This is referring to a section in a HJSON file that starts with a *key*. For example: ```hjson diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 8ddda2d8..d8d21af8 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -21,7 +21,7 @@ Below is a table of **common** menu entry members. These members apply to most e | `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. | | `art` | An art file *spec*. See [General Art Information](/docs/art/general.md). | | `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. | -| `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. | +| `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in the `prompts` section. See **Prompts** for more information. | | `submit` | Defines a submit handler when using `prompt`. | `form` | An object defining one or more *forms* available on this menu. | | `module` | Sets the module name to use for this menu. See **Menu Modules** below. | @@ -183,6 +183,9 @@ In the above entry, you'll notice `form`. This defines a form(s) object. In this * The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). * Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match (due to 0 being the first index in the list and `matrixSubmit` being the arg name in question) causing `action` of `@menu:login` to be executed (go to `login` menu). +## Prompts +TODO: describe the "prompts" section, default setup, etc. + ## ACS Checks Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax. diff --git a/docs/configuration/prompt-hjson.md b/docs/configuration/prompt-hjson.md deleted file mode 100644 index 993e5b8e..00000000 --- a/docs/configuration/prompt-hjson.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -layout: page -title: prompt.hjson ---- -:zap: This page is to describe general information the `prompt.hjson` file. It -needs fleshing out, please submit a PR if you'd like to help! - -See [HJSON General Information](hjson.md) for more information.