From 3329e0f1432fbe25b528df4e26463a5438e62cca Mon Sep 17 00:00:00 2001
From: Eric Oulashin <eric.oulashin@gmail.com>
Date: Mon, 5 Feb 2024 18:51:48 -0800
Subject: [PATCH] DDMsgReader: Use the P_UTF8 mode bit when writing UTF8 header
 information so it looks right on UTF-8 terminals. This also includes a
 dd_lightbar_menu.js update (for the message list).

---
 exec/load/dd_lightbar_menu.js         |   40 +-
 xtrn/DDMsgReader/DDMsgReader.js       |   66 +-
 xtrn/DDMsgReader/dd_lightbar_menu.js  | 4017 +++++++++++++++++++++++++
 xtrn/DDMsgReader/readme.txt           |    4 +-
 xtrn/DDMsgReader/revision_history.txt |    7 +-
 5 files changed, 4108 insertions(+), 26 deletions(-)
 create mode 100644 xtrn/DDMsgReader/dd_lightbar_menu.js

diff --git a/exec/load/dd_lightbar_menu.js b/exec/load/dd_lightbar_menu.js
index 8e7b65536c..b24956ad48 100644
--- a/exec/load/dd_lightbar_menu.js
+++ b/exec/load/dd_lightbar_menu.js
@@ -355,11 +355,15 @@ if (typeof(require) === "function")
 {
 	require("sbbsdefs.js", "K_UPPER");
 	require("mouse_getkey.js", "mouse_getkey");
+	require("userdefs.js", "USER_UTF8");
+	require("utf8_cp437.js", "utf8_cp437");
 }
 else
 {
 	load("sbbsdefs.js");
 	load("mouse_getkey.js");
+	load("userdefs.js");
+	load("utf8_cp437.js");
 }
 
 
@@ -543,6 +547,7 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 	this.DrawPartial = DDLightbarMenu_DrawPartial;
 	this.DrawPartialAbs = DDLightbarMenu_DrawPartialAbs;
 	this.GetItemText = DDLightbarMenu_GetItemText;
+	this.ItemTextIsUTF8 = DDLightbarMenu_ItemTextIsUTF8;
 	this.Erase = DDLightbarMenu_Erase;
 	this.SetItemHotkey = DDLightbarMenu_SetItemHotkey;
 	this.AddItemHotkey = DDLightbarMenu_AddItemHotkey;
@@ -633,10 +638,12 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 //  pRetval: The value to return when the item is chosen.  Can be any type of value.
 //  pHotkey: Optional - A key to select the item when pressed by the user
 //  pSelectable: Optional - Whether or not the item is to be selectable. Defaults to true.
-function DDLightbarMenu_Add(pText, pRetval, pHotkey, pSelectable)
+//  pIsUTF8: Optional boolenan - Whether or not the text is UTF-8.  Defaults to false.
+function DDLightbarMenu_Add(pText, pRetval, pHotkey, pSelectable, pIsUTF8)
 {
 	var item = getDefaultMenuItem();
 	item.text = pText;
+	item.textIsUTF8 = (typeof(pIsUTF8) === "boolean" ? pIsUTF8 : false);
 	item.retval = (pRetval == undefined ? this.NumItems() : pRetval);
 	item.isSelectable = (typeof(pSelectable) === "boolean" ?  pSelectable : true);
 	// If pHotkey is defined, then use it as the hotkey.  Otherwise, if
@@ -1165,6 +1172,16 @@ function DDLightbarMenu_DrawBorder()
 function DDLightbarMenu_WriteItem(pIdx, pItemLen, pHighlight, pSelected, pScreenX, pScreenY)
 {
 	var itemText = this.GetItemText(pIdx, pItemLen, pHighlight, pSelected);
+	// If the text is UTF-8 and the user's terminal is UTF-8, then set the mode bit accordingly.
+	// If the text is UTF-8 and the user's terminal doesn't support UTF-8, convert the text to cp437.
+	var printModeBits = P_NONE;
+	if (this.ItemTextIsUTF8(pIdx))
+	{
+		if (console.term_supports(USER_UTF8))
+			printModeBits = P_UTF8;
+		else
+			itemText = utf8_cp437(itemText);
+	}
 	// If this.nextDrawOnlyItemSubstr is an object with start & end properties,
 	// then create a string that is shortened from itemText from those start & end
 	// indexes, and add color to it.
@@ -1174,10 +1191,10 @@ function DDLightbarMenu_WriteItem(pIdx, pItemLen, pHighlight, pSelected, pScreen
 		var len = this.nextDrawOnlyItemSubstr.end - this.nextDrawOnlyItemSubstr.start;
 		var shortenedText = substrWithAttrCodes(itemText, this.nextDrawOnlyItemSubstr.start, len);
 		console.gotoxy(pScreenX+this.nextDrawOnlyItemSubstr.start, pScreenY);
-		console.print(shortenedText + "\x01n");
+		console.print(shortenedText + "\x01n", printModeBits);
 	}
 	else
-		console.print(itemText + "\x01n");
+		console.print(itemText + "\x01n", printModeBits);
 }
 
 // Writes a menu item at its location on the menu.  This should only be called
@@ -1595,6 +1612,22 @@ function DDLightbarMenu_GetItemText(pIdx, pItemLen, pHighlight, pSelected)
 	return itemText;
 }
 
+// Returns whether or not an item's text is UTF-8, as specified in the item.
+//
+// Parameters:
+//  pIdx: The index of the item
+//
+// Return value: Whether or not the item's text is UTF-8
+function DDLightbarMenu_ItemTextIsUTF8(pIdx)
+{
+	if (typeof(pIdx) !== "number")
+		return false;
+	if (pIdx < 0 || pIdx >= this.NumItems())
+		return false;
+
+	return this.GetItem(pIdx).textIsUTF8;
+}
+
 // Erases the menu - Draws black (normal color) where the menu was
 function DDLightbarMenu_Erase()
 {
@@ -3576,6 +3609,7 @@ function getBackgroundAttrAtIdx(pAttrs, pIdx)
 function getDefaultMenuItem() {
 	return {
 		text: "",
+		textIsUTF8: false,
 		retval: null,
 		hotkeys: "",
 		useAltColors: false,
diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index 4220d57160..3ff6640591 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -145,6 +145,9 @@
  *                              indexedModeMenuSnapToNextWithNewAftarMarkAllRead
  * 2024-01-23 Eric Oulashin     Version 1.95a
  *                              Bug fix: Abort when sub-board code isn't available when editing personal email
+ * 2024-02-04 Eric Oulashin     Version 1.95b
+ *                              Bug fix: Use the P_UTF8 mode bit when printing UTF-8 message header info (such as 'from' and 'to').
+ *                              A dd_lightbar_menu.js update goes along with this.
  */
 
 "use strict";
@@ -248,10 +251,9 @@ load('822header.js');
 var ansiterm = require("ansiterm_lib.js", 'expand_ctrl_a');
 var hexdump = load('hexdump_lib.js');
 
-
 // Reader version information
-var READER_VERSION = "1.95a";
-var READER_DATE = "2024-01-23";
+var READER_VERSION = "1.95b";
+var READER_DATE = "2024-02-04";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -4407,6 +4409,7 @@ function DigDistMsgReader_CreateLightbarMsgListMenu()
 			// When setting the item text, call PrintMessageInfo with true as
 			// the last parameter to return the string instead
 			menuItemObj.text = strip_ctrl(this.msgReader.PrintMessageInfo(msgHdr, false, itemIdx+1, true));
+			menuItemObj.textIsUTF8 = msgHdr.hasOwnProperty("is_utf8") && msgHdr.is_utf8;
 			menuItemObj.retval = msgHdr.number;
 			var msgIsToUser = userHandleAliasNameMatch(msgHdr.to);
 			var msgIsFromUser = userHandleAliasNameMatch(msgHdr.from);
@@ -4753,11 +4756,10 @@ function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pRet
 	var fromName = pMsgHeader.from;
 	var toName = pMsgHeader.to;
 	var subject = pMsgHeader.subject;
-	if (pMsgHeader.hasOwnProperty("is_utf8") && pMsgHeader.is_utf8)
+	var userConsoleSupportsUTF8 = (typeof(USER_UTF8) != "undefined" ? console.term_supports(USER_UTF8) : false);
+	var msgIsUTF8 = pMsgHeader.hasOwnProperty("is_utf8") && pMsgHeader.is_utf8;
+	if (msgIsUTF8)
 	{
-		var userConsoleSupportsUTF8 = false;
-		if (typeof(USER_UTF8) != "undefined")
-			userConsoleSupportsUTF8 = console.term_supports(USER_UTF8);
 		if (!userConsoleSupportsUTF8)
 		{
 			fromName = utf8_cp437(fromName);
@@ -4862,7 +4864,8 @@ function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pRet
 	var returnStrInstead = (typeof(pReturnStrInstead) == "boolean" ? pReturnStrInstead : false);
 	if (!returnStrInstead)
 	{
-		console.print(msgHdrStr);
+		var printMode = (terminalSupportsUTF8 && hdrIsUTF8 ? P_UTF8 : P_NONE);
+		console.print(msgHdrStr, printMode);
 		console.cleartoeol("\x01n"); // To clear away any extra text that may have been entered by the user
 	}
 	return msgHdrStr;
@@ -9788,6 +9791,13 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr)
 	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
 		return;
 
+	// Check whether the header is UTF-8 and whether the user's terminal supports UTF-8, and
+	// set the print mode accordingly (the header fields should already be converted from
+	// UTF-8 to cp437 if the header is UTF-8 and the user's terminal doesn't support UTF-8)
+	var terminalSupportsUTF8 = (typeof(USER_UTF8) != "undefined" ? console.term_supports(USER_UTF8) : false);
+	var hdrIsUTF8 = pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8;
+	var printMode = (terminalSupportsUTF8 && hdrIsUTF8 ? P_UTF8 : P_NONE);
+
 	// Note: The message header has the following fields:
 	// 'number': The message number
 	// 'offset': The message offset
@@ -9800,7 +9810,7 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr)
 	//var dateTimeStr = strftime("%Y-%m-%d %H:%M:%S", msgHeader.when_imported_time)
 	// Use the date text in the message header, without the time
 	// zone offset at the end.
-	var dateTimeStr = pMsgHdr["date"].replace(/ [-+][0-9]+$/, "");
+	var dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, "");
 
 	// Check to see if there is a msghdr file in the sbbs/text/menu
 	// directory.  If there is, then use it to display the message
@@ -9828,7 +9838,7 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr)
 				// message read prompt, this script now has to parse & replace some of
 				// the @-codes in the message header line, since Synchronet doesn't know
 				// that the user is reading a message.
-				console.putmsg(this.ParseMsgAtCodes(fileLine, pMsgHdr, null, dateTimeStr, false, true));
+				console.putmsg(this.ParseMsgAtCodes(fileLine, pMsgHdr, null, dateTimeStr, false, true), printMode);
 				console.crlf();
 			}
 			msgHdrFile.close();
@@ -9845,15 +9855,15 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr)
 		console.print("\x01n\x01w" + charStr(HORIZONTAL_DOUBLE, 78));
 		console.crlf();
 		var horizSingleFive = charStr(HORIZONTAL_SINGLE, 5);
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cFrom\x01w\x01h: \x01b" + pMsgHdr["from"].substr(0, console.screen_columns-12));
+		console.print("\x01n\x01w" + horizSingleFive + "\x01cFrom\x01w\x01h: \x01b" + pMsgHdr.from.substr(0, console.screen_columns-12), printMode);
 		console.crlf();
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cTo  \x01w\x01h: \x01b" + pMsgHdr["to"].substr(0, console.screen_columns-12));
+		console.print("\x01n\x01w" + horizSingleFive + "\x01cTo  \x01w\x01h: \x01b" + pMsgHdr.to.substr(0, console.screen_columns-12), printMode);
 		console.crlf();
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cSubj\x01w\x01h: \x01b" + pMsgHdr["subject"].substr(0, console.screen_columns-12));
+		console.print("\x01n\x01w" + horizSingleFive + "\x01cSubj\x01w\x01h: \x01b" + pMsgHdr.subject.substr(0, console.screen_columns-12), printMode);
 		console.crlf();
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cDate\x01w\x01h: \x01b" + dateTimeStr.substr(0, console.screen_columns-12));
+		console.print("\x01n\x01w" + horizSingleFive + "\x01cDate\x01w\x01h: \x01b" + dateTimeStr.substr(0, console.screen_columns-12), printMode);
 		console.crlf();
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cAttr\x01w\x01h: \x01b" + allMsgAttrStr.substr(0, console.screen_columns-12));
+		console.print("\x01n\x01w" + horizSingleFive + "\x01cAttr\x01w\x01h: \x01b" + allMsgAttrStr.substr(0, console.screen_columns-12), printMode);
 		console.crlf();
 	}
 }
@@ -11492,7 +11502,7 @@ function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartS
 		return;
 	if ((enhMsgHdrLines.length == 0) || (this.enhMsgHeaderWidth == 0))
 		return;
-
+	
 	// Create a formatted date & time string.  Adjust the message's time to
 	// the BBS local time zone if possible.
 	var dateTimeStr = "";
@@ -11505,7 +11515,25 @@ function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartS
 	}
 	else
 		dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, "");
+
+	// Check whether the header is UTF-8 and whether the user's terminal supports UTF-8, and
+	// set the print mode accordingly (the header fields should already be converted from
+	// UTF-8 to cp437 if the header is UTF-8 and the user's terminal doesn't support UTF-8)
+	var terminalSupportsUTF8 = (typeof(USER_UTF8) != "undefined" ? console.term_supports(USER_UTF8) : false);
+	var hdrIsUTF8 = pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8;
+	var printMode = (terminalSupportsUTF8 && hdrIsUTF8 ? P_UTF8 : P_NONE);
 	
+	// If the message is in UTF-8 and the user's terminal supports UTF-8, then we'll be printing
+	// with the P_UTF8 mode bit. We'll need to convert the display header lines to UTF-8 so they'll
+	// display properly.
+	if (hdrIsUTF8 && terminalSupportsUTF8)
+	{
+		var hdrLines_UTF8 = []; // We need to make a copy so we don't modify the original
+		for (var i = 0; i < enhMsgHdrLines.length; ++i)
+			hdrLines_UTF8.push(utf8_encode(enhMsgHdrLines[i]));
+		enhMsgHdrLines = hdrLines_UTF8;
+	}
+
 	var enhHdrLines = enhMsgHdrLines.slice(0);
 	// Do some things if using the internal header (not loaded externally)
 	if (this.usingInternalEnhMsgHdr)
@@ -11547,7 +11575,7 @@ function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartS
 		{
 			console.gotoxy(screenX, screenY++);
 			console.putmsg(this.ParseMsgAtCodes(enhHdrLines[hdrFileIdx], pMsgHdr,
-			               pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false));
+			               pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false), printMode);
 		}
 		// Older - Used to center the header lines, but I'm not sure this is necessary,
 		// and it might even make the header off by one, which could be bad.
@@ -11582,7 +11610,7 @@ function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartS
 		for (var hdrFileIdx = 0; hdrFileIdx < enhHdrLines.length; ++hdrFileIdx)
 		{
 			console.putmsg(this.ParseMsgAtCodes(enhHdrLines[hdrFileIdx], pMsgHdr,
-			               pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false));
+			               pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false), printMode);
 		}
 		// Note: Avatar display is only supported for ANSI
 	}
