diff --git a/ctrl/modopts.d/xtrnmenu.ini b/ctrl/modopts.d/xtrnmenu.ini
new file mode 100644
index 0000000000000000000000000000000000000000..561fb774ea100c6e3d16bae9da860d275e0805f4
--- /dev/null
+++ b/ctrl/modopts.d/xtrnmenu.ini
@@ -0,0 +1,137 @@
+; Options for Xtrn Menu Mod
+; http://wiki.synchro.net/module:xtrnmenu
+
+; Refer to the wiki for instructions on how to customize individual menus
+
+[xtrnmenu]
+
+; Uncomment if you wish to have the mod hand over section menus to xtrn_sec.js
+;use_xtrn_sec = true
+
+; Default sort, can be "name", "key", or false (no sort)
+sort = false
+
+; Enable multi-column display (when more than 10 external programs in a section)
+multicolumn = true
+
+; Clear the (remote) terminal screen before displaying the menu
+clear_screen = true    
+ 
+; uncomment and modify any of the below to change the look of the external programs menu
+; to remove titles or underline, set to the text ' '
+;multicolumn_separator: " "
+
+; set the singlecolumn margin to the number of lines of your footer, if you have one
+;singlecolumn_margin=2
+
+;header_fmt: \1n\1c\1h%s \1n\1cExternal Programs:\r\n\r\n
+;titles: \1n\1cKey \1h\xb3\1n\1c Name                             \1n\1c
+multicolumn_fmt: \1h\1c%3s \xb3 \1n\1c%-32.32s \1h
+singlecolumn_fmt: \1h\1c%3s \xb3 \1n\1c%s \1h
+;which: \r\n\1-\1cWhich or \1h~Q\1n\1cuit: \1h
+;underline: \1c\1h\xc4\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4\xc4
+; used by the lightbar
+multicolumn_fmt_inverse: \1n\1w[\1y\1h%2s\1n\1w] \1n\1w\1h\x015%-32.32s \1n\1h
+singlecolumn_fmt_inverse: \1n\1w[\1y\1h%2s\1n\1w] \1n\1w\1h\x015%s \1n\1h
+
+; to enable cost display, use these instead
+; Note: cost not supported in special menus
+;titles: \1n\1cInput \1h\xb3\1n\1c Name                       Cost \1n\1c
+;multicolumn_fmt: \1h\1c%3s \xb3 \1n\1c%-26.26s\1h %5u
+;singlecolumn_fmt: \1h\1c%3s \xb3 \1n\1c%-26.26s\1h %5u
+;multicolumn_fmt_inverse: \1h\1c%3s \xb3 \1n\x016\1w\1h%-26.26s\1n \1c%5u
+;singlecolumn_fmt_inverse: \1h\1c%3s \xb3 \1n\x016\1w\1h%-26.26s\1n \1c%5u
+
+; These are used for the special menus (most launched, etc.)
+; It adds the "stats" field which is the number of launches, amount of time, etc.
+multicolumn_fmt_special: \1h\1c%3s \xb3 \1m%-21.21s\1h \1m%-10s
+singlecolumn_fmt_special: \1h\1c%3s \xb3 \1m%-21.21s\1h \1m%-10s
+multicolumn_fmt_special: \1h\1c%3s \xb3 \1m%-21.21s\1h \1m%-10s
+singlecolumn_fmt_special: \1h\1c%3s \xb3 \1m%-21.21s\1h \1m%-10s
+
+; Override the message when there are no programs available
+; (defaults to text.dat NoXtrnPrograms)
+;no_programs_msg:
+
+; Override the restricted user msg
+; (defaults to text.dat R_ExternalPrograms)
+;restricted_user_msg:
+
+; JSON service powers special menus (most popular, recent, etc.)
+; Add appropriate entry to json-service.ini and set host/port here
+;json_enabled = true
+;json_host = localhost
+;json_port = 10088
+
+; Exclude external codes or external sections from being tracked
+; and showing up in special menus (most popular, recent, etc.)    
+blacklist_tracking_xtrncodes: bullshit,sbbslist,avatchoo
+blacklist_tracking_xtrnsec: operator
+
+; Formatting for the "return to previous menu" option
+return_msg: "\xae Return to Previous Menu"
+logoff_msg: "Logoff"
+quit_msg: "Quit"
+; Formatting for the "return to previous menu" option
+return_msg: "\xae Return to Previous Menu"
+logoff_msg: "Logoff"
+quit_msg: "Quit"
+return_multicolumn_fmt: \1h\1c%3s \xb3 \1n\1c%-32.32s \1h
+return_singlecolumn_fmt: \1h\1c%3s \xb3 \1n\1c%s \1h
+return_multicolumn_fmt_inverse: \1h\1c%3s \xb3 \1n\x016\1w\1h%-32.32s \1n\1h
+return_singlecolumn_fmt_inverse: \1h\1c%3s \xb3 \1n\x016\1w\1h%s \1n\1h
+return_multicolumn_special_fmt: \1h\1c%3s \xb3 \1m%-32.32s\1h
+return_singlecolumn_special_fmt: \1h\1c%3s \xb3 \1m%s\1h
+return_multicolumn_special_fmt_inverse: \1h\1c%3s \xb3 \x015\1w\1h%-32.32s\1n
+return_singlecolumn_special_fmt_inverse: \1h\1c%3s \xb3 \x015\1w\1h%s\1n
+
+; Change display of filearea command menu
+;xfer_prompt: "\x01n\x01c\xfe \x01b\x01hFile \x01n\x01c\xfe \x01h"
+;xfer_prompt2: " \x01n\x01c@DIR@: \x01n"
+;command_prompt: "\r\n\x01gCommand: "
+;view_prompt: "\r\n\x01c\x01hView File(s)\r\n"
+;searchfname_prompt: "\r\n\x01c\x01hSearch for Filename(s)\r\n"
+;remove_prompt: "\r\n\x01c\x01hRemove/Edit File(s)\r\n"
+;finddesc_prompt: "\r\n\x01c\x01hFind Text in File Descriptions (no wildcards)\r\n"
+;download:prompt:  "\r\n\x01c\x01hDownload File(s)\r\n"
+
+; Change display of search menu
+entersearchterm: "\x01y\x01hEnter search term: "
+searchresultheader: "\x01n\x01cSearch Results for \x01h%s"
+searchagainmsg: "\x01n\x01mPress S to Search Again."
+
+; Feedback Module
+;feedback_subject: "Game Server Feedback\r\n"
+;feedback_msg: Thank you for your Feedback, @SYSOP@ will get back to you ASAP!\r\n\r\n
+
+; Favorites Module
+;favorite_add_item: "Add Item"
+;favorte_remove_item: "Remove Item"
+;add_favorites_msg: "\x01c\x01hAdd Favorite"
+;remove_favorites_msg: "\x01c\x01hRemove Favorite"
+;favorites_inst: "\x01n\x01w\x01h\x012 [Up/Down/Home/End] to Navigate, [Enter] to Select, [Q] to Quit, [S] to Search "
+;favorites_inst_rem: "\x01n\x01w\x01h\x012 [Up/Down/Home/End] to Navigate, [Enter] to Select, [Q] to Quit "
+;favorite_add_search_prompt: "\x01c\x01hSearch (ESC to Cancel): \x01n"
+
+; autocomplete (search) box on add favorite
+; see cga_defs.js for color codes
+; autocomplete input box
+;favorite_add_search_fg=LIGHTGRAY
+;favorite_add_search_bg=BG_BLACK
+; autocomplete result box
+;favorite_add_search_sfg=LIGHTGRAY
+;favorite_add_search_sbg=BG_BLACK
+; autocomplete highlighted row
+;favorite_add_search_hsfg=WHITE
+;favorite_add_search_hsbg=BG_MAGENTA
+
+; scrolling menu on add favorite
+; lightbar non-current item
+;favorite_add_fg=LIGHTGRAY
+;favorite_add_bg=BG_BLACK
+; lightbar current item
+;favorite_add_lfg=WHITE
+;favorite_add_lbg=BG_MAGENTA
+; tree heading 
+;favorite_add_cfg=WHITE
+;favorite_add_cbg=BG_BLACK
\ No newline at end of file
diff --git a/ctrl/modopts.ini b/ctrl/modopts.ini
index e70b2a5f693eee9d2428faa6438a97750bedc628..efa0d44398c07b3c5d6f26d413ab4a3baec13836 100644
--- a/ctrl/modopts.ini
+++ b/ctrl/modopts.ini
@@ -177,3 +177,5 @@
     forum_extended_ascii=false
     max_messages=0
     nodelist_ibbs = true
+    darkmode_off=false
+
diff --git a/exec/binkit.js b/exec/binkit.js
index cc45a027327e99a2bcea83e0eb9470cb982debe4..e12c365985b9737752b9ffdabcd1168a587dc394 100644
--- a/exec/binkit.js
+++ b/exec/binkit.js
@@ -26,6 +26,7 @@ var version_notice = "BinkIT/" + REVISION;
 var semaphores = [];
 // data/binkstats.ini
 var stats = { inbound: { true: {}, false: {} }, callout: { true: {}, false: {} }, totals: {} };
