Skip to content
Snippets Groups Projects
DDMsgReader.js 790 KiB
Newer Older
	this.MSGNUM_LEN = (typeof(pMsgNumLen) == "number" ? pMsgNumLen : this.NumMessages(null, true).toString().length);
	if (this.MSGNUM_LEN < 4)
		this.MSGNUM_LEN = 4;
	this.DATE_LEN = 10; // i.e., YYYY-MM-DD
	this.TIME_LEN = 8;  // i.e., HH:MM:SS
	// Variable field widths: From, to, and subject
	this.FROM_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0);
	this.TO_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0);
	//var colsLeftForSubject = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces
	//this.SUBJ_LEN = (console.screen_columns * (colsLeftForSubject/console.screen_columns)).toFixed(0);
	this.SUBJ_LEN = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces
	if (this.showScoresInMsgList)
	{
		this.SUBJ_LEN -= (this.SCORE_LEN + 1);
		this.sMsgListHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-"
		                          + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %"
		                          + this.SCORE_LEN + "s %-" + this.DATE_LEN + "s %-"
		                          + this.TIME_LEN + "s";

		this.sMsgInfoFormatStr = this.colors.msgListMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                       + this.colors.msgListFromColor + "%-" + this.FROM_LEN + "s "
		                       + this.colors.msgListToColor + "%-" + this.TO_LEN + "s "
		                       + this.colors.msgListSubjectColor + "%-" + this.SUBJ_LEN + "s "
		                       + this.colors.msgListScoreColor + "%" + this.SCORE_LEN + "d "
		                       + this.colors.msgListDateColor + "%-" + this.DATE_LEN + "s "
		                       + this.colors.msgListTimeColor + "%-" + this.TIME_LEN + "s";
		// Message information format string with colors to use when the message is
		// written to the user.
		this.sMsgInfoToUserFormatStr = this.colors.msgListToUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                             + this.colors.msgListToUserFromColor
		                             + "%-" + this.FROM_LEN + "s " + this.colors.msgListToUserToColor + "%-"
		                             + this.TO_LEN + "s " + this.colors.msgListToUserSubjectColor + "%-"
		                             + this.SUBJ_LEN + "s " + this.colors.msgListToUserScoreColor + "%"
		                             + this.SCORE_LEN + "d " + this.colors.msgListToUserDateColor
		                             + "%-" + this.DATE_LEN + "s " + this.colors.msgListToUserTimeColor
		                             + "%-" + this.TIME_LEN + "s";
		// Message information format string with colors to use when the message is
		// from the user.
		this.sMsgInfoFromUserFormatStr = this.colors.msgListFromUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                               + this.colors.msgListFromUserFromColor
		                               + "%-" + this.FROM_LEN + "s " + this.colors.msgListFromUserToColor + "%-"
		                               + this.TO_LEN + "s " + this.colors.msgListFromUserSubjectColor + "%-"
		                               + this.SUBJ_LEN + "s " + this.colors.msgListFromUserScoreColor + "%"
		                               + this.SCORE_LEN + "d " + this.colors.msgListFromUserDateColor
		                               + "%-" + this.DATE_LEN + "s " + this.colors.msgListFromUserTimeColor
		                               + "%-" + this.TIME_LEN + "s";
		// Highlighted message information line for the message list (used for the
		// lightbar interface)
		this.sMsgInfoFormatHighlightStr = this.colors.msgListMsgNumHighlightColor
		                                + "%" + this.MSGNUM_LEN + "d%s"
		                                + this.colors.msgListFromHighlightColor + "%-" + this.FROM_LEN
		                                + "s " + this.colors.msgListToHighlightColor + "%-" + this.TO_LEN + "s "
		                                + this.colors.msgListSubjHighlightColor + "%-" + this.SUBJ_LEN + "s "
		                                + this.colors.msgListScoreHighlightColor + "%" + this.SCORE_LEN + "d "
		                                + this.colors.msgListDateHighlightColor + "%-" + this.DATE_LEN + "s "
		                                + this.colors.msgListTimeHighlightColor + "%-" + this.TIME_LEN + "s";
	}
	else
	{
		this.sMsgListHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-"
		                          + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %-"
		                          + this.DATE_LEN + "s %-" + this.TIME_LEN + "s";

		this.sMsgInfoFormatStr = this.colors.msgListMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                       + this.colors.msgListFromColor + "%-" + this.FROM_LEN + "s "
		                       + this.colors.msgListToColor + "%-" + this.TO_LEN + "s "
		                       + this.colors.msgListSubjectColor + "%-" + this.SUBJ_LEN + "s "
		                       + this.colors.msgListDateColor + "%-" + this.DATE_LEN + "s "
		                       + this.colors.msgListTimeColor + "%-" + this.TIME_LEN + "s";
		// Message information format string with colors to use when the message is
		// written to the user.
		this.sMsgInfoToUserFormatStr = this.colors.msgListToUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                             + this.colors.msgListToUserFromColor
		                             + "%-" + this.FROM_LEN + "s " + this.colors.msgListToUserToColor + "%-"
		                             + this.TO_LEN + "s " + this.colors.msgListToUserSubjectColor + "%-"
		                             + this.SUBJ_LEN + "s " + this.colors.msgListToUserDateColor
		                             + "%-" + this.DATE_LEN + "s " + this.colors.msgListToUserTimeColor
		                             + "%-" + this.TIME_LEN + "s";
		// Message information format string with colors to use when the message is
		// from the user.
		this.sMsgInfoFromUserFormatStr = this.colors.msgListFromUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                               + this.colors.msgListFromUserFromColor
		                               + "%-" + this.FROM_LEN + "s " + this.colors.msgListFromUserToColor + "%-"
		                               + this.TO_LEN + "s " + this.colors.msgListFromUserSubjectColor + "%-"
		                               + this.SUBJ_LEN + "s " + this.colors.msgListFromUserDateColor
		                               + "%-" + this.DATE_LEN + "s " + this.colors.msgListFromUserTimeColor
		                               + "%-" + this.TIME_LEN + "s";
		// Highlighted message information line for the message list (used for the
		// lightbar interface)
		this.sMsgInfoFormatHighlightStr = this.colors.msgListMsgNumHighlightColor
		                                + "%" + this.MSGNUM_LEN + "d%s"
		                                + this.colors.msgListFromHighlightColor + "%-" + this.FROM_LEN
		                                + "s " + this.colors.msgListToHighlightColor + "%-" + this.TO_LEN + "s "
		                                + this.colors.msgListSubjHighlightColor + "%-" + this.SUBJ_LEN + "s "
		                                + this.colors.msgListDateHighlightColor + "%-" + this.DATE_LEN + "s "
		                                + this.colors.msgListTimeHighlightColor + "%-" + this.TIME_LEN + "s";
	}

	// If the user's terminal doesn't support ANSI, then append a newline to
	// the end of the header format string (we won't be able to move the cursor).
