diff --git a/exec/load/dd_lightbar_menu.js b/exec/load/dd_lightbar_menu.js
index dd07949159d5c9c80205b567a81874a867cda9fe..09e28af006de7ae219c186b50047514745d0f7d3 100644
--- a/exec/load/dd_lightbar_menu.js
+++ b/exec/load/dd_lightbar_menu.js
@@ -1,5 +1,3 @@
-// $Id: dd_lightbar_menu.js,v 1.24 2020/05/08 04:49:10 nightfox Exp $
-
 /* Digital Distortion Lightbar Menu library
  * Author: Eric Oulashin (AKA Nightfox)
  * BBS: Digital Distortion
@@ -47,6 +45,7 @@ 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.
@@ -325,6 +324,20 @@ The parameters:
  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";
@@ -417,6 +430,7 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 		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",
@@ -485,12 +499,26 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 	// 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;
@@ -510,12 +538,18 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 	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.SetTopItemIdxToTopOfLastPage = DDLightbarMenu_SetTopItemIdxToTopOfLastPage;
+	this.CalcAndSetTopItemIdxToTopOfLastPage = DDLightbarMenu_CalcAndSetTopItemIdxToTopOfLastPage;
+	this.CalcPageForItemAndSetTopItemIdx = DDLightbarMenu_CalcPageForItemAndSetTopItemIdx;
 	this.AddAdditionalQuitKeys = DDLightbarMenu_AddAdditionalQuitKeys;
 	this.QuitKeysIncludes = DDLightbarMenu_QuitKeysIncludes;
 	this.ClearAdditionalQuitKeys = DDLightbarMenu_ClearAdditionalQuitKeys;
@@ -538,6 +572,7 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 	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
@@ -572,11 +607,13 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 //  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
-function DDLightbarMenu_Add(pText, pRetval, pHotkey)
+//  pSelectable: Optional - Whether or not the item is to be selectable. Defaults to true.
+function DDLightbarMenu_Add(pText, pRetval, pHotkey, pSelectable)
 {
 	var item = getDefaultMenuItem();
 	item.text = pText;
 	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
@@ -647,11 +684,180 @@ function DDLightbarMenu_NumItems()
 // Return value: The item (or null if pItemIndex is invalid)
 function DDLightbarMenu_GetItem(pItemIndex)
 {
-	if ((pItemIndex < 0) || (pItemIndex >= this.items.length))
+	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:
@@ -730,80 +936,106 @@ function DDLightbarMenu_SetHeight(pHeight)
 //  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.
-function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders, pDrawScrollbar)
+//  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 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)
+	var numMenuItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
+	if (this.ANSISupported())
 	{
-		this.itemNumLen = this.NumItems().toString().length;
-		itemLen -= this.itemNumLen;
-		--itemLen; // Have a space for separation between the numbers and items
-	}
+		var drawBorders = (typeof(pDrawBorders) == "boolean" ? pDrawBorders : true);
+		var drawScrollbar = (typeof(pDrawScrollbar) == "boolean" ? pDrawScrollbar : true);
 
-	// 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 < this.NumItems()) && (numItemsWritten < numPossibleItems); ++idx)
-	{
-		writeTheItem = ((this.nextDrawOnlyItems.length == 0) || (this.nextDrawOnlyItems.indexOf(idx) > -1));
-		if (writeTheItem)
+		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)
 		{
-			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);
+			itemLen -= 2;
+			++curPos.x;
+			++curPos.y;
+			if (drawBorders)
+				this.DrawBorder();
 		}
-		++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)
+		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(numItemsWritten) > -1));
+			writeTheItem = ((this.nextDrawOnlyItems.length == 0) || (this.nextDrawOnlyItems.indexOf(idx) > -1));
 			if (writeTheItem)
 			{
-				console.gotoxy(curPos.x, curPos.y++);
-				console.print("\x01n");
-				if (this.numberedMode)
-					printf(numberFormatStr, "");
-				var itemText = addAttrsToString(format(itemFormatStr, ""), this.colors.itemColor);
-				console.print(itemText);
+				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.print("\x01n");
+					if (this.numberedMode)
+						printf(numberFormatStr, "");
+					var itemText = addAttrsToString(format(itemFormatStr, ""), this.colors.itemColor);
+					console.print(itemText);
+				}
 			}
 		}
 	}
+	else
+	{
+		// 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.print("\x01n");
+		for (var i = 0; i < numMenuItems; ++i)
+		{
+			var showMultiSelectMark = (this.multiSelect && (typeof(pSelectedItemIndexes) == "object") && pSelectedItemIndexes.hasOwnProperty(idx));
+			console.print(this.GetItemText(i, itemLen, false, showMultiSelectMark) + "\x01n");
+			console.crlf();
+		}
+		this.numberedMode = numberedModeBackup;
+	}
 
 	this.drawnAlready = true;
 	this.nextDrawOnlyItemSubstr = null;
@@ -1165,7 +1397,7 @@ function DDLightbarMenu_GetItemText(pIdx, pItemLen, pHighlight, pSelected)
 	if ((pIdx >= 0) && (pIdx < numItems))
 	{
 		var itemLen = 0;
-		if (typeof(pItemLen) == "number")
+		if (typeof(pItemLen) === "number")
 			itemLen = pItemLen;
 		else
 		{
@@ -1201,19 +1433,27 @@ function DDLightbarMenu_GetItemText(pIdx, pItemLen, pHighlight, pSelected)
 		else
 			selectedItemColor = (menuItem.useAltColors ? this.colors.altSelectedItemColor : this.colors.selectedItemColor);
 		var itemColor = "";
-		if (typeof(pHighlight) === "boolean")
+		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, and truncate it to the displayable item width.
-		// 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);
+		// 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 = itemText.substr(0, 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
@@ -1389,13 +1629,35 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 	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);
+		this.Draw(pSelectedItemIndexes, null, null, numItems);
 		if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
 			this.DisplayInitialScrollbar(this.scrollbarInfo.solidBlockLastStartRow);
 	}
@@ -1403,82 +1665,66 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 	if (this.callOnItemNavOnStartup && typeof(this.OnItemNav) === "function")
 		this.OnItemNav(0, this.selectedItemIdx);
 
-	// User input loop
-	var userChoices = null; // For multi-select mode
-	var selectedItemIndexes = { }; // For multi-select mode
-	if (typeof(pSelectedItemIndexes) == "object")
-		selectedItemIndexes = pSelectedItemIndexes;
-	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.ANSISupported())
 	{
-		if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
-			this.UpdateScrollbarWithHighlightedItem();
+		// User input loop
+		var userChoices = null; // For multi-select mode
+		var selectedItemIndexes = { }; // For multi-select mode
+		if (typeof(pSelectedItemIndexes) == "object")
+			selectedItemIndexes = pSelectedItemIndexes;
+		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;
+			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)
+			// 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)
 			{
-				// 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))
+				mk = mouse_getkey(inputMode, this.inputTimeoutMS > 1 ? this.inputTimeoutMS : undefined, this.mouseEnabled);
+				if (mk.mouse !== null)
 				{
-					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
+					// 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))
 					{
-						// 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)
+						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)
 						{
-							if (this.multiSelect)
-								this.lastUserInput = " ";
+							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
 							{
-								// No mouse action
+								// 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;
@@ -1486,603 +1732,813 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 						}
 						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()))
+							// 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
 							{
-								this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
-								this.selectedItemIdx = itemIdx;
-								this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+								// 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;
 							}
-							// Don't have the later code do anything
-							this.lastUserInput = "";
-							mouseNoAction = true;
-							mouseInputOnly_continue = true;
 						}
-					}
 
-					this.lastMouseClickTime = system.timer;
+						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
 				{
-					// 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;
+					// mouse is null, so a keybaord key must have been pressed
+					this.lastUserInput = mk.key;
 				}
 			}
-			else
+			else // this.mouseEnabled is false
 			{
-				// mouse is null, so a keybaord key must have been pressed
-				this.lastUserInput = mk.key;
+				this.lastUserInput = getKeyWithESCChars(inputMode, this.inputTimeoutMS);
 			}
-		}
-		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;
+			// 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)))
-		{
-			// 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))
-		{
-			if (this.selectedItemIdx > 0)
+			// Take the appropriate action based on the user's last input/keypress
+			if ((this.lastUserInput == KEY_ESC) || (this.QuitKeysIncludes(this.lastUserInput)))
 			{
-				// Draw the current item in regular colors
-				this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
-				var oldSelectedItemIdx = this.selectedItemIdx--;
-				// 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)
+				// 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)
 				{
-					--this.topItemIdx;
-					this.Draw(selectedItemIndexes);
+					goAheadAndExit = !mouseNoAction; // Only really needed with an input timer?
 				}
-				else
+				if (goAheadAndExit)
 				{
-					// 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));
+					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
 				}
-				if (typeof(this.OnItemNav) === "function")
-					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
-			else
+			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.DoPageUp(selectedItemIndexes, numItems);
+			else if (this.lastUserInput == KEY_PAGEDN)
+				this.DoPageDown(selectedItemIndexes, numItems);
+			else if (this.lastUserInput == KEY_HOME)
+			{
+				// 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)
+			{
+				// 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)))
 			{
-				// selectedItemIdx is 0.  If wrap navigation is enabled, then go to the
-				// last item.
-				if (this.wrapNavigation)
+				// 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)
 				{
-					// Draw the current item in regular colors
-					//this.WriteItemAtItsLocation(pIdx, pHighlight, pSelected)
-					this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
-					// Go to the last item and scroll to the bottom if necessary
-					var oldSelectedItemIdx = this.selectedItemIdx;
-					this.selectedItemIdx = numItems - 1;
-					var oldTopItemIdx = this.topItemIdx;
-					var numItemsPerPage = (this.borderEnabled ? this.size.height - 2 : this.size.height);
-					this.topItemIdx = numItems - numItemsPerPage;
-					if (this.topItemIdx < 0)
-						this.topItemIdx = 0;
-					if (this.topItemIdx != oldTopItemIdx)
-						this.Draw(selectedItemIndexes);
-					else
+					// 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)
 					{
-						// Draw the new current item in selected colors
-						this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+						if (Object.keys(selectedItemIndexes).length == 0)
+							selectedItemIndexes[+(this.selectedItemIdx)] = true;
 					}
-					if (typeof(this.OnItemNav) === "function")
-						this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+					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 == KEY_DOWN) || (this.lastUserInput == KEY_RIGHT))
-		{
-			this.DoKeyDown(selectedItemIndexes, numItems);
-		}
-		else if (this.lastUserInput == KEY_PAGEUP)
-		{
-			// Only do this if we're not already at the top of the list
-			if (this.topItemIdx > 0)
+			else if (this.lastUserInput == " ") // Add the current item to multi-select
 			{
-				var oldSelectedItemIdx = this.selectedItemIdx;
-				var numItemsPerPage = (this.borderEnabled ? this.size.height - 2 : this.size.height);
-				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
+				// Add the current item to multi-select if multi-select is enabled
+				if (this.multiSelect)
 				{
-					// 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)
+					// 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 topIndexForLastPage = numItems - numItemsPerPage;
-						if (topIndexForLastPage < 0)
-							topIndexForLastPage = 0;
-						else if (topIndexForLastPage >= numItems)
-							topIndexForLastPage = numItems - 1;
-
-						this.topItemIdx = topIndexForLastPage;
-						this.selectedItemIdx = topIndexForLastPage;
-						this.Draw(selectedItemIndexes);
+						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");
+						}
 					}
 				}
-				if (typeof(this.OnItemNav) === "function")
-					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
-			else
+			// 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)
 			{
-				// 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 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.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
-					this.selectedItemIdx = 0;
-					this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(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 if (this.lastUserInput == KEY_PAGEDN)
-		{
-			var numItemsPerPage = (this.borderEnabled ? this.size.height - 2 : this.size.height);
-			// Only do the pageDown if we're not showing the last item already
-			var lastItemIdx = this.NumItems() - 1;
-			if (lastItemIdx > this.topItemIdx+numItemsPerPage-1)
+			else
 			{
-				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
+				// 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)
 				{
-					// 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 theItem = this.GetItem(i);
+					for (var h = 0; h < theItem.hotkeys.length; ++h)
 					{
-						this.topItemIdx = 0;
-						this.selectedItemIdx = 0;
+						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;
+						}
 					}
-					this.Draw(selectedItemIndexes);
 				}
-				if (typeof(this.OnItemNav) === "function")
-					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
-			else
+		}
+	}
+	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);
+			if (!console.aborted && userEnteredItemNum > 0)
 			{
-				// 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)
+				if (this.ItemIsSelectable(userEnteredItemNum-1))
 				{
-					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);
+					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 if (this.lastUserInput == KEY_HOME)
+	}
+	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)
 		{
-			// Go to the first item in the list
-			if (this.selectedItemIdx > 0)
+			if (this.topItemIdx > 0)
 			{
-				var oldSelectedItemIdx = this.selectedItemIdx;
-				this.oldTopItemIdx = this.topItemIdx;
-				// If the current item index is not on first current page, then scroll.
-				// Otherwise, draw more efficiently by drawing the current item in
-				// regular colors and the first item in highlighted colors.
-				this.topItemIdx = 0;
-				var numItemsPerPage = this.GetNumItemsPerPage();
-				if (this.oldTopItemIdx > 0)
-				{
-					this.selectedItemIdx = 0;
-					this.Draw(pSelectedItemIndexes, false);
-				}
-				else
-				{
-					// We're already on the first page, so we can re-draw the
-					// 2 items more efficiently.
-					// Draw the current item in regular colors
-					if (this.borderEnabled)
-						console.gotoxy(this.pos.x+1, this.pos.y+this.selectedItemIdx-this.topItemIdx+1);
-					else
-						console.gotoxy(this.pos.x, this.pos.y+this.selectedItemIdx-this.topItemIdx);
-					this.WriteItem(this.selectedItemIdx, null, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
-					this.selectedItemIdx = 0;
-					// Draw the new current item in selected colors
-					if (this.borderEnabled)
-						console.gotoxy(this.pos.x+1, this.pos.y+this.selectedItemIdx-this.topItemIdx+1);
-					else
-						console.gotoxy(this.pos.x, this.pos.y+this.selectedItemIdx-this.topItemIdx);
-					this.WriteItem(this.selectedItemIdx, null, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
-				}
-				if (typeof(this.OnItemNav) === "function")
-					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
+				--this.topItemIdx;
+				this.Draw(selectedItemIndexes);
 			}
+			else
+				canWrapNav = true;
 		}
-		else if (this.lastUserInput == KEY_END)
+		if (canWrapNav && this.wrapNavigation)
 		{
-			// Go to the last item in the list
-			var numItemsPerPage = this.GetNumItemsPerPage();
-			if (this.selectedItemIdx < numItems-1)
+			// 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;
-				var lastPossibleTop = numItems - numItemsPerPage;
-				if (lastPossibleTop < 0)
-					lastPossibleTop = 0;
-				var lastItemIdx = numItems - 1;
-				// If the last item index is below the current page, then scroll.
-				// Otherwise, draw more efficiently by drawing the current item in
-				// regular colors and the last item in highlighted colors.
-				if (lastItemIdx >= this.topItemIdx + numItemsPerPage)
-				{
-					this.topItemIdx = lastPossibleTop;
-					this.selectedItemIdx = lastItemIdx;
-					this.Draw(pSelectedItemIndexes, false);
-				}
-				else
-				{
-					// We're already on the last page, so we can re-draw the
-					// 2 items more efficiently.
-					// Draw the current item in regular colors
-					if (this.borderEnabled)
-						console.gotoxy(this.pos.x+1, this.pos.y+this.selectedItemIdx-this.topItemIdx+1);
-					else
-						console.gotoxy(this.pos.x, this.pos.y+this.selectedItemIdx-this.topItemIdx);
-					this.WriteItem(this.selectedItemIdx, null, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
-					this.selectedItemIdx = this.topItemIdx + numItemsPerPage - 1;
-					if (this.selectedItemIdx >= numItems)
-						this.selectedItemIdx = lastItemIdx;
-					// Draw the new current item in selected colors
-					if (this.borderEnabled)
-						console.gotoxy(this.pos.x+1, this.pos.y+this.selectedItemIdx-this.topItemIdx+1);
-					else
-						console.gotoxy(this.pos.x, this.pos.y+this.selectedItemIdx-this.topItemIdx);
-					this.WriteItem(this.selectedItemIdx, null, true, selectedItemIndexes.hasOwnProperty(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);
 			}
 		}
-		// 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)))
+	}
+}
+// 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)
 		{
-			// 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)
+			// 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)
 			{
-				// 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;
+				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 if (this.lastUserInput == " ") // Add the current item to multi-select
+	}
+	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)
 		{
-			// Add the current item to multi-select if multi-select is enabled
-			if (this.multiSelect)
+			var topIndexForLastPage = numItems - this.GetNumItemsPerPage();
+			if (topIndexForLastPage < 0)
+				topIndexForLastPage = 0;
+			else if (topIndexForLastPage >= numItems)
+				topIndexForLastPage = numItems - 1;
+			if (this.topItemIdx < topIndexForLastPage)
 			{
-				// 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");
-					}
-				}
+				++this.topItemIdx;
+				this.Draw(selectedItemIndexes);
 			}
+			else
+				canWrapNav = true;
 		}
-		// 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)
+		if (canWrapNav && this.wrapNavigation)
 		{
-			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)
+			// 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 = 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);
-					}
-				}
+				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
-				console.gotoxy(originalCurpos); // Move the cursor back where it was
+			{
+				// 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
 		{
-			// 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)
+			// 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 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;
-					}
-				}
+				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);
 	}
-
-	// Set the screen color back to normal so that text written to the screen
-	// after this looks good.
-	console.print("\x01n");
-	
-	// If in multi-select mode, populate userChoices with the choices
-	// that the user selected.
-	if (this.multiSelect && (Object.keys(selectedItemIndexes).length > 0))
+	else
 	{
-		userChoices = [];
-		for (var prop in selectedItemIndexes)
-			userChoices.push(this.GetItem(prop).retval);
+		// 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);
+		}
 	}
-
-	return (this.multiSelect ? userChoices : retVal);
+	*/
 }