@@ -20145,7 +20173,7 @@ function parseLoadableModuleArgs(argv)
 {
 	// TODO: Allow indexed reader mode?
 	//argVals.indexedmode = true;
-
+	
 	var argVals = getDefaultArgParseObj();
 
 	var allDigitsRegex = /^[0-9]+$/; // To check if a string consists only of digits
diff --git a/xtrn/DDMsgReader/dd_lightbar_menu.js b/xtrn/DDMsgReader/dd_lightbar_menu.js
new file mode 100644
index 0000000000..91b641466b
--- /dev/null
+++ b/xtrn/DDMsgReader/dd_lightbar_menu.js
@@ -0,0 +1,4017 @@
+/* Digital Distortion Lightbar Menu library
+ * Author: Eric Oulashin (AKA Nightfox)
+ * BBS: Digital Distortion
+ * Addresses: digitaldistortionbbs.com
+ *            digdist.synchro.net
+
+This is a lightbar menu library.  This allows creating a scrollable menu of
+text items for the user to choose from.  The user can naviate the list using
+the up & down arrows, PageUp, PageDown, Home, and End keys.  The enter key
+selects an item.  The ESC key will exit the menu and return null.
+This menu library requires the use of an ANSI terminal.
+By default, this menu library does not display a border around the menu.
+If you want this library to draw a border around the menu, you can set the
+borderEnabled property to true.  Without a border, the menu gains 2
+characters of width and 2 lines of height.  If using a border, a title (text)
+can be displayed in the top border by setting the topBorderText property (it
+defaults to an empty string, for no title).
+
+This script provides an object, DDLightbarMenu.  Use the DDLightbarMenu
+constructor to create the object.  Some other notable methods:
+Add()
+SetItemHotkey()
+AddItemHotkey()
+SetPos()
+SetSize()
+GetVal()
+AddAdditionalSelectItemKeys()
+SetBorderChars()
+SetColors()
+
+To change the colors used for displaying the items, you can change the values
+in the colors object within the DDLightbarMenu object.  These are the current
+supported colors:
+itemColor: The color to use for non-selected items (current default is white
+           on blue).  This can be a string (with the color/attribute values)
+           or an array to specify colors for different sections of the item
+		   text to display in the menu.  See the note on item color arrays
+		   below.
+selectedItemColor: The color to use for selected items (current default is blue
+                   on white).  This can be a string (with the color/attribute values)
+                   or an array to specify colors for different sections of the item
+		           text to display in the menu.  See the note on item color arrays
+		           below.
+itemTextCharHighlightColor: The color of a highlighted non-space character in an
+                            item text (specified by having a & in the item text).
+							It's important not to specify a "\x01n" in here in case
+							the item text should have a background color.
+unselectableItemColor: The color to use for items that are not selectable
+borderColor: The color for the borders (if borders are enabled)
+You can also call SetColors() and pass in a JS object with any or all of the
+above properties to set the colors internally in the DDLightbarMenu object.
+
+Item color arrays: Currently, colors.itemColor and colors.seletedItemColor within
+a DDLightbarMenu object can be either a string (containing color/attribute codes)
+or an array with color/attribute codes for different sections of the item strings
+to display in the menu.  The array is to contain objects with the following
+properties:
+start: The index of the first character in the item string to apply the colors to
+end: One past the last character index in the string to apply the colors to
+attrs: The Synchronet attribute codes to apply to the section of the item string
+For the last item, the 'end' property can be -1, 0, or greater than the length
+of the item to apply the color/attribute codes to the rest of the string.
+
+
+By default, the menu selection will wrap around to the beginning/end when using
+the down/up arrows.  That behavior can be disabled by setting the wrapNavigation
+property to false.
+
+You can enable the display of a scrollbar by setting the scrollbarEnabled property
+to true.  By default, it is false.  For instance (assuming the menu object is lbMenu):
+lbMenu.scrollbarEnabled = true;
+The scrollbar can help to visually show how far the user is through the menu.  When
+enabled, the scrollbar will appear on the right side of the menu.  If borders are enabled,
+the scrollbar will appear just inside the right border. Also, if the scrollbar is
+enabled but all the items would fit in a single "page" in the menu, then the scrollbar
+won't be displayed.
+The scrollbar uses block characters to draw the scrollbar: ASCII character 176 for
+the background and ASCII 177 for the block that moves on the scrollbar.  If you want
+to change those characters, you can change the scrollbarInfo.BGChar and
+scrollbarInfo.blockChar properties in the menu object.
+By default, the scrollbar colors are high (bright) black for the background and high
+(bright) white for the moving block character.  If desired, those can be changed
+with the colors.scrollbarBGColor and colors.scrollbarScrollBlockColor properties in
+the menu object.
+
+This menu object supports adding multiple hotkeys to each menu item.  A hotkey
+can be specified in the Add() method a couple of different ways - By specifying
+a hotkey as the 3rd parameter and/or by putting a & in the menu item text just
+before a key you want to use as the hotkey.  For example, in the text "E&xit",
+"x" would be used as the hotkey for the item.  If you want to disable the use of
+ampersands for hotkeys in menu items (for instance, if you want an item to
+literally display an ampersand before a character), set the
+ampersandHotkeysInItems property to false.  For instance:
+lbMenu.ampersandHotkeysInItems = false;
+Note that ampersandHotkeysInItems must be set before adding menu items.
+
+You can call the SetItemHotkey() method to set a single hotkey to be used for
+a menu item or AddItemHotkey() to add an additional hotkey for an item in
+addition to any existing hotkeys it might already have.
+
+You can call AddAdditionalSelectItemKeys() to add additional keys that can be
+used to select any item (in addition to Enter).  That function takes a string
+of characters, and the keys are case-sensitive.  For example, to add the key E
+to select an item:
+lbMenu.AddAdditionalSelectItemKeys("E");
+To make a case-insensitive verison, both the uppercase and lowercase letter
+would need to be added, as in the following example for E:
+lbMenu.AddAdditionalSelectItemKeys("Ee");
+
+Also, after showing the menu & getting a value from the user (using the GetVal()
+function), the lastUserInput property will have the user's last keypress.
+
+This menu class also supports an optional "numbered mode", where each option is
+displayed with a number to the left (starting at 1), and the user is allowed to
+choose an option by typing the number of the item.  Numbered mode is disabled
+by default and can be enabled by setting the numberedMode property to true.
+For example:
+lbMenu.numberedMode = true;
+When numbered mode is enabled and the user starts typing a number, the menu will
+prompt the user for an item number.  Note that the prompt will be located on the
+line below the menu, so in addition to the menu's height, you'll also need an
+extra line on the screen to account for the item prompt.  In addition, when the
+user presses the enter key after the item number, a carriage return/line feed
+will be outputted, so in numbered mode, the menu's height should not go further
+than 2 lines below the console height.  Otherwise, the display of the menu will
+not be correct if the user decides not to enter a number.
+When numbered mode is enabled, you can specify the color used to display the
+item numbers.  For a non-selected item, set .colors.itemNumColor.  For selected items,
+set .colors.highlightedItemNumColor.  This is separate from the item color setting
+(.colors.itemColor).  For example:
+lbMenu.colors.itemNumColor = "\x01c"; // Use cyan for the item numbers for non-selected items.
+// For the selected item, use high cyan with a blue background for the item number
+lbMenu.colors.highlightedItemNumColor = "\x01" + "4\x01c\x01h";
+
+This menu also supports multiple options selected (by default, that is not enabled).
+To enable that, set the multiSelect property to true.  When enabled, the GetVal()
+method will return an array of the user's selections rather than a string (or null if
+the user aborted).  You can also set a limit on the number of items selected in
+multi-select mode by setting the maxNumSelections property.  The default value is -1,
+which means no limit (0 also means no limit).
+Example, with a limit:
+lbMenu.multiSelect = true;
+lbMenu.maxNumSelections = 5;
+
+Example usage:
+require("dd_lightbar_menu.js", "DDLightbarMenu");
+// Create a menu at position 1, 3 with width 45 and height of 10
+var lbMenu = new DDLightbarMenu(1, 3, 45, 10);
+// Add 12 items to the menu, each of which will return the text of the item
+for (var i = 0; i < 12; ++i)
+	lbMenu.Add("Item " + +(i+1), "Item " + +(i+1));
+// Set up the hotkey "s" to select the 2nd item
+lbMenu.SetItemHotkey(1, "s");
+// Show the menu and get the chosen item from the user
+var val = lbMenu.GetVal();
+// Output the chosen menu item
+console.print("\x01n\r\n");
+console.print("Value:" + val + ":, type: " + typeof(val) + "\r\n");
+console.pause();
+
+// Changing the normal item color to green & selected item color to bright green:
+lbMenu.colors.itemColor = "\x01n\x01g";
+lbMenu.colors.selectedItemColor = "\x01n\x01h\x01g";
+
+// Disabling the navigation wrap behavior:
+lbMenu.wrapNavigation = false;
+
+// If you want a particular character in an item's text highlighted with
+// a different color, you can put a & character immediately before it, as
+// long as it's not a space.  For instance, to highlight the "x" in "Exit":
+lbMenu.Add("E&xit", -1);
+
+// The last parameter to Add() is a boolean specifying whether the item text is
+// in UTF-8 format.
+// Adding an item where the text is known to be in UTF-8 format
+lbMenu.Add(someonesName, 1, null, true, true);
+
+To enable borders, set the borderEnabled property to true.  Example:
+lbMenu.borderEnabled = true;
+
+The menu object has an object called borderChars, which stores the characters used
+to draw the border.  you can change the characters used to draw the border by
+setting the following properties of the borderChars object:
+  upperLeft: The character to use for the upper-left corner
+  upperRight: The character to use for the upper-right corner
+  lowerLeft: The character to use for the lower-left corner
+  lowerRight: The character to use for the lower-right corner
+  top: The character to use for the top border
+  bottom: The character to use for the bottom border
+  left: The character to use for the left border
+  right: The character to use for the right border
+For example:
+lbMenu.borderChars.upperLeft = "\xDA"; // Single-line upper-left character
+Alternately, you can call the SetBorderChars() function and pass in a JS object
+with any or all of the above properties to set those values internally in the
+DDLightbarMenu object.
+
+If you want hotkeys to be case-sensitive, you can set the hotkeyCaseSensitive
+property to true (it is false by default).  For example:
+lbMenu.hotkeyCaseSensitive = true;
+
+To add additional key characters as quit keys (in addition to ESC), call
+AddAdditionalQuitKeys() with a string of characters.  For example:
+lbMenu.AddAdditionalQuitKeys("qQ");
+
+To clear the additional quit keys: ClearAdditionalQuitKeys()
+lbMenu.ClearAdditionalQuitKeys();
+
+Similarly for additional PageUp keys: AddAdditionalPageUpKeys and ClearAdditionalPageUpKeys
+For additional PageDown: AddAdditionalPageDownKeys and ClearAdditionalPageDownKeys
+For additional first page (like HOME): AddAdditionalFirstPageKeys and ClearAdditionalFirstPageKeys
+For additional last page (like END): AddAdditionalLastPageKeys and ClearAdditionalLastPageKeys
+
+
+To enable the border and set top and bottom border text:
+lbMenu.borderEnabled = true;
+lbMenu.topBorderText = "Options";
+lbMenu.bottomBorderText = "Enter = Select";
+
+
+For a more advanced usage, if you have another large list of items you want
+to use in the menu instead of the menu's own list of items, you can replace
+the NumItems and GetItem functions in the menu object and write your own
+versions that access a different list of items.  This can be useful, for instance,
+if you're working with a Synchronet messagebase (which may include a large number
+of messages), so you can avoid the time taken to add those items to a DDLightbarMenu.
+NumItems() needs to return the number of items in the list.  GetItem() takes an item
+index as a parameter and needs to return an item object that is compatible with
+DDLightbarMenu.  You can get a default item object by calling MakeItemWithRetval()
+or MakeItemWithTextAndRetval(), then change its text and retval properties as
+needed, then return the item object.  In the item object, the 'text' property
+is the text to display in the menu, and the 'retval' proprety is the value to return
+when the user chooses that item.
+An example (assuming the lightbar menu object is called lbMenu):
+lbMenu.NumItems = function() {
+	// Do your own thing to get the number of items in your list.
+	// ...
+	// Assuming myNumItems is the number of items in your list:
+	return myNumItems;
+};
+lbMenu.GetItem = function(pItemIndex) {
+	// Get a default item object from the menu with an initial return value of -1
+	var menuItemObj = this.MakeItemWithRetval(-1);
+	// Do your own thing to get the item text and return value for the menu.
+	// ...
+	// Assuming itemText is the text to display in the menu and itemRetval is
+	// the return value to return from the menu:
+	menuItemObj.text = itemText;
+	menuItemObj.retval = itemRetval;
+
+	// And if the text in the item is UTF-8, you must specify so as follows:
+	menuItemObj.textIsUTF8 = true;
+
+	return menuItemObj; // The DDLightbarMenu object will use this when displaying the menu
+};
+
+If you want to set the currently selected item before calling GetVal() to allow user input,
+you should call the SetSelectedItemIdx() function and pass the index to that.
+lbMenu.SetSelectedItemIdx(5);
+
+The property inputTimeoutMS sets the input timeout in milliseconds (defaults to 300000).
+lbMenu.inputTimeoutMS = 300000; // 300,000 milliseconds (5 minutes)
+
+The property mouseEnabled can be used to enable mouse support.  By default it is false.
+When mouse support is enabled, there can be problems inputting the ESC key from the user.
+lbMenu.mouseEnabled = true;
+
+For selecting an item, it may be desirable to validate whether a user should be allowed
+to select the item.  DDLightbarMenu has a member function it calls, ValidateSelectItem(),
+to do just that.  It takes the selected item's return value and returns a boolean to signify
+whether the user can select it.   By default, it just returns true (allowing the user to
+select any item).  When the user can't choose a value, your code should output why.
+To change its behavior, you can overwrite it as follows (assuming lbMenu
+is a DDLightbarMenu object):
+
+lbMenu.ValidateSelectItem = function(pItemRetval) {
+	// Should the user be able to select the item with the return val indicated
+	// by pItemRetval?
+	if (yourValidationCode(pItemRetval))
+		return true;
+	else
+	{
+		console.print("* Can't choose " + pItemRetval + " because blah blah blah!\r\n\x01p");
+		return false;
+	}
+}
+
+OnItemSelect is a function that is called when an item is selected, or toggled 
+if multi-select is enabled.
+
+Parameters:
+ pItemRetval: The return value of the item selected
+ pSelected: Boolean - Whether the item was selected or de-selected.  De-selection
+            is possible when multi-select is enabled.
+lbMenu.OnItemSelect = function(pItemRetval, pSelected)
+{
+	// Do something with pItemRetval.  pSelected tells whether the item was selected,
+	// or de-selected if multi-select is enabled.
+}
+
+The property exitOnItemSelect specifies whether or not to exit the input loop when an item is
+selected/submitted (i.e. with ENTER; not for toggling with multi-select).  This is true by
+default.  It can be desirable to set this to false in some situations, such as when you want a
+menu with a custom OnItemSelect() function specified and you want the menu to continue to
+be displayed allowing the user to select an item.
+lbMenu.exitOnItemSelect = false;
+
+OnItemNav is a function that is called when the user navigates to a new item (i.e., via
+the up or down arrow, PageUp, PageDown, Home, End, etc.).  Its parameters are the old
+item index and the new item index.
+lbMenu.OnItemNav = function(pOldItemIdx, pNewItemIdx) { }
+
+To have the menu object call OnItemNav() when it is first displayed to get the user's
+choice, set the callOnItemNavOnStartup property to true:
+lbMenu.callOnItemNavOnStartup = true;
+By default, it is false.
+
+The 'key down' behavior can be called explicitly, if needed, by calling the DoKeyDown() function.
+It takes 2 parameters: An object of selected item indexes (as passed to GetVal()) and, optionally,
+the pre-calculated number of items.
+lbMenu.DoKeyDown(pNumItems, pSelectedItemIndexes);
+
+
+For screen refreshing, DDLightbarMenu includes the function DrawPartial(), which can be used to
+redraw only a portion of the menu, specified by starting X & Y coordinates, width, and height.
+The starting X & Y coordinates are relative to the upper-left corner of the menu (not absolute
+screen coordinates) and start at (1, 1).  The function signature looks like this:
+ DrawPartial(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes)
+The parameters:
+ pStartX: The column of the character in the menu to start at
+ pStartY: The row of the character in the menu to start at
+ pWidth: The width of the content to draw
+ pHeight: The height of the content to draw
+ pSelectedItemIndexes: Optional - An object containing indexes of selected items
+
+Another function, DrawPartialAbs(), provies the same functionality but with absolute screen coordinates
+(also starting at (1, 1) in the upper-left corner):
+ DrawPartialAbs(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes)
+The parameters:
+ pStartX: The column of the character in the menu to start at
+  pStartY: The row of the character in the menu to start at
+ pWidth: The width of the content to draw
+ pHeight: The height of the content to draw
+ pSelectedItemIndexes: Optional - An object containing indexes of selected items
+
+
+Menu items can be marked not selectable by setting the isSelectable proprty of the item to false.
+Alternately, the menu function ToggleItemSelectable can be used for this purpose too:
+Parameters:
+- The index of the item to be toggled
+- Boolean: Whether or not the item should be selectable
+Example - Making the first item not selectable:
+lbMenu.ToggleItemSelectable(0, false);
+
+By default, DDLightbarMenu ignores the isSelectable attribute of items and considers all items
+selectable (for efficiency).  To enable usage of unselectable items, set the allowUnselectableItems
+property to true:
+lbMenu.allowUnselectableItems = true;
+*/
+
+"use strict";
+
+if (typeof(require) === "function")
+{
+	require("sbbsdefs.js", "K_UPPER");
+	require("mouse_getkey.js", "mouse_getkey");
+	require("userdefs.js", "USER_UTF8");
+	require("utf8_cp437.js", "utf8_cp437");
+}
+else
+{
+	load("sbbsdefs.js");
+	load("mouse_getkey.js");
+	load("userdefs.js");
+	load("utf8_cp437.js");
+}
+
+
+// Keyboard keys
+var KEY_ENTER = "\x0d";
+// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017
+// use CTRL-P and CTRL-N for PageUp and PageDown, respectively.  key_defs.js
+// defines them as KEY_PAGEUP and KEY_PAGEDN (key_defs.js is loaded by
+// sbbsdefs.js).
+//var KEY_ESC = ascii(27);
+
+// Box-drawing/border characters: Single-line
+var UPPER_LEFT_SINGLE = "\xDA";
+var HORIZONTAL_SINGLE = "\xC4";
+var UPPER_RIGHT_SINGLE = "\xBF";
+var VERTICAL_SINGLE = "\xB3";
+var LOWER_LEFT_SINGLE = "\xC0";
+var LOWER_RIGHT_SINGLE = "\xD9";
+var T_SINGLE = "\xC2";
+var LEFT_T_SINGLE = "\xC3";
+var RIGHT_T_SINGLE = "\xB4";
+var BOTTOM_T_SINGLE = "\xC1";
+var CROSS_SINGLE = "\xC5";
+// Box-drawing/border characters: Double-line
+var UPPER_LEFT_DOUBLE = "\xC9";
+var HORIZONTAL_DOUBLE = "\xCD";
+var UPPER_RIGHT_DOUBLE = "\xBB";
+var VERTICAL_DOUBLE = "\xBA";
+var LOWER_LEFT_DOUBLE = "\xC8";
+var LOWER_RIGHT_DOUBLE = "\xBC";
+var T_DOUBLE = "\xCB";
+var LEFT_T_DOUBLE = "\xCC";
+var RIGHT_T_DOUBLE = "\xB9";
+var BOTTOM_T_DOUBLE = "\xCA";
+var CROSS_DOUBLE = "\xCE";
+// Box-drawing/border characters: Vertical single-line with horizontal double-line
+var UPPER_LEFT_VSINGLE_HDOUBLE = "\xD5";
+var UPPER_RIGHT_VSINGLE_HDOUBLE = "\xB8";
+var LOWER_LEFT_VSINGLE_HDOUBLE = "\xD4";
+var LOWER_RIGHT_VSINGLE_HDOUBLE = "\xBE";
+// Other characters
+var CHECK_CHAR = "\xFB";
+var BLOCK1 = "\xB0"; // Dimmest block
+var BLOCK2 = "\xB1";
+var BLOCK3 = "\xB2";
+var BLOCK4 = "\xDB"; // Brightest block
+
+// Border types for a menu
+var BORDER_NONE = 0;
+var BORDER_SINGLE = 1;
+var BORDER_DOUBLE = 2;
+
+// DDLightbarMenu object contstructor
+//
+// Parameters:
+//  pX: Optional - The column (X) of the upper-left corner.  Defaults to 1.
+//  pY: Optional - The row (Y) of the upper-left corner.  Defaults to 1.
+//  pWidth: Optional - The width of the menu.  Defaults to 45.
+//  pHeight: Optional - The height of the menu.  Defaults to 10.
+function DDLightbarMenu(pX, pY, pWidth, pHeight)
+{
+	// Data members
+	this.items = [];
+	this.pos = {
+		x: 1,
+		y: 1
+	};
+	this.size = {
+		width: 45,
+		height: 10
+	};
+	this.scrollbarEnabled = false;
+	this.borderEnabled = false;
+	this.drawnAlready = false;
+	this.colors = {
+		itemColor: "\x01n\x01w\x01" + "4", // Can be either a string or an array specifying colors within the item
+		selectedItemColor: "\x01n\x01b\x01" + "7", // Can be either a string or an array specifying colors within the item
+		altItemColor: "\x01n\x01w\x01" + "4", // Alternate item color.  Can be either a string or an array specifying colors within the item
+		altSelectedItemColor: "\x01n\x01b\x01" + "7", // Alternate selected item color.  Can be either a string or an array specifying colors within the item
+		unselectableItemColor: "\x01n\x01b\x01h", // Can be either a string or an array specifying colors within the item
+		itemTextCharHighlightColor: "\x01y\x01h",
+		borderColor: "\x01n\x01b",
+		scrollbarScrollBlockColor: "\x01h\x01w",
+		scrollbarBGColor: "\x01h\x01k",
+		itemNumColor: "\x01n",
+		highlightedItemNumColor: "\x01n"
+	};
+	// Characters to use to draw the border
+	this.borderChars = {
+		upperLeft: UPPER_LEFT_DOUBLE,
+		upperRight: UPPER_RIGHT_DOUBLE,
+		lowerLeft: LOWER_LEFT_DOUBLE,
+		lowerRight: LOWER_RIGHT_DOUBLE,
+		top: HORIZONTAL_DOUBLE,
+		bottom: HORIZONTAL_DOUBLE,
+		left: VERTICAL_DOUBLE,
+		right: VERTICAL_DOUBLE
+	};
+	// Scrollbar information (characters, etc.)
+	this.scrollbarInfo = {
+		blockChar: BLOCK2,
+		BGChar: BLOCK1,
+		numSolidScrollBlocks: 0,
+		numNonSolidScrollBlocks: 0,
+		solidBlockLastStartRow: 0
+	};
+
+	this.selectedItemIdx = 0;
+	this.topItemIdx = 0;
+	this.wrapNavigation = true;
+	this.hotkeyCaseSensitive = false;
+	this.ampersandHotkeysInItems = true;
+	this.multiSelect = false;
+	this.maxNumSelections = -1; // -1 or 0 means no limit on the number of selections
+	this.multiSelectItemChar = CHECK_CHAR; // The character to display for a selected item in multi-select mode
+	this.numberedMode = false;
+	this.itemNumLen = 0; // For the length of the item numbers in numbered mode
+	this.additionalQuitKeys = ""; // A string of additional keys besides ESC to quit out of the menu
+	this.additionalPageUpKeys = ""; // A string of additional keys besides PageUp for page up
+	this.additionalPageDnKeys = ""; // A string of additional keys besides PageDown for page down
+	this.additionalFirstPageKeys = ""; // A string of additional keys besides HOME for going to the first page
+	this.additionalLastPageKeys = ""; // A string of additional keys besides END for going to the last page
+	this.additionalSelectItemKeys = ""; // A string of additional keys to select any item
+	this.topBorderText = ""; // Text to display in the top border
+	this.bottomBorderText = ""; // Text to display in the bottom border
+	this.lastUserInput = null; // The user's last keypress when the menu was shown/used
+	// nextDrawOnlyItemSubstr can be an object containing start & end properties, as
+	// indexes (end is one past last index) for drawing shortened versions of items on the
+	// next draw
+	this.nextDrawOnlyItemSubstr = null;
+	// nextDrawOnlyItems is an array specifying the indexes of the items to write
+	// on the next draw.  If this is empty, then all items on the page will be drawn.
+	this.nextDrawOnlyItems = [];
+
+	// This is a regex to do a case-insensitive test for Synchronet attribute
+	// codes in strings.
+	// For one that looks at the whole word having only Synchronet attribute
+	// codes, it would have ^ and $ around it, as in
+	// /^\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]$/i
+	this.syncAttrRegex = /\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]/i;
+
+	// Whether or not to exit the input loop when an item is selected/submitted
+	// (i.e. with ENTER; not for toggling with multi-select)
+	this.exitOnItemSelect = true;
+
+	this.inputTimeoutMS = 300000; // Input timeout in ms
+	this.mouseEnabled = false;
+
+	// Whether or not to call OnItemNav() when the menu is first displayed
+	// to get the user's choice
+	this.callOnItemNavOnStartup = false;
+
+	// Whether or not to allow unselectable items (pay attention to the isSelectable attribute of items).
+	// Defaults to false, mainly for backwards compatibility.
+	this.allowUnselectableItems = false;
+
+	// Whether or not to allow ANSI behavior. Mainly for testing (this should be true).
+	this.allowANSI = true;
+
+	// Member functions
+	this.Add = DDLightbarMenu_Add;
+	this.Remove = DDLightbarMenu_Remove;
+	this.RemoveAllItems = DDLightbarMenu_RemoveAllItems;
+	this.NumItems = DDLightbarMenu_NumItems;
+	this.GetItem = DDLightbarMenu_GetItem;
+	this.ItemIsSelectable = DDLightbarMenu_ItemIsSelectable;
+	this.FindSelectableItemForward = DDLightbarMenu_FindSelectableItemForward;
+	this.FindSelectableItemBackward = DDLightbarMenu_FindSelectableItemBackward;
+	this.HasAnySelectableItems = DDLightbarMenu_HasAnySelectableItems;
+	this.ToggleItemSelectable = DDLightbarMenu_ToggleItemSelectable;
+	this.FirstSelectableItemIdx = DDLightbarMenu_FirstSelectableItemIdx;
+	this.LastSelectableItemIdx = DDLightbarMenu_LastSelectableItemIdx;
+	this.SetPos = DDLightbarMenu_SetPos;
+	this.SetSize = DDLightbarMenu_SetSize;
+	this.SetWidth = DDLightbarMenu_SetWidth;
+	this.SetHeight = DDLightbarMenu_SetHeight;
+	this.Draw = DDLightbarMenu_Draw;
+	this.DrawBorder = DDLightbarMenu_DrawBorder;
+	this.WriteItem = DDLightbarMenu_WriteItem;
+	this.WriteItemAtItsLocation = DDLightbarMenu_WriteItemAtItsLocation;
+	this.DrawPartial = DDLightbarMenu_DrawPartial;
+	this.DrawPartialAbs = DDLightbarMenu_DrawPartialAbs;
+	this.GetItemText = DDLightbarMenu_GetItemText;
+	this.ItemTextIsUTF8 = DDLightbarMenu_ItemTextIsUTF8;
+	this.Erase = DDLightbarMenu_Erase;
+	this.SetItemHotkey = DDLightbarMenu_SetItemHotkey;
+	this.AddItemHotkey = DDLightbarMenu_AddItemHotkey;
+	this.RemoveItemHotkey = DDLightbarMenu_RemoveItemHotkey;
+	this.RemoveItemHotkeys = DDLightbarMenu_RemoveItemHotkeys;
+	this.RemoveAllItemHotkeys = DDLightbarMenu_RemoveAllItemHotkeys;
+	this.GetMouseClickRegion = DDLightbarMenu_GetMouseClickRegion;
+	this.GetVal = DDLightbarMenu_GetVal;
+	this.DoKeyUp = DDLightbarMenu_DoKeyUp;
+	this.DoKeyDown = DDLightbarMenu_DoKeyDown;
+	this.DoPageUp = DDLightbarMenu_DoPageUp;
+	this.DoPageDown = DDLightbarMenu_DoPageDown;
+	this.NavMenuForNewSelectedItemTop = DDLightbarMenu_NavMenuForNewSelectedItemTop;
+	this.NavMenuForNewSelectedItemBottom = DDLightbarMenu_NavMenuForNewSelectedItemBottom;
+	this.SetBorderChars = DDLightbarMenu_SetBorderChars;
+	this.SetColors = DDLightbarMenu_SetColors;
+	this.GetNumItemsPerPage = DDLightbarMenu_GetNumItemsPerPage;
+	this.GetTopItemIdxOfLastPage = DDLightbarMenu_GetTopItemIdxOfLastPage;
+	this.CalcAndSetTopItemIdxToTopOfLastPage = DDLightbarMenu_CalcAndSetTopItemIdxToTopOfLastPage;
+	this.CalcPageForItemAndSetTopItemIdx = DDLightbarMenu_CalcPageForItemAndSetTopItemIdx;
+	this.AddAdditionalQuitKeys = DDLightbarMenu_AddAdditionalQuitKeys;
+	this.QuitKeysIncludes = DDLightbarMenu_QuitKeysIncludes;
+	this.ClearAdditionalQuitKeys = DDLightbarMenu_ClearAdditionalQuitKeys;
+	this.AddAdditionalPageUpKeys = DDLightbarMenu_AddAdditionalPageUpKeys;
+	this.PageUpKeysIncludes = DDLightbarMenu_PageUpKeysIncludes;
+	this.ClearAdditionalPageUpKeys = DDLightbarMenu_ClearAdditionalPageUpKeys;
+	this.AddAdditionalPageDownKeys = DDLightbarMenu_AddAdditionalPageDownKeys;
+	this.PageDownKeysIncludes = DDLightbarMenu_PageDownKeysIncludes;
+	this.ClearAdditionalPageDownKeys = DDLightbarMenu_ClearAdditionalPageDownKeys;
+	this.AddAdditionalFirstPageKeys = DDLightbarMenu_AddAdditionalFirstPageKeys;
+	this.FirstPageKeysIncludes = DDLightbarMenu_FirstPageKeysIncludes;
+	this.ClearAdditionalFirstPageKeys = DDLightbarMenu_ClearAdditionalFirstPageKeys;
+	this.AddAdditionalLastPageKeys = DDLightbarMenu_AddAdditionalLastPageKeys;
+	this.LastPageKeysIncludes = DDLightbarMenu_LastPageKeysIncludes;
+	this.ClearAdditionalLastPageKeys = DDLightbarMenu_ClearAdditionalLastPageKeys;
+	this.AddAdditionalSelectItemKeys = DDLightbarMenu_AddAdditionalSelectItemKeys;
+	this.SelectItemKeysIncludes = DDLightbarMenu_SelectItemKeysIncludes;
+	this.ClearAdditionalSelectItemKeys = DDLightbarMenu_ClearAdditionalSelectItemKeys;
+	this.DisplayInitialScrollbar = DDLightbarMenu_DisplayInitialScrollbar;
+	this.UpdateScrollbar = DDLightbarMenu_UpdateScrollbar;
+	this.CalcScrollbarBlocks = DDLightbarMenu_CalcScrollbarBlocks;
+	this.CalcScrollbarSolidBlockStartRow = DDLightbarMenu_CalcScrollbarSolidBlockStartRow;
+	this.UpdateScrollbarWithHighlightedItem = DDLightbarMenu_UpdateScrollbarWithHighlightedItem;
+	this.CanShowAllItemsInWindow = DDLightbarMenu_CanShowAllItemsInWindow;
+	this.MakeItemWithTextAndRetval = DDLightbarMenu_MakeItemWithTextAndRetval;
+	this.MakeItemWithRetval = DDLightbarMenu_MakeItemWithRetval;
+	this.ItemUsesAltColors = DDLightbarMenu_ItemUsesAltColors;
+	this.GetColorForItem = DDLightbarMenu_GetColorForItem;
+	this.GetSelectedColorForItem = DDLightbarMenu_GetSelectedColorForItem;
+	this.SetSelectedItemIdx = DDLightbarMenu_SetSelectedItemIdx;
+	this.GetBottomItemIdx = DDLightbarMenu_GetBottomItemIdx;
+	this.GetTopDisplayedItemPos = DDLightbarMenu_GetTopDisplayedItemPos;
+	this.GetBottomDisplayedItemPos = DDLightbarMenu_GetBottomDisplayedItemPos;
+	this.ScreenRowForItem = DDLightbarMenu_ScreenRowForItem;
+	this.ANSISupported = DDLightbarMenu_ANSISupported;
+
+	// ValidateSelectItem is a function for validating that the user can select an item.
+	// It takes the selected item's return value and returns a boolean to signify whether
+	// the user can select it.
+	this.ValidateSelectItem = function(pItemRetval) { return true; }
+
+	// OnItemSelect is a function that is called when an item is selected, or toggled 
+	// if multi-select is enabled.
+	//
+	// Parameters:
+	//  pItemRetval: The return value of the item selected
+	//  pSelected: Boolean - Whether the item was selected or de-selected.  De-selection
+	//             is possible when multi-select is enabled.
+	this.OnItemSelect = function(pItemRetval, pSelected) { }
+
+	// OnItemNav is a function that is called when the user navigates to
+	// new item (i.e., up/down arrow, pageUp, pageDown, home, end)
+	this.OnItemNav = function(pOldItemIdx, pNewItemIdx) { }
+
+	// Set some things based on the parameters passed in
+	if ((typeof(pX) == "number") && (typeof(pY) == "number"))
+		this.SetPos(pX, pY);
+	if (typeof(pWidth) == "number")
+		this.SetWidth(pWidth);
+	if (typeof(pHeight) == "number")
+		this.SetHeight(pHeight);
+}
+
+// Adds an item to the menu
+//
+// Parameters:
+//  pText: The text of the menu item
+//  pRetval: The value to return when the item is chosen.  Can be any type of value.
+//  pHotkey: Optional - A key to select the item when pressed by the user
+//  pSelectable: Optional - Whether or not the item is to be selectable. Defaults to true.
+//  pIsUTF8: Optional boolenan - Whether or not the text is UTF-8.  Defaults to false.
+function DDLightbarMenu_Add(pText, pRetval, pHotkey, pSelectable, pIsUTF8)
+{
+	var item = getDefaultMenuItem();
+	item.text = pText;
+	item.textIsUTF8 = (typeof(pIsUTF8) === "boolean" ? pIsUTF8 : false);
+	item.retval = (pRetval == undefined ? this.NumItems() : pRetval);
+	item.isSelectable = (typeof(pSelectable) === "boolean" ?  pSelectable : true);
+	// If pHotkey is defined, then use it as the hotkey.  Otherwise, if
+	// ampersandHotkeysInItems is true, look for the first & in the item text
+	// and if there's a non-space after it, then use that character as the
+	// hotkey.
+	if (typeof(pHotkey) == "string")
+		item.hotkeys += pHotkey;
+
+	if (this.ampersandHotkeysInItems)
+	{
+		var ampersandIndex = pText.indexOf("&");
+		if (ampersandIndex > -1)
+		{
+			// See if the next character is a space character.  If not, then
+			// don't count it in the length.
+			if (pText.length > ampersandIndex+1)
+			{
+				var nextChar = pText.substr(ampersandIndex+1, 1);
+				if (nextChar != " ")
+					item.hotkeys += nextChar;
+			}
+		}
+	}
+
+	this.items.push(item);
+}
+
+// Removes an item
+//
+// Parameters:
+//  pIdx: The index of the item to remove
+function DDLightbarMenu_Remove(pIdx)
+{
+	if ((typeof(pIdx) != "number") || (pIdx < 0) || (pIdx >= this.items.length))
+		return; // pIdx is invalid
+
+	this.items.splice(pIdx, 1);
+	if (this.items.length > 0)
+	{
+		if (this.selectedItemIdx >= this.items.length)
+			this.selectedItemIdx = this.items.length - 1;
+	}
+	else
+	{
+		this.selectedItemIdx = 0;
+		this.topItemIdx = 0;
+	}
+}
+
+// Removes all items
+function DDLightbarMenu_RemoveAllItems()
+{
+	this.items = [];
+	this.selectedItemIdx = 0;
+	this.topItemIdx = 0;
+}
+
+// Returns the number of items in the menu
+function DDLightbarMenu_NumItems()
+{
+	return this.items.length;
+}
+
+// Returns an item from the list
+//
+// Parameters:
+//  pItemIndex: The index of the item to get
+//
+// Return value: The item (or null if pItemIndex is invalid)
+function DDLightbarMenu_GetItem(pItemIndex)
+{
+	if ((pItemIndex < 0) || (pItemIndex >= this.NumItems()))
+		return null;
+	return this.items[pItemIndex];
+}
+
+// Returns whether an item is selectable
+//
+// Parameters:
+//  pItemIndex: The index of the item to check
+//
+// Return value: Boolean - Whether or not the item is selectable
+function DDLightbarMenu_ItemIsSelectable(pItemIndex)
+{
+	if ((pItemIndex < 0) || (pItemIndex >= this.NumItems()))
+		return false;
+
+	if (!this.allowUnselectableItems)
+		return true;
+
+	var item = this.GetItem(pItemIndex);
+	if (item == null || typeof(item) !== "object")
+		return false;
+	if (item.hasOwnProperty("isSelectable"))
+		return item.isSelectable;
+	else
+		return false;
+}
+
+// Finds a selectable menu item index going forward, starting at a given item index
+//
+// Parameters:
+//  pStartItemIdx: The index of the item to start at. This will be included in the search.
+//  pWrapAround: Boolean - Whether or not to wrap around. Defaults to false.
+//
+// Return value: The index of the next selectable item, or -1 if none is found.
+function DDLightbarMenu_FindSelectableItemForward(pStartItemIdx, pWrapAround)
+{
+	var numItems = this.NumItems();
+	if (typeof(pStartItemIdx) !== "number" || pStartItemIdx < 0 || pStartItemIdx >= numItems)
+		return -1;
+
+	if (!this.allowUnselectableItems)
+		return pStartItemIdx;
+
+	var wrapAround = (typeof(pWrapAround) === "boolean" ? pWrapAround : false);
+
+	var selectableItemIdx = -1;
+	var wrappedAround = false;
+	var onePastLastItemIdx = numItems;
+	for (var i = pStartItemIdx; i < onePastLastItemIdx && selectableItemIdx == -1; ++i)
+	{
+		var item = this.GetItem(i);
+		if (item.isSelectable)
+			selectableItemIdx = i;
+		else
+		{
+			if (i == pStartItemIdx - 1 && wrappedAround)
+				break;
+			else if (i == numItems-1 && wrapAround)
+			{
+				i = -1;
+				onePastLastItemIdx = pStartItemIdx;
+				wrappedAround = true;
+			}
+		}
+	}
+	return selectableItemIdx;
+}
+
+// Finds a selectable menu item index going backward, starting at a given item index
+//
+// Parameters:
+//  pStartItemIdx: The index of the item to start at. This will be included in the search.
+//  pWrapAround: Boolean - Whether or not to wrap around. Defaults to false.
+//
+// Return value: The index of the previous selectable item, or -1 if none is found.
+function DDLightbarMenu_FindSelectableItemBackward(pStartItemIdx, pWrapAround)
+{
+	var numItems = this.NumItems();
+	if (typeof(pStartItemIdx) !== "number" || pStartItemIdx < 0 || pStartItemIdx >= numItems)
+		return -1;
+
+	if (!this.allowUnselectableItems)
+		return pStartItemIdx;
+
+	var wrapAround = (typeof(pWrapAround) === "boolean" ? pWrapAround : false);
+
+	var selectableItemIdx = -1;
+	var wrappedAround = false;
+	for (var i = pStartItemIdx; i >= 0 && selectableItemIdx == -1; --i)
+	{
+		var item = this.GetItem(i);
+		if (item.isSelectable)
+			selectableItemIdx = i;
+		else
+		{
+			if (i == pStartItemIdx - 1 && wrappedAround)
+				break;
+			else if (i == numItems-1 && wrapAround)
+			{
+				i = this.NumItems() + 1;
+				onePastLastItemIdx = pStartItemIdx;
+				wrappedAround = true;
+			}
+		}
+	}
+	return selectableItemIdx;
+}
+
+// Returns whether there are any selectable items in the menu
+function DDLightbarMenu_HasAnySelectableItems(pNumItems)
+{
+	if (!this.allowUnselectableItems)
+		return true;
+
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	var anySelectable = false;
+	for (var i = 0; i < numItems && !anySelectable; ++i)
+		anySelectable = this.GetItem(i).isSelectable;
+	return anySelectable;
+}
+
+// Toggles whether an item is selectable
+//
+// Parameters:
+//  pItemIdx: The index of the item to toggle
+//  pSelectable: Boolean - Whether or not the item should be selectable
+function DDLightbarMenu_ToggleItemSelectable(pItemIdx, pSelectable)
+{
+	if (typeof(pItemIdx) !== "number" || pItemIdx < 0 || pItemIdx >= this.NumItems() || typeof(pSelectable) !== "boolean")
+		return;
+	this.GetItem(pItemIdx).isSelectable = false;
+}
+
+// Returns the index of the first electable item, or -1 if there is none.
+function DDLightbarMenu_FirstSelectableItemIdx(pNumItems)
+{
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	if (numItems == 0)
+		return -1;
+
+	if (!this.allowUnselectableItems)
+		return 0;
+
+	var selectableItemIdx = -1;
+	var anySelectable = false;
+	for (var i =0; i < numItems && selectableItemIdx == -1; ++i)
+	{
+		if (this.GetItem(i).isSelectable)
+			selectableItemIdx = i;
+	}
+	return selectableItemIdx;
+}
+
+// Returns the index of the last selectable item, or -1 if there is none.
+function DDLightbarMenu_LastSelectableItemIdx(pNumItems)
+{
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	if (numItems == 0)
+		return -1;
+
+	if (!this.allowUnselectableItems)
+		return numItems - 1;
+
+	var selectableItemIdx = -1;
+	var anySelectable = false;
+	for (var i = numItems-1; i >= 0 && selectableItemIdx == -1; --i)
+	{
+		if (this.GetItem(i).isSelectable)
+			selectableItemIdx = i;
+	}
+	return selectableItemIdx;
+}
+
+// Sets the menu's upper-left corner position
+//
+// Parameters:
+//  pX: The column (X) of the upper-left corner.
+//  pY: The row (Y) of the upper-left corner.
+function DDLightbarMenu_SetPos(pX, pY)
+{
+	if (typeof(pX) == "object")
+	{
+		if (pX.hasOwnProperty("x") && pX.hasOwnProperty("y"))
+		{
+			this.pos.x = pX.x;
+			this.pos.y = pX.y;
+		}
+		
+	}
+	else if ((typeof(pX) == "number") && (typeof(pY) == "number"))
+	{
+		this.pos.x = pX;
+		this.pos.y = pY;
+	}
+}
+
+// Sets the menu's size.
+//
+// Parameters:
+//  pSize: An object containing 'width' and 'height' members (numeric)
+function  DDLightbarMenu_SetSize(pSize)
+{
+	if (typeof(pSize) == "object")
+	{
+		if (pSize.hasOwnProperty("width") && pSize.hasOwnProperty("height") && (typeof(pSize.width) == "number") && (typeof(pSize.height) == "number"))
+		{
+			if ((pSize.width > 0) && (pSize.width <= console.screen_columns))
+				this.size.width = pSize.width;
+			if ((pSize.height > 0) && (pSize.height <= console.screen_rows))
+				this.size.height = pSize.height;
+		}
+	}
+}
+
+// Sets the menu's width
+//
+// Parameters:
+//  pWidth: The width of the menu
+function DDLightbarMenu_SetWidth(pWidth)
+{
+	if (typeof(pWidth) == "number")
+	{
+		if ((pWidth > 0) && (pWidth <= console.screen_columns))
+			this.size.width = pWidth;
+	}
+}
+
+// Sets the height of the menu
+//
+// Parameters:
+//  pHeight: The height of the menu
+function DDLightbarMenu_SetHeight(pHeight)
+{
+	if (typeof(pHeight) == "number")
+	{
+		if ((pHeight > 0) && (pHeight <= console.screen_rows))
+			this.size.height = pHeight;
+	}
+}
+
+// Draws the menu with all menu items.  The selected item will be highlighted.
+//
+// Parameters:
+//  pSelectedItemIndexes: An object that can contain multiple indexes of selected
+//                        items.  Only for multi-select mode.  These are used
+//                        for drawing a marking character in the item text.
+//  pDrawBorders: Optional boolean - Whether or not to draw the borders, if borders
+//                are enabled.  Defaults to true.
+//  pDrawScrollbar: Optional boolean - Whether or not to draw the scrollbar, if
+//                  the scrollbar is enabled.  Defaults to this.scrollbarEnabled, and the scrollbar
+//                  will only be drawn if not all items can be shown in a single page.
+//  pNumItems: Optional - A cached value for the number of menu items.  If not specified, this will
+//             call this.NumItems();
+function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders, pDrawScrollbar, pNumItems)
+{
+	var numMenuItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	if (this.ANSISupported())
+	{
+		var drawBorders = (typeof(pDrawBorders) == "boolean" ? pDrawBorders : true);
+		var drawScrollbar = (typeof(pDrawScrollbar) == "boolean" ? pDrawScrollbar : true);
+
+		var curPos = { x: this.pos.x, y: this.pos.y }; // For writing the menu items
+		var itemLen = this.size.width;
+		// If borders are enabled, then adjust the item length, starting x, and starting
+		// y accordingly, and draw the border.
+		if (this.borderEnabled)
+		{
+			itemLen -= 2;
+			++curPos.x;
+			++curPos.y;
+			if (drawBorders)
+				this.DrawBorder();
+		}
+		if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
+			--itemLen; // Leave room for the scrollbar in the item lengths
+		// If the scrollbar is enabled & needed and we are to update it,
+		// then calculate the scrollbar blocks and update it on the screen.
+		if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow() && drawScrollbar)
+		{
+			this.CalcScrollbarBlocks();
+			if (!this.drawnAlready)
+				this.DisplayInitialScrollbar(this.pos.y);
+			else
+				this.UpdateScrollbarWithHighlightedItem(true);
+		}
+		// For numbered mode, we'll need to know the length of the longest item number
+		// so that we can use that space to display the item numbers.
+		if (this.numberedMode)
+		{
+			this.itemNumLen = numMenuItems.toString().length;
+			itemLen -= this.itemNumLen;
+			--itemLen; // Have a space for separation between the numbers and items
+		}
+
+		// Write the menu items, only up to the height of the menu
+		var numPossibleItems = (this.borderEnabled ? this.size.height - 2 : this.size.height);
+		var numItemsWritten = 0;
+		var writeTheItem = true;
+		for (var idx = this.topItemIdx; (idx < numMenuItems) && (numItemsWritten < numPossibleItems); ++idx)
+		{
+			writeTheItem = ((this.nextDrawOnlyItems.length == 0) || (this.nextDrawOnlyItems.indexOf(idx) > -1));
+			if (writeTheItem)
+			{
+				console.gotoxy(curPos.x, curPos.y);
+				var showMultiSelectMark = (this.multiSelect && (typeof(pSelectedItemIndexes) == "object") && pSelectedItemIndexes.hasOwnProperty(idx));
+				this.WriteItem(idx, itemLen, idx == this.selectedItemIdx, showMultiSelectMark, curPos.x, curPos.y);
+			}
+			++curPos.y;
+			++numItemsWritten;
+		}
+		// If there are fewer items than the height of the menu, then write blank lines to fill
+		// the rest of the height of the menu.
+		if (numItemsWritten < numPossibleItems)
+		{
+			var numberFormatStr = "%" + this.itemNumLen + "s ";
+			var itemFormatStr = "%-" + itemLen + "s";
+			for (; numItemsWritten < numPossibleItems; ++numItemsWritten)
+			{
+				writeTheItem = ((this.nextDrawOnlyItems.length == 0) || (this.nextDrawOnlyItems.indexOf(numItemsWritten) > -1));
+				if (writeTheItem)
+				{
+					console.gotoxy(curPos.x, curPos.y++);
+					console.attributes = "N";
+					if (this.numberedMode)
+						printf(numberFormatStr, "");
+					var itemText = addAttrsToString(format(itemFormatStr, ""), this.colors.itemColor);
+					console.print(itemText);
+				}
+			}
+		}
+	}
+	else
+	{
+		// ANSI mode disabled, or the user's terminal doesn't support ANSI
+		var numberedModeBackup = this.numberedMode;
+		this.numberedMode = true;
+		var itemLen = this.size.width;
+		// For numbered mode, we'll need to know the length of the longest item number
+		// so that we can use that space to display the item numbers.
+		this.itemNumLen = numMenuItems.toString().length;
+		itemLen -= this.itemNumLen;
+		--itemLen; // Have a space for separation between the numbers and items
+		console.attributes = "N";
+		for (var i = 0; i < numMenuItems; ++i)
+		{
+			var showMultiSelectMark = (this.multiSelect && (typeof(pSelectedItemIndexes) == "object") && pSelectedItemIndexes.hasOwnProperty(idx));
+			var itemText = this.GetItemText(i, itemLen, false, showMultiSelectMark);
+			// TODO: Once, it seemed the text must be shortened by 3 less than the console width or else
+			// it behaves like there's an extra CRLF, but 2 should be correct.
+			console.print(substrWithAttrCodes(itemText, 0, console.screen_columns-2) + "\x01n");
+			console.crlf();
+		}
+		this.numberedMode = numberedModeBackup;
+	}
+
+	this.drawnAlready = true;
+	this.nextDrawOnlyItemSubstr = null;
+	this.nextDrawOnlyItems = [];
+}
+
+// Draws the border around the menu items
+function DDLightbarMenu_DrawBorder()
+{
+	if (!this.borderEnabled)
+		return;
+
+	// Draw the border around the menu options
+	console.print("\x01n" + this.colors.borderColor);
+	// Upper border
+	console.gotoxy(this.pos.x, this.pos.y);
+	if (this.borderChars.hasOwnProperty("upperLeft") && (typeof(this.borderChars.upperLeft) == "string"))
+		console.print(this.borderChars.upperLeft);
+	else
+		console.print(" ");
+	var lineLen = this.size.width - 2;
+	if (this.borderChars.hasOwnProperty("top") && (typeof(this.borderChars.top) == "string"))
+	{
+		// Display the top border text (if any) in the top border.  Ensure the text
+		// length is no longer than the maximum possible length (lineLen).
+		var borderText = shortenStrWithAttrCodes(this.topBorderText, lineLen);
+		console.print("\x01n" + borderText + "\x01n" + this.colors.borderColor);
+		var remainingLineLen = lineLen - console.strlen(borderText);
+		for (var i = 0; i < remainingLineLen; ++i)
+			console.print(this.borderChars.top);
+	}
+	else
+	{
+		for (var i = 0; i < lineLen; ++i)
+			console.print(" ");
+	}
+	if (this.borderChars.hasOwnProperty("upperRight") && (typeof(this.borderChars.upperRight) == "string"))
+		console.print(this.borderChars.upperRight);
+	else
+		console.print(" ");
+	// Lower border
+	console.gotoxy(this.pos.x, this.pos.y+this.size.height-1);
+	if (this.borderChars.hasOwnProperty("lowerLeft") && (typeof(this.borderChars.lowerLeft) == "string"))
+		console.print(this.borderChars.lowerLeft);
+	else
+		console.print(" ");
+	var lineLen = this.size.width - 2;
+	if (this.borderChars.hasOwnProperty("bottom") && (typeof(this.borderChars.bottom) == "string"))
+	{
+		// Display the bottom border text (if any) in the bottom border.  Ensure the text
+		// length is no longer than the maximum possible length (lineLen).
+		var borderText = shortenStrWithAttrCodes(this.bottomBorderText, lineLen);
+		console.print("\x01n" + borderText + "\x01n" + this.colors.borderColor);
+		var remainingLineLen = lineLen - console.strlen(borderText);
+		for (var i = 0; i < remainingLineLen; ++i)
+			console.print(this.borderChars.bottom);
+	}
+	else
+	{
+		for (var i = 0; i < lineLen; ++i)
+			console.print(" ");
+	}
+	if (this.borderChars.hasOwnProperty("lowerRight") && (typeof(this.borderChars.lowerRight) == "string"))
+		console.print(this.borderChars.lowerRight);
+	else
+		console.print(" ");
+	// Side borders
+	var leftSideChar = " ";
+	var rightSideChar = " ";
+	if (this.borderChars.hasOwnProperty("left") && (typeof(this.borderChars.left) == "string"))
+		leftSideChar = this.borderChars.left;
+	if (this.borderChars.hasOwnProperty("right") && (typeof(this.borderChars.right) == "string"))
+		rightSideChar = this.borderChars.right;
+	lineLen = this.size.height - 2;
+	var lineNum = 1;
+	for (var lineNum = 1; lineNum <= lineLen; ++lineNum)
+	{
+		console.gotoxy(this.pos.x, this.pos.y+lineNum);
+		console.print(leftSideChar);
+		console.gotoxy(this.pos.x+this.size.width-1, this.pos.y+lineNum);
+		console.print(rightSideChar);
+	}
+}
+
+// Writes a single menu item
+//
+// Parameters:
+//  pIdx: The index of the item to write
+//  pItemLen: Optional - Calculated length of the item (in case the scrollbar is showing).
+//            If this is not given, then this will be calculated.
+//  pHighlight: Optional - Whether or not to highlight the item.  If this is not given,
+//              the item will be highlighted based on whether the current selected item
+//              matches the given index, pIdx.
+//  pSelected: Optional - Whether or not this item is selected (mainly intended for multi-select
+//             mode).  Defaults to false.  If true, then a mark character will be displayed
+//             at the end of the item's text.
+//  pScreenX: Optional - The horizontal screen coordinate of the start of the item
+//  pScreenY: Optional - The vertical screen coordinate of the start of the item
+function DDLightbarMenu_WriteItem(pIdx, pItemLen, pHighlight, pSelected, pScreenX, pScreenY)
+{
+	var itemText = this.GetItemText(pIdx, pItemLen, pHighlight, pSelected);
+	// If the text is UTF-8 and the user's terminal is UTF-8, then set the mode bit accordingly.
+	// If the text is UTF-8 and the user's terminal doesn't support UTF-8, convert the text to cp437.
+	var printModeBits = P_NONE;
+	if (this.ItemTextIsUTF8(pIdx))
+	{
+		if (console.term_supports(USER_UTF8))
+			printModeBits = P_UTF8;
+		else
+			itemText = utf8_cp437(itemText);
+	}
+	// If this.nextDrawOnlyItemSubstr is an object with start & end properties,
+	// then create a string that is shortened from itemText from those start & end
+	// indexes, and add color to it.
+	// Otherwise, just print the full item text.
+	if ((this.nextDrawOnlyItemSubstr != null) && (typeof(this.nextDrawOnlyItemSubstr) == "object") && this.nextDrawOnlyItemSubstr.hasOwnProperty("start") && this.nextDrawOnlyItemSubstr.hasOwnProperty("end") && (typeof(pScreenX) == "number") && (typeof(pScreenY) == "number"))
+	{
+		var len = this.nextDrawOnlyItemSubstr.end - this.nextDrawOnlyItemSubstr.start;
+		var shortenedText = substrWithAttrCodes(itemText, this.nextDrawOnlyItemSubstr.start, len);
+		console.gotoxy(pScreenX+this.nextDrawOnlyItemSubstr.start, pScreenY);
+		console.print(shortenedText + "\x01n", printModeBits);
+	}
+	else
+		console.print(itemText + "\x01n", printModeBits);
+}
+
+// Writes a menu item at its location on the menu.  This should only be called
+// if the item is on the current page.
+//
+// Parameters:
+//  pIdx: The index of the item to write
+//  pHighlight: Whether or not the item should be highlighted
+//  pSelected: Whether or not the item is selected
+function DDLightbarMenu_WriteItemAtItsLocation(pIdx, pHighlight, pSelected)
+{
+	if (this.borderEnabled)
+		console.gotoxy(this.pos.x+1, this.pos.y+pIdx-this.topItemIdx+1);
+	else
+		console.gotoxy(this.pos.x, this.pos.y+pIdx-this.topItemIdx);
+	this.WriteItem(pIdx, null, pHighlight, pSelected);
+}
+
+// Draws part of the menu, starting at a certain location within the menu and
+// with a given width & height (for screen refreshing).  The start X and Y location
+// are relative to the menu (not the screen), and they start at (1, 1) in the upper-left
+//
+// Parameters:
+//  pStartX: The column of the character in the menu to start at
+//  pStartY: The row of the character in the menu to start at
+//  pWidth: The width of the content to draw
+//  pHeight: The height of the content to draw
+//  pSelectedItemIndexes: Optional - An object containing indexes of selected items
+function DDLightbarMenu_DrawPartial(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes)
+{
+	// Sanity check the parameters
+	if (typeof(pStartX) !== "number" || typeof(pStartY) !== "number" || typeof(pWidth) !== "number" || typeof(pHeight) !== "number")
+		return;
+	if (pStartX < 1 || pStartX > this.size.width)
+		return;
+	if (pStartY < 1 || pStartY > this.size.height)
+		return;
+
+	// Fix the width & height if needed
+	var width = pWidth;
+	if (width > (this.size.width - pStartX + 1))
+		width = (this.size.width - pStartX + 1);
+	var height = pHeight;
+	if (height > (this.size.height - pStartY + 1))
+		height = (this.size.height - pStartY + 1);
+	/*
+	// Temporary
+	if (user.is_sysop)
+	{
+		console.print("\x01n\r\n");
+		printf("DrawPartial 1 - X, Y, width, height: %d, %d; %d, %d\r\n", pStartX, pStartY, width, height);
+		console.pause();
+	}
+	// End Temporary
+	*/
+
+	var selectedItemIndexes = { }; // For multi-select mode
+	if (typeof(pSelectedItemIndexes) == "object")
+		selectedItemIndexes = pSelectedItemIndexes;
+
+	// If borders are enabled, draw any border characters in the region first
+	// The X & Y locations are 1-based
+	var lastLineNum = (pStartY  + this.pos.y + height) - 1; // Last line # on the screen
+	if (lastLineNum > this.pos.y + this.size.height - 1)
+		lastLineNum = this.pos.y + this.size.height - 1;
+	if (this.borderEnabled)
+	{
+		var lastX = pStartX + width - 1;
+		for (var lineNum = pStartY + this.pos.y - 1; lineNum <= lastLineNum; ++lineNum)
+		{
+			// Top line
+			if (lineNum == this.pos.y)
+			{
+				console.print("\x01n" + this.colors.borderColor);
+				for (var posX = pStartX; posX <= lastX; ++posX)
+				{
+					console.gotoxy(posX, lineNum);
+					if (posX == this.pos.x)
+						console.print(this.borderChars.upperLeft);
+					else if (posX == this.pos.x + this.size.width - 1)
+						console.print(this.borderChars.upperRight);
+					else
+						console.print(this.borderChars.top);
+				}
+			}
+			// Bottom line
+			else if (lineNum == this.pos.y + this.size.height - 1)
+			{
+				console.print("\x01n" + this.colors.borderColor);
+				for (var posX = pStartX; posX <= lastX; ++posX)
+				{
+					console.gotoxy(posX, lineNum);
+					if (posX == this.pos.x)
+						console.print(this.borderChars.lowerLeft);
+					else if (posX == this.pos.x + this.size.width - 1)
+						console.print(this.borderChars.lowerRight);
+					else
+						console.print(this.borderChars.bottom);
+				}
+			}
+			// Somewhere between the top & bottom line
+			else
+			{
+				var printedBorderColor = false;
+				for (var posX = pStartX; posX <= lastX; ++posX)
+				{
+					console.gotoxy(posX, lineNum);
+					if (posX == this.pos.x)
+					{
+						if (!printedBorderColor)
+						{
+							console.print("\x01n" + this.colors.borderColor);
+							printedBorderColor = true;
+						}
+						console.print(this.borderChars.left);
+					}
+					else if (posX == this.pos.x + this.size.width - 1)
+					{
+						if (!printedBorderColor)
+						{
+							console.print("\x01n" + this.colors.borderColor);
+							printedBorderColor = true;
+						}
+						console.print(this.borderChars.right);
+					}
+				}
+			}
+		}
+	}
+	// Calculate the width and starting index of the menu items
+	// Note that pStartX is relative to the menu, not the screen
+	var itemLen = width;
+	var writeMenuItems = true; // Might not if the draw area only includes the scrollbar or border
+	var itemTxtStartIdx = pStartX - 1;
+	if (this.borderEnabled)
+	{
+		if (itemTxtStartIdx > 0)
+			--itemTxtStartIdx; // pStartX - 2
+		if (pStartX == 1)
+			--itemLen;
+		// Starts on 2 & width is 5: 2, 3, 4, 5, 6
+		var lastCol = this.pos.x + pStartX + width - 1;
+		if (this.pos.x + pStartX + width - 1 >= lastCol) // The last column drawn will contain the right border char
+			--itemLen;
+		if ((pStartX == 1 && width == 1) || pStartX == this.size.width)
+			writeMenuItems  = false;
+		else if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow() && pStartX == this.size.width-1)
+			writeMenuItems = false;
+	}
+	if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
+	{
+		var scrollbarCol = this.borderEnabled ? this.pos.x + this.size.width - 2 : this.pos.x + this.size.width - 1;
+		// If the rightmost column is at or past the scrollbar column,
+		// then subtract from the item length so that we don't overwrite
+		// the scrollbar.
+		var rightmostCol = this.pos.x + pStartX + width - 2;
+		if (rightmostCol >= scrollbarCol)
+		{
+			var lenDiff = scrollbarCol - rightmostCol + 1; // The amount to subtract from the length
+			itemLen -= lenDiff;
+		}
+		if (!this.borderEnabled && pStartX == this.size.width)
+			writeMenuItems = false;
+		// Just draw the whole srollbar to ensure it's updated
+		this.DisplayInitialScrollbar(this.scrollbarInfo.solidBlockLastStartRow, this.scrollbarInfo.numSolidScrollBlocks);
+	}
+	if (itemTxtStartIdx < 0)
+		itemTxtStartIdx = 0;
+	// Write the menu items
+	if (writeMenuItems)
+	{
+		/*
+		// Temporary
+		if (user.is_sysop)
+		{
+			console.print("\x01n\r\n");
+			console.print("itemTxtStartIdx: " + itemTxtStartIdx + ", itemLen: " + itemLen + "\r\n");
+			console.pause();
+		}
+		// End Temporary
+		*/
+		var blankItemTextFormatStr = "\x01n%" + itemLen + "s";
+		for (var lineNum = pStartY + this.pos.y - 1; lineNum <= lastLineNum; ++lineNum)
+		{
+			var startX = pStartX;
+			// If borders are enabled, skip the top & bottom lines since borders were already drawn
+			if (this.borderEnabled)
+			{
+				if (lineNum == this.pos.y || lineNum == lastLineNum)
+					continue;
+				else
+				{
+					if (pStartX + this.pos.x - 1 == this.pos.x)
+						++startX;
+				}
+			}
+			// Write the menu item text
+			var itemIdx = this.topItemIdx + (lineNum - this.pos.y);
+			if (this.borderEnabled) --itemIdx;
+			var highlightItem = itemIdx == this.selectedItemIdx;
+			var itemText = this.GetItemText(itemIdx, null, highlightItem, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			//var shortenedText = substrWithAttrCodes(itemText, itemTxtStartIdx, itemLen);
+			var shortenedText = substrWithAttrCodes(itemText, itemTxtStartIdx, itemLen, true);
+			// If shortenedText is empty (perhaps there's no menu item for this line),
+			// then make shortenedText consist of all spaces at the proper length
+			if (shortenedText.length == 0)
+				shortenedText = format(blankItemTextFormatStr, "");
+			console.gotoxy(startX, lineNum);
+			console.print(shortenedText + "\x01n");
+		}
+	}
+}
+// Draws part of the menu, starting at a certain location within the menu and
+// with a given width & height (for screen refreshing).  For this version, the start X
+// and Y location are absolute on the screen.  They start at (1, 1) in the upper-left.
+//
+// Parameters:
+//  pStartX: The column of the character in the menu to start at
+//  pStartY: The row of the character in the menu to start at
+//  pWidth: The width of the content to draw
+//  pHeight: The height of the content to draw
+//  pSelectedItemIndexes: Optional - An object containing indexes of selected items
+function DDLightbarMenu_DrawPartialAbs(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes)
+{
+	if (typeof(pStartX) !== "number" || typeof(pStartY) !== "number" || typeof(pWidth) !== "number" || typeof(pHeight) !== "number")
+		return;
+
+	// Calculate the start X & Y coordinates relative to the menu (1-based), and adjust height &
+	// width if necessary.  Then draw partial.
+	var height = pHeight;
+	var width = pWidth;
+	var startX = pStartX - this.pos.x + 1;
+	var startY = pStartY - this.pos.y + 1;
+	/*
+	// Temporary
+	if (user.is_sysop)
+	{
+		console.print("\x01n\r\n");
+		printf("Here 1 - X, Y, width, height: %d, %d; %d, %d\r\n", startX, startY, width, height);
+		console.pause();
+	}
+	// End Temporary
+	*/
+	if (startX < 1)
+	{
+		var XDiff = 1 - startX;
+		startX += XDiff;
+		width -= XDiff;
+	}
+	if (startY < 1)
+	{
+		var YDiff = 1 - startY;
+		startY += YDiff;
+		height -= YDiff;
+	}
+	/*
+	// Temporary
+	if (user.is_sysop)
+	{
+		console.print("\x01n\r\n");
+		printf("Here 2 - X, Y, width, height: %d, %d; %d, %d\r\n", startX, startY, width, height);
+		console.pause();
+	}
+	// End Temporary
+	*/
+	this.DrawPartial(startX, startY, width, height, pSelectedItemIndexes);
+}
+
+
+// Gets the text of a menu item with colors applied
+//
+// Parameters:
+//  pIdx: The index of the item to get
+//  pItemLen: Optional - Calculated length of the item (in case the scrollbar is showing).
+//            If this is not given, then this will be calculated.
+//  pHighlight: Optional - Whether or not to highlight the item.  If this is not given,
+//              the item will be highlighted based on whether the current selected item
+//              matches the given index, pIdx.
+//  pSelected: Optional - Whether or not this item is selected (mainly intended for multi-select
+//             mode).  Defaults to false.  If true, then a mark character will be displayed
+//             at the end of the item's text.
+function DDLightbarMenu_GetItemText(pIdx, pItemLen, pHighlight, pSelected)
+{
+	var itemText = "";
+	var numItems = this.NumItems();
+	if ((pIdx >= 0) && (pIdx < numItems))
+	{
+		var itemLen = 0;
+		if (typeof(pItemLen) === "number")
+			itemLen = pItemLen;
+		else
+		{
+			itemLen = this.size.width;
+			// If the scrollbar is enabled & we can't show all items in the window,
+			// then subtract 1 from itemLen to make room for the srollbar.
+			if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
+				--itemLen;
+			// If borders are enabled, then subtract another 2 from itemLen to make
+			// room for the left & right borders
+			if (this.borderEnabled)
+				itemLen -= 2;
+			// For numbered mode, we'll need to know the length of the longest item number
+			// so that we can use that space to display the item numbers.
+			if (this.numberedMode)
+			{
+				this.itemNumLen = numItems.toString().length;
+				itemLen -= this.itemNumLen;
+				--itemLen; // Have a space for separation between the numbers and items
+			}
+		}
+
+		// Decide which color(s) to use for the item text
+		var menuItem = this.GetItem(pIdx);
+		var normalItemColor;
+		var selectedItemColor;
+		if (menuItem.itemColor != null)
+			normalItemColor = menuItem.itemColor;
+		else
+			normalItemColor = (menuItem.useAltColors ? this.colors.altItemColor : this.colors.itemColor);
+		if (menuItem.itemSelectedColor != null)
+			selectedItemColor = menuItem.itemSelectedColor;
+		else
+			selectedItemColor = (menuItem.useAltColors ? this.colors.altSelectedItemColor : this.colors.selectedItemColor);
+		var itemColor = "";
+		if (this.allowUnselectableItems && !menuItem.isSelectable)
+			itemColor = this.colors.unselectableItemColor;
+		else if (typeof(pHighlight) === "boolean")
+			itemColor = (pHighlight ? selectedItemColor : normalItemColor);
+		else
+			itemColor = (pIdx == this.selectedItemIdx ? selectedItemColor : normalItemColor);
+		var selected = (typeof(pSelected) == "boolean" ? pSelected : false);
+
+		// Get the item text
+		if ((typeof(itemColor) === "string" || Array.isArray(itemColor)) && itemColor.length > 0)
+		{
+			// Use strip_ctrl to ensure there are no attribute codes, since we will
+			// apply our own.  This might be only a temporary item returned by a
+			// replaced GetItem(), so we just have to strip_ctrl() it here.
+			itemText = strip_ctrl(menuItem.text);
+		}
+		else // Allow other colors in the text to be specified if the configured item color is empty
+			itemText = menuItem.text;
+		// Truncate the item text to the displayable item width
+		if (itemTextDisplayableLen(itemText, this.ampersandHotkeysInItems) > itemLen)
+			itemText = substrWithAttrCodes(itemText, 0, itemLen); //itemText = itemText.substr(0, itemLen);
+		// If the item text is empty, then fill it with spaces for the item length
+		// so that the line's colors/attributes will be applied for the whole line
+		// when written
+		if (strip_ctrl(itemText).length == 0)
+			itemText = format("%" + itemLen + "s", "");
+		// Add the item color to the item text
+		itemText = addAttrsToString(itemText, itemColor);
+		// If ampersandHotkeysInItems is true, see if there's an ampersand in
+		// the item text.  If so, we'll want to highlight the next character
+		// with a different color.
+		if (this.ampersandHotkeysInItems)
+		{
+			var ampersandIndex = itemText.indexOf("&");
+			if (ampersandIndex > -1)
+			{
+				// See if the next character is a space character.  If not, then remove
+				// the ampersand and highlight the next character in the text.
+				if (itemText.length > ampersandIndex+1)
+				{
+					var nextChar = itemText.substr(ampersandIndex+1, 1);
+					if (nextChar != " ")
+					{
+						itemText = itemText.substr(0, ampersandIndex) + this.colors.itemTextCharHighlightColor
+								 + nextChar + "\x01n" + itemColor + itemText.substr(ampersandIndex+2);
+					}
+				}
+			}
+		}
+		// If the item is selected, then display a check mark at the end of the item text.
+		if (selected)
+		{
+			var itemTextLen = itemTextDisplayableLen(itemText, this.ampersandHotkeysInItems);
+			if (itemTextLen < this.size.width)
+			{
+				var numSpaces = itemLen - itemTextLen - 2;
+				// Kludge? If numSpaces is positive, append a space and then the selected
+				// character,  Otherwise, we'll need to replace the last character in
+				// itemText with the selected character.
+				if (numSpaces > 0)
+					itemText += format("%" + numSpaces + "s %s", "", this.multiSelectItemChar);
+				else
+					itemText = itemText.substr(0, itemText.length-1) + this.multiSelectItemChar;
+			}
+			else
+			{
+				// itemText should already be shortened to only the menu width, so
+				// take 2 characters off the end and add a space and mark character
+				itemText = itemText.substr(0, itemText.length-2) + " " + this.multiSelectItemChar;
+			}
+		}
+		// Ensure the item text fills the width of the menu (in case there's a
+		// background color, it should be used for the entire width of the item
+		// text).  Then write the item.
+		var currentTextLen = itemTextDisplayableLen(itemText, this.ampersandHotkeysInItems);
+		if (currentTextLen < itemLen)
+			itemText += format("%" + +(itemLen-currentTextLen) + "s", ""); // Append spaces to the end of itemText
+		// If in numbered mode and the item is selectable, prepend the item number to the front of the item text.
+		if (this.numberedMode && menuItem.isSelectable)
+		{
+			if (this.itemNumLen == 0)
+				this.itemNumLen = numItems.toString().length;
+			var numColor = "\x01n" + this.colors.itemNumColor;
+			if (typeof(pHighlight) === "boolean")
+				numColor = (pHighlight ? this.colors.highlightedItemNumColor : this.colors.itemNumColor);
+			else
+				numColor = (pIdx == this.selectedItemIdx ? this.colors.highlightedItemNumColor : this.colors.itemNumColor);
+			itemText = format("\x01n" + numColor + "%" + this.itemNumLen + "d \x01n", pIdx+1) + itemText;
+		}
+	}
+	return itemText;
+}
+
+// Returns whether or not an item's text is UTF-8, as specified in the item.
+//
+// Parameters:
+//  pIdx: The index of the item
+//
+// Return value: Whether or not the item's text is UTF-8
+function DDLightbarMenu_ItemTextIsUTF8(pIdx)
+{
+	if (typeof(pIdx) !== "number")
+		return false;
+	if (pIdx < 0 || pIdx >= this.NumItems())
+		return false;
+
+	return this.GetItem(pIdx).textIsUTF8;
+}
+
+// Erases the menu - Draws black (normal color) where the menu was
+function DDLightbarMenu_Erase()
+{
+	var formatStr = "%" + this.size.width + "s"; // For use with printf()
+	console.attributes = "N";
+	var curPos = { x: this.pos.x, y: this.pos.y };
+	for (var i = 0; i < this.size.height; ++i)
+	{
+		console.gotoxy(curPos.x, curPos.y++);
+		printf(formatStr, "");
+	}
+	console.gotoxy(curPos);
+}
+
+// Sets a hotkey for a menu item
+//
+// Parameters:
+//  pIdx: The index of the menu item
+//  pHotkey: The hotkey to set for the menu item
+function DDLightbarMenu_SetItemHotkey(pIdx, pHotkey)
+{
+	if ((typeof(pIdx) == "number") && (pIdx >= 0) && (pIdx < this.items.length) && (typeof(pHotkey) == "string"))
+		this.items[pIdx].hotkeys = pHotkey;
+}
+
+// Adds a hotkey for a menu item (in addition to the item's other hotkeys)
+//
+// Parameters:
+//  pIdx: The index of the menu item
+//  pHotkey: The hotkey to add for the menu item
+function DDLightbarMenu_AddItemHotkey(pIdx, pHotkey)
+{
+	if ((typeof(pIdx) == "number") && (pIdx >= 0) && (pIdx < this.items.length) && (typeof(pHotkey) == "string") && (this.items[pIdx].hotkeys.indexOf(pHotkey) == -1))
+		this.items[pIdx].hotkeys += pHotkey;
+}
+
+// Removes a specific hotkey from an item.
+//
+// Parameters:
+//  pIdx: The index of the item
+//  pHotkey: The hotkey to remove from the item
+function DDLightbarMenu_RemoveItemHotkey(pIdx, pHotkey)
+{
+	if ((typeof(pIdx) == "number") && (pIdx >= 0) && (pIdx < this.items.length))
+	{
+		var hotkeyIdx = this.items[pIdx].hotkeys.indexOf(pHotkey);
+		while (hotkeyIdx > -1)
+		{
+			this.items[pIdx].hotkeys = this.items[pIdx].hotkeys.substr(0, hotkeyIdx) + this.items[pIdx].hotkeys.substr(hotkeyIdx+1);
+			hotkeyIdx = this.items[pIdx].hotkeys.indexOf(pHotkey);
+		}
+	}
+}
+
+// Removes all hotkeys for an item
+//
+// Parameters:
+//  pIdx: The index of the item
+function  DDLightbarMenu_RemoveItemHotkeys(pIdx)
+{
+	if ((typeof(pIdx) == "number") && (pIdx >= 0) && (pIdx < this.items.length))
+		this.items[pIdx].hotkeys = "";
+}
+
+// Removes the hotkeys from all items
+function DDLightbarMenu_RemoveAllItemHotkeys()
+{
+	for (var i = 0; i < this.items.length; ++i)
+		this.items[i].hotkeys = "";
+}
+
+// Returns an object specifying the mouse valid click region for the menu,
+// with properties left, right, top, and bottom.
+function DDLightbarMenu_GetMouseClickRegion()
+{
+	var clickRegion = {
+		left: this.pos.x,
+		right: this.pos.x + this.size.width - 1,
+		top: this.pos.y,
+		bottom: this.pos.y + this.size.height - 1
+	};
+	if (this.borderEnabled)
+	{
+		++clickRegion.left;
+		++clickRegion.top;
+		--clickRegion.right;
+		--clickRegion.bottom;
+	}
+	return clickRegion;
+}
+
+// Waits for user input, optionally drawing the menu first.
+//
+// Parameters:
+//  pDraw: Optional - Whether or not to draw the menu first.  By default, the
+//         menu will be drawn first.
+//  pSelectedItemIndexes: Optional - An object containing indexes of selected items
+function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
+{
+	this.lastUserInput = null;
+
+	var numItems = this.NumItems();
+	if (numItems == 0)
+		return null;
+
+	// If allowing unselectable items, then make sure there are selectable items before
+	// doing the input loop (and if not, return null).  If there are selectable items,
+	// make sure the current selected item is selectable (if not, go to the next one).
+	if (this.allowUnselectableItems)
+	{
+		if (this.HasAnySelectableItems())
+		{
+			if (!this.ItemIsSelectable(this.selectedItemIdx))
+			{
+				var nextSelectableItemIdx = this.FindSelectableItemForward(this.selectedItemIdx+1, true);
+				// nextSelectableItemIdx should be valid since we know there are selectable items
+				if (nextSelectableItemIdx > -1 && nextSelectableItemIdx != this.selectedItemIdx)
+				{
+					this.selectedItemIdx = nextSelectableItemIdx;
+					this.CalcPageForItemAndSetTopItemIdx(this.GetNumItemsPerPage(), numItems);
+				}
+			}
+		}
+		else // No selectable items
+			return null;
+	}
+
+	if (typeof(this.lastMouseClickTime) == "undefined")
+		this.lastMouseClickTime = -1;
+
+	var draw = (typeof(pDraw) == "boolean" ? pDraw : true);
+	if (draw)
+	{
+		this.Draw(pSelectedItemIndexes, null, null, numItems);
+		if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
+			this.DisplayInitialScrollbar(this.scrollbarInfo.solidBlockLastStartRow);
+	}
+
+	if (this.callOnItemNavOnStartup && typeof(this.OnItemNav) === "function")
+		this.OnItemNav(0, this.selectedItemIdx);
+
+	var selectedItemIndexes = { }; // For multi-select mode
+	if (typeof(pSelectedItemIndexes) == "object")
+		selectedItemIndexes = pSelectedItemIndexes;
+	if (this.ANSISupported())
+	{
+		// User input loop
+		var userChoices = null; // For multi-select mode
+		var retVal = null; // For single-choice mode
+		// mouseInputOnly_continue specifies whether to continue to the
+		// next iteration if the mouse was clicked & there's no need to
+		// process user input further
+		var mouseInputOnly_continue = false;
+		var continueOn = true;
+		while (continueOn)
+		{
+			if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
+				this.UpdateScrollbarWithHighlightedItem();
+
+			mouseInputOnly_continue = false;
+
+			// TODO: With mouse_getkey(), it seems you need to press ESC twice
+			// to get the ESC key and exit the menu
+			var inputMode = K_NOECHO|K_NOSPIN|K_NOCRLF;
+			var mk = null; // Will be used for mouse support
+			var mouseNoAction = false;
+			if (this.mouseEnabled)
+			{
+				mk = mouse_getkey(inputMode, this.inputTimeoutMS > 1 ? this.inputTimeoutMS : undefined, this.mouseEnabled);
+				if (mk.mouse !== null)
+				{
+					// See if the user clicked anywhere in the region where items are
+					// listed or the scrollbar
+					var clickRegion = this.GetMouseClickRegion();
+					// Button 0 is the left/main mouse button
+					if (mk.mouse.press && (mk.mouse.button == 0) && (mk.mouse.motion == 0) &&
+						(mk.mouse.x >= clickRegion.left) && (mk.mouse.x <= clickRegion.right) &&
+						(mk.mouse.y >= clickRegion.top) && (mk.mouse.y <= clickRegion.bottom))
+					{
+						var isDoubleClick = ((this.lastMouseClickTime > -1) && (system.timer - this.lastMouseClickTime <= 0.4));
+
+						// If the scrollbar is enabled, then see if the mouse click was
+						// in the scrollbar region.  If below the scrollbar bright blocks,
+						// then we'll want to do a PageDown.  If above the scrollbar bright
+						// blocks, then we'll want to do a PageUp.
+						var scrollbarX = this.pos.x + this.size.width - 1;
+						if (this.borderEnabled)
+							--scrollbarX;
+						if ((mk.mouse.x == scrollbarX) && this.scrollbarEnabled)
+						{
+							var scrollbarSolidBlockEndRow = this.scrollbarInfo.solidBlockLastStartRow + this.scrollbarInfo.numSolidScrollBlocks - 1;
+							if (mk.mouse.y < this.scrollbarInfo.solidBlockLastStartRow)
+								this.lastUserInput = KEY_PAGEUP;
+							else if (mk.mouse.y > scrollbarSolidBlockEndRow)
+								this.lastUserInput = KEY_PAGEDN;
+							else
+							{
+								// Mouse click no-action
+								// TODO: Can we detect if they're holding the mouse down
+								// and scroll while the user holds the mouse & scrolls on
+								// the scrollbar?
+								this.lastUserInput = "";
+								mouseNoAction = true;
+								mouseInputOnly_continue = true;
+							}
+						}
+						else
+						{
+							// The user didn't click on the scrollbar or the scrollbar
+							// isn't enabled.
+							// For a double-click, if multi-select is enabled, set the
+							// last user input to a space to select/de-select the item.
+							if (isDoubleClick)
+							{
+								if (this.multiSelect)
+									this.lastUserInput = " ";
+								else
+								{
+									// No mouse action
+									this.lastUserInput = "";
+									mouseNoAction = true;
+									mouseInputOnly_continue = true;
+								}
+							}
+							else
+							{
+								// Make the clicked-on item the currently highlighted
+								// item.  Only select the item if the index is valid.
+								var topItemY = (this.borderEnabled ? this.pos.y + 1 : this.pos.y);
+								var distFromTopY = mk.mouse.y - topItemY;
+								var itemIdx = this.topItemIdx + distFromTopY;
+								if ((itemIdx >= 0) && (itemIdx < this.NumItems()))
+								{
+									this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+									this.selectedItemIdx = itemIdx;
+									this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+								}
+								// Don't have the later code do anything
+								this.lastUserInput = "";
+								mouseNoAction = true;
+								mouseInputOnly_continue = true;
+							}
+						}
+
+						this.lastMouseClickTime = system.timer;
+					}
+					else
+					{
+						// The mouse click is outside the click region.  Set the appropriate
+						// variables for mouse no-action.
+						// TODO: Perhaps this may also need to be done in some places above
+						// where no action needs to be taken
+						this.lastUserInput = "";
+						mouseNoAction = true;
+						mouseInputOnly_continue = true;
+					}
+				}
+				else
+				{
+					// mouse is null, so a keybaord key must have been pressed
+					this.lastUserInput = mk.key;
+				}
+			}
+			else // this.mouseEnabled is false
+				this.lastUserInput = getKeyWithESCChars(inputMode, this.inputTimeoutMS);
+
+			// If no further input processing needs to be done due to a mouse click
+			// action, then continue to the next loop iteration.
+			if (mouseInputOnly_continue)
+				continue;
+
+			// Take the appropriate action based on the user's last input/keypress
+			if ((this.lastUserInput == KEY_ESC) || this.QuitKeysIncludes(this.lastUserInput) || console.aborted)
+			{
+				// Only exit if there was not a no-action mouse click
+				// TODO: Is this logic good and clean?
+				var goAheadAndExit = true;
+				if (mk !== null && mk.mouse !== null)
+				{
+					goAheadAndExit = !mouseNoAction; // Only really needed with an input timer?
+				}
+				if (goAheadAndExit)
+				{
+					continueOn = false;
+					// Ensure any returned choice objects are null/empty to signal
+					// that the user aborted
+					userChoices = null; // For multi-select mode
+					selectedItemIndexes = { }; // For multi-select mode
+					retVal = null; // For single-choice mode
+				}
+			}
+			else if ((this.lastUserInput == KEY_UP) || (this.lastUserInput == KEY_LEFT))
+				this.DoKeyUp(selectedItemIndexes, numItems);
+			else if ((this.lastUserInput == KEY_DOWN) || (this.lastUserInput == KEY_RIGHT))
+				this.DoKeyDown(selectedItemIndexes, numItems);
+			else if (this.lastUserInput == KEY_PAGEUP || this.PageUpKeysIncludes(this.lastUserInput))
+				this.DoPageUp(selectedItemIndexes, numItems);
+			else if (this.lastUserInput == KEY_PAGEDN || this.PageDownKeysIncludes(this.lastUserInput))
+				this.DoPageDown(selectedItemIndexes, numItems);
+			else if (this.lastUserInput == KEY_HOME || this.FirstPageKeysIncludes(this.lastUserInput))
+			{
+				// Go to the first item in the list
+				var firstSelectableItemIdx = this.FindSelectableItemForward(0, false);
+				if (this.selectedItemIdx > firstSelectableItemIdx)
+					this.NavMenuForNewSelectedItemTop(firstSelectableItemIdx, this.GetNumItemsPerPage(), numItems, selectedItemIndexes);
+			}
+			else if (this.lastUserInput == KEY_END || this.LastPageKeysIncludes(this.lastUserInput))
+			{
+				// Go to the last item in the list
+				var lastSelectableItem = this.FindSelectableItemBackward(numItems-1, false);
+				if (this.selectedItemIdx < lastSelectableItem)
+					this.NavMenuForNewSelectedItemBottom(lastSelectableItem, this.GetNumItemsPerPage(), numItems, selectedItemIndexes, true);
+			}
+			// Enter key or additional select-item key: Select the item & quit out of the input loop
+			else if ((this.lastUserInput == KEY_ENTER) || (this.SelectItemKeysIncludes(this.lastUserInput)))
+			{
+				// Let the user select the item if ValidateSelectItem() returns true
+				var allowSelectItem = true;
+				if (typeof(this.ValidateSelectItem) === "function")
+					allowSelectItem = this.ValidateSelectItem(this.GetItem(this.selectedItemIdx).retval);
+				if (allowSelectItem)
+				{
+					// If multi-select is enabled and if the user hasn't made any choices,
+					// then add the current item to the user choices.  Otherwise, choose
+					// the current item.  Then exit.
+					if (this.multiSelect)
+					{
+						if (Object.keys(selectedItemIndexes).length == 0)
+							selectedItemIndexes[+(this.selectedItemIdx)] = true;
+					}
+					else
+						retVal = this.GetItem(this.selectedItemIdx).retval;
+
+					// Run the OnItemSelect event function
+					if (typeof(this.OnItemSelect) === "function")
+						this.OnItemSelect(retVal, true);
+
+					// Exit the input loop if this.exitOnItemSelect is set to true
+					if (this.exitOnItemSelect)
+						continueOn = false;
+				}
+			}
+			else if (this.lastUserInput == " ") // Add the current item to multi-select
+			{
+				// Add the current item to multi-select if multi-select is enabled
+				if (this.multiSelect)
+				{
+					// Only let the user select the item if ValidateSelectItem() returns true
+					var allowSelectItem = true;
+					if (typeof(this.ValidateSelectItem) === "function")
+						allowSelectItem = this.ValidateSelectItem(this.GetItem(this.selectedItemIdx).retval);
+					if (allowSelectItem)
+					{
+						var added = false; // Will be true if added or false if deleted
+						if (selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)))
+							delete selectedItemIndexes[+(this.selectedItemIdx)];
+						else
+						{
+							var addIt = true;
+							if (this.maxNumSelections > 0)
+								addIt = (Object.keys(selectedItemIndexes).length < this.maxNumSelections);
+							if (addIt)
+							{
+								selectedItemIndexes[+(this.selectedItemIdx)] = true;
+								added = true;
+							}
+						}
+
+						// Run the OnItemSelect event function
+						if (typeof(this.OnItemSelect) === "function")
+						{
+							//this.OnItemSelect = function(pItemRetval, pSelected) { }
+							this.OnItemSelect(this.GetItem(this.selectedItemIdx).retval, added);
+						}
+
+						// Draw a character next to the item if it's selected, or nothing if it's not selected
+						var XPos = this.pos.x + this.size.width - 2;
+						var YPos = this.pos.y+(this.selectedItemIdx-this.topItemIdx);
+						if (this.borderEnabled)
+						{
+							--XPos;
+							++YPos;
+						}
+						if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
+							--XPos;
+						console.gotoxy(XPos, YPos);
+						if (added)
+						{
+							// If the item color is an array, then default to a color string here
+							var itemColor = this.GetColorForItem(this.selectedItemIdx, true);
+							if (Array.isArray(itemColor))
+							{
+								var bkgColor = getBackgroundAttrAtIdx(itemColor, this.size.width-1);
+								itemColor = "\x01n\x01h\x01g" + bkgColor;
+							}
+							console.print(itemColor + " " + this.multiSelectItemChar + "\x01n");
+						}
+						else
+						{
+							// Display the last 2 characters of the regular item text
+							var itemText = this.GetItemText(this.selectedItemIdx, null, true, false);
+							var textToPrint = substrWithAttrCodes(itemText, console.strlen(itemText)-2, 2);
+							console.print(textToPrint + "\x01n");
+						}
+					}
+				}
+			}
+			// For numbered mode, if the user enters a number, allow the user to
+			// choose an item by typing its number.
+			else if (/[0-9]/.test(this.lastUserInput) && this.numberedMode)
+			{
+				var originalCurpos = console.getxy();
+
+				// Put the user's input back in the input buffer to
+				// be used for getting the rest of the message number.
+				console.ungetstr(this.lastUserInput);
+				// Move the cursor to the bottom of the screen and
+				// prompt the user for the message number.
+				var promptX = this.pos.x;
+				var promptY = this.pos.y+this.size.height;
+				console.gotoxy(promptX, promptY);
+				printf("\x01n%" + this.size.width + "s", ""); // Blank out what might be on the screen already
+				console.gotoxy(promptX, promptY);
+				console.print("\x01cItem #: \x01h");
+				var userEnteredItemNum = console.getnum(numItems);
+				// Blank out the input prompt
+				console.gotoxy(promptX, promptY);
+				printf("\x01n%" + this.size.width + "s", "");
+				// If the user entered a number, then get that item's return value
+				// and stop the input loop.
+				if (userEnteredItemNum > 0)
+				{
+					var oldSelectedItemIdx = this.selectedItemIdx;
+					this.selectedItemIdx = userEnteredItemNum-1;
+					if (this.multiSelect)
+					{
+						if (selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)))
+							delete selectedItemIndexes[+(this.selectedItemIdx)];
+						else
+						{
+							var addIt = true;
+							if (this.maxNumSelections > 0)
+								addIt = (Object.keys(selectedItemIndexes).length < this.maxNumSelections);
+							if (addIt)
+								selectedItemIndexes[+(this.selectedItemIdx)] = true;
+						}
+					}
+					else
+					{
+						retVal = this.GetItem(this.selectedItemIdx).retval;
+						continueOn = false;
+					}
+					// If the item typed by the user is different than the current selected item
+					// index, then refresh the selected item on the menu (if they're visible).
+					// If multi-select mode is enabled, also toggle the checkmark in the item text.
+					if (this.selectedItemIdx != oldSelectedItemIdx)
+					{
+						if (this.ScreenRowForItem(oldSelectedItemIdx) > -1)
+						{
+							var oldIsSelected = selectedItemIndexes.hasOwnProperty(oldSelectedItemIdx);
+							this.WriteItemAtItsLocation(oldSelectedItemIdx, false, oldIsSelected);
+						}
+						if (this.ScreenRowForItem(this.selectedItemIdx) > -1)
+						{
+							var newIsSelected = selectedItemIndexes.hasOwnProperty(this.selectedItemIdx);
+							this.WriteItemAtItsLocation(this.selectedItemIdx, true, newIsSelected);
+						}
+					}
+
+					if (typeof(this.OnItemNav) === "function")
+						this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+				}
+				else
+					console.gotoxy(originalCurpos); // Move the cursor back where it was
+			}
+			else
+			{
+				// See if the user pressed a hotkey set for one of the items.  If so,
+				// then choose that item.
+				for (var i = 0; i < numItems; ++i)
+				{
+					var theItem = this.GetItem(i);
+					for (var h = 0; h < theItem.hotkeys.length; ++h)
+					{
+						var userPressedHotkey = false;
+						if (this.hotkeyCaseSensitive)
+							userPressedHotkey = (this.lastUserInput == theItem.hotkeys.charAt(h));
+						else
+							userPressedHotkey = (this.lastUserInput.toUpperCase() == theItem.hotkeys.charAt(h).toUpperCase());
+						if (userPressedHotkey)
+						{
+							if (this.multiSelect)
+							{
+								if (selectedItemIndexes.hasOwnProperty(i))
+									delete selectedItemIndexes[i];
+								else
+								{
+									var addIt = true;
+									if (this.maxNumSelections > 0)
+										addIt = (Object.keys(selectedItemIndexes).length < this.maxNumSelections);
+									if (addIt)
+										selectedItemIndexes[i] = true;
+								}
+								// TODO: Screen refresh?
+							}
+							else
+							{
+								retVal = theItem.retval;
+								var oldSelectedItemIdx = this.selectedItemIdx;
+								this.selectedItemIdx = i;
+								continueOn = false;
+								if (typeof(this.OnItemNav) === "function")
+									this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+							}
+							break;
+						}
+					}
+				}
+			}
+		}
+	}
+	else
+	{
+		// The user's terminal doesn't support ANSI
+		var userAnswerIsValid = false;
+		do
+		{
+			console.print("\x01n\x01c\x01hY\x01n\x01cour \x01hC\x01n\x01choice\x01h\x01g: \x01c");
+			console.attributes = "N";
+			var userEnteredItemNum = console.getnum(numItems);
+			this.lastUserInput = userEnteredItemNum.toString();
+			if (!console.aborted && userEnteredItemNum > 0)
+			{
+				if (this.ItemIsSelectable(userEnteredItemNum-1))
+				{
+					var chosenItem = this.GetItem(userEnteredItemNum-1);
+					if (typeof(chosenItem) === "object" && chosenItem.hasOwnProperty("retval"))
+						retVal = chosenItem.retval;
+					userAnswerIsValid = true;
+				}
+			}
+			else
+			{
+				this.lastUserInput = "Q"; // To signify quitting
+				userAnswerIsValid = true;
+			}
+		} while (!userAnswerIsValid);
+	}
+
+	// Set the screen color back to normal so that text written to the screen
+	// after this looks good.
+	console.attributes = "N";
+	
+	// If in multi-select mode, populate userChoices with the choices
+	// that the user selected.
+	if (this.multiSelect && (Object.keys(selectedItemIndexes).length > 0))
+	{
+		userChoices = [];
+		for (var prop in selectedItemIndexes)
+			userChoices.push(this.GetItem(prop).retval);
+	}
+
+	return (this.multiSelect ? userChoices : retVal);
+}
+// Performs the key-up behavior for showing the menu items
+//
+// Parameters:
+//  pSelectedItemIndexes: An object containing indexes of selected items.  This is
+//                        normally a temporary object created/used in GetVal().
+//  pNumItems: The pre-calculated number of menu items.  If this not given, this
+//             will be retrieved by calling NumItems().
+function DDLightbarMenu_DoKeyUp(pSelectedItemIndexes, pNumItems)
+{
+	var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {});
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	if (this.selectedItemIdx > this.FirstSelectableItemIdx(numItems))
+	{
+		var prevSelectableItemIdx = this.FindSelectableItemBackward(this.selectedItemIdx-1, false);
+		if (prevSelectableItemIdx < this.selectedItemIdx && prevSelectableItemIdx > -1)
+		{
+			// Draw the current item in regular colors
+			this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			var oldSelectedItemIdx = this.selectedItemIdx;
+			this.selectedItemIdx = prevSelectableItemIdx;
+			var numItemsDiff = oldSelectedItemIdx - prevSelectableItemIdx;
+			// Draw the new current item in selected colors
+			// If the selected item is above the top of the menu, then we'll need to
+			// scroll the items down.
+			if (this.selectedItemIdx < this.topItemIdx)
+			{
+				this.topItemIdx -= numItemsDiff;
+				this.Draw(selectedItemIndexes);
+			}
+			else
+			{
+				// The selected item is not above the top of the menu, so we can
+				// just draw the selected item highlighted.
+				this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			}
+			if (typeof(this.OnItemNav) === "function")
+				this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+		}
+	}
+	else
+	{
+		// selectedItemIdx is 0.  If wrap navigation is enabled, then go to the
+		// last item.
+		// If there are unselectable items above the current one, then scroll the item list up before
+		// wrapping down to the last selectable item
+		/*
+		var canWrapNav = false;
+		if (this.allowUnselectableItems && this.selectedItemIdx > 0)
+		{
+			if (this.topItemIdx > 0)
+			{
+				--this.topItemIdx;
+				this.Draw(selectedItemIndexes);
+			}
+			else
+				canWrapNav = true;
+		}
+		*/
+		var canWrapNav = true;
+		if (this.allowUnselectableItems)
+		{
+			canWrapNav = false;
+			if (this.selectedItemIdx > 0)
+			{
+				if (this.topItemIdx > 0)
+				{
+					--this.topItemIdx;
+					this.Draw(selectedItemIndexes);
+				}
+				else
+					canWrapNav = true;
+			}
+		}
+		if (canWrapNav && this.wrapNavigation)
+		{
+			// If there are more items than can fit on the menu, then ideally, the top
+			// item index would be the one at the top of the page where the rest of the items
+			// fill the menu.
+			var prevSelectableItemIdx = this.FindSelectableItemBackward(numItems-1, false);
+			if (prevSelectableItemIdx > this.selectedItemIdx && prevSelectableItemIdx > -1)
+			{
+				// Draw the current item in regular colors
+				this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+				// Set the new selected item index, and figure out what page it's on
+				var oldSelectedItemIdx = this.selectedItemIdx;
+				this.selectedItemIdx = prevSelectableItemIdx;
+				// Calculate the top index for the page of the new selected item.  If the page
+				// is different, go to that page.
+				if (this.CalcPageForItemAndSetTopItemIdx(this.GetNumItemsPerPage(), numItems))
+					this.Draw(selectedItemIndexes);
+				else // The selected item is on the current page
+					this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+
+				if (typeof(this.OnItemNav) === "function")
+					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+			}
+		}
+	}
+}
+// Performs the key-down behavior for showing the menu items
+//
+// Parameters:
+//  pSelectedItemIndexes: An object containing indexes of selected items.  This is
+//                        normally a temporary object created/used in GetVal().
+//  pNumItems: The pre-calculated number of menu items.  If this not given, this
+//             will be retrieved by calling NumItems().
+function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems)
+{
+	var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {});
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+
+	if (this.selectedItemIdx < this.LastSelectableItemIdx(numItems))
+	{
+		var nextSelectableItemIdx = this.FindSelectableItemForward(this.selectedItemIdx+1, false);
+		if (nextSelectableItemIdx > this.selectedItemIdx)
+		{
+			// Draw the current item in regular colors
+			this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
+			var oldSelectedItemIdx = this.selectedItemIdx;
+			this.selectedItemIdx = nextSelectableItemIdx;
+			var numItemsDiff = nextSelectableItemIdx - oldSelectedItemIdx;
+			// Draw the new current item in selected colors
+			// If the selected item is below the bottom of the menu, then we'll need to
+			// scroll the items up.
+			var numItemsPerPage = this.GetNumItemsPerPage();
+			if (this.selectedItemIdx > this.topItemIdx + numItemsPerPage-1)
+			{
+				this.topItemIdx += numItemsDiff;
+				this.Draw(selectedItemIndexes);
+			}
+			else
+			{
+				// The selected item is not below the bottom of the menu, so we can
+				// just draw the selected item highlighted.
+				this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
+			}
+			if (typeof(this.OnItemNav) === "function")
+				this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+		}
+	}
+	else
+	{
+		// selectedItemIdx is the last item index.  If wrap navigation is enabled,
+		// then go to the first item.
+		// If there are unselectable items below the current one, then scroll the item down up before
+		// wrapping up to the first selectable item
+		/*
+		var canWrapNav = false;
+		if (this.allowUnselectableItems && this.selectedItemIdx > 0)
+		{
+			var topIndexForLastPage = numItems - this.GetNumItemsPerPage();
+			if (topIndexForLastPage < 0)
+				topIndexForLastPage = 0;
+			else if (topIndexForLastPage >= numItems)
+				topIndexForLastPage = numItems - 1;
+			if (this.topItemIdx < topIndexForLastPage)
+			{
+				++this.topItemIdx;
+				this.Draw(selectedItemIndexes);
+			}
+			else
+				canWrapNav = true;
+		}
+		*/
+		var canWrapNav = true;
+		if (this.allowUnselectableItems)
+		{
+			canWrapNav = false;
+			if (this.selectedItemIdx > 0)
+			{
+				var topIndexForLastPage = numItems - this.GetNumItemsPerPage();
+				if (topIndexForLastPage < 0)
+					topIndexForLastPage = 0;
+				else if (topIndexForLastPage >= numItems)
+					topIndexForLastPage = numItems - 1;
+				if (this.topItemIdx < topIndexForLastPage)
+				{
+					++this.topItemIdx;
+					this.Draw(selectedItemIndexes);
+				}
+				else
+					canWrapNav = true;
+			}
+		}
+		if (canWrapNav && this.wrapNavigation)
+		{
+			// If there are more items than can fit on the menu, then ideally, the top
+			// item index would be the one at the top of the page where the rest of the items
+			// fill the menu.
+			//var nextSelectableItemIdx = this.FindSelectableItemForward(0, false);
+			var nextSelectableItemIdx = this.FirstSelectableItemIdx(numItems);
+			if (nextSelectableItemIdx < this.selectedItemIdx && nextSelectableItemIdx > -1)
+			{
+				// Draw the current item in regular colors
+				this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+				// Set the new selected item index, and figure out what page it's on
+				var oldSelectedItemIdx = this.selectedItemIdx;
+				this.selectedItemIdx = nextSelectableItemIdx;
+				// Calculate the top index for the page of the new selected item.  If the page
+				// is different, go to that page.
+				if (this.CalcPageForItemAndSetTopItemIdx(this.GetNumItemsPerPage(), numItems))
+					this.Draw(selectedItemIndexes);
+				else // The selected item is on the current page
+					this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+
+				if (typeof(this.OnItemNav) === "function")
+					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+			}
+
+			// Older, before non-selectable items:
+			/*
+			// Draw the current item in regular colors
+			this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
+			// Go to the first item and scroll to the top if necessary
+			var oldSelectedItemIdx = this.selectedItemIdx;
+			this.selectedItemIdx = 0;
+			var oldTopItemIdx = this.topItemIdx;
+			this.topItemIdx = 0;
+			if (this.topItemIdx != oldTopItemIdx)
+				this.Draw(selectedItemIndexes);
+			else
+			{
+				// Draw the new current item in selected colors
+				this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
+			}
+			if (typeof(this.OnItemNav) === "function")
+				this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+			*/
+		}
+	}
+}
+// Performs the page-up behavior for showing the menu items
+//
+// Parameters:
+//  pSelectedItemIndexes: An object containing indexes of selected items.  This is
+//                        normally a temporary object created/used in GetVal().
+//  pNumItems: The pre-calculated number of menu items.  If this not given, this
+//             will be retrieved by calling NumItems().
+function DDLightbarMenu_DoPageUp(pSelectedItemIndexes, pNumItems)
+{
+	var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {});
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	var numItemsPerPage = this.GetNumItemsPerPage();
+
+	var prevSelectableItemIdx = 0;
+	var currentPageNum = findPageNumOfItemNum(this.selectedItemIdx+1, numItemsPerPage, numItems, false);
+	if (currentPageNum > 1)
+	{
+		var startIdxToCheck = this.selectedItemIdx - numItemsPerPage;
+		if (startIdxToCheck < 0)
+		{
+			//startIdxToCheck = 0;
+			startIdxToCheck = (this.selectedItemIdx > 0 ? this.selectedItemIdx - 1 : 0);
+		}
+		prevSelectableItemIdx = this.FindSelectableItemBackward(startIdxToCheck, this.wrapNavigation);
+		//this.NavMenuForNewSelectedItemTop(prevSelectableItemIdx, numItemsPerPage, numItems, selectedItemIndexes);
+	}
+	else
+		prevSelectableItemIdx = this.FindSelectableItemForward(0, this.wrapNavigation);
+	this.NavMenuForNewSelectedItemTop(prevSelectableItemIdx, numItemsPerPage, numItems, selectedItemIndexes);
+
+	// Older, before un-selectable items:
+	/*
+	// Only do this if we're not already at the top of the list
+	if (this.topItemIdx > 0)
+	{
+		var oldSelectedItemIdx = this.selectedItemIdx;
+		var numItemsPerPage = this.GetNumItemsPerPage();
+		var newTopItemIdx = this.topItemIdx - numItemsPerPage;
+		if (newTopItemIdx < 0)
+			newTopItemIdx = 0;
+		if (newTopItemIdx != this.topItemIdx)
+		{
+			this.topItemIdx = newTopItemIdx;
+			this.selectedItemIdx -= numItemsPerPage;
+			if (this.selectedItemIdx < 0)
+				this.selectedItemIdx = 0;
+			this.Draw(selectedItemIndexes);
+		}
+		else
+		{
+			// The top index is the top index for the last page.
+			// If wrapping is enabled, then go back to the first page.
+			if (this.wrapNavigation)
+			{
+				var topIndexForLastPage = numItems - numItemsPerPage;
+				if (topIndexForLastPage < 0)
+					topIndexForLastPage = 0;
+				else if (topIndexForLastPage >= numItems)
+					topIndexForLastPage = numItems - 1;
+
+				this.topItemIdx = topIndexForLastPage;
+				this.selectedItemIdx = topIndexForLastPage;
+				this.Draw(selectedItemIndexes);
+			}
+		}
+		if (typeof(this.OnItemNav) === "function")
+			this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+	}
+	else
+	{
+		// We're already showing the first page of items.
+		// If the currently selected item is not the first
+		// item, then make it so.
+		if (this.selectedItemIdx > 0)
+		{
+			var oldSelectedItemIdx = this.selectedItemIdx;
+			this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			this.selectedItemIdx = 0;
+			this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			if (typeof(this.OnItemNav) === "function")
+				this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+		}
+	}
+	*/
+}
+// Performs the page-down behavior for showing the menu items
+//
+// Parameters:
+//  pSelectedItemIndexes: An object containing indexes of selected items.  This is
+//                        normally a temporary object created/used in GetVal().
+//  pNumItems: The pre-calculated number of menu items.  If this not given, this
+//             will be retrieved by calling NumItems().
+function DDLightbarMenu_DoPageDown(pSelectedItemIndexes, pNumItems)
+{
+	var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {});
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+
+	var numItemsPerPage = this.GetNumItemsPerPage();
+	var startIdxToCheck = this.selectedItemIdx + numItemsPerPage;
+	if (startIdxToCheck >= numItems)
+		startIdxToCheck = numItems - 1;
+	var nextSelectableItemIdx = this.FindSelectableItemForward(startIdxToCheck, this.wrapNavigation);
+	this.NavMenuForNewSelectedItemBottom(nextSelectableItemIdx, numItemsPerPage, numItems, selectedItemIndexes, true);
+
+	// Older, before un-selectable items:
+	/*
+	// Only do the pageDown if we're not showing the last item already
+	var lastItemIdx = numItems - 1;
+	if (lastItemIdx > this.topItemIdx+numItemsPerPage-1)
+	{
+		var oldSelectedItemIdx = this.selectedItemIdx;
+		// Figure out the top index for the last page.
+		var topIndexForLastPage = numItems - numItemsPerPage;
+		if (topIndexForLastPage < 0)
+			topIndexForLastPage = 0;
+		else if (topIndexForLastPage >= numItems)
+			topIndexForLastPage = numItems - 1;
+		if (topIndexForLastPage != this.topItemIdx)
+		{
+			// Update the selected & top item indexes
+			this.selectedItemIdx += numItemsPerPage;
+			this.topItemIdx += numItemsPerPage;
+			if (this.selectedItemIdx >= topIndexForLastPage)
+				this.selectedItemIdx = topIndexForLastPage;
+			if (this.topItemIdx > topIndexForLastPage)
+				this.topItemIdx = topIndexForLastPage;
+			this.Draw(selectedItemIndexes);
+		}
+		else
+		{
+			// The top index is the top index for the last page.
+			// If wrapping is enabled, then go back to the first page.
+			if (this.wrapNavigation)
+			{
+				this.topItemIdx = 0;
+				this.selectedItemIdx = 0;
+			}
+			this.Draw(selectedItemIndexes);
+		}
+		if (typeof(this.OnItemNav) === "function")
+			this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+	}
+	else
+	{
+		// We're already showing the last page of items.
+		// If the currently selected item is not the last
+		// item, then make it so.
+		if (this.selectedItemIdx < lastItemIdx)
+		{
+			var oldSelectedItemIdx = this.selectedItemIdx;
+			this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			this.selectedItemIdx = lastItemIdx;
+			this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			if (typeof(this.OnItemNav) === "function")
+				this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+		}
+	}
+	*/
+}
+
+function DDLightbarMenu_NavMenuForNewSelectedItemTop(pNewSelectedItemIdx, pNumItemsPerPage, pNumItems, pSelectedItemIndexes)
+{
+	var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {});
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	if (pNewSelectedItemIdx > -1 && pNewSelectedItemIdx != this.selectedItemIdx)
+	{
+		var indexDiff = 0;
+		if (pNewSelectedItemIdx < this.selectedItemIdx)
+			indexDiff = this.selectedItemIdx - pNewSelectedItemIdx;
+		else if (pNewSelectedItemIdx > this.selectedItemIdx)
+			indexDiff = pNewSelectedItemIdx - this.selectedItemIdx;
+		var oldSelectedItemIdx = this.selectedItemIdx;
+		this.selectedItemIdx = pNewSelectedItemIdx;
+		var pageNum = findPageNumOfItemNum(this.selectedItemIdx + 1, pNumItemsPerPage, numItems, false);
+		if (pageNum > 0)
+		{
+			var newTopItemIdx = pNumItemsPerPage * (pageNum-1);
+			if (newTopItemIdx != this.topItemIdx)
+			{
+				this.topItemIdx = newTopItemIdx;
+				this.Draw(selectedItemIndexes);
+			}
+			else
+			{
+				// We're already showing the first page of items.
+				// Re-draw the old & new selected items with the proper highlighting
+				this.WriteItemAtItsLocation(oldSelectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+				this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			}
+		}
+		if (typeof(this.OnItemNav) === "function")
+			this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+	}
+}
+
+function DDLightbarMenu_NavMenuForNewSelectedItemBottom(pNewSelectedItemIdx, pNumItemsPerPage, pNumItems, pSelectedItemIndexes, pLastItemAtBottom)
+{
+	var numItemsPerPage = (typeof(pNumItemsPerPage) === "number" ? pNumItemsPerPage : this.GetNumItemsPerPage());
+	var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {});
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	var lastItemAtBottom = (typeof(pLastItemAtBottom) === "boolean" ? pLastItemAtBottom : false);
+	if (pNewSelectedItemIdx > -1 && pNewSelectedItemIdx != this.selectedItemIdx)
+	{
+		if (lastItemAtBottom)
+		{
+			var oldSelectedItemIdx = this.selectedItemIdx;
+			this.selectedItemIdx = pNewSelectedItemIdx;
+			var newTopItemIdx = pNewSelectedItemIdx - numItemsPerPage + 1;
+			if (newTopItemIdx < 0)
+				newTopItemIdx = 0;
+			if (newTopItemIdx != this.topItemIdx)
+			{
+				this.topItemIdx = newTopItemIdx;
+				this.Draw(selectedItemIndexes);
+			}
+			else
+			{
+				// We're already showing the page with the calculated top index
+				// Re-draw the old & new selected items with the proper highlighting
+				this.WriteItemAtItsLocation(oldSelectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+				this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+			}
+		}
+		else
+		{
+			var indexDiff = 0;
+			if (pNewSelectedItemIdx < this.selectedItemIdx)
+				indexDiff = this.selectedItemIdx - pNewSelectedItemIdx;
+			else if (pNewSelectedItemIdx > this.selectedItemIdx)
+				indexDiff = pNewSelectedItemIdx - this.selectedItemIdx;
+			var oldSelectedItemIdx = this.selectedItemIdx;
+			this.selectedItemIdx = pNewSelectedItemIdx;
+			var pageNum = findPageNumOfItemNum(this.selectedItemIdx + 1, numItemsPerPage, numItems, false);
+			if (pageNum > 0)
+			{
+				var newTopItemIdx = numItemsPerPage * (pageNum-1);
+				// Figure out the top index for the last page.
+				var topIndexForLastPage = numItems - numItemsPerPage;
+				if (topIndexForLastPage < 0)
+					topIndexForLastPage = 0;
+				else if (topIndexForLastPage >= numItems)
+					topIndexForLastPage = numItems - 1;
+				if (newTopItemIdx != topIndexForLastPage)
+				{
+					this.topItemIdx = newTopItemIdx;
+					this.Draw(selectedItemIndexes);
+				}
+				else
+				{
+					// We're already showing the last page of items.
+					// Re-draw the old & new selected items with the proper highlighting
+					this.WriteItemAtItsLocation(oldSelectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+					this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+				}
+			}
+		}
+		if (typeof(this.OnItemNav) === "function")
+			this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+	}
+}
+
+// Sets the characters to use for drawing the border.  Takes an object specifying
+// the values to set, but does not overwrite the whole borderChars object in the
+// menu object.
+//
+// Parameters:
+//  pBorderChars: An object with the following properties:
+//                upperLeft: The character to use for the upper-left corner
+//                upperRight: The character to use for the upper-right corner
+//                lowerLeft: The character to use for the lower-left corner
+//                lowerRight: The character to use for the lower-right corner
+//                top: The character to use for the top border
+//                bottom: The character to use for the bottom border
+//                left: The character to use for the left border
+//                right: The character to use for the right border
+function DDLightbarMenu_SetBorderChars(pBorderChars)
+{
+	if (typeof(pBorderChars) !== "object")
+		return;
+
+	var borderPropNames = [ "upperLeft", "upperRight", "lowerLeft", "lowerRight",
+	                        "top", "bottom", "left", "right" ];
+	for (var i = 0; i < borderPropNames.length; ++i)
+	{
+		if (pBorderChars.hasOwnProperty(borderPropNames[i]))
+			this.borderChars[borderPropNames[i]] = pBorderChars[borderPropNames[i]];
+	}
+}
+
+// Sets the colors to use with the menu.  Takes an object specifying the values
+// to set, but does not overwrite the whole colors object in the menu object.
+//
+// Parameters:
+//  pColors: An object with the following properties:
+//           itemColor: The color to use for non-highlighted items
+//           selectedItemColor: The color to use for selected items
+//           itemTextCharHighlightColor: The color to use for a highlighted
+//                                       non-space character in an item text
+//                                       (specified by having a & in the item
+//                                       text).
+//                                       It's important not to specify a "\x01n"
+//                                       in here in case the item text should
+//                                       have a background color.
+//           borderColor: The color to use for the border
+//           scrollbarScrollBlockColor: The color to use for the scrollbar block
+//           scrollbarBGColor: The color to use for the scrollbar background
+function DDLightbarMenu_SetColors(pColors)
+{
+	if (typeof(pColors) != "object")
+		return;
+
+	var colorPropNames = ["itemColor", "selectedItemColor", "altItemColor", "altSelectedItemColor",
+	                      "itemTextCharHighlightColor", "borderColor", "scrollbarScrollBlockColor",
+	                      "scrollbarBGColor", "unselectableItemColor"];
+	for (var i = 0; i < colorPropNames.length; ++i)
+	{
+		if (pColors.hasOwnProperty(colorPropNames[i]))
+			this.colors[colorPropNames[i]] = pColors[colorPropNames[i]];
+	}
+}
+
+// Returns the number of (possible) items per page
+function DDLightbarMenu_GetNumItemsPerPage()
+{
+	var numItemsPerPage = this.size.height;
+	if (this.borderEnabled)
+		numItemsPerPage -= 2;
+	return numItemsPerPage;
+}
+
+// Gets the top item index of the last page of items
+function DDLightbarMenu_GetTopItemIdxOfLastPage()
+{
+	var numItemsPerPage = this.size.height;
+	if (this.borderEnabled)
+		numItemsPerPage -= 2;
+	var topItemIndex = this.NumItems() - numItemsPerPage;
+	if (topItemIndex < 0)
+		topItemIndex = 0;
+	return topItemIndex;
+}
+
+// Calculates & sets the top item index to the top item of the last page of items
+function DDLightbarMenu_CalcAndSetTopItemIdxToTopOfLastPage(pNumItems)
+{
+	var numItemsPerPage = this.size.height;
+	if (this.borderEnabled)
+		numItemsPerPage -= 2;
+
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	this.topItemIdx = numItems - numItemsPerPage;
+	if (this.topItemIdx < 0)
+		this.topItemIdx = 0;
+}
+
+// Calculates the page for an item (by its index) and sets the top index for the menu
+// based on that page.
+//
+// Parameters:
+//  pNumItemsPerPage: Optional - The number of items per page, if already calculated
+//  pNumItems: Optional - The number of items in the menu, if already known
+//
+//
+// Return value: Boolean - Whether or not the top index of the menu changed
+function DDLightbarMenu_CalcPageForItemAndSetTopItemIdx(pNumItemsPerPage, pNumItems)
+{
+	var numItemsPerPage = (typeof(pNumItemsPerPage) === "number" ? pNumItemsPerPage : this.GetNumItemsPerPage());
+	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+
+	var topItemIdxChanged = false;
+	var pageNum = findPageNumOfItemNum(this.selectedItemIdx+1, numItemsPerPage, numItems, false);
+	if (pageNum > 0)
+	{
+		var topItemIdxOnNewPage = numItemsPerPage * (pageNum-1);
+		if (topItemIdxOnNewPage + numItemsPerPage >= numItems)
+			topItemIdxOnNewPage = numItems - numItemsPerPage;
+		if (topItemIdxOnNewPage < 0)
+			topItemIdxOnNewPage = 0;
+		if (topItemIdxOnNewPage != this.topItemIdx)
+		{
+			this.topItemIdx = topItemIdxOnNewPage;
+			topItemIdxChanged = true;
+		}
+	}
+	return topItemIdxChanged;
+}
+
+// Adds additional key characters to cause quitting out of the menu
+// in addition to ESC.  The keys will be case-sensitive.
+//
+// Parameters:
+//  pAdditionalQuitKeys: A string of key characters
+function DDLightbarMenu_AddAdditionalQuitKeys(pAdditionalQuitKeys)
+{
+	if (typeof(pAdditionalQuitKeys) === "string")
+		this.additionalQuitKeys += pAdditionalQuitKeys;
+}
+
+// Returns whether or not the additional quit keys array contains a given
+// key character.
+//
+// Parameters:
+//  pKey: The key to look for in the additional quit keys
+//
+// Return value: Boolean - Whether or not the additional quit keys includes
+//               pKey
+function DDLightbarMenu_QuitKeysIncludes(pKey)
+{
+	return (this.additionalQuitKeys.indexOf(pKey) > -1);
+}
+
+// Clears the string of additional key characters to quit out of the menu
+function DDLightbarMenu_ClearAdditionalQuitKeys()
+{
+	this.additionalQuitKeys = "";
+}
+
+function DDLightbarMenu_AddAdditionalPageUpKeys(pAdditionalKeys)
+{
+	if (typeof(pAdditionalKeys) === "string")
+		this.additionalPageUpKeys += pAdditionalKeys;
+}
+
+function DDLightbarMenu_PageUpKeysIncludes(pKey)
+{
+	return (this.additionalPageUpKeys.indexOf(pKey) > -1);
+}
+
+function DDLightbarMenu_ClearAdditionalPageUpKeys()
+{
+	this.additionalPageUpKeys = "";
+}
+
+function DDLightbarMenu_AddAdditionalPageDownKeys(pAdditionalKeys)
+{
+	if (typeof(pAdditionalKeys) === "string")
+		this.additionalPageDnKeys += pAdditionalKeys;
+}
+
+function DDLightbarMenu_PageDownKeysIncludes(pKey)
+{
+	return (this.additionalPageDnKeys.indexOf(pKey) > -1);
+}
+
+function DDLightbarMenu_ClearAdditionalPageDownKeys()
+{
+	this.additionalPageDnKeys = "";
+}
+
+function DDLightbarMenu_AddAdditionalFirstPageKeys(pAdditionalKeys)
+{
+	if (typeof(pAdditionalKeys) === "string")
+		this.additionalFirstPageKeys += pAdditionalKeys;
+}
+
+function DDLightbarMenu_FirstPageKeysIncludes(pKey)
+{
+	return (this.additionalFirstPageKeys.indexOf(pKey) > -1);
+}
+
+function DDLightbarMenu_ClearAdditionalFirstPageKeys()
+{
+	this.additionalFirstPageKeys = "";
+}
+
+function DDLightbarMenu_AddAdditionalLastPageKeys(pAdditionalKeys)
+{
+	if (typeof(pAdditionalKeys) === "string")
+		this.additionalLastPageKeys += pAdditionalKeys;
+}
+
+function DDLightbarMenu_LastPageKeysIncludes(pKey)
+{
+	return (this.additionalLastPageKeys.indexOf(pKey) > -1);
+}
+
+function DDLightbarMenu_ClearAdditionalLastPageKeys()
+{
+	this.additionalLastPageKeys = "";
+}
+
+// Adds additional key characters to select any item.  The keys will be case-sensitive.
+//
+// Parameters:
+//  pAdditionalAddItemKeys: A string containing key characters
+function DDLightbarMenu_AddAdditionalSelectItemKeys(pAdditionalAddItemKeys)
+{
+	this.additionalSelectItemKeys += pAdditionalAddItemKeys;
+}
+
+// Returns whether or not the additional select-item keys array contains a given
+// key character.
+//
+// Parameters:
+//  pKey: The key to look for in the additional select-itemkeys
+//
+// Return value: Boolean - Whether or not the additional select-item keys includes
+//               pKey
+function DDLightbarMenu_SelectItemKeysIncludes(pKey)
+{
+	return (this.additionalSelectItemKeys.indexOf(pKey) > -1);
+}
+
+// Clears the string of additional key characters to select any item
+function DDLightbarMenu_ClearAdditionalSelectItemKeys()
+{
+	this.additionalSelectItemKeys = "";
+}
+
+// Displays an initial scrollbar
+//
+// Parameters:
+//  pSolidBlockStartRow: The starting row for the solid/bright blocks
+//  pNumSolidBlocks: The number of solid/bright blocks to write.  If this
+//                   is omitted, this.scrollbarInfo.numSolidScrollBlocks
+//                   will be used.
+function DDLightbarMenu_DisplayInitialScrollbar(pSolidBlockStartRow, pNumSolidBlocks)
+{
+	var numSolidBlocks = (typeof(pNumSolidBlocks) == "number" ? pNumSolidBlocks : this.scrollbarInfo.numSolidScrollBlocks);
+
+	var numSolidBlocksWritten = 0;
+	var wroteBrightBlockColor = false;
+	var wroteDimBlockColor = false;
+	var startY = this.pos.y;
+	var screenBottomRow = this.pos.y + this.size.height - 1;
+	var scrollbarCol = this.pos.x + this.size.width - 1;
+	if (this.borderEnabled)
+	{
+		++startY;
+		--screenBottomRow;
+		--scrollbarCol;
+	}
+	this.scrollbarInfo.solidBlockLastStartRow = startY;
+	for (var screenY = startY; screenY <= screenBottomRow; ++screenY)
+	{
+		console.gotoxy(scrollbarCol, screenY);
+		if ((screenY >= pSolidBlockStartRow) && (numSolidBlocksWritten < numSolidBlocks))
+		{
+			if (!wroteBrightBlockColor)
+			{
+				console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
+				wroteBrightBlockColor = true;
+				wroteDimBlockColor = false;
+			}
+			console.print(this.scrollbarInfo.blockChar);
+			++numSolidBlocksWritten;
+		}
+		else
+		{
+			if (!wroteDimBlockColor)
+			{
+				console.print("\x01n" + this.colors.scrollbarBGColor);
+				wroteDimBlockColor = true;
+			}
+			console.print(this.scrollbarInfo.BGChar);
+		}
+	}
+}
+
+// For the DigDistMsgReader class: Updates the scrollbar for a message, for use
+// in enhanced reader mode.  This does only the necessary character updates to
+// minimize the number of characters that need to be updated on the screen.
+//
+// Parameters:
+//  pNewStartRow: The new (current) start row for solid/bright blocks
+//  pOldStartRow: The old start row for solid/bright blocks
+//  pNumSolidBlocks: The number of solid/bright blocks.  If this is omitted,
+//                   this.scrollbarInfo.numSolidScrollBlocks will be used.
+function DDLightbarMenu_UpdateScrollbar(pNewStartRow, pOldStartRow, pNumSolidBlocks)
+{
+	var numSolidBlocks = (typeof(pNumSolidBlocks) == "number" ? pNumSolidBlocks : this.scrollbarInfo.numSolidScrollBlocks);
+
+	var startY = this.pos.y;
+	var screenBottomRow = this.pos.y + this.size.height - 1;
+	var scrollbarCol = this.pos.x + this.size.width - 1;
+	if (this.borderEnabled)
+	{
+		++startY;
+		--screenBottomRow;
+		--scrollbarCol;
+	}
+
+	// Calculate the difference in the start row.  If the difference is positive,
+	// then the solid block section has moved down; if the diff is negative, the
+	// solid block section has moved up.
+	var solidBlockStartRowDiff = pNewStartRow - pOldStartRow;
+	var oldLastRow = pOldStartRow + numSolidBlocks - 1;
+	var newLastRow = pNewStartRow + numSolidBlocks - 1;
+	if (solidBlockStartRowDiff > 0)
+	{
+		// The solid block section has moved down
+		if (pNewStartRow > oldLastRow)
+		{
+			// No overlap
+			// Write dim blocks over the old solid block section
+			console.print("\x01n" + this.colors.scrollbarBGColor);
+			for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY)
+			{
+				console.gotoxy(scrollbarCol, screenY);
+				console.print(this.scrollbarInfo.BGChar);
+			}
+			// Write solid blocks in the new locations
+			console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
+			for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY)
+			{
+				console.gotoxy(scrollbarCol, screenY);
+				console.print(this.scrollbarInfo.blockChar);
+			}
+		}
+		else
+		{
+			// There is some overlap
+			// Write dim blocks on top
+			console.print("\x01n" + this.colors.scrollbarBGColor);
+			for (var screenY = pOldStartRow; screenY < pNewStartRow; ++screenY)
+			{
+				console.gotoxy(scrollbarCol, screenY);
+				console.print(this.scrollbarInfo.BGChar);
+			}
+			// Write bright blocks on the bottom
+			console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
+			for (var screenY = oldLastRow+1; screenY <= newLastRow; ++screenY)
+			{
+				console.gotoxy(scrollbarCol, screenY);
+				console.print(this.scrollbarInfo.blockChar);
+			}
+		}
+	}
+	else if (solidBlockStartRowDiff < 0)
+	{
+		// The solid block section has moved up
+		if (pOldStartRow > newLastRow)
+		{
+			// No overlap
+			// Write dim blocks over the old solid block section
+			console.print("\x01n" + this.colors.scrollbarBGColor);
+			for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY)
+			{
+				console.gotoxy(scrollbarCol, screenY);
+				console.print(this.scrollbarInfo.BGChar);
+			}
+			// Write solid blocks in the new locations
+			console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
+			for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY)
+			{
+				console.gotoxy(scrollbarCol, screenY);
+				console.print(this.scrollbarInfo.blockChar);
+			}
+		}
+		else
+		{
+			// There is some overlap
+			// Write bright blocks on top
+			console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
+			var endRow = pOldStartRow;
+			for (var screenY = pNewStartRow; screenY < endRow; ++screenY)
+			{
+				console.gotoxy(scrollbarCol, screenY);
+				console.print(this.scrollbarInfo.blockChar);
+			}
+			// Write dim blocks on the bottom
+			console.print("\x01n" + this.colors.scrollbarBGColor);
+			endRow = pOldStartRow + numSolidBlocks;
+			for (var screenY = pNewStartRow+numSolidBlocks; screenY < endRow; ++screenY)
+			{
+				console.gotoxy(scrollbarCol, screenY);
+				console.print(this.scrollbarInfo.BGChar);
+			}
+		}
+	}
+}
+
+// Calculates the starting row for the solid blocks on the scrollbar
+//
+// Return value: The starting row for the solid blocks on the scrollbar
+function DDLightbarMenu_CalcScrollbarSolidBlockStartRow()
+{
+	var scrollbarStartY = this.pos.y;
+	var scrollbarHeight = this.size.height;
+	if (this.borderEnabled)
+	{
+		++scrollbarStartY;
+		scrollbarHeight -= 2;
+	}
+	var scrollbarBottomY = scrollbarStartY + scrollbarHeight - 1;
+	var solidBlockStartRow = scrollbarStartY;
+	var numMenuItems = this.NumItems();
+	if (numMenuItems > 0)
+	{
+		var scrollbarFraction = this.selectedItemIdx / numMenuItems;
+		var scrollbarStartRow = scrollbarStartY + Math.floor(scrollbarHeight * scrollbarFraction);
+		solidBlockStartRow = scrollbarStartRow - Math.floor(this.scrollbarInfo.numSolidScrollBlocks / 2);
+		// Don't let the solid blocks go above the starting screen row or below the ending
+		// screen row of the scrollbar
+		if (solidBlockStartRow < scrollbarStartY)
+			solidBlockStartRow = scrollbarStartY;
+		else if (solidBlockStartRow + this.scrollbarInfo.numSolidScrollBlocks > scrollbarBottomY)
+			solidBlockStartRow = scrollbarBottomY - this.scrollbarInfo.numSolidScrollBlocks + 1;
+	}
+	return solidBlockStartRow;
+}
+
+// Updates the scrollbar position based on the currently-selected
+// item index, this.selectedItemIdx.
+//
+// Parameters:
+//  pForceUpdate: Boolean - Whether or not to force the redraw regardless of block location.
+//                Defaults to false.
+function DDLightbarMenu_UpdateScrollbarWithHighlightedItem(pForceUpdate)
+{
+	var forceUpdate = (typeof(pForceUpdate) === "boolean" ? pForceUpdate : false);
+	var solidBlockStartRow = this.CalcScrollbarSolidBlockStartRow();
+	if (forceUpdate || (solidBlockStartRow != this.scrollbarInfo.solidBlockLastStartRow))
+		this.UpdateScrollbar(solidBlockStartRow, this.scrollbarInfo.solidBlockLastStartRow, this.scrollbarInfo.numSolidScrollBlocks);
+	this.scrollbarInfo.solidBlockLastStartRow = solidBlockStartRow;
+}
+
+function DDLightbarMenu_CanShowAllItemsInWindow()
+{
+	var pageHeight = (this.borderEnabled ? this.size.height - 2 : this.size.height);
+	return (this.NumItems() <= pageHeight);
+}
+
+// Makes an item object that is compatible with DDLightbarMenu, with a given
+// item text and return value.
+//
+// Parameters:
+//  pText: The text to show in the menu for the item
+//  pRetval: The return value of the item when the user selects it from the menu
+//
+// Return value: An object with the given text & return value compatible with DDLightbarMenu
+function DDLightbarMenu_MakeItemWithTextAndRetval(pText, pRetval)
+{
+	var item = getDefaultMenuItem();
+	item.text = pText;
+	item.retval = pRetval;
+	return item;
+}
+
+// Makes an item object that is compatible with DDLightbarMenu, with a given
+// return value.
+//
+// Parameters:
+//  pRetval: The return value of the item when the user selects it from the menu
+//
+// Return value: An object with the given return value compatible with DDLightbarMenu
+function DDLightbarMenu_MakeItemWithRetval(pRetval)
+{
+	var item = getDefaultMenuItem();
+	item.retval = pRetval;
+	return item;
+}
+
+// Returns whether an item is set to use the alternate item colors
+//
+// Parameters:
+//  pItemIndex: The index of the item
+//
+// Return value: Boolean - Whether or not an item is configured to use alternate item colors
+function DDLightbarMenu_ItemUsesAltColors(pItemIndex)
+{
+	if ((pItemIndex < 0) || (pItemIndex >= this.NumItems()))
+		return false;
+
+	return this.GetItem(pItemIndex).useAltColors;
+}
+
+// Returns either the normal or alternate color for an item
+//
+// Parameters:
+//  pItemIndex: The index of the item
+//  pSelected: Whether or not to use selected item colors.  Defaults to false.
+//
+// Return value: Either colors.itemColor or colors.altItemColor
+function DDLightbarMenu_GetColorForItem(pItemIndex, pSelected)
+{
+	if ((pItemIndex < 0) || (pItemIndex >= this.NumItems()))
+		return "";
+
+	var selected = (typeof(pSelected) == "boolean" ? pSelected : false);
+	if (selected)
+		return (this.GetItem(pItemIndex).useAltColors ? this.colors.altSelectedItemColor : this.colors.selectedItemColor);
+	else
+		return (this.GetItem(pItemIndex).useAltColors ? this.colors.altItemColor : this.colors.itemColor);
+}
+
+// Returns either the selected or alternate selected color for an item
+//
+// Parameters:
+//  pItemIndex: The index of the item
+//
+// Return value: Either colors.selectedItemColor or colors.altSelectedItemColor
+function DDLightbarMenu_GetSelectedColorForItem(pItemIndex)
+{
+	if (typeof(pItemIndex) !== "number")
+		return;
+	if ((pItemIndex < 0) || (pItemIndex >= this.NumItems()))
+		return "";
+
+	return (this.GetItem(pItemIndex).useAltColors ? this.colors.altSelectedItemColor : this.colors.selectedItemColor);
+}
+
+// Sets the selected item index for the menu, and sets anything else as appropriate
+// (such as the index of the topmost menu item).
+//
+// Parameters:
+//  pSelectedItemIdx: The index of the selected item
+function DDLightbarMenu_SetSelectedItemIdx(pSelectedItemIdx)
+{
+	if (typeof(pSelectedItemIdx) !== "number")
+		return;
+	if ((pSelectedItemIdx < 0) || (pSelectedItemIdx >= this.NumItems()))
+		return;
+
+	this.selectedItemIdx = pSelectedItemIdx;
+	if (this.selectedItemIdx == 0)
+		this.topItemIdx = 0;
+	else if (this.selectedItemIdx >= this.topItemIdx+this.GetNumItemsPerPage())
+		this.topItemIdx = this.selectedItemIdx - this.GetNumItemsPerPage() + 1;
+	else if (this.selectedItemIdx < this.topItemIdx)
+		this.topItemIdx = this.selectedItemIdx;
+}
+
+// Gets the index of the bottommost item on the menu
+function DDLightbarMenu_GetBottomItemIdx()
+{
+	var bottomItemIdx = this.topItemIdx + this.size.height - 1;
+	if (this.borderEnabled)
+		bottomItemIdx -= 2;
+	return bottomItemIdx;
+}
+
+// Returns the absolute screen position (x, y) of the topmost displayed item on the menu
+//
+// Return value: An object with the following properties:
+//               x: The horizontal screen location of the top item (1-based)
+//               y: The vertical screen location of the top item (1-based)
+function DDLightbarMenu_GetTopDisplayedItemPos()
+{
+	var itemPos = {
+		x: this.pos.x,
+		y: this.pos.y
+	};
+	if (this.borderEnabled)
+	{
+		++itemPos.x;
+		++itemPos.y;
+	}
+	return itemPos;
+}
+
+// Returns the absolute screen position (x, y) of the bottommost displayed item on the menu
+//
+// Return value: An object with the following properties:
+//               x: The horizontal screen location of the top item (1-based)
+//               y: The vertical screen location of the top item (1-based)
+function DDLightbarMenu_GetBottomDisplayedItemPos()
+{
+	var itemPos = {
+		x: this.pos.x,
+		y: this.pos.y + this.size.height - 1
+	};
+	if (this.borderEnabled)
+	{
+		++itemPos.x;
+		--itemPos.y;
+	}
+	return itemPos;
+}
+
+// Returns the absolute screen row number for an item index, if it is visible
+// on the menu.  If the item is not visible on the menu, this will return -1.
+//
+// Parameters:
+//  pItemIdx: The index of the menu item to check
+//
+// Return value: The absolute row number on the screen where the item is, if it is
+//               visible on the menu, or -1 if the item is not visible on the menu.
+function DDLightbarMenu_ScreenRowForItem(pItemIdx)
+{
+	if (typeof(pItemIdx) !== "number")
+		return -1;
+	if (pItemIdx < 0 || pItemIdx >= this.NumItems())
+		return -1;
+
+	var screenRow = -1;
+	if (pItemIdx >= this.topItemIdx && pItemIdx <= this.GetBottomItemIdx())
+	{
+		if (this.borderEnabled)
+			screenRow = this.pos.y + pItemIdx - this.topItemIdx + 1;
+		else
+			screenRow = this.pos.y + pItemIdx - this.topItemIdx;
+	}
+	return screenRow;
+}
+
+// Returns whether ANSI is supported by the user's terminal. Also checks this.allowANSI
+function DDLightbarMenu_ANSISupported()
+{
+	return (console.term_supports(USER_ANSI) && this.allowANSI);
+}
+
+// Calculates the number of solid scrollbar blocks & non-solid scrollbar blocks
+// to use.  Saves the information in this.scrollbarInfo.numSolidScrollBlocks and
+// this.scrollbarInfo.numNonSolidScrollBlocks.
+function DDLightbarMenu_CalcScrollbarBlocks()
+{
+	var menuDisplayHeight = this.size.height;
+	if (this.borderEnabled)
+		menuDisplayHeight -= 2;
+	var numMenuItems = this.NumItems();
+	if (numMenuItems > 0)
+	{
+		var menuListFractionShown = menuDisplayHeight / numMenuItems;
+		if (menuListFractionShown > 1)
+			menuListFractionShown = 1.0;
+		this.scrollbarInfo.numSolidScrollBlocks = Math.floor(menuDisplayHeight * menuListFractionShown);
+		if (this.scrollbarInfo.numSolidScrollBlocks <= 0)
+			this.scrollbarInfo.numSolidScrollBlocks = 1;
+		else if (this.scrollbarInfo.numSolidScrollBlocks > menuDisplayHeight)
+			this.scrollbarInfo.numSolidScrollBlocks = menuDisplayHeight;
+		this.scrollbarInfo.numNonSolidScrollBlocks = menuDisplayHeight - this.scrollbarInfo.numSolidScrollBlocks;
+	}
+	else
+	{
+		this.scrollbarInfo.numSolidScrollBlocks = menuDisplayHeight;
+		this.scrollbarInfo.numNonSolidScrollBlocks = 0;
+	}
+}
+
+
+
+
+//////////////////////////////////////////////////////////
+// Helper functions, not part of the DDLightbarMenu class
+
+// Returns the length of an item's text, not counting non-displayable
+// characters (such as Synchronet color attributes and an ampersand
+// immediately before a non-space)
+//
+// Parameters:
+//  pText: The text to test
+//  pAmpersandHotkeysInItems: Boolean - Whether or not ampersand hotkeys are enabled for the item text
+function itemTextDisplayableLen(pText, pAmpersandHotkeysInItems)
+{
+	var textLen = console.strlen(pText);
+	// If pAmpersandHotkeysInItems is true, look for ampersands immediately
+	// before a non-space and if found, don't count those.
+	if (pAmpersandHotkeysInItems)
+	{
+		var startIdx = 0;
+		var ampersandIndex = pText.indexOf("&", startIdx);
+		while (ampersandIndex > -1)
+		{
+			// See if the next character is a space character.  If not, then
+			// don't count it in the length.
+			if (pText.length > ampersandIndex+1)
+			{
+				var nextChar = pText.substr(ampersandIndex+1, 1);
+				if (nextChar != " ")
+					--textLen;
+			}
+			startIdx = ampersandIndex+1;
+			ampersandIndex = pText.indexOf("&", startIdx);
+		}
+	}
+	return textLen;
+}
+
+// Shortens a string, accounting for control/attribute codes.  Returns a new
+// (shortened) copy of the string.
+//
+// Parameters:
+//  pStr: The string to shorten
+//  pNewLength: The new (shorter) length of the string
+//  pFromLeft: Optional boolean - Whether to start from the left (default) or
+//             from the right.  Defaults to true.
+//
+// Return value: The shortened version of the string
+function shortenStrWithAttrCodes(pStr, pNewLength, pFromLeft)
+{
+	if (typeof(pStr) != "string")
+		return "";
+	if (typeof(pNewLength) != "number")
+		return pStr;
+	if (pNewLength >= console.strlen(pStr))
+		return pStr;
+
+	var fromLeft = (typeof(pFromLeft) == "boolean" ? pFromLeft : true);
+	var strCopy = "";
+	var tmpStr = "";
+	var strIdx = 0;
+	var lengthGood = true;
+	if (fromLeft)
+	{
+		while (lengthGood && (strIdx < pStr.length))
+		{
+			tmpStr = strCopy + pStr.charAt(strIdx++);
+			if (console.strlen(tmpStr) <= pNewLength)
+				strCopy = tmpStr;
+			else
+				lengthGood = false;
+		}
+	}
+	else
+	{
+		strIdx = pStr.length - 1;
+		while (lengthGood && (strIdx >= 0))
+		{
+			tmpStr = pStr.charAt(strIdx--) + strCopy;
+			if (console.strlen(tmpStr) <= pNewLength)
+				strCopy = tmpStr;
+			else
+				lengthGood = false;
+		}
+	}
+	return strCopy;
+}
+
+// Returns whether or not all string attribute objects in an array have the
+// expected properties, and that the property types are correct, for menu item
+// string color definitions.
+//
+// Parameters:
+//  pAttrsArray: An array of objects which are expected to containg the
+//               following properties: start, end, attrs
+//
+// Return value: Boolean - Whether or not all elements in the array
+//               have all the expected properties
+function attrsArrayElementsHaveAllCorrectProps(pAttrsArray)
+{
+	var allElementsHaveCorrectProps = true;
+	for (var i = 0; (i < pAttrsArray.length) && allElementsHaveCorrectProps; ++i)
+	{
+		allElementsHaveCorrectProps = ((typeof(pAttrsArray[i]) == "object") &&
+		                               pAttrsArray[i].hasOwnProperty("start") &&
+		                               pAttrsArray[i].hasOwnProperty("end") &&
+		                               pAttrsArray[i].hasOwnProperty("attrs") &&
+		                               (typeof(pAttrsArray[i].start) == "number") &&
+		                               (typeof(pAttrsArray[i].end) == "number") &&
+		                               (typeof(pAttrsArray[i].attrs) == "string"));
+	}
+	return allElementsHaveCorrectProps;
+}
+
+// Adds color/attribute codes to a string.
+//
+// Parameters:
+//  pStr: The string to add attribute codes to
+//  pAttrs: This can be either a string containing attribute codes or an array
+//          of objects with start, end, and color properties, for applying attribute
+//          codes to different parts of the string.  These are the properties of
+//          each object in the string (note: for the last one, end can be 0 or -1
+//          to apply the attributes to the rest of the string):
+//           start: The start index in the string to apply the attributes to
+//           end: One past the last index in the part of the string to apply the attributes to
+//           attrs: The attributes to apply to that part of the string
+//
+// Return value: A copy of the string with attributes applied
+function addAttrsToString(pStr, pAttrs)
+{
+	if (typeof(pStr) != "string")
+		return "";
+	else if (pStr.length == 0)
+		return "";
+
+	var str;
+	if (Array.isArray(pAttrs))
+	{
+		// To use the attributes array, the array must have some objects and
+		// each element of the array must have start, end, and attrs properties
+		if ((pAttrs.length > 0) && attrsArrayElementsHaveAllCorrectProps(pAttrs))
+		{
+			// Colorize the string with the object in pAttrs.
+			// Don't do the last object in this loop, because for the last object,
+			// we'll want to check if its end index is valid.
+			str = "";
+			var lastEnd = -1;
+			for (var i = 0; i < pAttrs.length; ++i)
+			{
+				// If the current object's start is more than 1 character after
+				// the last's end, then append the gap in the string with the
+				// normal attribute
+				if ((i > 0) && (pAttrs[i].start > pAttrs[i-1].end))
+					str += "\x01n" + pStr.substring(pAttrs[i-1].end, pAttrs[i].start);
+				// If the properties for the current attrib object are all valid, append
+				// the current part of the string with the given attributes
+				if ((pAttrs[i].start >= lastEnd) && (pAttrs[i].start >= 0) && (pAttrs[i].start < pStr.length) && (pAttrs[i].end > pAttrs[i].start) && (pAttrs[i].end <= pStr.length))
+					str += "\x01n" + pAttrs[i].attrs + pStr.substring(pAttrs[i].start, pAttrs[i].end);
+				// For the last attribute object, allow the end index to be <= 0 or
+				// more than the length of the string to apply the attributes to the
+				// rest of the string.
+				//else if ((i == pAttrs.length-1) && (pAttrs[i].start >= lastEnd) && (pAttrs[i].start >= 0) && (pAttrs[i].start < pStr.length) && (pAttrs[i].end <= 0))
+				else if ((i == pAttrs.length-1) && (pAttrs[i].start >= lastEnd) && (pAttrs[i].start >= 0) && (pAttrs[i].start < pStr.length) && ((pAttrs[i].end <= 0) || (pAttrs[i].end > pStr.length)))
+					str += "\x01n" + pAttrs[i].attrs + pStr.substring(pAttrs[i].start);
+				lastEnd = pAttrs[i].end;
+			}
+
+			// If str is shorter than the passed-in string, then append the rest of the string
+			// with the normal attribute.
+			var theStrLen = console.strlen(str);
+			if (theStrLen < pStr.length)
+				str += "\x01n" + pStr.substring(theStrLen);
+		}
+		else
+			str = pStr;
+	}
+	else if (typeof(pAttrs) == "string")
+		str = "\x01n" + pAttrs + pStr;
+	else
+		str = pStr;
+	return str;
+}
+
+function getBackgroundAttrAtIdx(pAttrs, pIdx)
+{
+	if (typeof(pIdx) != "number")
+		return "";
+	if (pIdx < 0)
+		return "";
+
+	// Synchronet background color codes:
+	// Black: 0
+	// Red: 1
+	// Green: 2
+	// Yellow/brown: 3
+	// Blue: 4
+	// Magenta: 5
+	// Cyan: 6
+	// White/grey: 7
+	var syncBkgAttrRegex = /\x01[01234567]/;
+	var bkgAttr = "";
+	if (Array.isArray(pAttrs))
+	{
+		if ((pAttrs.length > 0) && attrsArrayElementsHaveAllCorrectProps(pAttrs))
+		{
+			// Go through the array, and if a start & end is found where pIdx
+			// falls between, check that objects attrs property for its last
+			// background attribute, if there is one
+			for (var i = 0; i < pAttrs.length; ++i)
+			{
+				if ((pIdx >= pAttrs[i].start) && ((pIdx < pAttrs[i].end) || (pAttrs[i].end == 0)))
+				{
+					// Check the attrs for the last background attribute, starting
+					// from the end
+					if (pAttrs[i].attrs.length >= 2)
+					{
+						for (var attrIdx = pAttrs[i].attrs.length - 2; attrIdx >= 0; attrIdx -= 2)
+						{
+							var currentTwo = pAttrs[i].attrs.substr(attrIdx, 2);
+							if (syncBkgAttrRegex.test(currentTwo))
+							{
+								bkgAttr = currentTwo;
+								break;
+							}
+						}
+					}
+					break;
+				}
+			}
+		}
+	}
+	else if (typeof(pAttrs) == "string")
+	{
+		if ((pIdx >= 0) || (pIdx < pAttrs.length))
+		{
+			// Starting from pIdx, go backwards through pAttrs, and if a Synchronet
+			// background attribute code is found, then use it.
+			for (var i = pIdx - 2; i >= 0; i -= 2)
+			{
+				var currentTwo = pAttrs.substr(i, 2);
+				if (syncBkgAttrRegex.test(currentTwo))
+				{
+					bkgAttr = currentTwo;
+					break;
+				}
+			}
+		}
+	}
+	return bkgAttr;
+}
+
+// Returns a default item object for a DDLightbarMenu
+function getDefaultMenuItem() {
+	return {
+		text: "",
+		textIsUTF8: false,
+		retval: null,
+		hotkeys: "",
+		useAltColors: false,
+		itemColor: null,
+		itemSelectedColor: null,
+		isSelectable: true
+	};
+}
+
+// Returns a substring of a string, accounting for Synchronet attribute
+// codes (not including the attribute codes in the start index or length)
+//
+// Parameters:
+//  pStr: The string to perform the substring on
+//  pLen: Optional: The length of the substring. If not specified, the rest of the string will be used.
+//
+// Return value: A substring of the string according to the parameters
+function substrWithAttrCodes(pStr, pStartIdx, pLen)
+{
+	if (typeof(pStr) !== "string")
+		return "";
+	if (typeof(pStartIdx) !== "number")
+		return "";
+	var len = typeof(pLen) === "number" ? pLen : console.strlen(pStr)-pStartIdx;
+	if (len <= 0)
+		return "";
+	if ((pStartIdx <= 0) && (pLen >= console.strlen(pStr)))
+		return pStr;
+	var startIdx = 0;
+	var screenLen = console.strlen(pStr);
+	if (typeof(pStartIdx) === "number" && pStartIdx >= 0 && pStartIdx < screenLen)
+		startIdx = pStartIdx;
+
+	// Find the actual start & end indexes, considering (not counting) attribute codes,
+	// and return the substring including any applicable attributes from the string
+	var actualStartIdx = findIdxConsideringAttrs(pStr, startIdx);
+	var actualEndIdx = findIdxConsideringAttrs(pStr, startIdx+len+1);
+	// With the actual start & end indexes, make sure we'll get the string
+	// length desired; if not, adjust actualEndIdx;
+	var lenWithActualIndexes = actualEndIdx - actualStartIdx;
+	if (actualEndIdx-actualStartIdx < len)
+		actualEndIdx += len - lenWithActualIndexes;
+	return getAttrsBeforeStrIdx(pStr, actualStartIdx) + pStr.substring(actualStartIdx, actualEndIdx);
+}
+// Helper for substrWithAttrCodes(): Maps a 'visual' character index in a string to its
+// actual index within the string, considering any attribute codes in the string.
+//
+// Parameters:
+//  pStr: A string
+//  pIndex: The index of a character as displayed on the screen, to be mapped to actual string index
+//
+// Return value: The character index mapped to the actual index within the string, considering attribute codes
+function findIdxConsideringAttrs(pStr, pIndex)
+{
+	if (typeof(pStr) !== "string" || pStr.length == 0)
+		return 0;
+	if (typeof(pIndex) !== "number" || pIndex < 0)
+		return 0;
+
+	var printableLen = console.strlen(pStr);
+	var index = (pIndex >= printableLen ? printableLen - 1 : pIndex);
+
+	var actualIdx = 0;
+	var numTimesUpdated = 0;
+	for (var i = 0; i < pStr.length && numTimesUpdated <= index; ++i)
+	{
+		// If this character is the attribute control character, skip it along with
+		// the next character
+		if (pStr.charAt(i) == "\x01")
+		{
+			++i;
+			continue;
+		}
+		actualIdx = i;
+		++numTimesUpdated;
+	}
+	// Alternate implementation:
+	/*
+	var previousChar = "";
+	var syncAttrLen = 0;
+	var numTimesUpdated = 0;
+	for (var i = 0; i < pStr.length && numTimesUpdated <= index; ++i)
+	{
+		// If this character is the attribute control character, skip it along with
+		// the next character
+		var currentChar = pStr.charAt(i);
+		if (currentChar == "\x01")
+			syncAttrLen = 1;
+		else if (previousChar == "\x01")
+			++syncAttrLen;
+		else
+			syncAttrLen = 0;
+		if (syncAttrLen == 0)
+		{
+			actualIdx = i;
+			++numTimesUpdated;
+		}
+		previousChar = currentChar;
+	}
+	*/
+	return actualIdx;
+}
+// Helper for substrWithAttrCodes(): Returns a string with any Synchronet color/attribute
+// codes found in a string before a given index.
+//
+// Parameters:
+//  pStr: The string to search in
+//  pIdx: The index in the string to search before
+//
+// Return value: A string containing any Synchronet attribute codes found before
+//               the given index in the given string
+function getAttrsBeforeStrIdx(pStr, pIdx)
+{
+	if (typeof(pStr) !== "string")
+		return "";
+	if (typeof(pIdx) !== "number")
+		return "";
+	if (pIdx < 0)
+		return "";
+
+	var idx = (pIdx < pStr.length ? pIdx : pStr.length-1);
+	var attrStartIdx = strIdxOfSyncAttrBefore(pStr, idx, true, false);
+	var attrEndIdx = strIdxOfSyncAttrBefore(pStr, idx, false, false); // Start of 2-character code
+	var attrsStr = "";
+	if ((attrStartIdx > -1) && (attrEndIdx > -1))
+		attrsStr = pStr.substring(attrStartIdx, attrEndIdx+2);
+	return attrsStr;
+}
+
+// Returns the index of the first Synchronet attribute code before a given index
+// in a string.
+//
+// Parameters:
+//  pStr: The string to search in
+//  pIdx: The index to search back from
+//  pSeriesOfAttrs: Optional boolean - Whether or not to look for a series of
+//                  attributes.  Defaults to false (look for just one attribute).
+//  pOnlyInWord: Optional boolean - Whether or not to look only in the current word
+//               (with words separated by whitespace).  Defaults to false.
+//
+// Return value: The index of the first Synchronet attribute code before the given
+//               index in the string, or -1 if there is none or if the parameters
+//               are invalid
+function strIdxOfSyncAttrBefore(pStr, pIdx, pSeriesOfAttrs, pOnlyInWord)
+{
+	if (typeof(pStr) != "string")
+		return -1;
+	if (typeof(pIdx) != "number")
+		return -1;
+	if ((pIdx < 0) || (pIdx >= pStr.length))
+		return -1;
+
+	var seriesOfAttrs = (typeof(pSeriesOfAttrs) == "boolean" ? pSeriesOfAttrs : false);
+	var onlyInWord = (typeof(pOnlyInWord) == "boolean" ? pOnlyInWord : false);
+
+	var attrCodeIdx = pStr.lastIndexOf("\x01", pIdx-1);
+	if (attrCodeIdx > -1)
+	{
+		// If we are to only check the current word, then continue only if
+		// there isn't a space between the attribute code and the given index.
+		if (onlyInWord)
+		{
+			if (pStr.lastIndexOf(" ", pIdx-1) >= attrCodeIdx)
+				attrCodeIdx = -1;
+		}
+	}
+	if (attrCodeIdx > -1)
+	{
+		var syncAttrRegexWholeWord = /^\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]$/i;
+		if (syncAttrRegexWholeWord.test(pStr.substr(attrCodeIdx, 2)))
+		{
+			if (seriesOfAttrs)
+			{
+				for (var i = attrCodeIdx - 2; i >= 0; i -= 2)
+				{
+					if (syncAttrRegexWholeWord.test(pStr.substr(i, 2)))
+						attrCodeIdx = i;
+					else
+						break;
+				}
+			}
+		}
+		else
+			attrCodeIdx = -1;
+	}
+	return attrCodeIdx;
+}
+
+// Converts a 'printed' index in a string to its real index in the string
+//
+// Parameters:
+//  pStr: The string to search in
+//  pIdx: The printed index in the string
+//
+// Return value: The actual index in the string object, or -1 on error
+function printedToRealIdxInStr(pStr, pIdx)
+{
+	if (typeof(pStr) != "string")
+		return -1;
+	if ((pIdx < 0) || (pIdx >= pStr.length))
+		return -1;
+
+	// Store the character at the given index if the string didn't have attribute codes.
+	// Also, to help ensure this returns the correct index, get a substring with several
+	// characters starting at the given index to match a word within the string
+	var strWithoutAttrCodes = strip_ctrl(pStr);
+	var substr_len = 5;
+	var substrWithoutAttrCodes = strWithoutAttrCodes.substr(pIdx, substr_len);
+	var printableCharAtIdx = strWithoutAttrCodes.charAt(pIdx);
+	// Iterate through pStr until we find that character and return that index.
+	var realIdx = 0;
+	for (var i = 0; i < pStr.length; ++i)
+	{
+		// tempStr is the string to compare with substrWithoutAttrCodes
+		var tempStr = strip_ctrl(pStr.substr(i)).substr(0, substr_len);
+		if ((pStr.charAt(i) == printableCharAtIdx) && (tempStr == substrWithoutAttrCodes))
+		{
+			realIdx = i;
+			break;
+		}
+	}
+	return realIdx;
+}
+
+// Inputs a keypress from the user and handles some ESC-based
+// characters such as PageUp, PageDown, and ESC.  If PageUp
+// or PageDown are pressed, this function will return the
+// string defined by KEY_PAGE_UP or EY_PAGE_DOWN,
+// respectively.  Also, F1-F5 will be returned as "\x01F1"
+// through "\x01F5", respectively.
+// Thanks goes to Psi-Jack for the original impementation
+// of this function.
+//
+// Parameters:
+//  pGetKeyMode: Optional - The mode bits for console.getkey().
+//               If not specified, K_NONE will be used.
+//  pInputTimeoutMS: The input timeout in milliseconds (defaults to 300000).
+//                   If the user is a sysop, this will use a timeout of 0 for no timeout.
+//
+// Return value: The user's keypress
+function getKeyWithESCChars(pGetKeyMode, pInputTimeoutMS)
+{
+	var getKeyMode = (typeof(pGetKeyMode) === "number" ? pGetKeyMode : K_NONE);
+	var inputTimeoutMS = (typeof(pInputTimeoutMS) === "number" ? pInputTimeoutMS : 300000);
+	if (inputTimeoutMS == 0)
+		inputTimeoutMS = 300000;
+	// Input a key from the user and take action based on the user's input.  If
+	// the user is a sysop, don't use an input timeout.
+	var userInput = "";
+	if (user.compare_ars("SYSOP"))
+		userInput = console.getkey(getKeyMode);
+	else
+		userInput = console.inkey(getKeyMode, inputTimeoutMS);
+	if (userInput == KEY_ESC)
+	{
+		switch (console.inkey(K_NOECHO|K_NOSPIN, 2))
+		{
+			case '[':
+				switch (console.inkey(K_NOECHO|K_NOSPIN, 2))
+				{
+					case 'V':
+						userInput = KEY_PAGE_UP;
+						break;
+					case 'U':
+						userInput = KEY_PAGE_DOWN;
+						break;
+				}
+				break;
+			case 'O':
+				switch (console.inkey(K_NOECHO|K_NOSPIN, 2))
+				{
+					case 'P':
+						userInput = KEY_F1;
+						break;
+					case 'Q':
+						userInput = KEY_F2;
+						break;
+					case 'R':
+						userInput = KEY_F3;
+						break;
+					case 'S':
+						userInput = KEY_F4;
+						break;
+					case 't':
+						userInput = KEY_F5;
+						break;
+				}
+			default:
+				break;
+		}
+	}
+
+	return userInput;
+}
+
+// Calculates & returns a page number.
+//
+// Parameters:
+//  pTopIndex: The index (0-based) of the topmost item on the page
+//  pNumPerPage: The number of items per page
+//
+// Return value: The page number
+function calcPageNum(pTopIndex, pNumPerPage)
+{
+  return ((pTopIndex / pNumPerPage) + 1);
+}
+
+// Finds the (1-based) page number of an item by number (1-based).  If no page
+// is found, then the return value will be 0.
+//
+// Parameters:
+//  pItemNum: The item number (1-based)
+//  pNumPerPage: The number of items per page
+//  pTotoalNum: The total number of items in the list
+//  pReverseOrder: Boolean - Whether or not the list is in reverse order.  If not specified,
+//                 this will default to false.
+//
+// Return value: The page number (1-based) of the item number.  If no page is found,
+//               the return value will be 0.
+function findPageNumOfItemNum(pItemNum, pNumPerPage, pTotalNum, pReverseOrder)
+{
+	if ((typeof(pItemNum) !== "number") || (typeof(pNumPerPage) !== "number") || (typeof(pTotalNum) !== "number"))
+		return 0;
+	if ((pItemNum < 1) || (pItemNum > pTotalNum))
+		return 0;
+
+	var reverseOrder = (typeof(pReverseOrder) == "boolean" ? pReverseOrder : false);
+	var itemPageNum = 0;
+	if (reverseOrder)
+	{
+		var pageNum = 1;
+		for (var topNum = pTotalNum; ((topNum > 0) && (itemPageNum == 0)); topNum -= pNumPerPage)
+		{
+			if ((pItemNum <= topNum) && (pItemNum >= topNum-pNumPerPage+1))
+				itemPageNum = pageNum;
+			++pageNum;
+		}
+	}
+	else // Forward order
+		itemPageNum = Math.ceil(pItemNum / pNumPerPage);
+
+	return itemPageNum;
+}
+
+
+
+
+function logStackTrace(levels) {
+    var callstack = [];
+    var isCallstackPopulated = false;
+    try {
+        i.dont.exist += 0; //doesn't exist- that's the point
+    } catch (e) {
+        if (e.stack) { //Firefox / chrome
+            var lines = e.stack.split('\n');
+            for (var i = 0, len = lines.length; i < len; i++) {
+                    callstack.push(lines[i]);
+            }
+            //Remove call to logStackTrace()
+            callstack.shift();
+            isCallstackPopulated = true;
+        }
+        else if (window.opera && e.message) { //Opera
+            var lines = e.message.split('\n');
+            for (var i = 0, len = lines.length; i < len; i++) {
+                if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
+                    var entry = lines[i];
+                    //Append next line also since it has the file info
+                    if (lines[i + 1]) {
+                        entry += " at " + lines[i + 1];
+                        i++;
+                    }
+                    callstack.push(entry);
+                }
+            }
+            //Remove call to logStackTrace()
+            callstack.shift();
+            isCallstackPopulated = true;
+        }
+    }
+    if (!isCallstackPopulated) { //IE and Safari
+        var currentFunction = arguments.callee.caller;
+        while (currentFunction) {
+            var fn = currentFunction.toString();
+            var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf("(")) || "anonymous";
+            callstack.push(fname);
+            currentFunction = currentFunction.caller;
+        }
+    }
+    if (levels) {
+        console.print(callstack.slice(0, levels).join("\r\n"));
+    }
+    else {
+        console.print(callstack.join("\r\n"));
+    }
+}
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index 22cf366c2b..fa798ecf91 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
-                                 Version 1.95a
-                           Release date: 2024-01-24
+                                 Version 1.95b
+                           Release date: 2024-02-04
 
                                      by
 
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index 398a8ee82e..f79a140603 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,8 +5,11 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
-1.95a    2024-01-23   Bug fix: Aborting when the sub-board isn't available when
-                      editing a personal email message
+1.95b    2024-02-04   Bug fix: Use the P_UTF8 mode bit when printing UTF-8
+                      message header info (such as 'from' and 'to').
+                      A dd_lightbar_menu.js update goes along with this.
+1.95a    2024-01-23   Bug fix: Was aborting when sub-board code isn't available
+                      when editing personal email
 1.95     2024-01-20   Removed user option to display indexed mode menu in
                       newscan after all new messages are read.
                       Command-line option -indexedMode can now be specified with
-- 
GitLab