Skip to content
Snippets Groups Projects
DDMsgAreaChooser.js 138 KiB
Newer Older
/* This is a script that lets the user choose a message area,
 * with either a lightbar or traditional user interface.
 *
 * Date       Author          Description
 * ... Trimmed comments ...
 * 2019-08-22 Eric Oulashin   Version 1.18
 *                            Added message area searching.
 *                            Also, improved the time to display sub-boards
 *                            with the latest message date & time.
 * 2019-08-24 Eric Oulashin   Version 1.19
 *                            Fixed a bug with 'Next' search when returning from
 *                            sub-board choosing.
 * 2020-04-19 Eric Oulashin   Version 1.20
 *                            For lightbar mode, it now uses DDLightbarMenu
 *                            instead of using internal lightbar code.
 * 2020-11-01 Eric Oulashin   Version 1.21 Beta
 *                            Working on sub-board collapsing
 * 2022-01-15 Eric Oulashin   Version 1.21
 *                            Finished sub-board collapsing (finally) and releasing
 *                            this version.
 * 2022-02-12 Eric Oulashin   Version 1.22
 *                            Updated the version to match the file area chooser
 * 2022-03-18 Eric Oulashin   Version 1.23
 *                            For sub-board collapsing, if there's only one sub-group,
 *                            then it won't be collapsed.
 *                            Also fixed an issue: Using Q to quit out of the 2nd level
 *                            (sub-board/sub-group) for lightbar mode no longer quits
 *                            out of the chooser altogether.
 * 2022-05-17 Eric Oulashin   Version 1.24
 *                            Fix for search error reporting (probably due to
 *                            mistaken copy & paste in an earlier commit)
 * 2022-06-06 Eric Oulashin   Version 1.25
 *                            Fix for miscolored digit(s) in # messages column in
 *                            the sub-board list when using the lightbar menu
 * 2022-06-11 Eric Oulashin   Version 1.26
 *                            Updated to try to prevent the error "this.subBoardListPrintfInfo[pGrpIdx] is undefined"
 *                            when only choosing a sub-board within the user's current message group.
 * 2022-07-23 Eric Oulashin   Version 1.29
 *                            Re-arranged the help text for lightbar mode to be more consistent with my message reader.
 * 2022-08-19 Eric Oulashin   Version 1.30
 *                            Set the control key pass-thru so that some hotkeys (such as Ctrl-P for PageUp) only
 *                            get caught by this script.
 * 2022-11-04 Eric Oulashin   Version 1.31
 *                            Made use of the 'posts' property in msg_area.sub[sub-code] (or msg_area.grp_list.sub_list)
 *                            for the number of posts without votes
 * 2022-11-07 Eric Oulashin   Version 1.32
 *                            Bug fix for numeric input when choosing a sub-board.  Bug fix for getting the number of
 *                            posts with the traditional user interface.
 * 2023-03-19 Eric Oulashin   Version 1.33
 *                            Updated wording for inputting the library/dir # in lightbar mode
 * 2023-04-15 Eric Oulashin   Version 1.34
 *                            Fix: For lightbar mode with sub-board collapsing, now sets the selected item based
 *                            on the user's current sub-board. Also, color settings no longer need the control
 *                            character (they can just be a list of the attribute characters).
 * 2023-05-14 Eric Oulashin   Version 1.35
 *                            Refactored the code for reading the configuration file
 * 2023-07-16 Eric Oulashin   Version 1.36
 *                            Possible fix for not allowing to change sub-board if the first group is empty
// TODO: In the area list, the 10,000ths digit (for # posts) is in a different color)

// TODO: Passing "false" as the first command-line argument no longer works.
// That should allow choosing a sub-board within the user's current message
// group.

   1 (argv[0]): Boolean - Whether or not to choose a message group first (default).  If
                false, the user will only be able to choose a different sub-board within
				their current message group.
   2 (argv[1]): Boolean - Whether or not to run the area chooser (if false,
                then this file will just provide the DDMsgAreaChooser class).
*/

	require("dd_lightbar_menu.js", "DDLightbarMenu");
}

// This script requires Synchronet version 3.14 or higher.
// Exit if the Synchronet version is below the minimum.
if (system.version_num < 31400)
{
	var message = "\1n\1h\1y\1i* Warning:\1n\1h\1w Digital Distortion Message Area Chooser "
	             + "requires version \1g3.14\1w or\r\n"
	             + "higher of Synchronet.  This BBS is using version \1g" + system.version
	             + "\1w.  Please notify the sysop.";
	console.crlf();
	console.print(message);
	console.crlf();
	console.pause();
	exit();
}

// Version & date variables
var DD_MSG_AREA_CHOOSER_VERSION = "1.36";
var DD_MSG_AREA_CHOOSER_VER_DATE = "2023-07-16";
var CTRL_M = "\x0d";
var KEY_ENTER = CTRL_M;
var BACKSPACE = CTRL_H;
var CTRL_F = "\x06";
// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017
// use CTRL-P and CTRL-N for PageUp and PageDown, respectively.  sbbsdefs.js
// defines them as KEY_PAGEUP and KEY_PAGEDN; I've used slightly different names
// in this script so that this script will work with Synchronet systems before
// and after the update containing those key definitions.
var KEY_PAGE_UP = "\x10"; // Ctrl-P
var KEY_PAGE_DOWN = "\x0e"; // Ctrl-N
// Ensure KEY_PAGE_UP and KEY_PAGE_DOWN are set to what's defined in sbbs.js
// for KEY_PAGEUP and KEY_PAGEDN in case they change
if (typeof(KEY_PAGEUP) === "string")
	KEY_PAGE_UP = KEY_PAGEUP;
if (typeof(KEY_PAGEDN) === "string")
	KEY_PAGE_DOWN = KEY_PAGEDN;

// Key codes for display
var UP_ARROW = ascii(24);
var DOWN_ARROW = ascii(25);

// Characters for display
var HORIZONTAL_SINGLE = "\xC4";

// Misc. defines
var ERROR_WAIT_MS = 1500;
var SEARCH_TIMEOUT_MS = 10000;

// Determine the script's startup directory.
// This code is a trick that was created by Deuce, suggested by Rob Swindell
// as a way to detect which directory the script was executed in.  I've
// shortened the code a little.
var gStartupPath = '.';
try { throw dig.dist(dist); } catch(e) { gStartupPath = e.fileName; }
gStartupPath = backslash(gStartupPath.replace(/[\/\\][^\/\\]*$/,''));

// gIsSysop stores whether or not the user is a sysop.
var gIsSysop = user.compare_ars("SYSOP"); // Whether or not the user is a sysop

// 1st command-line argument: Whether or not to choose a message group first (if
// false, then only choose a sub-board within the user's current group).  This
// can be true or false.
	gChooseMsgGrpOnStartup = (argv[0].toLowerCase() == "true");
// 2nd command-line argument: Determine whether or not to execute the message listing
// code (true/false)
if (typeof(argv[1]) == "boolean")
	executeThisScript = argv[1];
else if (typeof(argv[1]) == "string")
	executeThisScript = (argv[1].toLowerCase() == "true");