// For the DigDistMessageReader class: Writes a temporary error message at the key help line
// for lightbar mode.
//
// Parameters:
//  pErrorMsg: The error message to write
//  pHelpLineRefreshDef: Optional - Specifies which help line to refresh on the screen
//                       (i.e., REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE)
function DigDistMsgReader_WriteLightbarKeyHelpErrorMsg(pErrorMsg, pLineRefreshDef)
{
	console.gotoxy(1, console.screen_rows);
	console.cleartoeol("\x01n");
	console.gotoxy(1, console.screen_rows);
	console.print("\x01y\x01h" + pErrorMsg + "\x01n");
	mswait(ERROR_WAIT_MS);
	var helpLineRefreshDef = (typeof(pHelpLineRefreshDef) == "number" ? pHelpLineRefreshDef : -1);
	if (helpLineRefreshDef == REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE)
		this.WriteChgMsgAreaKeysHelpLine();
}

// For the scrollable reader interface: Refreshes a rectangular region on the screen
// by printing part of the message text.
//
//  pTxtLines: The array of text lines of the message being displayed
//  pTopLineIdx: The index of the text line currently at the top row in the reader area
//  pTopLeftX: The upper-left corner column of the rectangle to be refreshed (1-based) - Absolute screen coordinate
//  pTopLeftY: The upper-left corner row of the rectangle to be refreshed (1-based) - Absolute screen coordinate
//  pWidth: The width of the rectangle to be refreshed
//  pHeight: The height of the rectangle to be refreshed
function DigDistMsgReader_RefreshMsgAreaRectangle(pTxtLines, pTopLineIdx, pTopLeftX, pTopLeftY, pWidth, pHeight)
	if (typeof(pTxtLines) !== "object")
		return;
	if (typeof(pTopLineIdx) !== "number" || pTopLineIdx < 0 || pTopLineIdx >= pTxtLines.length)
		return;
	if (typeof(pTopLeftX) !== "number" || pTopLeftX < 1 || pTopLeftX > console.screen_columns)
		return;
	if (typeof(pTopLeftY) !== "number" || pTopLeftY < 1 || pTopLeftY > console.screen_rows)
		return;
	if (typeof(pWidth) !== "number" || pWidth <= 0 || typeof(pHeight) !== "number" || pHeight <= 0)
		return;

	var firstTxtLineIdx = pTopLeftY - pTopLineIdx - 1;
	if (firstTxtLineIdx < 0) firstTxtLineIdx = 0; // Shouldn't happen, but just in case
	//this.msgAreaLeft = 1;
	//this.msgAreaRight = console.screen_columns - 1;
	//this.msgAreaWidth = this.msgAreaRight - this.msgAreaLeft + 1;
	//this.msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
	// Sanity checking
	if (pTopLeftY < this.msgAreaTop)
	{
		var diff = this.msgAreaTop - pTopLeftY;
		pTopLeftY = this.msgAreaTop;
		pTopLineIdx += diff;
	}
	var lastScreenRow = pTopLeftY + pHeight - 1; // Inclusive, for loop
	if (lastScreenRow > this.msgAreaBottom)
		lastScreenRow = this.msgAreaBottom;
	if (pTopLeftX + pWidth > this.msgAreaRight)
		pWidth = this.msgAreaRight - pTopLeftX + 1;

	// Print the parts of the text lines that make up the rectangle
	var txtLineIdx = pTopLineIdx + (pTopLeftY-this.msgAreaTop);
	if (txtLineIdx < 0) txtLineIdx = 0; // Just in case, but shouldn't happen
	var txtLineStartIdx = pTopLeftX - this.msgAreaLeft; // Within each text line (it seemed right to subtract 1 but it wasn't)
	if (txtLineStartIdx < 0) txtLineStartIdx = 0; // Just in case, but shouldn't happen
	var emptyFormatStr = "\x01n%" + pWidth + "s"; // For printing empty strings after printing all text lines
	console.print("\x01n");
	for (var screenRow = pTopLeftY; screenRow <= lastScreenRow; ++screenRow)
	{
		console.gotoxy(pTopLeftX, screenRow);
		// If the current text line index is within the array of text lines, then output the section of the
		// text line.  Otherwise, output an empty string.
		if (txtLineIdx < pTxtLines.length)
		{
			// Get the text attributes up to the current point and output them
			console.print(getAllEditLineAttrsUntilLineIdx(pTxtLines, txtLineIdx, true, txtLineStartIdx));
			// Get the section of line (and make sure it can fill the needed width), and print it
			// Note: substrWithAttrCodes(pStr, pStartIdx, pLen) is defined in dd_lightbar_menu.js
			var lineText = substrWithAttrCodes(pTxtLines[txtLineIdx], txtLineStartIdx, pWidth);
			var printableTxtLen = console.strlen(lineText);
			if (printableTxtLen < pWidth)
			{
				var lenDiff = pWidth - printableTxtLen;
				lineText += format("\x01n%" + lenDiff + "s", "");
			}
			console.print(lineText);
		}
		else // We've printed all the remaining text lines, so now print an empty string.
			printf(emptyFormatStr, "");

		++txtLineIdx;
	}
}