-// Performs the key-down behavior for showing the menu items
+// 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_DoKeyDown(pSelectedItemIndexes, pNumItems)
+function DDLightbarMenu_DoPageDown(pSelectedItemIndexes, pNumItems)
 {
 	var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {});
 	var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems());
 
-	if (this.selectedItemIdx < numItems-1)
+	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)
 	{
-		// Draw the current item in regular colors
-		this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
-		var oldSelectedItemIdx = this.selectedItemIdx++;
-		// 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.borderEnabled ? this.size.height - 2 : this.size.height);
-		if (this.selectedItemIdx > 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)
 		{
-			++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 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)));
+			// 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
 	{
-		// selectedItemIdx is the last item index.  If wrap navigation is enabled,
-		// then go to the first item.
-		if (this.wrapNavigation)
+		// 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)
 		{
-			// 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.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
 			{
-				// Draw the new current item in selected colors
-				this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
+				// 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);
 		}
+		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);
 	}
 }
 
@@ -2102,7 +2558,7 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems)
 //                right: The character to use for the right border
 function DDLightbarMenu_SetBorderChars(pBorderChars)
 {
-	if (typeof(pBorderChars) != "object")
+	if (typeof(pBorderChars) !== "object")
 		return;
 
 	var borderPropNames = [ "upperLeft", "upperRight", "lowerLeft", "lowerRight",
@@ -2138,7 +2594,7 @@ function DDLightbarMenu_SetColors(pColors)
 
 	var colorPropNames = ["itemColor", "selectedItemColor", "altItemColor", "altSelectedItemColor",
 	                      "itemTextCharHighlightColor", "borderColor", "scrollbarScrollBlockColor",
-	                      "scrollbarBGColor"];
+	                      "scrollbarBGColor", "unselectableItemColor"];
 	for (var i = 0; i < colorPropNames.length; ++i)
 	{
 		if (pColors.hasOwnProperty(colorPropNames[i]))
@@ -2167,17 +2623,51 @@ function DDLightbarMenu_GetTopItemIdxOfLastPage()
 	return topItemIndex;
 }
 
-// Sets the top item index to the top item of the last page of items
-function DDLightbarMenu_SetTopItemIdxToTopOfLastPage()
+// 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;
-	this.topItemIdx = this.NumItems() - numItemsPerPage;
+
+	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.
 //
@@ -2622,6 +3112,12 @@ function DDLightbarMenu_ScreenRowForItem(pItemIdx)
 	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.
@@ -2650,6 +3146,9 @@ function DDLightbarMenu_CalcScrollbarBlocks()
 	}
 }
 
+
+
+
 //////////////////////////////////////////////////////////
 // Helper functions, not part of the DDLightbarMenu class
 
@@ -2907,7 +3406,8 @@ function getDefaultMenuItem() {
 		hotkeys: "",
 		useAltColors: false,
 		itemColor: null,
-		itemSelectedColor: null
+		itemSelectedColor: null,
+		isSelectable: true
 	};
 }
 
@@ -3084,3 +3584,105 @@ function getKeyWithESCChars(pGetKeyMode, pInputTimeoutMS)
 
 	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"));
+    }
+}
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index a3b311b8207081bd3178dff7db065b57fa202e04..871d289ed3f80a2bbfb29156474711732e0f25ae 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -112,6 +112,11 @@
  *                              Bug fix for deleting multiple selected messages: When updating message
  *                              headers in the cached arrays, don't try to save them back to the database,
  *                              because that was already done (this avoids a 'header has expanded fields' error).
+ * 2023-04-04 Eric Oulashin     Version 1.70
+ *                              Added "indexed" reader mode, which lists sub-boards with total and
+ *                              number of new/unread messages and lets the user read messages in those
+ *                              sub-boards.
+ *                              Also, utf-8 characters should now be converted properfly for non utf-8 terminals.
  */
 
 "use strict";
@@ -217,8 +222,8 @@ var ansiterm = require("ansiterm_lib.js", 'expand_ctrl_a');
 
 
 // Reader version information
-var READER_VERSION = "1.69";
-var READER_DATE = "2023-03-24";
+var READER_VERSION = "1.70";
+var READER_DATE = "2023-04-04";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -359,6 +364,11 @@ const THREAD_BY_TITLE = 16;
 const THREAD_BY_AUTHOR = 17;
 const THREAD_BY_TO_USER = 18;
 
+// Scan scopes
+const SCAN_SCOPE_SUB_BOARD = 0;
+const SCAN_SCOPE_GROUP = 1;
+const SCAN_SCOPE_ALL = 2;
+
 // Reader mode - Actions
 const ACTION_NONE = 19;
 const ACTION_GO_NEXT_MSG = 20;
@@ -371,6 +381,8 @@ const ACTION_CHG_MSG_AREA = 26;
 const ACTION_GO_PREV_MSG_AREA = 27;
 const ACTION_GO_NEXT_MSG_AREA = 28;
 const ACTION_QUIT = 29;
+// Actions for indexed Mode
+const INDEXED_MODE_SUBBOARD_MENU = 30;
 
 // Definitions for help line refresh parameters for error functions
 const REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE = 0;
@@ -597,10 +609,7 @@ if (gDoDDMR)
 	}
 	var msgReader = new DigDistMsgReader(readerSubCode, gCmdLineArgVals);
 	if (gCmdLineArgVals.indexedmode)
-	{
-		// TODO: Finish indexed mode
-		msgReader.DoIndexedMode();
-	}
+		msgReader.DoIndexedMode(SCAN_SCOPE_ALL);
 	else
 	{
 		// If the option to choose a message area first was enabled on the command-line
@@ -859,6 +868,7 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	// file.  The configuration file can be either READER_MODE_READ or
 	// READER_MODE_LIST, but the optional "mode" parameter in the command-line
 	// arguments can specify another mode.
+	// Note: This isn't used for "indexed" reader mode (but maybe it should be in the future)
 	this.startMode = READER_MODE_LIST;
 
 	// hdrsForCurrentSubBoard is an array that will be populated with the
@@ -895,6 +905,12 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	this.subBoardCode = bbs.cursub_code; // The message sub-board code
 	this.readingPersonalEmail = false;
 
+	// Whether or not we're in indexed reader mode
+	this.indexedMode = false;
+	// For indexed reader mode, whether or not to enable caching the message
+	// header lists for performance
+	this.enableIndexedModeMsgListCache = true;
+
 	// this.colors will be an array of colors to use in the message list
 	this.colors = getDefaultColors();
 	this.readingPersonalEmailFromUser = false;
@@ -1108,9 +1124,6 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	// Whether or not to convert Y-style MCI attribute codes to Synchronet attribute codes
 	this.convertYStyleMCIAttrsToSync = false;
 
-	// Whether or not to use the scrollbar in the enhanced message reader
-	this.useEnhReaderScrollbar = true;
-
 	// Whether or not to prepend the subject for forwarded messages with "Fwd: "
 	this.prependFowardMsgSubject = true;
 
@@ -1132,7 +1145,11 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	this.cfgFileSuccessfullyRead = false;
 	this.ReadConfigFile();
 	this.userSettings = {
-		twitList: []
+		twitList: [],
+		// Whether or not to use the scrollbar in the enhanced message reader
+		useEnhReaderScrollbar: true,
+		// Whether or not to use indexed reader mode for doing a newscan
+		useIndexedModeForNewscan: false
 	};
 	this.ReadUserSettingsFile(false);
 	// Set any other values specified by the command-line parameters
@@ -1394,9 +1411,11 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	this.MsgHdrFromOrToInUserTwitlist = DigDistMsgReader_MsgHdrFromOrToInUserTwitlist;
 	// For indexed mode
 	this.DoIndexedMode = DigDistMsgReader_DoIndexedMode;
-	this.DoIndexedModeLightbar = DigDistMsgReader_DoIndexedModeLightbar;
-	this.DoIndexedModeTraditional = DigDistMsgReader_DoIndexedModeTraditional;
+	this.IndexedModeChooseSubBoard = DigDistMsgReader_IndexedModeChooseSubBoard;
 	this.MakeLightbarIndexedModeMenu = DigDistMsgReader_MakeLightbarIndexedModeMenu;
+	this.GetIndexedModeSubBoardMenuItemTextAndInfo = DigDistMsgReader_GetIndexedModeSubBoardMenuItemTextAndInfo;
+	this.MakeIndexedModeHelpLine = DigDistMsgReader_MakeIndexedModeHelpLine;
+	this.ShowIndexedListHelp = DigDistMsgReader_ShowIndexedListHelp;
 
 	// printf strings for message group/sub-board lists
 	// Message group information (printf strings)
@@ -1458,7 +1477,7 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	var leftPadLen = Math.floor(padLen/2);
 	var rightPadLen = padLen - leftPadLen;
 	this.lightbarAreaChooserHelpLine = this.colors.lightbarAreaChooserHelpLineGeneralColor
-	                                 + format("%" + leftPadLen + "s", "")
+	                                 + format("%*s", leftPadLen, "")
 	                                 + this.lightbarAreaChooserHelpLine
 	                                 + this.colors.lightbarAreaChooserHelpLineGeneralColor
 	                                 + format("%" + rightPadLen + "s", "") + "\x01n";
@@ -2424,6 +2443,17 @@ function DigDistMsgReader_MessageAreaScan(pScanCfgOpt, pScanMode, pScanScopeChar
 		writeToSysAndNodeLog(logMessage);
 	}
 
+	// If doing a newscan of all sub-boards, and the user has their setting for indexed mode
+	// for newscan enabled, then do that and return instead of the traditional newscan.
+	if (pScanCfgOpt === SCAN_CFG_NEW && pScanMode === SCAN_NEW && this.userSettings.useIndexedModeForNewscan)
+	{
+		var scanScope = SCAN_SCOPE_ALL;
+		if (scanScopeChar === "S") scanScope = SCAN_SCOPE_SUB_BOARD;
+		else if (scanScopeChar === "G") scanScope = SCAN_SCOPE_GROUP;
+		msgReader.DoIndexedMode(scanScope);
+		return;
+	}
+
 	// Save the original search type, sub-board code, searched message headers,
 	// etc. to be restored later
 	var originalSearchType = this.searchType;
@@ -2774,6 +2804,8 @@ function DigDistMsgReader_ReadMessages(pSubBoardCode, pStartingMsgOffset, pRetur
 		msgIndex = this.GetLastReadMsgIdxAndNum().lastReadMsgIdx;
 		if (msgIndex == -1)
 			msgIndex = 0;
+		else if (msgIndex >= numOfMessages)
+			msgIndex = numOfMessages - 1;
 	}
 
 	// If the current message index is for a message that has been
@@ -4540,11 +4572,11 @@ function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pRet
 		}
 		else
 		{
-			msgHdrStr += format(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar,
+			msgHdrStr += format(format(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar,
 			       fromName.substr(0, this.FROM_LEN),
 			       pMsgHeader.to.substr(0, this.TO_LEN),
 			       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
-			       sDate, sTime);
+			       sDate, sTime));
 		}
 	}
 	else
@@ -4835,7 +4867,8 @@ function DigDistMsgReader_ReadMessageEnhanced(pOffset, pAllowChgArea)
 	// Get the message text and see if it has any ANSI codes.  Remove any pause
 	// codes it might have.  If it has ANSI codes, then don't use the scrolling
 	// interface so that the ANSI gets displayed properly.
-	var messageText = this.GetMsgBody(msgHeader);
+	var getMsgBodyRetObj = this.GetMsgBody(msgHeader);
+	var messageText = getMsgBodyRetObj.msgBody;
 
 	if (msgHdrHasAttachmentFlag(msgHeader))
 	{
@@ -4853,9 +4886,9 @@ function DigDistMsgReader_ReadMessageEnhanced(pOffset, pAllowChgArea)
 	// Use the scrollable reader interface if the setting is enabled & the user's
 	// terminal supports ANSI.  Otherwise, use a more traditional user interface.
 	if (useScrollingInterface)
-		retObj = this.ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset);
+		retObj = this.ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset, getMsgBodyRetObj.pmode);
 	else