// If executeThisScript is true, then create a DDMsgAreaChooser object and use
// it to let the user choose a message area.
if (executeThisScript)
{
	// Starting with the user's current messsage group, find the first message group with sub-boards.
	// If there are none, output an error message and exit.
	//var firstGrpIdxWithSubBoards = findNextGrpIdxWithSubBoards(0);
	var firstGrpIdxWithSubBoards = findNextGrpIdxWithSubBoards(-1);
	if (firstGrpIdxWithSubBoards < 0)
	{
		console.clear("\x01n");
		console.print("\1y\1hThere are no message sub-boards available.\r\n\1p");
		exit(0);
	}

	// When exiting this script, make sure to set the ctrl key pasthru back to what it was originally
	js.on_exit("console.ctrlkey_passthru = " + console.ctrlkey_passthru);
	console.ctrlkey_passthru = "+ACGKLOPQRTUVWXYZ_"; // So that control key combinations only get caught by this script

	var msgAreaChooser = new DDMsgAreaChooser();
	// If we are to let the user choose a sub-board within
	// their current group (and not choose a message group
	// first), then we need to capture the chosen sub-board
	// here just in case, and change the user's message area
	// here.  Otherwise, if choosing the message group first,
	// SelectMsgArea() will change the user's sub-board.
	//var msgGroupIdx = (gChooseMsgGrpOnStartup ? firstGrpIdxWithSubBoards/*null*/ : +bbs.curgrp);
	var msgGroupIdx = +bbs.curgrp; // Default to the user's current message group
	if (!gChooseMsgGrpOnStartup)
		msgAreaChooser.BuildSubBoardPrintfInfoForGrp(msgGroupIdx);
	var chosenIdx = msgAreaChooser.SelectMsgArea(gChooseMsgGrpOnStartup, msgGroupIdx);
	if (!gChooseMsgGrpOnStartup && (typeof(chosenIdx) === "number"))
		bbs.cursub_code = msg_area.grp_list[bbs.curgrp].sub_list[chosenIdx].code;
// End of script execution

///////////////////////////////////////////////////////////////////////////////////
// DDMsgAreaChooser class stuff

function DDMsgAreaChooser()
{
	// this.colors will be an associative array of colors (indexed by their
	// usage) used for the message group/sub-board lists.
	// Colors for the file & message area lists
	this.colors = {
		areaNum: "\1n\1w\1h",
		desc: "\1n\1c",
		numItems: "\1b\1h",
		header: "\1n\1y\1h",
		subBoardHeader: "\1n\1g",
		areaMark: "\1g\1h",
		latestDate: "\1n\1g",
		latestTime: "\1n\1m",
		// Highlighted colors (for lightbar mode)
		bkgHighlight: "\1" + "4", // Blue background
		areaNumHighlight: "\1w\1h",
		descHighlight: "\1c",
		dateHighlight: "\1w\1h",
		timeHighlight: "\1w\1h",
		numItemsHighlight: "\1w\1h",
		// Lightbar help line colors
		lightbarHelpLineBkg: "\1" + "7",
		lightbarHelpLineGeneral: "\1b",
		lightbarHelpLineHotkey: "\1r",
		lightbarHelpLineParen: "\1m"
	};

	// showImportDates is a boolean to specify whether or not to display the
	// message import dates.  If false, the message written dates will be
	// displayed instead.
	this.showImportDates = true;

	// useLightbarInterface specifies whether or not to use the lightbar
	// interface.  The lightbar interface will still only be used if the
	// user's terminal supports ANSI.
	this.useLightbarInterface = true;

	// Filename base of a header to display above the area list
	this.areaChooserHdrFilenameBase = "msgAreaChgHeader";
	this.areaChooserHdrMaxLines = 5;

	// Whether or not to show the latest message date/time in the
	// sub-board list
	this.showDatesInSubBoardList = true;

	// Whether or not to enable sub-board collapsing.  For
	// example, for sub-board in a group starting with
	// common text and a separator (specified below), the
	// common text will be the only one displayed, and when
	// the user selects it, a 3rd tier with the sub-board
	// after the separator will be shown
	this.useSubCollapsing = true;
	// The separator character to use for sub-board collapsing
	this.subCollapseSeparator = ":";
	// If userSubCollapsing is true, then group_list will be populated
	// with some information from Synchronet's msg_area.grp_list,
	// including a dir_list for each group.  The dir_list arrays
	// could have one that was collapsed from multiple sub-board
	// set up in the BBS - The dir_list within that one would then
	// contain multiple sub-board split based on the dir collapse
	// separator.
	this.group_list = [];

	// Set the function pointers for the object
	this.ReadConfigFile = DDMsgAreaChooser_ReadConfigFile;
	this.WriteKeyHelpLine = DDMsgAreaChooser_writeKeyHelpLine;
	this.WriteGrpListHdrLine = DDMsgAreaChooser_writeGrpListTopHdrLine;
	this.WriteSubBrdListHdr1Line = DMsgAreaChooser_writeSubBrdListHdr1Line;
	this.SelectMsgArea = DDMsgAreaChooser_SelectMsgArea;
	this.SelectMsgArea_Lightbar = DDMsgAreaChooser_SelectMsgArea_Lightbar;
	this.CreateLightbarMsgGrpMenu = DDMsgAreaChooser_CreateLightbarMsgGrpMenu;
	this.CreateLightbarSubBoardMenu = DDMsgAreaChooser_CreateLightbarSubBoardMenu;
	this.SelectMsgArea_Traditional = DDMsgAreaChooser_SelectMsgArea_Traditional;
	this.SelectSubBoard_Traditional = DDMsgAreaChooser_SelectSubBoard_Traditional;
	this.SelectSubSubWithinSub_Traditional = DDMsgAreaChooser_SelectSubSubWithinSub_Traditional;
	this.ListMsgGrps = DDMsgAreaChooser_ListMsgGrps_Traditional;
	this.ListSubBoardsInMsgGroup = DDMsgAreaChooser_ListSubBoardsInMsgGroup_Traditional;
	this.CurrentSubBoardIsInSubSubsForSub = DDMsgAreaChooser_CurrentSubBoardIsInSubSubsForSub;
	this.GetSubBoardInfo = DDMsgAreaChooser_GetSubBoardInfo;
	// Lightbar-specific functions
	this.WriteMsgGroupLine = DDMsgAreaChooser_writeMsgGroupLine;
	this.GetMsgSubBrdLine = DDMsgAreaChooser_GetMsgSubBrdLine;
	// Help screen
	this.ShowHelpScreen = DDMsgAreaChooser_showHelpScreen;
	// Function to build the sub-board printf information for a message
	// group
	this.BuildSubBoardPrintfInfoForGrp = DDMsgAreaChooser_BuildSubBoardPrintfInfoForGrp;
	this.DisplayAreaChgHdr = DDMsgAreaChooser_DisplayAreaChgHdr;
	this.DisplayListHdrLines = DDMsgAreaChooser_DisplayListHdrLines;
	this.WriteLightbarKeyHelpErrorMsg = DDMsgAreaChooser_WriteLightbarKeyHelpErrorMsg;
	this.SetUpGrpListWithCollapsedSubBoards = DDMsgAreaChooser_SetUpGrpListWithCollapsedSubBoards;
	this.FindMsgGrpIdxFromText = DDMsgAreaChooser_FindMsgGrpIdxFromText;
	this.FindSubBoardIdxFromText = DDMsgAreaChooser_FindSubBoardIdxFromText;
	this.GetSubNameLenAndNumMsgsLen = DDMsgAreaChooser_GetSubNameLenAndNumMsgsLen;

	// Read the settings from the config file.
	this.ReadConfigFile();
	// These variables store default lengths of the various columns displayed in
	// the message group/sub-board lists.
	// Sub-board info field lengths
	this.areaNumLen = 4;
	this.numItemsLen = 4;
	this.dateLen = 10; // i.e., YYYY-MM-DD
	this.timeLen = 8;  // i.e., HH:MM:SS
	// Sub-board name length - This should be 47 for an 80-column display.
	this.subBoardNameLen = console.screen_columns - this.areaNumLen - this.numItemsLen - 5;
	if (this.showDatesInSubBoardList)
		this.subBoardNameLen -= (this.dateLen + this.timeLen + 2);
	// Message group description length (67 chars on an 80-column screen)
	this.msgGrpDescLen = console.screen_columns - this.areaNumLen - this.numItemsLen - 5;

	// printf strings for various things
	// Message group information (printf strings)
	this.msgGrpListPrintfStr = "\1n " + this.colors.areaNum + "%" + this.areaNumLen
	                         + "d " + this.colors.desc + "%-"
	                         + this.msgGrpDescLen + "s " + this.colors.numItems
	                         + "%" + this.numItemsLen + "d";
	this.msgGrpListHilightPrintfStr = "\1n" + this.colors.bkgHighlight + " "
	                                + "\1n" + this.colors.bkgHighlight
	                                + this.colors.areaNumHighlight + "%" + this.areaNumLen
	                                + "d \1n" + this.colors.bkgHighlight
	                                + this.colors.descHighlight + "%-"
	                                + this.msgGrpDescLen + "s \1n" + this.colors.bkgHighlight
	                                + this.colors.numItemsHighlight + "%" + this.numItemsLen
	                                + "d";
	// Message group list header (printf string)
	this.msgGrpListHdrPrintfStr = this.colors.header + "%6s %-"
	                            + +(this.msgGrpDescLen-8) + "s %-12s";
	// Sub-board information header (printf string)
	this.subBoardListHdrPrintfStr = this.colors.header + " %5s %-"
	                              + +(this.subBoardNameLen-3) + "s %-7s";
	if (this.showDatesInSubBoardList)
		this.subBoardListHdrPrintfStr += " %-19s";
	// Lightbar mode key help line
	this.lightbarKeyHelpText = "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + UP_ARROW
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + DOWN_ARROW
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "PgUp"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + "/"
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "Dn"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "HOME"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "END"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "F"
	              + "\1n" + this.colors.lightbarHelpLineParen
	              + this.colors.lightbarHelpLineBkg + ")"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + "irst pg, "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "L"
	              + "\1n" + this.colors.lightbarHelpLineParen
	              + this.colors.lightbarHelpLineBkg + ")"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + "ast pg, "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "#"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "CTRL-F"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "/"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "N"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "Q"
				  + "\1n" + this.colors.lightbarHelpLineParen
				  + this.colors.lightbarHelpLineBkg + ")"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + "uit, "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "?";
	// Pad the lightbar key help text on either side to center it on the screen
	// (but leave off the last character to avoid screen drawing issues)
	var helpTextLen = console.strlen(this.lightbarKeyHelpText);
	var helpTextStartCol = (console.screen_columns/2) - (helpTextLen/2);
	this.lightbarKeyHelpText = "\1n" + this.colors.lightbarHelpLineBkg
	                         + format("%" + +(helpTextStartCol) + "s", "")
							 + this.lightbarKeyHelpText + "\1n"
							 + this.colors.lightbarHelpLineBkg;
	var numTrailingChars = console.screen_columns - (helpTextStartCol+helpTextLen) - 1;
	this.lightbarKeyHelpText += format("%" + +(numTrailingChars) + "s", "") + "\1n";
	// this.subBoardListPrintfInfo will be an array of printf strings
	// for the sub-boards in the message groups.  The index is the
	// message group index.  The sub-board printf information is created
	// on the fly the first time the user lists sub-boards for a message
	// group.

	// areaChangeHdrLines is an array of text lines to use as a header to display
	// above the message area changer lists.
	this.areaChangeHdrLines = loadTextFileIntoArray(this.areaChooserHdrFilenameBase, this.areaChooserHdrMaxLines);
}

// For the DDMsgAreaChooser class: Writes the line of key help at the bottom
// row of the screen.
function DDMsgAreaChooser_writeKeyHelpLine()
{
	console.gotoxy(1, console.screen_rows);
	console.print(this.lightbarKeyHelpText);
}

// For the DDMsgAreaChooser class: Outputs the header line to appear above
// the list of message groups.
//
// Parameters:
//  pNumPages: The number of pages.  This is optional; if this is
//             not passed, then it won't be used.
//  pPageNum: The page number.  This is optional; if this is not passed,
//            then it won't be used.
function DDMsgAreaChooser_writeGrpListTopHdrLine(pNumPages, pPageNum)
{
	if ((typeof(pPageNum) === "number") && (typeof(pNumPages) === "number"))
		descStr += "    (Page " + pPageNum + " of " + pNumPages + ")";
	else if ((typeof(pPageNum) === "number") && (typeof(pNumPages) !== "number"))
		descStr += "    (Page " + pPageNum + ")";
	else if ((typeof(pPageNum) !== "number") && (typeof(pNumPages) === "number"))
		descStr += "    (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
	printf(this.msgGrpListHdrPrintfStr, "Group#", descStr, "# Sub-Boards");
	console.cleartoeol("\1n");
}

// For the DDMsgAreaChooser class: Outputs the first header line to appear
// above the sub-board list for a message group.
//
// Parameters:
//  pGrpIndex: The index of the message group (assumed to be valid)
//  pSubIndex: Optional - The index of a sub-board within the message group (if using sub-board
//             collapsing, we might want to append the sub-board name to the description)
//  pNumPages: The number of pages.  This is optional; if this is
//             not passed, then it won't be used.
//  pPageNum: The page number.  This is optional; if this is not passed,
//            then it won't be used.
function DMsgAreaChooser_writeSubBrdListHdr1Line(pGrpIndex, pSubIndex, pNumPages, pPageNum)
	var descLen = 25;
	var descFormatStr = "\1n" + this.colors.subBoardHeader + "Sub-boards of \1h%-" + descLen + "s     \1n"
	if ((typeof(pPageNum) === "number") && (typeof(pNumPages) === "number"))
		descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")";
	else if ((typeof(pPageNum) === "number") && (typeof(pNumPages) !== "number"))
		descFormatStr += "(Page " + pPageNum + ")";
	else if ((typeof(pPageNum) !== "number") && (typeof(pNumPages) === "number"))
		descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
	// If using sub-board collapsing, then build the description as needed.  Otherwise,
	// just use the sub-board description.
	var desc = "";
	if (this.useSubCollapsing)
	{
		// Ensure this.group_list is set up
		this.SetUpGrpListWithCollapsedSubBoards();
		// The description should be the library's description.  Also, if pSubIdx
		// is a number, then append the directory description to the description.
		desc = this.group_list[pGrpIndex].description;
		if ((typeof(pSubIndex) === "number") && (this.group_list[pGrpIndex].sub_list[pSubIndex].sub_subboard_list.length > 0))
			desc += this.subCollapseSeparator + " " + this.group_list[pGrpIndex].sub_list[pSubIndex].description;
	}
	else
		desc = msg_area.grp_list[pGrpIndex].description;
	printf(descFormatStr, desc.substr(0, descLen));
}