// For the scrollable reader interface: Returns whether a 'from' or 'to' name in a message header
// is in the user's personal twitlist.
//
// Parameters:
//  pMsgHdr: A message header to check
//
// Return value: Boolean - Whether or not the header 'from' or 'to' name is in the user's personal twitlist
function DigDistMsgReader_MsgHdrFromOrToInUserTwitlist(pMsgHdr)
{
	if (pMsgHdr == null || typeof(pMsgHdr) !== "object")
		return false;
	if (!pMsgHdr.hasOwnProperty("from") && !pMsgHdr.hasOwnProperty("to"))
		return false;

	// The names in the user's twitlist have been converted to lowercase for case-insensitive matching.
	var fromLower = pMsgHdr.from.toLowerCase();
	var toLower = pMsgHdr.to.toLowerCase();
	var hdrNamesInTwitlist = false;
	for (var i = 0; i < this.userSettings.twitList.length && !hdrNamesInTwitlist; ++i)
		hdrNamesInTwitlist = (this.userSettings.twitList[i] == fromLower || this.userSettings.twitList[i] == toLower);
	return hdrNamesInTwitlist;
}

///////////////////////////////////////////////////////////////////////////////////
// Helper functions

// Displays the program information.
function DisplayProgramInfo()
{
	displayTextWithLineBelow("Digital Distortion Message Reader", true, "\x01n\x01c\x01h", "\x01k\x01h")
	console.center("\x01n\x01cVersion \x01g" + READER_VERSION + " \x01w\x01h(\x01b" + READER_DATE + "\x01w)");
	console.crlf();
}