-		retObj = this.ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset);
+		retObj = this.ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset, getMsgBodyRetObj.pmode);
 
 	// Mark the message as read if it was written to the current user
 	if (userNameHandleAliasMatch(msgHeader.to) && ((msgHeader.attr & MSG_READ) == 0))
@@ -4871,15 +4904,24 @@ function DigDistMsgReader_ReadMessageEnhanced(pOffset, pAllowChgArea)
 	// scan & last read message pointers.
 	if ((this.subBoardCode != "mail") && (this.searchType == SEARCH_NONE))
 	{
-		if (msgHeader.number > GetScanPtrOrLastMsgNum(this.subBoardCode))
+		if (typeof(msg_area.sub[this.subBoardCode].scan_ptr) === "number")
+		{
+			if (msg_area.sub[this.subBoardCode].scan_ptr != 0xffffffff && msg_area.sub[this.subBoardCode].scan_ptr < msgHeader.number)
+				msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number;
+		}
+		else
 			msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number;
+		//if (msgHeader.number > GetScanPtrOrLastMsgNum(this.subBoardCode))
+		//	msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number;
 		msg_area.sub[this.subBoardCode].last_read = msgHeader.number;
+		//if (msgHeader.number > msg_area.sub[this.subBoardCode].last_read)
+		//	msg_area.sub[this.subBoardCode].last_read = msgHeader.number;
 	}
 
 	return retObj;
 }
 // Helper method for ReadMessageEnhanced() - Does the scrollable reader interface
-function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset)
+function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset, pmode)
 {
 	var retObj = {
 		offsetValid: true,
@@ -4913,7 +4955,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 		console.gotoxy(1, console.screen_rows);
 	}
 
-	var msgAreaWidth = this.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
+	var msgAreaWidth = this.userSettings.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
 
 	// We could word-wrap the message to ensure words aren't split across lines, but
 	// doing so could make some messages look bad (i.e., messages with drawing characters),
@@ -4963,7 +5005,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 	// column of the message area showing the fraction of the message shown and what
 	// part of the message is currently being shown.  The scrollbar will be updated
 	// minimally in the input loop to minimize screen redraws.
-	if (this.useEnhReaderScrollbar)
+	if (this.userSettings.useEnhReaderScrollbar)
 		this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
 
 	// Input loop (for scrolling the message up & down)
@@ -4991,8 +5033,9 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 		var scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx,
 									   this.colors.msgBodyColor, writeMessage,
 									   this.msgAreaLeft, this.msgAreaTop, msgAreaWidth,
-									   msgAreaHeight, 1, console.screen_rows, this.useEnhReaderScrollbar,
-									   msgScrollbarUpdateFn, scrollbarInfoObj);
+									   msgAreaHeight, 1, console.screen_rows,
+									   this.userSettings.useEnhReaderScrollbar,
+									   msgScrollbarUpdateFn, scrollbarInfoObj, pmode);
 		topMsgLineIdx = scrollRetObj.topLineIdx;
 		retObj.lastKeypress = scrollRetObj.lastKeypress;
 		switch (retObj.lastKeypress)
@@ -5091,7 +5134,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 						// refresh it.
 						//var scrollBarBlock = "\x01n\x01h\x01k" + BLOCK1; // Dim block
 						// Dim block
-						if (this.useEnhReaderScrollbar)
+						if (this.userSettings.useEnhReaderScrollbar)
 						{
 							var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
 							if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom)