// For the DDMsgAreaChooser class: Lets the user choose a message group and
// sub-board via numeric input, using a lightbar interface (if enabled and
// if the user's terminal uses ANSI) or a traditional user interface.
//
// Parameters:
//  pChooseGroup: Boolean - Whether or not to choose the message group.  If false,
//                then this will allow choosing a sub-board within the user's
//                current message group.  This is optional; defaults to true.
//  pGrpIdx: The group index (can be null) - This is for the lightbar
//           interface logic; the traditional interface doesn't use this
//           for SelectMsgArea().
function DDMsgAreaChooser_SelectMsgArea(pChooseGroup, pGrpIdx)
	// If sub-board collapsing is enabled, then set up
	// this.group_list.
	if (this.useSubCollapsing)
		this.SetUpGrpListWithCollapsedSubBoards();

	if (this.useLightbarInterface && console.term_supports(USER_ANSI))
		return this.SelectMsgArea_Lightbar(pChooseGroup ? 1 : 2, pGrpIdx);
		return this.SelectMsgArea_Traditional(pChooseGroup ? 1 : 2, pGrpIdx);
}

// For the DDMsgAreaChooser class: Displays the header & header lines above the list.
//
// Parameters:
//  pScreenRow: The row on the screen to write the lines at.  If no cursor movements are desired, this can be null.
//  pChooseGroup: Boolean - Whether or not the user is choosing a message group
//  pGrpIdx: The index of the message group being used
//  pNumPages: Optional - The number of pages of items
//  pPageNum: Optional - The current page number for the items
function DDMsgAreaChooser_DisplayListHdrLines(pScreenRow, pChooseGroup, pGrpIdx, pNumPages, pPageNum)
{
	this.DisplayAreaChgHdr(1);
	if (typeof(pScreenRow) === "number")
		console.gotoxy(1, pScreenRow);
	if (pChooseGroup)
		this.WriteGrpListHdrLine(pNumPages, pPageNum);
	else
	{
		// For the number of items in the sub-board list, use the text "Posts" or "Items", depending
		// on whether sub-board collapsing is enabled and there are sub-subboard for the given group
		// & sub-board index
		var numItemsText = "Posts";
		if (this.useSubCollapsing)
		{
			for (var subIdx in this.group_list[pGrpIdx].sub_list)
			{
				if (typeof(this.group_list[pGrpIdx].sub_list[subIdx].sub_subboard_list) !== "undefined" && this.group_list[pGrpIdx].sub_list[subIdx].sub_subboard_list.length > 0)
				{
					numItemsText = "Items";
					break;
				}
			}
		}
		// Write the list header lines
		this.WriteSubBrdListHdr1Line(pGrpIdx);
		if (typeof(pScreenRow) === "number")
			console.gotoxy(1, pScreenRow+2);
		if (this.showDatesInSubBoardList)
			printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + numItemsText, "Latest date & time");
		else
			printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + numItemsText);
	}
}

// For the DDMsgAreaChooser class: Lets the user choose a message group and
// sub-board via numeric input, using a lightbar user interface.
//  pLevel: The file heirarchy level:
//          1: Message groups
//          2: Sub-boards within message groups
//          3: "Sub-subboards" within sub-boards (if sub-board name collapsing is enabled)
//          This is optional and defaults to 1.
//  pGrpIdx: Optional - The group index, if choosing a sub-board
function DDMsgAreaChooser_SelectMsgArea_Lightbar(pLevel, pGrpIdx, pSubIdx)
	// If there are no message groups or sub-boards, then don't let the user
	// choose one.
	if (msg_area.grp_list.length == 0)
	{
		console.clear("\1n");
		console.print("\1y\1hThere are no message groups.\r\n\1p");
		return;
	}
	var level = (typeof(pLevel) === "number" ? pLevel : 1);
	if ((level < 1) || (level > 3))
		return;
	else if (level == 1)
		// If there are no sub-boards in the given group index, then see if there's a next group with
		// sub-boards (and wrap around)
		if (msg_area.grp_list[pGrpIdx].sub_list.length == 0)
			var nextGrpIdx = findNextGrpIdxWithSubBoards(pGrpIdx);
			if (nextGrpIdx > -1 && msg_area.grp_list[nextGrpIdx].sub_list.length > 0)
				pGrpIdx = nextGrpIdx;
			else
			{
				console.clear("\1n");
				console.print("\1y\1hThere are no sub-boards available.\r\n\1p");
				return;
			}
	// 2: Choose a sub-board within a message group
	// 3: Choose a sub-subboard within a sub-board, for sub-board name collapsing
	else if ((level == 2) || (level == 3))
		if (typeof(pGrpIdx) !== "number")
			return;
		if (msg_area.grp_list[pGrpIdx].sub_list.length == 0)
			console.print("\1y\1hThere are no sub-boards in " + msg_area.grp_list[pGrpIdx].description + ".\r\n\1p");
	// Clear the screen, write the header, help line, and list header(s)
	console.clear("\1n");
	this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
	// Create the menu and do the user input loop
	var msgAreaMenu = (chooseGroup ? this.CreateLightbarMsgGrpMenu() : this.CreateLightbarSubBoardMenu(pLevel, pGrpIdx, pSubIdx));
	var drawMenu = true;
	var lastSearchText = "";
	var lastSearchFoundIdx = -1;
	var chosenIdx = -1;
	var continueOn = true;
	// Let the user choose a group, and also respond to other user choices
	while (continueOn)
	{
		chosenIdx = -1;
		var returnedMenuIdx = msgAreaMenu.GetVal(drawMenu);
		drawMenu = true;
		var lastUserInputUpper = (typeof(msgAreaMenu.lastUserInput) === "string" ? msgAreaMenu.lastUserInput.toUpperCase() : "");
		//if (user.is_sysop) console.print("\x01n\r\nlastUserInputUpper: " + lastUserInputUpper + ":\r\n\x01p"); // Temporary
			chosenIdx = returnedMenuIdx;
		// If userChoice is not a number, then it should be null in this case,
		// and the user would have pressed one of the additional quit keys set
		// up for the menu.  So look at the menu's lastUserInput and do the
		// appropriate thing.
		else if ((lastUserInputUpper == "Q") || (lastUserInputUpper == KEY_ESC)) // Quit
			continueOn = false;
		else if ((lastUserInputUpper == "/") || (lastUserInputUpper == CTRL_F)) // Start of find
			console.gotoxy(1, console.screen_rows);
			console.cleartoeol("\1n");
			console.gotoxy(1, console.screen_rows);
			var promptText = "Search: ";
			console.print(promptText);
			var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS);
			lastSearchText = searchText;
			// If the user entered text, then do the search, and if found,
			// found, go to the page and select the item indicated by the
			// search.
			if (searchText.length > 0)
				var oldLastSearchFoundIdx = lastSearchFoundIdx;
				var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx;
				var idx = -1;
				if (chooseGroup)
					idx = this.FindMsgGrpIdxFromText(searchText, msgAreaMenu.selectedItemIdx);
					idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, msgAreaMenu.selectedItemIdx+1);
				lastSearchFoundIdx = idx;
				if (idx > -1)
				{
					// Set the currently selected item in the menu, and ensure it's
					// visible on the page
					msgAreaMenu.selectedItemIdx = idx;
					if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
					else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
					else
						// If the current index and the last index are both on the same page on the
						// menu, then have the menu only redraw those items.
						msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
						idx = this.FindMsgGrpIdxFromText(searchText, 0);
						idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, 0);
					lastSearchFoundIdx = idx;
					if (idx > -1)
						// Set the currently selected item in the menu, and ensure it's
						// visible on the page
						msgAreaMenu.selectedItemIdx = idx;
						if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
						else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
							// The current index and the last index are both on the same page on the
							// menu, so have the menu only redraw those items.
							msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
						this.WriteLightbarKeyHelpErrorMsg("Not found");
						drawMenu = false;
				}
			}
			else
				drawMenu = false;
			this.WriteKeyHelpLine();
		}
		else if (lastUserInputUpper == "N") // Next search result (requires an existing search term)
		{
			// This works but seems a little strange sometimes.
			// - Should this always start from the selected index?
			// - If it wraps around to one of the items on the first page,
			//   should it always set the top index to 0?
			if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1))
			{
				var oldLastSearchFoundIdx = lastSearchFoundIdx;
				var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx;
				// Do the search, and if found, go to the page and select the item
				// indicated by the search.
				var idx = 0;
				if (chooseGroup)
					idx = this.FindMsgGrpIdxFromText(searchText, lastSearchFoundIdx+1);
					idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, lastSearchFoundIdx+1);
				if (idx > -1)
				{
					lastSearchFoundIdx = idx;
					// Set the currently selected item in the menu, and ensure it's
					// visible on the page
					msgAreaMenu.selectedItemIdx = idx;
					if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
						if (msgAreaMenu.topItemIdx < 0)
							msgAreaMenu.topItemIdx = 0;
					else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
					else
						// The current index and the last index are both on the same page on the
						// menu, so have the menu only redraw those items.
						msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
						idx = this.FindMsgGrpIdxFromText(searchText, 0);
						idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, 0);
					lastSearchFoundIdx = idx;
					if (idx > -1)
						// Set the currently selected item in the menu, and ensure it's
						// visible on the page
						msgAreaMenu.selectedItemIdx = idx;
						if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
							if (msgAreaMenu.topItemIdx < 0)
								msgAreaMenu.topItemIdx = 0;
						else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
							// The current index and the last index are both on the same page on the
							// menu, so have the menu only redraw those items.
							msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
						this.WriteLightbarKeyHelpErrorMsg("Not found");
						drawMenu = false;
						this.WriteKeyHelpLine();
				this.WriteLightbarKeyHelpErrorMsg("There is no previous search", true);
				drawMenu = false;
				this.WriteKeyHelpLine();
		else if (lastUserInputUpper == "?") // Show help
			this.ShowHelpScreen(true, true);
			console.pause();
			// Refresh the screen
			console.clear("\1n");
			this.DisplayAreaChgHdr(1);
			this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
		// If the user entered a numeric digit, then treat it as
		// the start of the message group number.
		if (lastUserInputUpper.match(/[0-9]/))
			// Put the user's input back in the input buffer to
			// be used for getting the rest of the message number.
			console.ungetstr(lastUserInputUpper);
			// Move the cursor to the bottom of the screen and
			// prompt the user for the message number.
			console.gotoxy(1, console.screen_rows);
			console.clearline("\1n");
			var itemPromptWord = "";
			if (this.useSubCollapsing)
			{
				if (level == 1)
					itemPromptWord = "group";
				else if (level == 2)
					itemPromptWord = "item";
				else if (level == 3)
					itemPromptWord = "sub-board";
			}
			else
				itemPromptWord = (level == 1 ? "group" : "sub-board");
			printf("\1cChoose %s #: \1h", itemPromptWord);
			var userInput = console.getnum(msgAreaMenu.NumItems());
			if (userInput > 0)
				chosenIdx = userInput - 1;
			else
				// The user didn't make a selection.  So, we need to refresh
				// the screen due to everything being moved up one line.
				this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
		// If a group/sub-board/sub-subboard was chosen, then deal with it.
			// If choosing a message group, then let the user choose a
			// sub-board within the group.  Otherwise, return the user's
			// chosen sub-board.
			if (chooseGroup)
				// Show a "Loading..." text in case there are many sub-boards in
				// the chosen message group
				console.crlf();
				console.print("\1nLoading...");
				console.line_counter = 0; // To prevent a pause before the message list comes up
				// Ensure that the sub-board printf information is created for
				// the chosen message group.
				this.BuildSubBoardPrintfInfoForGrp(chosenIdx);
				var defaultSubIdx = chosenIdx == bbs.curgrp ? bbs.cursub : 0;
				var subCodeBackup = bbs.cursub_code;
				var chosenSubBoardIdx = this.SelectMsgArea_Lightbar(2, chosenIdx, defaultSubIdx);
				if (typeof(chosenSubBoardIdx) === "number" && chosenSubBoardIdx > -1)
					if (this.useSubCollapsing)
						bbs.cursub_code = this.group_list[chosenIdx].sub_list[chosenSubBoardIdx].code;
					else
						bbs.cursub_code = msg_area.grp_list[chosenIdx].sub_list[chosenSubBoardIdx].code;
					// A message sub-board was not chosen, so we'll have to re-draw
					// the header and key help line
					this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
			else if (level == 2) // Choosing a sub-board
			{
				if (this.useSubCollapsing)
				{
					// Ensure this.group_list is set up
					this.SetUpGrpListWithCollapsedSubBoards();

					// If the current file directory has subdirectories,
					// let the user choose one
					// pGrpIdx is the group index, and chosenIdx is the sub-board index
					if ((typeof(this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list) !== "undefined") && (this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list.length > 0))
					{
						var chosenSubSubBoardIdx = this.SelectMsgArea_Lightbar(3, pGrpIdx, chosenIdx);
						if (chosenSubSubBoardIdx > -1)
						{
							// Set the current message sub-board
							bbs.cursub_code = this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list[chosenSubSubBoardIdx].code;
							continueOn = false;
						}
						else
						{
							// A file directory was not chosen, so we'll have to re-draw
							// the header and list of message groups.
							// TODO:
							//this.DisplayListHdrLines(level, pLibIdx);
							//this.WriteKeyHelpLine();
						}
					}
					else // No subdirectories - Return the chosen index
						return chosenIdx;
				}
				else
					return chosenIdx; // Return the chosen file directory index
			}
			else if (level == 3)
				return chosenIdx; // Return the chosen subdirectory index
