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);
						// 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);
						// 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;
}