Major progress on revamp
* Deprecated explicit prompt.hjson/general.promptFile, etc.: menu.hjson can simply include any number of files * All menus and themes, their events, etc. are managed by ThemeManager allowing includes, refs, etc. and much cleaner code
This commit is contained in:
parent
1a96ad41d2
commit
4d4be5d6a9
|
@ -4,6 +4,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
|
||||||
## 0.0.12-beta
|
## 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 `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).
|
* 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
|
## 0.0.11-beta
|
||||||
* Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point!
|
* Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point!
|
||||||
|
|
|
@ -162,15 +162,14 @@ class Achievements {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const configOptions = {
|
this.config = new ConfigLoader({
|
||||||
onReload : err => {
|
onReload : err => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
configLoaded();
|
configLoaded();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
this.config = new ConfigLoader(configOptions);
|
|
||||||
this.config.init(configPath, err => {
|
this.config.init(configPath, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
|
12
core/bbs.js
12
core/bbs.js
|
@ -212,15 +212,9 @@ function initialize(cb) {
|
||||||
function initStatLog(callback) {
|
function initStatLog(callback) {
|
||||||
return require('./stat_log.js').init(callback);
|
return require('./stat_log.js').init(callback);
|
||||||
},
|
},
|
||||||
function initConfigs(callback) {
|
function initMenusAndThemes(callback) {
|
||||||
return require('./config_util.js').init(callback);
|
const { ThemeManager } = require('./theme');
|
||||||
},
|
return ThemeManager.create(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 loadSysOpInformation(callback) {
|
function loadSysOpInformation(callback) {
|
||||||
//
|
//
|
||||||
|
|
|
@ -83,7 +83,7 @@ function Client(/*input, output*/) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
this.user = new User();
|
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.lastActivityTime = Date.now();
|
||||||
this.menuStack = new MenuStack(this);
|
this.menuStack = new MenuStack(this);
|
||||||
this.acs = new ACS( { client : this, user : this.user } );
|
this.acs = new ACS( { client : this, user : this.user } );
|
||||||
|
@ -94,6 +94,26 @@ function Client(/*input, output*/) {
|
||||||
this.mciCache = {};
|
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', {
|
Object.defineProperty(this, 'node', {
|
||||||
get : function() {
|
get : function() {
|
||||||
return self.session.id;
|
return self.session.id;
|
||||||
|
@ -120,7 +140,7 @@ function Client(/*input, output*/) {
|
||||||
|
|
||||||
this.themeChangedListener = function( { themeId } ) {
|
this.themeChangedListener = function( { themeId } ) {
|
||||||
if(_.get(self.currentTheme, 'info.themeId') === themeId) {
|
if(_.get(self.currentTheme, 'info.themeId') === themeId) {
|
||||||
self.currentTheme = require('./theme.js').getAvailableThemes().get(themeId);
|
self.currentThemeConfig = require('./theme.js').getAvailableThemes().get(themeId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ module.exports = () => {
|
||||||
closedSystem : false, // is the system closed to new users?
|
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
|
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',
|
achievementFile : 'achievements.hjson',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const ConfigCache = require('./config_cache.js');
|
|
||||||
const Events = require('./events.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const async = require('async');
|
|
||||||
|
|
||||||
exports.init = init;
|
exports.getConfigPath = getConfigPath;
|
||||||
exports.getConfigPath = getConfigPath;
|
|
||||||
exports.getFullConfig = getFullConfig;
|
|
||||||
|
|
||||||
function getConfigPath(filePath) {
|
function getConfigPath(filePath) {
|
||||||
// |filePath| is assumed to be in the config path if it's only a file name
|
// |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;
|
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -24,10 +24,11 @@ function getMenuConfig(client, name, cb) {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function locateMenuConfig(callback) {
|
function locateMenuConfig(callback) {
|
||||||
if(_.has(client.currentTheme, [ 'menus', name ])) {
|
const menuConfig = _.get(client.currentTheme, [ 'menus', name ]);
|
||||||
const menuConfig = client.currentTheme.menus[name];
|
if (menuConfig) {
|
||||||
return callback(null, menuConfig);
|
return callback(null, menuConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(Errors.DoesNotExist(`No menu entry for "${name}"`));
|
return callback(Errors.DoesNotExist(`No menu entry for "${name}"`));
|
||||||
},
|
},
|
||||||
function locatePromptConfig(menuConfig, callback) {
|
function locatePromptConfig(menuConfig, callback) {
|
||||||
|
|
593
core/theme.js
593
core/theme.js
|
@ -5,16 +5,16 @@ const Config = require('./config.js').get;
|
||||||
const art = require('./art.js');
|
const art = require('./art.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const Log = require('./logger.js').log;
|
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 asset = require('./asset.js');
|
||||||
const ViewController = require('./view_controller.js').ViewController;
|
const ViewController = require('./view_controller.js').ViewController;
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const ErrorReasons = require('./enig_error.js').ErrorReasons;
|
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const AnsiPrep = require('./ansi_prep.js');
|
const AnsiPrep = require('./ansi_prep.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
|
const ConfigLoader = require('./config_loader');
|
||||||
|
const { getConfigPath } = require('./config_util');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
|
@ -26,213 +26,262 @@ exports.getThemeArt = getThemeArt;
|
||||||
exports.getAvailableThemes = getAvailableThemes;
|
exports.getAvailableThemes = getAvailableThemes;
|
||||||
exports.getRandomTheme = getRandomTheme;
|
exports.getRandomTheme = getRandomTheme;
|
||||||
exports.setClientTheme = setClientTheme;
|
exports.setClientTheme = setClientTheme;
|
||||||
exports.initAvailableThemes = initAvailableThemes;
|
|
||||||
exports.displayPreparedArt = displayPreparedArt;
|
exports.displayPreparedArt = displayPreparedArt;
|
||||||
exports.displayThemeArt = displayThemeArt;
|
exports.displayThemeArt = displayThemeArt;
|
||||||
exports.displayThemedPause = displayThemedPause;
|
exports.displayThemedPause = displayThemedPause;
|
||||||
exports.displayThemedPrompt = displayThemedPrompt;
|
exports.displayThemedPrompt = displayThemedPrompt;
|
||||||
exports.displayThemedAsset = displayThemedAsset;
|
exports.displayThemedAsset = displayThemedAsset;
|
||||||
|
|
||||||
function refreshThemeHelpers(theme) {
|
// global instance of ThemeManager; see ThemeManager.create()
|
||||||
//
|
let themeManagerInstance;
|
||||||
// Create some handy helpers
|
|
||||||
//
|
|
||||||
theme.helpers = {
|
|
||||||
getPasswordChar : function() {
|
|
||||||
let pwChar = _.get(
|
|
||||||
theme,
|
|
||||||
'customization.defaults.passwordChar',
|
|
||||||
Config().theme.passwordChar
|
|
||||||
);
|
|
||||||
|
|
||||||
if(_.isString(pwChar)) {
|
exports.ThemeManager = class ThemeManager {
|
||||||
pwChar = pwChar.substr(0, 1);
|
constructor() {
|
||||||
} else if(_.isNumber(pwChar)) {
|
this.availableThemes = new Map();
|
||||||
pwChar = String.fromCharCode(pwChar);
|
}
|
||||||
|
|
||||||
|
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);
|
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) ||
|
if(!_.isObject(theme.info) ||
|
||||||
!_.isString(theme.info.name) ||
|
!_.isString(theme.info.name) ||
|
||||||
!_.isString(theme.info.author))
|
!_.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')) {
|
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();
|
Events.emit(
|
||||||
|
Events.getSystemEvents().ThemeChanged,
|
||||||
const IMMUTABLE_MCI_PROPERTIES = [
|
{ themeId }
|
||||||
'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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
_finalizeTheme(themeConfig) {
|
||||||
// Add in data we won't be altering directly from the theme
|
// These TODOs are left over from the old system - decide what/if to do with them:
|
||||||
//
|
// :TODO: merge in defaults (customization.defaults{} )
|
||||||
mergedTheme.info = theme.info;
|
// :TODO: apply generic stuff, e.g. "VM" (vs "VM1")
|
||||||
mergedTheme.helpers = theme.helpers;
|
|
||||||
mergedTheme.achievements = _.get(theme, 'customization.achievements');
|
|
||||||
|
|
||||||
//
|
// start out with menu.hjson
|
||||||
// merge customizer to disallow immutable MCI properties
|
const mergedTheme = _.cloneDeep(this.menuConfig.get());
|
||||||
//
|
|
||||||
const mciCustomizer = function(objVal, srcVal, key) {
|
|
||||||
return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getFormKeys(fromObj) {
|
const theme = themeConfig.get();
|
||||||
// remove all non-numbers
|
|
||||||
return _.remove(_.keys(fromObj), k => !isNaN(k));
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeMciProperties(dest, src) {
|
// some data brought directly over
|
||||||
Object.keys(src).forEach(mci => {
|
mergedTheme.info = theme.info;
|
||||||
if(dest[mci]) {
|
mergedTheme.achievements = _.get(theme, 'customization.achievements');
|
||||||
_.mergeWith(dest[mci], src[mci], mciCustomizer);
|
|
||||||
} else {
|
|
||||||
// theme contains MCI not in menu; bring in as-is
|
|
||||||
dest[mci] = src[mci];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyThemeMciBlock(dest, src, formKey) {
|
// Create some helpers for this theme
|
||||||
if(_.isObject(src.mci)) {
|
this._setThemeHelpers(mergedTheme);
|
||||||
mergeMciProperties(dest, src.mci);
|
|
||||||
} else {
|
|
||||||
if(_.has(src, [ formKey, 'mci' ])) {
|
|
||||||
mergeMciProperties(dest, src[formKey].mci);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
// merge customizer to disallow immutable MCI properties
|
||||||
// menu.hjson can have a couple different structures:
|
const ImmutableMCIProperties = [
|
||||||
// 1) Explicit declaration of expected MCI code(s) under 'form:<id>' before a 'mci' block
|
'maxLength', 'argName', 'submit', 'validate'
|
||||||
// (this allows multiple layout types defined by one menu for example)
|
];
|
||||||
//
|
|
||||||
// 2) Non-explicit declaration: 'mci' directly under 'form:<id>'
|
|
||||||
//
|
|
||||||
// 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());
|
|
||||||
|
|
||||||
menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) {
|
const mciCustomizer = (objVal, srcVal, key) => {
|
||||||
let applyFrom;
|
return ImmutableMCIProperties.indexOf(key) > -1 ? objVal : srcVal;
|
||||||
if(_.has(menuTheme, [ mciKey, 'mci' ])) {
|
};
|
||||||
applyFrom = menuTheme[mciKey];
|
|
||||||
|
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 {
|
} 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) {
|
const applyThemeMciBlock = (dst, src, formKey) => {
|
||||||
_.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) {
|
if(_.isObject(src.mci)) {
|
||||||
let createdFormSection = false;
|
mergeMciProperties(dst, src.mci);
|
||||||
const mergedThemeMenu = mergedTheme[sectionName][menuName];
|
} 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:<id>' before a 'mci' block
|
||||||
|
// (this allows multiple layout types defined by one menu for example)
|
||||||
|
//
|
||||||
|
// 2) Non-explicit declaration: 'mci' directly under 'form:<id>'
|
||||||
|
//
|
||||||
|
// 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
|
menuMciCodeKeys.forEach(mciKey => {
|
||||||
// :TODO: should probably be _.merge()
|
const src = _.has(menuTheme, [ mciKey, 'mci' ]) ?
|
||||||
if(menuTheme.config) {
|
menuTheme[mciKey] :
|
||||||
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
|
menuTheme;
|
||||||
}
|
|
||||||
|
|
||||||
if('menus' === sectionName) {
|
applyThemeMciBlock(form[mciKey].mci, src, formKey);
|
||||||
if(_.isObject(mergedThemeMenu.form)) {
|
});
|
||||||
getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) {
|
}
|
||||||
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
|
};
|
||||||
});
|
|
||||||
} else {
|
[ 'menus', 'prompts'].forEach(sectionName => {
|
||||||
if(_.isObject(menuTheme.mci)) {
|
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
|
// Not specified at menu level means we apply anything from the
|
||||||
// theme to form.0.mci{}
|
// theme to form.0.mci{}
|
||||||
|
@ -241,158 +290,84 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
|
||||||
mergeMciProperties(mergedThemeMenu.form[0], menuTheme);
|
mergeMciProperties(mergedThemeMenu.form[0], menuTheme);
|
||||||
createdFormSection = true;
|
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
|
// Finished merging for this menu/prompt
|
||||||
//
|
//
|
||||||
// If the following conditions are true, set runtime.autoNext to true:
|
// If the following conditions are true, set runtime.autoNext to true:
|
||||||
// * This is a menu
|
// * This is a menu
|
||||||
// * There is/was no explicit 'form' section
|
// * There is/was no explicit 'form' section
|
||||||
// * There is no 'prompt' specified
|
// * There is no 'prompt' specified
|
||||||
//
|
//
|
||||||
if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) &&
|
if('menus' === sectionName &&
|
||||||
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
|
!_.isString(mergedThemeMenu.prompt) &&
|
||||||
{
|
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
|
||||||
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } );
|
{
|
||||||
}
|
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) {
|
if(_.isString(pwChar)) {
|
||||||
const config = Config();
|
pwChar = pwChar.substr(0, 1);
|
||||||
async.waterfall(
|
} else if(_.isNumber(pwChar)) {
|
||||||
[
|
pwChar = String.fromCharCode(pwChar);
|
||||||
function loadMenuConfig(callback) {
|
}
|
||||||
getFullConfig(config.general.menuFile, (err, menuConfig) => {
|
|
||||||
return callback(err, menuConfig);
|
return pwChar;
|
||||||
});
|
|
||||||
},
|
},
|
||||||
function loadPromptConfig(menuConfig, callback) {
|
getDateFormat : function(style = 'short') {
|
||||||
getFullConfig(config.general.promptFile, (err, promptConfig) => {
|
const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY';
|
||||||
return callback(err, menuConfig, promptConfig);
|
return _.get(theme, `customization.defaults.dateFormat.${style}`, format);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
function loadIt(menuConfig, promptConfig, callback) {
|
getTimeFormat : function(style = 'short') {
|
||||||
loadTheme(themeId, (err, theme) => {
|
const format = Config().theme.timeFormat[style] || 'h:mm a';
|
||||||
if(err) {
|
return _.get(theme, `customization.defaults.timeFormat.${style}`, format);
|
||||||
if(ErrorReasons.NotEnabled !== err.reasonCode) {
|
},
|
||||||
Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme');
|
getDateTimeFormat : function(style = 'short') {
|
||||||
return;
|
const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a';
|
||||||
}
|
return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
],
|
};
|
||||||
(err, theme) => {
|
}
|
||||||
if(err) {
|
|
||||||
Log.warn( { themeId, error : err.message }, 'Failed to reload theme');
|
|
||||||
} else {
|
|
||||||
Log.debug( { info : theme.info }, 'Theme recached' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadAllThemes()
|
_reloadAllThemes() {
|
||||||
{
|
async.each([ ...this.availableThemes.keys() ], (themeId, nextThemeId) => {
|
||||||
async.each([ ...availableThemes.keys() ], themeId => reloadTheme(themeId));
|
this._loadTheme(themeId, err => {
|
||||||
}
|
if (!err) {
|
||||||
|
Log.info({ themeId }, 'Theme reloaded');
|
||||||
function initAvailableThemes(cb) {
|
}
|
||||||
const config = Config();
|
return nextThemeId(null); // always proceed
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAvailableThemes() {
|
function getAvailableThemes() {
|
||||||
return availableThemes;
|
return themeManagerInstance.getAvailableThemes();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRandomTheme() {
|
function getRandomTheme() {
|
||||||
if(availableThemes.size > 0) {
|
const avail = getAvailableThemes();
|
||||||
const themeIds = [ ...availableThemes.keys() ];
|
if(avail.size > 0) {
|
||||||
|
const themeIds = [ ...avail.keys() ];
|
||||||
return themeIds[Math.floor(Math.random() * themeIds.length)];
|
return themeIds[Math.floor(Math.random() * themeIds.length)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
- [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %})
|
- [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %})
|
||||||
- [HJSON Config Files]({{ site.baseurl }}{% link configuration/hjson.md %})
|
- [HJSON Config Files]({{ site.baseurl }}{% link configuration/hjson.md %})
|
||||||
- [Menus]({{ site.baseurl }}{% link configuration/menu-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 %})
|
- [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %})
|
||||||
- [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %})
|
- [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %})
|
||||||
- [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %})
|
- [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.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. |
|
| `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. |
|
| `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
|
#### Defaults
|
||||||
| Item | Description |
|
| Item | Description |
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
layout: page
|
layout: page
|
||||||
title: Colour Codes
|
title: Colour Codes
|
||||||
---
|
---
|
||||||
ENiGMA½ supports Renegade-style pipe colour codes for formatting strings. You'll see them used in [`config.hjson`](config-hjson),
|
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.
|
||||||
[`prompt.hjson`](prompt-hjson), [`menu.hjson`](menu-hjson), and can also be used in places like the oneliner, rumour mod,
|
|
||||||
full screen editor etc.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
When ENiGMA½ encounters colour codes in strings, they'll be processed in order and combined where possible.
|
When ENiGMA½ encounters colour codes in strings, they'll be processed in order and combined where possible.
|
||||||
|
@ -21,5 +19,5 @@ For example:
|
||||||
: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!
|
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")
|
||||||
|
|
||||||
|
|
|
@ -10,5 +10,5 @@ Your initial configuration skeleton can be created using the `oputil.js` command
|
||||||
./oputil.js config new
|
./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/<bbsName>-menu.hjson` and `config/<bbsName>-prompt.hjson` files (where `<bbsName>` 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/<bbsName>-menu.hjson` and `config/<bbsName>-prompt.hjson` files (where `<bbsName>` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) for more information.
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ layout: page
|
||||||
title: HJSON Config Files
|
title: HJSON Config Files
|
||||||
---
|
---
|
||||||
## JSON for Humans!
|
## 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:
|
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).
|
* `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/<yourBBSName>-menu.hjson`. See [Menus](menu-hjson.md).
|
* `menu.hjson`: Refers to `/path/to/enigma-bbs/config/<yourBBSName>-menu.hjson`. See [Menus](menu-hjson.md).
|
||||||
* `prompt.hjson`: Refers to `/path/to/enigma-bbs/config/<yourBBSName>-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 *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:
|
* 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
|
```hjson
|
||||||
|
|
|
@ -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. |
|
| `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). |
|
| `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. |
|
| `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`.
|
| `submit` | Defines a submit handler when using `prompt`.
|
||||||
| `form` | An object defining one or more *forms* available on this menu. |
|
| `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. |
|
| `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 (`*`).
|
* 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).
|
* 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
|
## 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.
|
Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
Loading…
Reference in New Issue