// This function returns an array of default colors used in the
// DigDistMessageReader class.
function getDefaultColors()
{
	return {
		// Colors for the message header displayed above messages in the scrollable reader mode
		msgHdrMsgNumColor: "\x01n\x01b\x01h", // Message #
		msgHdrFromColor: "\x01n\x01b\x01h",   // From username
		msgHdrToColor: "\x01n\x01b\x01h",     // To username
		msgHdrToUserColor: "\x01n\x01g\x01h", // To username when it's to the current user
		msgHdrSubjColor: "\x01n\x01b\x01h",   // Message subject
		msgHdrDateColor: "\x01n\x01b\x01h",   // Message date

		// Message list header line: "Current msg group:"
		msgListHeaderMsgGroupTextColor: "\x01n\x01" + "4\x01c", 	// Normal cyan on blue background
		//	msgListHeaderMsgGroupTextColor: "\x01n\x01" + "4\x01w", 	// Normal white on blue background

		// Message list header line: Message group name
		msgListHeaderMsgGroupNameColor: "\x01h\x01c", 	// High cyan
		//	msgListHeaderMsgGroupNameColor: "\x01h\x01w", 	// High white

		// Message list header line: "Current sub-board:"
		msgListHeaderSubBoardTextColor: "\x01n\x01" + "4\x01c", 	// Normal cyan on blue background
		//	msgListHeaderSubBoardTextColor: "\x01n\x01" + "4\x01w", 	// Normal white on blue background

		// Message list header line: Message sub-board name
		msgListHeaderMsgSubBoardName: "\x01h\x01c", 	// High cyan
		//	msgListHeaderMsgSubBoardName: "\x01h\x01w", 	// High white
		//	msgListColHeader: "\x01h\x01w", 	// High white (keep blue background)
		msgListColHeader: "\x01n\x01h\x01w", 	// High white on black background
		//	msgListColHeader: "\x01h\x01c", 	// High cyan (keep blue background)
		//	msgListColHeader: "\x01" + "4\x01h\x01y", 	// High yellow (keep blue background)
		msgListMsgNumColor: "\x01n\x01h\x01y",
		msgListFromColor: "\x01n\x01c",
		msgListToColor: "\x01n\x01c",
		msgListSubjectColor: "\x01n\x01c",
		msgListScoreColor: "\x01n\x01c",
		msgListDateColor: "\x01h\x01b",
		msgListTimeColor: "\x01h\x01b",
		// Message information for messages written to the user
		msgListToUserMsgNumColor: "\x01n\x01h\x01y",
		msgListToUserFromColor: "\x01h\x01g",
		msgListToUserToColor: "\x01h\x01g",
		msgListToUserSubjectColor: "\x01h\x01g",
		msgListToUserScoreColor: "\x01h\x01g",
		msgListToUserDateColor: "\x01h\x01b",
		msgListToUserTimeColor: "\x01h\x01b",
		// Message information for messages from the user
		msgListFromUserMsgNumColor: "\x01n\x01h\x01y",
		msgListFromUserFromColor: "\x01n\x01c",
		msgListFromUserToColor: "\x01n\x01c",
		msgListFromUserSubjectColor: "\x01n\x01c",
		msgListFromUserScoreColor: "\x01n\x01c",
		msgListFromUserDateColor: "\x01h\x01b",
		msgListFromUserTimeColor: "\x01h\x01b",
		msgListHighlightBkgColor: "\x01" + "4", 	// Background
		msgListMsgNumHighlightColor: "\x01h\x01y",
		msgListFromHighlightColor: "\x01h\x01c",
		msgListToHighlightColor: "\x01h\x01c",
		msgListSubjHighlightColor: "\x01h\x01c",
		msgListScoreHighlightColor: "\x01h\x01c",
		msgListDateHighlightColor: "\x01h\x01w",
		msgListTimeHighlightColor: "\x01h\x01w",
		lightbarMsgListHelpLineBkgColor: "\x01" + "7", 	// Background
		lightbarMsgListHelpLineGeneralColor: "\x01b",
		lightbarMsgListHelpLineHotkeyColor: "\x01r",
		lightbarMsgListHelpLineParenColor: "\x01m",
		tradInterfaceContPromptMainColor: "\x01n\x01g", 	// Main text color
		tradInterfaceContPromptHotkeyColor: "\x01h\x01c", 	// Hotkey color
		tradInterfaceContPromptUserInputColor: "\x01h\x01g", 	// User input color
		msgBodyColor: "\x01n\x01w",
		readMsgConfirmColor: "\x01n\x01c",
		readMsgConfirmNumberColor: "\x01h\x01c",
		// Prompt for continuing to list messages after reading a message
		afterReadMsg_ListMorePromptColor: "\x01n\x01c",
		tradInterfaceHelpScreenColor: "\x01n\x01h\x01w",

		// Colors for choosing a message group & sub-board
		areaChooserMsgAreaNumColor: "\x01n\x01w\x01h",
		areaChooserMsgAreaDescColor: "\x01n\x01c",
		areaChooserMsgAreaNumItemsColor: "\x01b\x01h",
		areaChooserMsgAreaHeaderColor: "\x01n\x01y\x01h",
		areaChooserSubBoardHeaderColor: "\x01n\x01g",
		areaChooserMsgAreaMarkColor: "\x01g\x01h",
		areaChooserMsgAreaLatestDateColor: "\x01n\x01g",
		areaChooserMsgAreaLatestTimeColor: "\x01n\x01m",
		// Highlighted colors (for lightbar mode)
		areaChooserMsgAreaBkgHighlightColor: "\x01" + "4", 	// Blue background
		areaChooserMsgAreaNumHighlightColor: "\x01w\x01h",
		areaChooserMsgAreaDescHighlightColor: "\x01c",
		areaChooserMsgAreaDateHighlightColor: "\x01w\x01h",
		areaChooserMsgAreaTimeHighlightColor: "\x01w\x01h",
		areaChooserMsgAreaNumItemsHighlightColor: "\x01w\x01h",
		lightbarAreaChooserHelpLineBkgColor: "\x01" + "7", 	// Background
		lightbarAreaChooserHelpLineGeneralColor: "\x01b",
		lightbarAreaChooserHelpLineHotkeyColor: "\x01r",
		lightbarAreaChooserHelpLineParenColor: "\x01m",

		// Scrollbar background and scroll block colors (for the enhanced
		// message reader interface)
		scrollbarBGColor: "\x01n\x01h\x01k",
		scrollbarScrollBlockColor: "\x01n\x01h\x01w",
		// Color for the line drawn in the 2nd to last line of the message
		// area in the enhanced reader mode before a prompt
		enhReaderPromptSepLineColor: "\x01n\x01h\x01g",
		// Colors for the enhanced reader help line
		enhReaderHelpLineBkgColor: "\x01" + "7",
		enhReaderHelpLineGeneralColor: "\x01b",
		enhReaderHelpLineHotkeyColor: "\x01r",
		enhReaderHelpLineParenColor: "\x01m",
		hdrLineLabelColor: "\x01n\x01c",
		hdrLineValueColor: "\x01n\x01b\x01h",
		selectedMsgMarkColor: "\x01n\x01w\x01h"
}

// This function returns the month number (1-based) from a capitalized
// month name.
//
// Parameters:
//  pMonthName: The name of the month
//
// Return value: The number of the month (1-12).
function getMonthNum(pMonthName)
{
	var monthNum = 1;

	if (pMonthName.substr(0, 3) == "Jan")
		monthNum = 1;
	else if (pMonthName.substr(0, 3) == "Feb")
		monthNum = 2;
	else if (pMonthName.substr(0, 3) == "Mar")
		monthNum = 3;
	else if (pMonthName.substr(0, 3) == "Apr")
		monthNum = 4;
	else if (pMonthName.substr(0, 3) == "May")
		monthNum = 5;
	else if (pMonthName.substr(0, 3) == "Jun")
		monthNum = 6;
	else if (pMonthName.substr(0, 3) == "Jul")
		monthNum = 7;
	else if (pMonthName.substr(0, 3) == "Aug")
		monthNum = 8;
	else if (pMonthName.substr(0, 3) == "Sep")
		monthNum = 9;
	else if (pMonthName.substr(0, 3) == "Oct")
		monthNum = 10;
	else if (pMonthName.substr(0, 3) == "Nov")
		monthNum = 11;
	else if (pMonthName.substr(0, 3) == "Dec")
		monthNum = 12;

	return monthNum;
}

// Clears each line from a given line to the end of the screen.
//
// Parameters:
//  pStartLineNum: The line number to start at (1-based)
function clearToEOS(pStartLineNum)
{
	if (typeof(pStartLineNum) == "undefined")
		return;
	if (pStartLineNum == null)
		return;

	for (var lineNum = pStartLineNum; lineNum <= console.screen_rows; ++lineNum)
	{
		console.gotoxy(1, lineNum);
		console.clearline();
	}
}

// Returns the number of messages in a sub-board.
//
// Parameters:
//  pSubBoardCode: The sub-board code (i.e., from bbs.cursub_code)
//
// Return value: The number of messages in the sub-board, or 0
//               if the sub-board could not be opened.
function numMessages(pSubBoardCode)
{
   var messageCount = 0;

   var myMsgbase = new MsgBase(pSubBoardCode);
	if (myMsgbase.open())
		messageCount = myMsgbase.total_msgs;
	myMsgbase.close();
	myMsgbase = null;

	return messageCount;
}

// Removes multiple, leading, and/or trailing spaces
// The search & replace regular expressions used in this
// function came from the following URL:
//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
//
// Parameters:
//  pString: The string to trim
//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
//
// Return value: The string with whitespace trimmed
function trimSpaces(pString, pLeading, pMultiple, pTrailing)
{
	var leading = true;
	var multiple = true;
	var trailing = true;
	if(typeof(pLeading) != "undefined")
		leading = pLeading;
	if(typeof(pMultiple) != "undefined")
		multiple = pMultiple;
	if(typeof(pTrailing) != "undefined")
		trailing = pTrailing;

	// To remove both leading & trailing spaces:
	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");

	if (leading)
		pString = pString.replace(/(^\s*)/gi,"");
	if (multiple)
		pString = pString.replace(/[ ]{2,}/gi," ");
	if (trailing)
		pString = pString.replace(/(\s*$)/gi,"");

	return pString;
}

// Returns whether an internal sub-board code is valid.
//
// Parameters:
//  pSubBoardCode: The internal sub-board code to test
//
// Return value: Boolean - Whether or not the given internal code is a valid
//               sub-board code
function subBoardCodeIsValid(pSubBoardCode)
{
   return ((pSubBoardCode == "mail") || (typeof(msg_area.sub[pSubBoardCode]) == "object"))
}

// Displays some text with a solid horizontal line on the next line.
//
// Parameters:
//  pText: The text to display
//  pCenter: Whether or not to center the text.  Optional; defaults
//           to false.
//  pTextColor: The color to use for the text.  Optional; by default,
//              normal white will be used.
//  pLineColor: The color to use for the line underneath the text.
//              Optional; by default, bright black will be used.
function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor)
{
	var centerText = (typeof(pCenter) == "boolean" ? pCenter : false);
	var textColor = (typeof(pTextColor) == "string" ? pTextColor : "\x01n\x01w");
	var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\x01n\x01k\x01h");
	// Output the text and a solid line on the next line.
	if (centerText)
	{
		console.center(textColor + pText);
		var solidLine = "";
		var textLength = console.strlen(pText);
		for (var i = 0; i < textLength; ++i)
		console.center(lineColor + solidLine);
	}
	else
	{
		console.print(textColor + pText);
		console.crlf();
		console.print(lineColor);
		var textLength = console.strlen(pText);
		for (var i = 0; i < textLength; ++i)
			console.print(HORIZONTAL_SINGLE);
}

// Removes multiple, leading, and/or trailing spaces.
// The search & replace regular expressions used in this
// function came from the following URL:
//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
//
// Parameters:
//  pString: The string to trim
//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
function trimSpaces(pString, pLeading, pMultiple, pTrailing)
{
	var leading = true;
	var multiple = true;
	var trailing = true;
	if(typeof(pLeading) != "undefined")
		leading = pLeading;
	if(typeof(pMultiple) != "undefined")
		multiple = pMultiple;
	if(typeof(pTrailing) != "undefined")
		trailing = pTrailing;
		
	// To remove both leading & trailing spaces:
	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");

	if (leading)
		pString = pString.replace(/(^\s*)/gi,"");
	if (multiple)
		pString = pString.replace(/[ ]{2,}/gi," ");
	if (trailing)
		pString = pString.replace(/(\s*$)/gi,"");

	return pString;
}

// Calculates & returns a page number.
//
// Parameters:
//  pTopIndex: The index (0-based) of the topmost item on the page
//  pNumPerPage: The number of items per page
//
// Return value: The page number
function calcPageNum(pTopIndex, pNumPerPage)
{
  return ((pTopIndex / pNumPerPage) + 1);
}

// Returns the greatest number of messages of all sub-boards within
// a message group.
//
// Parameters:
//  pGrpIndex: The index of the message group
//
// Returns: The greatest number of messages of all sub-boards within
//          the message group
function getGreatestNumMsgs(pGrpIndex)
{
  // Sanity checking
  if (typeof(pGrpIndex) != "number")
    return 0;
  if (typeof(msg_area.grp_list[pGrpIndex]) == "undefined")
    return 0;

  var greatestNumMsgs = 0;
  var msgBase = null;
  for (var subIndex = 0; subIndex < msg_area.grp_list[pGrpIndex].sub_list.length; ++subIndex)
  {
    msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[subIndex].code);
    if (msgBase == null) continue;
    if (msgBase.open())
    {
      if (msgBase.total_msgs > greatestNumMsgs)
        greatestNumMsgs = msgBase.total_msgs;
      msgBase.close();
    }
  }
  return greatestNumMsgs;
}