// For the DDMsgAreaChooser class: Creates the DDLightbarMenu for use with
// choosing a message group in lightbar mode.
//
// Return value: A DDLightbarMenu object for choosing a message group
function DDMsgAreaChooser_CreateLightbarMsgGrpMenu()
{
	// Start & end indexes for the various items in each mssage group list row
	// Selected mark, group#, description, # sub-boards
	var msgGrpListIdxes = {
		markCharStart: 0,
		markCharEnd: 1,
		grpNumStart: 1,
		grpNumEnd: 2 + (+this.areaNumLen)
	};
	msgGrpListIdxes.descStart = msgGrpListIdxes.grpNumEnd;
	msgGrpListIdxes.descEnd = msgGrpListIdxes.descStart + +this.msgGrpDescLen;
	msgGrpListIdxes.numItemsStart = msgGrpListIdxes.descEnd;
	// Set numItemsEnd to -1 to let the whole rest of the lines be colored
	msgGrpListIdxes.numItemsEnd = -1;
	var listStartRow = this.areaChangeHdrLines.length + 2;
	var msgGrpMenuHeight = console.screen_rows - listStartRow;
	var msgGrpMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, msgGrpMenuHeight);
	msgGrpMenu.scrollbarEnabled = true;
	msgGrpMenu.borderEnabled = false;
	msgGrpMenu.SetColors({
		itemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaMark},
		            {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaNum},
		            {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.desc},
		            {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.numItems}],
		selectedItemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight},
		                    {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight},
		                    {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight},
		                    {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}]
	});

	msgGrpMenu.multiSelect = false;
	msgGrpMenu.ampersandHotkeysInItems = false;
	msgGrpMenu.wrapNavigation = false;

	// Add additional keypresses for quitting the menu's input loop so we can
	// respond to these keys
	msgGrpMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F);

	// Change the menu's NumItems() and GetItem() function to reference
	// the message list in this object rather than add the menu items
	// to the menu
	msgGrpMenu.areaChooser = this; // Add this object to the menu object
	msgGrpMenu.NumItems = function() {
		return msg_area.grp_list.length;
	};
	msgGrpMenu.GetItem = function(pGrpIndex) {
		var menuItemObj = this.MakeItemWithRetval(-1);
		if ((pGrpIndex >= 0) && (pGrpIndex < msg_area.grp_list.length))
			var showAreaMark = false;
			if (this.areaChooser.useSubCollapsing)
			{
				//this.group_list[pGrpIndex].sub_list[pSubIndex]
				showAreaMark = ((typeof(bbs.curgrp) === "number") && (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index));
			}
			else
			{
				showAreaMark = ((typeof(bbs.curgrp) === "number") && (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index));
			}
			menuItemObj.text = (showAreaMark ? "*" : " ");
			menuItemObj.text += format(this.areaChooser.msgGrpListPrintfStr, +(pGrpIndex+1),
			                           msg_area.grp_list[pGrpIndex].description.substr(0, this.areaChooser.msgGrpDescLen),
			                           msg_area.grp_list[pGrpIndex].sub_list.length);
			menuItemObj.text = strip_ctrl(menuItemObj.text);
			menuItemObj.retval = pGrpIndex;
	// Set the currently selected item to the current group
	msgGrpMenu.selectedItemIdx = msg_area.sub[bbs.cursub_code].grp_index;
	if (msgGrpMenu.selectedItemIdx >= msgGrpMenu.topItemIdx+msgGrpMenu.GetNumItemsPerPage())
		msgGrpMenu.topItemIdx = msgGrpMenu.selectedItemIdx - msgGrpMenu.GetNumItemsPerPage() + 1;

	return msgGrpMenu;
}