@@ -5139,7 +5182,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 							// Display the scrollbar again, and ensure it's in the correct position
-							if (this.useEnhReaderScrollbar)
+							if (this.userSettings.useEnhReaderScrollbar)
 							{
 								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5166,7 +5209,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 				// Display the scrollbar again, and ensure it's in the correct position
-				if (this.useEnhReaderScrollbar)
+				if (this.userSettings.useEnhReaderScrollbar)
 				{
 					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5228,7 +5271,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 							// Display the scrollbar again to refresh it on the screen
-							if (this.useEnhReaderScrollbar)
+							if (this.userSettings.useEnhReaderScrollbar)
 							{
 								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5263,7 +5306,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 					// Display the scrollbar again to refresh it on the screen
-					if (this.useEnhReaderScrollbar)
+					if (this.userSettings.useEnhReaderScrollbar)
 					{
 						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5506,7 +5549,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader, (retObj.lastKeypress == this.enhReaderKeys.showKludgeLines));
 					if (extdHdrInfoLines.length > 0)
 					{
-						if (this.useEnhReaderScrollbar)
+						if (this.userSettings.useEnhReaderScrollbar)
 						{
 							// Calculate information for the scrollbar for the kludge lines
 							var infoFractionShown = this.msgAreaHeight / extdHdrInfoLines.length;
@@ -5522,9 +5565,9 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 						}
 						scrollTextLines(extdHdrInfoLines, 0, this.colors["msgBodyColor"], true, this.msgAreaLeft,
 						                this.msgAreaTop, msgAreaWidth, msgAreaHeight, 1, console.screen_rows,
-						                this.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
+						                this.userSettings.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
 						// Display the scrollbar for the message to refresh it on the screen
-						if (this.useEnhReaderScrollbar)
+						if (this.userSettings.useEnhReaderScrollbar)
 						{
 							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5579,7 +5622,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 					// Display the scrollbar again to refresh it on the screen
-					if (this.useEnhReaderScrollbar)
+					if (this.userSettings.useEnhReaderScrollbar)
 					{
 						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5658,7 +5701,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 					// Display the scrollbar again to refresh it on the screen
-					if (this.useEnhReaderScrollbar)
+					if (this.userSettings.useEnhReaderScrollbar)
 					{
 						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5691,7 +5734,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 				// Display the scrollbar again to refresh it on the screen
-				if (this.useEnhReaderScrollbar)
+				if (this.userSettings.useEnhReaderScrollbar)
 				{
 					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5796,7 +5839,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					// Display the vote info and let the user scroll through them
 					// (the console height should be enough, but do this just in case)
 					// Calculate information for the scrollbar for the vote info lines
-					if (this.useEnhReaderScrollbar)
+					if (this.userSettings.useEnhReaderScrollbar)
 					{
 						var infoFractionShown = this.msgAreaHeight / voteInfo.length;
 						if (infoFractionShown > 1)
@@ -5814,9 +5857,9 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 						// TODO
 					}
 					scrollTextLines(voteInfo, 0, this.colors["msgBodyColor"], true, this.msgAreaLeft, this.msgAreaTop, msgAreaWidth,
-					                msgAreaHeight, 1, console.screen_rows, this.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
+					                msgAreaHeight, 1, console.screen_rows, this.userSettings.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
 					// Display the scrollbar for the message to refresh it on the screen
-					if (this.useEnhReaderScrollbar)
+					if (this.userSettings.useEnhReaderScrollbar)
 					{
 						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -5941,8 +5984,8 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				*/
 				break;
 			case this.enhReaderKeys.userSettings:
-				// Make a backup copy of the this.useEnhReaderScrollbar setting in case it changes, so we can tell if we need to refresh the scrollbar
-				var oldUseEnhReaderScrollbar = this.useEnhReaderScrollbar;
+				// Make a backup copy of the this.userSettings.useEnhReaderScrollbar setting in case it changes, so we can tell if we need to refresh the scrollbar
+				var oldUseEnhReaderScrollbar = this.userSettings.useEnhReaderScrollbar;
 				var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { pReader.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); });
 				retObj.lastKeypress = "";
 				writeMessage = userSettingsRetObj.needWholeScreenRefresh;
@@ -5998,7 +6041,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				if (userSettingsRetObj.needWholeScreenRefresh)
 				{
 					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-					if (this.useEnhReaderScrollbar)
+					if (this.userSettings.useEnhReaderScrollbar)
 						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
 					else
 					{
@@ -6009,9 +6052,9 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				else
 				{
 					// If the message scrollbar was toggled, then draw/erase the scrollbar
-					if (this.useEnhReaderScrollbar != oldUseEnhReaderScrollbar)
+					if (this.userSettings.useEnhReaderScrollbar != oldUseEnhReaderScrollbar)
 					{
-						msgAreaWidth = this.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
+						msgAreaWidth = this.userSettings.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
 						// If the message is ANSI, then re-create the Graphic object to account for the
 						// new width
 						if (msgHasANSICodes)
@@ -6028,7 +6071,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 							messageText = graphic.MSG;
 						}
 						// Display or erase the scrollbar
-						if (this.useEnhReaderScrollbar)
+						if (this.userSettings.useEnhReaderScrollbar)
 						{
 							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
 							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
@@ -6165,7 +6208,7 @@ function DigDistMsgReader_ScrollReaderDetermineClickCoordAction(pScrollRetObj, p
 		if ((pScrollRetObj.mouse.x == pEnhReadHelpLineClickCoords[coordIdx].x) && (pScrollRetObj.mouse.y == pEnhReadHelpLineClickCoords[coordIdx].y))
 		{
 			// The up arrow, down arrow, PageUp, PageDown, Home, and End aren't handled
-			// here - Those are handled in scrollTextlines().
+			// here - Those are handled in scrollTextLines().
 			if (pEnhReadHelpLineClickCoords[coordIdx].actionStr == LEFT_ARROW)
 				retObj.actionStr = this.enhReaderKeys.previousMsg;
 			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr == RIGHT_ARROW)
@@ -6192,7 +6235,7 @@ function DigDistMsgReader_ScrollReaderDetermineClickCoordAction(pScrollRetObj, p
 	return retObj;
 }
 // Helper method for ReadMessageEnhanced() - Does the traditional (non-scrollable) reader interface
-function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset)
+function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset, pmode)
 {
 	var retObj = {
 		offsetValid: true,
@@ -6258,8 +6301,8 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 				console.clear("\x01n");
 			// Write the message header & message body to the screen
 			this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-			console.print("\x01n" + this.colors["msgBodyColor"]);
-			console.putmsg(msgTextWrapped, P_NOATCODES);
+			console.print("\x01n" + this.colors.msgBodyColor);
+			console.putmsg(msgTextWrapped, pmode|P_NOATCODES);
 		}
 		// Write the prompt text
 		if (writePromptText)
@@ -7740,10 +7783,10 @@ function DigDistMsgReader_DisplayTraditionalMsgListHelp(pDisplayHeader, pChgSubB
 	}
 	console.crlf();
 
-	console.print("\x01n" + this.colors["tradInterfaceHelpScreenColor"]);
+	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor);
 	displayTextWithLineBelow("Page navigation and message selection", false,
-	                         this.colors["tradInterfaceHelpScreenColor"], "\x01k\x01h");
-	console.print(this.colors["tradInterfaceHelpScreenColor"]);
+	                         this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
+	console.print(this.colors.tradInterfaceHelpScreenColor);
 	console.print("The message lister will display a page of message header information.  At\r\n");
 	console.print("the end of each page, a prompt is displayed, allowing you to navigate to\r\n");
 	console.print("the next page, previous page, first page, or the last page.  If you would\r\n");
@@ -7754,33 +7797,35 @@ function DigDistMsgReader_DisplayTraditionalMsgListHelp(pDisplayHeader, pChgSubB
 	console.crlf();
 	console.crlf();
 	displayTextWithLineBelow("Summary of the keyboard commands:", false,
-	                         this.colors["tradInterfaceHelpScreenColor"], "\x01k\x01h");
-	console.print(this.colors["tradInterfaceHelpScreenColor"]);
-	console.print("\x01n\x01h\x01cN" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the next page\r\n");
-	console.print("\x01n\x01h\x01cP" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the previous page\r\n");
-	console.print("\x01n\x01h\x01cF" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the first page\r\n");
-	console.print("\x01n\x01h\x01cL" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the last page\r\n");
-	console.print("\x01n\x01h\x01cG" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to a specific message by number (the message will appear at the top\r\n" +
+	                         this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
+	console.print(this.colors.tradInterfaceHelpScreenColor);
+	console.print("\x01n\x01h\x01cN" + this.colors.tradInterfaceHelpScreenColor + ": Go to the next page\r\n");
+	console.print("\x01n\x01h\x01cP" + this.colors.tradInterfaceHelpScreenColor + ": Go to the previous page\r\n");
+	console.print("\x01n\x01h\x01cF" + this.colors.tradInterfaceHelpScreenColor + ": Go to the first page\r\n");
+	console.print("\x01n\x01h\x01cL" + this.colors.tradInterfaceHelpScreenColor + ": Go to the last page\r\n");
+	console.print("\x01n\x01h\x01cG" + this.colors.tradInterfaceHelpScreenColor + ": Go to a specific message by number (the message will appear at the top\r\n" +
 	              "   of the list)\r\n");
-	console.print("\x01n\x01h\x01cNumber" + this.colors["tradInterfaceHelpScreenColor"] + ": Read the message corresponding with that number\r\n");
+	console.print("\x01n\x01h\x01cNumber" + this.colors.tradInterfaceHelpScreenColor + ": Read the message corresponding with that number\r\n");
 	//console.print("The following commands are available only if you have permission to do so:\r\n");
 	if (this.CanDelete() || this.CanDeleteLastMsg())
-		console.print("\x01n\x01h\x01cD" + this.colors["tradInterfaceHelpScreenColor"] + ": Mark a message for deletion\r\n");
+		console.print("\x01n\x01h\x01cD" + this.colors.tradInterfaceHelpScreenColor + ": Mark a message for deletion\r\n");
 	if (this.CanEdit())
-		console.print("\x01n\x01h\x01cE" + this.colors["tradInterfaceHelpScreenColor"] + ": Edit an existing message\r\n");
+		console.print("\x01n\x01h\x01cE" + this.colors.tradInterfaceHelpScreenColor + ": Edit an existing message\r\n");
 	if (pChgSubBoardAllowed)
-		console.print("\x01n\x01h\x01cC" + this.colors["tradInterfaceHelpScreenColor"] + ": Change to another message sub-board\r\n");
-	console.print("\x01n\x01h\x01cS" + this.colors["tradInterfaceHelpScreenColor"] + ": Select messages (for batch delete, etc.)\r\n");
-	console.print("\x01n" + this.colors["tradInterfaceHelpScreenColor"] + "  A message number or multiple numbers can be entered separated by commas or\r\n");
-	console.print("\x01n" + this.colors["tradInterfaceHelpScreenColor"] + "  spaces.  Additionally, a range of numbers (separated by a dash) can be used.\r\n");
-	console.print("\x01n" + this.colors["tradInterfaceHelpScreenColor"] + "  Examples:\r\n");
-	console.print("\x01n" + this.colors["tradInterfaceHelpScreenColor"] + "  125\r\n");
-	console.print("\x01n" + this.colors["tradInterfaceHelpScreenColor"] + "  1,2,3\r\n");
-	console.print("\x01n" + this.colors["tradInterfaceHelpScreenColor"] + "  1 2 3\r\n");
-	console.print("\x01n" + this.colors["tradInterfaceHelpScreenColor"] + "  1,2,10-20\r\n");
-	console.print("\x01n\x01h\x01cCTRL-D" + this.colors["tradInterfaceHelpScreenColor"] + ": Batch delete selected messages\r\n");
-	console.print("\x01n\x01h\x01cQ" + this.colors["tradInterfaceHelpScreenColor"] + ": Quit\r\n");
-	console.print("\x01n\x01h\x01c?" + this.colors["tradInterfaceHelpScreenColor"] + ": Show this help screen\r\n\r\n");
+		console.print("\x01n\x01h\x01cC" + this.colors.tradInterfaceHelpScreenColor + ": Change to another message sub-board\r\n");
+	console.print("\x01n\x01h\x01cS" + this.colors.tradInterfaceHelpScreenColor + ": Select messages (for batch delete, etc.)\r\n");
+	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  A message number or multiple numbers can be entered separated by commas or\r\n");
+	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  spaces.  Additionally, a range of numbers (separated by a dash) can be used.\r\n");
+	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  Examples:\r\n");
+	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  125\r\n");
+	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  1,2,3\r\n");
+	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  1 2 3\r\n");
+	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  1,2,10-20\r\n");
+	console.print("\x01n\x01h\x01cCTRL-D" + this.colors.tradInterfaceHelpScreenColor + ": Batch delete selected messages\r\n");
+	console.print("\x01n\x01h\x01cQ" + this.colors.tradInterfaceHelpScreenColor + ": Quit\r\n");
+	if (this.indexedMode)
+		console.print(" Currently in indexed mode; quitting will quit back to the index list.\r\n");
+	console.print("\x01n\x01h\x01c?" + this.colors.tradInterfaceHelpScreenColor + ": Show this help screen\r\n\r\n");
 
 	// If pPauseAtEnd is true, then output a newline and
 	// prompt the user whether or not to continue.
@@ -7818,7 +7863,7 @@ function DigDistMsgReader_DisplayLightbarMsgListHelp(pDisplayHeader, pChgSubBoar
 			numOfMessages = msgbase.total_msgs;
 			msgbase.close();
 		}
-		console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current area.");
+		console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current sub-board.");
 		console.crlf();
 	}
 	// If there is currently a search (which also includes personal messages),
@@ -7842,8 +7887,8 @@ function DigDistMsgReader_DisplayLightbarMsgListHelp(pDisplayHeader, pChgSubBoar
 	console.crlf();
 
 	displayTextWithLineBelow("Lightbar interface: Page navigation and message selection",
-	                         false, this.colors["tradInterfaceHelpScreenColor"], "\x01k\x01h");
-	console.print(this.colors["tradInterfaceHelpScreenColor"]);
+	                         false, this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
+	console.print(this.colors.tradInterfaceHelpScreenColor);
 	console.print("The message lister will display a page of message header information.  You\r\n");
 	console.print("may use the up and down arrows to navigate the list of messages.  The\r\n");
 	console.print("currently-selected message will be highlighted as you navigate through\r\n");
@@ -7853,29 +7898,31 @@ function DigDistMsgReader_DisplayLightbarMsgListHelp(pDisplayHeader, pChgSubBoar
 	this.DisplayMessageListNotesHelp();
 	console.crlf();
 	console.crlf();
-	displayTextWithLineBelow("Summary of the keyboard commands:", false, this.colors["tradInterfaceHelpScreenColor"], "\x01k\x01h");
-	console.print(this.colors["tradInterfaceHelpScreenColor"]);
-	console.print("\x01n\x01h\x01cDown arrow" + this.colors["tradInterfaceHelpScreenColor"] + ": Move the cursor down/select the next message\r\n");
-	console.print("\x01n\x01h\x01cUp arrow" + this.colors["tradInterfaceHelpScreenColor"] + ": Move the cursor up/select the previous message\r\n");
-	console.print("\x01n\x01h\x01cN" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the next page\r\n");
-	console.print("\x01n\x01h\x01cP" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the previous page\r\n");
-	console.print("\x01n\x01h\x01cF" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the first page\r\n");
-	console.print("\x01n\x01h\x01cL" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the last page\r\n");
-	console.print("\x01n\x01h\x01cG" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to a specific message by number (the message will be highlighted and\r\n" +
+	displayTextWithLineBelow("Summary of the keyboard commands:", false, this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
+	console.print(this.colors.tradInterfaceHelpScreenColor);
+	console.print("\x01n\x01h\x01cDown arrow" + this.colors.tradInterfaceHelpScreenColor + ": Move the cursor down/select the next message\r\n");
+	console.print("\x01n\x01h\x01cUp arrow" + this.colors.tradInterfaceHelpScreenColor + ": Move the cursor up/select the previous message\r\n");
+	console.print("\x01n\x01h\x01cN" + this.colors.tradInterfaceHelpScreenColor + ": Go to the next page\r\n");
+	console.print("\x01n\x01h\x01cP" + this.colors.tradInterfaceHelpScreenColor + ": Go to the previous page\r\n");
+	console.print("\x01n\x01h\x01cF" + this.colors.tradInterfaceHelpScreenColor + ": Go to the first page\r\n");
+	console.print("\x01n\x01h\x01cL" + this.colors.tradInterfaceHelpScreenColor + ": Go to the last page\r\n");
+	console.print("\x01n\x01h\x01cG" + this.colors.tradInterfaceHelpScreenColor + ": Go to a specific message by number (the message will be highlighted and\r\n" +
 	              "   may appear at the top of the list)\r\n");
-	console.print("\x01n\x01h\x01cENTER" + this.colors["tradInterfaceHelpScreenColor"] + ": Read the selected message\r\n");
-	console.print("\x01n\x01h\x01cNumber" + this.colors["tradInterfaceHelpScreenColor"] + ": Read the message corresponding with that number\r\n");
+	console.print("\x01n\x01h\x01cENTER" + this.colors.tradInterfaceHelpScreenColor + ": Read the selected message\r\n");
+	console.print("\x01n\x01h\x01cNumber" + this.colors.tradInterfaceHelpScreenColor + ": Read the message corresponding with that number\r\n");
 	if (this.CanDelete() || this.CanDeleteLastMsg())
-		console.print("\x01n\x01h\x01cDEL" + this.colors["tradInterfaceHelpScreenColor"] + ": Mark the selected message for deletion\r\n");
+		console.print("\x01n\x01h\x01cDEL" + this.colors.tradInterfaceHelpScreenColor + ": Mark the selected message for deletion\r\n");
 	if (this.CanEdit())
-		console.print("\x01n\x01h\x01cE" + this.colors["tradInterfaceHelpScreenColor"] + ": Edit the selected message\r\n");
+		console.print("\x01n\x01h\x01cE" + this.colors.tradInterfaceHelpScreenColor + ": Edit the selected message\r\n");
 	if (pChgSubBoardAllowed)
-		console.print("\x01n\x01h\x01cC" + this.colors["tradInterfaceHelpScreenColor"] + ": Change to another message sub-board\r\n");
-	console.print("\x01n\x01h\x01cSpacebar" + this.colors["tradInterfaceHelpScreenColor"] + ": Select message (for batch delete, etc.)\r\n");
-	console.print("\x01n\x01h\x01cCTRL-A" + this.colors["tradInterfaceHelpScreenColor"] + ": Select/de-select all messages\r\n");
-	console.print("\x01n\x01h\x01cCTRL-D" + this.colors["tradInterfaceHelpScreenColor"] + ": Batch delete selected messages\r\n");
-	console.print("\x01n\x01h\x01cQ" + this.colors["tradInterfaceHelpScreenColor"] + ": Quit\r\n");
-	console.print("\x01n\x01h\x01c?" + this.colors["tradInterfaceHelpScreenColor"] + ": Show this help screen\r\n");
+		console.print("\x01n\x01h\x01cC" + this.colors.tradInterfaceHelpScreenColor + ": Change to another message sub-board\r\n");
+	console.print("\x01n\x01h\x01cSpacebar" + this.colors.tradInterfaceHelpScreenColor + ": Select message (for batch delete, etc.)\r\n");
+	console.print("\x01n\x01h\x01cCTRL-A" + this.colors.tradInterfaceHelpScreenColor + ": Select/de-select all messages\r\n");
+	console.print("\x01n\x01h\x01cCTRL-D" + this.colors.tradInterfaceHelpScreenColor + ": Batch delete selected messages\r\n");
+	console.print("\x01n\x01h\x01cQ" + this.colors.tradInterfaceHelpScreenColor + ": Quit\r\n");
+	if (this.indexedMode)
+		console.print(" Currently in indexed mode; quitting will quit back to the index list.\r\n");
+	console.print("\x01n\x01h\x01c?" + this.colors.tradInterfaceHelpScreenColor + ": Show this help screen\r\n");
 
 	// If pPauseAtEnd is true, then pause.
 	if (pPauseAtEnd && !console.aborted)
@@ -8124,7 +8171,6 @@ function DigDistMsgReader_SetEnhancedReaderHelpLine()
 	var helpLineScreenLen = (console.strlen(this.enhReadHelpLine) - numHotkeyChars);
 	var numCharsRemaining = console.screen_columns - helpLineScreenLen - 1;
 	var frontPaddingLen = Math.floor(numCharsRemaining/2);
-	//var padding = format("%" + frontPaddingLen + "s", "");
 	var padding = format("%*s", frontPaddingLen, "");
 	this.enhReadHelpLine = padding + this.enhReadHelpLine;
 	this.enhReadHelpLine = "\x01n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLine;
@@ -8351,6 +8397,8 @@ function DigDistMsgReader_ReadConfigFile()
 					this.convertYStyleMCIAttrsToSync = (valueUpper == "TRUE");
 				else if (settingUpper == "PREPENDFOWARDMSGSUBJECT")
 					this.prependFowardMsgSubject = (valueUpper == "TRUE");
+				else if (settingUpper == "ENABLEINDEXEDMODEMSGLISTCACHE")
+					this.enableIndexedModeMsgListCache = (valueUpper == "TRUE");
 			}
 		}
 
@@ -8455,7 +8503,13 @@ function DigDistMsgReader_ReadConfigFile()
 					    (setting == "msgListScoreHighlightColor") || (setting == "msgHdrMsgNumColor") ||
 					    (setting == "msgHdrFromColor") || (setting == "msgHdrToColor") ||
 					    (setting == "msgHdrToUserColor") || (setting == "msgHdrSubjColor") ||
-					    (setting == "msgHdrDateColor"))
+					    (setting == "msgHdrDateColor") || (setting == "lightbarIndexedModeHelpLineBkgColor") ||
+					    (setting == "lightbarIndexedModeHelpLineHotkeyColor") || (setting == "lightbarIndexedModeHelpLineGeneralColor") ||
+					    (setting == "lightbarIndexedModeHelpLineParenColor") || (setting == "indexMenuDesc") || (setting == "indexMenuTotalMsgs") ||
+					    (setting == "indexMenuNumNewMsgs") || (setting == "indexMenuLastPostDate") ||
+					    (setting == "indexMenuHighlightBkg") || (setting == "indexMenuDescHighlight") ||
+					    (setting == "indexMenuTotalMsgsHighlight") || (setting == "indexMenuNumNewMsgsHighlight") ||
+					    (setting == "indexMenuLastPostDateHighlight"))
 					{
 						// Trim spaces from the color value
 						value = trimSpaces(value, true, true, true);
@@ -8583,7 +8637,8 @@ function DigDistMsgReader_ReadUserSettingsFile(pOnlyTwitlist)
 		if (userSettingsFile.open("r"))
 		{
 			//var behavior = userSettingsFile.iniGetObject("BEHAVIOR");
-			this.useEnhReaderScrollbar = userSettingsFile.iniGetValue("BEHAVIOR", "useEnhReaderScrollbar", true);
+			this.userSettings.useEnhReaderScrollbar = userSettingsFile.iniGetValue("BEHAVIOR", "useEnhReaderScrollbar", true);
+			this.userSettings.useIndexedModeForNewscan = userSettingsFile.iniGetValue("BEHAVIOR", "useIndexedModeForNewscan", false);
 			userSettingsFile.close();
 		}
 	}
@@ -8599,7 +8654,8 @@ function DigDistMsgReader_WriteUserSettingsFile()
 	var userSettingsFile = new File(gUserSettingsFilename);
 	if (userSettingsFile.open(userSettingsFile.exists ? "r+" : "w+"))
 	{
-		userSettingsFile.iniSetValue("BEHAVIOR", "useEnhReaderScrollbar", this.useEnhReaderScrollbar);
+		userSettingsFile.iniSetValue("BEHAVIOR", "useEnhReaderScrollbar", this.userSettings.useEnhReaderScrollbar);
+		userSettingsFile.iniSetValue("BEHAVIOR", "useIndexedModeForNewscan", this.userSettings.useIndexedModeForNewscan);
 		userSettingsFile.close();
 		writeSucceeded = true;
 	}
@@ -8958,7 +9014,7 @@ function DigDistMsgReader_NumMessages(pMsgbase, pCheckDeletedAttributes)
 
 	var msgbase = null;
 	var closeMsgbaseInThisFunc = false;
-	if ((pMsgbase != null) && (typeof(pMsgbase) == "object"))
+	if ((pMsgbase != null) && (typeof(pMsgbase) === "object"))
 		msgbase = pMsgbase;
 	else
 	{
@@ -10341,7 +10397,7 @@ function DigDistMsgReader_DisplayEnhancedReaderHelp(pDisplayChgAreaOpt, pDisplay
 			numOfMessages = msgbase.total_msgs;
 			msgbase.close();
 		}
-		console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current area.");
+		console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current sub-board.");
 		console.crlf();
 	}
 	// If there is currently a search (which also includes personal messages),
@@ -10441,6 +10497,8 @@ function DigDistMsgReader_DisplayEnhancedReaderHelp(pDisplayChgAreaOpt, pDisplay
 	}
 	keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.showVotes + "                \x01g: \x01n\x01cShow vote (tally) stats for the message");
 	keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.quit + "                \x01g: \x01n\x01cQuit back to the BBS");
+	if (this.indexedMode)
+		keyHelpLines.push(" \x01n\x01cCurrently in indexed mode; quitting will quit back to the index list.");
 	for (var idx = 0; idx < keyHelpLines.length; ++idx)
 	{
 		console.print("\x01n" + keyHelpLines[idx]);
@@ -12006,7 +12064,7 @@ function DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIn
 					while (!isReadableMsgHdr(msgHeader, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code) && (msgIdx >= 0))
 					  msgHeader = msgBase.get_msg_header(true, --msgIdx, true);
 					if (this.msgAreaList_lastImportedMsg_showImportTime)
-						subBoardInfo.newestPostDate = msgHeader.when_imported_time
+						subBoardInfo.newestPostDate = msgHeader.when_imported_time;
 					else
 					{
 						//subBoardInfo.newestPostDate = msgHeader.when_written_time;
@@ -13038,7 +13096,7 @@ function DigDistMsgReader_GetScanPtrMsgIdx()
 	// the user hasn't read messages in the sub-board yet.  In that case,
 	// just use 0.  Otherwise, get the user's scan pointer message index.
 	var msgIdx = 0;
-	// If pMsgNum is 4294967295 (0xffffffff, or ~0), that is a special value
+	// Check for 4294967295 (0xffffffff, or ~0): That is a special value
 	// for the user's scan_ptr meaning it should point to the latest message
 	// in the messagebase.
 	if (msg_area.sub[this.subBoardCode].scan_ptr != 0xffffffff)
@@ -13769,15 +13827,12 @@ function DigDistMsgReader_DoUserSettings_Scrollable(pDrawBottomhelpLineFn)
 
 	// Save the user's current settings so that we can check them later to see if any
 	// of them changed, in order to determine whether to save the user's settings file.
-	var gOriginalUseEnhScrollbar = this.useEnhReaderScrollbar;
-	/*
 	var originalSettings = {};
-	for (var prop in gUserSettings)
+	for (var prop in this.userSettings)
 	{
-		if (gUserSettings.hasOwnProperty(prop))
-			originalSettings[prop] = gUserSettings[prop];
+		if (this.userSettings.hasOwnProperty(prop))
+			originalSettings[prop] = this.userSettings[prop];
 	}
-	*/
 
 	// Create the user settings box
 	var optBoxTitle = "Setting                                      Enabled";
@@ -13803,12 +13858,17 @@ function DigDistMsgReader_DoUserSettings_Scrollable(pDrawBottomhelpLineFn)
 	// Add the options to the option box
 	const checkIdx = 48;
 	const ENH_SCROLLBAR_OPT_INDEX = optionBox.addTextItem("Scrollbar in reader                            [ ]");
-	if (this.useEnhReaderScrollbar)
+	if (this.userSettings.useEnhReaderScrollbar)
 		optionBox.chgCharInTextItem(ENH_SCROLLBAR_OPT_INDEX, checkIdx, CHECK_CHAR);
 
+	const INDEXED_MODE_NEWSCAN_OPT_INDEX = optionBox.addTextItem("Use indexed mode for newscan                   [ ]");
+	if (this.userSettings.useIndexedModeForNewscan)
+		optionBox.chgCharInTextItem(INDEXED_MODE_NEWSCAN_OPT_INDEX, checkIdx, CHECK_CHAR);
+
 	// Create an object containing toggle values (true/false) for each option index
 	var optionToggles = {};
-	optionToggles[ENH_SCROLLBAR_OPT_INDEX] = this.useEnhReaderScrollbar;
+	optionToggles[ENH_SCROLLBAR_OPT_INDEX] = this.userSettings.useEnhReaderScrollbar;
+	optionToggles[INDEXED_MODE_NEWSCAN_OPT_INDEX] = this.userSettings.useIndexedModeForNewscan;
 
 	// Other actions
 	var USER_TWITLIST_OPT_INDEX = optionBox.addTextItem("Personal twit list");
@@ -13835,7 +13895,10 @@ function DigDistMsgReader_DoUserSettings_Scrollable(pDrawBottomhelpLineFn)
 				switch (itemIndex)
 				{
 					case ENH_SCROLLBAR_OPT_INDEX:
-						this.readerObj.useEnhReaderScrollbar = !this.readerObj.useEnhReaderScrollbar;
+						this.readerObj.userSettings.useEnhReaderScrollbar = !this.readerObj.userSettings.useEnhReaderScrollbar;
+						break;
+					case INDEXED_MODE_NEWSCAN_OPT_INDEX:
+						this.readerObj.userSettings.useIndexedModeForNewscan = !this.readerObj.userSettings.useIndexedModeForNewscan;
 						break;
 					default:
 						break;
@@ -13872,13 +13935,11 @@ function DigDistMsgReader_DoUserSettings_Scrollable(pDrawBottomhelpLineFn)
 	// If the user changed any of their settings, then save the user settings.
 	// If the save fails, then output an error message.
 	var settingsChanged = false;
-	settingsChanged = (gOriginalUseEnhScrollbar != this.useEnhReaderScrollbar);
 	for (var prop in this.userSettings)
 	{
 		if (this.userSettings.hasOwnProperty(prop))
 		{
-			// TODO
-			//settingsChanged = settingsChanged || (originalSettings[prop] != this.userSettings[prop]);
+			settingsChanged = settingsChanged || (originalSettings[prop] != this.userSettings[prop]);
 			if (settingsChanged)
 				break;
 		}
@@ -13925,19 +13986,27 @@ function DigDistMsgReader_DoUserSettings_Traditional()
 
 	console.crlf();
 	console.print("\x01n\x01c\x01hU\x01n\x01cser \x01hS\x01n\x01cettings\x01n\r\n");
-	console.print("\x01c\x01h1\x01g: \x01n\x01c\x01hP\x01n\x01cersonal \x01ht\x01n\x01cwit list\x01n\r\n");
-	var USER_TWITLIST_OPT_NUM = 1;
+	console.print("\x01c\x01h1\x01g: \x01n\x01c\x01hU\x01n\x01cse \x01hI\x01n\x01cndexed \x01hm\x01n\x01code \x01hf\x01n\x01cor \x01hn\x01n\x01cewscan\x01n\r\n");
+	console.print("\x01c\x01h2\x01g: \x01n\x01c\x01hP\x01n\x01cersonal \x01ht\x01n\x01cwit list\x01n\r\n");
+	var USER_INDEXED_MODE_FOR_NEWSCAN_OPT_NUM = 1;
+	var USER_TWITLIST_OPT_NUM = 2;
 	var HIGHEST_CHOICE_NUM = USER_TWITLIST_OPT_NUM;
 	console.crlf();
 	console.print("\x01cYour choice (\x01hQ\x01n\x01c: Quit)\x01h: \x01g");
 	var userChoiceNum = console.getnum(HIGHEST_CHOICE_NUM);
-	console.print("\x01n");
+	console.attributes = "N";
 	var userChoiceStr = userChoiceNum.toString().toUpperCase();
 	if (userChoiceStr.length == 0 || userChoiceStr == "Q")
 		return retObj;
 
+	var userSettingsChanged = false;
 	switch (userChoiceNum)
 	{
+		case USER_INDEXED_MODE_FOR_NEWSCAN_OPT_NUM:
+			var oldIndexedModeNewscanSetting = this.userSettings.useIndexedModeForNewscan;
+			this.userSettings.useIndexedModeForNewscan = !console.noyes("Use indexed mode for newscan-all");
+			userSettingsChanged = (this.userSettings.useIndexedModeForNewscan != oldIndexedModeNewscanSetting);
+			break;
 		case USER_TWITLIST_OPT_NUM:
 			console.editfile(gUserTwitListFilename);
 			// Re-read the user's twitlist and see if the user's twitlist changed
@@ -13949,92 +14018,533 @@ function DigDistMsgReader_DoUserSettings_Traditional()
 			break;
 	}
 
-	// For future changes, if any settings that apply to the traditional interface:
-	/*
-	if (!WriteUserSettingsFile())
+	// If any user settings changed, then write them to the user settings file
+	if (userSettingsChanged)
 	{
-		console.print("\x01n\r\n\x01y\x01hFailed to save settings!\x01n");
-		console.crlf();
-		console.pause();
+		if (!this.WriteUserSettingsFile())
+		{
+			console.print("\x01n\r\n\x01y\x01hFailed to save settings!\x01n");
+			console.crlf();
+			console.pause();
+		}
 	}
-	*/
 
 	return retObj;
 }
 
-// For the DigDistMsgReader class: Starts indexed mode
-function DigDistMsgReader_DoIndexedMode()
-{
-	if (this.msgListUseLightbarListInterface && canDoHighASCIIAndANSI())
-		this.DoIndexedModeLightbar();
-	else
-		this.DoIndexedModeTraditional();
-}
+///////////////////////////////////////////////////////////////
+// Stuff for indexed reader mode
 
-function DigDistMsgReader_DoIndexedModeLightbar()
+// For the DigDistMsgReader class: Starts indexed mode
+//
+// Parameters:
+//  pScanScope: Numeric - Whether to scan the current sub-board, group, or all.
+//              This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL.
+function DigDistMsgReader_DoIndexedMode(pScanScope)
 {
-	// Stuff for DDMsgReader "Indexed" reader mode:
+	// GitLab issue for DDMsgReader "Indexed" reader mode:
 	// https://gitlab.synchro.net/main/sbbs/-/issues/354
-	for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx)
+	
+	var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL);
+
+	this.indexedMode = true;
+
+	// msgHdrsCache is used to prevent long loading again when loading a sub-board
+	// a 2nd time or later. Only if this.enableIndexedModeMsgListCache is true.
+	var msgHdrsCache = {};
+
+	var clearScreenForMenu = true;
+	var drawMenu = true;
+	var writeBottomHelpLine = true;
+	var continueOn = true;
+	while (continueOn)
 	{
-		console.print(msg_area.grp_list[grpIdx].name + " - " + msg_area.grp_list[grpIdx].description + ":\r\n");
-		for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx)
+		// Let the user choose a sub-board, and if their choice is valid,
+		// let them read the sub-board.
+		var indexRetObj = this.IndexedModeChooseSubBoard(clearScreenForMenu, drawMenu, writeBottomHelpLine, pScanScope);
+		if (typeof(indexRetObj.chosenSubCode) === "string" && msg_area.sub.hasOwnProperty(indexRetObj.chosenSubCode))
 		{
-			//msg_area.grp_list[grpIdx].sub_list[subIdx].
-			//console.print(" " + msg_area.grp_list[grpIdx].sub_list[subIdx].name + " - " + msg_area.grp_list[grpIdx].sub_list[subIdx].description + "\r\n");
-			// scan_ptr: user's current new message scan pointer (highest-read message number)
-			var displayThisSub = false;
-			// posts: number of messages currently posted to this sub-board (introduced in v3.18c)
-			var totalNumMsgsInSub = msg_area.grp_list[grpIdx].sub_list[subIdx].posts;
-			var newMsgsInSub = 0;
-			if (typeof(msg_area.grp_list[grpIdx].sub_list[subIdx].scan_ptr) === "number")
+			this.subBoardCode = indexRetObj.chosenSubCode;
+			if (!this.enableIndexedModeMsgListCache || !msgHdrsCache.hasOwnProperty(indexRetObj.chosenSubCode))
 			{
-				var msgbase = new MsgBase(msg_area.grp_list[grpIdx].sub_list[subIdx].code);
-				if (msgbase.open())
+				// Display a "Loading..." status text and populate the headers for this sub-board
+				if (console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI) // this.msgListUseLightbarListInterface
 				{
-					//displayThisSub = (msg_area.grp_list[grpIdx].sub_list[subIdx].scan_ptr < msgbase.last_msg);
-					if (msg_area.grp_list[grpIdx].sub_list[subIdx].scan_ptr < msgbase.last_msg)
-					{
-						displayThisSub = true;
-						var msgIdx = msgbase.get_msg_index(false, msg_area.grp_list[grpIdx].sub_list[subIdx].scan_ptr, false);
-						if (msgIdx != null)
-						{
-							newMsgsInSub = msg_area.grp_list[grpIdx].sub_list[subIdx].posts - msgIdx.offset;
-							if (newMsgsInSub < 0) newMsgsInSub = 0;
-						}
-					}
-					msgbase.close();
+					console.gotoxy(1, console.screen_rows);
+					console.cleartoeol("\x01n");
+					console.print("\x01n\x01cLoading\x01h...\x01n");
 				}
+				this.PopulateHdrsForCurrentSubBoard();
+			}
+			else
+			{
+				this.hdrsForCurrentSubBoard = msgHdrsCache[indexRetObj.chosenSubCode].hdrsForCurrentSubBoard;
+				this.hdrsForCurrentSubBoardByMsgNum = msgHdrsCache[indexRetObj.chosenSubCode].hdrsForCurrentSubBoardByMsgNum;
+			}
+			var numMessages = this.NumMessages(null, true);
+			var startIdx = numMessages - indexRetObj.numNewMsgs;
+			if (startIdx < 0)
+				startIdx = numMessages - 1;
+			// Let the user read the sub-board
+			// pSubBoardCode, pStartingMsgOffset, pReturnOnMessageList, pAllowChgArea, pReturnOnNextAreaNav,
+			// pPromptToGoToNextAreaIfNoSearchResults
+			var readRetObj = this.ReadMessages(indexRetObj.chosenSubCode, startIdx, false, false, true, false);
+			// Update the text for the current menu item to ensure the message numbers are up to date
+			var currentMenuItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx);
+			var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(indexRetObj.chosenSubCode);
+			currentMenuItem.text = itemInfo.itemText;
+			currentMenuItem.retval.numNewMsgs = itemInfo.numNewMsgs;
+			// If enabled, store the message headers for this sub-board as a cache for performance
+			if (this.enableIndexedModeMsgListCache)
+			{
+				msgHdrsCache[indexRetObj.chosenSubCode] = {
+					hdrsForCurrentSubBoard: this.hdrsForCurrentSubBoard,
+					hdrsForCurrentSubBoardByMsgNum: this.hdrsForCurrentSubBoardByMsgNum
+				};
 			}
 			/*
-			// last_read: user's last-read message number
-			if (typeof(msg_area.grp_list[grpIdx].sub_list[subIdx].last_read) === "number")
+			switch (readRetObj.lastAction)
 			{
-				
+				case ACTION_QUIT:
+					//continueOn = false;
+					break;
+				default:
+					break;
 			}
 			*/
-			if (displayThisSub)
+		}
+		else
+		{
+			// On ? keypress, show the help screen. Otherwise, quit.
+			if (indexRetObj.lastUserInput == "?")
 			{
-				var descWidth = 50;
-				var numMsgsWidth = 5;
-				var numNewMsgsWidth = 5;
-				var formatStr = "%-" + descWidth + "s %" + numMsgsWidth + "s %" + numNewMsgsWidth + "s";
-				var subDesc = msg_area.grp_list[grpIdx].sub_list[subIdx].name + " - " + msg_area.grp_list[grpIdx].sub_list[subIdx].description;
-				subDesc = subDesc.substr(0, descWidth);
-				printf(formatStr + "\r\n", "Description", "Total", "New");
-				printf(formatStr + "\r\n", subDesc, totalNumMsgsInSub, newMsgsInSub);
-				console.crlf();
+				this.ShowIndexedListHelp();
+				drawMenu = true;
+				clearScreenForMenu = true;
+				writeBottomHelpLine = true;
+			}
+			// Ctrl-U: User settings
+			else if (indexRetObj.lastUserInput == CTRL_U)
+			{
+				drawMenu = false;
+				clearScreenForMenu = false;
+				writeBottomHelpLine = false;
+				if (console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI) // pReader.msgListUseLightbarListInterface
+				{
+					var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) {
+						var usingANSI = console.term_supports(USER_ANSI) && pReader.indexedModeMenu.allowANSI; // pReader.msgListUseLightbarListInterface
+						if (usingANSI)
+						{
+							// Make sure the help line is built, if not already
+							pReader.MakeIndexedModeHelpLine();
+							// Display the help line at the bottom of the screen
+							console.gotoxy(1, console.screen_rows);
+							console.attributes = "N";
+							console.putmsg(pReader.indexedModeHelpLine); // console.putmsg() can process @-codes, which we use for mouse click tracking
+							console.attributes = "N";
+						}
+					});
+					if (userSettingsRetObj.needWholeScreenRefresh)
+					{
+						drawMenu = true;
+						clearScreenForMenu = true;
+						writeBottomHelpLine = true;
+					}
+					else
+					{
+						this.indexedModeMenu.DrawPartialAbs(userSettingsRetObj.optionBoxTopLeftX,
+						                                    userSettingsRetObj.optionBoxTopLeftY,
+						                                    userSettingsRetObj.optionBoxWidth,
+						                                    userSettingsRetObj.optionBoxHeight);
+					}
+				}
+				else
+					this.DoUserSettings_Traditional();
+			}
+			else
+				continueOn = false;
+		}
+	}
+
+	this.indexedMode = false;
+}
+
+// For indexed mode: Displays any sub-boards with new messages and lets the user choose one
+//
+// Parameters:
+//  pClearScreen: Whether or not to clear the screen. Defaults to true.
+//  pDrawMenu: Whether or not to draw the menu. Defaults to true.
+//  pDisplayHelpLine: Whether or not to draw the help line at the bottom of the screen. Defaults to true.
+//  pScanScope: Numeric - Whether to scan the current sub-board, group, or all.
+//              This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL.
+//
+// Return value: An object containing the following values:
+//               chosenSubCode: The user's chosen sub-board code; if none selected, this will be null
+//               numNewMsgs: The number of new messages in the chosen sub-board
+function DigDistMsgReader_IndexedModeChooseSubBoard(pClearScreen, pDrawMenu, pDisplayHelpLine, pScanScope)
+{
+	var retObj = {
+		chosenSubCode: null,
+		numNewMsgs: 0,
+		lastUserInput: ""
+	};
+
+	var clearScreen = (typeof(pClearScreen) === "boolean" ? pClearScreen : true);
+	var drawMenu = (typeof(pDrawMenu) === "boolean" ? pDrawMenu : true);
+	var displayHelpLine = (typeof(pDisplayHelpLine) === "boolean" ? pDisplayHelpLine : true);
+
+	var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL);
+
+	// Note: DDlightbarMenu now supports non-ANSI terminals with a more traditional UI
+	// of listing the items and letting the user choose one by typing its number.
+
+	// Set text widths for the menu items
+	var newMsgWidthObj = findWidestNumMsgsAndNumNewMsgs(scanScope);
+	var numMsgsWidth = newMsgWidthObj.widestNumMsgs;
+	var numNewMsgsWidth = newMsgWidthObj.widestNumNewMsgs;
+	var lastPostDateWidth = 10;
+	this.indexedModeItemDescWidth = console.screen_columns - numMsgsWidth - numNewMsgsWidth - lastPostDateWidth - 4;
+	this.indexedModeSubBoardMenuFormatStrNumbers = "%-" + this.indexedModeItemDescWidth + "s %" + numMsgsWidth + "d %" + numNewMsgsWidth + "d %" + lastPostDateWidth + "s";
+	if (typeof(this.indexedModeMenu) !== "object")
+		this.indexedModeMenu = this.MakeLightbarIndexedModeMenu(numMsgsWidth, numNewMsgsWidth, lastPostDateWidth, this.indexedModeItemDescWidth, this.indexedModeSubBoardMenuFormatStrNumbers);
+
+	for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx)
+	{
+		// If scanning the user's current group or sub-board and this is the wrong group, then skip this group.
+		if ((scanScope == SCAN_SCOPE_GROUP || scanScope == SCAN_SCOPE_SUB_BOARD) && bbs.curgrp != grpIdx)
+			continue;
+
+		var grpNameItemAddedToMenu = false;
+		for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx)
+		{
+			if (!msg_area.grp_list[grpIdx].sub_list[subIdx].can_read)
+				continue;
+			if ((msg_area.grp_list[grpIdx].sub_list[subIdx].scan_cfg & SCAN_CFG_NEW) == 0)
+				continue;
+			// If scanning the user's current sub-board and this is the wrong sub-board, then
+			// skip this sub-board (the other groups should have been skipped in the outer loop).
+			if (scanScope == SCAN_SCOPE_SUB_BOARD && bbs.cursub != subIdx)
+				continue;
+
+			if (!grpNameItemAddedToMenu)
+			{
+				//var grpDesc = msg_area.grp_list[grpIdx].name + " - " + msg_area.grp_list[grpIdx].description;
+				var grpDesc = msg_area.grp_list[grpIdx].name;
+				if (msg_area.grp_list[grpIdx].name != msg_area.grp_list[grpIdx].description)
+					grpDesc += " - " + msg_area.grp_list[grpIdx].description;
+				var menuItemText = "\x01n\x01b";
+				for (var i = 0; i < 5; ++i)
+					menuItemText += HORIZONTAL_SINGLE;
+				menuItemText += "\x01y\x01h ";
+				menuItemText += grpDesc;
+				var menuItemLen = console.strlen(menuItemText);
+				if (menuItemLen < this.indexedModeMenu.size.width)
+				{
+					menuItemText += " \x01n\x01b";
+					var numChars = this.indexedModeMenu.size.width - menuItemLen - 1;
+					menuItemText += charStr(HORIZONTAL_SINGLE, numChars);
+				}
+				this.indexedModeMenu.Add(menuItemText, null, null, false); // Not selectable
+				grpNameItemAddedToMenu = true;
 			}
+
+			var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(msg_area.grp_list[grpIdx].sub_list[subIdx].code);
+			this.indexedModeMenu.Add(itemInfo.itemText, {
+				subCode: msg_area.grp_list[grpIdx].sub_list[subIdx].code,
+				numNewMsgs: itemInfo.numNewMsgs
+			});
+		}
+	}
+
+	// Clear the screen, if desired
+	if (clearScreen)
+		console.clear("\x01n");
+
+	// If using ANSI, then display the help line at the bottom of the scren
+	var usingANSI = console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI; // this.msgListUseLightbarListInterface
+	if (usingANSI && displayHelpLine)
+	{
+		// Make sure the help line is built, if not already
+		this.MakeIndexedModeHelpLine();
+		// Display the help line at the bottom of the screen
+		console.gotoxy(1, console.screen_rows);
+		console.attributes = "N";
+		console.putmsg(this.indexedModeHelpLine); // console.putmsg() can process @-codes, which we use for mouse click tracking
+		console.attributes = "N";
+	}
+
+	// Show the menu and let the user make a choice
+	if (usingANSI)
+		console.gotoxy(1, 1);
+	var dateWidth = (this.indexedModeMenu.CanShowAllItemsInWindow() ? lastPostDateWidth : lastPostDateWidth-1);
+	var formatStrStrs = "%-" + (this.indexedModeItemDescWidth-1) + "s %" + numMsgsWidth + "s %" + numNewMsgsWidth + "s %" + dateWidth + "s";
+	printf(formatStrStrs, "Description", "Total", "New", "Last Post");
+	console.attributes = "N";
+	if (!usingANSI) console.crlf();
+	var menuRetval = this.indexedModeMenu.GetVal(drawMenu);
+	retObj.lastUserInput = this.indexedModeMenu.lastUserInput;
+	if (menuRetval != null)
+	{
+		retObj.chosenSubCode = menuRetval.subCode;
+		retObj.numNewMsgs = menuRetval.numNewMsgs;
+	}
+	console.attributes = "N";
+	return retObj;
+}
+
+// Returns a string to use for a sub-board for the indexed mode sub-board menu
+//
+// Parameters:
+//  pSubCode: The internal code of the sub-board
+//
+// Return value: An object containing the following properties:
+//               itemText: A string for the indexed mode menu item for the sub-board
+//               numNewMsgs: The number of new messages in the sub-board
+function DigDistMsgReader_GetIndexedModeSubBoardMenuItemTextAndInfo(pSubCode)
+{
+	var retObj = {
+		itemText: "",
+		numNewMsgs: 0
+	};
+
+	if (typeof(this.indexedModeSubBoardMenuFormatStrNumbers) !== "string" || typeof(this.indexedModeItemDescWidth) !== "number")
+		return retObj;
+
+	// posts: number of messages currently posted to this sub-board (introduced in v3.18c)
+	var totalNumMsgsInSub = msg_area.sub[pSubCode].posts;
+	var latestPostInfo = getLatestPostTimestampAndNumNewMsgs(pSubCode);
+	var lastPostDate = strftime("%Y-%m-%d", latestPostInfo.latestMsgTimestamp);
+	var subDesc = (latestPostInfo.numNewMsgs > 0 ? "NEW " : "    ");
+	subDesc += msg_area.sub[pSubCode].name;
+	if (msg_area.sub[pSubCode].name !== msg_area.sub[pSubCode].description)
+		subDesc += " - " + msg_area.sub[pSubCode].description;
+	subDesc = subDesc.substr(0, this.indexedModeItemDescWidth);
+	retObj.itemText = format(this.indexedModeSubBoardMenuFormatStrNumbers, subDesc, totalNumMsgsInSub, latestPostInfo.numNewMsgs, lastPostDate);
+	retObj.numNewMsgs = latestPostInfo.numNewMsgs;
+	return retObj;
+}
+
+// Builds the indexed mode help line (for the bottom of the screen) if it's not already
+// built yet
+function DigDistMsgReader_MakeIndexedModeHelpLine()
+{
+	// If it's already built, then just return now
+	if (typeof(this.indexedModeHelpLine) === "string")
+		return;
+
+	this.indexedModeHelpLine = this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/"
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`ENTER`" + KEY_ENTER + "@"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`END`" + KEY_END + "@"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`Ctrl-U`" + CTRL_U + "@"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`Q`Q@"
+	                         + this.colors.lightbarIndexedModeHelpLineParenColor + ")"
+	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + "uit, "
+	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`?`?@";
+	// Add spaces so that the text is centered on the screen
+	var helpLineLen = 49; // 41 without Ctrl-U
+	var leftSideNumChars = Math.floor(console.screen_columns / 2) - Math.floor(helpLineLen / 2);
+	var rightSideNumChars = leftSideNumChars;
+	var totalNumChars = leftSideNumChars + rightSideNumChars + helpLineLen;
+	var maxLen = console.screen_columns - 1;
+	if (totalNumChars > maxLen)
+		rightSideNumChars -= (totalNumChars - maxLen);
+	else if (totalNumChars < maxLen)
+		rightSideNumChars += (maxLen - totalNumChars);
+	this.indexedModeHelpLine = "\x01n" + this.colors.lightbarIndexedModeHelpLineBkgColor + format("%*s", leftSideNumChars, "")
+	                         + this.indexedModeHelpLine + format("%*s", rightSideNumChars, "");
+}
+
+// Shows help for the indexed mode list
+function DigDistMsgReader_ShowIndexedListHelp()
+{
+	console.clear("\x01n");
+	DisplayProgramInfo();
+	console.attributes = "N";
+	console.print(this.colors.tradInterfaceHelpScreenColor);
+	console.print("The current mode is Indexed Mode, which shows total and new messages for any\r\n");
+	console.print("sub-boards in your newscan configuration. You may choose a sub-board from this\r\n");
+	console.print("list to read messages in that sub-board.\r\n");
+	console.crlf();
+	if (console.term_supports(USER_ANSI) && this.msgListUseLightbarListInterface)
+	{
+		console.crlf();
+		displayTextWithLineBelow("Summary of the keyboard commands:", false, this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
+		console.print(this.colors.tradInterfaceHelpScreenColor);
+		console.print("\x01n\x01h\x01cDown arrow" + this.colors.tradInterfaceHelpScreenColor + ": Move the cursor down/select the next message\r\n");
+		console.print("\x01n\x01h\x01cUp arrow" + this.colors.tradInterfaceHelpScreenColor + ": Move the cursor up/select the previous message\r\n");
+		console.print("\x01n\x01h\x01cPageDown" + this.colors.tradInterfaceHelpScreenColor + ": Go to the next page\r\n");
+		console.print("\x01n\x01h\x01cPageUp" + this.colors.tradInterfaceHelpScreenColor + ": Go to the previous page\r\n");
+		console.print("\x01n\x01h\x01cHOME" + this.colors.tradInterfaceHelpScreenColor + ": Go to the first item\r\n");
+		console.print("\x01n\x01h\x01cEND" + this.colors.tradInterfaceHelpScreenColor + ": Go to the last item\r\n");
+		console.print("\x01n\x01h\x01cCtrl-U" + this.colors.tradInterfaceHelpScreenColor + ": User settings\r\n");
+		console.print("\x01n\x01h\x01cQ" + this.colors.tradInterfaceHelpScreenColor + ": Quit\r\n");
+		//console.print("\x01n\x01h\x01c?" + this.colors.tradInterfaceHelpScreenColor + ": Show this help screen\r\n");
+	}
+	console.pause();
+	console.aborted = false;
+}
+
+// Returns an object with the widest text length of the number of new messsages and
+// number of new-to-you messages in all readable sub-boards
+//
+// Parameters:
+//  pScanScope: Numeric - Whether to scan the current sub-board, group, or all.
+//              This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL.
+//
+// Return value: An object with the following properties:
+//               widestNumMsgs: The biggest length of the number of messages in the sub-boards
+//               widestNumNewMsgs: The biggest length of the number of new (unread) messages in the sub-boards
+function findWidestNumMsgsAndNumNewMsgs(pScanScope)
+{
+	var retObj = {
+		widestNumMsgs: 0,
+		widestNumNewMsgs: 0
+	};
+
+	var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL);
+
+	for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx)
+	{
+		// If scanning the user's current group or sub-board and this is the wrong group, then skip this group.
+		if ((scanScope == SCAN_SCOPE_GROUP || scanScope == SCAN_SCOPE_SUB_BOARD) && bbs.curgrp != grpIdx)
+			continue;
+
+		for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx)
+		{
+			if (!msg_area.grp_list[grpIdx].sub_list[subIdx].can_read)
+				continue;
+			if ((msg_area.grp_list[grpIdx].sub_list[subIdx].scan_cfg & SCAN_CFG_NEW) == 0)
+				continue;
+			// If scanning the user's current sub-board and this is the wrong sub-board, then
+			// skip this sub-board (the other groups should have been skipped in the outer loop).
+			if (scanScope == SCAN_SCOPE_SUB_BOARD && bbs.cursub != subIdx)
+				continue;
+
+			var totalNumMsgsInSub = msg_area.grp_list[grpIdx].sub_list[subIdx].posts;
+			var totalNumMsgsInSubLen = totalNumMsgsInSub.toString().length;
+			if (totalNumMsgsInSubLen > retObj.widestNumMsgs)
+				retObj.widestNumMsgs = totalNumMsgsInSubLen;
+			var latestPostInfo = getLatestPostTimestampAndNumNewMsgs(msg_area.grp_list[grpIdx].sub_list[subIdx].code);
+			var numNewMessagesInSubLen = latestPostInfo.numNewMsgs.toString().length;
+			if (numNewMessagesInSubLen > retObj.widestNumNewMsgs)
+				retObj.widestNumNewMsgs = numNewMessagesInSubLen;
 		}
 	}
+	return retObj;
 }
 
-function DigDistMsgReader_DoIndexedModeTraditional()
+// Gets the timestamp of the latest post in a sub-board and number of new messages (unread
+// to the user) in a sub-board (based on the user's scan_ptr).
+//
+// Parameters:
+//  pSubCode: The internal code of a sub-board to check
+//
+// Return value: An object with the following properties:
+//               latestMsgTimestamp: The timestamp of the latest post in the sub-board
+//               numnewMsgs: The number of new messages (unread to the user) in the sub-board
+function getLatestPostTimestampAndNumNewMsgs(pSubCode)
 {
+	var retObj = {
+		latestMsgTimestamp: 0,
+		numNewMsgs: 0
+	};
+
+	var msgbase = new MsgBase(pSubCode);
+	if (msgbase.open())
+	{
+		retObj.latestMsgTimestamp = getLatestPostTimeWithMsgbase(msgbase, pSubCode);
+		//var totalNumMsgs = msgbase.total_msgs;
+		msgbase.close();
+		// scan_ptr: user's current new message scan pointer (highest-read message number)
+		if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number")
+		{
+			var lastNonDeletedMsgHdr = GetLastNonDeletedMsgHdr(pSubCode);
+			if (lastNonDeletedMsgHdr != null)
+			{
+				// Check for 4294967295 (0xffffffff, or ~0): That is a special value
+				// for the user's scan_ptr meaning it should point to the latest message
+				// in the messagebase.
+				if (msg_area.sub[pSubCode].scan_ptr != 0xffffffff)
+					retObj.numNewMsgs = lastNonDeletedMsgHdr.number - msg_area.sub[pSubCode].scan_ptr;
+			}
+		}
+		else if (typeof(msg_area.sub[pSubCode].last_read) === "number")
+		{
+			var lastNonDeletedMsgHdr = GetLastNonDeletedMsgHdr(pSubCode);
+			if (lastNonDeletedMsgHdr != null)
+				retObj.numNewMsgs = lastNonDeletedMsgHdr.number - msg_area.sub[pSubCode].last_read;
+		}
+		else
+			retObj.numNewMsgs = msg_area.sub[pSubCode].posts;
+		if (retObj.numNewMsgs < 0)
+			retObj.numNewMsgs = 0;
+	}
+	return retObj;
 }
 
-function DigDistMsgReader_MakeLightbarIndexedModeMenu()
+// Makes the DDLightbarMenu object for indexed reader mode
+function DigDistMsgReader_MakeLightbarIndexedModeMenu(pNumMsgsWidth, pNumNewMsgsWidth, pLastPostDateWidth, pDescWidth)
 {
+	// Start & end indexes for the selectable items
+	var indexMenuIdxes = {
+		descStart: 0,
+		descEnd: pDescWidth+1,
+		totalStart: pDescWidth+1,
+		totalEnd: pDescWidth+pNumMsgsWidth+2,
+		newMsgsStart: pDescWidth+1+pNumMsgsWidth+1,
+		newMsgsEnd: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+2,
+		lastPostDateStart: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+2,
+		lastPostDateEnd: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+pLastPostDateWidth+3
+	};
+
+	//var menuHeight = 12;
+	// For the menu height, -2 gives one row at the top for the column headers and one row
+	// at the bottom for the help line
+	var menuHeight = console.screen_rows - 2;
+
+	var indexedModeMenu = new DDLightbarMenu(1, 2, console.screen_columns, menuHeight);
+	indexedModeMenu.allowUnselectableItems = true;
+	indexedModeMenu.scrollbarEnabled = true;
+	indexedModeMenu.borderEnabled = false;
+	// Colors:
+	var descHigh = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuDescHighlight;
+	var totalMsgsHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuTotalMsgsHighlight;
+	var numNewMsgsHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuNumNewMsgsHighlight;
+	var lastPostDateHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuLastPostDateHighlight;
+	indexedModeMenu.SetColors({
+		itemColor: [{start: indexMenuIdxes.descStart, end: indexMenuIdxes.descEnd, attrs: "\x01n" + this.colors.indexMenuDesc},
+		            {start: indexMenuIdxes.totalStart, end: indexMenuIdxes.totalEnd, attrs: "\x01n" + this.colors.indexMenuTotalMsgs},
+		            {start: indexMenuIdxes.newMsgsStart, end: indexMenuIdxes.newMsgsEnd, attrs: "\x01n" + this.colors.indexMenuNumNewMsgs},
+		            {start: indexMenuIdxes.lastPostDateStart, end: indexMenuIdxes.lastPostDateEnd, attrs: "\x01n" + this.colors.indexMenuLastPostDate}],
+		selectedItemColor: [{start: indexMenuIdxes.descStart, end: indexMenuIdxes.descEnd, attrs: descHigh},
+		                    {start: indexMenuIdxes.totalStart, end: indexMenuIdxes.totalEnd, attrs: totalMsgsHi},
+		                    {start: indexMenuIdxes.newMsgsStart, end: indexMenuIdxes.newMsgsEnd, attrs: numNewMsgsHi},
+		                    {start: indexMenuIdxes.lastPostDateStart, end: indexMenuIdxes.lastPostDateEnd, attrs: lastPostDateHi}],
+		unselectableItemColor: ""
+	});
+
+	indexedModeMenu.multiSelect = false;
+	indexedModeMenu.ampersandHotkeysInItems = false;
+	indexedModeMenu.wrapNavigation = false;
+	indexedModeMenu.allowANSI = this.msgListUseLightbarListInterface;
+
+	// Add additional keypresses for quitting the menu's input loop so we can
+	// respond to these keys
+	indexedModeMenu.AddAdditionalQuitKeys("Qq?" + CTRL_U); // Ctrl-U for user settings
+
+	return indexedModeMenu;
 }
 
 // For the DigDistMsgReader class: Writes message lines to a file on the BBS
@@ -15145,14 +15655,22 @@ function DigDistMsgReader_GetUpvoteAndDownvoteInfo(pMsgHdr)
 // Parameters:
 //  pMsgHeader: The message header
 //
-// Return value: The poll results, colorized.  If the message is not a
-//               poll message, then an empty string will be returned.
+// Return value: An object with the following properties:
+//               msgBody: The message body
+//               pmode: The mode flags to be used when printing the message body
 function DigDistMsgReader_GetMsgBody(pMsgHdr)
 {
+	var retObj = {
+		msgBody: "",
+		pmode: 0
+	};
+
 	var msgbase = new MsgBase(this.subBoardCode);
 	if (!msgbase.open())
-		return "";
-	var msgBody = "";
+		return retObj;
+
+	retObj.pmode = msg_pmode(msgbase, pMsgHdr);
+
 	if ((typeof(MSG_TYPE_POLL) != "undefined") && (pMsgHdr.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
 	{
 		// A poll is intended to be parsed (and displayed) using on the header data. The
@@ -15200,7 +15718,7 @@ function DigDistMsgReader_GetMsgBody(pMsgHdr)
 					totalNumVotes += pMsgHdr.tally[tallyI];
 			}
 			// Go through field_list and append the voting options and stats to
-			// msgBody
+			// retObj.msgBody
 			var pollComment = "";
 			var optionNum = 1;
 			var numVotes = 0;
@@ -15223,24 +15741,24 @@ function DigDistMsgReader_GetMsgBody(pMsgHdr)
 						}
 					}
 					// Append to the message text
-					msgBody += format(numVotes == 0 ? unvotedOptionFormatStr : votedOptionFormatStr,
+					retObj.msgBody += format(numVotes == 0 ? unvotedOptionFormatStr : votedOptionFormatStr,
 					                  optionNum++, pMsgHdr.field_list[fieldI].data.substr(0, voteOptDescLen),
 					                  numVotes, votePercentage);
 					if (numVotes > 0)
-						msgBody += " " + CHECK_CHAR;
-					msgBody += "\r\n";
+						retObj.msgBody += " " + CHECK_CHAR;
+					retObj.msgBody += "\r\n";
 					++tallyIdx;
 				}
 			}
 			if (pollComment.length > 0)
-				msgBody = pollComment + "\r\n" + msgBody;
+				retObj.msgBody = pollComment + "\r\n" + retObj.msgBody;
 
 			// If voting is allowed in this sub-board and the current logged-in
 			// user has not voted on this message, then append some text saying
 			// how to vote.
 			var votingAllowed = ((this.subBoardCode != "mail") && (((msg_area.sub[this.subBoardCode].settings & SUB_NOVOTING) == 0)));
 			if (votingAllowed && !this.HasUserVotedOnMsg(pMsgHdr.number))
-				msgBody += "\x01n\r\n\x01gTo vote in this poll, press \x01w\x01h" + this.enhReaderKeys.vote + "\x01n\x01g now.\r\n";
+				retObj.msgBody += "\x01n\r\n\x01gTo vote in this poll, press \x01w\x01h" + this.enhReaderKeys.vote + "\x01n\x01g now.\r\n";
 
 			// If the current logged-in user created this poll, then show the
 			// users who have voted on it so far.
@@ -15276,7 +15794,7 @@ function DigDistMsgReader_GetMsgBody(pMsgHdr)
 							msgbaseCfgName = msgbase.cfg.name;
 							msgbase.close();
 						}
-						msgBody += format(userVotedInYourPollText, voteDate, grpName, msgbaseCfgName, tmpHdrs[tmpProp].from, pMsgHdr.subject);
+						retObj.msgBody += format(userVotedInYourPollText, voteDate, grpName, msgbaseCfgName, tmpHdrs[tmpProp].from, pMsgHdr.subject);
 					}
 				}
 			}
@@ -15286,29 +15804,29 @@ function DigDistMsgReader_GetMsgBody(pMsgHdr)
 	{
 		// If the message is UTF8 and the terminal is not UTF8-capable, then convert
 		// the text to cp437.
-		msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
+		retObj.msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
 		if (pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8)
 		{
 			var userConsoleSupportsUTF8 = false;
 			if (typeof(USER_UTF8) != "undefined")
 				userConsoleSupportsUTF8 = console.term_supports(USER_UTF8);
 			if (!userConsoleSupportsUTF8)
-				msgBody = utf8_cp437(msgBody);
+				retObj.msgBody = utf8_cp437(retObj.msgBody);
 		}
 		// Remove any initial coloring from the message body, which can color the whole message
-		msgBody = removeInitialColorFromMsgBody(msgBody);
+		retObj.msgBody = removeInitialColorFromMsgBody(retObj.msgBody);
 		// For HTML-formatted messages, convert HTML entities
 		if (pMsgHdr.hasOwnProperty("text_subtype") && pMsgHdr.text_subtype.toLowerCase() == "html")
 		{
-			msgBody = html2asc(msgBody);
+			retObj.msgBody = html2asc(retObj.msgBody);
 			// Remove excessive blank lines after HTML-translation
-			msgBody = msgBody.replace(/\r\n\r\n\r\n/g, '\r\n\r\n');
+			retObj.msgBody = retObj.msgBody.replace(/\r\n\r\n\r\n/g, '\r\n\r\n');
 		}
 	}
 	msgbase.close();
 
 	// Remove any Synchronet pause codes that might exist in the message
-	msgBody = msgBody.replace("\x01p", "").replace("\x01P", "");
+	retObj.msgBody = retObj.msgBody.replace("\x01p", "").replace("\x01P", "");
 
 	// If the user is a sysop, this is a moderated message area, and the message
 	// hasn't been validated, then prepend the message with a message to let the
@@ -15322,11 +15840,11 @@ function DigDistMsgReader_GetMsgBody(pMsgHdr)
 			for (var i = 0; i < 79; ++i)
 				validateNotice += HORIZONTAL_SINGLE;
 			validateNotice += "\x01n\r\n";
-			msgBody = validateNotice + msgBody;
+			retObj.msgBody = validateNotice + retObj.msgBody;
 		}
 	}
 
-	return msgBody;
+	return retObj;
 }
 
 // For the DigDistMsgReader class: Refreshes a message header in one of the
@@ -15572,11 +16090,7 @@ function DigDistMsgReader_RefreshMsgAreaRectangle(pTxtLines, pTopLineIdx, pTopLe
 			var lineText = substrWithAttrCodes(pTxtLines[txtLineIdx], txtLineStartIdx, pWidth);
 			var printableTxtLen = console.strlen(lineText);
 			if (printableTxtLen < pWidth)
-			{
-				var lenDiff = pWidth - printableTxtLen;
-				//lineText += format("\x01n%" + lenDiff + "s", "");
-				lineText += format("\x01n%*s", lenDiff, "");
-			}
+				lineText += format("\x01n%*s", pWidth - printableTxtLen, "");
 			console.print(lineText);
 		}
 		else // We've printed all the remaining text lines, so now print an empty string.
@@ -16344,13 +16858,15 @@ function userHandleAliasNameMatch(pName)
 //                   - fractionToLastPage: The fraction of the top index divided
 //                     by the top index for the last page (basically, the progress
 //                     to the last page).
+//  pScrollbarInfo: 
+//  pmode: Optional - Print mode (important for UTF8 info)
 //
 // Return value: An object with the following properties:
 //               lastKeypress: The last key pressed by the user (a string)
 //               topLineIdx: The new top line index of the text lines, in case of scrolling
 function scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTopLeftX, pTopLeftY,
                          pWidth, pHeight, pPostWriteCurX, pPostWriteCurY, pUseScrollbar, pScrollUpdateFn,
-                         pScrollbarInfo)
+                         pScrollbarInfo, pmode)
 {
 	// Variables for the top line index for the last page, scrolling, etc.
 	var topLineIdxForLastPage = pTxtLines.length - pHeight;
@@ -16404,8 +16920,8 @@ function scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTo
 			{
 				console.gotoxy(pTopLeftX, screenY++);
 				// Print the text line, then clear the rest of the line
-				console.print(pTxtAttrib + pTxtLines[lineIdx]);
-				printf("\x01n%" + +(pWidth - console.strlen(pTxtLines[lineIdx])) + "s", "");
+				console.print(pTxtAttrib + pTxtLines[lineIdx], typeof(pmode) === "number" ? pmode|P_NOATCODES : P_NOATCODES);
+				printf("\x01n%*s", pWidth-console.strlen(pTxtLines[lineIdx]), "");
 			}
 			// If there are still some lines left in the message reading area, then
 			// clear the lines.
@@ -17092,7 +17608,7 @@ function parseArgs(argv)
 	var argVals = getDefaultArgParseObj();
 
 	// Sanity checking for argv - Make sure it's an array
-	if ((typeof(argv) != "object") || (typeof(argv.length) != "number"))
+	if (!Array.isArray(argv))
 		return argVals;
 
 	// First, test the arguments to see if they're in a format as called by
@@ -17202,6 +17718,9 @@ function parseArgs(argv)
 //               module arguments were specified.
 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
@@ -19229,34 +19748,45 @@ function strWithToUserColor(pStr, pToUserColor)
 function GetScanPtrOrLastMsgNum(pSubCode)
 {
 	var msgNumToReturn = 0;
-	// If pMsgNum is 4294967295 (0xffffffff, or ~0), that is a special value
+	// Check for 4294967295 (0xffffffff, or ~0): That is a special value
 	// for the user's scan_ptr meaning it should point to the latest message
 	// in the messagebase.
 	if (msg_area.sub[pSubCode].scan_ptr != 0xffffffff)
 		msgNumToReturn = msg_area.sub[pSubCode].scan_ptr;
 	else
 	{
-		var msgbase = new MsgBase(pSubCode);
-		if (msgbase.open())
-		{
-			var numMsgs = msgbase.total_msgs;
-			for (var msgIdx = numMsgs - 1; msgIdx >= 0; --msgIdx)
-			{
-				var msgHdr = msgbase.get_msg_header(true, msgIdx);
-				if ((msgHdr != null) && ((msgHdr.attr & MSG_DELETE) == 0))
-				{
-					msgNumToReturn = msgHdr.number;
-					break;
-				}
-			}
-
-			msgbase.close();
-		}
+		var lastNonDeletedMsgHdr = GetLastNonDeletedMsgHdr(pSubCode);
+		if (lastNonDeletedMsgHdr != null)
+			msgNumToReturn = lastNonDeletedMsgHdr.number;
 	}
 
 	return msgNumToReturn;
 }
 
+// Returns the last non-deleted message header in a sub-board, if available
+//
+// Parameters:
+//  pSubCode: A sub-board internal code
+//
+// Return value: The header of the last non-deleted message in the sub-board, or null if there is none
+function GetLastNonDeletedMsgHdr(pSubCode)
+{
+	var lastReadableMsgHdr = null;
+	var msgbase = new MsgBase(pSubCode);
+	if (msgbase.open())
+	{
+		var numMsgs = msgbase.total_msgs;
+		for (var msgIdx = numMsgs - 1; msgIdx >= 0 && lastReadableMsgHdr == null; --msgIdx)
+		{
+			var msgHdr = msgbase.get_msg_header(true, msgIdx);
+			if ((msgHdr != null) && ((msgHdr.attr & MSG_DELETE) == 0))
+				lastReadableMsgHdr = msgHdr;
+		}
+		msgbase.close();
+	}
+	return lastReadableMsgHdr;
+}
+
 // Returns whether a message header has one of the attachment flags
 // enabled (for Synchtonet 3.17 or newer).
 //
@@ -20615,6 +21145,55 @@ function replaceAtCodesAndRemoveCRLFs(pText)
 	return formattedText;
 }
 
+function getLatestPostTimeWithMsgbase(pMsgbase, pSubCode)
+{
+	if (typeof(pMsgbase) !== "object")
+		return 0;
+	if (!pMsgbase.is_open)
+		return 0;
+
+	var latestMsgTimestamp = 0;
+	if (pMsgbase.total_msgs > 0)
+	{
+		var msgIdx = pMsgbase.total_msgs - 1;
+		var msgHeader = pMsgbase.get_msg_header(true, msgIdx, false);
+		while (!isReadableMsgHdr(msgHeader, pSubCode) && (msgIdx >= 0))
+			msgHeader = pMsgbase.get_msg_header(true, --msgIdx, true);
+		if (this.msgAreaList_lastImportedMsg_showImportTime)
+			latestMsgTimestamp = msgHeader.when_imported_time;
+		else
+		{
+			var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader);
+			if (msgWrittenLocalTime != -1)
+				latestMsgTimestamp = msgWrittenTimeToLocalBBSTime(msgHeader);
+			else
+				latestMsgTimestamp = msgHeader.when_written_time;
+		}
+	}
+	return latestMsgTimestamp;
+}
+
+// Given a messagebase and message header object, this returns a mode flag for
+// use with console.print() to affect the character set based on the terminal.
+function msg_pmode(pMsgbase, pMsgHdr)
+{
+	var pmode = pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8 ? P_UTF8 : P_NONE;
+	if (pMsgHdr.from_ext !== "1")
+		pmode |= P_NOATCODES;
+	if (pMsgbase.cfg)
+	{
+		pmode |= pMsgbase.cfg.print_mode;
+		pmode &= ~pMsgbase.cfg.print_mode_neg;
+	}
+	return pmode;
+}
+
+// Returns whether a value is a valid scan scope value.
+function isValidScanScopeVal(pScanScope)
+{
+	return (typeof(pScanScope) === "number" && (pScanScope == SCAN_SCOPE_SUB_BOARD || pScanScope == SCAN_SCOPE_GROUP || pScanScope == SCAN_SCOPE_ALL));
+}
+
 ///////////////////////////////////////////////////////////////////////////////////
 
 // For debugging: Writes some text on the screen at a given location with a given pause.
diff --git a/xtrn/DDMsgReader/DefaultTheme.cfg b/xtrn/DDMsgReader/DefaultTheme.cfg
index 799a4dbc94d9a88b1163e660447314767c0957ff..cf6255345d1ecf247d53d9ea400d9a9c0796e54b 100644
--- a/xtrn/DDMsgReader/DefaultTheme.cfg
+++ b/xtrn/DDMsgReader/DefaultTheme.cfg
@@ -122,6 +122,28 @@ lightbarMsgListHelpLineHotkeyColor=r
 ; help line in the lightbar message list
 lightbarMsgListHelpLineParenColor=m
 
+; Colors for the indexed mode sub-board menu:
+indexMenuDesc=nw
+indexMenuTotalMsgs=nw
+indexMenuNumNewMsgs=nw
+indexMenuLastPostDate=bh
+; Highlighted/selected:
+indexMenuHighlightBkg=4
+indexMenuDescHighlight=wh
+indexMenuTotalMsgsHighlight=wh
+indexMenuNumNewMsgsHighlight=wh
+indexMenuLastPostDateHighlight=wh
+
+; Colors for the indexed mode help line text:
+; Background
+lightbarIndexedModeHelpLineBkgColor=7
+; Hotkey color
+lightbarIndexedModeHelpLineHotkeyColor=r
+; General text
+lightbarIndexedModeHelpLineGeneralColor=b
+; For ) separating the hotkeys from general text
+lightbarIndexedModeHelpLineParenColor=m
+
 ; Go to message number prompt text - Used for the G key in the message list to
 ; "go to" (move the screen/lightbar selection to) a specified message number
 goToMsgNumPromptText=\x01n\x01cGo to message # (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index f8319e3b1e6b3349c5f17ec0b05b2cbfc0e51bc5..4a05170b4da2b50e9a124b4a864b6a32456cd2a2 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
-                                 Version 1.69
-                           Release date: 2023-03-24
+                                 Version 1.70
+                           Release date: 2023-04-04
 
                                      by
 
@@ -30,8 +30,9 @@ Contents
 6. Configuration file & color/text theme configuration file
    - Main configuration file (DDMsgReader.cfg)
    - Theme configuration file
-7. Drop file for replying to messages with Synchronet message editors
-8. text.dat lines used in Digital Distortion Message Reader
+7. Indexed reader mode
+8. Drop file for replying to messages with Synchronet message editors
+9. text.dat lines used in Digital Distortion Message Reader
 
 
 1. Disclaimer
@@ -139,6 +140,9 @@ confirmation before deleting the messages.
   files.  The configuration files may be placed in the same directory as the
   .js script or in the sbbs/ctrl directory.
 - Allows a personal twit list, editable via user settings (Ctrl-U)
+- Has an "indexed" reader mode, which lists all sub-boards configured for
+  newscan by the user, with total number of messages, number of new messages,
+  and last post date, allowing the user to select a sub-board to read
 
 If a message has been marked for deletion, it will appear in the message list
 with a blinking red asterisk (*) after the message number.
@@ -299,6 +303,12 @@ scan.  Another example is -personalEmail which lets the user read their
 personal email.
 
 The following are the command-line parameters supported by DDMsgReader.js:
+-indexedMode: Starts DDMsgreader in "indexed" reader mode, which lists all
+              sub-boards configured for newscan by the user, with total number
+              of messages, number of new messages, and last post date, allowing
+              the user to select a sub-board to read. This is intended to work
+              if it is the only command-line option.
+
 -search: A search type.  Available options:
  keyword_search: Do a keyword search in message subject/body text (current message area)
  from_name_search: 'From' name search (current message area)
@@ -436,6 +446,9 @@ common message operations.
 - Scan for all messages to the user:
 ?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read -search=to_user_all_scan
 
+- Start in indexed reader mode:
+?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode
+
 - Text (keyword) search in the current sub-board, and list the messages found:
 ?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -startMode=list
 
@@ -713,6 +726,10 @@ prependFowardMsgSubject               Whether or not to prepend the subject for
                                       values are true and false. Defaulse to
                                       true.
 
+enableIndexedModeMsgListCache         For indexed reader mode, whether or not to
+                                      enable caching the message header lists
+                                      for performance
+
 themeFilename                         The name of the configuration file to
                                       use for colors & string settings
 
@@ -811,6 +828,41 @@ msgListDateHighlightColor            Message list highlighted date color (for
 msgListTimeHighlightColor            Message list highlighted time color (for
                                      lightbar mode)
 
+ Colors for the indexed mode sub-board menu:
+indexMenuDesc                        Indexed mode menu item description
+
+indexMenuTotalMsgs                   Indexed mode menu total number of messages
+
+indexMenuNumNewMsgs                  Indexed mode menu number of new messages
+
+indexMenuLastPostDate                Indexed mode menu last post date
+
+indexMenuHighlightBkg                Indexed mode menu highlighted item
+                                     background
+
+indexMenuDescHighlight               Indexed mode menu highlighted description
+
+indexMenuTotalMsgsHighlight          Indexed mode menu highlighted total number
+                                     of messages
+
+indexMenuNumNewMsgsHighlight         Indexed mode menu highlighted number of new
+                                     messages
+
+indexMenuLastPostDateHighlight       Indexed mode menu highlighted last post
+                                     date
+
+Colors for the indexed mode lightbar help line text:
+lightbarIndexedModeHelpLineBkgColor  Indexed help line background
+
+lightbarIndexedModeHelpLineHotkeyColor  Indexed help line hotkey color
+
+lightbarIndexedModeHelpLineGeneralColor Indexed help line general text color
+
+lightbarIndexedModeHelpLineParenColor  Indexed help line - For the ) separating
+                                       the hotkeys from general text
+
+Lightbar message list help line colors:
+
 lightbarMsgListHelpLineBkgColor      Background color for the lightbar message
                                      list help line
 lightbarMsgListHelpLineGeneralColor  Color for the general text in the lightbar
@@ -1084,7 +1136,53 @@ selectedMsgMarkColor                 The color to use for the checkmark for
                                      selected messages (used in the message
                                      list)
 
-7. Drop file for replying to messages with Synchronet message editors
+7. Indexed reader mode
+======================
+"Indexed" reader mode is a new mode that was added to DDMsgReader v1.70.  This
+mode displays a menu/list of message sub-boards within their groups with total
+and number of new messages and allows the user to select one to read.  There is
+also a user setting to allow the user to use this mode for a newscan (rather
+than the traditional newscan) if they wish.
+
+Indexed reader mode may also be started with the -indexedMode command-line
+parameter.  For example, if you are using a JavaScript command shell:
+  bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode");
+With the above command-line parameter, DDMsgReader will show all sub-boards the
+user is allowed to read and which they have in their newscan configuration.
+If the user has enabled indexed mode for newscans, then during a newscan, it
+will show sub-boards based on the user's chosen option for current
+sub-board/group/all.
+  
+This is an example of the sub-board menu that appears in indexed mode - And from
+here, the user can choose a sub-board to read:
+
+Description                                                 Total New Last Post
+───── AgoraNet ────────────────────────────────────────────────────────────────
+    AGN GEN - General Chat                                   1004  0 2023-04-02
+    AGN BBS - BBS Discussion                                 1000  0 2023-01-17
+NEW AGN ART - Art/Demo Scene                                  603  1 2023-04-02
+    AGN DEV - Software Development                            398  0 2021-11-09
+    AGN NIX - Unix/Linux Related                              297  0 2023-04-02
+    AGN L46 - League Scores & Recons                         1000  0 2016-09-10
+NEW AGN TST - Testing Setups                                 2086 10 2023-04-03
+    AGN SYS - Sysops Only                                    1000  0 2023-01-19
+───── FIDO - FidoNet ──────────────────────────────────────────────────────────
+NEW BBS CARNIVAL - BBS Software Chatter                       660  5 2023-04-04
+    BBS INTERNET - DOS/Win/OS2/Unix Internet BBS Applicatio    18  0 2023-03-04
+    CHWARE - Cheepware Support/Discussion                     111  0 2023-03-16
+    CLASSIC COMPUTER - Classic Computers                      191  0 2023-02-10
+    CONSPRCY - Conspiracy Discussions                          59  0 2023-03-14
+    CONTROVERSIAL - Controversial Topics, current events, at    3  0 2023-01-31
+NEW DOORGAMES - BBS Doorgames and discussions                 288  1 2023-04-03
+NEW FUNNY - FUNNY Jokes and Stories                          1184  3 2023-04-04
+    FUTURE4FIDO - Discussion of new and future Fidonet tec    152  0 2023-04-01
+    LINUX BBS - Linux BBSing                                   46  0 2023-04-01
+    LINUX - Linux operating system (OS), a Unix vari         1076  0 2023-04-01
+    LINUX-UBUNTU - The Ubuntu Linux Distribution Discussion    18  0 2023-02-17
+NEW MEMORIES - NOSTALGIA                                     2434  3 2023-04-03
+
+
+8. Drop file for replying to messages with Synchronet message editors
 =====================================================================
 When reading a message, the message lister will write a drop file in the node
 directory, called DDML_SyncSMBInfo.txt, which contains some information about
@@ -1119,7 +1217,7 @@ Note that if you have SlyEdit installed on your BBS, this version of Digital
 Distortion Message Reader (1.00) requires version 1.27 or newer of SlyEdit in
 order for SlyEdit to properly get the correct message from the message lister.
 
-8. text.dat lines used in Digital Distortion Message Reader
+9. text.dat lines used in Digital Distortion Message Reader
 ===========================================================
 This message reader uses the following lines from Synchronet's text.dat file
 (located in the sbbs/ctrl directory):
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index 1a23f4c33cf0112f83fa7dcc3db2405b62b5755c..61aefc7ac37400ac21263de84541c9cd9fe975a9 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,6 +5,10 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.70     2023-04-04   Added "indexed" reader mode, which lists sub-boards with
+                      total and number of new/unread messages and lets the user
+                      read messages in those sub-boards. Also, utf-8 characters
+                      should now be converted properfly for non utf-8 terminals.
 1.69     2023-03-24   Bug fix for deleting multiple selected messages: When
                       updating message headers in the cached arrays, don't try
                       to save them back to the database, because that was