// Inputs a keypress from the user and handles some ESC-based
// characters such as PageUp, PageDown, and ESC.  If PageUp
// or PageDown are pressed, this function will return the
// string defined by KEY_PAGE_UP or KEY_PAGE_DOWN,
// respectively.  Also, F1-F5 will be returned as "\x01F1"
// through "\x01F5", respectively.
// Thanks goes to Psi-Jack for the original impementation
// of this function.
//
// Parameters:
//  pGetKeyMode: Optional - The mode bits for console.getkey().
//               If not specified, K_NONE will be used.
//
// Return value: The user's keypress
function getKeyWithESCChars(pGetKeyMode)
{
	var getKeyMode = K_NONE;
	if (typeof(pGetKeyMode) == "number")
		getKeyMode = pGetKeyMode;

	var userInput = console.getkey(getKeyMode);
	if (userInput == KEY_ESC) {
		switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
			case '[':
				switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
					case 'V':
						userInput = KEY_PAGE_UP;
						break;
					case 'U':
						userInput = KEY_PAGE_DOWN;
						break;
				}
				break;
			case 'O':
				switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
					case 'P':
}

// Finds the next or previous non-empty message sub-board.  Returns an
// object containing the message group & sub-board indexes.  If all of
// the next/previous sub-boards are empty, then the given current indexes
// will be returned.
//
// Parameters:
//  pStartGrpIdx: The index of the message group to start from
//  pStartSubIdx: The index of the sub-board in the message group to start from
//  pForward: Boolean - Whether or not to search forward (true) or backward (false).
//            Optional; defaults to true, to search forward.
//
// Return value: An object with the following properties:
//               foundSubBoard: Boolean - Whether or not a different sub-board was found
//               grpIdx: The message group index of the found sub-board
//               subIdx: The sub-board index in the group of the found sub-board
//               subCode: The internal code of the sub-board
//               subChanged: Boolean - Whether or not the found sub-board is
//                           different from the one that was passed in
//               paramsValid: Boolean - Whether or not all the passed-in parameters
//                            were valid.
function findNextOrPrevNonEmptySubBoard(pStartGrpIdx, pStartSubIdx, pForward)
{
   var retObj = {
	   grpIdx: pStartGrpIdx,
	   subIdx: pStartSubIdx,
	   subCode: msg_area.grp_list[pStartGrpIdx].sub_list[pStartSubIdx].code,
	   foundSubBoard: false
   };

   // Sanity checking
   retObj.paramsValid = ((pStartGrpIdx >= 0) && (pStartGrpIdx < msg_area.grp_list.length) &&
                         (pStartSubIdx >= 0) &&
                         (pStartSubIdx < msg_area.grp_list[pStartGrpIdx].sub_list.length));
   if (!retObj.paramsValid)
      return retObj;

   var grpIdx = pStartGrpIdx;
   var subIdx = pStartSubIdx;
   var searchForward = (typeof(pForward) == "boolean" ? pForward : true);
   if (searchForward)
   {
      // Advance the sub-board (and group) index, and determine whether or not
      // to do the search (i.e., we might not want to if the starting sub-board
      // is the last sub-board in the last group).
      var searchForSubBoard = true;
      if (subIdx <  msg_area.grp_list[grpIdx].sub_list.length - 1)
         ++subIdx;
      else
      {
         if ((grpIdx < msg_area.grp_list.length - 1) && (msg_area.grp_list[grpIdx+1].sub_list.length > 0))
         {
            subIdx = 0;
            ++grpIdx;
         }
         else
            searchForSubBoard = false;
      }
      // If we can search, then do it.
      if (searchForSubBoard)
      {
         while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0)
         {
            if (subIdx < msg_area.grp_list[grpIdx].sub_list.length - 1)
               ++subIdx;
            else
            {
               if ((grpIdx < msg_area.grp_list.length - 1) && (msg_area.grp_list[grpIdx+1].sub_list.length > 0))
               {
                  subIdx = 0;
                  ++grpIdx;
               }
               else
                  break; // Stop searching
            }
         }
      }
   }
   else
   {
      // Search the sub-boards in reverse
      // Decrement the sub-board (and group) index, and determine whether or not
      // to do the search (i.e., we might not want to if the starting sub-board
      // is the first sub-board in the first group).
      var searchForSubBoard = true;
      if (subIdx > 0)
         --subIdx;
      else
      {
         if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0))
         {
            --grpIdx;
            subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1;
         }
         else
            searchForSubBoard = false;
      }
      // If we can search, then do it.
      if (searchForSubBoard)
      {
         while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0)
         {
            if (subIdx > 0)
               --subIdx;
            else
            {
               if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0))
               {
                  --grpIdx;
                  subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1;
               }
               else
                  break; // Stop searching
            }
         }
      }
   }
   // If we found a sub-board with messages in it, then set the variables
   // in the return object
   if (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) > 0)
   {
      retObj.grpIdx = grpIdx;
      retObj.subIdx = subIdx;
      retObj.subCode = msg_area.grp_list[grpIdx].sub_list[subIdx].code;
      retObj.foundSubBoard = true;
      retObj.subChanged = ((grpIdx != pStartGrpIdx) || (subIdx != pStartSubIdx));
   }

   return retObj;
}