// For the DDMsgAreaChooser class: Creates the DDLightbarMenu for use with
// choosing a sub-board in lightbar mode.
//
// Parameters:
//  pLevel: The level of menu (1=Group, 2=Sub-board, 3=Sub board within sub-board if sub-board name collapsing is enabled)
//  pGrpIdx: The index of the message group
//  pSubIdx: The sub-board index (for sub-board name collapsing)
//
// Return value: A DDLightbarMenu object for choosing a sub-board within
// the given message group
function DDMsgAreaChooser_CreateLightbarSubBoardMenu(pLevel, pGrpIdx, pSubIdx)
{
	// Start & end indexes for the various items in each mssage group list row
	// Selected mark, group#, description, # sub-boards
	var lengthsObj = this.GetSubNameLenAndNumMsgsLen(pGrpIdx);
	var subBoardListIdxes = {
		markCharStart: 0,
		markCharEnd: 1,
		subNumStart: 1,
		subNumEnd: 3 + (+this.areaNumLen)
	subBoardListIdxes.descStart = subBoardListIdxes.subNumEnd;
	subBoardListIdxes.descEnd = subBoardListIdxes.descStart + lengthsObj.nameLen + 1;
	subBoardListIdxes.numItemsStart = subBoardListIdxes.descEnd;
	subBoardListIdxes.numItemsEnd = subBoardListIdxes.numItemsStart + lengthsObj.numMsgsLen + 1;
	subBoardListIdxes.dateStart = subBoardListIdxes.numItemsEnd;
	subBoardListIdxes.dateEnd = subBoardListIdxes.dateStart + +this.dateLen + 1;
	subBoardListIdxes.timeStart = subBoardListIdxes.dateEnd;
	// Set timeEnd to -1 to let the whole rest of the lines be colored
	subBoardListIdxes.timeEnd = -1;
	var listStartRow = this.areaChangeHdrLines.length + 3; // or + 2?
	var subBoardMenuHeight = console.screen_rows - listStartRow;
	var subBoardMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, subBoardMenuHeight);
	subBoardMenu.scrollbarEnabled = true;
	subBoardMenu.borderEnabled = false;
	subBoardMenu.SetColors({
		itemColor: [{start: subBoardListIdxes.markCharStart, end: subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark},
		            {start: subBoardListIdxes.subNumStart, end: subBoardListIdxes.subNumEnd, attrs: this.colors.areaNum},
		            {start: subBoardListIdxes.descStart, end: subBoardListIdxes.descEnd, attrs: this.colors.desc},
		            {start: subBoardListIdxes.numItemsStart, end: subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems},
		            {start: subBoardListIdxes.dateStart, end: subBoardListIdxes.dateEnd, attrs: this.colors.latestDate},
		            {start: subBoardListIdxes.timeStart, end: subBoardListIdxes.timeEnd, attrs: this.colors.latestTime}],
		selectedItemColor: [{start: subBoardListIdxes.markCharStart, end: subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.subNumStart, end: subBoardListIdxes.subNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.descStart, end: subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.numItemsStart, end: subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.dateStart, end: subBoardListIdxes.dateEnd, attrs: this.colors.dateHighlight + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.timeStart, end: subBoardListIdxes.timeEnd, attrs: this.colors.timeHighlight + this.colors.bkgHighlight}]
	});

	subBoardMenu.multiSelect = false;
	subBoardMenu.ampersandHotkeysInItems = false;
	subBoardMenu.wrapNavigation = false;

	// Add additional keypresses for quitting the menu's input loop so we can
	// respond to these keys
	subBoardMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F);

	// Change the menu's NumItems() and GetItem() function to reference
	// the message list in this object rather than add the menu items
	// to the menu
	subBoardMenu.areaChooser = this; // Add this object to the menu object
	subBoardMenu.grpIdx = pGrpIdx;
			subBoardMenu.NumItems = function() {
				return this.areaChooser.group_list[this.grpIdx].sub_list.length;
			};
			subBoardMenu.GetItem = function(pSubIdx) {
				var menuItemObj = this.MakeItemWithRetval(-1);
				var subIdxValid = true;
				/*
				if (this.areaChooser.useSubCollapsing)
							showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx);
				*/
				if ((pSubIdx >= 0) && (pSubIdx < this.areaChooser.group_list[this.grpIdx].sub_list.length))
				{
					var showSubBoardMark = false;
					if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
					{
						showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx);
						/*
						if (this.areaChooser.useSubCollapsing)
							showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx);
						else
							showSubBoardMark = ((this.grpIdx == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIdx == msg_area.sub[bbs.cursub_code].index));
						*/
					}
					// Set the sub-board description.  And if it has sub-subboards,
					// then append some text indicating so.
					var subDesc = this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].description;
					var numItems = 0;
					var lastMsgPostTimestamp = 0;
					var subSubBoardListExists = false;
					if (Array.isArray(this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list) && this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0)
					{
						subSubBoardListExists = true;
						subDesc += "  <subsubs>";
						numItems = this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list.length;
					}
					// Get information from the messagebase
					var msgBase = new MsgBase(this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].code);
					if (msgBase.open())
					{
						if (this.areaChooser.showDatesInSubBoardList)
						{
							var latestMsgHdr = getLatestMsgHdrWithMsgbase(msgBase, 100); // One of the last 100 messages should be readable
							if (latestMsgHdr != null)
								lastMsgPostTimestamp = latestMsgHdr.when_written_time; // when_imported_time
						}
						if (!subSubBoardListExists)
						{
							// There is no sub-subboard list, so this is just a regular sub-board.
							// Get the number of readable messages in the sub-board.
							numItems = numReadableMsgs(msgBase, this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].code);
					menuItemObj.text = (showSubBoardMark ? "*" : " ");
					if (this.areaChooser.showDatesInSubBoardList)
					{
						menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[this.grpIdx].printfStr, +(pSubIdx+1),
						                           subDesc.substr(0, this.areaChooser.descFieldLen), numItems,
						                           strftime("%Y-%m-%d", lastMsgPostTimestamp),
						                           strftime("%H:%M:%S", lastMsgPostTimestamp));
					}
					else
					{
						menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[this.grpIdx].printfStr, +(pSubIdx+1),
						                           subDesc.substr(0, this.areaChooser.descFieldLen), numItems);
					}
					menuItemObj.text = strip_ctrl(menuItemObj.text);
					menuItemObj.retval = pSubIdx;
				}

			// Set the currently selected item.  If the current sub-board is in this list,
			// then set the selected item to that; otherwise, the selected item should be
			// the first sub-board.
			if (msg_area.sub[bbs.cursub_code].grp_index == pGrpIdx)
			{
				if ((pSubIdx >= 0) && (pSubIdx < this.group_list[pGrpIdx].sub_list.length))
				{
					var subSubsValid = Array.isArray(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list) && this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0;
					if (!subSubsValid && bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].code)
					{
						subBoardMenu.selectedItemIdx = pSubIdx;
						if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
							subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
					}
				}
			}
		}
		else if (pLevel == 3)
		{
			subBoardMenu.subIdx = pSubIdx;
			subBoardMenu.NumItems = function() {
				return this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list.length;
			};
			subBoardMenu.GetItem = function(pSubSubIdx) {
				var menuItemObj = this.MakeItemWithRetval(-1);
				if ((pSubSubIdx >= 0) && (pSubSubIdx < this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list.length))
				{
					var showSubBoardMark = false;
					if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
						showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +(this.subIdx));
					menuItemObj.text = (showSubBoardMark ? "*" : " ");
					var subdirDesc = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].description;
					var subdirDirIdx = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].index;
					var subCode = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].code;
					menuItemObj.text = strip_ctrl(this.areaChooser.GetMsgSubBrdLine(this.grpIdx, msg_area.sub[subCode].index, false));
					menuItemObj.retval = pSubSubIdx;
				}

				return menuItemObj;
			}

			// Set the currently selected item.  If the current sub-board is in this list,
			// then set the selected item to that; otherwise, the selected item should be
			// the first sub-board.
			if (msg_area.sub[bbs.cursub_code].grp_index == pGrpIdx)
			{
				for (var i = 0; i < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; ++i)
				{
					if (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[i].code)
					{
						subBoardMenu.selectedItemIdx = i;
						if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
							subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
						break;
					}
				}
			}
		subBoardMenu.NumItems = function() {
			return msg_area.grp_list[this.grpIdx].sub_list.length;
		};
		subBoardMenu.GetItem = function(pSubIdx) {
			var menuItemObj = this.MakeItemWithRetval(-1);
			if ((pSubIdx >= 0) && (pSubIdx < msg_area.grp_list[this.grpIdx].sub_list.length))
			{
				var showSubBoardMark = false;
				if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
					showSubBoardMark = ((this.grpIdx == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIdx == msg_area.sub[bbs.cursub_code].index));
				menuItemObj.text = strip_ctrl(this.areaChooser.GetMsgSubBrdLine(this.grpIdx, pSubIdx, false));
				menuItemObj.retval = pSubIdx;
			}

			return menuItemObj;
		};

		// Set the currently selected item.  If the current sub-board is in this list,
		// then set the selected item to that; otherwise, the selected item should be
		// the first sub-board.
		if (msg_area.sub[bbs.cursub_code].grp_index == pGrpIdx)
		{
			subBoardMenu.selectedItemIdx = msg_area.sub[bbs.cursub_code].index;
			if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
				subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
		}
		else
		{
			subBoardMenu.selectedItemIdx = 0;
			subBoardMenu.topItemIdx = 0;
		}
}

// For the DDMsgAreaChooser class: Lets the user choose a message group and
// sub-board via numeric input, using a traditional user interface.
//
// Parameters:
//  pChooseGroup: Boolean - Whether or not to choose the message group.  If false,
//                then this will allow choosing a sub-board within the user's
//                current message group.  This is optional; defaults to true.
function DDMsgAreaChooser_SelectMsgArea_Traditional(pChooseGroup)
	// If there are no message groups, then don't let the user
	// choose one.
	if (msg_area.grp_list.length == 0)
	{
		console.clear("\1n");
		console.print("\1y\1hThere are no message groups.\r\n\1p");
		return;
	}
	var chooseGroup = (typeof(pChooseGroup) == "boolean" ? pChooseGroup : true);
	if (chooseGroup)
	{
		// Show the message groups & sub-boards and let the user choose one.
		var selectedGrp = 0;      // The user's selected message group
		var selectedSubBoard = 0; // The user's selected sub-board
		var usersCurrentIdxVals = getGrpAndSubIdxesFromCode(bbs.cursub_code, true);
		var continueChoosingMsgArea = true;
		while (continueChoosingMsgArea)
		{
			// Clear the BBS command string to make sure there are no extra
			// commands in there that could cause weird things to happen.
			bbs.command_str = "";
			this.DisplayAreaChgHdr(1);
			if (this.areaChangeHdrLines.length > 0)
				console.crlf();
			console.print("\1n\1b\1hþ \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + +(usersCurrentIdxVals.grpIdx+1) + "\1n\1c]: \1h");
			// Accept Q (quit), / or CTRL_F (Search) or a file library number
			selectedGrp = console.getkeys("Q/" + CTRL_F, msg_area.grp_list.length);

			// If the user just pressed enter (selectedGrp would be blank),
			// default to the current group.
			if (selectedGrp.toString() == "")
				selectedGrp = usersCurrentIdxVals.grpIdx + 1;

			if (selectedGrp.toString() == "Q")
				continueChoosingMsgArea = false;
			else if ((selectedGrp.toString() == "/") || (selectedGrp.toString() == CTRL_F))
			{
				console.crlf();
				var searchPromptText = "\1n\1c\1hSearch\1g: \1n";
				console.print(searchPromptText);
				var searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE);
				if (searchText.length > 0)
					grpSearchText = searchText;
			}
				// If the user specified a message group number, then
				// set it and let the user choose a sub-board within
				// the group.
				if (selectedGrp > 0)
				{
					// Set the default sub-board #: The current sub-board, or if the
					// user chose a different group, then this should be set
					// to the first sub-board.
					var defaultSubBoard = usersCurrentIdxVals.subIdx + 1;
					if (selectedGrp-1 != usersCurrentIdxVals.grpIdx)
						defaultSubBoard = 1;

					console.clear("\1n");
					var selectSubRetVal = this.SelectSubBoard_Traditional(selectedGrp-1, defaultSubBoard-1);
					// If the user chose a directory, then set the user's
					// message sub-board and quit the message group loop.
					if (selectSubRetVal.subBoardCode != "")
						bbs.cursub_code = selectSubRetVal.subBoardCode;
					}
				}
			}
		}
	}
	else
	{
		// Don't choose a group, just a sub-board within the user's current group.
		var idxVals = getGrpAndSubIdxesFromCode(bbs.cursub_code, true);
		var selectSubRetVal = this.SelectSubBoard_Traditional(idxVals.grpIdx, idxVals.subIdx);
		// If the user chose a directory, then set the user's sub-board.
		if (selectSubRetVal.subBoardCode != "")
			bbs.cursub_code = selectSubRetVal.subBoardCode;
	}
}

