From 40944e5e7a4bbe1e982781f3c62f542acb1f3f01 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 12 Jan 2016 10:30:38 -0700 Subject: [PATCH 1/3] Note about QEMU caveat --- docs/doors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doors.md b/docs/doors.md index dd1c0ec4..0a1fbfdf 100644 --- a/docs/doors.md +++ b/docs/doors.md @@ -84,7 +84,7 @@ doorPimpWars: { ``` ### QEMU with abracadabra -[QEMU](http://wiki.qemu.org/Main_Page) provides a robust, cross platform solution for launching doors under many platforms (likely anwywhere Node.js is supported and ENiGMA½ can run). Being more flexible means being a bit more complex. Let's look at an example for running L.O.R.D. under a UNIX like system such as Linux or FreeBSD. +[QEMU](http://wiki.qemu.org/Main_Page) provides a robust, cross platform solution for launching doors under many platforms (likely anwywhere Node.js is supported and ENiGMA½ can run). Note however that there is an important and major caveat: **Multiple instances of a particular door/OS image should not be run at once!** Being more flexible means being a bit more complex. Let's look at an example for running L.O.R.D. under a UNIX like system such as Linux or FreeBSD. Basically we'll be creating a bootstrap shell script that generates a temporary node specific `go.bat` to launch our door. This will be called from `autoexec.bat` within our QEMU FreeDOS partition. From 4fdd3dbbfe442ecb5a8c0a2fcc26d2fa3dca383d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 14 Jan 2016 22:44:33 -0700 Subject: [PATCH 2/3] MAJOR CHANGE to theming system: * Less complex * Themes are only loaded once. Users share avail themes[] objects * Themes are applied to configuration _once_ * Users can switch themes in configuration * Other related improvements --- core/bbs.js | 8 +- core/config.js | 1 + core/menu_module.js | 7 -- core/menu_stack.js | 2 +- core/menu_util.js | 56 +++++--------- core/theme.js | 180 ++++++++++++++++++++++++++++++++++++++++++-- core/user_config.js | 4 + core/user_login.js | 15 ++-- 8 files changed, 207 insertions(+), 66 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 2b9d0020..92b0e094 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -274,9 +274,7 @@ function prepareClient(client, cb) { } else { client.user.properties.theme_id = conf.config.preLoginTheme; } - - theme.loadTheme(client.user.properties.theme_id, function themeLoaded(err, theme) { - client.currentTheme = theme; - cb(null); - }); + + theme.setClientTheme(client, client.user.properties.theme_id); + cb(null); // note: currently useless to use cb here - but this may change...again... } \ No newline at end of file diff --git a/core/config.js b/core/config.js index a199d54d..6bc9ac61 100644 --- a/core/config.js +++ b/core/config.js @@ -87,6 +87,7 @@ function getDefaultConfig() { loginAttempts : 3, menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./mods) + promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./mods) }, // :TODO: see notes below about 'theme' section - move this! diff --git a/core/menu_module.js b/core/menu_module.js index cdacb484..3c5f10eb 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -194,13 +194,6 @@ MenuModule.prototype.enter = function(client) { this.client = client; assert(_.isObject(client)); - menuUtil.applyGeneralThemeCustomization( { - name : this.menuName, - client : this.client, - type : 'menus', - config : this.menuConfig.config, - }); - if(_.isString(this.menuConfig.status)) { this.client.currentStatus = this.menuConfig.status; } else { diff --git a/core/menu_stack.js b/core/menu_stack.js index ed855897..5caf6531 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -101,7 +101,7 @@ MenuStack.prototype.goto = function(name, options, cb) { client : self.client, }; - if(options) { + if(_.isObject(options)) { loadOpts.extraArgs = options.extraArgs; } diff --git a/core/menu_util.js b/core/menu_util.js index abca4e94..936992d2 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 configCache = require('./config_cache.js'); +var getFullConfig = require('./config_util.js').getFullConfig; var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; var acsUtil = require('./acs_util.js'); @@ -22,53 +22,33 @@ exports.loadMenu = loadMenu; exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; exports.handleAction = handleAction; exports.handleNext = handleNext; -exports.applyGeneralThemeCustomization = applyGeneralThemeCustomization; -exports.applyMciThemeCustomization = applyMciThemeCustomization; +//exports.applyGeneralThemeCustomization = applyGeneralThemeCustomization; +//exports.applyMciThemeCustomization = applyMciThemeCustomization; -function getMenuConfig(name, cb) { +function getMenuConfig(client, name, cb) { var menuConfig; async.waterfall( [ - function loadMenuJSON(callback) { - var menuFilePath = Config.general.menuFile; - - // menuFile is assumed to be in 'mods' if a path is not supplied - if('.' === paths.dirname(menuFilePath)) { - menuFilePath = paths.join(__dirname, '../mods', menuFilePath); - } - - configCache.getConfig(menuFilePath, function loaded(err, menuJson) { - callback(err, menuJson); - }); - }, - function locateMenuConfig(menuJson, callback) { - if(_.has(menuJson, [ 'menus', name ])) { - menuConfig = menuJson.menus[name]; + function locateMenuConfig(callback) { + if(_.has(client.currentTheme, [ 'menus', name ])) { + menuConfig = client.currentTheme.menus[name]; callback(null); } else { callback(new Error('No menu entry for \'' + name + '\'')); } }, - function loadPromptJSON(callback) { + function locatePromptConfig(callback) { if(_.isString(menuConfig.prompt)) { - configCache.getModConfig('prompt.hjson', function loaded(err, promptJson, reCached) { - callback(err, promptJson); - }); - } else { - callback(null, null); - } - }, - function locatePromptConfig(promptJson, callback) { - if(promptJson) { - if(_.has(promptJson, [ 'prompts', menuConfig.prompt ])) { - menuConfig.promptConfig = promptJson.prompts[menuConfig.prompt]; + if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) { + menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; + callback(null); } else { callback(new Error('No prompt entry for \'' + menuConfig.prompt + '\'')); - return; - } + } + } else { + callback(null); } - callback(null); } ], function complete(err) { @@ -85,7 +65,7 @@ function loadMenu(options, cb) { async.waterfall( [ function getMenuConfiguration(callback) { - getMenuConfig(options.name, function menuConfigLoaded(err, menuConfig) { + getMenuConfig(options.client, options.name, function menuConfigLoaded(err, menuConfig) { callback(err, menuConfig); }); }, @@ -272,7 +252,7 @@ function handleNext(client, nextSpec, conf) { // :TODO: Seems better in theme.js, but that includes ViewController...which would then include theme.js // ...theme.js only brings in VC to create themed pause prompt. Perhaps that should live elsewhere - +/* function applyGeneralThemeCustomization(options) { // // options.name @@ -298,8 +278,9 @@ function applyGeneralThemeCustomization(options) { } } } +*/ - +/* function applyMciThemeCustomization(options) { // // options.name : menu/prompt name @@ -346,3 +327,4 @@ function applyMciThemeCustomization(options) { // :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 ae6e1ca6..d4846b5f 100644 --- a/core/theme.js +++ b/core/theme.js @@ -7,6 +7,7 @@ var ansi = require('./ansi_term.js'); var miscUtil = require('./misc_util.js'); var Log = require('./logger.js').log; var configCache = require('./config_cache.js'); +var getFullConfig = require('./config_util.js').getFullConfig; var asset = require('./asset.js'); var ViewController = require('./view_controller.js').ViewController; @@ -16,11 +17,10 @@ var async = require('async'); var _ = require('lodash'); var assert = require('assert'); - -exports.loadTheme = loadTheme; exports.getThemeArt = getThemeArt; exports.getAvailableThemes = getAvailableThemes; exports.getRandomTheme = getRandomTheme; +exports.setClientTheme = setClientTheme; exports.initAvailableThemes = initAvailableThemes; exports.displayThemeArt = displayThemeArt; exports.displayThemedPause = displayThemedPause; @@ -102,25 +102,174 @@ function loadTheme(themeID, cb) { var availableThemes = {}; +var 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) + // + var mergedTheme = _.cloneDeep(menuConfig); + + if(_.isObject(promptConfig.prompts)) { + mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); + } + + // + // Add in data we won't be altering directly from the theme + // + mergedTheme.info = theme.info; + mergedTheme.helpers = theme.helpers; + + // + // merge customizer to disallow immutable MCI properties + // + var mciCustomizer = function(objVal, srcVal, key) { + return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; + }; + + function getFormKeys(fromObj) { + return _.remove(_.keys(fromObj), function pred(k) { + return !isNaN(k); // remove all non-numbers + }); + } + + function mergeMciProperties(dest, src) { + Object.keys(src).forEach(function mciEntry(mci) { + _.merge(dest[mci], src[mci], mciCustomizer); + }); + } + + function applyThemeMciBlock(dest, src, formKey) { + if(_.isObject(src.mci)) { + mergeMciProperties(dest, src.mci); + } else { + if(_.has(src, [ formKey, 'mci' ])) { + mergeMciProperties(dest, src[formKey].mci); + } + } + } + + // + // 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 { + var menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { + return k === k.toUpperCase(); // remove anything not uppercase + }); + + menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { + var applyFrom; + if(_.has(menuTheme, [ mciKey, 'mci' ])) { + applyFrom = menuTheme[mciKey]; + } else { + applyFrom = menuTheme; + } + + applyThemeMciBlock(form[mciKey].mci, applyFrom); + }); + } + } + + [ 'menus', 'prompts' ].forEach(function areaEntry(areaName) { + _.keys(mergedTheme[areaName]).forEach(function menuEntry(menuName) { + if(_.has(theme, [ 'customization', areaName, menuName ])) { + + var menuTheme = theme.customization[areaName][menuName]; + var mergedThemeMenu = mergedTheme[areaName][menuName]; + + // config block is direct assign/overwrite + // :TODO: should probably be _.merge() + if(menuTheme.config) { + mergedThemeMenu.config = _.assign(mergedThemeMenu || {}, menuTheme.config); + } + + if('menus' === areaName) { + if(_.isObject(mergedThemeMenu.form)) { + getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { + applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); + }); + } + } else if('prompts' === areaName) { + // no 'form' or form keys for prompts -- direct to mci + applyToForm(mergedThemeMenu, menuTheme); + } + } + }); + }); + + + return mergedTheme; +} + function initAvailableThemes(cb) { + var menuConfig; + var promptConfig; + async.waterfall( [ + function loadMenuConfig(callback) { + getFullConfig(Config.general.menuFile, function gotConfig(err, mc) { + menuConfig = mc; + callback(err); + }); + }, + function loadPromptConfig(callback) { + getFullConfig(Config.general.promptFile, function gotConfig(err, pc) { + promptConfig = pc; + callback(err); + }); + }, function getDir(callback) { - fs.readdir(Config.paths.themes, function onReadDir(err, files) { + fs.readdir(Config.paths.themes, function dirRead(err, files) { callback(err, files); }); }, function filterFiles(files, callback) { - var filtered = files.filter(function onFilter(file) { + var filtered = files.filter(function filter(file) { return fs.statSync(paths.join(Config.paths.themes, file)).isDirectory(); }); callback(null, filtered); }, function populateAvailable(filtered, callback) { - filtered.forEach(function onTheme(themeId) { + // :TODO: this is a bit broken with callback placement and configCache.on() handler + + filtered.forEach(function themeEntry(themeId) { loadTheme(themeId, function themeLoaded(err, theme, themePath) { if(!err) { - availableThemes[themeId] = theme; + availableThemes[themeId] = getMergedTheme(menuConfig, promptConfig, theme); configCache.on('recached', function recached(path) { if(themePath === path) { @@ -164,6 +313,20 @@ function getRandomTheme() { } } +function setClientTheme(client, themeId) { + var desc; + + try { + client.currentTheme = getAvailableThemes()[themeId]; + desc = 'Set client theme'; + } catch(e) { + client.currentTheme = getAvailableThemes()[Config.defaults.theme]; + desc = 'Failed setting theme by supplied ID; Using default'; + } + + client.log.debug( { themeId : themeId, info : client.currentTheme.info }, desc); +} + function getThemeArt(options, cb) { // // options - required: @@ -333,11 +496,12 @@ function displayThemedPause(options, cb) { }, function clearPauseArt(callback) { if(options.clearPrompt) { - if(artInfo.startRow) { + if(artInfo.startRow && artInfo.height) { options.client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); + // :TODO: This will not work with NetRunner: options.client.term.rawWrite(ansi.deleteLine(artInfo.height)); } else { - options.client.term.rawWrite(ansi.up(1) + ansi.deleteLine()); + options.client.term.rawWrite(ansi.eraseLine(1)) } } callback(null); diff --git a/core/user_config.js b/core/user_config.js index 5dfb85e5..66eaa2e9 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -120,7 +120,11 @@ function UserConfigModule(options) { term_height : formData.value.termHeight.toString(), theme_id : self.availThemeInfo[formData.value.theme].themeId, }; + + // runtime set theme + theme.setClientTheme(self.client, newProperties.theme_id); + // persist all changes self.client.user.persistProperties(newProperties, function persisted(err) { if(err) { self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); diff --git a/core/user_login.js b/core/user_login.js index 3b5798a9..72ca22e2 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -1,11 +1,12 @@ /* jslint node: true */ 'use strict'; -var theme = require('./theme.js'); +var setClientTheme = require('./theme.js').setClientTheme; var clientConnections = require('./client_connections.js').clientConnections; var userDb = require('./database.js').dbs.user; var sysProp = require('./system_property.js'); var logger = require('./logger.js'); +var Config = require('./config.js').config; var async = require('async'); var _ = require('lodash'); @@ -47,7 +48,7 @@ function userLogin(client, username, password, cb) { ); var existingConnError = new Error('Already logged in as supplied user'); - existingClientConnection.existingConn = true; + existingConnError.existingConn = true; return cb(existingClientConnection); } @@ -59,12 +60,10 @@ function userLogin(client, username, password, cb) { async.parallel( [ - function loadThemeConfig(callback) { - theme.loadTheme(user.properties.theme_id, function themeLoaded(err, theme) { - client.currentTheme = theme; - callback(null); // always non-fatal - }); - }, + function setTheme(callback) { + setClientTheme(client, user.properties.theme_id); + callback(null); + }, function updateSystemLoginCount(callback) { var sysLoginCount = sysProp.getSystemProperty('login_count') || 0; sysLoginCount = parseInt(sysLoginCount, 10) + 1; From 5688926989adb19ab997de07efc8c17e099863e3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 14 Jan 2016 22:48:42 -0700 Subject: [PATCH 3/3] Feedback to op from main, etc. --- mods/menu.hjson | 132 +++++++++++++++++++ mods/themes/luciano_blocktronics/theme.hjson | 15 +++ 2 files changed, 147 insertions(+) diff --git a/mods/menu.hjson b/mods/menu.hjson index 7c710ce2..de4f40da 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -636,6 +636,14 @@ value: { command: "S" } action: @menu:mainMenuSystemStats } + { + value: { command: "!" } + action: @menu:mainMenuGlobalNewScan + } + { + value: { command: "K" } + action: @menu:mainMenuFeedbackToSysOp + } { value: 1 action: @menu:mainMenu @@ -775,6 +783,130 @@ } } } + + mainMenuGlobalNewScan: { + desc: Performing New Scan + module: @systemModule:new_scan + art: NEWSCAN + config: { + messageListMenu: newScanMessageList + } + } + + mainMenuFeedbackToSysOp: { + status: Feedback to SysOp + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaName: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @config:general.sysOp.username + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + /////////////////////////////////////////////////////////////////////// // Doors Menu /////////////////////////////////////////////////////////////////////// diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/mods/themes/luciano_blocktronics/theme.hjson index fdc783ad..60d56059 100644 --- a/mods/themes/luciano_blocktronics/theme.hjson +++ b/mods/themes/luciano_blocktronics/theme.hjson @@ -142,6 +142,21 @@ } } + mainMenuFeedbackToSysOp: { + 0: { + mci: { + TL1: { width: 19, textOverflow: "..." } + ET2: { width: 19, textOverflow: "..." } + ET3: { width: 19, textOverflow: "..." } + } + } + 1: { + mci: { + MT1: { height: 14 } + } + } + } + messageAreaMessageList: { config: { listFormat: "|00|15{msgNum:>4} |03{subj:<29.29} |11{from:<20.20} |03{ts} |01|31{newIndicator}"