// Returns the number of messages in a sub-board.
//
// Parameters:
//  pSubBoardCode: The internal code of the sub-board to check
//  pIncludeDeleted: Optional boolean - Whether or not to include deleted
//                   messages in the count.  Defaults to false.
//
// Return value: The number of messages in the sub-board
function numMsgsInSubBoard(pSubBoardCode, pIncludeDeleted)
{
   var numMessages = 0;
   var msgbase = new MsgBase(pSubBoardCode);
   if (msgbase.open())
   {
      var includeDeleted = (typeof(pIncludeDeleted) == "boolean" ? pIncludeDeleted : false);
      if (includeDeleted)
         numMessages = msgbase.total_msgs;
      else
      {
         // Don't include deleted messages.  Go through each message
         // in the sub-board and count the ones that aren't marked
         // as deleted.
         for (var msgIdx = 0; msgIdx < msgbase.total_msgs; ++msgIdx)
         {
            var msgHdr = msgbase.get_msg_header(true, msgIdx, false);
            if ((msgHdr != null) && ((msgHdr.attr & MSG_DELETE) == 0))
               ++numMessages;
         }
      }
      msgbase.close();
   }
   return numMessages;
}

// Replaces @-codes in a string and returns the new string.
//
// Parameters:
//  pStr: A string in which to replace @-codes
//
// Return value: A version of the string with @-codes interpreted
function replaceAtCodesInStr(pStr)
{
	if (typeof(pStr) != "string")
		return "";

	// This code was originally written by Deuce.  I updated it to check whether
	// the string returned by bbs.atcode() is null, and if so, just return
	// the original string.
	return pStr.replace(/@([^@]+)@/g, function(m, code) {
		var decoded = bbs.atcode(code);
		return (decoded != null ? decoded : "@" + code + "@");
	});
}