// For the DDMsgAreaChooser class: Allows the user to select a sub-board with the
// traditional user interface.
//
// Parameters:
//  pGrpIdx: The index of the message group to choose a sub-board for
//  pDefaultSubBoardIdx: The index of the default sub-board
//
// Return value: An object containing the following values:
//               subBoardChosen: Boolean - Whether or not a sub-board was chosen.
//               subBoardIndex: Numeric - The sub-board that was chosen (if any).
//                              Will be -1 if none chosen.
//               subBoardCode: The internal code of the chosen sub-board (or "" if none chosen)
function DDMsgAreaChooser_SelectSubBoard_Traditional(pGrpIdx, pDefaultSubBoardIdx)
	var retObj = {
		subBoardChosen: false,
		subBoardIndex: 1,
		subBoardCode: ""
	}
	var searchText = "";
	var defaultSubBoardIdx = pDefaultSubBoardIdx;
	var continueOn = false;
	do
	{
		this.DisplayAreaChgHdr(1);
		if (this.areaChangeHdrLines.length > 0)
			console.crlf();
		this.ListSubBoardsInMsgGroup(pGrpIdx, null, defaultSubBoardIdx, searchText);
		if (defaultSubBoardIdx >= 0)
			console.print("\1n\1b\1hþ \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + +(defaultSubBoardIdx+1) + "\1n\1c]: \1h");
		else
			console.print("\1n\1b\1hþ \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c: \1h");
		// Accept Q (quit) or a sub-board number
		var selectedSubBoard = console.getkeys("Q/" + CTRL_F, msg_area.grp_list[pGrpIdx].sub_list.length);

		// If the user just pressed enter (selectedSubBoard would be blank),
		// default the selected directory.
		var selectedSubBoardStr = selectedSubBoard.toString();
		if (selectedSubBoardStr == "")
		{
			if (defaultSubBoardIdx >= 0)
			{
				selectedSubBoard = defaultSubBoardIdx + 1; // Make this 1-based
				continueOn = false;
			}
		}
		else if ((selectedSubBoardStr == "/") || (selectedSubBoardStr == CTRL_F))
		{
			// Search
			console.crlf();
			var searchPromptText = "\1n\1c\1hSearch\1g: \1n";
			console.print(searchPromptText);
			searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE);
			console.print("\1n");
			console.crlf();
			if (searchText.length > 0)
				defaultSubBoardIdx = -1;
			else
				defaultSubBoardIdx = pDefaultSubBoardIdx;
			continueOn = true;
		}
		else if (selectedSubBoardStr == "Q")
			continueOn = false;
		// If a sub-board was chosen, then select it.
		if (selectedSubBoard > 0)
		{
			var selectedSubIdx = selectedSubBoard - 1;
			// If using sub-board name collapsing and the selected sub-board has sub-subboards, then
			// let the user choose a sub-subboard within the sub-board.  Otherwise, just select the
			// current sub-board.
			if (this.useSubCollapsing && this.group_list[pGrpIdx].sub_list[selectedSubIdx].sub_subboard_list.length > 0)
			{
				var subSubRetObj = this.SelectSubSubWithinSub_Traditional(pGrpIdx, selectedSubIdx);
				if (subSubRetObj.areaSelected)
				{
					retObj.subBoardChosen = true;
					retObj.subBoardIndex = subSubRetObj.subIndex;
					retObj.subBoardCode = subSubRetObj.subCode;
					continueOn = false;
				}
				else // An area wasn't chosen
				{
					continueOn = true;
					console.clear("\1n");
				}
			}
			else
			{
				retObj.subBoardChosen = true;
				retObj.subBoardIndex = selectedSubIdx;
				retObj.subBoardCode = msg_area.grp_list[pGrpIdx].sub_list[selectedSubIdx].code;
				continueOn = false;
			}
		}
	} while (continueOn);

	return retObj;
}

