From 5faa11664bc11f5bf456d80cc89117b265e3a3fc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 16 Apr 2015 22:29:53 -0600 Subject: [PATCH] * Lots of work with fonts: Support mappings of various cterm/SyncTERM fonts. * Load font info from SAUCE * Better work with defaults & theme values --- core/ansi_term.js | 122 +++++++++++++++++++++++++++++++++++-- core/art.js | 30 ++++++++- core/button_view.js | 2 +- core/client.js | 9 +-- core/config.js | 9 ++- core/connect.js | 22 ++++++- core/mci_view_factory.js | 17 +----- core/menu_module.js | 10 ++- core/text_view.js | 2 +- core/theme.js | 36 ++++++++++- core/vertical_menu_view.js | 2 +- core/view.js | 2 +- core/view_controller.js | 6 +- mods/apply.js | 2 +- mods/menu.json | 1 + 15 files changed, 233 insertions(+), 39 deletions(-) diff --git a/core/ansi_term.js b/core/ansi_term.js index 671f041c..903d5b08 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -14,6 +14,8 @@ var assert = require('assert'); var binary = require('binary'); var miscUtil = require('./misc_util.js'); +var _ = require('lodash'); + exports.getFGColorValue = getFGColorValue; exports.getBGColorValue = getBGColorValue; exports.sgr = sgr; @@ -22,7 +24,7 @@ exports.resetScreen = resetScreen; exports.normal = normal; exports.goHome = goHome; exports.disableVT100LineWrapping = disableVT100LineWrapping; -exports.setSyncTermFont = setSyncTermFont; +exports.setFont = setFont; exports.fromPipeCode = fromPipeCode; @@ -115,6 +117,95 @@ function getBGColorValue(name) { // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // :TODO: document +// :TODO: Create mappings for aliases... maybe make this a map to values instead +var FONT_MAP = { + // Codepage 437 English + 'cp437' : 0, + 'ibmpc' : 0, + 'ibm_pc' : 0, + 'ibm_vga' : 0, + 'pc' : 0, + 'cp437_art' : 0, + 'ibmpcart' : 0, + 'ibmpc_art' : 0, + 'ibm_pc_art' : 0, + 'msdos_art' : 0, + 'msdosart' : 0, + 'pc_art' : 0, + 'pcart' : 0, + + // Codepage 1251 Cyrillic, (swiss) + 'cp1251-swiss' : 1, + + // Russian koi8-r + 'koi8_r' : 2, + 'koi8-r' : 2, + 'koi8r' : 2, + + // ISO-8859-2 Central European + 'iso8859_2' : 3, + 'iso8859-2' : 3, + + // ISO-8859-4 Baltic wide (VGA 9bit mapped) + 'iso8859_4-baltic9b' : 4, + + // Codepage 866 (c) Russian + 'cp866-c' : 5, + + 'iso8859_9' : 6, + 'haik8' : 7, + 'iso8859_8' : 8, + 'koi8_u' : 9, + 'iso8859_15-thin' : 10, + 'iso8859_4' : 11, + 'koi8_r_b' : 12, + 'iso8859_4-baltic-wide' : 13, + 'iso8859_5' : 14, + 'ARMSCII_8' : 15, + 'iso8859_15' : 16, + 'cp850' : 17, + 'cp850-thin' : 18, + 'cp885-thin' : 19, + 'cp1251' : 20, + 'iso8859_7' : 21, + 'koi8-r_c' : 22, + 'iso8859_4-baltic' : 23, + 'iso8859_1' : 24, + 'cp866' : 25, + 'cp437-thin' : 26, + 'cp866-b' : 27, + 'cp885' : 28, + 'cp866_u' : 29, + 'iso8859_1-thin' : 30, + 'cp1131' : 31, + 'c64_upper' : 32, + 'c64_lower' : 33, + 'c128_upper' : 34, + 'c128_lower' : 35, + + 'atari' : 36, + 'atarist' : 36, + + 'pot_noodle' : 37, + 'p0tnoodle' : 37, + + 'mo_soul' : 38, + 'mosoul' : 38, + 'mO\'sOul' : 38, + + 'microknight_plus' : 39, + + 'topaz_plus' : 40, + 'topazplus' : 40, + 'amiga_topaz_2+' : 40, + 'topaz2plus' : 40, + + 'microknight' : 41, + 'topaz' : 42, + +}; + + var SYNC_TERM_FONTS = [ 'cp437', 'cp1251', @@ -155,8 +246,10 @@ var SYNC_TERM_FONTS = [ 'atari', 'pot_noodle', 'mo_soul', - 'microknight', - 'topaz' + 'microknight_plus', + 'topaz_plus', + 'microknight', + 'topaz', ]; // Create methods such as up(), nextLine(),... @@ -242,7 +335,12 @@ function disableVT100LineWrapping() { return ESC_CSI + '7l'; } -function setSyncTermFont(name, fontPage) { +// +// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt +// +// :TODO: allow full spec here. +/* +function setFont(name, fontPage) { fontPage = miscUtil.valueWithDefault(fontPage, 0); assert(fontPage === 0 || fontPage === 1); // see spec @@ -253,6 +351,22 @@ function setSyncTermFont(name, fontPage) { } return ''; } +*/ + +function setFont(name, fontPage) { + name = name.toLowerCase().replace(/ /g, '_'); // conform to map + + var p1 = miscUtil.valueWithDefault(fontPage, 0); + + assert(p1 >= 0 && p1 <= 3); + + var p2 = FONT_MAP[name]; + if(_.isNumber(p2)) { + return ESC_CSI + p1 + ';' + p2 + ' D'; + } + + return ''; +} // Also add: // * fromRenegade(): |<0-23> diff --git a/core/art.js b/core/art.js index c3bd56c3..bea77442 100644 --- a/core/art.js +++ b/core/art.js @@ -189,6 +189,12 @@ function parseCharacterSAUCE(sauce) { return result; } +function getFontNameFromSAUCE(sauce) { + if(sauce.Character) { + return sauce.Character.fontName; + } +} + function sliceAtEOF(data, eofMarker) { var eof = data.length; // :TODO: max scan back or other beter way of doing this?! @@ -213,7 +219,7 @@ function getArtFromPath(path, options, cb) { // var ext = paths.extname(path).toLowerCase(); var encoding = options.encodedAs || defaultEncodingFromExtension(ext); - + // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? function sliceOfData() { @@ -248,12 +254,14 @@ function getArtFromPath(path, options, cb) { // the information provided by SAUCE, use that. // if(!options.encodedAs) { + /* if(sauce.Character && sauce.Character.fontName) { var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName]; if(enc) { encoding = enc; } } + */ } cb(null, getResult(sauce)); } @@ -469,6 +477,26 @@ function display(options, cb) { } }); + // :TODO: If options.font, set the font via ANSI + // ...this should come from sauce, be passed in, or defaulted + + var ansiFont = ''; + if(options.font) { + // :TODO: how to set to ignore SAUCE? + ansiFont = ansi.setFont(options.font); + } else if(options.sauce) { + var fontName = getFontNameFromSAUCE(options.sauce); + + if(fontName) { + ansiFont = ansi.setFont(fontName); + } + } + + if(ansiFont.length > 1) { + options.client.term.write(ansiFont); + } + + if(iceColors) { options.client.term.write(ansi.blinkToBrightIntensity()); } diff --git a/core/button_view.js b/core/button_view.js index 37215e87..9f68d6cc 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -29,6 +29,6 @@ ButtonView.prototype.onKeyPress = function(key, isSpecial) { } }; -ButtonView.prototype.getViewData = function() { +ButtonView.prototype.getData = function() { return null; }; diff --git a/core/client.js b/core/client.js index 913e4868..ae634665 100644 --- a/core/client.js +++ b/core/client.js @@ -79,10 +79,11 @@ function Client(input, output) { var self = this; - this.input = input; - this.output = output; - this.term = new term.ClientTerminal(this.output); - this.user = new user.User(); + this.input = input; + this.output = output; + this.term = new term.ClientTerminal(this.output); + this.user = new user.User(); + this.currentThemeInfo = { name : 'N/A', description : 'None' }; // // Peek at |data| and emit for any specialized handling diff --git a/core/config.js b/core/config.js index 26a7cd39..d2194d49 100644 --- a/core/config.js +++ b/core/config.js @@ -5,6 +5,8 @@ var fs = require('fs'); var paths = require('path'); var miscUtil = require('./misc_util.js'); +// :TODO: it would be nice to allow for defaults here & .json file only overrides -- e.g. merge the two + module.exports = { defaultPath : function() { var base = miscUtil.resolvePath('~/'); @@ -15,6 +17,7 @@ module.exports = { initFromFile : function(path, cb) { var data = fs.readFileSync(path, 'utf8'); + // :TODO: strip comments this.config = JSON.parse(data); }, @@ -32,7 +35,11 @@ module.exports = { usernameMax : 22, passwordMin : 6, requireActivation : true, // require SysOp activation? - defaultTheme : 'NU-MAYA', + }, + + defaults : { + theme : 'NU-MAYA', + passwordChar : '*', }, paths : { diff --git a/core/connect.js b/core/connect.js index a07fa25e..570491ec 100644 --- a/core/connect.js +++ b/core/connect.js @@ -2,7 +2,8 @@ 'use strict'; var ansi = require('./ansi_term.js'); -var artwork = require('./art.js'); +//var artwork = require('./art.js'); +var theme = require('./theme.js'); var moduleUtil = require('./module_util.js'); var Log = require('./logger.js').log; var Config = require('./config.js').config; @@ -74,7 +75,23 @@ function connectEntry(client) { setTimeout(function onTimeout() { term.write(ansi.clearScreen()); - artwork.getArt('CONNECT', { random : true, readSauce : true }, function onArt(err, art) { + + var dispOptions = { + name : 'CONNECT', + client : client, + }; + + // :TODO: if connect.js were a MenuModule, MCI/etc. would function here! + // ... could also avoid some of the boilerplate code + theme.displayThemeArt(dispOptions, function artDisplayed(err) { + var timeout = err ? 0 : 2000; + + setTimeout(function timeout() { + client.gotoMenuModule( { name : Config.entryMod } ); + }, timeout); + }); + + /*artwork.getArt('CONNECT', { random : true, readSauce : true }, function onArt(err, art) { var timeout = 0; if(!err) { @@ -88,6 +105,7 @@ function connectEntry(client) { client.gotoMenuModule({ name : Config.entryMod } ); }, timeout); }); +*/ }, 500); } diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index cdb77e28..914d8059 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -100,20 +100,9 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { setOption(1, 'textStyle'); if(options.textStyle === 'P') { - // Supply from theme, if available - // :TODO: Move this logic elsewhere - if(this.client.currentThemeInfo && this.client.currentThemeInfo.config) { - var themePwChar = this.client.currentThemeInfo.config.passwordChar; - if(_.isString(themePwChar)) { - options.textMaskChar = themePwChar.substr(0, 1); - } else if(_.isNumber(themePwChar)) { - options.textMaskChar = String.fromCharCode(themePwChar); - } else { - options.textMaskChar = '*'; - } - } else { - options.textMaskChar = '*'; - } + // Assign the proper password character / text mask + assert(_.isObject(this.client.currentThemeInfo)); + options.textMaskChar = this.client.currentThemeInfo.getPasswordChar(); } setFocusOption(0, 'focusTextStyle'); diff --git a/core/menu_module.js b/core/menu_module.js index 8450a767..cd7b16d7 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -27,8 +27,14 @@ function MenuModule(options) { self.beforeArt(); callback(null); }, - function displayArt(callback) { - theme.displayThemeArt(self.menuConfig.art, self.client, function onArt(err, mciMap) { + function displayArt(callback) { + var dispOptions = { + name : self.menuConfig.art, + font : self.menuConfig.font, + client : self.client, + }; + + theme.displayThemeArt(dispOptions, function onArt(err, mciMap) { // :TODO: If the art simply is not found, or failed to load... we need to continue if(err) { console.log('TODO: log this error properly... maybe handle slightly diff.'); diff --git a/core/text_view.js b/core/text_view.js index 1e393237..2b37a810 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -80,7 +80,7 @@ TextView.prototype.setFocus = function(focused) { this.client.term.write(this.getANSIColor(this.getFocusColor())); }; -TextView.prototype.getViewData = function() { +TextView.prototype.getData = function() { return this.text; }; diff --git a/core/theme.js b/core/theme.js index e4e6955e..5b5f128d 100644 --- a/core/theme.js +++ b/core/theme.js @@ -10,6 +10,7 @@ var fs = require('fs'); var paths = require('path'); var async = require('async'); var _ = require('lodash'); +var assert = require('assert'); exports.getThemeInfo = getThemeInfo; exports.getThemeArt = getThemeArt; @@ -27,6 +28,22 @@ function getThemeInfo(themeID, cb) { try { // :TODO: strip comments/etc. ala menu.json var info = JSON.parse(data); + + // + // Create some handy helpers + // + info.getPasswordChar = function() { + var pwChar = Config.defaults.passwordChar; + if(_.isObject(info.config)) { + if(_.isString(info.config.passwordChar)) { + pwChar = info.config.passwordChar.substr(0, 1); + } else if(_.isNumber(info.config.passwordChar)) { + pwChar = String.fromCharCode(info.config.passwordChar); + } + } + return pwChar; + }; + cb(null, info); } catch(e) { cb(err); @@ -116,8 +133,12 @@ function getThemeArt(name, themeID, options, cb) { }); } -function displayThemeArt(name, client, cb) { - getThemeArt(name, client.user.properties.art_theme_id, function onArt(err, artInfo) { +function displayThemeArt(options, cb) { + assert(_.isObject(options)); + assert(_.isObject(options.client)); + assert(_.isString(options.name)); + + getThemeArt(options.name, options.client.user.properties.art_theme_id, function onArt(err, artInfo) { if(err) { cb(err); } else { @@ -128,7 +149,16 @@ function displayThemeArt(name, client, cb) { } } - art.display( { art : artInfo.data, client : client, iceColors : iceColors }, function onDisplayed(err, mci) { + var dispOptions = { + art : artInfo.data, + sauce : artInfo.sauce, + client : options.client, + iceColors : iceColors, + font : options.font, + }; + + + art.display(dispOptions, function onDisplayed(err, mci) { cb(err, mci, artInfo); }); } diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 859ce5e1..3e67fe99 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -112,7 +112,7 @@ VerticalMenuView.prototype.onSpecialKeyPress = function(keyName) { VerticalMenuView.super_.prototype.onSpecialKeyPress.call(this, keyName); }; -VerticalMenuView.prototype.getViewData = function() { +VerticalMenuView.prototype.getData = function() { return this.focusedItemIndex; }; diff --git a/core/view.js b/core/view.js index d4d5c7bc..aad3e320 100644 --- a/core/view.js +++ b/core/view.js @@ -187,5 +187,5 @@ View.prototype.onSpecialKeyPress = function(keyName) { } }; -View.prototype.getViewData = function() { +View.prototype.getData = function() { }; \ No newline at end of file diff --git a/core/view_controller.js b/core/view_controller.js index d4671bf8..6d0db7ef 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -86,7 +86,7 @@ function ViewController(options) { var viewData; for(var id in self.views) { try { - viewData = self.views[id].getViewData(); + viewData = self.views[id].getData(); if(typeof viewData !== 'undefined') { formData.value[id] = viewData; } @@ -315,7 +315,7 @@ ViewController.prototype.loadFromMCIMapAndConfig = function(options, cb) { // if(_.isObject(formConfig.submit)) { // :TODO: If this model is kept, formData does not need to include actual data, just form ID & submitID - // we can get the rest here via each view in form -> getViewData() + // we can get the rest here via each view in form -> getData() self.on('submit', function onSubmit(formData) { Log.debug( { formData : formData }, 'Submit form'); @@ -419,7 +419,7 @@ ViewController.prototype.formatMCIString = function(format) { return match; } - return view.getViewData(); + return view.getData(); }); }; diff --git a/mods/apply.js b/mods/apply.js index f6d27d33..676c4690 100644 --- a/mods/apply.js +++ b/mods/apply.js @@ -56,7 +56,7 @@ function ApplyModule(menuConfig) { affiliation : args.affils, email_address : args.email, web_address : args.web, - art_theme_id : Config.users.defaultTheme, // :TODO: allow '*' = random + art_theme_id : Config.defaults.theme, // :TODO: allow '*' = random account_status : user.User.AccountStatus.inactive, // :TODO: Other defaults diff --git a/mods/menu.json b/mods/menu.json index 37b5f3a5..00692c14 100644 --- a/mods/menu.json +++ b/mods/menu.json @@ -167,6 +167,7 @@ "newUserActive" : { "art" : "NEWACT", "options" : { + // :TODO: implement MCI codes for this "clearScreen" : true }, "form" : {