// Shortens a string, accounting for control/attribute codes.  Returns a new
// (shortened) copy of the string.
//
// Parameters:
//  pStr: The string to shorten
//  pNewLength: The new (shorter) length of the string
//  pFromLeft: Optional boolean - Whether to start from the left (default) or
//             from the right.  Defaults to true.
//
// Return value: The shortened version of the string
function shortenStrWithAttrCodes(pStr, pNewLength, pFromLeft)
	if (typeof(pStr) != "string")
		return "";
	if (typeof(pNewLength) != "number")
		return pStr;
	if (pNewLength >= console.strlen(pStr))
		return pStr;

	var fromLeft = (typeof(pFromLeft) == "boolean" ? pFromLeft : true);
	var strCopy = "";
	var tmpStr = "";
	var strIdx = 0;
	var lengthGood = true;
	if (fromLeft)
	{
		while (lengthGood && (strIdx < pStr.length))
		{
			tmpStr = strCopy + pStr.charAt(strIdx++);
			if (console.strlen(tmpStr) <= pNewLength)
				strCopy = tmpStr;
			else
				lengthGood = false;
		}
	}
	else
	{
		strIdx = pStr.length - 1;
		while (lengthGood && (strIdx >= 0))
		{
			tmpStr = pStr.charAt(strIdx--) + strCopy;
			if (console.strlen(tmpStr) <= pNewLength)
				strCopy = tmpStr;
			else
				lengthGood = false;
		}
	}
	return strCopy;
}