// For the DDMsgAreaChooser class: Lets the user select a sub-subbboard within a
// message sub-board - Traditional user interface.  This is meant for sub-board
// name collapsing, at the 3rd level.
//
// Parameters:
//  pGrpIdx: The message group index
//  pSubIdx: The index of the sub-board within the message group
//
// Return value: An object containing the following properties:
//               areaSelected: Boolean - Whether or not the user chose a sub-subboard.
//               subIndex: The index of the sub-board in Synchronet's sub_list array in the group
//               subCode: The internal code of the sub-board chosen, if chose.  If not chosen,
//                        this will be an empty string.
function DDMsgAreaChooser_SelectSubSubWithinSub_Traditional(pGrpIdx, pSubIdx)
{
	var retObj = {
		areaSelected: false,
		subIndex: -1,
		subCode: ""
	};

	if (!this.useSubCollapsing || this.group_list.length == 0)
		return retObj;
	if ((pGrpIdx < 0) || (pGrpIdx >= this.group_list.length))
		return retObj;
	if ((pSubIdx < 0) || (pSubIdx >= this.group_list[pGrpIdx].sub_list.length))
	{
		console.clear("\1n");
		console.print("\1y\1hThere are no sub-boards in this message group.\r\n\1p");
		return retObj;
	}
	if (this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length == 0)
	{
		console.clear("\1n");
		console.print("\1y\1hThere are no sub-subboards in this sub-board.\r\n\1p");
		return retObj;
	}

	// Gets the default sub-subdirectory number (1-based)
	function getDefaultSubSubNum(pGrpList, pGrpIdx, pSubIdx)
	{
		var subSubNum = 0; // Will be 1-based
		for (var subSubIdx = 0; subSubIdx < pGrpList[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; ++subSubIdx)
		{
			if (bbs.curdir_code == pGrpList[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code)
			{
				subSubNum = subSubIdx + 1;
				break;
			}
		}
		return subSubNum;
	}

	// defaultSubSubNum is the default sub-subboard # (will be 1-based)
	var defaultSubSubNum = getDefaultSubSubNum(this.group_list, pGrpIdx, pSubIdx);
	var searchText = "";
	var continueOn = false;
	do
	{
		console.clear("\1n");
		this.DisplayAreaChgHdr(1);
		if (this.areaChangeHdrLines.length > 0)
			console.crlf();
		// Note: This will list sub-subboards within the sub-board with a valid sub-board index
		// as the 2nd parameter.
		// TODO: When quitting out of the sub-suboard list, it's going back to the
		// message group list
		this.ListSubBoardsInMsgGroup(pGrpIdx, pSubIdx, defaultSubSubNum, searchText);
		if (defaultSubSubNum >= 1)
			console.print("\1n\1b\1hþ \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + defaultSubSubNum + "\1n\1c]: \1h");
		else
			console.print("\1n\1b\1hþ \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c: \1h");
		// Accept Q (quit), / or CTRL_F to search, or a file sub-board number
		var selectedSubSubNum = console.getkeys("Q/" + CTRL_F, this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length);

		// If the user just pressed enter (selectedSubSubNum would be blank),
		// default the selected sub-board.
		if (selectedSubSubNum.toString() == "Q")
			continueOn = false;
		else if (selectedSubSubNum.toString() == "")
			selectedSubSubNum = defaultSubSubNum;
		else if ((selectedSubSubNum == "/") || (selectedSubSubNum == CTRL_F))
		{
			// Search
			console.crlf();
			var searchPromptText = "\1n\1c\1hSearch\1g: \1n";
			console.print(searchPromptText);
			searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE);
			console.print("\1n");
			console.crlf();
			if (searchText.length > 0)
				defaultSubSubNum = -1;
			else
				defaultSubSubNum = getDefaultSubSubNum(this.group_list, pGrpIdx, pSubIdx);
			continueOn = true;
			console.line_counter = 0; // To avoid pausing before the clear screen
		}

		// If the user chose a sub-board, then set the user's message sub-board.
		if (selectedSubSubNum > 0)
		{
			retObj.areaSelected = true;
			retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[selectedSubSubNum-1].code;
			retObj.subIndex = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[selectedSubSubNum-1].index;
}

// For the DDMsgAreaChooser class: Lists all message groups (for the traditional
// user interface).
//
// Parameters:
//  pSearchText: Optional - Search text for the message groups
function DDMsgAreaChooser_ListMsgGrps_Traditional(pSearchText)
	// Print the header
	this.WriteGrpListHdrLine();
	console.print("\1n");

	var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");

	for (var i = 0; i < msg_area.grp_list.length; ++i)
	{
		if (searchText.length > 0)
			printIt = ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[i].description.toUpperCase().indexOf(searchText) >= 0));
		else
			printIt = true;

		if (printIt)
		{
			console.crlf();
			this.WriteMsgGroupLine(i, false);
		}
// For the DDMsgAreaChooser class: Lists the sub-boards in a message group
// (or sub-subboards in a sub-board, for sub-board name collapsing), for the
// traditional user interface.
//
// Parameters:
//  pGrpIndex: The index of the message group (0-based)
//  pSubIdx: Optional - For sub-board name collapsing, this is the (0-based)
//           index of the sub-board to show sub-subboards for.  To ignore this
//           and only show sub-boards of the given group, this can be null or -1.
//  pMarkIndex: An index of a message group to highlight.  This
//                   is optional; if left off, this will default to
//                   the current sub-board.
//  pSearchText: Optional - Search text for the sub-boards
//  pSortType: Optional - A string describing how to sort the list (if desired):
//             "none": Default behavior - Sort by sub-board #
//             "dateAsc": Sort by date, ascending
//             "dateDesc": Sort by date, descending
//             "description": Sort by description
function DDMsgAreaChooser_ListSubBoardsInMsgGroup_Traditional(pGrpIndex, pSubIdx, pMarkIndex, pSearchText, pSortType)
	// Default to the current message group & sub-board if pGrpIndex
	// and pMarkIndex aren't specified.
	var grpIndex = 0;
	if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
		grpIndex = msg_area.sub[bbs.cursub_code].grp_index;
	if ((pGrpIndex != null) && (typeof(pGrpIndex) === "number"))
	var highlightIndex = 0;
	if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
		highlightIndex = (pGrpIndex == msg_area.sub[bbs.cursub_code].index);
	if ((pMarkIndex != null) && (typeof(pMarkIndex) === "number"))
		highlightIndex = pMarkIndex;

	// Make sure grpIndex and highlightIndex are valid (they might not be for
	// brand-new users).
	if ((grpIndex == null) || (typeof(grpIndex) == "undefined"))
		grpIndex = 0;
	if ((highlightIndex == null) || (typeof(highlightIndex) == "undefined"))
		highlightIndex = 0;

	// Check whether pSubIdx is valid
	var subIdxValid = typeof(pSubIdx) === "number" && (pSubIdx > -1);

	// Ensure that the sub-board printf information is created for
	// this message group.
	this.BuildSubBoardPrintfInfoForGrp(grpIndex);

	// Print the headers
	this.WriteSubBrdListHdr1Line(grpIndex);
	console.crlf();
	var itemsHdrStr = "Posts";
	if (this.useSubCollapsing)
		itemsHdrStr = subIdxValid ? "Posts" : "Items";
		printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + itemsHdrStr, "Latest date & time");
		printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + itemsHdrStr);
	// Make the search text uppercase for case-insensitive matching
	var searchTextUpper = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");

	// List each sub-board in the message group.
	var subBoardArray = null; // For sorting, if desired
	var newestDate = {};      // For storing the date of the newest post in a sub-board
	var msgBase = null;       // For opening the sub-boards with a MsgBase object
	var msgHeader = null;     // For getting the date & time of the newest post in a sub-board
	var subBoardNum = 0;      // 0-based sub-board number (because the array index is the number as a str)
	// If a sort type is specified, then add the sub-board information to
	// subBoardArray so that it can be sorted.
	if ((typeof(pSortType) == "string") && (pSortType != "") && (pSortType != "none"))
	{
		var subList = msg_area.grp_list[grpIndex].sub_list;
		if (this.useSubCollapsing)
		{
			if (subIdxValid)
				subList = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list;
			else
				subList = this.group_list[grpIndex].sub_list;
		}
		for (var subIdx in subList)
		{
			// Open the current sub-board with the msgBase object.
			// If the search text is set, then use it to filter the sub-boards.
			addSubBoard = true;
			if (searchTextUpper.length > 0)
			{
				if (this.useSubCollapsing && subIdxValid)
				{
					// For sub-board name collapsing, sub-subboards only have descriptions, no names
					addSubBoard = (this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description.indexOf(searchTextUpper) >= 0);
				}
				else
				{
					addSubBoard = ((msg_area.grp_list[grpIndex].sub_list[subIdx].name.indexOf(searchTextUpper) >= 0) ||
					               (msg_area.grp_list[grpIndex].sub_list[subIdx].description.indexOf(searchTextUpper) >= 0));
				}
				var subCode = "";
				if (this.useSubCollapsing && subIdxValid)
					subCode = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].code;
				else
					subCode = msg_area.grp_list[grpIndex].sub_list[subIdx].code;
				msgBase = new MsgBase(subCode);
					subBoardInfo = new MsgSubBoardInfo();
					subBoardInfo.subBoardNum = +(subIdx);
					if (this.useSubCollapsing && subIdxValid)
					{
						subBoardInfo.subBoardIdx = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].index;
						subBoardInfo.description = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description;
					}
					else
					{
						subBoardInfo.subBoardIdx = msg_area.grp_list[grpIndex].sub_list[subIdx].index;
						subBoardInfo.description = msg_area.grp_list[grpIndex].sub_list[subIdx].description;
					}

					subBoardInfo.numPosts = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[subIdx].code);

					// Get the date & time when the last message was imported.
					if (this.showDatesInSubBoardList && (subBoardInfo.numPosts > 0))
						//var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
						var msgHeader = getLatestMsgHdr(msg_area.grp_list[grpIndex].sub_list[subIdx].code);
						if (msgHeader === null)
							msgHeader = getBogusMsgHdr();
						if (this.showImportDates)
							subBoardInfo.newestPostDate = msgHeader.when_imported_time;
						{
							var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
							if (msgWrittenLocalBBSTime != -1)
								subBoardInfo.newestPostDate = msgWrittenLocalBBSTime;
							else
								subBoardInfo.newestPostDate = msgHeader.when_written_time;
						}
				msgBase.close();
				subBoardArray.push(subBoardInfo);
				delete msgBase; // Free some memory?
		// Sort sub-board list.
		if (pSortType == "dateAsc")
		{
			subBoardArray.sort(function(pA, pB)
			{
				// Return -1, 0, or 1, depending on whether pA's date comes
				// before, is equal to, or comes after pB's date.
				var returnValue = 0;
				if (pA.newestPostDate < pB.newestPostDate)
					returnValue = -1;
				else if (pA.newestPostDate > pB.newestPostDate)
					returnValue = 1;
				return returnValue;
			});
		}
		else if (pSortType == "dateDesc")
		{
				subBoardArray.sort(function(pA, pB)
				{
					// Return -1, 0, or 1, depending on whether pA's date comes
					// after, is equal to, or comes before pB's date.
					var returnValue = 0;
					if (pA.newestPostDate > pB.newestPostDate)
						returnValue = -1;
					else if (pA.newestPostDate < pB.newestPostDate)
						returnValue = 1;
					return returnValue;
				});
			}
		}
		else if (pSortType == "description")
		{
			// Binary safe string comparison  
			// 
			// version: 909.322
			// discuss at: http://phpjs.org/functions/strcmp    // +   original by: Waldo Malqui Silva
			// +      input by: Steve Hilder
			// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
			// +    revised by: gorthaur
			// *     example 1: strcmp( 'waldo', 'owald' );    // *     returns 1: 1
			// *     example 2: strcmp( 'owald', 'waldo' );
			// *     returns 2: -1
			subBoardArray.sort(function(pA, pB)
			{
				return ((pA.description == pB.description) ? 0 : ((pA.description > pB.description) ? 1 : -1));
			});
		}
		// Display the sub-board list.
		for (var i = 0; i < subBoardArray.length; ++i)
		{
			console.crlf();
			var showSubBoardMark = false;
			if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
			{
				if (subBoardArray[i].subBoardNum == highlightIndex)
					showSubBoardMark = ((grpIndex == msg_area.sub[bbs.cursub_code].grp_index) && (highlightIndex == subBoardArray[i].subBoardIdx));
			}
			console.print(showSubBoardMark ? "\1n" + this.colors.areaMark + "*" : " ");
			if (this.showDatesInSubBoardList)
			{
				printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1),
				       subBoardArray[i].description.substr(0, this.subBoardNameLen),
				       subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate),
				       strftime("%H:%M:%S", subBoardArray[i].newestPostDate));
			}
			else
			{
				printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1),
				       subBoardArray[i].description.substr(0, this.subBoardNameLen),
				       subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate));
			}
		}
	}
	// If no sort type is specified, then output the sub-board information in
	// order of sub-board number.
	else
	{
		//var subList = this.useSubCollapsing ? this.group_list[grpIndex].sub_list : msg_area.grp_list[grpIndex].sub_list;
		var subList = msg_area.grp_list[grpIndex].sub_list;
		if (this.useSubCollapsing)
		{
			if (subIdxValid)
				subList = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list;
			else
				subList = this.group_list[grpIndex].sub_list;
		}
		for (var subIdx in subList)
			// If the search text is set, then use it to filter the sub-board list.
			includeSubBoard = true;
			if (searchTextUpper.length > 0)
			{
				if (this.useSubCollapsing && subIdxValid)
				{
					// For sub-board name collapsing, sub-subboards only have descriptions, no names
					includeSubBoard = (this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description.indexOf(searchTextUpper) >= 0);
				}
				else
				{
					includeSubBoard = ((msg_area.grp_list[grpIndex].sub_list[subIdx].name.toUpperCase().indexOf(searchTextUpper) >= 0) ||
					                   (msg_area.grp_list[grpIndex].sub_list[subIdx].description.toUpperCase().indexOf(searchTextUpper) >= 0));
				}
				// Call GetSubBoardInfo() to get the information about the sub-board or sub-subboard.
				// Make sure we pass the correct indexes.
				var subInfo = null;
				if (this.useSubCollapsing)
					// For some reason subIdx is a string, so ensure it's a number when calling GetSubBoardInfo()
					if (subIdxValid) // If pSubIdx is valid
						subInfo = this.GetSubBoardInfo(grpIndex, pSubIdx, +subIdx);
						subInfo = this.GetSubBoardInfo(grpIndex, +subIdx);
				}
				else
					subInfo = this.GetSubBoardInfo(grpIndex, subIdx);
				newestDate.date = strftime("%Y-%m-%d", subInfo.newestTime);
				newestDate.time = strftime("%H:%M:%S", subInfo.newestTime);
				// Print the sub-board information
				subBoardNum = +(subIdx);
				console.crlf();
				var showSubBoardMark = false;
				if (this.useSubCollapsing)
				{
					if (subIdxValid) // If using sub-subboards
						showSubBoardMark = (bbs.cursub_code == subList[subIdx].code);
						showSubBoardMark = this.CurrentSubBoardIsInSubSubsForSub(grpIndex, +subIdx);
				}
				else
					showSubBoardMark = (subBoardNum == highlightIndex);
				console.print(showSubBoardMark ? "\1n" + this.colors.areaMark + "*" : " ");
				var lengthsObj = this.GetSubNameLenAndNumMsgsLen(grpIndex);
				if (this.showDatesInSubBoardList)
				{
					printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
					         subInfo.desc.substr(0, lengthsObj.nameLen), subInfo.numItems,
					         newestDate.date, newestDate.time);
				}
				else
				{
					printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
					         subInfo.desc.substr(0, lengthsObj.nameLen, subInfo.numItems));
				}
			}
		}
	}
}

// For the DDMsgAreaChooser class: Returns whether the user's current selected sub-board is
// one of the sub-subboards for a sub-board (if sub-board name collapsing is enabled), or
// is the given sub-board in the message group.
//
// Parameters:
//  pGrpIdx: The index of the message group (0-based)
//  pSubIdx: The index of the sub-board within the message group (0-based)
//
// Return value: True/false, whether or not the user's current selected sub-board is one of
//               the sub-subboards for a sub-board (if sub-board name collapsing is enabled)
//               or is the given sub-board in the message group.
function DDMsgAreaChooser_CurrentSubBoardIsInSubSubsForSub(pGrpIdx, pSubIdx)
{
	// Sanity checking
	if (typeof(bbs.cursub_code) !== "string") // Rare case, for brand new user accounts
		return false;
	if (typeof(pGrpIdx) !== "number")
		return false;
	if (typeof(pSubIdx) !== "number")
		return false;

	var chosenSubMatch = false;
	if (this.useSubCollapsing)
	{
		if (pGrpIdx >= 0 && pGrpIdx < this.group_list.length && pSubIdx >= 0 && pSubIdx < this.group_list[pGrpIdx].sub_list.length)
		{
			// If this sub-board has a list of sub-subboards, then go through the sub-subboards and see if the
			// user's current sub-board is one of the sub-subboards.
			if (typeof(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list) !== "undefined" && this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0)
			{
				for (var subSubIdx = 0; subSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length && !chosenSubMatch; ++subSubIdx)
					chosenSubMatch = (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code);
			}
			else // There is no sub-subboard list for this sub-board
				chosenSubMatch = (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].code);
		}
	}
	else
	{
		if (pGrpIdx >= 0 && pGrpIdx < msg_area.grp_list.length && pSubIdx >= 0 && pSubIdx < msg_area.grp_list[pGrpIdx].sub_list.length)
			chosenSubMatch = (bbs.cursub_code == msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code);
	}
	return chosenSubMatch;
}

