* Add rumorz mod

* ANSI/pipe working properly in VerticalMenuView
* Fix bug in renderStringLength()
* Make initSequence() part of prototype chain for inheritance
* Use proper 'desc' field vs 'status' for menus when setting client/user status
* Move pipeToAnsi() to setItems/setFocusItems vs every render
* Add %RR random rumor MCI
* Predefined MCI's can be init @ startup - RR uses random as a test bed
* Add some StatLog functionality for ordering, keep forever, etc.
* Fix TextView redraw issue
* Better VerticalMenuView drawItem() logic
* Add 'key press' emit for View
* Enable formats for BBS list - works with MCI
* Remove old system_property.js
This commit is contained in:
Bryan Ashby 2016-08-10 22:48:13 -06:00
parent 2b68201f7d
commit 30ba609fb4
13 changed files with 492 additions and 241 deletions

View File

@ -40,8 +40,73 @@ function MenuModule(options) {
this.initViewControllers();
this.initSequence = function() {
this.shouldPause = function() {
return 'end' === self.menuConfig.options.pause || true === self.menuConfig.options.pause;
};
this.hasNextTimeout = function() {
return _.isNumber(self.menuConfig.options.nextTimeout);
};
this.autoNextMenu = function(cb) {
function goNext() {
if(_.isString(self.menuConfig.next) || _.isArray(self.menuConfig.next)) {
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
} else {
return self.prevMenu(cb);
}
}
if(_.has(self.menuConfig, 'runtime.autoNext') && true === self.menuConfig.runtime.autoNext) {
/*
If 'next' is supplied, we'll use it. Otherwise, utlize fallback which
may be explicit (supplied) or non-explicit (previous menu)
'next' may be a simple asset, or a object with next.asset and
extrArgs
next: assetSpec
-or-
next: {
asset: assetSpec
extraArgs: ...
}
*/
if(self.hasNextTimeout()) {
setTimeout( () => {
return goNext();
}, this.menuConfig.options.nextTimeout);
} else {
goNext();
}
}
};
this.haveNext = function() {
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
};
}
require('util').inherits(MenuModule, PluginModule);
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
MenuModule.prototype.enter = function() {
if(_.isString(this.menuConfig.desc)) {
this.client.currentStatus = this.menuConfig.desc;
} else {
this.client.currentStatus = 'Browsing menus';
}
this.initSequence();
};
MenuModule.prototype.initSequence = function() {
var mciData = { };
const self = this;
async.series(
[
@ -144,70 +209,6 @@ function MenuModule(options) {
);
};
this.shouldPause = function() {
return 'end' === self.menuConfig.options.pause || true === self.menuConfig.options.pause;
};
this.hasNextTimeout = function() {
return _.isNumber(self.menuConfig.options.nextTimeout);
};
this.autoNextMenu = function(cb) {
function goNext() {
if(_.isString(self.menuConfig.next) || _.isArray(self.menuConfig.next)) {
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
} else {
return self.prevMenu(cb);
}
}
if(_.has(self.menuConfig, 'runtime.autoNext') && true === self.menuConfig.runtime.autoNext) {
/*
If 'next' is supplied, we'll use it. Otherwise, utlize fallback which
may be explicit (supplied) or non-explicit (previous menu)
'next' may be a simple asset, or a object with next.asset and
extrArgs
next: assetSpec
-or-
next: {
asset: assetSpec
extraArgs: ...
}
*/
if(self.hasNextTimeout()) {
setTimeout( () => {
return goNext();
}, this.menuConfig.options.nextTimeout);
} else {
goNext();
}
}
};
this.haveNext = function() {
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
};
}
require('util').inherits(MenuModule, PluginModule);
require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);
MenuModule.prototype.enter = function() {
if(_.isString(this.menuConfig.status)) {
this.client.currentStatus = this.menuConfig.status;
} else {
this.client.currentStatus = 'Browsing menus';
}
this.initSequence();
};
MenuModule.prototype.getSaveState = function() {
// nothing in base
};

View File

@ -4,6 +4,7 @@
// ENiGMA½
const View = require('./view.js').View;
const miscUtil = require('./misc_util.js');
const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
// deps
const util = require('util');
@ -18,6 +19,8 @@ function MenuView(options) {
View.call(this, options);
this.disablePipe = options.disablePipe || false;
const self = this;
if(options.items) {
@ -60,10 +63,16 @@ function MenuView(options) {
util.inherits(MenuView, View);
MenuView.prototype.setItems = function(items) {
const self = this;
if(items) {
this.items = [];
items.forEach( itemText => {
this.items.push( { text : itemText } );
this.items.push(
{
text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client)
}
);
});
}
};
@ -110,10 +119,16 @@ MenuView.prototype.onKeyPress = function(ch, key) {
};
MenuView.prototype.setFocusItems = function(items) {
const self = this;
if(items) {
this.focusItems = [];
items.forEach( itemText => {
this.focusItems.push( { text : itemText } );
this.focusItems.push(
{
text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client)
}
);
});
}
};

View File

@ -16,6 +16,24 @@ const _ = require('lodash');
const moment = require('moment');
exports.getPredefinedMCIValue = getPredefinedMCIValue;
exports.init = init;
function init(cb) {
setNextRandomRumor(cb);
}
function setNextRandomRumor(cb) {
StatLog.getSystemLogEntries('system_rumorz', StatLog.Order.Random, 1, (err, entry) => {
if(entry) {
entry = entry[0];
}
const randRumor = entry && entry.log_value ? entry.log_value : '';
StatLog.setNonPeristentSystemStat('random_rumor', randRumor);
if(cb) {
return cb(null);
}
});
}
function getPredefinedMCIValue(client, code) {
@ -138,6 +156,13 @@ function getPredefinedMCIValue(client, code) {
TC : function totalCalls() { return StatLog.getSystemStat('login_count').toString(); },
RR : function randomRumor() {
// start the process of picking another random one
setNextRandomRumor();
return StatLog.getSystemStat('random_rumor');
},
//
// Special handling for XY
//

View File

@ -44,6 +44,21 @@ class StatLog {
);
}
get KeepDays() {
return {
Forever : -1,
};
}
get Order() {
return {
Timestamp : 'timestamp_asc',
TimestampAsc : 'timestamp_asc',
TimestampDesc : 'timestamp_desc',
Random : 'random',
};
}
setNonPeristentSystemStat(statName, statValue) {
this.systemStats[statName] = statValue;
}
@ -123,6 +138,13 @@ class StatLog {
//
// Handle keepDays
//
if(-1 === keepDays) {
if(cb) {
return cb(null);
}
return;
}
sysDb.run(
`DELETE FROM system_event_log
WHERE log_name = ? AND timestamp <= DATETIME("now", "-${keepDays} day");`,
@ -146,8 +168,16 @@ class StatLog {
switch(order) {
case 'timestamp' :
case 'timestamp_asc' :
sql += ' ORDER BY timestamp ASC';
break;
case 'timestamp_desc' :
sql += ' ORDER BY timestamp DESC';
break;
case 'random' :
sql += ' ORDER BY RANDOM()';
}
if(!cb && _.isFunction(limit)) {
@ -177,6 +207,13 @@ class StatLog {
//
// Handle keepDays
//
if(-1 === keepDays) {
if(cb) {
return cb(null);
}
return;
}
sysDb.run(
`DELETE FROM user_event_log
WHERE user_id = ? AND log_name = ? AND timestamp <= DATETIME("now", "-${keepDays} day");`,

View File

@ -236,7 +236,8 @@ exports.stringFormatExtensions = stringFormatExtensions;
// :TODO: Add other codes from ansi_escape_parser
const ANSI_REGEXP = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const ANSI_OR_PIPE_REGEXP = new RegExp(ANSI_REGEXP.source + '\\|[A-Z\d]{2}', 'g');
const PIPE_REGEXP = /\|[A-Z\d]{2}/g;
const ANSI_OR_PIPE_REGEXP = new RegExp(ANSI_REGEXP.source + '|' + PIPE_REGEXP.source, 'g');
//
// Similar to substr() but works with ANSI/Pipe code strings
@ -279,10 +280,8 @@ function renderSubstr(str, start, length) {
// Pipe and ANSI color codes. Note that currently ANSI *movement*
// codes are not considred!
//
// :TODO: consolidate ANSI code RegExp's and such
const PIPE_AND_ANSI_RE = ANSI_OR_PIPE_REGEXP;// /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\|[A-Z\d]{2}/g
function renderStringLength(str) {
return str.replace(PIPE_AND_ANSI_RE, '').length;
return str.replace(ANSI_OR_PIPE_REGEXP, '').length;
}

View File

@ -1,38 +0,0 @@
/* jslint node: true */
'use strict';
var sysDb = require('./database.js').dbs.system;
exports.loadSystemProperties = loadSystemProperties;
exports.persistSystemProperty = persistSystemProperty;
exports.getSystemProperty = getSystemProperty;
var systemProperties = {};
exports.systemProperties = systemProperties;
function loadSystemProperties(cb) {
sysDb.each(
'SELECT prop_name, prop_value ' +
'FROM system_property;',
function rowResult(err, row) {
systemProperties[row.prop_name] = row.prop_value;
},
cb
);
}
function persistSystemProperty(propName, propValue, cb) {
// update live
systemProperties[propName] = propValue;
sysDb.run(
'REPLACE INTO system_property ' +
'VALUES (?, ?);',
[ propName, propValue ],
cb
);
}
function getSystemProperty(propName) {
return systemProperties[propName];
}

View File

@ -34,7 +34,6 @@ function TextView(options) {
this.justify = options.justify || 'right';
this.resizable = miscUtil.valueWithDefault(options.resizable, true);
this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true);
this.text = options.text || '';
if(_.isString(options.textOverflow)) {
this.textOverflow = options.textOverflow;
@ -136,8 +135,7 @@ function TextView(options) {
return this.position.col + offset;
};
// :TODO: Whatever needs init here should be done separately from setText() since it redraws/etc.
// this.setText(options.text || '');
this.setText(options.text || '', false); // false=do not redraw now
}
util.inherits(TextView, View);
@ -175,7 +173,9 @@ TextView.prototype.getData = function() {
return this.text;
};
TextView.prototype.setText = function(text) {
TextView.prototype.setText = function(text, redraw) {
redraw = _.isBoolean(redraw) ? redraw : true;
if(!_.isString(text)) {
text = text.toString();
}
@ -201,7 +201,9 @@ TextView.prototype.setText = function(text) {
this.dimens.width = this.text.length + widthDelta;
}
if(redraw) {
this.redraw();
}
};
/*

View File

@ -48,60 +48,33 @@ function VerticalMenuView(options) {
};
};
/*
this.drawItem = function(index) {
var item = self.items[index];
if(!item) {
return;
}
var text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle);
self.client.term.write(
ansi.goto(item.row, self.position.col) +
(index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()) +
strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)
);
};
*/
this.drawItem = function(index) {
const item = self.items[index];
if(!item) {
return;
}
let focusItem;
let text;
if(self.hasFocusItems()) {
focusItem = self.focusItems[index];
}
if(focusItem) {
if(item.focused) {
text = strUtil.stylizeString(focusItem.text, self.focusTextStyle);
let sgr;
if(item.focused && self.hasFocusItems()) {
const focusItem = self.focusItems[index];
text = strUtil.stylizeString(
focusItem ? focusItem.text : item.text,
self.textStyle
);
sgr = '';
} else {
text = strUtil.stylizeString(item.text, self.textStyle);
sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR());
}
// :TODO: Need to support pad()
// :TODO: shoudl we detect if pipe codes are used?
self.client.term.write(
ansi.goto(item.row, self.position.col) +
colorCodes.pipeToAnsi(text, self.client)
);
} else {
text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle);
text += self.getSGR();
self.client.term.write(
ansi.goto(item.row, self.position.col) +
(index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()) +
sgr +
strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)
);
}
};
}

View File

@ -254,7 +254,7 @@ View.prototype.setFocus = function(focused) {
View.prototype.onKeyPress = function(ch, key) {
if(false === this.hasFocus) {
console.log('doh!');
console.log('doh!'); // :TODO: fix me -- assert here?
}
assert(this.hasFocus, 'View does not have focus');
assert(this.acceptsInput, 'View does not accept input');
@ -272,6 +272,8 @@ View.prototype.onKeyPress = function(ch, key) {
if(ch) {
assert(1 === ch.length);
}
this.emit('key press', ch, key);
};
View.prototype.getData = function() {

View File

@ -23,7 +23,6 @@ function ViewController(options) {
assert(_.isObject(options));
assert(_.isObject(options.client));
events.EventEmitter.call(this);
var self = this;

View File

@ -117,21 +117,11 @@ function BBSListModule(options) {
};
this.setEntries = function(entriesView) {
/*
:TODO: This is currently disabled until VerticalMenuView 'justify' works properly with pipe code strings
const listFormat = config.listFormat || '{bbsName}';
const focusListFormat = config.focusListFormat || '{bbsName}';
entriesView.setItems(self.entries.map( e => {
return listFormat.format(e);
}));
entriesView.setFocusItems(self.entries.map( e => {
return focusListFormat.format(e);
}));
*/
entriesView.setItems(self.entries.map(e => e.bbsName));
entriesView.setItems(self.entries.map( e => listFormat.format(e) ) );
entriesView.setFocusItems(self.entries.map( e => focusListFormat.format(e) ) );
};
this.displayBBSList = function(clearScreen, cb) {

View File

@ -66,7 +66,7 @@ LastCallersModule.prototype.mciReady = function(mciData, cb) {
function fetchHistory(callback) {
callersView = vc.getView(MciCodeIds.CallerList);
StatLog.getSystemLogEntries('user_login_history', 'timestamp_desc', callersView.dimens.height, (err, lh) => {
StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, callersView.dimens.height, (err, lh) => {
loginHistory = lh;
return callback(err);
});

246
mods/rumorz.js Normal file
View File

@ -0,0 +1,246 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const theme = require('../core/theme.js');
const resetScreen = require('../core/ansi_term.js').resetScreen;
const StatLog = require('../core/stat_log.js');
const renderStringLength = require('../core/string_util.js').renderStringLength;
// deps
const async = require('async');
const _ = require('lodash');
exports.moduleInfo = {
name : 'Rumorz',
desc : 'Standard local rumorz',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.rumorz',
};
const STATLOG_KEY_RUMORZ = 'system_rumorz';
const FormIds = {
View : 0,
Add : 1,
};
const MciCodeIds = {
ViewForm : {
Entries : 1,
AddPrompt : 2,
},
AddForm : {
NewEntry : 1,
EntryPreview : 2,
AddPrompt : 3,
}
};
exports.getModule = class RumorzModule extends MenuModule {
constructor(options) {
super(options);
this.menuMethods = {
viewAddScreen : (formData, extraArgs, cb) => {
return this.displayAddScreen(cb);
},
addEntry : (formData, extraArgs, cb) => {
if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) {
const rumor = formData.value.rumor.trim(); // remove any trailing ws
StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, () => {
this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls
});
} else {
// empty message - treat as if cancel was hit
return this.displayViewScreen(true, cb); // true=cls
}
},
cancelAdd : (formData, extraArgs, cb) => {
this.clearAddForm();
return this.displayViewScreen(true, cb); // true=cls
}
};
}
get config() { return this.menuConfig.config; }
clearAddForm() {
const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
newEntryView.setText('');
// preview is optional
if(previewView) {
previewView.setText('');
}
}
initSequence() {
const self = this;
async.series(
[
function beforeDisplayArt(callback) {
self.beforeArt(callback);
},
function display(callback) {
self.displayViewScreen(false, callback);
}
],
err => {
if(err) {
// :TODO: Handle me -- initSequence() should really take a completion callback
}
self.finishedLoading();
}
);
}
displayViewScreen(clearScreen, cb) {
const self = this;
async.waterfall(
[
function clearAndDisplayArt(callback) {
if(self.viewControllers.add) {
self.viewControllers.add.setFocus(false);
}
if(clearScreen) {
self.client.term.rawWrite(resetScreen());
}
theme.displayThemedAsset(
self.config.art.entries,
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
return callback(err, artData);
}
);
},
function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController(
'view',
new ViewController( { client : self.client, formId : FormIds.View } )
);
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds.View,
};
return vc.loadFromMenuConfig(loadOpts, callback);
} else {
self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw();
return callback(null);
}
},
function fetchEntries(callback) {
const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries);
StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => {
return callback(err, entriesView, entries);
});
},
function populateEntries(entriesView, entries, callback) {
const config = self.config;
const listFormat = config.listFormat || '{rumor}';
const focusListFormat = config.focusListFormat || listFormat;
entriesView.setItems(entries.map( e => listFormat.format( { rumor : e.log_value } ) ) );
entriesView.setFocusItems(entries.map(e => focusListFormat.format( { rumor : e.log_value } ) ) );
entriesView.redraw();
return callback(null);
},
function finalPrep(callback) {
const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt);
promptView.setFocusItemIndex(1); // default to NO
return callback(null);
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
displayAddScreen(cb) {
const self = this;
async.waterfall(
[
function clearAndDisplayArt(callback) {
self.viewControllers.view.setFocus(false);
self.client.term.rawWrite(resetScreen());
theme.displayThemedAsset(
self.config.art.add,
self.client,
{ font : self.menuConfig.font },
(err, artData) => {
return callback(err, artData);
}
);
},
function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController(
'add',
new ViewController( { client : self.client, formId : FormIds.Add } )
);
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds.Add,
};
return vc.loadFromMenuConfig(loadOpts, callback);
} else {
self.viewControllers.add.setFocus(true);
self.viewControllers.add.redrawAll();
self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry);
return callback(null);
}
},
function initPreviewUpdates(callback) {
const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview);
const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry);
if(previewView) {
let timerId;
entryView.on('key press', () => {
clearTimeout(timerId);
timerId = setTimeout( () => {
const focused = self.viewControllers.add.getFocusedView();
if(focused === entryView) {
previewView.setText(entryView.getData());
focused.setFocus(true);
}
}, 500);
});
}
return callback(null);
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
};