// Returns whether a given name matches the logged-in user's handle, alias, or
// name.
//
// Parameters:
//  pName: A name to match against the logged-in user
//
// Return value: Boolean - Whether or not the given name matches the logged-in
//               user's handle, alias, or name
function userHandleAliasNameMatch(pName)
{
	if (typeof(pName) != "string")
		return false;
	var userMatch = false;
	var nameUpper = pName.toUpperCase();
	if (user.handle.length > 0)
		userMatch = (nameUpper.indexOf(user.handle.toUpperCase()) > -1);
	if (!userMatch && (user.alias.length > 0))
		userMatch = (nameUpper.indexOf(user.alias.toUpperCase()) > -1);
	if (!userMatch && (user.name.length > 0))
		userMatch = (nameUpper.indexOf(user.name.toUpperCase()) > -1);
	return userMatch;
}

// Displays a range of text lines on the screen and allows scrolling through them
// with the up & down arrow keys, PageUp, PageDown, HOME, and END.  It is assumed
// that the array of text lines are already truncated to fit in the width of the
// text area, as a speed optimization.
//
// Parameters:
//  pTxtLines: The array of text lines to allow scrolling for
//  pTopLineIdx: The index of the text line to display at the top
//  pTxtAttrib: The attribute(s) to apply to the text lines
//  pWriteTxtLines: Boolean - Whether or not to write the text lines (in addition
//                  to doing the message loop).  If false, this will only do the
//                  the message loop.  This parameter is intended as a screen
//                  refresh optimization.
//  pTopLeftX: The upper-left corner column for the text area
//  pTopLeftY: The upper-left corner row for the text area
//  pWidth: The width of the text area
//  pHeight: The height of the text area
//  pPostWriteCurX: The X location for the cursor after writing the message
//                  lines
//  pPostWriteCurY: The Y location for the cursor after writing the message
//                  lines
//  pScrollUpdateFn: A function that the caller can provide for updating the
//                   scroll position.  This function has one parameter:
//                   - fractionToLastPage: The fraction of the top index divided
//                     by the top index for the last page (basically, the progress
//                     to the last page).
//
// Return value: An object with the following properties:
//               lastKeypress: The last key pressed by the user (a string)
//               topLineIdx: The new top line index of the text lines, in case of scrolling
function scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTopLeftX, pTopLeftY,
                         pWidth, pHeight, pPostWriteCurX, pPostWriteCurY, pScrollUpdateFn,
	// Variables for the top line index for the last page, scrolling, etc.
	var topLineIdxForLastPage = pTxtLines.length - pHeight;
	if (topLineIdxForLastPage < 0)
		topLineIdxForLastPage = 0;
	var msgFractionShown = pHeight / pTxtLines.length;
	if (msgFractionShown > 1)
		msgFractionShown = 1.0;
	var fractionToLastPage = 0;
	var lastTxtRow = pTopLeftY + pHeight - 1;
	var txtLineFormatStr = "%-" + pWidth + "s";
	};

	// Create an array of color/attribute codes for each line of
	// text, in case there are any such codes in the text lines,
	// so that the colors in the message are displayed properly.
	// First, get the last color/attribute codes from first text
	// line and apply them to the next line, and so on.
	var attrCodes = getAttrsBeforeStrIdx(pTxtLines[0], pTxtLines[0].length-1);
	for (var lineIdx = 1; lineIdx < pTxtLines.length; ++lineIdx)
	{
		pTxtLines[lineIdx] = attrCodes + pTxtLines[lineIdx];
		attrCodes = getAttrsBeforeStrIdx(pTxtLines[lineIdx], pTxtLines[lineIdx].length-1);
	}
	var writeTxtLines = pWriteTxtLines;
	var continueOn = true;
	var mouseInputOnly_continue = false;