+var cfgfname;
 
 function update_stats(stats, addr, bp, host)
 {
@@ -609,7 +610,7 @@ function callout(addr, scfg, locks, bicfg)
 
 	log(LOG_INFO, format("%s callout to %s started", bp.revision, addr));
 	if (bicfg === undefined)
-		bicfg = new BinkITCfg();
+		bicfg = new BinkITCfg(cfgfname);
 	bp.system_operator = bicfg.sysop;
 	bp.plain_auth_only = bicfg.plain_auth_only;
 	bp.crypt_support = bicfg.crypt_support;
@@ -845,8 +846,8 @@ function run_outbound(ran)
 	var scfg_ob;
 
 	log(LOG_DEBUG, "Running outbound");
-	scfg = new SBBSEchoCfg();
-	bicfg = new BinkITCfg();
+	scfg = new SBBSEchoCfg(cfgfname);
+	bicfg = new BinkITCfg(cfgfname);
 
 	if (!scfg.is_flo) {
 		log(LOG_ERROR, "sbbsecho not configured for FLO-style mailers.");
@@ -1011,8 +1012,8 @@ function run_inbound(sock)
 
 	log(LOG_INFO, bp.revision + " inbound connection from " +sock.remote_ip_address+":"+sock.remote_port);
 	bp.cb_data = {
-		binkitcfg:new BinkITCfg(),
-		binkit_scfg:new SBBSEchoCfg(),
+		binkitcfg:new BinkITCfg(cfgfname),
+		binkit_scfg:new SBBSEchoCfg(cfgfname),
 		binkit_file_actions:{},
 		binkit_flow_contents:{},
 		binkit_locks:locks
@@ -1057,7 +1058,7 @@ function poll_node(addr_str, scfg, bicfg, myaddr)
 	var locks = [];
 
 	if (scfg === undefined)
-		scfg = new SBBSEchoCfg();
+		scfg = new SBBSEchoCfg(cfgfname);
 
 	if (myaddr === undefined)
 		myaddr = FIDO.parse_addr(system.fido_addr_list[0], 1, 'fidonet');
@@ -1092,8 +1093,8 @@ function run_polls(ran)
 	var locks = [];
 
 	log(LOG_DEBUG, "Running polls");
-	scfg = new SBBSEchoCfg();
-	bicfg = new BinkITCfg();
+	scfg = new SBBSEchoCfg(cfgfname);
+	bicfg = new BinkITCfg(cfgfname);
 	myaddr = FIDO.parse_addr(system.fido_addr_list[0], 1, 'fidonet');
 
 	Object.keys(bicfg.node).forEach(function(addr_str) {
@@ -1194,6 +1195,11 @@ if (system.fido_addr_list.length < 1) {
 	exit(1);
 }
 
+for (i = 0; i < argv.length; i++) {
+	if(file_getext(argv[i]) == ".ini")
+		cfgfname = argv[i];
+}
+
 // If we're running as a service, call run_inbound().
 if (sock !== undefined && sock.descriptor !== -1)
 	run_inbound(sock);
diff --git a/exec/load/typeahead.js b/exec/load/typeahead.js
index f177513024f568956b51c1bd6dd837250ccf8a88..c2af7e9f8e4e7feb7bdae50b207dad58f4d008b0 100644
--- a/exec/load/typeahead.js
+++ b/exec/load/typeahead.js
@@ -216,37 +216,37 @@ load('tree.js');
 var Typeahead = function (options) {
 
     var properties = {
-        'x' : 0,
-        'y' : 0,
-        'len' : console.screen_columns,
-        'height' : 0,
-        'prompt' : '',
-        'fg' : LIGHTGRAY,
-        'bg' : BG_BLUE,
-        'sfg' : LIGHTGRAY,
-        'sbg' : BG_BLUE,
-        'hsfg' : WHITE,
-        'hsbg' : BG_CYAN,
-        'cursor' : ascii(219),
-        'position' : 0,
-        'text' : '',
-        'datasources' : [],
-        'delay' : 1,
-        'minLength' : 1,
-        'lastKey' : system.timer,
-        'suggested' : false,
-        'attr' : console.attributes,
-        'focus' : true,
-        'maxResults': 0,
+        x: 0,
+        y: 0,
+        len: console.screen_columns,
+        height: 0,
+        prompt: '',
+        fg: LIGHTGRAY,
+        bg: BG_BLUE,
+        sfg: LIGHTGRAY,
+        sbg: BG_BLUE,
+        hsfg: WHITE,
+        hsbg: BG_CYAN,
+        cursor: ascii(219),
+        position: 0,
+        text: '',
+        datasources: [],
+        delay: 1,
+        minLength: 1,
+        lastKey: system.timer,
+        suggested: false,
+        attr: console.attributes,
+        focus: true,
+        maxResults: 0,
     };
 
     var display = {
-        'parentFrame' : undefined,
-        'frame' : undefined,
-        'inputFrame' : undefined,
-        'cursor' : undefined,
-        'treeFrame' : undefined,
-        'tree' : undefined
+        parentFrame: undefined,
+        frame: undefined,
+        inputFrame: undefined,
+        cursor: undefined,
+        treeFrame: undefined,
+        tree: undefined
     };
 
     function initSettings() {
@@ -355,27 +355,26 @@ var Typeahead = function (options) {
             display.treeFrame.invalidate();
         }
 
-        if (suggestions.length < 1) {
-            display.tree = undefined;
-            return;
-        }
-
         display.tree = new Tree(display.treeFrame);
         display.tree.colors.fg = properties.sfg;
         display.tree.colors.bg = properties.sbg;
         display.tree.colors.lfg = properties.hsfg;
         display.tree.colors.lbg = properties.hsbg;
 
-        display.tree.addItem('');
-        for (var n = 0; n < (properties.maxResults || suggestions.length); n++) {
-            if (typeof suggestions[n] === 'object' && typeof suggestions[n].text === 'string') {
-                var item = display.tree.addItem(suggestions[n].text);
-                item.suggestion = suggestions[n];
-            } else if (typeof suggestions[n] === 'string') {
-                display.tree.addItem(suggestions[n]);
+        if (suggestions.length < 1) {
+            display.tree.addItem('No results found');
+        } else {
+            display.tree.addItem('');
+            for (var n = 0; n < (properties.maxResults || suggestions.length); n++) {
+                if (typeof suggestions[n] === 'object' && typeof suggestions[n].text === 'string') {
+                    var item = display.tree.addItem(suggestions[n].text);
+                    item.suggestion = suggestions[n];
+                } else if (typeof suggestions[n] === 'string') {
+                    display.tree.addItem(suggestions[n]);
+                }
             }
         }
-
+        
         display.tree.open();
 
         properties.suggested = true;
@@ -412,7 +411,8 @@ var Typeahead = function (options) {
                 break;
             case KEY_UP:
             case KEY_DOWN:
-                if (typeof display.tree !== 'undefined') {
+                //if (typeof display.tree !== 'undefined') {
+                if (display.tree.items.length > 1) {
                     display.tree.getcmd(key);
                 }
                 break;
@@ -429,6 +429,7 @@ var Typeahead = function (options) {
                 properties.position = (properties.position >= properties.text.length) ? properties.text.length : properties.position + 1;
                 break;
             case '\b':
+            case '\x08':
                 if (properties.position === 0) break;
                 properties.text = properties.text.split('');
                 properties.text.splice((properties.position - 1), 1);
diff --git a/exec/load/xtrnmenulib.js b/exec/load/xtrnmenulib.js
index f9ba1d62e4dfbb59633d7f56c3dc26904934d484..71e6e790c4bf2278e337328dbdbddd6611a1341d 100644
--- a/exec/load/xtrnmenulib.js
+++ b/exec/load/xtrnmenulib.js
@@ -20,7 +20,7 @@ function ExternalMenus() {
 	this.options = {};
 	this.xtrn_custommenu_options = {};
 	this.menuconfig = {};
-
+	
 	this.getOptions();
 	this.getMenuConfig();
 }
@@ -33,7 +33,7 @@ ExternalMenus.prototype.getMenuConfig = function() {
 		config_src = config_file.read();
 		config_file.close();
 	}
-
+	
 	if (typeof config_src !== "undefined") {
 		this.menuconfig = JSON.parse(config_src.toString());
 	}
@@ -43,67 +43,73 @@ ExternalMenus.prototype.getOptions = function(menutype, menuid) {
 	if (typeof menutype === "undefined") {
 		menutype = 'custommenu';
 	}
-
+	
 	// Get xtrn_sec options from modopts.ini [xtrn_sec]
 	if ((this.options = load({}, "modopts.js", "xtrn_sec")) == null) {
 		this.options = { multicolumn: true, sort: false };
 	}
-
+	
 	// Get xtrn_custommenu options from modopts.ini [xtrn_custommenu]
 	if ((this.xtrn_custommenu_options = load({}, "modopts.js", "xtrnmenu")) == null) {
 		this.xtrn_custommenu_options = { };
 	}
-
+	
 	// in all cases, we start with the xtrn_sec options as the base and set the defaults
 	if (this.options.multicolumn === undefined)
 		this.options.multicolumn = true;
-
+	
 	if (this.options.multicolumn_separator === undefined)
 		this.options.multicolumn_separator = " ";
-
+	
 	if (this.options.multicolumn_fmt === undefined)
 		this.options.multicolumn_fmt = system.text(XtrnProgLstFmt);
-
+	
 	if (this.options.singlecolumn_fmt === undefined)
 		this.options.singlecolumn_fmt = "\x01h\x01c%3u \xb3 \x01n\x01c%s\x01h ";
-
+	
+	if (this.options.multicolumn_fmt_inverse === undefined)
+		this.options.multicolumn_fmt_inverse = system.text(XtrnProgLstFmt);
+	
+	if (this.options.singlecolumn_fmt_inverse === undefined)
+		this.options.singlecolumn_fmt_inverse = "\x01h\x01c%3u \xb3 \x01n\x01c%s\x01h ";
+	
 	if (this.options.singlecolumn_margin == undefined)
 		this.options.singlecolumn_margin = 7;
-
+	
 	if (this.options.restricted_user_msg === undefined)
 		this.options.restricted_user_msg = system.text(R_ExternalPrograms);
-
+	
 	if (this.options.no_programs_msg === undefined)
 		this.options.no_programs_msg =  system.text(NoXtrnPrograms);
-
+	
 	if (this.options.header_fmt === undefined)
 		this.options.header_fmt = system.text(XtrnProgLstHdr);
-
+	
 	if (this.options.titles === undefined)
 		this.options.titles = system.text(XtrnProgLstTitles);
-
+	
 	if (this.options.underline === undefined)
 		this.options.underline = system.text(XtrnProgLstUnderline);
-
+	
 	if (this.options.which === undefined)
 		this.options.which = system.text(WhichXtrnProg);
-
+	
 	if (this.options.clear_screen === undefined)
 		this.options.clear_screen = true;
-
+	
 	if (this.options.section_fmt === undefined)
 		this.options.section_fmt = "\x01y\x01g%3d:\x01n\x01g %s"
-
+	
 	if (this.options.section_header_fmt === undefined)
 		this.options.section_header_fmt = "\x01-\x01gSelect \x01hExternal Program Section\x01-\x01g:"
-
+	
 	if (this.options.section_which === undefined)
 		this.options.section_which = "\r\n\x01-\x01gWhich, \x01w\x01h~Q\x01n\x01guit or [1]: \x01h"
-
+	
 	if (typeof (this.options.use_xtrn_sec) == "undefined") {
 		this.options.use_xtrn_sec = false;
 	}
-
+	
 	// if its a custom menu, then override with custommenu_options
 	if (menutype == 'custommenu') {
 		if (typeof this.xtrn_custommenu_options.multicolumn_fmt !== "undefined") {
@@ -112,78 +118,100 @@ ExternalMenus.prototype.getOptions = function(menutype, menuid) {
 			// cannot default to xtrn_sec multicolumn_fmt due to use of %u instead of %s
 			this.options.multicolumn_fmt = "\x01h\x01c%3s \xb3 \x01n\x01c%-32.32s\x01h ";
 		}
-
+		
 		if (typeof this.xtrn_custommenu_options.singlecolumn_fmt !== "undefined") {
 			this.options.singlecolumn_fmt = this.xtrn_custommenu_options.singlecolumn_fmt;
 		} else {
 			// cannot default to xtrn_sec multicolumn_fmt due to use of %u instead of %s
 			this.options.singlecolumn_fmt = "\x01h\x01c%3s \xb3 \x01n\x01c%s\x01h ";
 		}
-
+		
+		if (typeof this.xtrn_custommenu_options.multicolumn_fmt_inverse !== "undefined") {
+			this.options.multicolumn_fmt_inverse = this.xtrn_custommenu_options.multicolumn_fmt_inverse;
+		} else {
+			// cannot default to xtrn_sec multicolumn_fmt due to use of %u instead of %s
+			this.options.multicolumn_fmt_inverse = "\x01h\x01c%3s \xb3 \x01n\x016\x01w\x01h%-32.32s \x01n\x01h";
+		}
+		
+		if (typeof this.xtrn_custommenu_options.singlecolumn_fmt_inverse !== "undefined") {
+			this.options.singlecolumn_fmt_inverse = this.xtrn_custommenu_options.singlecolumn_fmt_inverse;
+		} else {
+			// cannot default to xtrn_sec multicolumn_fmt due to use of %u instead of %s
+			this.options.singlecolumn_fmt_inverse = "\x01h\x01c%3s \xb3 \x01n\x016\x01w\x01h%s \x01n\x01h";
+		}
+		
 		this.options.header_fmt = (typeof this.xtrn_custommenu_options.header_fmt !== "undefined")
 			? this.xtrn_custommenu_options.header_fmt : this.options.header_fmt;
-
+		
 		this.options.titles = (typeof this.xtrn_custommenu_options.titles !== "undefined")
 			? this.xtrn_custommenu_options.titles : this.options.titles;
-
+		
 		this.options.which = (typeof this.xtrn_custommenu_options.which !== "undefined")
 			? this.xtrn_custommenu_options.which : this.options.which;
-
+		
 		this.options.underline = (typeof this.xtrn_custommenu_options.underline !== "undefined")
 			? this.xtrn_custommenu_options.underline : this.options.underline;
-
+		
 		this.options.multicolumn_separator = (typeof this.xtrn_custommenu_options.multicolumn_separator !== "undefined")
 			? this.xtrn_custommenu_options.multicolumn_separator : this.options.multicolumn_separator;
-
+		
 		this.options.multicolumn = (typeof this.xtrn_custommenu_options.multicolumn !== "undefined")
 			? this.xtrn_custommenu_options.multicolumn : this.options.multicolumn;
-
+		
 		this.options.sort = (typeof this.xtrn_custommenu_options.sort !== "undefined")
 			? this.xtrn_custommenu_options.sort : this.options.sort;
-
+		
 		this.options.clear_screen = (typeof this.xtrn_custommenu_options.clear_screen !== "undefined")
 			? this.xtrn_custommenu_options.clear_screen : this.options.clear_screen;
-
+		
 		this.options.singlecolumn_margin = (typeof this.xtrn_custommenu_options.singlecolumn_margin !== "undefined")
 			? this.xtrn_custommenu_options.singlecolumn_margin : this.options.singlecolumn_margin;
-
+		
 		if (typeof bbs !== "undefined") {
-			this.options.singlecolumn_height = (typeof this.xtrn_custommenu_options.singlecolumn_height !== "undefined")
-				? this.xtrn_custommenu_options.singlecolumn_height : this.options.singlecolumn_height;
-
-			if (this.options.singlecolumn_height == undefined)
-				this.options.singlecolumn_height = console.screen_rows - this.options.singlecolumn_margin;
-
 			// override and turn off multicolumn if terminal width is less than 80
 			if (console.screen_columns < 80)
 				options.multicolumn = false;
 		}
-
+		
 		// no need to override restricted_user_msg or no_programs_msg - these
 		// will be the same for both types of menus
-
+		
 		// Allow overriding on a per-menu basis
 		var menuoptions = load({}, "modopts.js", "xtrnmenu:" + menuid);
-		if ((typeof menuid !== "undefined") && (menuoptions != null)) {
+		if ((typeof menuid !== "undefined") && (menuoptions !== null)) {
 			for (var m in menuoptions) {
 				this.options[m] = menuoptions[m];
 			}
 		}
 	}
-
+	
+	this.options.json_enabled = (typeof this.xtrn_custommenu_options.json_enabled !== undefined)
+		? this.xtrn_custommenu_options.json_enabled : false;
+	this.options.json_host = (typeof this.xtrn_custommenu_options.json_host !== undefined)
+		? this.xtrn_custommenu_options.json_host : "localhost";
+	this.options.json_port = (typeof this.xtrn_custommenu_options.json_port !== undefined)
+		? this.xtrn_custommenu_options.json_port : 10088;
+	
+	this.options.blacklist_tracking_xtrncodes = (typeof this.xtrn_custommenu_options.blacklist_tracking_xtrncodes !== undefined)
+		? this.xtrn_custommenu_options.blacklist_tracking_xtrncodes : "";
+	this.options.blacklist_tracking_xtrnsec = (typeof this.xtrn_custommenu_options.blacklist_tracking_xtrnsec !== undefined)
+		? this.xtrn_custommenu_options.blacklist_tracking_xtrnsec : "";
+	
 	// these options only apply to terminals/consoles
-
+	
 	// the intention is to obtain all the mod_opts options for xtrn_sec, and
 	// override if a custom menu global setting is set
-
+	
 	//// The following are used for the enhanced custom menu functionality
 	if (this.options.custom_menu_not_found_msg === undefined) {
 		this.options.custom_menu_not_found_msg = "Menu %MENUID% not found";
 	}
-
+	
 	if (this.options.custom_menu_program_not_found_msg === undefined) {
 		this.options.custom_menu_program_not_found_msg = "Program %PROGRAMID% not found";
 	}
+	
+	this.options.custom = this.xtrn_custommenu_options;
 
 	return this.options;
 }
@@ -196,9 +224,9 @@ ExternalMenus.prototype.getMenu = function(menuid) {
 	} else {
 		menuid = menuid.toLowerCase();
 	}
-
+	
 	var menu;
-
+	
 	if ((typeof this.menuconfig !== "undefined") && (typeof this.menuconfig.menus !== "undefined")) {
 		this.menuconfig.menus.some(function (indmenu) {
 			if (indmenu.id.toLowerCase() == menuid) {
@@ -206,30 +234,30 @@ ExternalMenus.prototype.getMenu = function(menuid) {
 			}
 		});
 	}
-
+	
 	if (!menu && (menuid == "main")) {
 		// no custom menus defined, make one to mimic old behavior
-
+		
 		var menuitems = [];
-
+		
 		var i;
 		xtrn_area.sec_list.forEach(function (sec) {
 			if (sec.can_access) {
 				menuitems.push({
-					"input": i,
-					"target": sec.code,
-					"type": "xtrnmenu",
-					"title": sec.name,
-					"access_string": sec.ars
+					input: i,
+					target: sec.code,
+					type: "xtrnmenu",
+					title: sec.name,
+					access_string: sec.ars
 				});
 				i++;
 			}
 		});
-
+		
 		menu = {
-			"id": "main",
-			"title": "Main Menu",
-			"items": menuitems
+			id: "main",
+			title: "Main Menu",
+			items: menuitems
 		};
 	}
 	return menu;
@@ -237,61 +265,62 @@ ExternalMenus.prototype.getMenu = function(menuid) {
 
 // return a section menu object (stock synchronet external door section)
 ExternalMenus.prototype.getSectionMenu = function(menuid) {
-
+	
 	// grab the specified menu, or get the main menu
 	if ((typeof menuid === "undefined") || !menuid) {
 		return false;
 	}
-
+	
 	var menuitems = [];
-	var menu, title;
-
+	var title;
+	
+	var menu = {
+		id: menuid,
+		title: null,
+		items: []
+	}
+	
 	xtrn_area.sec_list.some(function (sec) {
-
 		if (sec.code.toLowerCase() == menuid.toLowerCase()) {
-			title = sec.name;
-
+			menu.title = sec.name;
+			
 			if (!sec.can_access || sec.prog_list.length < 1) {
 				return false;
 			}
-
+			
 			var i = 1;
 			sec.prog_list.some(function (prog) {
 				menuitems.push({
-					'input' : i,
-					'target': prog.code,
-					'title': prog.name,
-					'type': 'xtrnprog',
-					'access_string': prog.ars,
-					'cost': prog.cost
+					input : i,
+					target: prog.code,
+					title: prog.name,
+					type: 'xtrnprog',
+					access_string: prog.ars,
+					cost: prog.cost
 				});
 				i++;
 			});
-
+			
 			if (menuitems.length > 0) {
-				menu = {
-					'id': menuid,
-					'title': title,
-					'items': menuitems,
-				};
+				menu.items = menuitems;
 			}
 			return;
 		}
 	});
-
+	
 	return menu;
 }
 
 // Sort the menu items according to options
 ExternalMenus.prototype.getSortedItems = function(menuobj) {
 	var sort_type;
-
+	
 	if ((typeof menuobj.sort_type !== "undefined") && menuobj.sort_type) {
 		sort_type = menuobj.sort_type; // "name" or "key"
 	} else {
 		sort_type = this.options.sort; // bool
 	}
-
+	
 	// first, build a new menu with only options they have access to
 	var menuitemsfiltered = [];
 	for (i in menuobj.items) {
@@ -305,7 +334,7 @@ ExternalMenus.prototype.getSortedItems = function(menuobj) {
 					}
 				}
 				break;
-
+			
 			case 'xtrnprog':
 				for (j in xtrn_area.sec_list) {
 					for (var k in xtrn_area.sec_list[j].prog_list) {
@@ -317,7 +346,7 @@ ExternalMenus.prototype.getSortedItems = function(menuobj) {
 					}
 				}
 				break;
-
+			
 			case 'custommenu':
 			default:
 				if ((typeof menuobj.items[i].access_string === "undefined") || !menuobj.items[i].access_string) {
@@ -332,7 +361,7 @@ ExternalMenus.prototype.getSortedItems = function(menuobj) {
 				break;
 		}
 	}
-
+	
 	// if no custom input keys are specified for an input, then assign a numeric input
 	// this would mimic the built-in external section menu functionality
 	// but this is only assigned on sort_type key because if the sort type is for
@@ -347,7 +376,7 @@ ExternalMenus.prototype.getSortedItems = function(menuobj) {
 		}
 	}
 	sortind++;
-
+	
 	if (sort_type == "key") {
 		for (i in menuitemsfiltered) {
 			if (!menuitemsfiltered[i].input) {
@@ -356,7 +385,7 @@ ExternalMenus.prototype.getSortedItems = function(menuobj) {
 			}
 		}
 	}
-
+	
 	if (sort_type) {
 		switch (sort_type) {
 			case "key":
@@ -369,7 +398,7 @@ ExternalMenus.prototype.getSortedItems = function(menuobj) {
 				break;
 		}
 	}
-
+	
 	// if this is a sort by title and the key is empty, it will be assigned the next available number
 	// this is to support auto-generated inputs like is done on the built-in section menus
 	// however, to keep the numeric keys in order, the numbers could not be pre-assigned like it done
@@ -380,7 +409,7 @@ ExternalMenus.prototype.getSortedItems = function(menuobj) {
 			sortind++;
 		}
 	}
-
+	
 	return menuitemsfiltered;
 }
 
@@ -412,3 +441,214 @@ ExternalMenus.prototype.sort_by_input = function(a, b) {
 	}
 	return 0;
 }
+
+// Handle json update
+ExternalMenus.prototype.processUpdate = function(update) {
+	log(ERROR, "Unhandled JSON DB packet: " + JSON.stringify(update));
+}
+
+// Sort for special menus, by value numerically desc
+ExternalMenus.prototype.sort_special = function(obj) {
+	var keys = [];
+	for(var key in obj) {
+		keys.push(key);
+	}
+	return keys.sort(function(a,b) { return obj[b]-obj[a]});
+}
+
+// return menu object for special menu (most recent, etc.)
+ExternalMenus.prototype.getSpecial = function(menutype, title, itemcount) {
+	var options = this.getOptions('custommenu', menutype);
+
+	var menu;
+	var menuitems = [];
+	
+	if (itemcount === undefined) {
+		itemcount = 0;
+	}
+ 	
+	if (menutype === undefined) {
+		log(LOG_DEBUG, "xtrnmenulib: getSpecial called without menutype");
+		return false;
+	}
+	
+	if (!this.options.json_enabled) {
+		log(LOG_DEBUG, "xtrnmenulib: Skipping " + menutype + " because JSON is not enabled");
+		return false;
+	}
+
+	try {
+		require("json-client.js", "JSONClient");
+		var jsonClient = new JSONClient(this.options.json_host, this.options.json_port);
+		jsonClient.callback = this.processUpdate;
+	} catch (e) {
+		writeln("NP" + e);
+		log(LOG_ERR, "xtrnmenulib: Could not initialize JSON database so special menu is now disabled: " + e);
+		return false;
+	}
+
+	var sortedItems = [];
+	var jsonData = [];
+	switch (menutype) {
+		// game keys sorted by most recent descending
+		case 'recentall':
+			jsonData = jsonClient.read("xtrnmenu", "launchstart", 1);
+			break;
+		case 'recentuser':
+			jsonData = jsonClient.read("xtrnmenu", "launchstart_user_" + user.alias, 1);
+			break;
+		case 'mostlaunchedall':
+			jsonData = jsonClient.read("xtrnmenu", "launchnum", 1);
+			break;
+		case 'mostlauncheduser':
+			jsonData = jsonClient.read("xtrnmenu", "launchnum_user_" + user.alias, 1);
+			break;
+		case 'longestrunall':
+			jsonData = jsonClient.read("xtrnmenu", "timeran", 1);
+			break;
+		case 'longestrunuser':
+			jsonData = jsonClient.read("xtrnmenu", "timeran_user_" + user.alias, 1);
+			break;
+		default:
+			log(LOG_ERR, "xtrnmenulib: Unknown special menu type: " + menutype);
+			return false;
+	}
+	
+	if (!jsonData) {
+		jsonData = {};
+	}
+	sortedItems = this.sort_special(jsonData);
+	
+	var i = 1;
+	for (var d=0; d<sortedItems.length; d++) {
+		var doorid = sortedItems[d];
+		for (var e in xtrn_area.prog) {
+			if (xtrn_area.prog[e].code.toLowerCase() == doorid.toLowerCase()) {
+				var stats;
+				switch (menutype) {
+					case 'longestrunall':
+					case 'longestrunuser':
+						var seconds = jsonData[doorid];
+						var hours = ~~(seconds/3600);
+						var mins = ~~((seconds % 3600) / 60);
+						var secs = ~~seconds % 60;
+						stats = ("00"+hours).slice(-2) + ":" + ("00"+mins).slice(-2) + ":" + ("00"+secs).slice(-2);
+						break;
+					case 'recentall':
+					case 'recentuser':
+						var elapsedtime = (time() - jsonData[doorid])*1000;
+						var msPerMin = 60000;
+						var msPerHour = msPerMin * 60;
+						var msPerDay = msPerHour * 24;
+						var msPerMon = msPerDay * 30;
+						var msPerYear = msPerDay * 365;
+						if (elapsedtime < msPerMin) {
+							stats = Math.round(elapsedtime/1000) + ' sec ago';
+						} else if (elapsedtime < msPerHour) {
+							stats = Math.round(elapsedtime/msPerMin) + ' min ago';
+						} else if (elapsedtime < msPerDay) {
+							var stat = Math.round(elapsedtime/msPerHour)
+							stats = Math.round(elapsedtime/msPerHour) + ' hr ago';
+						} else if (elapsedtime < msPerMon) {
+							stats = Math.round(elapsedtime/msPerDay) + ' day ago';
+						} else if (elapsedtime < msPerYear) {
+							stats = Math.round(elapsedtime/msPerMonth) + ' mon ago';
+						} else {
+							stats = Math.round(elapsedtime/msPerYear) + ' yr ago';
+						}
+						break;
+					default:
+						stats = jsonData[doorid].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");;
+						break;
+				}
+				menuitems.push({
+					input: i,
+					target: xtrn_area.prog[e].code,
+					title: xtrn_area.prog[e].name,
+					type: 'xtrnprog',
+					access_string: xtrn_area.prog[e].ars,
+					cost: xtrn_area.prog[e].cost,
+					stats: stats,
+				});
+				i++;
+				break;
+			}
+		}
+		
+		if ((itemcount > 0) && ((d + 1) >= itemcount)) {
+			break;
+		}
+	}
+	if (menuitems.length > 0) {
+		menu = {
+			id: menutype,
+			title: title,
+			items: menuitems,
+		};
+	}
+
+	return menu;
+}
+
+// return menu object for user's favorites
+ExternalMenus.prototype.getFavorites = function(title, itemcount) {
+	var menu;
+	var menuitems = [];
+	
+	if (itemcount === undefined) {
+		itemcount = 0;
+	}
+	
+	if (!this.options.json_enabled) {
+		log(LOG_DEBUG, "xtrnmenulib: Skipping favorites because JSON is not enabled");
+		return false;
+	}
+	
+	try {
+		require("json-client.js", "JSONClient");
+		var jsonClient = new JSONClient(this.options.json_host, this.options.json_port);
+		jsonClient.callback = this.processUpdate;
+	} catch (e) {
+		log(LOG_ERR, "xtrnmenulib: Could not initialize JSON database so favorites is now disabled: " + e);
+		return false;
+	}
+	
+	var sortedItems = [];
+	var jsonData = jsonClient.read("xtrnmenu", "favorites_" + user.alias, 1);
+	
+	if (!jsonData) {
+		jsonData = {};
+	}
+	
+	var i = 1;
+	for (var d=0; d<jsonData.length; d++) {
+		for (var e in xtrn_area.prog) {
+			if (xtrn_area.prog[e].code.toLowerCase() == jsonData[d].toLowerCase()) {
+				var stats;
+				
+				menuitems.push({
+					input: i,
+					target: xtrn_area.prog[e].code,
+					title: xtrn_area.prog[e].name,
+					type: 'xtrnprog',
+					access_string: xtrn_area.prog[e].ars,
+					cost: xtrn_area.prog[e].cost,
+				});
+				i++;
+				break;
+			}
+		}
+		
+		if ((itemcount > 0) && ((d + 1) >= itemcount)) {
+			break;
+		}
+	}
+
+	menu = {
+		id: 'favorites',
+		title: title !== undefined ? title : 'Favorites',
+		items: menuitems,
+	};
+	
+	return menu;
+}
\ No newline at end of file
diff --git a/exec/xtrnmenu.js b/exec/xtrnmenu.js
index 85a57e864b17153722f02140a0590ecf813e6545..3fa3fed9d1535663cc624bd2ef45ab5a15f31a0a 100644
--- a/exec/xtrnmenu.js
+++ b/exec/xtrnmenu.js
@@ -19,6 +19,8 @@ require("sbbsdefs.js", "K_NONE");
 
 require("xtrnmenulib.js", "MENU_LOADED");
 
+const ansiterm = load({}, 'ansiterm_lib.js');
+
 var options, xsec = -1;
 
 //// Main
@@ -27,10 +29,17 @@ const menuconfig = ExternalMenus.menuconfig;
 
 var i,j;
 
-if ((argv[0] == 'command') && (typeof argv[1] != "undefined")) {
-	docommand(argv[1]);
-} else if (argv[0] == 'gamesrv') {
-	external_section_menu_custom('gamesrv');
+var gamesrv = false;
+
+if ((argv[0] == 'command') && (typeof argv[1] !== "undefined")) {
+	docommand(argv[1], (typeof argv[2] !== "undefined" ? argv[2] : ""), (typeof argv[3] !== "undefined" ? argv[3] : ""));
+} else if (argv[0] === 'gamesrv') {
+	gamesrv = true;
+	external_menu_custom();
+} else if ((argv[0] === 'pre') && (typeof argv[1] !== "undefined")) {
+	dopre(argv[1]);
+} else if ((argv[0] === 'post') && (typeof argv[1] !== "undefined")) {
+	dopost(argv[1]);
 } else {
 	for (i in argv) {
 		for (j in xtrn_area.sec_list) {
@@ -38,32 +47,241 @@ if ((argv[0] == 'command') && (typeof argv[1] != "undefined")) {
 				xsec = j;
 		}
 	}
-
+	
 	if (xsec > -1) {
 		// if its the id of a standard section menu
 		if (options.use_xtrn_sec) {
 			// stock menu
 			js.exec("xtrn_sec.js", {}, xsec);
 		} else {
-			external_section_menu_custom(xsec);
+			external_menu_custom(xsec);
 		}
 	} else if (typeof argv[0] !== "undefined") {
 		// if its not a section menu, assume it is a custom menu
-		external_section_menu_custom(argv[0]);
+		external_menu_custom(argv[0]);
 	} else {
 		// main custom menu
-		external_section_menu_custom();
+		external_menu_custom();
+	}
+}
+
+// Runs pre-door stat tracking
+function dopre(progCode)
+{
+	require("nodedefs.js", "NODE_XTRN");
+	if (bbs.node_action != NODE_XTRN) {
+		log(LOG_DEBUG, "xtrnmenu pre: Skipping " + progCode + " because its not during a normal door run");
+		// don't want to track door stats for login events
+		return;
+	}
+	
+	var options = ExternalMenus.getOptions('custommenu', 'main');
+	
+	if (!options.json_enabled) {
+		log(LOG_DEBUG, "xtrnmenu pre: Skipping " + progCode + " because JSON is not enabled");
+		return;
+	}
+	
+	if (typeof options.blacklist_tracking_xtrncodes !== "undefined") {
+		var blacklist_xtrncodes = options.blacklist_tracking_xtrncodes.split(',');
+		for (var b=0; b < blacklist_xtrncodes.length; b++) {
+			if (blacklist_xtrncodes[b].toLowerCase() == progCode.toLowerCase()) {
+				log(LOG_DEBUG, "xtrnmenu pre: Skipping " + progCode + " because in blacklist_tracking_xtrncodes");
+				return;
+			}
+		}
+	}
+	
+	// get section of this program and block tracking if its in blacklist config
+	var secCode;
+	for (var j in xtrn_area.sec_list) {
+		for (var k in xtrn_area.sec_list[j].prog_list) {
+			if (xtrn_area.sec_list[j].prog_list[k].code.toLowerCase() == progCode.toLowerCase()) {
+				secCode = xtrn_area.sec_list[j].code;
+			}
+		}
+	}
+	if (typeof options.blacklist_tracking_xtrnsec !== "undefined") {
+		var blacklist_seccodes = options.blacklist_tracking_xtrnsec.split(',');
+		for (var b=0; b < blacklist_seccodes.length; b++) {
+			if (blacklist_seccodes[b].toLowerCase() == secCode) {
+				log(LOG_DEBUG, "xtrnmenu pre: Skipping " + progCode + " because in blacklist_tracking_xtrnsec " + secCode);
+				return;
+			}
+		}
+	}
+	
+	try {
+		require("json-client.js", "JSONClient");
+		var jsonClient = new JSONClient(options.json_host, options.json_port);
+		jsonClient.callback = ExternalMenus.processUpdate;
+	} catch (e) {
+		log(LOG_ERR, "xtrnmenu pre: Could not initialize JSON database so door tracking is now disabled: " + e);
+		return;
+	}
+	
+	// global times a door is run
+	var globalLaunchNum = jsonClient.read("xtrnmenu", "launchnum", 1);
+	
+	if (typeof globalLaunchNum === "undefined") {
+		globalLaunchNum = {};
+	}
+	
+	if (typeof globalLaunchNum[progCode] !== "undefined") {
+		globalLaunchNum[progCode]++;
+	} else {
+		globalLaunchNum[progCode] = 1;
+	}
+	log(LOG_DEBUG, "xtrnmenu pre: globalLaunchNum " + progCode + " = " + globalLaunchNum[progCode]);
+	
+	jsonClient.write("xtrnmenu", "launchnum", globalLaunchNum, 2);
+	// user's times a door is run
+	var userLaunchNum = jsonClient.read("xtrnmenu", "launchnum_user_" + user.alias, 1);
+	if (!userLaunchNum) {
+		userLaunchNum = {};
+	}
+	
+	if (typeof userLaunchNum[progCode] !== "undefined") {
+		userLaunchNum[progCode]++;
+	} else {
+		userLaunchNum[progCode] = 1;
+	}
+	log(LOG_DEBUG, "xtrnmenu pre: userLaunchNum " + progCode + " = " + userLaunchNum[progCode]);
+	jsonClient.write("xtrnmenu", "launchnum_user_" + user.alias,userLaunchNum, 2);
+	
+	// global launch start time (most recent)
+	var globalLaunchStart = jsonClient.read("xtrnmenu", "launchstart", 1);
+	if (!globalLaunchStart) {
+		globalLaunchStart = {};
+	}
+	
+	globalLaunchStart[progCode] = time();
+	log(LOG_DEBUG, "xtrnmenu pre: globalLaunchStart " + progCode + " = " + globalLaunchStart[progCode] );
+	jsonClient.write("xtrnmenu", "launchstart", globalLaunchStart, 2);
+	
+	// user launch start time (most recent)
+	var userLaunchStart = jsonClient.read("xtrnmenu", "launchstart_user_" + user.alias, 1);
+	if (!userLaunchStart) {
+		userLaunchStart = {};
+	}
+	
+	userLaunchStart[progCode] = time();
+	log(LOG_DEBUG, "xtrnmenu pre: userLaunchStart " + progCode + " = " + userLaunchStart[progCode] );
+	jsonClient.write("xtrnmenu", "launchstart_user_" + user.alias, userLaunchStart, 2);
+	
+	jsonClient.cycle();
+}
+
+
+// Runs post-door stat tracking
+function dopost(progCode)
+{
+	require("nodedefs.js", "NODE_XTRN");
+	if (bbs.node_action != NODE_XTRN) {
+		log(LOG_DEBUG, "xtrnmenu post: Skipping " + progCode + " because its not during a normal door run");
+		// don't want to track door stats for login events
+		return;
+	}
+	
+	var options = ExternalMenus.getOptions('custommenu', 'main');
+	
+	if (!options.json_enabled) {
+		log(LOG_DEBUG, "xtrnmenu post: Skipping " + progCode + " because JSON is not enabled");
+		return;
+	}
+	
+	if (typeof options.blacklist_tracking_xtrncodes !== "undefined") {
+		var blacklist_xtrncodes = options.blacklist_tracking_xtrncodes.split(',');
+		for (var b=0; b < blacklist_xtrncodes.length; b++) {
+			if (blacklist_xtrncodes[b].toLowerCase() == progCode.toLowerCase()) {
+				log(LOG_DEBUG, "xtrnmenu post: Skipping " + progCode + " because in blacklist_tracking_xtrncodes");
+				return;
+			}
+		}
+	}
+	
+	// get section of this program and block tracking if its in blacklist config
+	var secCode;
+	for (var j in xtrn_area.sec_list) {
+		for (var k in xtrn_area.sec_list[j].prog_list) {
+			if (xtrn_area.sec_list[j].prog_list[k].code.toLowerCase() == progCode.toLowerCase()) {
+				secCode = xtrn_area.sec_list[j].code;
+			}
+		}
+	}
+	if (typeof options.blacklist_tracking_xtrnsec !== "undefined") {
+		var blacklist_seccodes = options.blacklist_tracking_xtrnsec.split(',');
+		for (var b=0; b < blacklist_seccodes.length; b++) {
+			if (blacklist_seccodes[b].toLowerCase() == secCode) {
+				log(LOG_DEBUG, "xtrnmenu post: Skipping " + progCode + " because in blacklist_tracking_xtrnsec " + secCode);
+				return;
+			}
+		}
+	}
+	
+	try {
+		require("json-client.js", "JSONClient");
+		var jsonClient = new JSONClient(options.json_host, options.json_port);
+		jsonClient.callback = ExternalMenus.processUpdate;
+	} catch (e) {
+		log(LOG_ERR, "xtrnmenu pre: Could not initialize JSON database so door tracking is now disabled: " + e);
+		return;
+	}
+	
+	// user time ran
+	var newTimeRan;
+	var userLaunchStart = jsonClient.read("xtrnmenu", "launchstart_user_" + user.alias, 1);
+	if (userLaunchStart) {
+		
+		if (typeof userLaunchStart[progCode] !== "undefined") {
+			var lasttime = userLaunchStart[progCode];
+			var now = time();
+			var runtime = now - lasttime;
+			
+			var userTimeRan = jsonClient.read("xtrnmenu", "timeran_user_" + user.alias, 1);
+			if (!userTimeRan) {
+				// first time
+				userTimeRan = {};
+			}
+			
+			if (typeof userTimeRan[progCode] === "undefined") {
+				userTimeRan[progCode] = runtime;
+			} else {
+				userTimeRan[progCode] = userTimeRan[progCode] + runtime;
+			}
+			jsonClient.write("xtrnmenu", "timeran_user_" + user.alias, userTimeRan, 2);
+			
+			// global time ran
+			var globalTimeRan = jsonClient.read("xtrnmenu", "timeran", 1);
+			if (typeof globalTimeRan === "undefined") {
+				globalTimeRan = {};
+			}
+			
+			if (typeof globalTimeRan[progCode] === "undefined") {
+				globalTimeRan[progCode] = runtime;
+			} else {
+				globalTimeRan[progCode] = globalTimeRan[progCode] + runtime;
+			}
+			jsonClient.write("xtrnmenu", "timeran", globalTimeRan, 2);
+			
+			jsonClient.cycle();
+		}
 	}
 }
 
 // Runs custom commands, for gamesrv
-function docommand(command)
+function docommand(command, commandparam1, commandparam2)
 {
+	var options = ExternalMenus.getOptions('custommenu', 'main');
+	
 	switch (command) {
 		case 'checkmail':
-			var lmsg = user.stats.mail_wait;
+			var lmsg = user.stats.unread_mail_waiting;
 			if (lmsg > 0) {
-				console.putmsg('\r\n\x01gYou have \x01c' + parseInt(lmsg) + ' \x01gMessages waiting');
+				console.crlf();
+				if (console.yesno(user.stats.unread_mail_waiting + " message(s) waiting. Read now")) {
+					bbs.read_mail();
+				}
 			} else {
 				console.putmsg('\r\n\x01gNo New Messages');
 			}
@@ -71,10 +289,11 @@ function docommand(command)
 			console.clear();
 			break;
 		case 'feedback':
-			var subject;
-			console.putmsg('\r\n\x01\gPlease choose \x01wYes \x01gto forward to netmail!\r\n\r\n');
-			bbs.email(1, subject = "Game Server Feedback\r\n");
-			console.putmsg('Thank you for your Feedback, @SYSOP@ will get back to you ASAP!\r\n\r\n');
+			var subject = options.custom.feedback_subject !== undefined
+				? options.custom.feedback_subject: "Game Server Feedback\r\n";
+			bbs.email(1, WM_EMAIL, "", subject);
+			console.putmsg(options.custom.feedback_msg !== undefined ?
+				options.custom.feedback_msg : 'Thank you for your Feedback, @SYSOP@ will get back to you ASAP!\r\n\r\n');
 			break;
 		case 'prefs':
 			bbs.user_config();
@@ -86,36 +305,272 @@ function docommand(command)
 			commandstr = console.getstr();
 			str_cmds(commandstr);
 			break;
+		case 'textsec':
+			bbs.text_sec();
+			break;
+		case 'filearea':
+			if(file_exists(system.text_dir+"menu/tmessage.*"))
+				bbs.menu("tmessage");
+			filearea(commandparam1, commandparam2);
+			break;
+		case 'chat':
+			load("chat_sec.js");
+			break;
 		default:
 			doerror("Unknown command " + command);
 			break;
 	}
 }
 
-// Renders the top-level external menu
-function external_section_menu_custom(menuid)
+// Present a basic download area
+// Taken from classic shell
+function filearea(filelib, filedir) {
+	var options = ExternalMenus.getOptions('custommenu', 'main');
+
+	var key;
+	
+	if (!filelib || !filedir) {
+		writeln("Error: No file area or dir selected");
+		return;
+	}
+	
+	bbs.curlib = filelib;
+	bbs.curdir = filedir;
+	
+	file_transfers:
+		while (1) {
+			console.clear();
+			
+			if (!bbs.menu("xtrnmenu_xfer", P_NOERROR)) {
+				console.crlf();
+				writeln("\x01c\x01h[B]\x01n\x01c Batch");
+				writeln("\x01c\x01h[D]\x01n\x01c Download");
+				writeln("\x01c\x01h[F]\x01n\x01c Find Text in Descriptions");
+				writeln("\x01c\x01h[V]\x01n\x01c View Files");
+				writeln("\x01c\x01h[L]\x01n\x01c List Files");
+				writeln("\x01c\x01h[S]\x01n\x01c Search Filename");
+				writeln("\x01c\x01h[Q]\x01n\x01c Quit");
+			}
+			
+			console.crlf();
+			
+			// Update node status
+			bbs.node_action = NODE_XFER;
+			bbs.node_sync();
+			
+			// Display main Prompt
+			console.print(typeof options.custom.xfer_prompt !== "undefined" ? 
+				options.custom.xfer_prompt : "\x01n\x01c\xfe \x01b\x01hFile \x01n\x01c\xfe \x01h");
+			if (bbs.compare_ars("exempt T"))
+				console.putmsg("@TUSED@", P_SAVEATR);
+			else
+				console.putmsg("@TLEFT@", P_SAVEATR);
+			console.putmsg(typeof options.custom.xfer_prompt2 !== "undefined" ?
+				options.custom.xfer_prompt2 : " \x01n\x01c@DIR@: \x01n");
+			
+			// Get key (with / extended commands allowed)
+			var str = console.getkey().toLowerCase();
+			
+			// Commands
+			switch (str) {
+				case ';':
+					require("str_cmds.js", "str_cmds");
+					console.putmsg(typeof options.custom.command_prompt !== "undefined" ?
+						options.custom.command_prompt : "\r\n\x01gCommand: ");
+					var commandstr = console.getstr();
+					str_cmds(commandstr);
+					break;
+					
+				case '!':
+					if (bbs.compare_ars("SYSOP")) {
+						bbs.menu("sysxfer");
+					}
+					break;
+					
+				case 'b':
+					bbs.batch_menu();
+					break;
+				
+				case 'd':
+					console.print(typeof options.custom.download_prompt !== "undefined" ?
+						options.custom.download_prompt : "\r\n\x01c\x01hDownload File(s)\r\n");
+					
+					if (bbs.batch_dnload_total > 0) {
+						if (console.yesno(bbs.text(DownloadBatchQ))) {
+							bbs.batch_download();
+							break;
+						}
+					}
+					str = bbs.get_filespec();
+					if ((str == null) || (file_area.lib_list.length == 0))
+						break;
+					if (user.security.restrictions & UFLAG_D) {
+						console.putmsg(bbs.text(R_Download), P_SAVEATR);
+						break;
+					}
+					if (!bbs.list_file_info(file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].number, str, FI_DOWNLOAD)) {
+						var s = 0;
+						console.putmsg(bbs.text(SearchingAllDirs), P_SAVEATR);
+						for (i = 0; i < file_area.lib_list[bbs.curlib].dir_list.length; i++) {
+							if (i != bbs.curdir &&
+								(s = bbs.list_file_info(file_area.lib_list[bbs.curlib].dir_list[i].number, str, FI_DOWNLOAD)) != 0) {
+								if (s == -1 || str.indexOf('?') != -1 || str.indexOf('*') != -1) {
+									continue file_transfers;
+								}
+							}
+						}
+						console.putmsg(bbs.text(SearchingAllLibs), P_SAVEATR);
+						for (i = 0; i < file_area.lib_list.length; i++) {
+							if (i == bbs.curlib)
+								continue;
+							for (j = 0; j < file_area.lib_list[i].dir_list.length; j++) {
+								if ((s = bbs.list_file_info(file_area.lib_list[i].dir_list[j].number, str, FI_DOWNLOAD)) != 0) {
+									if (s == -1 || str.indexOf('?') != -1 || str.indexOf('*') != -1) {
+										continue file_transfers;
+									}
+								}
+							}
+						}
+					}
+					break;
+					
+				case 'f':
+					console.print(typeof options.custom.finddesc_prompt !== "undefined" ?
+						options.custom.finddesc_prompt : "\r\n\x01c\x01hFind Text in File Descriptions (no wildcards)\r\n");
+					bbs.scan_dirs(FL_FINDDESC, false);
+					break;
+					
+				case 'i':
+					file_info();
+					break;
+					
+				case 'l':
+					i = bbs.list_files();
+					if (i == -1)
+						break;
+					if (i == 0)
+						console.putmsg(bbs.text(EmptyDir), P_SAVEATR);
+					else
+						console.putmsg(format(bbs.text(NFilesListed), i), P_SAVEATR);
+					break;
+					
+				case 'r':
+					console.print(typeof options.custom.remove_prompt !== "undefined" ?
+						options.custom.remove_prompt : "\r\n\x01c\x01hRemove/Edit File(s)\r\n");
+					str = bbs.get_filespec();
+					if (str == null)
+						break;
+					if (!bbs.list_file_info(file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].number, str, FI_REMOVE)) {
+						var s = 0;
+						console.putmsg(bbs.text(SearchingAllDirs), P_SAVEATR);
+						for (i = 0; i < file_area.lib_list[bbs.curlib].dir_list.length; i++) {
+							if (i != bbs.curdir &&
+								(s = bbs.list_file_info(file_area.lib_list[bbs.curlib].dir_list[i].number, str, FI_REMOVE)) != 0) {
+								if (s == -1 || str.indexOf('?') != -1 || str.indexOf('*') != -1) {
+									continue file_transfers;
+								}
+							}
+						}
+						console.putmsg(bbs.text(SearchingAllLibs), P_SAVEATR);
+						for (i = 0; i < file_area.lib_list.length; i++) {
+							if (i == bbs.curlib)
+								continue;
+							for (j = 0; j < file_area.lib_list[i].dir_list.length; j++) {
+								if ((s = bbs.list_file_info(file_area.lib_list[i].dir_list[j].number, str, FI_REMOVE)) != 0) {
+									if (s == -1 || str.indexOf('?') != -1 || str.indexOf('*') != -1) {
+										continue file_transfers;
+									}
+								}
+							}
+						}
+					}
+					break;
+					
+				case 'q':
+				case "\x1B":
+				case KEY_LEFT:
+					return;
+				
+				case 's':
+					console.print(typeof options.custom.searchfname_prompt !== "undefined" ?
+						options.custom.searchfname_prompt : "\r\n\x01c\x01hSearch for Filename(s)\r\n");
+					bbs.scan_dirs(FL_NO_HDR);
+					break;
+					
+				case 't':
+					bbs.temp_xfer();
+					break;
+					
+				case 'v':
+					console.print(typeof options.custom.view_prompt !== "undefined" ?
+						options.custom.view_prompt : "\r\n\x01c\x01hView File(s)\r\n");
+					str = bbs.get_filespec();
+					if (str == null)
+						continue file_transfers;
+					if (!bbs.list_files(file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].number, str, FL_VIEW)) {
+						console.putmsg(bbs.text(SearchingAllDirs), P_SAVEATR);
+						for (i = 0; i < file_area.lib_list[bbs.curlib].dir_list.length; i++) {
+							if (i == bbs.curdir)
+								continue;
+							if (bbs.list_files(file_area.lib_list[bbs.curlib].dir_list[i].number, str, FL_VIEW))
+								break;
+						}
+						if (i < file_area.lib_list[bbs.curlib].dir_list.length)
+							continue file_transfers;
+						console.putmsg(bbs.text(SearchingAllLibs), P_SAVEATR);
+						for (i = 0; i < file_area.lib_list.length; i++) {
+							if (i == bbs.curlib)
+								continue;
+							for (j = 0; j < file_area.lib_list[i].dir_list.length; j++) {
+								if (bbs.list_files(file_area.lib_list[i].dir_list[j].number, str, FL_VIEW))
+									continue file_transfers;
+							}
+						}
+					}
+					break;
+				
+				default:
+					break;
+			}
+			console.crlf();
+		}
+}
+
+// Renders the menu
+function external_menu_custom(menuid)
 {
 	var i, menucheck, menuobj, item_multicolumn_fmt, item_singlecolumn_fmt,
-		cost, multicolumn, menuitemsfiltered = [];
+		item_multicolumn_fmt_inverse, item_singlecolumn_fmt_inverse,
+		cost, multicolumn, selected_index = 0, menuitemsfiltered = [];
 	var validkeys = []; // valid chars on menu selection
 	var keymax = 0; // max integer allowed on menu selection
-
-	var gamesrv = menuid == "gamesrv" ? true : false;
-	if (gamesrv) {
-		menuid = undefined;
+	
+	if (menuid == undefined) {
+		menuid = 'main';
 	}
-
+	
 	var options = ExternalMenus.getOptions('custommenu', menuid);
-
+	
 	menuobj = ExternalMenus.getMenu(menuid);
-
+	
 	// Allow overriding auto-format on a per-menu basis
 	var multicolumn_fmt = options.multicolumn_fmt;
 	var singlecolumn_fmt = options.singlecolumn_fmt;
-
+	var multicolumn_fmt_inverse = options.multicolumn_fmt_inverse;
+	var singlecolumn_fmt_inverse = options.singlecolumn_fmt_inverse;
+	
+	var return_singlecolumn_fmt = options.custom.return_singlecolumn_fmt !== undefined
+		? options.custom.return_singlecolumn_fmt : singlecolumn_fmt;
+	var return_multicolumn_fmt = options.custom.return_multicolumn_fmt !== undefined
+		? options.custom.return_multicolumn_fmt : multicolumn_fmt;
+	var return_singlecolumn_fmt_inverse = options.custom.return_singlecolumn_fmt_inverse !== undefined
+		? options.custom.return_singlecolumn_fmt_inverse : singlecolumn_fmt_inverse;
+	var return_multicolumn_fmt_inverse = options.custom.return_multicolumn_fmt_inverse !== undefined
+		? options.custom.return_multicolumn_fmt_inverse : multicolumn_fmt_inverse;	
+	
 	while (bbs.online) {
 		console.aborted = false;
-
+		
 		if (typeof menuobj === "undefined") {
 			menuobj = ExternalMenus.getSectionMenu(menuid);
 			if (typeof menuobj === "undefined") {
@@ -123,37 +578,77 @@ function external_section_menu_custom(menuid)
 				break;
 			}
 		}
-
+		
 		if (options.clear_screen) {
 			console.clear(LIGHTGRAY);
 		}
-
+		
 		if (user.security.restrictions&UFLAG_X) {
 			write(options.restricted_user_msg);
 			break;
 		}
-
+		
 		if (!xtrn_area.sec_list.length) {
 			write(options.no_programs_msg);
 			break;
 		}
-
+		
+		// empty menu
+		if (!menuobj.items.length && (menuid != "main")) {
+			write(options.no_programs_msg);
+			break;
+		}
+		
 		system.node_list[bbs.node_num-1].aux = 0; /* aux is 0, only if at menu */
 		bbs.node_action = NODE_XTRN;
 		bbs.node_sync();
-
+		
 		menuitemsfiltered = ExternalMenus.getSortedItems(menuobj);
-
+		
+		// The quit item is intended to aid in the lightbar navigation
+		if (gamesrv && (menuid == 'main')) {
+			menuitemsfiltered.push({
+				input: 'Q',
+				title: options.custom_logoff_msg !== undefined ? options.custom_logoff_msg : 'Logoff',
+				target: '',
+				type: 'quit',
+			});
+		} else if (menuid == 'main') {
+			menuitemsfiltered.push({
+				input: 'Q',
+				title: options.custom.quit_msg !== undefined ? options.custom.quit_msg : 'Quit',
+				target: '',
+				type: 'quit',
+			});
+		} else {
+			menuitemsfiltered.unshift({
+				input: 'Q',
+				title: options.custom.return_msg !== undefined ? options.custom.return_msg : 'Return to Previous Menu',
+				target: '',
+				type: 'quit',
+			});
+		}
+		
 		if (!bbs.menu("xtrnmenu_head_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_head", P_NOERROR)) {
 			bbs.menu("xtrn_head", P_NOERROR);
 		}
-
+		
 		// if file exists text/menu/xtrnmenu_(menuid).[rip|ans|mon|msg|asc], 
 		// then display that, otherwise dynamiic
 		if (!bbs.menu("xtrnmenu_" + menuid, P_NOERROR)) {
-
+			
+			// determine lines left. we can't know the size of the footer so 
+			// let the sysop use singlecolumn_margin to specify that. Below
+			// calcution will always leave room for titles and underline even
+			// if they aren't rendered
+			var linesleft = console.screen_rows - console.line_counter - options.singlecolumn_margin 
+				- 2 - 2; // -2 for header_fmt/crlf and -2 for crlf and footer
+			if(options.titles.trimRight() != '') linesleft = linesleft - 1;
+			if(options.underline.trimRight() != '') linesleft = linesleft - 2;
+			
+			multicolumn = menuitemsfiltered.length  > linesleft;
+			
 			// if no custom menu file in text/menu, create a dynamic one
-			multicolumn = options.multicolumn && menuitemsfiltered.length > options.singlecolumn_height;
 			printf(options.header_fmt, menuobj.title);
 			if(options.titles.trimRight() != '')
 				write(options.titles);
@@ -172,7 +667,7 @@ function external_section_menu_custom(menuid)
 					write(options.underline);
 			}
 			console.crlf();
-
+			
 			// n is the number of items for the 1st column
 			var n;
 			if (multicolumn) {
@@ -180,7 +675,7 @@ function external_section_menu_custom(menuid)
 			} else {
 				n = menuitemsfiltered.length;
 			}
-
+			
 			// j is the index for each menu item on 2nd column
 			var j = n; // start j at the first item for 2nd column
 			for (i = 0; i < n && !console.aborted; i++) {
@@ -189,9 +684,9 @@ function external_section_menu_custom(menuid)
 					// if its an external program, get the cost
 					cost = xtrn_area.prog[menuitemsfiltered[i].target.toLowerCase()].cost;
 				}
-
+				
 				console.add_hotspot(menuitemsfiltered[i].input.toString());
-
+				
 				validkeys.push(menuitemsfiltered[i].input.toString());
 				var intCheck = Number(menuitemsfiltered[i].input);
 				if (!intCheck.isNaN) {
@@ -199,53 +694,108 @@ function external_section_menu_custom(menuid)
 						keymax = menuitemsfiltered[i].input;
 					}
 				}
-
+				
 				// allow overriding format on a per-item basis
 				// great for featuring a specific game
 				var checkkey = menuitemsfiltered[i].target + '-multicolumn_fmt';
 				checkkey = checkkey.toLowerCase();
 				item_multicolumn_fmt = (typeof options[checkkey] !== "undefined") ?
 					options[checkkey] : options.multicolumn_fmt;
-
+				item_multicolumn_fmt_inverse = (typeof options[checkkey + "_inverse"] !== "undefined") ?
+					options[checkkey + "_inverse"] : options.multicolumn_fmt_inverse;
+				
 				checkkey = menuitemsfiltered[i].target + '-singlecolumn_fmt'
 				checkkey = checkkey.toLowerCase();
 				item_singlecolumn_fmt = (typeof options[checkkey] !== "undefined") ?
 					options[checkkey] : options.singlecolumn_fmt;
-
-				printf(multicolumn ? item_multicolumn_fmt : item_singlecolumn_fmt,
-					menuitemsfiltered[i].input.toString().toUpperCase(),
-					menuitemsfiltered[i].title,
-					cost
-				);
-
+				item_singlecolumn_fmt_inverse = (typeof options[checkkey + "_inverse"] !== "undefined") ?
+					options[checkkey + "_inverse"] : options.singlecolumn_fmt_inverse;
+				
+				if (i == selected_index) {
+					if (menuitemsfiltered[i].type == 'quit') {
+						printf(multicolumn ? return_multicolumn_fmt_inverse : return_singlecolumn_fmt_inverse,
+							menuitemsfiltered[i].input.toString().toUpperCase(),
+							menuitemsfiltered[i].title,
+							''
+						);
+					} else {
+						printf(multicolumn ? item_multicolumn_fmt_inverse : item_singlecolumn_fmt_inverse,
+							menuitemsfiltered[i].input.toString().toUpperCase(),
+							menuitemsfiltered[i].title,
+							cost
+						);
+					}
+				} else {
+					if (menuitemsfiltered[i].type == 'quit') {
+						printf(multicolumn ? return_multicolumn_fmt : return_singlecolumn_fmt,
+							menuitemsfiltered[i].input.toString().toUpperCase(),
+							menuitemsfiltered[i].title,
+							''
+						);
+					} else {
+						printf(multicolumn ? item_multicolumn_fmt : item_singlecolumn_fmt,
+							menuitemsfiltered[i].input.toString().toUpperCase(),
+							menuitemsfiltered[i].title,
+							cost
+						);
+					}
+				}
+				
 				if (multicolumn) {
-					if ((typeof(menuitemsfiltered[j]) !== "undefined")) {
+					if (typeof menuitemsfiltered[j] !== "undefined") {
 						validkeys.push(menuitemsfiltered[j].input.toString());
-
+						
 						var intCheck = Number(menuitemsfiltered[j].input);
 						if (!intCheck.isNaN) {
 							if (intCheck > keymax) {
 								keymax = menuitemsfiltered[j].input;
 							}
 						}
-
+						
 						// allow overriding format on a per-item basis
 						// great for featuring a specific game
 						var checkkey = menuitemsfiltered[j].target + '-multicolumn_fmt';
 						checkkey = checkkey.toLowerCase();
 						item_multicolumn_fmt = (typeof options[checkkey] !== "undefined") ?
 							options[checkkey] : options.multicolumn_fmt;
-
+						item_multicolumn_fmt_inverse = (typeof options[checkkey + "_inverse"] !== "undefined") ?
+							options[checkkey + "_inverse"] : options.multicolumn_fmt_inverse;
+						
 						checkkey = menuitemsfiltered[j].target + '-singlecolumn_fmt'
 						checkkey = checkkey.toLowerCase();
-
+						
 						write(options.multicolumn_separator);
 						console.add_hotspot(menuitemsfiltered[j].input.toString());
-						printf(item_multicolumn_fmt,
-							menuitemsfiltered[j].input.toString().toUpperCase(),
-							menuitemsfiltered[j].title,
-							cost
-						);
+						
+						if (selected_index == j) {
+							if (menuitemsfiltered[j].type == 'quit') {
+								printf(multicolumn ? return_multicolumn_fmt_inverse : return_singlecolumn_fmt_inverse,
+									menuitemsfiltered[j].input.toString().toUpperCase(),
+									menuitemsfiltered[j].title,
+									''
+								);					
+							} else {
+								printf(item_multicolumn_fmt_inverse,
+									menuitemsfiltered[j].input.toString().toUpperCase(),
+									menuitemsfiltered[j].title,
+									cost
+								);
+							}
+						} else {
+							if (menuitemsfiltered[j].type == 'quit') {
+								printf(return_multicolumn_fmt,
+									menuitemsfiltered[j].input.toString().toUpperCase(),
+									menuitemsfiltered[j].title,
+									''
+								);
+							} else {
+								printf(item_multicolumn_fmt,
+									menuitemsfiltered[j].input.toString().toUpperCase(),
+									menuitemsfiltered[j].title,
+									cost
+								);
+							}
+						}
 					} else {
 						write(options.multicolumn_separator);
 					}
@@ -253,29 +803,38 @@ function external_section_menu_custom(menuid)
 				}
 				console.crlf();
 			}
-
+			
 			if (gamesrv) {
 				if (!bbs.menu("xtrn_gamesrv_tail_" + menuid, P_NOERROR)) {
 					bbs.menu("xtrn_gamesrv_tail", P_NOERROR);
 				}
 			}
-
+			
 			if (!bbs.menu("xtrnmenu_tail_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_tail", P_NOERROR)) {
 				bbs.menu("xtrn_tail", P_NOERROR);
 			}
-
+			
 			bbs.node_sync();
 			console.mnemonics(options.which);
 		}
-
+		
 		validkeys.push('q');
-
+		validkeys.push("\x1B");
+		validkeys.push(KEY_LEFT);
+		validkeys.push(KEY_RIGHT);
+		validkeys.push(KEY_UP);
+		validkeys.push(KEY_DOWN);
+		validkeys.push("\x0d"); //enter
+		
 		var keyin, keyin2;
 		var maxkeylen = 0;
 		var maxfirstkey = 0;
 		var morekeys = [];
 		var k;
 		for (k in validkeys) {
+			if (validkeys[k] == "\x1B") {
+				continue;
+			}
 			if (validkeys[k].length > maxkeylen) {
 				maxkeylen = validkeys[k].length;
 			}
@@ -283,58 +842,136 @@ function external_section_menu_custom(menuid)
 				morekeys.push(validkeys[k].toString().toLowerCase().substring(0,1));
 			}
 		}
-
+		
 		// get first key
 		keyin = console.getkey();
 		keyin = keyin.toString().toLowerCase();
-
+		
+		if (gamesrv && ((keyin == "\x1B")) && (menuid == 'main')) {
+			// don't have ESC do anything on the main menu
+		} else if ((keyin == 'q') || (keyin == "\x1B")) {
+			if (gamesrv && (menuid == 'main')) {
+				// doing it this way rather than calling bbs.logoff()
+				// so that the prompt defaults to Yes
+				console.crlf();
+				if (console.yesno("Do you wish to logoff")) {
+					bbs.logoff(false);
+				}
+			} else {
+				console.clear();
+				return;
+			}
+		} else if (keyin == KEY_UP) {
+			--selected_index;
+			if (selected_index < 0) {
+				selected_index = menuitemsfiltered.length - 1;
+			}
+			continue;
+		} else if (keyin == KEY_DOWN) {
+			++selected_index;
+			if (selected_index > (menuitemsfiltered.length - 1)) {
+				selected_index = 0;
+			}
+			continue;
+		} else if (keyin == KEY_LEFT) {
+			var row_limit = n -1;
+			if (selected_index == 0) {
+				// first item, go back menu
+				if (gamesrv && (menuid == 'main')) {
+					bbs.logoff();
+				} else {
+					console.clear();
+					return;
+				}
+			} else if (selected_index <= row_limit) {
+				// on first col
+				var new_selected_index = selected_index + n;
+				if (typeof menuitemsfiltered[new_selected_index] === "undefined") {
+					// no item on right, no change
+				} else {
+					selected_index = new_selected_index;
+				}
+			} else {
+				// on second col
+				selected_index = selected_index - n;
+			}
+		} else if (keyin == KEY_RIGHT) {
+			var row_limit = n - 1;
+			if (selected_index <= row_limit) {
+				// on first col
+				var new_selected_index = selected_index + n;
+				if (typeof menuitemsfiltered[new_selected_index] === "undefined") {
+					// no item on right, no change
+				} else {
+					selected_index = new_selected_index;
+				}
+			} else {
+				// on second col
+				selected_index = selected_index - n;
+			}
+		}
+		
+		// if ENTER on a QUIT item, exit
+		if (keyin == "\x0d") {
+			if (menuitemsfiltered[selected_index].type == "quit") {
+				if (gamesrv && (menuid == 'main')) {
+					bbs.logoff();
+				} else {
+					console.clear();
+					return;
+				}
+			}
+		}
+		
 		// The logic below is to make it not require enter for as
 		// many items as possible
-
+		
 		// if max keys is 2 and they entered something that might have
 		// a second digit/char, then get the key
-		if (maxkeylen == 2) {
+		if ((maxkeylen == 2) && (keyin !== "\x0d")) {
 			if (morekeys.indexOf(keyin) !== -1) {
 				write(keyin);
 				keyin2 = console.getkey(); // either the second digit or enter
-				if ((keyin2 != "\r") && (keyin2 != "\n") && (keyin2 != "\r\n")) {
+				if ((keyin2 !== "\r") && (keyin2 !== "\n") && (keyin2 !== "\r\n")) {
 					keyin = keyin + keyin2.toLowerCase();
 				}
 			}
-		} else if (maxkeylen > 2) {
+		} else if ((maxkeylen > 2) && (keyin !== "\x0d")) {
 			// there there are more than 99 items, then just use getkeys 
 			// for the rest
 			write(keyin);
 			keyin2 = console.getkeys(validkeys, keymax);
 			keyin = keyin + keyin2.toLowerCase();
 		}
-
+		
 		if (keyin) {
-			if (keyin == 'q') {
-				if (gamesrv && ('menuid' == 'main')) {
-					bbs.logoff();
-				} else {
-					console.clear();
-					return;
-				}
-			}
-
+			
 			menuitemsfiltered.some(function (menuitemfiltered) {
-				var menutarget = menuitemfiltered.target.toLowerCase();
+				var menutarget = menuitemfiltered.target ? menuitemfiltered.target.toLowerCase() : null;
 				var menuinput = menuitemfiltered.input.toString().toLowerCase();
-
-				if (menuinput == keyin) {
+				
+				if ((menuinput == keyin) || 
+					((keyin == "\x0d") && (menuitemsfiltered[selected_index].input == menuitemfiltered.input))) {
+					// update the selected index so the selected item is correct in the
+					// menu rendering if they used a key
+					for (var mm=0; mm<=menuitemsfiltered.length;mm++) {
+						if (menuitemsfiltered[mm].input == menuitemfiltered.input) {
+							selected_index = mm;
+							break;
+						}
+					}
+					
 					switch (menuitemfiltered.type) {
 						// custom menu, defined in xtrnmenu.json
 						case 'custommenu':
-							external_section_menu_custom(menutarget);
+							external_menu_custom(menutarget);
 							return true;
 						// external program section
 						case 'xtrnmenu':
 							if (options.use_xtrn_sec) {
 								js.exec("xtrn_sec.js", {}, menutarget);
 							} else {
-								external_section_menu_custom(menutarget);
+								external_menu_custom(menutarget);
 							}
 							return true;
 						// external program
@@ -350,6 +987,20 @@ function external_section_menu_custom(menuid)
 						case 'command':
 							bbs.exec(menutarget);
 							return true;
+						case 'recentall':
+						case 'recentuser':
+						case 'mostlaunchedall':
+						case 'mostlauncheduser':
+						case 'longestrunall':
+						case 'longestrunuser':
+							special_menu(menuitemfiltered.type, menuitemfiltered.title, menutarget);
+							return true;
+						case 'search':
+							search_menu(menuitemfiltered.title, menutarget);
+							return true;
+						case 'favorites':
+							favorites_menu(menuitemfiltered.title, menutarget);
+							return true;
 					} //switch
 				} // if menu item matched keyin
 			}); // foreach menu item
@@ -358,6 +1009,1477 @@ function external_section_menu_custom(menuid)
 }
 
 
+// Renders the special menu (recent, most launched, etc.)
+function special_menu(menutype, title, itemcount) {
+	if (itemcount === undefined) {
+		itemcount = 0;
+	}
+	
+	var i, menucheck, item_multicolumn_fmt, item_singlecolumn_fmt,
+		item_multicolumn_fmt_inverse, item_singlecolumn_fmt_inverse,
+		multicolumn, selected_index = 0, menuitemsfiltered = [];
+	var validkeys = []; // valid chars on menu selection
+	var keymax = 0; // max integer allowed on menu selection
+	
+	var menuid = menutype;
+	
+	var options = ExternalMenus.getOptions('custommenu', menuid);
+	
+	// Allow overriding auto-format on a per-menu basis
+	var multicolumn_fmt = options.custom.multicolumn_fmt_special !== undefined 
+		? options.custom.multicolumn_fmt_special : options.multicolumn_fmt;
+	var singlecolumn_fmt = options.custom.singlecolumn_fmt_special !== undefined
+		? options.custom.singlecolumn_fmt_special : options.singlecolumn_fmt;
+	var multicolumn_fmt_inverse = options.custom.multicolumn_fmt_special_inverse !== undefined
+		? options.custom.multicolumn_fmt_special_inverse : options.multicolumn_fmt_inverse;
+	var singlecolumn_fmt_inverse = options.custom.singlecolumn_fmt_special_inverse !== undefined
+		? options.custom.singlecolumn_fmt_special_inverse : options.singlecolumn_fmt_inverse;
+	
+	var return_singlecolumn_special_fmt = options.custom.return_singlecolumn_special_fmt !== undefined
+		? options.custom.return_singlecolumn_special_fmt : singlecolumn_fmt;
+	var return_multicolumn_special_fmt = options.custom.return_multicolumn_special_fmt !== undefined
+		? options.custom.return_multicolumn_special_fmt : multicolumn_fmt;
+	var return_singlecolumn_special_fmt_inverse = options.custom.return_singlecolumn_special_fmt_inverse !== undefined
+		? options.custom.return_singlecolumn_special_fmt_inverse : singlecolumn_fmt_inverse;
+	var return_multicolumn_special_fmt_inverse = options.custom.return_multicolumn_special_fmt_inverse !== undefined
+		? options.custom.return_multicolumn_special_fmt_inverse : multicolumn_fmt_inverse;
+	
+	while (bbs.online) {
+		console.aborted = false;
+		
+		if (options.clear_screen) {
+			console.clear(LIGHTGRAY);
+		}
+		
+		if (user.security.restrictions&UFLAG_X) {
+			write(options.restricted_user_msg);
+			break;
+		}
+		
+		var menuobj = ExternalMenus.getSpecial(menutype, title, itemcount);
+		if (!bbs.menu("xtrnmenu_head_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_head", P_NOERROR)) {
+			bbs.menu("xtrn_head", P_NOERROR);
+		}
+		
+		if ((menuobj === undefined) || (!menuobj.items.length)) {
+			write(options.no_programs_msg);
+			break;
+		}
+		
+		menuitemsfiltered = menuobj.items;
+		
+		// The quit item is intended to aid in the lightbar navigation
+		menuitemsfiltered.unshift({
+			input: 'Q',
+			title: options.custom.return_msg !== undefined ? options.custom.return_msg : 'Return to Previous Menu',
+			target: '',
+			type: 'quit',
+		});
+		
+		// determine lines left. we can't know the size of the footer so 
+		// let the sysop use singlecolumn_margin to specify that. Below
+		// calcution will always leave room for titles and underline even
+		// if they aren't rendered
+		var linesleft = console.screen_rows - console.line_counter - options.singlecolumn_margin
+			- 2 - 2; // -2 for header_fmt/crlf and -2 for crlf and footer
+		if(options.titles.trimRight() != '') linesleft = linesleft - 1;
+		if(options.underline.trimRight() != '') linesleft = linesleft - 2;
+		
+		multicolumn = menuitemsfiltered.length  > linesleft;
+		
+		// if no custom menu file in text/menu, create a dynamic one
+		printf(options.header_fmt, title);
+		if(options.titles.trimRight() != '')
+			write(options.titles);
+		if(multicolumn) {
+			write(options.multicolumn_separator);
+			if (options.titles.trimRight() != '')
+				write(options.titles);
+		}
+		if(options.underline.trimRight() != '') {
+			console.crlf();
+			write(options.underline);
+		}
+		if(multicolumn) {
+			write(options.multicolumn_separator);
+			if (options.underline.trimRight() != '')
+				write(options.underline);
+		}
+		console.crlf();
+		
+		// n is the number of items for the 1st column
+		var n;
+		if (multicolumn) {
+			n = Math.floor(menuitemsfiltered.length / 2) + (menuitemsfiltered.length & 1);
+		} else {
+			n = menuitemsfiltered.length;
+		}
+		
+		// j is the index for each menu item on 2nd column
+		var j = n; // start j at the first item for 2nd column
+		for (i = 0; i < n && !console.aborted; i++) {
+			console.add_hotspot(menuitemsfiltered[i].input.toString());
+			
+			validkeys.push(menuitemsfiltered[i].input.toString());
+			
+			if (menuitemsfiltered[i].input > keymax) {
+				keymax = menuitemsfiltered[i].input;
+			}
+			
+			if (i == selected_index) {
+				if (menuitemsfiltered[i].type == 'quit') {
+					printf(multicolumn ? return_multicolumn_special_fmt_inverse : return_singlecolumn_special_fmt_inverse,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title,
+						''
+					);
+				} else {
+					printf(multicolumn ? multicolumn_fmt_inverse : singlecolumn_fmt_inverse,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title,
+						menuitemsfiltered[i].stats
+					);
+				}
+			} else {
+				if (menuitemsfiltered[i].type == 'quit') {
+					printf(multicolumn ? return_multicolumn_special_fmt : return_singlecolumn_special_fmt,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title,
+						''
+					);
+				} else {
+					printf(multicolumn ? multicolumn_fmt : singlecolumn_fmt,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title,
+						menuitemsfiltered[i].stats
+					);
+				}
+			}
+			
+			if (multicolumn) {
+				if (typeof menuitemsfiltered[j] !== "undefined") {
+					validkeys.push(menuitemsfiltered[j].input.toString());
+					
+					if (menuitemsfiltered[j].input > keymax) {
+						keymax = menuitemsfiltered[i].input;
+					}
+					
+					write(options.multicolumn_separator);
+					console.add_hotspot(menuitemsfiltered[j].input.toString());
+					
+					if (selected_index == j) {
+						if (menuitemsfiltered[j].type == 'quit') {
+							printf(multicolumn ? return_multicolumn_special_fmt_inverse : return_singlecolumn_special_fmt_inverse,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title,
+								''
+							);
+						} else {
+							printf(multicolumn ? multicolumn_fmt_inverse : singlecolumn_fmt_inverse,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title,
+								menuitemsfiltered[j].stats
+							);
+						}
+					} else {
+						if (menuitemsfiltered[j].type == 'quit') {
+							printf(multicolumn ? return_multicolumn_special_fmt : return_singlecolumn_special_fmt,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title,
+								''
+							);
+						} else {
+							printf(multicolumn ? multicolumn_fmt : singlecolumn_fmt,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title,
+								menuitemsfiltered[j].stats
+							);
+						}
+					}
+				} else {
+					write(options.multicolumn_separator);
+				}
+				j++;
+			}
+			console.crlf();
+		}
+		
+		if (gamesrv) {
+			if (!bbs.menu("xtrn_gamesrv_tail_" + menuid, P_NOERROR)) {
+				bbs.menu("xtrn_gamesrv_tail", P_NOERROR);
+			}
+		}
+		
+		if (!bbs.menu("xtrnmenu_tail_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_tail", P_NOERROR)) {
+			bbs.menu("xtrn_tail", P_NOERROR);
+		}
+		
+		bbs.node_sync();
+		console.mnemonics(options.which);
+		
+		validkeys.push('q');
+		validkeys.push("\x1B");
+		validkeys.push(KEY_LEFT);
+		validkeys.push(KEY_RIGHT);
+		validkeys.push(KEY_UP);
+		validkeys.push(KEY_DOWN);
+		validkeys.push("\x0d"); //enter
+		
+		var keyin, keyin2;
+		var maxkeylen = 0;
+		var maxfirstkey = 0;
+		var morekeys = [];
+		var k;
+		for (k in validkeys) {
+			if (validkeys[k] == "\x1B") {
+				continue;
+			}
+			if (validkeys[k].length > maxkeylen) {
+				maxkeylen = validkeys[k].length;
+			}
+			if (validkeys[k].length > 1) {
+				morekeys.push(validkeys[k].toString().toLowerCase().substring(0,1));
+			}
+		}
+		
+		// get first key
+		keyin = console.getkey();
+		keyin = keyin.toString().toLowerCase();
+		
+		// if ENTER on a QUIT item, exit
+		if (keyin == "\x0d") {
+			if (menuitemsfiltered[selected_index].type == "quit") {
+				console.clear();
+				return;
+			}
+		}
+		
+		if ((keyin == 'q') || (keyin == "\x1B")) {
+			console.clear();
+			return;
+		} else if (keyin == KEY_UP) {
+			--selected_index;
+			if (selected_index < 0) {
+				selected_index = menuitemsfiltered.length - 1;
+			}
+			continue;
+		} else if (keyin == KEY_DOWN) {
+			++selected_index;
+			if (selected_index > (menuitemsfiltered.length - 1)) {
+				selected_index = 0;
+			}
+			continue;
+		} else if (keyin == KEY_LEFT) {
+			var row_limit = n -1;
+			if (selected_index == 0) {
+				// first item, go back menu
+				console.clear();
+				return;
+			} else if (selected_index <= row_limit) {
+				// on first col
+				var new_selected_index = selected_index + n;
+				if (typeof menuitemsfiltered[new_selected_index] === "undefined") {
+					// no item on right, no change
+				} else {
+					selected_index = new_selected_index;
+				}
+			} else {
+				// on second col
+				selected_index = selected_index - n;
+			}
+		} else if (keyin == KEY_RIGHT) {
+			var row_limit = n - 1;
+			if (selected_index <= row_limit) {
+				// on first col
+				var new_selected_index = selected_index + n;
+				if (typeof menuitemsfiltered[new_selected_index] === "undefined") {
+					// no item on right, no change
+				} else {
+					selected_index = new_selected_index;
+				}
+			} else {
+				// on second col
+				selected_index = selected_index - n;
+			}
+		}		
+		
+		// The logic below is to make it not require enter for as
+		// many items as possible
+		
+		// if max keys is 2 and they entered something that might have
+		// a second digit/char, then get the key
+		if ((maxkeylen == 2) && (keyin !== "\x0d")) {
+			if (morekeys.indexOf(keyin) !== -1) {
+				write(keyin);
+				keyin2 = console.getkey(); // either the second digit or enter
+				if ((keyin2 !== "\r") && (keyin2 !== "\n") && (keyin2 !== "\r\n")) {
+					keyin = keyin + keyin2.toLowerCase();
+				}
+			}
+		} else if ((maxkeylen > 2) && (keyin !== "\x0d")) {
+			// there there are more than 99 items, then just use getkeys 
+			// for the rest
+			write(keyin);
+			keyin2 = console.getkeys(validkeys, keymax);
+			keyin = keyin + keyin2.toLowerCase();
+		}
+		
+		if (keyin) {
+			menuitemsfiltered.some(function (menuitemfiltered) {
+				var menutarget = menuitemfiltered.target.toLowerCase();
+				var menuinput = menuitemfiltered.input.toString().toLowerCase();
+				
+				if ((menuinput == keyin) ||
+					((keyin == "\x0d") && (menuitemsfiltered[selected_index].input == menuitemfiltered.input))) {
+					// everything in this menu is an xtrnprog
+					
+					// reset selected index, because the menus are dynamic and
+					// the order may change between executions
+					selected_index = 0;
+					
+					// run the external program
+					if (typeof xtrn_area.prog[menutarget] !== "undefined") {
+						bbs.exec_xtrn(menutarget);
+						return true;
+					} else {
+						doerror(options.custom_menu_program_not_found_msg.replace('%PROGRAMID%', menutarget));
+					}
+				} // if menu item matched keyin
+			}); // foreach menu item
+		} // if keyin
+	} // main bbs.online loop
+}
+
+
+// Renders the search menu
+function search_menu(title, itemcount) {
+	if (itemcount === undefined) {
+		itemcount = 0;
+	}
+	
+	var i, menucheck, item_multicolumn_fmt, item_singlecolumn_fmt,
+		item_multicolumn_fmt_inverse, item_singlecolumn_fmt_inverse,
+		multicolumn, selected_index = 0, menuitemsfiltered = [];
+	var validkeys = []; // valid chars on menu selection
+	var keymax = 0; // max integer allowed on menu selection
+	
+	var menuid = 'search';
+	
+	var options = ExternalMenus.getOptions('custommenu', menuid);
+	
+	var return_multicolumn_fmt = options.custom.return_multicolumn_fmt !== undefined
+		? options.custom.return_multicolumn_fmt : multicolumn_fmt;
+	var return_singlecolumn_fmt = options.custom.return_singlecolumn_fmt !== undefined
+		? options.custom.return_singlecolumn_fmt : singlecolumn_fmt;
+	var return_singlecolumn_fmt_inverse = options.custom.return_singlecolumn_fmt_inverse !== undefined
+		? options.custom.return_singlecolumn_fmt_inverse : singlecolumn_fmt_inverse;
+	var return_multicolumn_fmt_inverse = options.custom.return_multicolumn_fmt_inverse !== undefined
+		? options.custom.return_multicolumn_fmt_inverse : multicolumn_fmt_inverse;
+	
+	while (bbs.online) {
+		console.aborted = false;
+		
+		if (options.clear_screen) {
+			console.clear(LIGHTGRAY);
+		}
+		
+		if (user.security.restrictions&UFLAG_X) {
+			write(options.restricted_user_msg);
+			break;
+		}
+		
+		if (!bbs.menu("xtrnmenu_head_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_head", P_NOERROR)) {
+			bbs.menu("xtrn_head", P_NOERROR);
+		}
+		
+		if (searchterm) {
+			printf(typeof options.custom.searchresultsheader !== "undefined"
+				? options.custom.searchresultsheader : "\x01n\x01cSearch Results for \x01h%s", searchterm);
+		} else {
+			write(typeof options.custom.entersearchterm !== "undefined" ?
+				options.custom.entersearchterm : "\x01y\x01hEnter search term: ");
+			var searchterm = console.getstr(searchterm, 40,
+				K_LINE | K_EDIT | K_NOEXASC | K_TRIM);			
+			if (!searchterm) {
+				return;
+			} else {
+				console.crlf();
+				printf(typeof options.custom.searchresultsheader !== "undefined"
+					? options.custom.searchresultsheader : "\x01n\x01cSearch Results for \x01h%s", searchterm);
+			}
+		}
+		
+		searchterm = searchterm.toLowerCase();
+		menuitemsfiltered = [];
+		
+		var input = 1;
+		
+		for (i in menuconfig.menus) {
+			if ((itemcount > 0) && (menuitemsfiltered.length >= itemcount)) {
+				break
+			}
+			if (menuconfig.menus[i].title.toLowerCase().indexOf(searchterm) !== -1) {
+				if (user.compare_ars(menuconfig.menus[i].title.access_string)) {
+					menuitemsfiltered.push({
+						input: input++,
+						id: menuconfig.menus[i].id,
+						title: menuconfig.menus[i].title,
+						target: menuconfig.menus[i].id,
+						type: 'custommenu'
+					});	
+				}
+			}
+		}
+		
+		for (i in xtrn_area.sec_list) {
+			if ((itemcount > 0) && (menuitemsfiltered.length >= itemcount)) {
+				break
+			}
+			if (xtrn_area.sec_list[i].name.toLowerCase().indexOf(searchterm) !== -1) {
+				if (xtrn_area.sec_list[i].can_access) {
+					menuitemsfiltered.push({
+						input: input++,
+						id: xtrn_area.sec_list[i].code,
+						title: xtrn_area.sec_list[i].name,
+						target: xtrn_area.sec_list[i].code,
+						type: 'xtrnmenu'
+					});
+				}
+			}
+		}
+		
+		for (i in xtrn_area.sec_list) {
+			for (var k in xtrn_area.sec_list[i].prog_list) {
+				if ((itemcount > 0) && (menuitemsfiltered.length >= itemcount)) {
+					break
+				}
+				if (xtrn_area.sec_list[i].prog_list[k].name.toLowerCase().indexOf(searchterm) !== -1) {
+					if (xtrn_area.sec_list[i].prog_list[k].can_access) {
+						menuitemsfiltered.push({
+							input: input++,
+							id: xtrn_area.sec_list[i].prog_list[k].code,
+							title: xtrn_area.sec_list[i].prog_list[k].name,
+							target: xtrn_area.sec_list[i].prog_list[k].code,
+							type: 'xtrnprog'
+						});
+					}
+				}
+			}
+		}
+		
+		if (!menuitemsfiltered.length) {
+			write(options.no_programs_msg);
+			break;
+		} else {
+			console.crlf();
+		}
+		
+		// The quit item is intended to aid in the lightbar navigation
+		menuitemsfiltered.unshift({
+			input: 'Q',
+			title: options.custom.return_msg !== undefined ? options.custom.return_msg : 'Return to Previous Menu',
+			target: '',
+			type: 'quit',
+		});
+		
+		// determine lines left. we can't know the size of the footer so 
+		// let the sysop use singlecolumn_margin to specify that. Below
+		// calcution will always leave room for titles and underline even
+		// if they aren't rendered
+		var linesleft = console.screen_rows - console.line_counter - options.singlecolumn_margin
+			- 2 - 2; // -2 for header_fmt/crlf and -2 for crlf and footer
+		if(options.titles.trimRight() != '') linesleft = linesleft - 1;
+		if(options.underline.trimRight() != '') linesleft = linesleft - 2;
+		
+		multicolumn = menuitemsfiltered.length  > linesleft;
+		
+		if(options.titles.trimRight() != '')
+			write(options.titles);
+		if(multicolumn) {
+			write(options.multicolumn_separator);
+			if (options.titles.trimRight() != '')
+				write(options.titles);
+		}
+		if(options.underline.trimRight() != '') {
+			console.crlf();
+			write(options.underline);
+		}
+		if(multicolumn) {
+			write(options.multicolumn_separator);
+			if (options.underline.trimRight() != '')
+				write(options.underline);
+		}
+		console.crlf();
+		
+		// n is the number of items for the 1st column
+		var n;
+		if (multicolumn) {
+			n = Math.floor(menuitemsfiltered.length / 2) + (menuitemsfiltered.length & 1);
+		} else {
+			n = menuitemsfiltered.length;
+		}
+		
+		// j is the index for each menu item on 2nd column
+		var j = n; // start j at the first item for 2nd column
+		for (i = 0; i < n && !console.aborted; i++) {
+			console.add_hotspot(menuitemsfiltered[i].input.toString());
+			
+			validkeys.push(menuitemsfiltered[i].input.toString());
+
+			if (menuitemsfiltered[i].input > keymax) {
+				keymax = menuitemsfiltered[i].input;
+			}
+			
+			if (i == selected_index) {
+				if (menuitemsfiltered[i].type == 'quit') {
+					printf(multicolumn ? return_multicolumn_fmt_inverse : return_singlecolumn_fmt_inverse,
+						menuitemsfiltered[i].input.toString().toUpperCase(),
+						menuitemsfiltered[i].title,
+						''
+					);
+				} else {
+					printf(multicolumn ? options.multicolumn_fmt_inverse : options.singlecolumn_fmt_inverse,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title
+					);
+				}
+			} else {
+				if (menuitemsfiltered[i].type == 'quit') {
+					printf(multicolumn ? return_multicolumn_fmt : return_singlecolumn_fmt,
+						menuitemsfiltered[i].input.toString().toUpperCase(),
+						menuitemsfiltered[i].title,
+						''
+					);
+				} else {
+					printf(multicolumn ? options.multicolumn_fmt : options.singlecolumn_fmt,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title
+					);
+				}
+			}
+			
+			if (multicolumn) {
+				if (typeof menuitemsfiltered[j] !== "undefined") {
+					validkeys.push(menuitemsfiltered[j].input.toString());
+					if (menuitemsfiltered[j].input > keymax) {
+						keymax = menuitemsfiltered[j].input;
+					}
+					
+					write(options.multicolumn_separator);
+					console.add_hotspot(menuitemsfiltered[j].input.toString());
+					
+					if (j == selected_index) {
+						if (menuitemsfiltered[j].type == 'quit') {
+							printf(multicolumn ? return_multicolumn_fmt_inverse : return_singlecolumn_fmt_inverse,
+								menuitemsfiltered[j].input.toString().toUpperCase(),
+								menuitemsfiltered[j].title,
+								''
+							);
+						} else {
+							printf(multicolumn ? options.multicolumn_fmt_inverse : options.singlecolumn_fmt_inverse,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title
+							);
+						}
+					} else {
+						if (menuitemsfiltered[j].type == 'quit') {
+							printf(multicolumn ? return_multicolumn_fmt : return_singlecolumn_fmt,
+								menuitemsfiltered[j].input.toString().toUpperCase(),
+								menuitemsfiltered[j].title,
+								''
+							);
+						} else {
+							printf(multicolumn ? options.multicolumn_fmt : options.singlecolumn_fmt,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title
+							);
+						}
+					}
+				} else {
+					write(options.multicolumn_separator);
+				}
+				j++;
+			}
+			console.crlf();
+		}
+		
+		if (gamesrv) {
+			if (!bbs.menu("xtrn_gamesrv_tail_" + menuid, P_NOERROR)) {
+				bbs.menu("xtrn_gamesrv_tail", P_NOERROR);
+			}
+		}
+		
+		if (!bbs.menu("xtrnmenu_tail_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_tail", P_NOERROR)) {
+			bbs.menu("xtrn_tail", P_NOERROR);
+		}
+		
+		console.crlf();
+		writeln(options.custom.searchagainmsg !== undefined ? options.custom.searchagainmsg : 
+			"\x01n\x01mPress S to Search Again.")
+		
+		bbs.node_sync();
+		console.mnemonics(options.which);
+		
+		validkeys.push('q');
+		validkeys.push('s');
+		validkeys.push("\x1B");
+		validkeys.push(KEY_LEFT);
+		validkeys.push(KEY_RIGHT);
+		validkeys.push(KEY_UP);
+		validkeys.push(KEY_DOWN);
+		validkeys.push("\x0d"); //enter
+		
+		var keyin, keyin2;
+		var maxkeylen = 0;
+		var maxfirstkey = 0;
+		var morekeys = [];
+		var k;
+		for (k in validkeys) {
+			if (validkeys[k] == "\x1B") {
+				continue;
+			}
+			if (validkeys[k].length > maxkeylen) {
+				maxkeylen = validkeys[k].length;
+			}
+			if (validkeys[k].length > 1) {
+				morekeys.push(validkeys[k].toString().toLowerCase().substring(0,1));
+			}
+		}
+		
+		// get first key
+		keyin = console.getkey();
+		keyin = keyin.toString().toLowerCase();
+		
+		// The logic below is to make it not require enter for as
+		// many items as possible
+		
+		// if ENTER on a QUIT item, exit
+		if (keyin == "\x0d") {
+			if (menuitemsfiltered[selected_index].type == "quit") {
+				if (gamesrv && (menuid == 'main')) {
+					bbs.logoff();
+				} else {
+					console.clear();
+					return;
+				}
+			}
+		}
+		
+		if ((keyin == 'q') || (keyin == "\x1B")) {
+			console.clear();
+			return;
+		} else if (keyin == KEY_UP) {
+			--selected_index;
+			if (selected_index < 0) {
+				selected_index = menuitemsfiltered.length - 1;
+			}
+			continue;
+		} else if (keyin == KEY_DOWN) {
+			++selected_index;
+			if (selected_index > (menuitemsfiltered.length - 1)) {
+				selected_index = 0;
+			}
+			continue;
+		} else if (keyin == KEY_LEFT) {
+			var row_limit = n -1;
+			if (selected_index == 0) {
+				// first item, go back menu
+				if (gamesrv && (menuid == 'main')) {
+					bbs.logoff();
+				} else {
+					console.clear();
+					return;
+				}
+			} else if (selected_index <= row_limit) {
+				// on first col
+				var new_selected_index = selected_index + n;
+				if (typeof menuitemsfiltered[new_selected_index] === "undefined") {
+					// no item on right, no change
+				} else {
+					selected_index = new_selected_index;
+				}
+			} else {
+				// on second col
+				selected_index = selected_index - n;
+			}
+		} else if (keyin == KEY_RIGHT) {
+			var row_limit = n - 1;
+			if (selected_index <= row_limit) {
+				// on first col
+				var new_selected_index = selected_index + n;
+				if (typeof menuitemsfiltered[new_selected_index] === "undefined") {
+					// no item on right, no change
+				} else {
+					selected_index = new_selected_index;
+				}
+			} else {
+				// on second col
+				selected_index = selected_index - n;
+			}
+		}
+		
+		// if max keys is 2 and they entered something that might have
+		// a second digit/char, then get the key
+		if ((maxkeylen == 2) && (keyin !== "\x0d")) {
+			if (morekeys.indexOf(keyin) !== -1) {
+				write(keyin);
+				keyin2 = console.getkey(); // either the second digit or enter
+				if ((keyin2 !== "\r") && (keyin2 !== "\n") && (keyin2 !== "\r\n")) {
+					keyin = keyin + keyin2.toLowerCase();
+				}
+			}
+		} else if ((maxkeylen > 2) && (keyin !== "\x0d")) {
+			// there there are more than 99 items, then just use getkeys 
+			// for the rest
+			write(keyin);
+			keyin2 = console.getkeys(validkeys, keymax);
+			keyin = keyin + keyin2.toLowerCase();
+		}
+		
+		if (keyin) {
+			if (keyin == 's') {
+				console.crlf();
+				console.crlf();
+				write(typeof options.custom.entersearchterm !== "undefined" ?
+					options.custom.entersearchterm : "Enter search term: ");
+				var searchterm = console.getstr(searchterm, 40,
+						K_LINE | K_EDIT | K_NOEXASC | K_TRIM);
+			} else {
+				menuitemsfiltered.some(function (menuitemfiltered) {
+					var menutarget = menuitemfiltered.target.toLowerCase();
+					var menuinput = menuitemfiltered.input.toString().toLowerCase();
+					
+					if ((menuinput == keyin) ||
+						((keyin == "\x0d") && (menuitemsfiltered[selected_index].input == menuitemfiltered.input))) {
+						switch (menuitemfiltered.type) {
+							// custom menu, defined in xtrnmenu.json
+							case 'custommenu':
+								external_menu_custom(menutarget);
+								return true;
+							// external program section
+							case 'xtrnmenu':
+								if (options.use_xtrn_sec) {
+									js.exec("xtrn_sec.js", {}, menutarget);
+								} else {
+									external_menu_custom(menutarget);
+								}
+								return true;
+							// external program
+							case 'xtrnprog':
+								// run the external program
+								if (typeof xtrn_area.prog[menutarget] !== "undefined") {
+									bbs.exec_xtrn(menutarget);
+									return true;
+								} else {
+									doerror(options.custom_menu_program_not_found_msg.replace('%PROGRAMID%', menutarget));
+								}
+								break;
+						}
+					} // if menu item matched keyin
+				}); // foreach menu item
+			}
+		} // if keyin
+	} // main bbs.online loop
+}
+
+// Return the favorites menu
+function favorites_menu(title, itemcount) {
+	if (itemcount === undefined) {
+		itemcount = 0;
+	}
+	
+	var i, menucheck, item_multicolumn_fmt, item_singlecolumn_fmt,
+		item_multicolumn_fmt_inverse, item_singlecolumn_fmt_inverse,
+		multicolumn, selected_index = 0, menuitemsfiltered = [];
+	var validkeys = []; // valid chars on menu selection
+	var keymax = 0; // max integer allowed on menu selection
+	
+	var menuid = 'favorites';
+	
+	var options = ExternalMenus.getOptions('custommenu', menuid);
+	
+	// Allow overriding auto-format on a per-menu basis
+	var multicolumn_fmt = options.multicolumn_fmt;
+	var singlecolumn_fmt = options.singlecolumn_fmt;
+	var multicolumn_fmt_inverse = options.multicolumn_fmt_inverse;
+	var singlecolumn_fmt_inverse = options.singlecolumn_fmt_inverse;
+	
+	var return_singlecolumn_fmt = options.custom.return_singlecolumn_fmt !== undefined
+		? options.custom.return_singlecolumn_fmt : singlecolumn_fmt;
+	var return_multicolumn_fmt = options.custom.return_multicolumn_fmt !== undefined
+		? options.custom.return_multicolumn_fmt : multicolumn_fmt;
+	var return_singlecolumn_fmt_inverse = options.custom.return_singlecolumn_fmt_inverse !== undefined
+		? options.custom.return_singlecolumn_fmt_inverse : singlecolumn_fmt_inverse;
+	var return_multicolumn_fmt_inverse = options.custom.return_multicolumn_fmt_inverse !== undefined
+		? options.custom.return_multicolumn_fmt_inverse : multicolumn_fmt_inverse;
+	
+	while (bbs.online) {
+		console.aborted = false;
+		
+		if (options.clear_screen) {
+			console.clear(LIGHTGRAY);
+		}
+		
+		if (user.security.restrictions&UFLAG_X) {
+			write(options.restricted_user_msg);
+			break;
+		}
+		
+		var menuobj = ExternalMenus.getFavorites(title, itemcount);
+		if (!bbs.menu("xtrnmenu_head_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_head", P_NOERROR)) {
+			bbs.menu("xtrn_head", P_NOERROR);
+		}
+		
+		menuitemsfiltered = typeof menuobj.items !== "undefined" ? menuobj.items : {};
+		
+		// The quit item is intended to aid in the lightbar navigation
+		menuitemsfiltered.unshift({
+			input: 'Q',
+			title: options.custom.return_msg !== undefined ? options.custom.return_msg : 'Return to Previous Menu',
+			target: '',
+			type: 'quit'
+		});
+		
+		menuitemsfiltered.push({
+			input: '+',
+			title: options.custom.favorite_add_item !== undefined ? options.custom.favorite_add_item : 'Add Item',
+			target: '',
+			type: 'add'
+		});
+		
+		menuitemsfiltered.push({
+			input: '-',
+			title: options.custom.favorite_remove_item !== undefined ? options.custom.favorite_remove_item : 'Remove Item',
+			target: '',
+			type: 'remove'
+		});
+		
+		// determine lines left. we can't know the size of the footer so 
+		// let the sysop use singlecolumn_margin to specify that. Below
+		// calcution will always leave room for titles and underline even
+		// if they aren't rendered
+		var linesleft = console.screen_rows - console.line_counter - options.singlecolumn_margin
+			- 2 - 2; // -2 for header_fmt/crlf and -2 for crlf and footer
+		if(options.titles.trimRight() != '') linesleft = linesleft - 1;
+		if(options.underline.trimRight() != '') linesleft = linesleft - 2;
+		
+		multicolumn = menuitemsfiltered.length  > linesleft;
+		
+		printf(options.header_fmt, title);
+		if(options.titles.trimRight() != '')
+			write(options.titles);
+		if(multicolumn) {
+			write(options.multicolumn_separator);
+			if (options.titles.trimRight() != '')
+				write(options.titles);
+		}
+		if(options.underline.trimRight() != '') {
+			console.crlf();
+			write(options.underline);
+		}
+		if(multicolumn) {
+			write(options.multicolumn_separator);
+			if (options.underline.trimRight() != '')
+				write(options.underline);
+		}
+		console.crlf();
+		
+		// n is the number of items for the 1st column
+		var n;
+		if (multicolumn) {
+			n = Math.floor(menuitemsfiltered.length / 2) + (menuitemsfiltered.length & 1);
+		} else {
+			n = menuitemsfiltered.length;
+		}
+		
+		// j is the index for each menu item on 2nd column
+		var j = n; // start j at the first item for 2nd column
+		for (i = 0; i < n && !console.aborted; i++) {
+			console.add_hotspot(menuitemsfiltered[i].input.toString());
+			
+			validkeys.push(menuitemsfiltered[i].input.toString());
+			
+			if (menuitemsfiltered[i].input > keymax) {
+				keymax = menuitemsfiltered[i].input;
+			}
+			
+			if (i == selected_index) {
+				if (menuitemsfiltered[i].type == 'quit') {
+					printf(multicolumn ? return_multicolumn_fmt_inverse : return_singlecolumn_fmt_inverse,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title
+					);
+				} else {
+					printf(multicolumn ? multicolumn_fmt_inverse : singlecolumn_fmt_inverse,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title
+					);
+				}
+			} else {
+				if (menuitemsfiltered[i].type == 'quit') {
+					printf(multicolumn ? return_multicolumn_fmt : return_singlecolumn_fmt,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title
+					);
+				} else {
+					printf(multicolumn ? multicolumn_fmt : singlecolumn_fmt,
+						menuitemsfiltered[i].input,
+						menuitemsfiltered[i].title
+					);
+				}
+			}
+			
+			if (multicolumn) {
+				if (typeof menuitemsfiltered[j] !== "undefined") {
+					validkeys.push(menuitemsfiltered[j].input.toString());
+					
+					if (menuitemsfiltered[j].input > keymax) {
+						keymax = menuitemsfiltered[i].input;
+					}
+					
+					write(options.multicolumn_separator);
+					console.add_hotspot(menuitemsfiltered[j].input.toString());
+					
+					if (selected_index == j) {
+						if (menuitemsfiltered[j].type == 'quit') {
+							printf(multicolumn ? return_multicolumn_fmt_inverse : return_singlecolumn_fmt_inverse,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title
+							);
+						} else {
+							printf(multicolumn ? multicolumn_fmt_inverse : singlecolumn_fmt_inverse,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title
+							);
+						}
+					} else {
+						if (menuitemsfiltered[j].type == 'quit') {
+							printf(multicolumn ? return_multicolumn_fmt : return_singlecolumn_fmt,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title
+							);
+						} else {
+							printf(multicolumn ? multicolumn_fmt : singlecolumn_fmt,
+								menuitemsfiltered[j].input,
+								menuitemsfiltered[j].title
+							);
+						}
+					}
+				} else {
+					write(options.multicolumn_separator);
+				}
+				j++;
+			}
+			console.crlf();
+		}
+		
+		if (gamesrv) {
+			if (!bbs.menu("xtrn_gamesrv_tail_" + menuid, P_NOERROR)) {
+				bbs.menu("xtrn_gamesrv_tail", P_NOERROR);
+			}
+		}
+		
+		if (!bbs.menu("xtrnmenu_tail_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_tail", P_NOERROR)) {
+			bbs.menu("xtrn_tail", P_NOERROR);
+		}
+		
+		bbs.node_sync();
+		console.mnemonics(options.which);
+		
+		validkeys.push('q');
+		validkeys.push("\x1B");
+		validkeys.push(KEY_LEFT);
+		validkeys.push(KEY_RIGHT);
+		validkeys.push(KEY_UP);
+		validkeys.push(KEY_DOWN);
+		validkeys.push("\x0d"); //enter
+		
+		var keyin, keyin2;
+		var maxkeylen = 0;
+		var maxfirstkey = 0;
+		var morekeys = [];
+		var k;
+		for (k in validkeys) {
+			if (validkeys[k] == "\x1B") {
+				continue;
+			}
+			if (validkeys[k].length > maxkeylen) {
+				maxkeylen = validkeys[k].length;
+			}
+			if (validkeys[k].length > 1) {
+				morekeys.push(validkeys[k].toString().toLowerCase().substring(0,1));
+			}
+		}
+		
+		// get first key
+		keyin = console.getkey();
+		keyin = keyin.toString().toLowerCase();
+		
+		// if ENTER 
+		if (keyin == "\x0d") {
+			if (menuitemsfiltered[selected_index].type == "quit") {
+				console.clear();
+				return;
+			} else if (menuitemsfiltered[selected_index].type == "add") {
+				add_favorite();
+			} else if (menuitemsfiltered[selected_index].type == "remove") {
+				remove_favorite();
+			}
+		} else if ((keyin == 'q') || (keyin == "\x1B")) {
+			console.clear();
+			return;
+		} else if (keyin == KEY_UP) {
+			--selected_index;
+			if (selected_index < 0) {
+				selected_index = menuitemsfiltered.length - 1;
+			}
+			continue;
+		} else if (keyin == KEY_DOWN) {
+			++selected_index;
+			if (selected_index > (menuitemsfiltered.length - 1)) {
+				selected_index = 0;
+			}
+			continue;
+		} else if (keyin == KEY_LEFT) {
+			var row_limit = n -1;
+			if (selected_index == 0) {
+				console.clear();
+				return;
+			} else if (selected_index <= row_limit) {
+				// on first col
+				var new_selected_index = selected_index + n;
+				if (typeof menuitemsfiltered[new_selected_index] === "undefined") {
+					// no item on right, no change
+				} else {
+					selected_index = new_selected_index;
+				}
+			} else {
+				// on second col
+				selected_index = selected_index - n;
+			}
+		} else if (keyin == KEY_RIGHT) {
+			var row_limit = n - 1;
+			if (selected_index <= row_limit) {
+				// on first col
+				var new_selected_index = selected_index + n;
+				if (typeof menuitemsfiltered[new_selected_index] === "undefined") {
+					// no item on right, no change
+				} else {
+					selected_index = new_selected_index;
+				}
+			} else {
+				// on second col
+				selected_index = selected_index - n;
+			}
+		} else if ((maxkeylen == 2) && (keyin !== "\x0d")) {
+			// The logic below is to make it not require enter for as
+			// many items as possible
+			
+			// if max keys is 2 and they entered something that might have
+			// a second digit/char, then get the key
+			if (morekeys.indexOf(keyin) !== -1) {
+				write(keyin);
+				keyin2 = console.getkey(); // either the second digit or enter
+				if ((keyin2 !== "\r") && (keyin2 !== "\n") && (keyin2 !== "\r\n")) {
+					keyin = keyin + keyin2.toLowerCase();
+				}
+			}
+		} else if ((maxkeylen > 2) && (keyin !== "\x0d")) {
+			// there there are more than 99 items, then just use getkeys 
+			// for the rest
+			write(keyin);
+			keyin2 = console.getkeys(validkeys, keymax);
+			keyin = keyin + keyin2.toLowerCase();
+		}
+		
+		if (keyin) {
+			if ((keyin == '+') || (keyin == '=')) {
+				add_favorite();
+			} else if ((keyin == "\x08") || (keyin == "\x7f")) {
+				// delete
+				if (menuitemsfiltered[selected_index].type == 'xtrnprog') {
+					console.crlf();
+					if (!console.noyes("Do you wish to remove " +
+						menuitemsfiltered[selected_index].title)) {
+						remove_favorite(menuitemsfiltered[selected_index].target.toLowerCase());
+					}
+				}
+				
+			} else if ((keyin == '-') || (keyin == '_')) {
+				remove_favorite();
+			} else {
+				for (var d in menuitemsfiltered) {
+					var menutarget = menuitemsfiltered[d].target.toLowerCase();
+					var menuinput = menuitemsfiltered[d].input.toString().toLowerCase();
+					if ((menuinput == keyin) ||
+						((keyin == "\x0d") && (menuitemsfiltered[selected_index].input == menuitemsfiltered[d].input))) {
+						// everything in this menu is an xtrnprog
+						
+						// reset selected index, because the menus are dynamic and
+						// the order may change between executions
+						selected_index = 0;
+						
+						// run the external program
+						if (typeof xtrn_area.prog[menutarget] !== "undefined") {
+							bbs.exec_xtrn(menutarget);
+						} else {
+							doerror(options.custom_menu_program_not_found_msg.replace('%PROGRAMID%', menutarget));
+						}
+					} // if menu item matched keyin
+				}
+			}
+		} // if keyin
+	} // main bbs.online loop
+}
+
+// Add an entry to the favorites menu
+function add_favorite()
+{
+	require("frame.js", "Frame");
+	require("tree.js", "Tree");
+	require("scrollbar.js", "ScrollBar");
+	require('typeahead.js', 'Typeahead');
+	load(js.global, 'cga_defs.js');
+	
+	var options = ExternalMenus.getOptions('custommenu', 'favorites');
+	
+	if (!options.json_enabled) {
+		log(LOG_DEBUG, "xtrnmenulib: Skipping favorites because JSON is not enabled");
+		return false;
+	}
+	
+	try {
+		require("json-client.js", "JSONClient");
+		var jsonClient = new JSONClient(options.json_host, options.json_port);
+		jsonClient.callback = ExternalMenus.processUpdate;
+	} catch (e) {
+		log(LOG_ERR, "xtrnmenu add_favorites: Could not initialize JSON database so favorites is now disabled: " + e);
+		return false;
+	}
+	
+	var sortedItems = [];
+	var jsonData = jsonClient.read("xtrnmenu", "favorites_" + user.alias, 1);
+	
+	if (!jsonData) {
+		jsonData = [];
+	}
+	
+	var frame = new Frame(1, 1, console.screen_columns, console.screen_rows, WHITE);
+	var treeframe = new Frame(frame.x, frame.y+4, frame.width, frame.height-5, WHITE);
+	var tree = new Tree(treeframe);
+	var s = new ScrollBar(tree);
+	frame.open();
+	treeframe.open();
+	
+	frame.putmsg(options.custom.add_favorites_msg !== undefined 
+		? options.custom.add_favorites_msg : "\x01c\x01hAdd Favorite");
+	frame.gotoxy(1, 3);
+	frame.putmsg(options.custom.favorites_inst !== undefined
+		? options.custom.favorites_inst : '\x01n\x01w\x01h\x012 [Up/Down/Home/End] to Navigate, [Enter] to Select, [Q] to Quit, [S] to Search ');
+	
+	var xtrnwidth = 0;
+	var sortedItems = [];
+	for (var m in xtrn_area.prog) {
+		if (xtrn_area.prog[m].can_access) {
+			var found = false;
+			jsonData.some(function (jsonxtrn) {
+				if (jsonxtrn == xtrn_area.prog[m].code) {
+					found = true;
+					return true;
+				}
+			});
+			if (!found) {
+				sortedItems.push({ code: xtrn_area.prog[m].code, name: xtrn_area.prog[m].name })
+				if (xtrn_area.prog[m].name.length > xtrnwidth) {
+					xtrnwidth = xtrn_area.prog[m].name.length;
+				}
+			}
+		}
+	}
+	
+	sortedItems.sort(function(a, b) {
+			if(a.name.toLowerCase()>b.name.toLowerCase()) return 1;
+			if(a.name.toLowerCase()<b.name.toLowerCase()) return -1;
+			return 0;
+		});
+	
+	for (m in sortedItems) {
+		const item = tree.addItem(sortedItems[m].name, sortedItems[m].code);
+		item.code = sortedItems[m].code;
+	}
+	
+	treeframe.width = xtrnwidth + 2;
+
+	// lightbar non-current item
+	tree.colors.fg = options.custom.favorite_add_fg 
+		? js.global[options.custom.favorite_add_fg] : LIGHTGRAY;
+	tree.colors.bg = options.custom.favorite_add_bg 
+		? js.global[options.custom.favorite_add_bg] : BG_BLACK;
+	// lightbar current item
+	tree.colors.lfg = options.custom.favorite_add_lfg 
+		? js.global[options.custom.favorite_add_lfg] : WHITE;
+	tree.colors.lbg = options.custom.favorite_add_lbg
+		? js.global[options.custom.favorite_add_lbg] : BG_MAGENTA;
+	// tree heading
+	tree.colors.cfg = options.custom.favorite_add_cfg 
+		? js.global[options.custom.favorite_add_cfg] : WHITE;
+	tree.colors.cbg = options.custom.favorite_add_cbg 
+		? js.global[options.custom.favorite_add_cbg] : BG_BLACK;
+	// other tree color settings not applicable to this implementation
+	tree.open();
+	
+	console.clear(BG_BLACK|LIGHTGRAY);
+	
+	frame.draw();
+	treeframe.draw();
+	
+	var selection;
+	var key;
+	var xtrn;
+	while(bbs.online) {
+		key = console.getkey(K_NOSPIN);
+		if (key == "\x0d") {
+			// hit enter, item is selected
+			break;
+		} else if ((key.toLowerCase() == 'q') || (key == "\x1B")) {
+			return;
+		} else if (key.toLowerCase() == 's') {
+			xtrn = add_favorite_search(frame, treeframe, options);
+			break;
+		} else {
+			selection = tree.getcmd({key: key, mouse: false});
+			
+			if (key == KEY_UP || key == KEY_DOWN || key == KEY_HOME || key == KEY_END) {
+				if ((key == KEY_UP) && (tree.line == 1)) {
+					// pressed up on first item, go to end
+					tree.end();
+					tree.refresh(); // fixes itself with it going to next to last item
+				} else if ((key == KEY_DOWN) && (tree.line == sortedItems.length)) {
+					// pressed down on last item, go to start
+					tree.home();
+					tree.refresh(); // fixes issue with it going to 2nd item
+				}
+				xtrn = tree.currentItem;
+			}
+		}
+		frame.cycle();
+		treeframe.cycle();
+		s.cycle();
+	}
+
+	if (typeof xtrn.code !== "undefined") {
+		jsonData.push(xtrn.code);
+		jsonClient.write("xtrnmenu", "favorites_" + user.alias, jsonData, 2);
+	}
+}
+
+// Type ahead search for add favorite
+function add_favorite_search(frame, treeframe, options) {
+	require("frame.js", "Frame");
+	require("tree.js", "Tree");
+	require('typeahead.js', 'Typeahead');
+	load(js.global, 'cga_defs.js');
+	
+	var sframe = new Frame(
+		1,
+		1,
+		console.screen_columns,
+		console.screen_rows,
+		LIGHTGRAY,
+		frame
+	);
+	sframe.open();
+	
+	const typeahead = new Typeahead({
+		x : 1,
+		y : 1,
+		bg : options.custom.favorite_add_search_fg 
+			? js.global[options.custom.favorite_add_search_fg] : LIGHTGRAY,
+		fg: options.custom.favorite_add_search_bg 
+			? js.global[options.custom.favorite_add_search_bg] : BG_BLACK,
+		sfg: options.custom.favorite_add_search_sfg 
+			? js.global[options.custom.favorite_add_search_sfg] : LIGHTGRAY,
+		sbg: options.custom.favorite_add_search_sbg 
+			? js.global[options.custom.favorite_add_search_sbg] : BG_BLACK,
+		hsfg: options.custom.favorite_add_search_hsfg 
+			? js.global[options.custom.favorite_add_search_hsfg] : WHITE,
+		hsbg: options.custom.favorite_add_search_hsbg 
+			? js.global[options.custom.favorite_add_search_hsbg] : BG_MAGENTA,
+		prompt: options.custom.favorite_add_search_prompt 
+			? options.custom.favorite_add_search_prompt : "\x01c\x01hSearch (ESC to Cancel): \x01n",
+		len: treeframe.width,
+		frame: sframe,
+		datasources: [
+			function (searchterm) {
+				const ret = [];
+				
+				for (i in xtrn_area.sec_list) {
+					for (var k in xtrn_area.sec_list[i].prog_list) {
+						if (xtrn_area.sec_list[i].prog_list[k].name.toLowerCase().indexOf(searchterm.toLowerCase()) !== -1) {
+							if (xtrn_area.sec_list[i].prog_list[k].can_access) {
+								ret.push({
+									'code': xtrn_area.sec_list[i].prog_list[k].code,
+									'text': xtrn_area.sec_list[i].prog_list[k].name,
+								});
+							}
+						}
+					}
+				}
+				return ret;
+			}
+		]
+	});
+	
+	var user_input = undefined;
+	while (typeof user_input !== 'object') {
+		var key = console.inkey(K_NONE, 5);
+		if (key == "\x1B") {
+			typeahead.close();
+			sframe.close();
+			return false;
+		}
+		
+		user_input = typeahead.inkey(key);
+		typeahead.cycle();
+		if (sframe.cycle()) typeahead.updateCursor();
+	}
+	
+	typeahead.close();
+	sframe.close();
+	return user_input;
+}
+
+// Remove an entry from the favorites menu
+function remove_favorite(xtrncode)
+{
+	var options = ExternalMenus.getOptions('custommenu', 'favorites');
+	
+	if (!options.json_enabled) {
+		log(LOG_DEBUG, "xtrnmenulib: Skipping favorites because JSON is not enabled");
+		return false;
+	}
+	
+	try {
+		require("json-client.js", "JSONClient");
+		var jsonClient = new JSONClient(options.json_host, options.json_port);
+		jsonClient.callback = ExternalMenus.processUpdate;
+	} catch (e) {
+		log(LOG_ERR, "xtrnmenu remove_favorites: Could not initialize JSON database so favorites is now disabled: " + e);
+		return false;
+	}
+	
+	var sortedItems = [];
+	var jsonData = jsonClient.read("xtrnmenu", "favorites_" + user.alias, 1);
+	
+	if (!jsonData) {
+		jsonData = [];
+	}
+	
+	if ((typeof xtrncode === "undefined") || (!xtrncode)) {
+		require("frame.js", "Frame");
+		require("tree.js", "Tree");
+		require("scrollbar.js", "ScrollBar");
+		
+		var frame = new Frame(1, 1, console.screen_columns, 4, WHITE);
+		var treeframe = new Frame(1, 5, console.screen_columns, console.screen_rows - 5, WHITE);
+		var tree = new Tree(treeframe);
+		var s = new ScrollBar(tree);
+		frame.open();
+		treeframe.open();
+		
+		frame.putmsg(options.custom.remove_favorites_msg !== undefined
+			? options.custom.remove_favorites_msg : "\x01c\x01hRemove Favorite");
+		frame.gotoxy(1, 3);
+		frame.putmsg(options.custom.favorites_inst_rem !== undefined
+			? options.custom.favorites_inst_rem : '\x01n\x01w\x01h\x012 [Up/Down/Home/End] to Navigate, [Enter] to Select, [Q] to Quit ');
+		
+		var xtrnwidth = 0;
+		var sortedItems = [];
+		for (var m in xtrn_area.prog) {
+			if (xtrn_area.prog[m].can_access) {
+				var found = false;
+				jsonData.some(function (jsonxtrn) {
+					if (jsonxtrn == xtrn_area.prog[m].code) {
+						found = true;
+						return true;
+					}
+				});
+				if (found) {
+					sortedItems.push({code: xtrn_area.prog[m].code, name: xtrn_area.prog[m].name})
+					if (xtrn_area.prog[m].name.length > xtrnwidth) {
+						xtrnwidth = xtrn_area.prog[m].name.length;
+					}
+				}
+			}
+		}
+		
+		sortedItems.sort(function (a, b) {
+			if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
+			if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
+			return 0;
+		});
+		
+		for (m in sortedItems) {
+			const item = tree.addItem(sortedItems[m].name, sortedItems[m].code);
+			item.code = sortedItems[m].code;
+		}
+		
+		treeframe.width = xtrnwidth + 2;
+		
+		tree.colors.fg = LIGHTGRAY;
+		tree.colors.bg = BG_BLACK;
+		tree.colors.lfg = WHITE;
+		tree.colors.lbg = BG_MAGENTA;
+		tree.colors.kfg = YELLOW;
+		
+		tree.open();
+		
+		console.clear(BG_BLACK | LIGHTGRAY);
+		
+		frame.draw();
+		treeframe.draw();
+		
+		var selection;
+		var key;
+		var xtrn;
+		while (bbs.online) {
+			key = console.getkey();
+			if (key == "\x0d") {
+				// hit enter, item is selected
+				break;
+			}
+			if ((key.toLowerCase() == 'q') || (key == "\x1B")) return;
+			
+			selection = tree.getcmd({key: key, mouse: false});
+			
+			if (key == KEY_UP || key == KEY_DOWN || key == KEY_HOME || key == KEY_END) {
+				if ((key == KEY_UP) && (tree.line == 1)) {
+					// pressed up on first item, go to end
+					tree.end();
+					tree.refresh(); // fixes itself with it going to next to last item
+				} else if ((key == KEY_DOWN) && (tree.line == sortedItems.length)) {
+					// pressed down on last item, go to start
+					tree.home();
+					tree.refresh(); // fixes issue with it going to 2nd item
+				}
+				xtrn = tree.currentItem;
+				xtrncode = xtrn.code;
+			}
+			treeframe.cycle();
+			s.cycle();
+		}
+	}
+	
+	var newJsonData = [];
+	jsonData.forEach(function (jsonxtrn) {
+		if (jsonxtrn != xtrncode) {
+			newJsonData.push(jsonxtrn);
+		}
+	});
+	
+	jsonClient.write("xtrnmenu", "favorites_" + user.alias, newJsonData, 2);
+}
+
 // Display error message to console and save to log
 function doerror(msg)
 {
diff --git a/exec/xtrnmenucfg.js b/exec/xtrnmenucfg.js
index db77a0d5076c1052be6de9c456a2a2905b43b444..531018e90d807385e02a4b6d675f98501ac2d973 100644
--- a/exec/xtrnmenucfg.js
+++ b/exec/xtrnmenucfg.js
@@ -72,7 +72,7 @@ var editMenu = function(menuid) {
     }
 
     while(1) {
-        uifc.help_text = word_wrap("This screen allows you to edit the configuration options for the custom menu.\r\n\r\nMost options default or are set in modopts.ini, but here you can define them on a per-menu basis.\r\n\r\nClick Edit Items to edit the individual entries (programs, menus, etc.)");
+        uifc.help_text = word_wrap("This screen allows you to edit the configuration options for the custom menu.\r\n\r\nMost options default or are set in modopts.ini, but here you can define them on a per-menu basis.\r\n\r\nClick Edit Items to edit the individual entries (programs, menus, etc.)", 72);
 
         selections = [];
         for (var j in menu.items) {
@@ -110,7 +110,7 @@ var editMenu = function(menuid) {
 
         switch (editproperty) {
             case 'id':
-                uifc.help_text = word_wrap("This is a unique ID for the menu, which can be used as the target to\r\ncall the menu from other menus.\r\n\r\nFor the top-level menu, the id should be 'main'.");
+                uifc.help_text = word_wrap("This is a unique ID for the menu, which can be used as the target to\r\ncall the menu from other menus.\r\n\r\nFor the top-level menu, the id should be 'main'.", 72);
                 var selection2 = uifc.input(WIN_MID, "Menu ID", menu.id, 50, K_EDIT);
                 if ((selection2 < 0) || (selection2 == null)) {
                     // escape key
@@ -129,7 +129,7 @@ var editMenu = function(menuid) {
                 break;
 
             case 'sort_type':
-                uifc.help_text = word_wrap("How to sort the menu:\r\nby input key\r\nby title\r\nnone (the items remain in the order they are in the config)");
+                uifc.help_text = word_wrap("How to sort the menu:\r\nby input key\r\nby title\r\nnone (the items remain in the order they are in the config)", 72);
 
                 switch (uifc.list(WIN_ORG | WIN_MID, "Sort Type", ["key", "title", "none"])) {
                     case 0:
@@ -183,47 +183,37 @@ var editItems = function(menuid) {
         }
     }
 
+    /* commented out as it prevents pasting into new menu
     if ((typeof menu.items == "undefined") || (menu.items.length == 0)) {
         // no items, prompt them to make one
         editItem(menu.id, 0);
     }
-
-
+     */
+    
     uifc.help_text = word_wrap("This menu allows editing the various items in this menu.\r\n\r\n"
         + "If you leave input key blank, it will use an auto-generated number at display time.\r\n\r\n"
-        + "Choose a type first and the dropdown to choose tha target will allow you to select your target.\r\n\r\n"
-        + "Access string only applies to custom menu items. For external sections or external programs, use the access settings in scfg.\r\n\r\n");
+        + "Choose a type first and the dropdown to choose the target will allow you to select your target.\r\n\r\n"
+        + "Access string only applies to custom menu items, commands, and special menus. For external sections or external programs, use the access settings in scfg.\r\n\r\n",
+        72);
 
     while(1) {
         items = [];
         itemids = [];
         for(i in menu.items) {
             items.push(format(
-                "%6s %10s %s",
+                "%6s %16s %s",
                 menu.items[i].input ? menu.items[i].input : '(auto)',
                 menu.items[i].type,
                 menu.items[i].title
             ));
             itemids.push(i);
         }
-        // WIN_ORG = original menu
-        // WIN_MID = centered mid
-        // WIN_ACT = menu remains active after a selection
-        // WIN_ESC = screen is active when escape is pressed
-        // WIN_XTR = blank line to insert
-        // WIN_INS = insert key
-        // WIN_DEL = delete
-        // WIN_CUT = cut ctrl-x
-        // WIN_COPY = copy ctrl-c
-        // WIN_PUT = paste ctrl-v
-        // WIN_SAV = use context/save position
         selection = uifc.list(
-            WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC|WIN_XTR|WIN_INS|WIN_DEL|WIN_CUT|WIN_COPY|WIN_PASTE|WIN_SAV,
+            WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC|WIN_XTR|WIN_INS|WIN_DEL|WIN_CUT|WIN_COPY|WIN_PASTE|WIN_PASTEXTR|WIN_SAV,
             menu.title + ": Items",
             items,
             ctxm
         );
-
         if (selection == -1) {
             // esc key
             break;
@@ -270,7 +260,8 @@ var editItems = function(menuid) {
             if ("type" in copyitem) {
                 // if item already exists in list, modify if since you can't have dupes (except empty input keys)
                 for (var i in menu.items) {
-                    if ((menu.items[i].input == copyitem.input) && (copyitem.input !== null) && (copyitem.input !== "")) {
+                    if ((menu.items[i].input == copyitem.input) 
+                        && (copyitem.input !== null) && (copyitem.input !== "")) {
                         oktopaste = true;
                         while(1) {
                             selection2 = uifc.input(WIN_MID, "Enter New Input Key", "", 3, K_EDIT);
@@ -302,13 +293,20 @@ var editItems = function(menuid) {
                 }
                 if ((oktopaste) || (copyitem.input === "null") || (copyitem.input === "")) {
                     var menuitems2 = [];
+                    var pushed = false;
                     for (i in menu.items) {
-                        menuitems2.push(menu.items[i]);
-                        // paste copied item after selected item
+                        // paste copied item before selected item
                         if (i == itemids[selection]) {
                             menuitems2.push(copyitem);
                             ctxm.cur = i-1;
+                            pushed = true;
                         }
+                        menuitems2.push(menu.items[i]);
+                    }
+                    if (!pushed) {
+                        // add to end
+                        menuitems2.push(copyitem);
+                        ctxm.cur = menuitems2.length-1;
                     }
                     menu.items = menuitems2;
                 }
@@ -334,7 +332,7 @@ var editItem = function(menuid, itemindex) {
     var displayoptions = [], displayoptionids = [], newitems = [];
     // used for building target selection
     var custommenuitems = [], custommenuitemsids = [], custommenunames = [];
-
+    
     if (typeof menuid === "undefined") {
         uifc.msg("Menu could not be found");
         return;
@@ -349,12 +347,13 @@ var editItem = function(menuid, itemindex) {
 
     if (typeof menu.items[itemindex] === "undefined") {
         // new item
+        // 
         menu.items.push({
-            "input": null,
-            "title": "New Item " + time(),
-            "type": null,
-            "target": null,
-            "access_string": null,
+            input: null,
+            title: null,
+            type: null,
+            target: null,
+            access_string: null,
         });
         itemindex = menu.items.length - 1;
         present_select_targettype(menu.items[itemindex]);
@@ -379,21 +378,56 @@ var editItem = function(menuid, itemindex) {
             ("type" in item ? item.type : "")));
         displayoptionids.push("type");
 
-        displayoptions.push(format("%23s: %s", "target",
-            ("target" in item ? item.target : "")));
-        displayoptionids.push("target");
+        switch (item.type) {
+            case 'recentall':
+            case 'recentuser':
+            case 'mostlaunchedall':
+            case 'mostlauncheduser':
+            case 'longestrunall':
+            case 'longestrunuser':
+            case 'search':
+            case 'favorites':
+                displayoptions.push(format("%23s: %s", "count",
+                    ("target" in item ? item.target : "")));
+                displayoptionids.push("target");
+                break;
+            case 'custommenu':
+            case 'xtrnmenu':
+            case 'xtrnprog':
+            case 'command':
+            default:
+                displayoptions.push(format("%23s: %s", "target",
+                    ("target" in item ? item.target : "")));
+                displayoptionids.push("target");
+                break;
+        }		
 
-        if ((item.type == "custommenu") || (item.type == "command")) {
-            displayoptions.push(format("%23s: %s", "access_string",
-                ("access_string" in item ? item.access_string : "(default)")));
-            displayoptionids.push("access_string");
+        switch (item.type) {
+            case 'custommenu':
+            case 'command':
+            case 'recentall':
+            case 'recentuser':
+            case 'mostlaunchedall':
+            case 'mostlauncheduser':
+            case 'longestrunuser':
+            case 'longestrunall':
+            case 'search':
+            case 'favorites':
+                displayoptions.push(format("%23s: %s", "access_string",
+                    ("access_string" in item ? item.access_string : "(default)")));
+                displayoptionids.push("access_string");
+                break;
+            case 'xtrnmenu':
+            case 'xtrnprog':
+            default:
+                break;
         }
 
         selection = uifc.list(WIN_ORG | WIN_MID | WIN_ACT | WIN_ESC,
             menu.title + ": Item " + itemindex, displayoptions, itemctx);
 
         if (selection < 0) {
-            if (!item.title || !item.type || !item.target) {
+            if (!item.title || !item.type) {
                 if (uifc.list(WIN_ORG | WIN_MID, "This item is missing required items.", ["Remove Item", "Edit Item"]) == 0) {
                     // delete item and continue
                     newitems = [];
@@ -405,6 +439,20 @@ var editItem = function(menuid, itemindex) {
                     menu.items = newitems;
                     break;
                 }
+            } else if (!item.target && ((item.type == "custommenu") || (item.type == "command")
+                || (item.type == "xtrnmenu") || (item.type == "xtrnprog"))) {
+                if (uifc.list(WIN_ORG | WIN_MID, "This item is missing required items.", ["Remove Item", "Edit Item"]) == 0) {
+                    // delete item and continue
+                    newitems = [];
+                    for (i in menu.items) {
+                        if (i != itemindex) {
+                            newitems.push(menu.items[i]);
+                        }
+                    }
+                    menu.items = newitems;
+                    break;
+                }
+                break;
             } else {
                 // leave menu
                 break;
@@ -414,7 +462,7 @@ var editItem = function(menuid, itemindex) {
         switch (displayoptionids[selection]) {
 
             case 'input':
-                uifc.help_text = word_wrap("The input key to access this item. Can be anything except Q. Leave blank to auto-generate a number.");
+                uifc.help_text = word_wrap("The input key to access this item. Can be anything except Q. Leave blank to auto-generate a number.", 72);
                 selection2 = uifc.input(WIN_MID, "Input Key", item.input, 3, K_EDIT);
                 if ((selection2 < 0) || (selection2 == null)) {
                     // escape key
@@ -435,8 +483,10 @@ var editItem = function(menuid, itemindex) {
                             keyused = true;
                         }
                     }
-
-                    if (keyused) {
+    
+                    if (selection2 == "Q") {
+                        uifc.msg("This input key Q is reserved");
+                    }  else if (keyused) {
                         uifc.msg("This input key is already used by another item.");
                     } else {
                         item.input = selection2;
@@ -470,7 +520,7 @@ var editItem = function(menuid, itemindex) {
                 break;
 
             case 'access_string':
-                uifc.help_text = word_wrap("The access string for the custom menu.\r\n\r\nOnly applies to custom menu items.\r\n\r\nExample: LEVEL 60");
+                uifc.help_text = word_wrap("The access string for the custom menu.\r\n\r\nOnly applies to custom menu items.\r\n\r\nExample: LEVEL 60", 72);
                 selection2 = uifc.input(WIN_MID, "Access String", item.access_string, 255, K_EDIT);
                 if ((selection2 < 0) || (selection2 == null)) {
                     // escape key
@@ -491,9 +541,17 @@ function present_select_targettype(item)
         + "custommenu is a custom menu defined in this tool.\r\n\r\n"
         + "xtrnmenu is a standard Syncrhonet External Section Menu (refer to the scfg tool).\r\n\r\n"
         + "xtrnprog is a direct link to an external program (refer to the scfg tool)"
-        + "command is a synchronet command line. See http://wiki.synchro.net/config:cmdline");
-
-    var targetypectx = uifc.list.CTX(0, 0, 0, 0, 0);
+        + "command is a synchronet command line. See http://wiki.synchro.net/config:cmdline"
+        + "recentall is a special menu of most recently used games, by all users"
+        + "recentuser is a special menu of most recently used games, for current user"
+        + "mostlaunchedall is a special menu of most launched games, by all users"
+        + "mostlauncheduser is a special menu of most launched games, for current user"
+        + "longestrunall is a special menu of games that users spent the most time in"
+        + "longestrunuser is a special menu of games that current user spent the most time in"
+        + "search is a special menu item to perform a search"
+        + "favorites is a special menu to let the user pick favorite games to play", 72);
+
+    // for existing items, set the popup to the correct value
     if (typeof item.type !== "undefined") {
         switch (item.type) {
             case 'custommenu':
@@ -512,10 +570,44 @@ function present_select_targettype(item)
                 targetypectx.cur = 3;
                 targetypectx.bar = 3;
                 break;
+            case 'recentall':
+                targetypectx.cur = 4;
+                targetypectx.bar = 4;
+                break;
+            case 'recentuser':
+                targetypectx.cur = 5;
+                targetypectx.bar = 5;
+                break;
+            case 'mostlaunchedall':
+                targetypectx.cur = 6;
+                targetypectx.bar = 6;
+                break;
+            case 'mostlauncheduser':
+                targetypectx.cur = 7;
+                targetypectx.bar = 7;
+                break;
+            case 'longestrunall':
+                targetypectx.cur = 8;
+                targetypectx.bar = 8;
+                break;
+            case 'longestrunuser':
+                targetypectx.cur = 9;
+                targetypectx.bar = 9;
+                break;
+            case 'search':
+                targetypectx.cur = 10;
+                targetypectx.bar = 10;
+                break;
+            case 'favorites':
+                targetypectx.cur = 11;
+                targetypectx.bar = 11;
+                break;				
         }
     }
     switch (uifc.list(WIN_ORG | WIN_MID | WIN_SAV,
-        "Target Type", ["custommenu", "xtrnmenu", "xtrnprog", "command"], targetypectx)) {
+        "Target Type", ["custommenu", "xtrnmenu", "xtrnprog", "command", "recentall",
+            "recentuser", "mostlaunchedall", "mostlauncheduser", "longestrunall",
+            "longestrunuser", "search", "favorites"], targetypectx)) {
         case 0:
             item.type = "custommenu";
             break;
@@ -528,6 +620,30 @@ function present_select_targettype(item)
         case 3:
             item.type = "command";
             break;
+        case 4:
+            item.type = "recentall";
+            break;
+        case 5:
+            item.type = "recentuser";
+            break;
+        case 6:
+            item.type = "mostlaunchedall";
+            break;
+        case 7:
+            item.type = "mostlauncheduser";
+            break;
+        case 8:
+            item.type = "longestrunall";
+            break;
+        case 9:
+            item.type = "longestrunuser";
+            break;
+        case 10:
+            item.type = "search";
+            break;
+        case 11:
+            item.type = "favorites";
+            break;
         default:
             // includes escape key
             break;
@@ -539,9 +655,8 @@ function present_select_targettype(item)
 
 function present_select_target(item)
 {
-    uifc.help_text = word_wrap("This is the ID of the custom menu, external program section, or external program to link to.");
-
-    var targetctx = uifc.list.CTX(0, 0, 0, 0, 0);
+    uifc.help_text = word_wrap("This is the ID of the custom menu, external program section, or external program to link to. "
+     + "For special menus (recentall, etc.), it is the number of items to display.", 72);
 
     var custommenuitems = [];
     var custommenuitemsids = [];
@@ -561,23 +676,31 @@ function present_select_target(item)
             if ((typeof item.target !== "undefined") && item.target) {
                 for (var p in custommenuitemsids) {
                     if (custommenuitemsids[p] == item.target) {
-                        targetctx.cur = p;
-                        targetctx.bar = p;
+                        targetctxmenu.cur = p;
+                        targetctxmenu.bar = p;
                     }
                 }
             }
-
-            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx);
+    
+            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctxmenu);
             if ((selection2 < 0) || (selection2 == null)) {
                 // escape key
                 break;
             }
 
+            // increment counter for rapid bulk adding
+            if (targetctxmenu.cur < custommenuitemsids.length) {
+                ++targetctxmenu.cur;
+                ++targetctxmenu.bar;
+            }
+
             item.target = custommenuitemsids[selection2];
 
             while(1) {
-                if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) {
+                if ((item.title !== null) && uifc.list(WIN_ORG | WIN_MID, "Replace item title with menus's name?", ["Yes", "No"]) == 0) {
                     item.title = custommenunames[selection2]; // for external program, change title to program name
+                } else if (item.title === null) {
+                    item.title = custommenunames[selection2];
                 }
                 break;
             }
@@ -600,30 +723,37 @@ function present_select_target(item)
             if ((typeof item.target !== "undefined") && item.target) {
                 for (var p in custommenuitemsids) {
                     if (custommenuitemsids[p].toLowerCase() == item.target.toLowerCase()) {
-                        targetctx.cur = p;
-                        targetctx.bar = p;
+                        targetctxsection.cur = p;
+                        targetctxsection.bar = p;
                     }
                 }
-            }
+            } 
 
-            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx);
+            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctxsection);
             if ((selection2 < 0) || (selection2 == null)) {
                 // escape key
                 break;
             }
+    
+            // increment counter for rapid bulk adding            
+            if (targetctxsection.cur < custommenuitemsids.length) {
+                ++targetctxsection.cur;
+                ++targetctxsection.bar;
+            }
 
             item.target = custommenuitemsids[selection2];
 
             while(1) {
-                if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) {
+                if ((item.title !== null) && uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) {
                     item.title = custommenunames[selection2]; // for external program, change title to program name
+                } else if (item.title === null) {
+                    item.title = custommenunames[selection2];
                 }
                 break;
             }
             break;
 
         case "xtrnprog":
-
             // present list of external programs
             // create sorted list
             var proglist = [];
@@ -641,44 +771,69 @@ function present_select_target(item)
             if ((typeof item.target !== "undefined") && item.target) {
                 for (var p in custommenuitemsids) {
                     if (custommenuitemsids[p].toLowerCase() == item.target.toLowerCase()) {
-                        targetctx.cur = p;
-                        targetctx.bar = p;
+                        targetctxprog.cur = p;
+                        targetctxprog.bar = p;
                     }
                 }
-            }
-
-            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx);
+            } 
+            
+            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctxprog);
             if ((selection2 < 0) || (selection2 == null)) {
                 // escape key
                 break;
             }
+    
+            // increment counter for rapid bulk adding
+            if (targetctxprog.cur < custommenuitemsids.length) {
+                ++targetctxprog.cur;
+                ++targetctxprog.bar;
+            }
+            
             if (selection2 || selection2 === 0) {
                 item.target = custommenuitemsids[selection2];
                 while(1) {
-                    if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) {
+                    if ((item.title !== null) && uifc.list(WIN_ORG | WIN_MID, "Replace item title with programs's name?", ["Yes", "No"]) == 0) {
                         item.title = custommenunames[selection2]; // for external program, change title to program name
+                    } else if (item.title === null) {
+                        item.title = custommenunames[selection2];
                     }
                     break;
                 }
             }
             break;
-
-            command:
-                selection2 = uifc.input(WIN_ORG | WIN_MID, "Command", item.target, 63, K_EDIT);
+            
+        case "command":
+            selection2 = uifc.input(WIN_ORG | WIN_MID, "Command", item.target, 63, K_EDIT);
             if ((selection2 < 0) || (selection2 == null)) {
                 // escape key
                 break;
             }
-
             item.target = selection2;
             break;
+            
+        case "recentall":
+        case "recentuser":
+        case "mostlaunchedall":
+        case "mostlauncheduser":
+        case "longestrunall":
+        case "longestrunuser":
+        case "search":
+        case "favorites":
+            selection2 = uifc.input(WIN_ORG | WIN_MID, "Number of Items to Display", item.target, 63, K_EDIT);
+            if ((selection2 < 0) || (selection2 == null)) {
+                // escape key
+                break;
+            }
+            item.target = selection2;
+            break;
+            break;
+            
         default:
             selection2 = uifc.input(WIN_ORG | WIN_MID, "Target", item.target, 50, K_EDIT);
             if ((selection2 < 0) || (selection2 == null)) {
                 // escape key
                 break;
             }
-
             item.target = selection2;
             break;
     }
@@ -711,6 +866,13 @@ try {
     var menuconfig = {};
     var copyitem = {}; // for menu item copy/paste
     var config_file = new File(file_cfgname(system.ctrl_dir, "xtrnmenu.cfg"));
+    // this is made to persist so that if adding multiple items, it will
+    // remember the position of the target type (good for bulk adds)
+    var targetypectx = uifc.list.CTX(0, 0, 0, 0, 0);
+    var targetctxmenu = uifc.list.CTX(0, 0, 0, 0, 0);
+    var targetctxsection = uifc.list.CTX(0, 0, 0, 0, 0);
+    var targetctxprog = uifc.list.CTX(0, 0, 0, 0, 0);
+    
     if (config_file.open('r+')) {
         var config_src = config_file.read();
         try {
@@ -740,7 +902,7 @@ try {
     var ctx = new uifc.list.CTX(0, 0, 0, 0, 0);
 
     while(1) {
-        uifc.help_text = word_wrap("This program allows managing the Enhanced External Program Menu feature.");
+        uifc.help_text = word_wrap("This program allows managing the Enhanced External Program Menu feature.", 72);
 
         // no menus or no main menu
         var mainmenufound = false;
diff --git a/src/sbbs3/scfg/GNUmakefile b/src/sbbs3/scfg/GNUmakefile
index 29128d85a82cbdf4c1d5d1b5c28be12e2a3e9d8d..3a4537061db340d28e79b598273ee094aace2120 100644
--- a/src/sbbs3/scfg/GNUmakefile
+++ b/src/sbbs3/scfg/GNUmakefile
@@ -8,4 +8,4 @@ vpath %.c ..
 
 $(SCFG): $(OBJS) $(CRYPT_DEPS)
 	@echo Linking $@
-	${QUIET}$(CC) $(LDFLAGS) $(MT_LDFLAGS) -o$@ $(OBJS) $(SMBLIB_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(UIFC-MT_LIBS) $(CIOLIB-MT_LIBS) $(XPDEV-MT_LIBS) $(CRYPT_LIBS)
+	${QUIET}$(CC) $(LDFLAGS) $(MT_LDFLAGS) -o$@ $(OBJS) $(SMBLIB_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(UIFC-MT_LIBS) $(CIOLIB-MT_LIBS) $(CRYPT_LIBS) $(XPDEV-MT_LIBS)
diff --git a/webv4/components/navbar.xjs b/webv4/components/navbar.xjs
index 42bf5de0cb92fc5fc7017b46d7d364960f5728d2..63fc1bedbb306c82d9fba40bb343ca503c598671 100644
--- a/webv4/components/navbar.xjs
+++ b/webv4/components/navbar.xjs
@@ -47,8 +47,21 @@
             <ul class="nav navbar-nav">
                 <?xjs menu(getPageList(settings.web_pages)); ?>
             </ul>
+
             <ul class="nav navbar-nav navbar-right">
-                <?xjs if (user.alias === settings.guest || user.number < 1) { ?>
+<?xjs if (!settings.darkmode_off) { ?>	            
+				<li class="nav-item dark-switch">
+					<div class="form-group">
+						<div class="checkbox checbox-switch darkswitchbox">
+							<label>
+								<input type="checkbox" id="darkSwitch" />Dark
+								<span></span>
+							</label>
+              			</div>
+              		</div>
+            	</li>
+<?xjs } ?>            	
+            	<?xjs if (user.alias === settings.guest || user.number < 1) { ?>
                     <?xjs if (settings.user_registration) { ?>
                         <li>
                             <a href="./?page=000-register.xjs"><? write(locale.strings.main.menu_item_register); ?></a>
diff --git a/webv4/lib/events/nodelist.js b/webv4/lib/events/nodelist.js
index 366315854c5224847d3f38dc8154e22059a8367e..88673c04ef6a1ae6782af47a06ac95ebe65fe46b 100644
--- a/webv4/lib/events/nodelist.js
+++ b/webv4/lib/events/nodelist.js
@@ -73,7 +73,7 @@ function scan() {
             if (e.status != NODE_INUSE) {
                 return {
                     node: i + 1,
-                    status: null,
+                    status: locale.strings.sidebar_node_list.label_waiting_for_call,
                     action: null,
                     user: null,
                     connection: ''
diff --git a/webv4/lib/locale/en_us.ini b/webv4/lib/locale/en_us.ini
index 655ad46eeb74c0d3a0420ad78ba2c0d42028886d..d231320aa24ac4d6f163a9731ef62d2249b0d262 100644
--- a/webv4/lib/locale/en_us.ini
+++ b/webv4/lib/locale/en_us.ini
@@ -100,6 +100,7 @@ label_node_column = Node
 label_send_telegram = Send a telegram
 label_status_column = Status
 label_status_web = browsing
+label_waiting_for_call = Waiting for call
 
 [sidebar_recent_visitors]
 label_title = Recent Visitors
diff --git a/webv4/pages/000-home.xjs b/webv4/pages/000-home.xjs
index 32b5353df82af16356ee2a36e23572ad651df34b..53b5280a04a8689c95a03eb1d49c4fff1d80604e 100644
--- a/webv4/pages/000-home.xjs
+++ b/webv4/pages/000-home.xjs
@@ -20,30 +20,32 @@
 
     	<script id="fTelnetScript" src="<?xjs write(get_url()); ?>"></script>
     	<script>
-            var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>;
-            var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>;
-    		var Options = new fTelnetOptions();
-    		Options.BareLFtoCRLF = false;
-            Options.BitsPerSecond = 57600;
-            Options.ButtonBarVisible = true;
-    		Options.ConnectionType = 'telnet';
-    		Options.Emulation = 'ansi-bbs';
-    		Options.Enter = '\r';
-    		Options.Font = 'CP437';
-    		Options.ForceWss = false;
-    		Options.Hostname = '<?xjs write(http_request.vhost); ?>';
-    		Options.LocalEcho = false;
-    		Options.Port = location.protocol == 'https:' ? wssp : wsp;
-    		Options.ScreenColumns = 80;
-    		Options.ScreenRows = 25;
-            Options.SplashScreen = '<?xjs write(get_splash()); ?>';
-            var fTelnet = new fTelnetClient('fTelnetContainer', Options);
-            fTelnet.ButtonBarVisible = true;
-            if ($('#ftelnet-connect').length) {
-                $('#ftelnet-connect').click(function() {
-                    fTelnet.Connect();
-                });
-            }
+    		window.addEventListener('load', (event) => {
+				var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>;
+				var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>;
+				var Options = new fTelnetOptions();
+				Options.BareLFtoCRLF = false;
+				Options.BitsPerSecond = 57600;
+				Options.ButtonBarVisible = true;
+				Options.ConnectionType = 'telnet';
+				Options.Emulation = 'ansi-bbs';
+				Options.Enter = '\r';
+				Options.Font = 'CP437';
+				Options.ForceWss = false;
+				Options.Hostname = '<?xjs write(http_request.vhost); ?>';
+				Options.LocalEcho = false;
+				Options.Port = location.protocol == 'https:' ? wssp : wsp;
+				Options.ScreenColumns = 80;
+				Options.ScreenRows = 25;
+				Options.SplashScreen = '<?xjs write(get_splash()); ?>';
+				var fTelnet = new fTelnetClient('fTelnetContainer', Options);
+				fTelnet.ButtonBarVisible = true;
+				if ($('#ftelnet-connect').length) {
+					$('#ftelnet-connect').click(function() {
+						fTelnet.Connect();
+					});
+				}
+			});
     	</script>
 
 <?xjs } ?>
diff --git a/webv4/pages/003-games.xjs b/webv4/pages/003-games.xjs
index 7b7604d8bb2b03cb992b44b613676ba42b0569e2..749594a43c12c17585f5b730dda21f57c02765e6 100644
--- a/webv4/pages/003-games.xjs
+++ b/webv4/pages/003-games.xjs
@@ -43,29 +43,31 @@
 
 <script id="fTelnetScript" src="<?xjs write(get_url()); ?>"></script>
 <script type="text/javascript">
-    var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>;
-    var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>;
-    var Options = new fTelnetOptions();
-    Options.BareLFtoCRLF = false;
-    Options.BitsPerSecond = 57600;
-    Options.ConnectionType = 'rlogin';
-    Options.Emulation = 'ansi-bbs';
-    Options.Enter = '\r';
-    Options.Font = 'CP437';
-    Options.ForceWss = false;
-    Options.Hostname = '<?xjs write(http_request.vhost); ?>';
-    Options.LocalEcho = false;
-    Options.Port = location.protocol == 'https:' ? wssp : wsp;
-    Options.RLoginClientUsername = '<?xjs write(user.security.password); ?>';
-    Options.RLoginServerUsername = '<?xjs write(user.alias); ?>';
-    Options.ScreenColumns = 80;
-    Options.ScreenRows = 25;
-    Options.SplashScreen = Options.SplashScreen = '<?xjs write(get_splash()); ?>';
-    Options.WebSocketUrlPath = '?Port=<?xjs write(GetRLoginPort()); ?>';
-    var fTelnet = new fTelnetClient('fTelnetContainer', Options);
-    fTelnet.OnConnectionClose = function () {
-        window.location.reload();
-    };
+	window.addEventListener('load', (event) => {
+		var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>;
+		var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>;
+		var Options = new fTelnetOptions();
+		Options.BareLFtoCRLF = false;
+		Options.BitsPerSecond = 57600;
+		Options.ConnectionType = 'rlogin';
+		Options.Emulation = 'ansi-bbs';
+		Options.Enter = '\r';
+		Options.Font = 'CP437';
+		Options.ForceWss = false;
+		Options.Hostname = '<?xjs write(http_request.vhost); ?>';
+		Options.LocalEcho = false;
+		Options.Port = location.protocol == 'https:' ? wssp : wsp;
+		Options.RLoginClientUsername = '<?xjs write(user.security.password); ?>';
+		Options.RLoginServerUsername = '<?xjs write(user.alias); ?>';
+		Options.ScreenColumns = 80;
+		Options.ScreenRows = 25;
+		Options.SplashScreen = Options.SplashScreen = '<?xjs write(get_splash()); ?>';
+		Options.WebSocketUrlPath = '?Port=<?xjs write(GetRLoginPort()); ?>';
+		window.fTelnet = new fTelnetClient('fTelnetContainer', Options);
+		fTelnet.OnConnectionClose = function () {
+			window.location.reload();
+		};
+	});
 
     async function launchXtrn(code) {
         await v4_get('./api/system.ssjs?call=set-xtrn-intent&code=' + code);
diff --git a/webv4/pages/099-xtrnmenu-games.xjs b/webv4/pages/099-xtrnmenu-games.xjs
index c7106969f157d3872695bbe5d7e9b140abcde545..6f2a37dc9086f0aac5345d8e25fb70c4b9c75100 100644
--- a/webv4/pages/099-xtrnmenu-games.xjs
+++ b/webv4/pages/099-xtrnmenu-games.xjs
@@ -1,10 +1,10 @@
-<!--HIDDEN:Games-->
+<!--Games-->
 <?xjs
 /**
  * Web Display for Custom External Program Menus
  * by Michael Long mlong  innerrealmbbs.us
  * 
- * See wiki at http://wiki.synchro.net/module:xtrnmenumod
+ * See wiki at http://wiki.synchro.net/module:xtrnmenu
  */
 
     load("ftelnethelper.js");
@@ -33,8 +33,43 @@
 
 <?xjs
     var menuobj;
-    if ((Request.get_param('type') == 'xtrnmenu') && Request.has_param('target')) {
-        menuobj = ExternalMenus.getSectionMenu(Request.get_param('target'));
+    if (Request.get_param('type')) {
+        var target = Request.get_param('target');
+        switch (Request.get_param('type')) {
+	        case 'custommenu':
+                if (target !== undefined) {
+			        menuobj = ExternalMenus.getMenu(target);
+                }
+                break;
+            case 'xtrnmenu':
+                if (target !== undefined) {
+			        menuobj = ExternalMenus.getSectionMenu(target);
+                }
+                break;
+            case 'recentall':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Most Recent - All", target);
+                break;
+            case 'recentuser':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Most Recent - Yours", target);
+                break;
+            case 'mostlaunchedall':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Most Launched - All", target);
+                break;
+            case 'mostlauncheduser':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Most Launched - Yours", target);
+                break;
+            case 'longestrunall':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Longest Run - All", target);
+                break;
+            case 'longestrunuser':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Longest Run - Yours", target);
+                break;
+            case 'favorites':                
+                menuobj = ExternalMenus.getFavorites("Favorites", target);
+                break;
+            default:
+                break;
+        }
     } else {
         if (Request.has_param('target')) {
             menuobj = ExternalMenus.getMenu(Request.get_param('target'));
@@ -57,11 +92,27 @@
             if (settings.xtrn_blacklist.indexOf(menuitem.target.toLowerCase()) > -1) {
                 return;
             }
-            menuitems.push({
-                'itemtitle': menuitem.title,
-                'itemtype': menuitem.type,
-                'itemtarget': menuitem.target
-            });
+            switch (menuitem.type) {
+                case 'custommenu':
+                case 'xtrnmenu':
+                case 'xtrnprog':
+                case 'favorites':
+                case 'recentall':
+                case 'recentuser':
+                case 'mostlaunchedall':
+                case 'mostlauncheduser':
+                case 'longestrunuser':
+                case 'longestrunall':
+                    menuitems.push({
+                        'itemtitle': menuitem.title,
+                        'itemtype': menuitem.type,
+                        'itemtarget': menuitem.target,
+                        'stats': typeof menuitem.stats !== undefined ? menuitem.stats : null
+                    });
+                    break;
+                default:
+                  break;
+            }
         });
     }
 ?>
@@ -69,29 +120,31 @@
 
 <script id="fTelnetScript" src="<?xjs write(get_url()); ?>"></script>
 <script type="text/javascript">
-    var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>;
-    var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>;
-    var Options = new fTelnetOptions();
-    Options.BareLFtoCRLF = false;
-    Options.BitsPerSecond = 57600;
-    Options.ConnectionType = 'rlogin';
-    Options.Emulation = 'ansi-bbs';
-    Options.Enter = '\r';
-    Options.Font = 'CP437';
-    Options.ForceWss = false;
-    Options.Hostname = '<?xjs write(http_request.vhost); ?>';
-    Options.LocalEcho = false;
-    Options.Port = location.protocol == 'https:' ? wssp : wsp;
-    Options.RLoginClientUsername = '<?xjs write(user.security.password); ?>';
-    Options.RLoginServerUsername = '<?xjs write(user.alias); ?>';
-    Options.ScreenColumns = 80;
-    Options.ScreenRows = 25;
-    Options.SplashScreen = Options.SplashScreen = '<?xjs write(get_splash()); ?>';
-    Options.WebSocketUrlPath = '?Port=<?xjs write(GetRLoginPort()); ?>';
-    var fTelnet = new fTelnetClient('fTelnetContainer', Options);
-    fTelnet.OnConnectionClose = function () {
-        window.location.reload();
-    };
+	window.addEventListener('load', (event) => {
+		var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>;
+		var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>;
+		var Options = new fTelnetOptions();
+		Options.BareLFtoCRLF = false;
+		Options.BitsPerSecond = 57600;
+		Options.ConnectionType = 'rlogin';
+		Options.Emulation = 'ansi-bbs';
+		Options.Enter = '\r';
+		Options.Font = 'CP437';
+		Options.ForceWss = false;
+		Options.Hostname = '<?xjs write(http_request.vhost); ?>';
+		Options.LocalEcho = false;
+		Options.Port = location.protocol == 'https:' ? wssp : wsp;
+		Options.RLoginClientUsername = '<?xjs write(user.security.password); ?>';
+		Options.RLoginServerUsername = '<?xjs write(user.alias); ?>';
+		Options.ScreenColumns = 80;
+		Options.ScreenRows = 25;
+		Options.SplashScreen = Options.SplashScreen = '<?xjs write(get_splash()); ?>';
+		Options.WebSocketUrlPath = '?Port=<?xjs write(GetRLoginPort()); ?>';
+		window.fTelnet = new fTelnetClient('fTelnetContainer', Options);
+		fTelnet.OnConnectionClose = function () {
+			window.location.reload();
+		};
+	});
 
     async function launchXtrn() {
         var code = event.srcElement.id;
@@ -109,11 +162,18 @@
         sessionStorage.setItem("mainmenu", currentTitle);
     }
     var div = $('#xtrn-list');
+    
     menuitems.forEach(function (menuitem) {
         var a = document.createElement('a');
         $(a).addClass("list-group-item");
         $(a).addClass("striped");
         a.text = menuitem.itemtitle;
+        
+        if (typeof menuitem.stats !== "undefined") {
+            var badge = document.createElement('span');
+            $(badge).addClass('badge').text(menuitem.stats);
+            a.appendChild(badge);
+        }
 
         if (menuitem.itemtype == "xtrnprog") {
             a.href = "#fTelnet";
diff --git a/webv4/pages/099-xtrnmenu-gamestats.xjs b/webv4/pages/099-xtrnmenu-gamestats.xjs
new file mode 100644
index 0000000000000000000000000000000000000000..240f19c0aecc020414075058d367e8f56771f480
--- /dev/null
+++ b/webv4/pages/099-xtrnmenu-gamestats.xjs
@@ -0,0 +1,54 @@
+<!--Game Stats-->
+<div class="list-group">
+
+<h1>Game Stats</h1>
+
+<?xjs
+    load('sbbsdefs.js');
+    load("xtrnmenulib.js");
+    
+    var ExternalMenus = new ExternalMenus();
+      
+    if (typeof settings.xtrn_blacklist === 'string') {
+        settings.xtrn_blacklist = settings.xtrn_blacklist.toLowerCase().split(',');
+    } else {
+        settings.xtrn_blacklist = [];
+    }
+
+	// Edit these lines as needed to adjust the page
+	gameStats("recentall", "Most Recent", 10);
+	gameStats("mostlaunchedall", "Top 15 Most Launched", 15);
+	gameStats("longestrunall", "Top 15 Longest Run", 15);
+	
+	function gameStats(menutype, title, maxitems) {
+	    var menuobj = ExternalMenus.getSpecial(menutype, title); 
+
+?>
+<div class="row list-group-item" style="background-color: #993399; color: #FFF">
+	<div class="col-sm-12"><h3><?xjs write(title) ?></h3></div>
+</div>
+
+<?xjs        
+		if ((typeof menuobj.items !== "undefined") || (menuobj.items.length > 0)) {
+			var i = 0;
+			menuobj.items.some(function (menuitem) {
+				if (menuitem.type != "xtrnprog") {
+					return;
+				}
+		
+				if (settings.xtrn_blacklist.indexOf(menuitem.target.toLowerCase()) > -1) return;
+				
+				if (++i > maxitems) return;
+?>
+
+<div class="row list-group-item striped">
+	<div class="col-sm-2"><span class="badge badge-inverse"><?xjs write(menuitem.stats) ?></span></div>
+	<div class="col-sm-10"><?xjs write(menuitem.title) ?></div>
+</div>
+
+<?xjs		
+			});
+		}
+	}
+?>	
+</div>
diff --git a/webv4/root/css/checkbox.css b/webv4/root/css/checkbox.css
new file mode 100644
index 0000000000000000000000000000000000000000..a19ff7084203dc200c0f131aba9b2118cc842304
--- /dev/null
+++ b/webv4/root/css/checkbox.css
@@ -0,0 +1,188 @@
+/* ---------------------------------------------------
+
+Project : CSS Checkbox Switch
+Author : Partha Kar (https://www.facebook.com/partha.creativemind)
+Version : 1.0
+Release Dtae : 15 November, 2017
+
+---------------------------------------------------- */
+
+
+.checkbox.checbox-switch {
+    padding-left: 0;
+}
+
+.checkbox.checbox-switch label,
+.checkbox-inline.checbox-switch {
+    display: inline-block;
+    position: relative;
+    padding-left: 0;
+}
+.checkbox.checbox-switch label input,
+.checkbox-inline.checbox-switch input {
+    display: none;
+}
+.checkbox.checbox-switch label span,
+.checkbox-inline.checbox-switch span {
+    width: 35px;
+    border-radius: 20px;
+    height: 18px;
+    border: 1px solid #dbdbdb;
+    background-color: rgb(255, 255, 255);
+    border-color: rgb(223, 223, 223);
+    box-shadow: rgb(223, 223, 223) 0px 0px 0px 0px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s;
+    display: inline-block;
+    vertical-align: middle;
+    margin-right: 5px;
+}
+.checkbox.checbox-switch label span:before,
+.checkbox-inline.checbox-switch span:before {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    border-radius: 50%;
+    background: rgb(255,255,255);
+    content: " ";
+    top: 0;
+    position: relative;
+    left: 0;
+    transition: all 0.3s ease;
+    box-shadow: 0 1px 4px rgba(0,0,0,0.4);
+}
+.checkbox.checbox-switch label > input:checked + span:before,
+.checkbox-inline.checbox-switch > input:checked + span:before {
+    left: 17px;
+}
+
+
+/* Switch Default */
+.checkbox.checbox-switch label > input:checked + span,
+.checkbox-inline.checbox-switch > input:checked + span {
+    background-color: rgb(180, 182, 183);
+    border-color: rgb(180, 182, 183);
+    box-shadow: rgb(180, 182, 183) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+.checkbox.checbox-switch label > input:checked:disabled + span,
+.checkbox-inline.checbox-switch > input:checked:disabled + span {
+    background-color: rgb(220, 220, 220);
+    border-color: rgb(220, 220, 220);
+    box-shadow: rgb(220, 220, 220) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+.checkbox.checbox-switch label > input:disabled + span,
+.checkbox-inline.checbox-switch > input:disabled + span {
+    background-color: rgb(232,235,238);
+    border-color: rgb(255,255,255);
+}
+.checkbox.checbox-switch label > input:disabled + span:before,
+.checkbox-inline.checbox-switch > input:disabled + span:before {
+    background-color: rgb(248,249,250);
+    border-color: rgb(243, 243, 243);
+    box-shadow: 0 1px 4px rgba(0,0,0,0.1);
+}
+
+/* Switch Light */
+.checkbox.checbox-switch.switch-light label > input:checked + span,
+.checkbox-inline.checbox-switch.switch-light > input:checked + span {
+    background-color: rgb(248,249,250);
+    border-color: rgb(248,249,250);
+    box-shadow: rgb(248,249,250) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+
+/* Switch Dark */
+.checkbox.checbox-switch.switch-dark label > input:checked + span,
+.checkbox-inline.checbox-switch.switch-dark > input:checked + span {
+    background-color: rgb(52,58,64);
+    border-color: rgb(52,58,64);
+    box-shadow: rgb(52,58,64) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+.checkbox.checbox-switch.switch-dark label > input:checked:disabled + span,
+.checkbox-inline.checbox-switch.switch-dark > input:checked:disabled + span {
+    background-color: rgb(100, 102, 104);
+    border-color: rgb(100, 102, 104);
+    box-shadow: rgb(100, 102, 104) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+
+/* Switch Success */
+.checkbox.checbox-switch.switch-success label > input:checked + span,
+.checkbox-inline.checbox-switch.switch-success > input:checked + span {
+    background-color: rgb(40, 167, 69);
+    border-color: rgb(40, 167, 69);
+    box-shadow: rgb(40, 167, 69) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+.checkbox.checbox-switch.switch-success label > input:checked:disabled + span,
+.checkbox-inline.checbox-switch.switch-success > input:checked:disabled + span {
+    background-color: rgb(153, 217, 168);
+    border-color: rgb(153, 217, 168);
+    box-shadow: rgb(153, 217, 168) 0px 0px 0px 8px inset;
+}
+
+/* Switch Danger */
+.checkbox.checbox-switch.switch-danger label > input:checked + span,
+.checkbox-inline.checbox-switch.switch-danger > input:checked + span {
+    background-color: rgb(200, 35, 51);
+    border-color: rgb(200, 35, 51);
+    box-shadow: rgb(200, 35, 51) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+.checkbox.checbox-switch.switch-danger label > input:checked:disabled + span,
+.checkbox-inline.checbox-switch.switch-danger > input:checked:disabled + span {
+    background-color: rgb(216, 119, 129);
+    border-color: rgb(216, 119, 129);
+    box-shadow: rgb(216, 119, 129) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+
+/* Switch Primary */
+.checkbox.checbox-switch.switch-primary label > input:checked + span,
+.checkbox-inline.checbox-switch.switch-primary > input:checked + span {
+    background-color: rgb(0, 105, 217);
+    border-color: rgb(0, 105, 217);
+    box-shadow: rgb(0, 105, 217) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+.checkbox.checbox-switch.switch-primary label > input:checked:disabled + span,
+.checkbox-inline.checbox-switch.switch-primary > input:checked:disabled + span {
+    background-color: rgb(109, 163, 221);
+    border-color: rgb(109, 163, 221);
+    box-shadow: rgb(109, 163, 221) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+
+/* Switch Info */
+.checkbox.checbox-switch.switch-info label > input:checked + span,
+.checkbox-inline.checbox-switch.switch-info > input:checked + span {
+    background-color: rgb(23, 162, 184);
+    border-color: rgb(23, 162, 184);
+    box-shadow: rgb(23, 162, 184) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+.checkbox.checbox-switch.switch-info label > input:checked:disabled + span,
+.checkbox-inline.checbox-switch.switch-info > input:checked:disabled + span {
+    background-color: rgb(102, 192, 206);
+    border-color: rgb(102, 192, 206);
+    box-shadow: rgb(102, 192, 206) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+
+/* Switch Warning */
+.checkbox.checbox-switch.switch-warning label > input:checked + span,
+.checkbox-inline.checbox-switch.switch-warning > input:checked + span {
+    background-color: rgb(255, 193, 7);
+    border-color: rgb(255, 193, 7);
+    box-shadow: rgb(255, 193, 7) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
+.checkbox.checbox-switch.switch-warning label > input:checked:disabled + span,
+.checkbox-inline.checbox-switch.switch-warning > input:checked:disabled + span {
+    background-color: rgb(226, 195, 102);
+    border-color: rgb(226, 195, 102);
+    box-shadow: rgb(226, 195, 102) 0px 0px 0px 8px inset;
+    transition: border 0.4s ease 0s, box-shadow 0.4s ease 0s, background-color 1.2s ease 0s;
+}
diff --git a/webv4/root/css/style.css b/webv4/root/css/style.css
index 7e49bcf0b1719cc5ecaf0b244f814668234257bf..9aaeae40472f7c135551e8abff5a8b882d912dff 100644
--- a/webv4/root/css/style.css
+++ b/webv4/root/css/style.css
@@ -18,8 +18,8 @@ a.unread {
 }
 
 span.badge.new {
-        background: #2E9AFE;
-        color: #FFFFFF;
+	background: #2E9AFE;
+	color: #FFFFFF;
 }
 
 /* A read mail message in the list view. */
@@ -53,7 +53,77 @@ span.message-header.unread {
 	background-color: #FCF8E3;
 }
 
-/*	You probably don't need to mess with rules below this line. */
+/*** Dark mode ***/
+
+/* default links, like the breadcrumbs */
+.dark a {
+	color: #75b8f1;
+}
+
+/* background of the top bar */
+.dark .navbar {
+	background-color: #333;
+}
+
+/* bbs name at top left */
+.dark .navbar-brand {
+	color: #ccc;
+}
+.dark .navbar-brand:hover {
+	color: #fff;
+}
+
+/* hover for the top nav buttons */
+.dark .navbar-default a:hover {
+	color: #8cc1ee !important;
+	color: #fff;
+	background-color: #444 !important;
+}
+
+/* the active top nav button when opened */
+.dark .navbar-default .navbar-nav>.open>a,
+.dark.navbar-default .navbar-nav>.open>a:focus,
+.dark.navbar-default .navbar-nav>.open>a:hover {
+	background-color: #555;
+	color: #EEE;
+}
+
+/* dropdown menus */
+.dark .dropdown-menu {
+	background-color: #333;
+}
+.dark .dropdown-menu li a {
+	color: #75b8f1;
+}
+
+/* background color of alternate rows in lists */
+.dark .striped:nth-of-type(even), .dark .table-striped > tbody > tr:nth-child(odd) > td, .dark  .table-striped > tbody > tr:nth-child(odd) > th {
+	background: #444;
+}
+
+/* this is the text and color for most items, including non-alternating rows */
+.dark .list-group-item {
+	background-color: #555;
+	color: #EEE;
+}
+
+/* A link in a list when the mouse is hovering over it (mostly applies to the Forum) */
+.dark a.list-group-item:hover, .dark  a.list-group-item:active {
+	background-color: #888;
+	color: #222;
+}
+
+.dark li.list-group-item.mail:hover {
+	background-color: #888;
+	color: #FFF;
+}
+
+/* background color for the breadcrumb box */
+.dark .breadcrumb {
+	background-color: #555;
+}
+
+/*** You probably don't need to mess with rules below this line. ***/
 
 blockquote {
 	margin: .5em;
@@ -180,3 +250,72 @@ animation: indicator-fade 3s ease 0s 1 alternate !important;
 .breadcrumb li {
 	display: inline;
 }
+
+.dark {
+	background-color: #222 !important;
+	color: #eee;
+}
+
+.dark span.badge.ignored {
+	background-color: #aaa;
+}
+
+.dark .text-danger {
+	color: #ef1c18;
+}
+
+.dark .text-success {
+	color: #3eef41;
+}
+
+.dark a.btn, .dark .icon {
+	color: #000;
+	background-color: #ddd !important;
+}
+
+.dark .btn {
+	color: #000;
+	background-color: #ddd ;
+}
+
+.dark .btn-primary {
+	color: #FFF;
+	background-color: #337ab7;
+}
+
+.dark a.btn :hover, .dark .icon:hover {
+	color: #000;
+	background-color: #fff !important;
+}
+
+@media (max-width: 767px) {
+	.dark-switch {
+		padding: 10px 15px;
+	}
+}
+
+@media (min-width: 768px) {
+	.dark-switch {
+		padding: 0px;
+	}
+}
+
+.dark .modal-content {
+	background-color: #555 !important;
+	color: #FFF !important;
+}
+
+.darkswitchbox {
+	margin-block-start: 1em;
+	margin-block-end: 1em;
+	margin-inline-start: 0px;
+	margin-inline-end: 0px;
+	padding-inline-start: 40px;
+}
+
+.dark input,.dark select,.dark textarea {
+	color: #333;
+	background-color: #DDD;
+}
+
+
diff --git a/webv4/root/index.xjs b/webv4/root/index.xjs
index 477b77f4189faa010e4ed6eebed7a4e7fbea5354..b6a1f93a4726b65da3b52e28cf0248de90140703 100644
--- a/webv4/root/index.xjs
+++ b/webv4/root/index.xjs
@@ -41,6 +41,13 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
+		<style type="text/css">
+			.hidden { display:none; }
+		</style>
+		<script src="./js/jquery.min.js"></script>
+		<script type="text/javascript">
+			jQuery('html').addClass('hidden');
+		</script>
 		<meta charset="utf-8">
 		<meta http-equiv="X-UA-Compatible" content="IE=edge">
 		<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -49,6 +56,9 @@
 		<link href="./bootstrap/css/bootstrap.min.css" rel="stylesheet">
 		<link href="./css/offcanvas.css" rel="stylesheet">
 		<link href="./css/style.css" rel="stylesheet">
+<?xjs if (!settings.darkmode_off) { ?>	
+		<link href="./css/checkbox.css" rel="stylesheet">
+<?xjs } ?>
 		<? if (file_exists(settings.web_root + 'css/custom.css')) { ?>
 			<link href="./css/custom.css" rel="stylesheet">
 		<? } ?>
@@ -56,7 +66,6 @@
 
 	<body>
 
-		<script src="./js/jquery.min.js"></script>
 		<script src="./bootstrap/js/bootstrap.min.js"></script>
 		<script src="./js/common.js"></script>
 
@@ -101,7 +110,6 @@
     			return false;
 			});
 		</script>
-
 	</body>
 
 </html>
diff --git a/webv4/root/js/common.js b/webv4/root/js/common.js
index aa4ff8fa7d9de414bcf14b32b44480cf2b84525a..c6208c9e2476bd8050bfe88f3a394881971820a9 100644
--- a/webv4/root/js/common.js
+++ b/webv4/root/js/common.js
@@ -91,6 +91,44 @@ function registerEventListener(scope, callback, params) {
 	};
 }
 
+document.addEventListener("DOMContentLoaded", function () {
+	// originally based on dark-mode-switch by Christian Oliff
+	var darkSwitch = document.getElementById("darkSwitch");
+	if (darkSwitch) {
+		initTheme();
+		darkSwitch.addEventListener("change", function (event) {
+			resetTheme();
+		});
+
+		function initTheme() {
+			var darkThemeSelected;
+			if (localStorage.getItem("darkSwitch") !== null) {
+				darkThemeSelected = localStorage.getItem("darkSwitch") === "dark";
+			} else {
+				darkThemeSelected = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+			}
+			darkSwitch.checked = darkThemeSelected;
+			if (darkThemeSelected) {
+				jQuery("body").addClass("dark")
+			} else {
+				jQuery("body").removeClass("dark");
+			}
+		}
+
+		function resetTheme() {
+			if (darkSwitch.checked) {
+				jQuery("body").addClass("dark");
+				localStorage.setItem("darkSwitch", "dark");
+			} else {
+				jQuery("body").removeClass("dark");
+				localStorage.setItem("darkSwitch", "light");
+			}
+		}
+	}
+
+	jQuery('html').removeClass('hidden');
+});
+
 window.onload =	function () {
 
 	$('#button-logout').click(logout);
diff --git a/webv4/sidebar/001-nodelist.xjs b/webv4/sidebar/001-nodelist.xjs
index ed922ea45b322d4ce4dc9ce2fbc5f779f3617035..dee0e8b50050b16a60c0dd233236f3b54b8a0586 100644
--- a/webv4/sidebar/001-nodelist.xjs
+++ b/webv4/sidebar/001-nodelist.xjs
@@ -1,8 +1,5 @@
 <h4><?xjs write(locale.strings.sidebar_node_list.label_title); ?></h4>
 <div id="sbbs-nodelist"></div>
-<?xjs if (settings.nodelist_ibbs) { ?>
-    <div id="sbbsimsg-nodelist"></div>
-<?xjs } ?>
 
 <script type="text/javascript">
 
@@ -10,30 +7,6 @@
     const nch = '<?xjs write(settings.active_node_list ? locale.strings.sidebar_node_list.label_connection_column : locale.strings.sidebar_node_list.label_node_column) ?>';
     const nll = <?xjs write(system.node_list.length); ?>;
     var niu = 0;
-    var ns = 0;
-
-    function _sb_list_node(e) {
-        if (e.action === null || e.user === null) return;
-        $('#sbbs-nodelist-table').append(
-            '<tr>' +
-                '<th scope="row">' +
-                    (anl ? e.connection : (typeof e.node == 'number' ? e.node + 1 : e.connection)) +
-                '</th>' +
-                '<td id="nodelist-' + e.node + '">' +
-                    (e.user == '' ? e.status : ('<strong>' + e.user + '</strong> ' + e.action)) +
-                '</td>' +
-            '</tr>'
-        );
-        if (e.user != '' && <?xjs write(user.alias != settings.guest); ?>) {
-            $('#nodelist-' + e.node).attr('title', '<?xjs write(locale.strings.sidebar_node_list.label_send_telegram); ?>');
-            $('#nodelist-' + e.node).css('cursor', 'pointer');
-            $('#nodelist-' + e.node).click(function () {
-                sendTelegram(e.user);
-            });
-        }
-        if (typeof e.node == 'number' && e.user != '') niu++;
-        ns++;
-    }
 
     function _sb_nodelist(evt) {
         const data = JSON.parse(evt.data);
@@ -49,30 +22,29 @@
 			'</table>'
 		);
         niu = 0;
-        ns = 0;
-        if (!anl) {
-            const nodes = Array(nll);
-            data.forEach(function (e) {
-                if (typeof e.node == 'number') {
-                    nodes[e.node] = e;
-                } else {
-                    nodes.push(e); // Web user
-                }
-            });
-            for (var n = 0; n < nodes.length; n++) {
-                if (!nodes[n]) {
-                    _sb_list_node({ node: n, user: '', status :'Waiting for call' });
-                } else {
-                    _sb_list_node(nodes[n], n);
-                }
+        var ns = data.reduce((a, c) => {
+            if (anl && (c.action === null || c.user === null)) return a;
+            $('#sbbs-nodelist-table').append(
+                '<tr>' +
+                    '<th scope="row">' +
+                        (anl ? c.connection : (typeof c.node == 'number' ? c.node : c.connection)) +
+                    '</th>' +
+                    `<td id="nodelist-${c.node}">` +
+                        (c.user ? `<strong>${c.user}</strong> ${c.action}` : c.status) +
+                    '</td>' +
+                '</tr>'
+            );
+            if (c.user && <?xjs write(user.alias != settings.guest); ?>) {
+                $(`#nodelist-${c.node}`).attr('title', '<?xjs write(locale.strings.sidebar_node_list.label_send_telegram); ?>');
+                $(`#nodelist-${c.node}`).css('cursor', 'pointer');
+                $(`#nodelist-${c.node}`).click(() => sendTelegram(c.user));
             }
+            if (typeof c.node == 'number' && c.user != '') niu++;
+            return a + 1;
+        }, 0);
+        if (ns) {
             $('#sbbs-nodelist').parent().removeClass('hidden');
-        } else {
-            data.forEach(_sb_list_node);
-            if (ns) {
-                $('#sbbs-nodelist').parent().removeClass('hidden');
-                $('#sbbs-nodelist').removeClass('hidden');
-            }
+            $('#sbbs-nodelist').removeClass('hidden');
         }
         // Spans with these classes are used in the 'system stats' sidebar module.
         // Update the nodes in use / available counters there.
@@ -82,68 +54,4 @@
 
     registerEventListener('nodelist', _sb_nodelist);
 
-    <?xjs if (settings.nodelist_ibbs) { ?>
-        function _send_ibbs_telegram(sys, host, user) {
-            async function send_ibbs_tg(evt) {
-                if (typeof evt !== 'undefined') evt.preventDefault();
-                await v4_post('./api/sbbsimsg.ssjs', {
-                    call: 'send_telegram',
-                    username: user,
-                    host: host,
-                    message: $('#telegram').val()
-                });
-                $('#popUpModal').modal('hide');
-        	};
-            $('#popUpModalTitle').html('Send a telegram to ' + user + '@' + sys);
-        	$('#popUpModalBody').html(
-                '<form id="ibbs-telegram-form">'
-                + '<input type="text" class="form-control" placeholder="My message" name="telegram" id="telegram">'
-                + '<input type="submit" value="submit" class="hidden">'
-                + '</form>'
-            );
-        	$('#popUpModalActionButton').show();
-            $('#ibbs-telegram-form').submit(send_ibbs_tg);
-            $('#popUpModalActionButton').click(send_ibbs_tg);
-        	$('#popUpModal').modal('show');
-        }
-
-        registerEventListener('sbbsimsg', function (e) {
-            const data = JSON.parse(e.data);
-            var users = 0;
-            $('#sbbsimsg-nodelist').addClass('hidden');
-            $('#sbbsimsg-nodelist').html('<h4>Other Systems</h4>');
-            Object.keys(data).forEach(function (e, i) {
-                if (!data[e].users.length) return;
-                const id = 'sbbsimsg-nodelist-' + i;
-                $('#sbbsimsg-nodelist').append(
-                    '<table id="' + id + '" class="table table-condensed table-responsive table-striped">'
-                    + '<tr><td><strong>' + e + '</strong></td></tr>'
-                    + '</table>'
-                );
-                data[e].users.forEach(function (ee, ii) {
-                    const nid = id + '-' + ii;
-                    $('#' + id).append(
-                        '<tr>'
-                            + '<td id="' + nid + '">'
-                                + '<strong>' + ee.name + '</strong> ' + ee.action
-                            + '</td>'
-                        +'</tr>'
-                    );
-                    if (<?xjs write(user.alias != settings.guest); ?>) {
-                        $('#' + nid).click(function () {
-                            _send_ibbs_telegram(e, data[e].host, ee.name);
-                        });
-                        $('#' + nid).attr('title', '<?xjs write(locale.strings.sidebar_node_list.label_send_telegram); ?>');
-                        $('#' + nid).css('cursor', 'pointer');
-                    }
-                });
-                users += data[e].users.length;
-            });
-            if (users) {
-                $('#sbbsimsg-nodelist').removeClass('hidden');
-                $('#sbbs-nodelist').parent().removeClass('hidden');
-            }
-        });
-    <?xjs } ?>
-
 </script>
diff --git a/xtrn/3rdp-install/carlton-doormania.js b/xtrn/3rdp-install/carlton-doormania.js
index b43dba8a4e4b30d21e19ef4a8b8b0fe1c7149796..4178bc97a93f5e641fdcdf6865d4f84138163459 100644
--- a/xtrn/3rdp-install/carlton-doormania.js
+++ b/xtrn/3rdp-install/carlton-doormania.js
@@ -17,6 +17,8 @@ var lines = file.readAll();
 file.close();
 
 lines[4] = gamedir;
+lines[5] = "MANIA.ASC";
+lines[6] = "MANIA.ANS";
 
 writeln("Beginning node config generation...");
 for(i = 0; i < system.nodes; i++) {
diff --git a/xtrn/3rdp-install/foodfite-wilson.js b/xtrn/3rdp-install/foodfite-wilson.js
index 1b74e28d2b5feb1dd13e1d1c1fcd7dce2f8421f0..38444ea2ccc463b06a6eea99a57cbd8c56816a96 100644
--- a/xtrn/3rdp-install/foodfite-wilson.js
+++ b/xtrn/3rdp-install/foodfite-wilson.js
@@ -14,6 +14,8 @@ file.close();
 lines[0] = "LINE";
 lines[1] = system.name;
 lines[2] = system.operator;
+lines[5] = "bull99.ans";
+lines[6] = "bull99.asc";
 lines[9] = "NONE";
 lines[10] = "";
 lines[11] = "";
diff --git a/xtrn/3rdp-install/sunrise-aceydeucey.js b/xtrn/3rdp-install/sunrise-aceydeucey.js
index 0214c91436a0b7688a0f0a8e956249e467ade8b0..7ff3d94f1eece521d652f55e3bbbe147ddc77c32 100644
--- a/xtrn/3rdp-install/sunrise-aceydeucey.js
+++ b/xtrn/3rdp-install/sunrise-aceydeucey.js
@@ -28,10 +28,10 @@ var op = system.operator.split(" ", 2);
 lines[2] = op[0];
 lines[3] = op[1];
 
-lines[7] = gamedir + "aceyd.ans";
-lines[8] = gamedir + "aceyd.asc";
-lines[9] = gamedir + "aceydhof.ans";
-lines[10] = gamedir + "aceydhof.asc";
+lines[7] = gamedir + "aceyd.asc";
+lines[8] = gamedir + "aceyd.ans";
+lines[9] = gamedir + "aceydhof.asc";
+lines[10] = gamedir + "aceydhof.ans";
 lines[11] = "1";
 
 for(i = 0; i < system.nodes; i++) {
diff --git a/xtrn/3rdp-install/sunrise-baseballdice.js b/xtrn/3rdp-install/sunrise-baseballdice.js
index 3074ea2e313659df0d5bd23ff7ce9b9c907303bb..48c258347f93e8d8de58458d0691957eac098ff9 100644
--- a/xtrn/3rdp-install/sunrise-baseballdice.js
+++ b/xtrn/3rdp-install/sunrise-baseballdice.js
@@ -27,10 +27,10 @@ var op = system.operator.split(" ", 2);
 lines[2] = op[0];
 lines[3] = op[1];
 
-lines[7] = gamedir + "bbd.ans";
-lines[8] = gamedir + "bbd.asc";
-lines[9] = gamedir + "bbdhof.ans";
-lines[10] = gamedir + "bbdhof.asc";
+lines[7] = gamedir + "bbd.asc";
+lines[8] = gamedir + "bbd.ans";
+lines[9] = gamedir + "bbdhof.asc";
+lines[10] = gamedir + "bbdhof.ans";
 lines[11] = "1";
 
 for(i = 0; i < system.nodes; i++) {
diff --git a/xtrn/3rdp-install/sunrise-betsyross.js b/xtrn/3rdp-install/sunrise-betsyross.js
index 8906ed4a53531748d9fe18da0d94714bd93b617a..bc3e29d9c85f1158ffc0ed26ffd56b1cbbbdce7b 100644
--- a/xtrn/3rdp-install/sunrise-betsyross.js
+++ b/xtrn/3rdp-install/sunrise-betsyross.js
@@ -27,10 +27,10 @@ var op = system.operator.split(" ", 2);
 lines[2] = op[0];
 lines[3] = op[1];
 
-lines[6] = gamedir + "betsy.ans";
 lines[7] = gamedir + "betsy.asc";
-lines[8] = gamedir + "betsyhof.ans";
+lines[8] = gamedir + "betsy.ans";
 lines[9] = gamedir + "betsyhof.asc";
+lines[10] = gamedir + "betsyhof.ans";
 
 for(i = 0; i < system.nodes; i++) {
 	var nodenum = i + 1;
diff --git a/xtrn/3rdp-install/sunrise-blackjack.js b/xtrn/3rdp-install/sunrise-blackjack.js
index 8143740f1a52f287319bfcb5af5c4064b9ef1f28..279e79a61b7c1f11a2674b183dd128a17f01cc22 100644
--- a/xtrn/3rdp-install/sunrise-blackjack.js
+++ b/xtrn/3rdp-install/sunrise-blackjack.js
@@ -27,10 +27,10 @@ var op = system.operator.split(" ", 2);
 lines[2] = op[0];
 lines[3] = op[1];
 
-lines[7] = gamedir + "srbj.ans";
-lines[8] = gamedir + "srbj.asc";
-lines[9] = gamedir + "srbjhof.ans";
-lines[10] = gamedir + "srbjhof.asc";
+lines[7] = gamedir + "srbj.asc";
+lines[8] = gamedir + "srbj.ans";
+lines[9] = gamedir + "srbjhof.asc";
+lines[10] = gamedir + "srbjhof.ans";
 lines[11] = "1";
 
 for(i = 0; i < system.nodes; i++) {
diff --git a/xtrn/3rdp-install/sunrise-boxdice.js b/xtrn/3rdp-install/sunrise-boxdice.js
index 4b17e3f1f33ede57f8ae127224e523d5dea4bcc5..64798716de5a0a2190ce2fd9e3a25bfe6d7dbf19 100644
--- a/xtrn/3rdp-install/sunrise-boxdice.js
+++ b/xtrn/3rdp-install/sunrise-boxdice.js
@@ -27,10 +27,10 @@ var op = system.operator.split(" ", 2);
 lines[2] = op[0];
 lines[3] = op[1];
 
-lines[7] = gamedir + "srboxd.ans";
-lines[8] = gamedir + "srboxd.asc";
-lines[9] = gamedir + "srboxd.ans";
-lines[10] = gamedir + "srboxd.asc";
+lines[7] = gamedir + "srboxd.asc";
+lines[8] = gamedir + "srboxd.ans";
+lines[9] = gamedir + "srboxdhf.asc";
+lines[10] = gamedir + "srboxdhf.ans";
 
 for(i = 0; i < system.nodes; i++) {
 	var nodenum = i + 1;
diff --git a/xtrn/3rdp-install/sunrise-boxpoker.js b/xtrn/3rdp-install/sunrise-boxpoker.js
index 262f09bcc4f42e56583d1735e77d3c90d76c8502..b4e58671089156155aec7afd0be4e3b52f77d2d6 100644
--- a/xtrn/3rdp-install/sunrise-boxpoker.js
+++ b/xtrn/3rdp-install/sunrise-boxpoker.js
@@ -27,10 +27,10 @@ var op = system.operator.split(" ", 2);
 lines[2] = op[0];
 lines[3] = op[1];
 
-lines[8] = gamedir + "srboxpkr.ans";
-lines[9] = gamedir + "srboxpkr.asc";
-lines[10] = gamedir + "srboxpkr.ans";
-lines[11] = gamedir + "srboxpkr.asc";
+lines[8] = gamedir + "srboxpkr.asc";
+lines[9] = gamedir + "srboxpkr.ans";
+lines[10] = gamedir + "srboxpkh.asc";
+lines[11] = gamedir + "srboxpkh.ans";
 
 for (i in system.node_list) {
 	var nodenum = parseInt(i, 10) + 1;
diff --git a/xtrn/3rdp-install/sunrise-concentration.js b/xtrn/3rdp-install/sunrise-concentration.js
index e70023bbf816a689e18b03f52e8696ec15f1785a..2b76940f2a14e4fcfda0e54e4e552572ee0fa6d0 100644
--- a/xtrn/3rdp-install/sunrise-concentration.js
+++ b/xtrn/3rdp-install/sunrise-concentration.js
@@ -28,10 +28,10 @@ var op = system.operator.split(" ", 2);
 lines[2] = op[0];
 lines[3] = op[1];
 
-lines[7] = gamedir + "srcon.ans";
-lines[8] = gamedir + "srcon.asc";
-lines[9] = gamedir + "srconhof.ans";
-lines[10] = gamedir + "srconhof.asc";
+lines[7] = gamedir + "srcon.asc";
+lines[8] = gamedir + "srcon.ans";
+lines[9] = gamedir + "srconhof.asc";
+lines[10] = gamedir + "srconhof.ans";
 
 for (i in system.node_list) {
 	var nodenum = parseInt(i, 10) + 1;
diff --git a/xtrn/3rdp-install/sunrise-cribbage.js b/xtrn/3rdp-install/sunrise-cribbage.js
index 8d7a2dc4b83a135ac10c332c3ceacadbd31821fa..72a1c090b68c3ddba99ff0bc961e2e0a48d6be7c 100644
--- a/xtrn/3rdp-install/sunrise-cribbage.js
+++ b/xtrn/3rdp-install/sunrise-cribbage.js
@@ -31,10 +31,10 @@ var op = system.operator.split(" ", 2);
 lines[2] = op[0];
 lines[3] = op[1];
 
-lines[7] = gamedir + "scrib.ans";
-lines[8] = gamedir + "scrib.asc";
-lines[9] = gamedir + "scribhof.ans";
-lines[10] = gamedir + "scribhof.asc";
+lines[7] = gamedir + "scrib.asc";
+lines[8] = gamedir + "scrib.ans";
+lines[9] = gamedir + "scribhof.asc";
+lines[10] = gamedir + "scribhof.ans";
 lines[11] = "1";
 lines[38] = "G";
 
diff --git a/xtrn/3rdp-install/warlordsofthebalance.ini b/xtrn/3rdp-install/warlordsofthebalance.ini
new file mode 100644
index 0000000000000000000000000000000000000000..37022fc23e4511f42464cd0e28d9bb43349c885a
--- /dev/null
+++ b/xtrn/3rdp-install/warlordsofthebalance.ini
@@ -0,0 +1,39 @@
+Name: Warlords of the Balance
+Desc: Multi-player fantasy wargame
+By:   Evan Taylor, BRC Electronics
+Cats: Games
+Subs: Fantasy,War
+exe: INSTALL.EXE
+
+[md5:bbc09f57ee623d0ffe94b7c3bea189f0]
+ver = 0.97e
+url = ftp://archives.thebbs.org/door_games/doors_u-z/wotb97e.zip
+
+[prog:WOTB]
+name = Warlords of the Balance
+cmd = warlord.bat %f
+ars = DOS
+execution_ars = NOT GUEST
+settings = XTRN_ANSI | XTRN_SH
+type = XTRN_GAP
+
+[exec:../3rdp-install/warlordsofthebalance.js]
+prompt = false
+required = true
+
+[event:WOTBMAINT]
+cmd = event.exe
+name = Warlords of the Balance Maintenance
+; all days
+days = 127
+time = 0:00
+
+[pre-eval:file_exists(startup_dir + 'WARGAME.EXE')]
+prompt = false
+required = true
+fail = Extract the zip files (*.PI~)
+
+[pre-eval:file_exists(startup_dir + 'LORDS.DAT')]
+prompt = false
+required = true
+fail = Run wargen.exe
diff --git a/xtrn/3rdp-install/warlordsofthebalance.js b/xtrn/3rdp-install/warlordsofthebalance.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d2e8d3957c246c8d0af5f85c9502df4a641d2a5
--- /dev/null
+++ b/xtrn/3rdp-install/warlordsofthebalance.js
@@ -0,0 +1,29 @@
+"use strict";
+
+writeln("Generating batch file...");
+
+var i;
+
+var gamedir = fullpath(js.startup_dir);
+
+var conffilesrc = "warlord.bat";
+var cfg_filename = gamedir + conffilesrc;
+
+var lines = [];
+lines[0] = "@echo off";
+lines[1] = "warlord %1";
+lines[2] = "wargame %1";
+
+writeln("Creating " + cfg_filename);
+
+var file = new File(cfg_filename);
+if (!file.open("w")) {
+	writeln("Error " + file.error + " opening " + file.name + " for writing");
+	exit(1)
+}
+file.writeAll(lines);
+file.close();
+
+writeln("Batch file generation complete");
+
+exit(0);
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index 69c0ed2403009a4ff43f2c4bd6af6fa4d9df2bfc..182de5e43cc2805d052bedcf9de0ff4d07ec7737 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -1,5 +1,3 @@
-// $Id: DDMsgReader.js,v 1.143 2020/05/23 23:30:28 nightfox Exp $
-
 /* This is a message reader/lister door for Synchronet.  Features include:
  * - Listing messages in the user's current message area with the ability to
  *   navigate forwards & backwards through the list (and for ANSI users, a
@@ -76,6 +74,9 @@
  * 2020-12-01 Eric Oulashin     Version 1.39
  *                              When forwarding a message, added the ability to
  *                              optionally edit the message before forwarding it.
+ * 2021-01-31 Michael Long      Version 1.40
+ *                              Fixed left/right colors not being customizable on message
+ *                              list lightbar
  */
 
 
@@ -187,8 +188,8 @@ if (system.version_num < 31500)
 }
 
 // Reader version information
-var READER_VERSION = "1.39";
-var READER_DATE = "2020-12-01";
+var READER_VERSION = "1.40";
+var READER_DATE = "2021-01-31";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -7303,13 +7304,13 @@ function DigDistMsgReader_SetMsgListPauseTextAndLightbarHelpLine()
 		var numLeft = Math.floor(numChars / 2);
 		var numRight = numChars - numLeft;
 		for (var i = 0; i < numLeft; ++i)
-			this.msgListLightbarModeHelpLine = "�" + this.msgListLightbarModeHelpLine;
+			this.msgListLightbarModeHelpLine = " " + this.msgListLightbarModeHelpLine;
 		this.msgListLightbarModeHelpLine = "\1n"
 		                             + this.colors.lightbarMsgListHelpLineBkgColor
 		                             + this.msgListLightbarModeHelpLine;
 		this.msgListLightbarModeHelpLine += "\1n" + this.colors.lightbarMsgListHelpLineBkgColor;
 		for (var i = 0; i < numRight; ++i)
-			this.msgListLightbarModeHelpLine += "�";
+			this.msgListLightbarModeHelpLine += ' ';
 	}
 }
 // For the DigDistMsgReader Class: Sets the hotkey help line for the enhanced
@@ -13358,8 +13359,7 @@ function DigDistMsgReader_ForwardMessage(pMsgHdr, pMsgBody)
 			newMsgBody += "==================================\n\n";
 			newMsgBody += pMsgBody;
 
-			// New - Editing the message
-			// TODO: Ask whether to edit the message before forwarding it,
+			// Ask whether to edit the message before forwarding it,
 			// and use console.editfile(filename) to edit it.
 			if (!console.noyes("Edit the message before sending"))
 			{
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index 46c3e6ce0fffc3598c2f4aaea86b67f9e419615b..bb1a3b80f7f0ffd015acab3d89d4ed0ac3d1dc71 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
-                                 Version 1.39
-                           Release date: 2020-12-01
+                                 Version 1.40
+                           Release date: 2021-12-31
 
                                      by
 
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index 8dd53fa16c6b3dec162927e48508c174e5d6e8f6..edf89b56a5accc85e9637872b574ba6fec06446b 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,6 +5,8 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.40     2021-01-31   (Michael Long) Fixed left/right colors not being
+                      customizable on message list lightbar
 1.39     2020-12-01   When forwarding a message, added the ability to optinally
                       edit the message before forwarding it.
 1.38     2020-11-26   Bug fix: When forwarding a message, it now correctly sets
diff --git a/xtrn/xtrnmenu/099-xtrnmenu-games.xjs b/xtrn/xtrnmenu/099-xtrnmenu-games.xjs
new file mode 100644
index 0000000000000000000000000000000000000000..6f2a37dc9086f0aac5345d8e25fb70c4b9c75100
--- /dev/null
+++ b/xtrn/xtrnmenu/099-xtrnmenu-games.xjs
@@ -0,0 +1,209 @@
+<!--Games-->
+<?xjs
+/**
+ * Web Display for Custom External Program Menus
+ * by Michael Long mlong  innerrealmbbs.us
+ * 
+ * See wiki at http://wiki.synchro.net/module:xtrnmenu
+ */
+
+    load("ftelnethelper.js");
+    load('sbbsdefs.js');
+    load("xtrnmenulib.js");
+    load(settings.web_lib + 'ftelnet.js');
+    load(settings.web_lib + 'request.js');
+
+    var ExternalMenus = new ExternalMenus();
+
+    if (typeof settings.xtrn_blacklist === 'string') {
+        settings.xtrn_blacklist = settings.xtrn_blacklist.toLowerCase().split(',');
+    } else {
+        settings.xtrn_blacklist = [];
+    }
+
+    var menuitems = [];
+?>
+
+<style>.fTelnetStatusBar { display : none; }</style>
+
+<a name="fTelnet"></a>
+<div id="fTelnetContainer" class="fTelnetContainer" style="margin-bottom:1em;"></div>
+
+<div id="xtrn-list" class="list-group">
+
+<?xjs
+    var menuobj;
+    if (Request.get_param('type')) {
+        var target = Request.get_param('target');
+        switch (Request.get_param('type')) {
+	        case 'custommenu':
+                if (target !== undefined) {
+			        menuobj = ExternalMenus.getMenu(target);
+                }
+                break;
+            case 'xtrnmenu':
+                if (target !== undefined) {
+			        menuobj = ExternalMenus.getSectionMenu(target);
+                }
+                break;
+            case 'recentall':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Most Recent - All", target);
+                break;
+            case 'recentuser':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Most Recent - Yours", target);
+                break;
+            case 'mostlaunchedall':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Most Launched - All", target);
+                break;
+            case 'mostlauncheduser':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Most Launched - Yours", target);
+                break;
+            case 'longestrunall':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Longest Run - All", target);
+                break;
+            case 'longestrunuser':
+                menuobj = ExternalMenus.getSpecial(Request.get_param('type'), "Longest Run - Yours", target);
+                break;
+            case 'favorites':                
+                menuobj = ExternalMenus.getFavorites("Favorites", target);
+                break;
+            default:
+                break;
+        }
+    } else {
+        if (Request.has_param('target')) {
+            menuobj = ExternalMenus.getMenu(Request.get_param('target'));
+        } else {
+            menuobj = ExternalMenus.getMenu('main');
+        }
+    }
+
+    if ((typeof menuobj === "undefined") || !menuobj
+        || (typeof menuobj.items === "undefined") || (menuobj.items.length < 1)) {
+        writeln("<h4>" + ExternalMenus.options.no_programs_msg + "</h4>");
+    } else if (user.security.restrictions&UFLAG_X) {
+        writeln("<h4>" + options.restricted_user_msg + "</h4>");
+    } else {
+        // ok to display menu
+
+        writeln("<h4>" + menuobj.title + "</h4>");
+        var menuitemsfiltered = ExternalMenus.getSortedItems(menuobj);
+        menuitemsfiltered.forEach(function (menuitem) {
+            if (settings.xtrn_blacklist.indexOf(menuitem.target.toLowerCase()) > -1) {
+                return;
+            }
+            switch (menuitem.type) {
+                case 'custommenu':
+                case 'xtrnmenu':
+                case 'xtrnprog':
+                case 'favorites':
+                case 'recentall':
+                case 'recentuser':
+                case 'mostlaunchedall':
+                case 'mostlauncheduser':
+                case 'longestrunuser':
+                case 'longestrunall':
+                    menuitems.push({
+                        'itemtitle': menuitem.title,
+                        'itemtype': menuitem.type,
+                        'itemtarget': menuitem.target,
+                        'stats': typeof menuitem.stats !== undefined ? menuitem.stats : null
+                    });
+                    break;
+                default:
+                  break;
+            }
+        });
+    }
+?>
+</div>
+
+<script id="fTelnetScript" src="<?xjs write(get_url()); ?>"></script>
+<script type="text/javascript">
+	window.addEventListener('load', (event) => {
+		var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>;
+		var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>;
+		var Options = new fTelnetOptions();
+		Options.BareLFtoCRLF = false;
+		Options.BitsPerSecond = 57600;
+		Options.ConnectionType = 'rlogin';
+		Options.Emulation = 'ansi-bbs';
+		Options.Enter = '\r';
+		Options.Font = 'CP437';
+		Options.ForceWss = false;
+		Options.Hostname = '<?xjs write(http_request.vhost); ?>';
+		Options.LocalEcho = false;
+		Options.Port = location.protocol == 'https:' ? wssp : wsp;
+		Options.RLoginClientUsername = '<?xjs write(user.security.password); ?>';
+		Options.RLoginServerUsername = '<?xjs write(user.alias); ?>';
+		Options.ScreenColumns = 80;
+		Options.ScreenRows = 25;
+		Options.SplashScreen = Options.SplashScreen = '<?xjs write(get_splash()); ?>';
+		Options.WebSocketUrlPath = '?Port=<?xjs write(GetRLoginPort()); ?>';
+		window.fTelnet = new fTelnetClient('fTelnetContainer', Options);
+		fTelnet.OnConnectionClose = function () {
+			window.location.reload();
+		};
+	});
+
+    async function launchXtrn() {
+        var code = event.srcElement.id;
+        await v4_get('./api/system.ssjs?call=set-xtrn-intent&code=' + code);
+        fTelnet._Options.RLoginTerminalType = 'xtrn=' + code;
+        fTelnet.Connect();
+    }
+
+    var menuitems = <?xjs write(JSON.stringify(menuitems)); ?>;
+    var currentTarget = "<?xjs Request.write_param('target'); ?>";
+    var currentType = "<?xjs Request.write_param('type'); ?>";
+    var currentTitle = `<?xjs write(menuobj.title) ?>`;
+    if (currentTitle && !currentTarget) {
+        // main menu - store title for breadcrumb
+        sessionStorage.setItem("mainmenu", currentTitle);
+    }
+    var div = $('#xtrn-list');
+    
+    menuitems.forEach(function (menuitem) {
+        var a = document.createElement('a');
+        $(a).addClass("list-group-item");
+        $(a).addClass("striped");
+        a.text = menuitem.itemtitle;
+        
+        if (typeof menuitem.stats !== "undefined") {
+            var badge = document.createElement('span');
+            $(badge).addClass('badge').text(menuitem.stats);
+            a.appendChild(badge);
+        }
+
+        if (menuitem.itemtype == "xtrnprog") {
+            a.href = "#fTelnet";
+            a.id = menuitem.itemtarget;
+            a.onclick = function () { launchXtrn(); };
+        } else {
+            a.href = "/?page=<?xjs Request.write_param('page') ?>&type=" +menuitem.itemtype
+                + "&target=" + menuitem.itemtarget;
+            a.onclick = function () {
+                sessionStorage.setItem('prev:' + menuitem.itemtarget.toLowerCase(), '<?xjs Request.write_param('target')?>');
+                sessionStorage.setItem('prevtype:' + menuitem.itemtarget.toLowerCase(), '<?xjs Request.write_param('type')?>');
+                sessionStorage.setItem('prevtitle:' + menuitem.itemtarget.toLowerCase(), currentTitle);
+            };
+        }
+        $(div).append(a);
+    });
+
+    // breadcrumb
+    var prevTarget = sessionStorage.getItem('prev:' + currentTarget.toLowerCase());
+    var prevType = sessionStorage.getItem('prevtype:' + currentTarget.toLowerCase());
+    var prevTitle = sessionStorage.getItem('prevtitle:' + currentTarget.toLowerCase());
+    if (prevType && prevTarget && prevTitle) {
+        $('#xtrn-list').prepend('<ol class="breadcrumb"><a href="/?page=<?xjs Request.write_param('page') ?>&type=' + prevType
+        + '&target=' + prevTarget + '">' + prevTitle + '</a></ol>');
+    } else if (currentTarget) {
+        // level 2, not main menu
+        var mainmenuTitle = sessionStorage.getItem('mainmenu');
+        if (!mainmenuTitle) {
+            mainmenuTitle = 'Games';
+        }
+        $('#xtrn-list').prepend('<ol class="breadcrumb"><a href="/?page=<?xjs Request.write_param('page') ?>">' + mainmenuTitle + '</a></ol>');
+    }
+</script>
diff --git a/xtrn/xtrnmenu/099-xtrnmenu-gamestats.xjs b/xtrn/xtrnmenu/099-xtrnmenu-gamestats.xjs
new file mode 100644
index 0000000000000000000000000000000000000000..240f19c0aecc020414075058d367e8f56771f480
--- /dev/null
+++ b/xtrn/xtrnmenu/099-xtrnmenu-gamestats.xjs
@@ -0,0 +1,54 @@
+<!--Game Stats-->
+<div class="list-group">
+
+<h1>Game Stats</h1>
+
+<?xjs
+    load('sbbsdefs.js');
+    load("xtrnmenulib.js");
+    
+    var ExternalMenus = new ExternalMenus();
+      
+    if (typeof settings.xtrn_blacklist === 'string') {
+        settings.xtrn_blacklist = settings.xtrn_blacklist.toLowerCase().split(',');
+    } else {
+        settings.xtrn_blacklist = [];
+    }
+
+	// Edit these lines as needed to adjust the page
+	gameStats("recentall", "Most Recent", 10);
+	gameStats("mostlaunchedall", "Top 15 Most Launched", 15);
+	gameStats("longestrunall", "Top 15 Longest Run", 15);
+	
+	function gameStats(menutype, title, maxitems) {
+	    var menuobj = ExternalMenus.getSpecial(menutype, title); 
+
+?>
+<div class="row list-group-item" style="background-color: #993399; color: #FFF">
+	<div class="col-sm-12"><h3><?xjs write(title) ?></h3></div>
+</div>
+
+<?xjs        
+		if ((typeof menuobj.items !== "undefined") || (menuobj.items.length > 0)) {
+			var i = 0;
+			menuobj.items.some(function (menuitem) {
+				if (menuitem.type != "xtrnprog") {
+					return;
+				}
+		
+				if (settings.xtrn_blacklist.indexOf(menuitem.target.toLowerCase()) > -1) return;
+				
+				if (++i > maxitems) return;
+?>
+
+<div class="row list-group-item striped">
+	<div class="col-sm-2"><span class="badge badge-inverse"><?xjs write(menuitem.stats) ?></span></div>
+	<div class="col-sm-10"><?xjs write(menuitem.title) ?></div>
+</div>
+
+<?xjs		
+			});
+		}
+	}
+?>	
+</div>