// For the DDMsgAreaChooser class: With a group & sub-board index, this function gets the date
// & time of the latest posted message from a sub-board (or group of sub-boards, if using
// sub-board name collapsing).  This function also gets the description of the sub-board (or
// group of sub-boards if using sub-board name collapsing).
//
// Parameters:
//  pGrpIdx: The index of the message group
//  pSubIdx: The index of the sub-board in the message group (if using
//           sub-board name collapsing, this could be the index of a set
//           of sub-subboards).
//  pSubSubIdx: Optional - When sub-board name collapsing is being used,
//              this specifies the index of the sub-subboard within the subboard.
//
// Return value: An object containing the following properties:
//               desc: The description of the sub-board (or group of sub-subboards if using name collapsing)
//               numItems: The number of messages in the sub-board or number of sub-subboards in the group,
//                         if using sub-board name collapsing
//               subCode: The internal code of the sub-board (this will be an empty string if it's a group of sub-subboards)
//               newestTime: A value containing the date & time of the newest post in the sub-board or group of sub-boards
function DDMsgAreaChooser_GetSubBoardInfo(pGrpIdx, pSubIdx, pSubSubIdx)
{
	var retObj = {
		desc: "",
		numItems: 0,
		subCode: "",
		newestTime: 0
	};
	// If using sub-board name collapsing and the given group & sub-board indexes has a
	// group of sub-subboards, then look through those sub-boards for information.
	if (this.useSubCollapsing)
	{
		if (typeof(pSubSubIdx) === "number" && pSubSubIdx >= 0 && pSubSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length)
		{
			retObj.desc = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].description;
			retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code;
			retObj.numItems = getNumMsgsInSubBoard(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code);
			retObj.newestTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code);
		}
		else // pSubSubIdx wasn't specified or is invalid
		{
			retObj.numItems = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length;
			retObj.desc = this.group_list[pGrpIdx].sub_list[pSubIdx].description;
			if (this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0)
			{
				for (var subSubIdx = 0; subSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list; ++subSubIdx)
				{
					var latestPostTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code);
					if (latestPostTime > retObj.newestTime)
						retObj.newestTime = latestPostTime;
			}
			else
			{
				// No sub-subboards in this sub-board
				retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].code;
				retObj.numItems = getNumMsgsInSubBoard(this.group_list[pGrpIdx].sub_list[pSubIdx].code);
				retObj.newestTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].code);
			}
		}
	}
	else // No sub-board name collapsing, or there are no sub-subboards in this sub-board
	{
		retObj.desc = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].description;
		retObj.subCode = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code;
		// Get the number of messages in the sub-board
		var numMsgs = numReadableMsgs(null, msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code);
		if (numMsgs > 0)
		{
			retObj.numItems = numMsgs;
			//var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
			var msgHeader = getLatestMsgHdr(retObj.subCode);
			if (msgHeader === null)
				msgHeader = getBogusMsgHdr();
			// Set the newest post time
			if (this.showImportDates)
				retObj.newestTime = msgHeader.when_imported_time;
			else
			{
				var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
				if (msgWrittenLocalBBSTime != -1)
					retObj.newestTime = msgWrittenLocalBBSTime;
				else
					retObj.newestTime = msgHeader.when_written_time;
}

//////////////////////////////////////////////
// Message group list stuff (lightbar mode) //
//////////////////////////////////////////////

// For the DDMsgAreaChooser class - Writes a message group information line.
//
// Parameters:
//  pGrpIndex: The index of the message group to write (assumed to be valid)
//  pHighlight: Boolean - Whether or not to write the line highlighted.
function DDMsgAreaChooser_writeMsgGroupLine(pGrpIndex, pHighlight)
{
	console.print("\1n");
	// Write the highlight background color if pHighlight is true.
	if (pHighlight)
		console.print(this.colors.bkgHighlight);

	// Write the message group information line
	var grpIsSelected = false;
	if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
		grpIsSelected = (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index);
	console.print(grpIsSelected ? this.colors.areaMark + "*" : " ");
	printf((pHighlight ? this.msgGrpListHilightPrintfStr : this.msgGrpListPrintfStr),
	       +(pGrpIndex+1),
	       msg_area.grp_list[pGrpIndex].description.substr(0, this.msgGrpDescLen),
	       msg_area.grp_list[pGrpIndex].sub_list.length);
	console.cleartoeol("\1n");
}

//////////////////////////////////////////////////
// Message sub-board list stuff (lightbar mode) //
//////////////////////////////////////////////////

// For the DDMsgAreaChooser class: Gets a message sub-board information line.
//
// Parameters:
//  pGrpIndex: The index of the message group (assumed to be valid)
//  pSubIndex: The index of the sub-board within the message group to write (assumed to be valid)
//  pHighlight: Boolean - Whether or not to write the line highlighted.
function DDMsgAreaChooser_GetMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight)
	// Write the highlight background color if pHighlight is true.
	if (pHighlight)
		subBoardLine += this.colors.bkgHighlight;

	// Determine if pGrpIndex and pSubIndex specify the user's
	// currently-selected group and sub-board.
	var currentSub = false;
	if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
		currentSub = ((pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIndex == msg_area.sub[bbs.cursub_code].index));

	// Open the current sub-board with the msgBase object (so that we can get
	// the date & time of the last imported message).
	var msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code);
	if (msgBase.open())
	{
		var numMsgs = numReadableMsgs(msgBase, msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code);
		var newestDate = {}; // For storing the date of the newest post
		// Get the date & time when the last message was imported.
		var msgHeader = getLatestMsgHdr(msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code);
		if (msgHeader != null)
		{
			// Construct the date & time strings of the latest post
			if (this.showImportDates)
			{
				newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time);
				newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time);
			}
			else
			{
				var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
				if (msgWrittenLocalBBSTime != -1)
				{
					newestDate.date = strftime("%Y-%m-%d", msgWrittenLocalBBSTime);
					newestDate.time = strftime("%H:%M:%S", msgWrittenLocalBBSTime);
				}
				else
				{
					newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
					newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
				}
			}
		}
		else
			newestDate.date = newestDate.time = "";

		// Print the sub-board information line.
		var lengthsObj = this.GetSubNameLenAndNumMsgsLen(pGrpIndex);
		subBoardLine += (currentSub ? this.colors.areaMark + "*" : " ");
			subBoardLine += format((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr),
			       +(pSubIndex+1), msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, lengthsObj.nameLen),
			       numMsgs, newestDate.date, newestDate.time);
		}
		else
		{
			subBoardLine += format((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr),
			       +(pSubIndex+1), msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, lengthsObj.nameLen),
}

///////////////////////////////////////////////
// Other functions for the msg. area chooser //
///////////////////////////////////////////////

// For the DDMsgAreaChooser class: Reads the configuration file.
function DDMsgAreaChooser_ReadConfigFile()
{
	// Determine the script's startup directory.
	// This code is a trick that was created by Deuce, suggested by Rob Swindell
	// as a way to detect which directory the script was executed in.  I've
	// shortened the code a little.
	var startup_path = '.';
	try { throw dig.dist(dist); } catch(e) { startup_path = e.fileName; }
	startup_path = backslash(startup_path.replace(/[\/\\][^\/\\]*$/,''));

	// Open the configuration file
	var cfgFile = new File(startup_path + "DDMsgAreaChooser.cfg");
	if (cfgFile.open("r"))
	{
		var behaviorSettings = cfgFile.iniGetObject("BEHAVIOR");
		var colorSettings = cfgFile.iniGetObject("COLORS");
		cfgFile.close();
		// Behavior settings
		var hdrMaxNumLines = parseInt(behaviorSettings["areaChooserHdrMaxLines"]);
		if (!isNaN(hdrMaxNumLines) && hdrMaxNumLines > 0)
			this.areaChooserHdrMaxLines = hdrMaxNumLines;
		if (typeof(behaviorSettings["useLightbarInterface"]) === "boolean")
			this.useLightbarInterface = behaviorSettings.useLightbarInterface;
		if (typeof(behaviorSettings["showImportDates"]) === "boolean")
			this.showImportDates = behaviorSettings.showImportDates;
		if (typeof(behaviorSettings["areaChooserHdrFilenameBase"]) === "string")
			this.areaChooserHdrFilenameBase = behaviorSettings.areaChooserHdrFilenameBase;
		if (typeof(behaviorSettings["useSubCollapsing"]) === "boolean")
			this.useSubCollapsing = behaviorSettings.useSubCollapsing;
		if (typeof(behaviorSettings["subCollapseSeparator"]) === "string" && behaviorSettings["subCollapseSeparator"].length > 0)
			this.subCollapseSeparator = behaviorSettings.subCollapseSeparator;
		if (typeof(behaviorSettings["showDatesInSubBoardList"]) === "boolean")
			this.showDatesInSubBoardList = behaviorSettings.showDatesInSubBoardList;
		// Color settings
		var onlySyncAttrsRegexWholeWord = new RegExp("^[\x01krgybmcw01234567hinq,;\.dtlasz]+$", 'i');
				// Make sure the value is a string (for attrCodeStr() etc; in some cases, such as a background attribute of 4, it will be a number)
				var value = colorSettings[prop].toString();
				// If the value doesn't have any control characters, then add the control character
				// before attribute characters
				if (!/\x01/.test(value))
					value = attrCodeStr(value);
				if (onlySyncAttrsRegexWholeWord.test(value))
					this.colors[prop] = value;
}

// For the DDMsgAreaChooser class: Shows the help screen
//
// Parameters:
//  pLightbar: Boolean - Whether or not to show lightbar help.  If
//             false, then this function will show regular help.
//  pClearScreen: Boolean - Whether or not to clear the screen first
function DDMsgAreaChooser_showHelpScreen(pLightbar, pClearScreen)
{
	if (pClearScreen)
		console.clear("\1n");
Loading
Loading full blame...