// $Id: SlyEdit_DCTStuff.js,v 1.21 2019/06/06 03:50:47 nightfox Exp $

/* This file contains DCTEdit-specific functions for SlyEdit.
 *
 * Author: Eric Oulashin (AKA Nightfox)
 * BBS: Digital Distortion
 * BBS address: digdist.bbsindex.com
 *
 * Date       User              Description
 * 2009-05-11 Eric Oulashin     Started development
 * 2009-08-09 Eric Oulashin     More development & testing
 * 2009-08-22 Eric Oulashin     Version 1.00
 *                              Initial public release
 * ... Removed comments ...
 * 2019-05-04 Eric Oulashin     Updated to use require() instead of load() if possible.
 * 2021-12-11 Eric Oulashin     Updated the quote window bottom border text
 */

if (typeof(require) === "function")
{
	require("sbbsdefs.js", "K_NOCRLF");
	require(getScriptDir() + "SlyEdit_Misc.js", "CTRL_A");
}
else
{
	load("sbbsdefs.js");
	load(getScriptDir() + "SlyEdit_Misc.js");
}

// DCTEdit menu item return values
var DCTMENU_FILE_SAVE = 0;
var DCTMENU_FILE_ABORT = 1;
var DCTMENU_FILE_EDIT = 2;
var DCTMENU_EDIT_INSERT_TOGGLE = 3;
var DCTMENU_EDIT_FIND_TEXT = 4;
var DCTMENU_EDIT_SPELL_CHECKER = 5;
var DCTMENU_EDIT_SETTINGS = 6;
var DCTMENU_SYSOP_IMPORT_FILE = 7;
var DCTMENU_SYSOP_EXPORT_FILE = 11;
var DCTMENU_HELP_COMMAND_LIST = 8;
var DCTMENU_HELP_GENERAL = 9;
var DCTMENU_HELP_PROGRAM_INFO = 10;
var DCTMENU_CROSS_POST = 12;
var DCTMENU_LIST_TXT_REPLACEMENTS = 13;

// Read the color configuration file
readColorConfig(gConfigSettings.DCTColors.ThemeFilename);

///////////////////////////////////////////////////////////////////////////////////
// Functions

// This function reads the color configuration for DCT style.
//
// Parameters:
//  pFilename: The name of the color configuration file
function readColorConfig(pFilename)
{
   var colors = readValueSettingConfigFile(pFilename, 512);
   if (colors != null)
   {
      gConfigSettings.DCTColors = colors;
      // Move the general color settings into gConfigSettings.genColors.*
      if (EDITOR_STYLE == "DCT")
        moveGenColorsToGenSettings(gConfigSettings.DCTColors, gConfigSettings);
   }
}

// Sets up any global screen-related variables needed for DCT style
function globalScreenVarsSetup_DCTStyle()
{
	gSubjPos.x = 12;
	gSubjPos.y = 4;
	gSubjScreenLen = console.screen_columns - 15;
}

// Re-draws the screen, in the style of DCTEdit.
//
// Parameters:
//  pEditLeft: The leftmost column of the edit area
//  pEditRight: The rightmost column of the edit area
//  pEditTop: The topmost row of the edit area
//  pEditBottom: The bottommost row of the edit area
//  pEditColor: The edit color
//  pInsertMode: The insert mode ("INS" or "OVR")
//  pUseQuotes: Whether or not message quoting is enabled
//  pEditLinesIndex: The index of the message line at the top of the edit area
//  pDisplayEditLines: The function that displays the edit lines
function redrawScreen_DCTStyle(pEditLeft, pEditRight, pEditTop, pEditBottom, pEditColor,
                               pInsertMode, pUseQuotes, pEditLinesIndex, pDisplayEditLines)
{
	// Top header
	// Generate & display the top border line (Note: Generate this
	// border only once, for efficiency).
	if (typeof(redrawScreen_DCTStyle.topBorder) == "undefined")
	{
		var innerWidth = console.screen_columns - 2;
		redrawScreen_DCTStyle.topBorder = UPPER_LEFT_SINGLE;
		for (var i = 0; i < innerWidth; ++i)
			redrawScreen_DCTStyle.topBorder += HORIZONTAL_SINGLE;
		redrawScreen_DCTStyle.topBorder += UPPER_RIGHT_SINGLE;
		redrawScreen_DCTStyle.topBorder = randomTwoColorString(redrawScreen_DCTStyle.topBorder,
		                                                       gConfigSettings.DCTColors.TopBorderColor1,
		                                                       gConfigSettings.DCTColors.TopBorderColor2);
	}
	// Print the border line on the screen
	console.clear();
	console.print(redrawScreen_DCTStyle.topBorder);

	// Next line
	// From name
	var lineNum = 2;
	console.gotoxy(1, lineNum);
	// Calculate the width of the from name field: 28 characters, based
	// on an 80-column screen width.
	var fieldWidth = (console.screen_columns * (28/80)).toFixed(0);
	screenText = gFromName.substr(0, fieldWidth);
	console.print(randomTwoColorString(VERTICAL_SINGLE, gConfigSettings.DCTColors.TopBorderColor1,
	                                   gConfigSettings.DCTColors.TopBorderColor2) +
				  " " + gConfigSettings.DCTColors.TopLabelColor + "From " +
				  gConfigSettings.DCTColors.TopLabelColonColor + ": " +
				  gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
				  gConfigSettings.DCTColors.TopFromFillColor + DOT_CHAR + 
				  gConfigSettings.DCTColors.TopFromColor + screenText +
				  gConfigSettings.DCTColors.TopFromFillColor);
	fieldWidth -= (screenText.length+1);
	for (var i = 0; i < fieldWidth; ++i)
		console.print(DOT_CHAR);
	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "]");

	// Message area
	fieldWidth = (console.screen_columns * (27/80)).toFixed(0);
	screenText = gMsgArea.substr(0, fieldWidth);
	var startX = console.screen_columns - fieldWidth - 9;
	console.gotoxy(startX, lineNum);
	console.print(gConfigSettings.DCTColors.TopLabelColor + "Area" +
	              gConfigSettings.DCTColors.TopLabelColonColor + ": " +
	              gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
	              gConfigSettings.DCTColors.TopAreaFillColor + DOT_CHAR +
	              gConfigSettings.DCTColors.TopAreaColor + screenText +
	              gConfigSettings.DCTColors.TopAreaFillColor);
	fieldWidth -= (screenText.length+1);
	for (var i = 0; i < fieldWidth; ++i)
		console.print(DOT_CHAR);
	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "] " +
	              randomTwoColorString(VERTICAL_SINGLE,
	                                   gConfigSettings.DCTColors.TopBorderColor1,
	                                   gConfigSettings.DCTColors.TopBorderColor2));

	// Next line: To, Time, time Left
	++lineNum;
	console.gotoxy(1, lineNum);
	// To name
	fieldWidth = (console.screen_columns * (28/80)).toFixed(0);
	screenText = gToName.substr(0, fieldWidth);
	console.print(randomTwoColorString(VERTICAL_SINGLE, gConfigSettings.DCTColors.TopBorderColor1,
	                                   gConfigSettings.DCTColors.TopBorderColor2) +
				  " " + gConfigSettings.DCTColors.TopLabelColor + "To   " +
				  gConfigSettings.DCTColors.TopLabelColonColor + ": " +
				  gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
				  gConfigSettings.DCTColors.TopToFillColor + DOT_CHAR +
				  gConfigSettings.DCTColors.TopToColor + screenText +
				  gConfigSettings.DCTColors.TopToFillColor);
	fieldWidth -= (screenText.length+1);
	for (var i = 0; i < fieldWidth; ++i)
		console.print(DOT_CHAR);
	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "]");

	// Current time
	console.gotoxy(startX, lineNum);
	console.print(gConfigSettings.DCTColors.TopLabelColor + "Time" +
	              gConfigSettings.DCTColors.TopLabelColonColor + ": " +
	              gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
	              gConfigSettings.DCTColors.TopTimeFillColor + DOT_CHAR);
	displayTime_DCTStyle();
	console.print(gConfigSettings.DCTColors.TopTimeFillColor + DOT_CHAR +
	              gConfigSettings.DCTColors.TopInfoBracketColor + "]");

	// Time left
	fieldWidth = (console.screen_columns * (7/80)).toFixed(0);
	startX = console.screen_columns - fieldWidth - 9;
	console.gotoxy(startX, lineNum);
	var timeStr = Math.floor(bbs.time_left / 60).toString().substr(0, fieldWidth);
	console.print(gConfigSettings.DCTColors.TopLabelColor + "Left" +
	              gConfigSettings.DCTColors.TopLabelColonColor + ": " +
	              gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
	              gConfigSettings.DCTColors.TopTimeLeftFillColor + DOT_CHAR +
	              gConfigSettings.DCTColors.TopTimeLeftColor + timeStr +
	              gConfigSettings.DCTColors.TopTimeLeftFillColor);
	fieldWidth -= (timeStr.length+1);
	for (var i = 0; i < fieldWidth; ++i)
		console.print(DOT_CHAR);
	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "] " +
	              randomTwoColorString(VERTICAL_SINGLE,
	              gConfigSettings.DCTColors.TopBorderColor1,
	              gConfigSettings.DCTColors.TopBorderColor2));

	// Line 3: Subject
	++lineNum;
	console.gotoxy(1, lineNum);
	//fieldWidth = (console.screen_columns * (66/80)).toFixed(0);
	fieldWidth = +(console.screen_columns - 15);
	screenText = gMsgSubj.substr(0, fieldWidth);
	console.print(randomTwoColorString(LOWER_LEFT_SINGLE, gConfigSettings.DCTColors.TopBorderColor1,
	                                   gConfigSettings.DCTColors.TopBorderColor1) +
	                                   " " + gConfigSettings.DCTColors.TopLabelColor + "Subj " +
	                                   gConfigSettings.DCTColors.TopLabelColonColor + ": " +
	                                   gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
	                                   gConfigSettings.DCTColors.TopSubjFillColor + DOT_CHAR +
	                                   gConfigSettings.DCTColors.TopSubjColor + screenText +
	                                   gConfigSettings.DCTColors.TopSubjFillColor);
	fieldWidth -= (screenText.length);
	for (var i = 0; i < fieldWidth; ++i)
		console.print(DOT_CHAR);
	console.print(DOT_CHAR);
	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "] " +
	              randomTwoColorString(LOWER_RIGHT_SINGLE,
	              gConfigSettings.DCTColors.TopBorderColor1,
	              gConfigSettings.DCTColors.TopBorderColor2));
	
	// Line 4: Top border for message area
	++lineNum;
	displayTextAreaTopBorder_DCTStyle(lineNum, pEditLeft, pEditRight);

	// Display the bottom message area border and help line
	DisplayTextAreaBottomBorder_DCTStyle(pEditBottom+1, null, pEditLeft, pEditRight, pInsertMode);
	DisplayBottomHelpLine_DCTStyle(console.screen_rows, pUseQuotes);
	
	// Go to the start of the edit area
	console.gotoxy(pEditLeft, pEditTop);

	// Write the message text that has been entered thus far.
	pDisplayEditLines(pEditTop, pEditLinesIndex);
	console.print(pEditColor);
}

function refreshSubjectOnScreen_DCTStyle(pX, pY, pLength, pText)
{
	console.print("\1n" + gConfigSettings.DCTColors.TopSubjColor);
	console.gotoxy(pX, pY);
	//printf("%-" + pLength + "s", pText.substr(0, pLength));
	// Ensure the text is no longer than the field width
	var subj = pText.substr(0, pLength);
	console.print(subj);
	console.print("\1n" + gConfigSettings.DCTColors.TopSubjFillColor);
	var fieldWidth = pLength - subj.length;
	for (var i = 0; i < fieldWidth; ++i)
		console.print(DOT_CHAR);
}

// Displays the top border of the message area, in the style of DCTEdit.
//
// Parameters:
//  pLineNum: The line number on the screen at which to draw the border
//  pEditLeft: The leftmost edit area column on the screen
//  pEditRight: The rightmost edit area column on the screen
function displayTextAreaTopBorder_DCTStyle(pLineNum, pEditLeft, pEditRight)
{
	// The border will use random bright/normal colors.  The colors
	// should stay the same each time we draw it, so a "static"
	// variable is used for the border text.  If that variable has
	// not been defined yet, then build it.
	if (typeof(displayTextAreaTopBorder_DCTStyle.border) == "undefined")
	{
		var numHorizontalChars = pEditRight - pEditLeft - 1;

		displayTextAreaTopBorder_DCTStyle.border = UPPER_LEFT_SINGLE;
		for (var i = 0; i < numHorizontalChars; ++i)
			displayTextAreaTopBorder_DCTStyle.border += HORIZONTAL_SINGLE;
		displayTextAreaTopBorder_DCTStyle.border += UPPER_RIGHT_SINGLE;
		displayTextAreaTopBorder_DCTStyle.border =
		randomTwoColorString(displayTextAreaTopBorder_DCTStyle.border,
		gConfigSettings.DCTColors.EditAreaBorderColor1,
		gConfigSettings.DCTColors.EditAreaBorderColor2);
	}

	// Draw the line on the screen
	//console.gotoxy((console.screen_columns >= 82 ? pEditLeft-1 : pEditLeft), pLineNum);
	console.gotoxy(pEditLeft, pLineNum);
	console.print(displayTextAreaTopBorder_DCTStyle.border);
}

// Displays the bottom border of the message area, in the style of DCTEdit.
//
// Parameters:
//  pLineNum: The line number on the screen at which to draw the border
//  pUseQuotes: This is not used; this is only here so that the signatures of
//              the IceEdit and DCTEdit versions match.
//  pEditLeft: The leftmost edit area column on the screen
//  pEditRight: The rightmost edit area column on the screen
//  pInsertMode: The insert mode ("INS" or "OVR")
//  pCanChgMsgColor: Whether or not changing the text color is allowed
function DisplayTextAreaBottomBorder_DCTStyle(pLineNum, pUseQuotes, pEditLeft, pEditRight,
                                               pInsertMode, pCanChgMsgColor)
{
	// The border will use random bright/normal colors.  The colors
	// should stay the same each time we draw it, so a "static"
	// variable is used for the border text.  If that variable has
	// not been defined yet, then build it.
	if (typeof(DisplayTextAreaBottomBorder_DCTStyle.border) == "undefined")
	{
		var innerWidth = pEditRight - pEditLeft - 1;

		DisplayTextAreaBottomBorder_DCTStyle.border = LOWER_LEFT_SINGLE;

		// This loop uses innerWidth-6 to make way for the insert mode
		// text.
		for (var i = 0; i < innerWidth-6; ++i)
			DisplayTextAreaBottomBorder_DCTStyle.border += HORIZONTAL_SINGLE;
		DisplayTextAreaBottomBorder_DCTStyle.border =
		randomTwoColorString(DisplayTextAreaBottomBorder_DCTStyle.border,
		gConfigSettings.DCTColors.EditAreaBorderColor1,
		gConfigSettings.DCTColors.EditAreaBorderColor2);
		// Insert mode
		DisplayTextAreaBottomBorder_DCTStyle.border += gConfigSettings.DCTColors.EditModeBrackets
		                                            + "[" + gConfigSettings.DCTColors.EditMode
		                                            + pInsertMode
		                                            + gConfigSettings.DCTColors.EditModeBrackets
		                                            + "]";
		// The last 2 border characters
		DisplayTextAreaBottomBorder_DCTStyle.border +=
		randomTwoColorString(HORIZONTAL_SINGLE + LOWER_RIGHT_SINGLE,
		gConfigSettings.DCTColors.EditAreaBorderColor1,
		gConfigSettings.DCTColors.EditAreaBorderColor2);
	}

	// Draw the border line on the screen.
	//console.gotoxy((console.screen_columns >= 82 ? pEditLeft-1 : pEditLeft), pLineNum);
	console.gotoxy(pEditLeft, pLineNum);
	console.print(DisplayTextAreaBottomBorder_DCTStyle.border);
}

// Displays the help line at the bottom of the screen, in the style
// of DCTEdit.
//
// Parameters:
//  pLineNum: The line number on the screen at which to draw the help line
//  pUsingQuotes: Boolean - Whether or not message quoting is enabled.
function DisplayBottomHelpLine_DCTStyle(pLineNum, pUsingQuotes)
{
	// For efficiency, define the help line variable only once.
	if (typeof(DisplayBottomHelpLine_DCTStyle.helpText) == "undefined")
	{
		DisplayBottomHelpLine_DCTStyle.helpText = gConfigSettings.DCTColors.BottomHelpBrackets
		                                        + "[" + gConfigSettings.DCTColors.BottomHelpKeys + "CTRL"
		                                        + gConfigSettings.DCTColors.BottomHelpFill + DOT_CHAR
		                                        + gConfigSettings.DCTColors.BottomHelpKeys + "Z"
		                                        + gConfigSettings.DCTColors.BottomHelpBrackets + "]\1n "
		                                        + gConfigSettings.DCTColors.BottomHelpKeyDesc + "Save\1n      "
		                                        + gConfigSettings.DCTColors.BottomHelpBrackets + "["
		                                        + gConfigSettings.DCTColors.BottomHelpKeys + "CTRL"
		                                        + gConfigSettings.DCTColors.BottomHelpFill + DOT_CHAR
		                                        + gConfigSettings.DCTColors.BottomHelpKeys + "A"
		                                        + gConfigSettings.DCTColors.BottomHelpBrackets + "]\1n "
		                                        + gConfigSettings.DCTColors.BottomHelpKeyDesc + "Abort";
		// If we can allow message quoting, then add a text to show Ctrl-Q for
		// quoting.
		if (pUsingQuotes)
		{
			DisplayBottomHelpLine_DCTStyle.helpText += "\1n      "
			                                        + gConfigSettings.DCTColors.BottomHelpBrackets + "["
			                                        + gConfigSettings.DCTColors.BottomHelpKeys + "CTRL"
			                                        + gConfigSettings.DCTColors.BottomHelpFill + DOT_CHAR
			                                        + gConfigSettings.DCTColors.BottomHelpKeys + "Q"
			                                        + gConfigSettings.DCTColors.BottomHelpBrackets + "]\1n "
			                                        + gConfigSettings.DCTColors.BottomHelpKeyDesc + "Quote";
		}
		DisplayBottomHelpLine_DCTStyle.helpText += "\1n      "
		                                        + gConfigSettings.DCTColors.BottomHelpBrackets + "["
		                                        + gConfigSettings.DCTColors.BottomHelpKeys + "ESC"
		                                        + gConfigSettings.DCTColors.BottomHelpBrackets + "]\1n "
		                                        + gConfigSettings.DCTColors.BottomHelpKeyDesc + "Menu";
		// Center the text by padding it in the front with spaces.  This is done instead
		// of using console.center() because console.center() will output a newline,
		// which would not be good on the last line of the screen.
		var numSpaces = (console.screen_columns/2).toFixed(0) - (strip_ctrl(DisplayBottomHelpLine_DCTStyle.helpText).length/2).toFixed(0);
		for (var i = 0; i < numSpaces; ++i)
			DisplayBottomHelpLine_DCTStyle.helpText = " " + DisplayBottomHelpLine_DCTStyle.helpText;
	}

	// Display the help line on the screen
	var lineNum = console.screen_rows;
	if ((typeof(pLineNum) != "undefined") && (pLineNum != null))
		lineNum = pLineNum;
	console.gotoxy(1, lineNum);
	console.print(DisplayBottomHelpLine_DCTStyle.helpText);
	console.print("\1n");
	console.cleartoeol();
}

// Updates the insert mode displayd on the screen, for DCT Edit style.
//
// Parameters:
//  pEditRight: The rightmost column on the screen for the edit area
//  pEditBottom: The bottommost row on the screen for the edit area
//  pInsertMode: The insert mode ("INS" or "OVR")
function updateInsertModeOnScreen_DCTStyle(pEditRight, pEditBottom, pInsertMode)
{
   // If the number of columns on the screen is more than 80, the horizontal
   // position will be 1 more to the right due to the added vertical lines
   // around the edit area.
   if (console.screen_columns > 80)
      console.gotoxy(pEditRight-5, pEditBottom+1);
   else
      console.gotoxy(pEditRight-6, pEditBottom+1);
	console.print(gConfigSettings.DCTColors.EditModeBrackets + "[" +
	              gConfigSettings.DCTColors.EditMode + pInsertMode +
	              gConfigSettings.DCTColors.EditModeBrackets + "]");
}

// Draws the top border of the quote window, DCT Edit style.
//
// Parameters:
//  pQuoteWinHeight: The height of the quote window
//  pEditLeft: The leftmost column on the screen for the edit window
//  pEditRight: The rightmost column on the screen for the edit window
function DrawQuoteWindowTopBorder_DCTStyle(pQuoteWinHeight, pEditLeft, pEditRight)
{
   // Generate the top border vairable only once.
   if (typeof(DrawQuoteWindowTopBorder_DCTStyle.border) == "undefined")
   {
      DrawQuoteWindowTopBorder_DCTStyle.border = gConfigSettings.DCTColors.QuoteWinBorderColor
                                + UPPER_LEFT_SINGLE + HORIZONTAL_SINGLE + " "
                                + gConfigSettings.DCTColors.QuoteWinBorderTextColor
                                + "Quote Window " + gConfigSettings.DCTColors.QuoteWinBorderColor;
      var curLength = strip_ctrl(DrawQuoteWindowTopBorder_DCTStyle.border).length;
      var borderWidth = pEditRight - pEditLeft;
      for (var i = curLength; i < borderWidth; ++i)
         DrawQuoteWindowTopBorder_DCTStyle.border += HORIZONTAL_SINGLE;
      DrawQuoteWindowTopBorder_DCTStyle.border += UPPER_RIGHT_SINGLE;
   }

   // Draw the top border line
   var screenLine = console.screen_rows - pQuoteWinHeight + 1;
   console.gotoxy(pEditLeft, screenLine);
   console.print(DrawQuoteWindowTopBorder_DCTStyle.border);
}

// Draws the bottom border of the quote window, DCT Edit style.  Note:
// The cursor should be placed at the start of the line (leftmost screen
// column on the screen line where the border should be drawn).
//
// Parameters:
//  pEditLeft: The leftmost column of the edit area
//  pEditRight: The rightmost column of the edit area
function DrawQuoteWindowBottomBorder_DCTStyle(pEditLeft, pEditRight)
{
   // Generate the bottom border vairable only once.
   if (typeof(DrawQuoteWindowBottomBorder_DCTStyle.border) == "undefined")
   {
      // Create a string containing the quote help text.
      var quoteHelpText = gConfigSettings.DCTColors.QuoteWinBorderTextColor
                         + "[Enter] Accept" + gConfigSettings.DCTColors.QuoteWinBorderColor
                         + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + gConfigSettings.DCTColors.QuoteWinBorderTextColor
                         + "[^Q/ESC] End" + gConfigSettings.DCTColors.QuoteWinBorderColor
                         + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + gConfigSettings.DCTColors.QuoteWinBorderTextColor
                         + "[" + UP_ARROW + "/" + DOWN_ARROW + "/PgUp/PgDn/Home/End] Scroll"
                         + gConfigSettings.DCTColors.QuoteWinBorderColor + HORIZONTAL_SINGLE
                         + HORIZONTAL_SINGLE + gConfigSettings.DCTColors.QuoteWinBorderTextColor
						 /*
						 + "[" + UP_ARROW + "/" + DOWN_ARROW + "/PgUp/PgDn] Scroll"
                         + gConfigSettings.DCTColors.QuoteWinBorderColor + HORIZONTAL_SINGLE
                         + HORIZONTAL_SINGLE + gConfigSettings.DCTColors.QuoteWinBorderTextColor
                         + "[F/L] First/last page";
						 */
      var helpTextLen = strip_ctrl(quoteHelpText).length;

      // Figure out the starting horizontal position on the screen so that
      // the quote help text line can be centered.
      var helpTextStartX = Math.floor((console.screen_columns/2) - (helpTextLen/2));

      // Start creating DrawQuoteWindowBottomBorder_DCTStyle.border with the
      // bottom border lines, up until helpTextStartX.
      DrawQuoteWindowBottomBorder_DCTStyle.border = gConfigSettings.DCTColors.QuoteWinBorderColor
                                                  + LOWER_LEFT_SINGLE;
      for (var XPos = pEditLeft+2; XPos < helpTextStartX; ++XPos)
         DrawQuoteWindowBottomBorder_DCTStyle.border += HORIZONTAL_SINGLE;
      // Add the help text, then display the rest of the bottom border characters.
      DrawQuoteWindowBottomBorder_DCTStyle.border += quoteHelpText
                                                   + gConfigSettings.DCTColors.QuoteWinBorderColor;
      for (var XPos = helpTextStartX + helpTextLen; XPos <= pEditRight-2; ++XPos)
         DrawQuoteWindowBottomBorder_DCTStyle.border += HORIZONTAL_SINGLE;
      DrawQuoteWindowBottomBorder_DCTStyle.border += LOWER_RIGHT_SINGLE;
   }

   // Print the border text on the screen
   console.print(DrawQuoteWindowBottomBorder_DCTStyle.border);
}

// Prompts the user for a yes/no question, DCTEdit-style.
//
// Parameters:
//  pQuestion: The question to ask, without the trailing "?"
//  pBoxTitle: The text to appear in the top border of the question box
//  pDefaultYes: Whether to default to "yes" (true/false).  If false, this
//               will default to "no".
//  pParamObj: An object containing the following members:
//   displayMessageRectangle: The function used for re-drawing the portion of the
//                            message on the screen when the yes/no box is no longer
//                            needed
//   editTop: The topmost row of the edit area
//   editBottom: The bottom row of the edit area
//   editWidth: The edit area width
//   editHeight: The edit area height
//   editLinesIndex: The current index into the edit lines array
// pAlwaysEraseBox: Boolean: erase the box regardless of a Yes or No answer.
function promptYesNo_DCTStyle(pQuestion, pBoxTitle, pDefaultYes, pParamObj, pAlwaysEraseBox)
{
	var userResponse = pDefaultYes;

	// Get the current cursor position, so that we can place the cursor
	// back there later.
	const originalCurpos = console.getxy();

	// Set up the abort confirmation box dimensions, and calculate
	// where on the screen to display it.
	//const boxWidth = 27;
	const boxWidth = pQuestion.length + 14;
	const boxHeight = 6;
	const boxX = (console.screen_columns/2).toFixed(0) - (boxWidth/2).toFixed(0);
	const boxY = +pParamObj.editTop + +(pParamObj.editHeight/2).toFixed(0) - +(boxHeight/2).toFixed(0);
	const innerBoxWidth = boxWidth - 2;

	// Display the question box
	// Upper-left corner, 1 horizontal line, and "Abort" text
	console.gotoxy(boxX, boxY);
	console.print(gConfigSettings.DCTColors.TextBoxBorder + UPPER_LEFT_SINGLE +
	              HORIZONTAL_SINGLE + " " + gConfigSettings.DCTColors.TextBoxBorderText +
	              pBoxTitle + gConfigSettings.DCTColors.TextBoxBorder + " ");
	// Remaining top box border
	for (var i = pBoxTitle.length + 5; i < boxWidth; ++i)
		console.print(HORIZONTAL_SINGLE);
	console.print(UPPER_RIGHT_SINGLE);
	// Inner box: Blank
	var endScreenLine = boxY + boxHeight - 2;
	for (var screenLine = boxY+1; screenLine < endScreenLine; ++screenLine)
	{
		console.gotoxy(boxX, screenLine);
		console.print(VERTICAL_SINGLE);
		printf("%" + innerBoxWidth + "s", "");
		console.print(VERTICAL_SINGLE);
	}
	// Bottom box border
	console.gotoxy(boxX, screenLine);
	console.print(LOWER_LEFT_SINGLE);
	for (var i = 0; i < innerBoxWidth; ++i)
		console.print(HORIZONTAL_SINGLE);
	console.print(LOWER_RIGHT_SINGLE);

	// Prompt the user whether or not to abort: Move the cursor to
	// the proper location on the screen, output the propmt text,
	// and get user input.
	console.gotoxy(boxX+3, boxY+2);
	console.print(gConfigSettings.DCTColors.TextBoxInnerText + pQuestion + "?  " +
	gConfigSettings.DCTColors.YesNoBoxBrackets + "[" +
	gConfigSettings.DCTColors.YesNoBoxYesNoText);
	// Default to yes/no, depending on the value of pDefaultYes.
	if (pDefaultYes)
	{
		console.print("Yes");
		userResponse = true;
	}
	else
	{
		console.print("No ");
		userResponse = false;
	}
	console.print(gConfigSettings.DCTColors.YesNoBoxBrackets + "]");

	// Input loop
	var userInput = "";
	var continueOn = true;
	while (continueOn)
	{
		// Move the cursor where it needs to be to write the "Yes"
		// or "No"
		console.gotoxy(boxX+20, boxY+2);
		// Get a key and take appropriate action.
		userInput = getUserKey(K_UPPER|K_NOECHO|K_NOCRLF|K_NOSPIN, gConfigSettings);
		if (userInput == KEY_ENTER)
			continueOn = false;
		else if (userInput == "Y")
		{
			userResponse = true;
			continueOn = false;
		}
		else if ((userInput == "N") || (userInput == KEY_ESC))
		{
			userResponse = false;
			continueOn = false;
		}
		// Arrow keys: Toggle back and forth
		else if ((userInput == KEY_UP) || (userInput == KEY_DOWN) ||
		         (userInput == KEY_LEFT) || (userInput == KEY_RIGHT))
		{
			// Toggle the userResponse variable
			userResponse = !userResponse;
			// Write "Yes" or "No", depending on the userResponse variable
			if (userResponse)
			{
				console.print(gConfigSettings.DCTColors.YesNoBoxYesNoText + "Yes" +
				gConfigSettings.DCTColors.YesNoBoxBrackets + "]");
			}
			else
			{
				console.print(gConfigSettings.DCTColors.YesNoBoxYesNoText + "No " +
				gConfigSettings.DCTColors.YesNoBoxBrackets + "]");
			}
		}
	}

	// If the user chose no, then erase the confirmation box and put the
	// cursor back where it originally was.
	if (!userResponse || pAlwaysEraseBox)
	{
		// Calculate the difference in the edit lines index we'll need to use
		// to erase the confirmation box.  This will be the difference between
		// the cursor position between boxY and the current cursor row.
		const editLinesIndexDiff = boxY - originalCurpos.y;
		pParamObj.displayMessageRectangle(boxX, boxY, boxWidth, boxHeight,
		                                  pParamObj.editLinesIndex + editLinesIndexDiff,
		                                  true);
		// Put the cursor back where it was
		console.gotoxy(originalCurpos);
	}

	return userResponse;
}

// Displays the time on the screen.
//
// Parameters:
//  pTimeStr: The time to display (optional).  If this is omitted,
//            then this funtion will get the current time.
function displayTime_DCTStyle(pTimeStr)
{
	// Calculate the horizontal location for the time string, using basically
	// the formula used for calculating the horizontal location of the message
	// area in redrawScreen_DCTStyle(), which the time lines up with.
	var fieldWidth = (console.screen_columns * (27/80)).toFixed(0);
   //var curposX = console.screen_columns - fieldWidth - 9 + 8;
   var curposX = console.screen_columns - fieldWidth - 1;
   console.gotoxy(curposX, 3);

	if (pTimeStr == null)
		console.print("n" + gConfigSettings.DCTColors.TopTimeColor + getCurrentTimeStr());
	else
		console.print("n" + gConfigSettings.DCTColors.TopTimeColor + pTimeStr);
}

// Displays the number of minutes remaining on the screen.
function displayTimeRemaining_DCTStyle()
{
	var fieldWidth = (console.screen_columns * (7/80)).toFixed(0);
	var startX = console.screen_columns - fieldWidth - 1;
	console.gotoxy(startX, 3);
	var timeStr = Math.floor(bbs.time_left / 60).toString().substr(0, fieldWidth);
	console.print(gConfigSettings.DCTColors.TopTimeLeftColor + timeStr +
	              gConfigSettings.DCTColors.TopTimeLeftFillColor);
	fieldWidth -= (timeStr.length+1);
	for (var i = 0; i < fieldWidth; ++i)
		console.print(DOT_CHAR);
}

// Displays & handles the input loop for the DCT style ESC menu.
//
// Parameters:
//  pEditLeft: The leftmost column of the edit area
//  pEditRight: The rightmost column of the edit area
//  pEditTop: The topmost row of the edit area
//  pDisplayMessageRectangle: The function for drawing part of the
//                            edit text on the screen.  This is used
//                            for refreshing the screen after a menu
//                            box disappears.
//  pEditLinesIndex: The current index into the edit lines array.  This
//                   is is required to pass to the pDisplayMessageRectangle
//                   function.
//  pEditLineDiff: The difference between the current edit line and the top of
//                 the edit area.
//  pIsSysop: Whether or not the user is a sysop.
//  pCanCrossPost: Whether or not cross-posting is allowed.
//
// Return value: The action code, based on the user's selection
function doDCTESCMenu(pEditLeft, pEditRight, pEditTop, pDisplayMessageRectangle,
                      pEditLinesIndex, pEditLineDiff, pIsSysop, pCanCrossPost)
{
	// This function displays the top menu options, with a given one highlighted.
	//
	// Parameters:
	//  pItemPositions: An object containing the menu item positions.
	//  pHighlightedItemNum: The index (0-based) of the menu item to be highlighted.
	//  pMenus: An array containing the menus
	//  pIsSysop: Whether or not the user is the sysop.
	//
	// Return value: The user's last keypress during the menu's input loop.
	function displayTopMenuItems(pItemPositions, pHighlightedItemNum, pMenus, pIsSysop)
	{
		// File
		console.gotoxy(pItemPositions.fileX, pItemPositions.mainMenuY);
		if (pHighlightedItemNum == 0)
		{
			console.print(gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_RIGHT +
			              gConfigSettings.DCTColors.SelectedMenuLabelText + "File" +
			              gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_LEFT);
		}
		else
			console.print("\1n " + gConfigSettings.DCTColors.UnselectedMenuLabelText + "File\1n ");
		// Edit
		console.gotoxy(pItemPositions.editX, pItemPositions.mainMenuY);
		if (pHighlightedItemNum == 1)
		{
			console.print(gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_RIGHT +
			              gConfigSettings.DCTColors.SelectedMenuLabelText + "Edit" +
			              gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_LEFT);
		}
		else
			console.print("\1n " + gConfigSettings.DCTColors.UnselectedMenuLabelText + "Edit\1n ");
		// SysOp
		if (pIsSysop)
		{
			console.gotoxy(pItemPositions.sysopX, pItemPositions.mainMenuY);
			if (pHighlightedItemNum == 2)
			{
				console.print(gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_RIGHT +
				              gConfigSettings.DCTColors.SelectedMenuLabelText + "SysOp" +
				              gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_LEFT);
			}
			else
				console.print("\1n " + gConfigSettings.DCTColors.UnselectedMenuLabelText + "SysOp\1n ");
		}
		// Help
		console.gotoxy(pItemPositions.helpX, pItemPositions.mainMenuY);
		if (pHighlightedItemNum == 3)
		{
			console.print(gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_RIGHT +
			              gConfigSettings.DCTColors.SelectedMenuLabelText + "Help" +
			              gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_LEFT);
		}
		else
			console.print("\1n " + gConfigSettings.DCTColors.UnselectedMenuLabelText + "Help\1n ");

		// Display the menu (and capture the return object so that we can
		// also return it here).
		var returnObj = pMenus[pHighlightedItemNum].doInputLoop();

		// Refresh the part of the edit text on the screen where the menu was.
		pDisplayMessageRectangle(pMenus[pHighlightedItemNum].topLeftX,
		                         pMenus[pHighlightedItemNum].topLeftY,
		                         pMenus[pHighlightedItemNum].width,
		                         pMenus[pHighlightedItemNum].height,
		                         pEditLinesIndex-pEditLineDiff, true);

		return returnObj;
	}

	// Set up an object containing the menu item positions.
	// Only create this object once.
	if (typeof(doDCTESCMenu.mainMenuItemPositions) == "undefined")
	{
		doDCTESCMenu.mainMenuItemPositions = new Object();
		// Vertical position on the screen for the main menu items
		doDCTESCMenu.mainMenuItemPositions.mainMenuY = pEditTop - 1;
		// Horizontal position of the "File" text
		doDCTESCMenu.mainMenuItemPositions.fileX = gEditLeft + 5;
		// Horizontal position of the "Edit" text
		doDCTESCMenu.mainMenuItemPositions.editX = doDCTESCMenu.mainMenuItemPositions.fileX + 11;
		// Horizontal position of the "SysOp" text
		doDCTESCMenu.mainMenuItemPositions.sysopX = doDCTESCMenu.mainMenuItemPositions.editX + 11;
		// Horizontal position of of the "Help" text
		if (pIsSysop)
			doDCTESCMenu.mainMenuItemPositions.helpX = doDCTESCMenu.mainMenuItemPositions.sysopX + 12;
		else
			doDCTESCMenu.mainMenuItemPositions.helpX = doDCTESCMenu.mainMenuItemPositions.sysopX;
	}

	// Variables for the menu numbers
	const fileMenuNum = 0;
	const editMenuNum = 1;
	const sysopMenuNum = 2;
	const helpMenuNum = 3;

	// Set up the menu objects.  Only create these objects once.
	if (typeof(doDCTESCMenu.allMenus) == "undefined")
	{
		doDCTESCMenu.allMenus = new Array();
		// File menu
		doDCTESCMenu.allMenus[fileMenuNum] = new DCTMenu(doDCTESCMenu.mainMenuItemPositions.fileX, doDCTESCMenu.mainMenuItemPositions.mainMenuY+1);
		doDCTESCMenu.allMenus[fileMenuNum].addItem("&Save    Ctrl-Z", DCTMENU_FILE_SAVE);
		doDCTESCMenu.allMenus[fileMenuNum].addItem("&Abort   Ctrl-A", DCTMENU_FILE_ABORT);
		if (pCanCrossPost)
			doDCTESCMenu.allMenus[fileMenuNum].addItem( "X-Post  Ctrl-&C", DCTMENU_CROSS_POST);
		doDCTESCMenu.allMenus[fileMenuNum].addItem("&Edit       ESC", DCTMENU_FILE_EDIT);
		doDCTESCMenu.allMenus[fileMenuNum].addExitLoopKey(CTRL_Z, DCTMENU_FILE_SAVE);
		doDCTESCMenu.allMenus[fileMenuNum].addExitLoopKey(CTRL_A, DCTMENU_FILE_ABORT);
		if (pCanCrossPost)
			doDCTESCMenu.allMenus[fileMenuNum].addExitLoopKey(CTRL_C, DCTMENU_CROSS_POST);
		doDCTESCMenu.allMenus[fileMenuNum].addExitLoopKey(KEY_ESC, DCTMENU_FILE_EDIT);

		// Edit menu
		doDCTESCMenu.allMenus[editMenuNum] = new DCTMenu(doDCTESCMenu.mainMenuItemPositions.editX, doDCTESCMenu.mainMenuItemPositions.mainMenuY+1);
		doDCTESCMenu.allMenus[editMenuNum].addItem("&Insert Mode    Ctrl-I", DCTMENU_EDIT_INSERT_TOGGLE);
		doDCTESCMenu.allMenus[editMenuNum].addItem("&Find Text      Ctrl-N", DCTMENU_EDIT_FIND_TEXT);
		doDCTESCMenu.allMenus[editMenuNum].addItem("Spe&ll Checker  Ctrl-W", DCTMENU_EDIT_SPELL_CHECKER);
		doDCTESCMenu.allMenus[editMenuNum].addItem("Setti&ngs       Ctrl-U", DCTMENU_EDIT_SETTINGS);
		doDCTESCMenu.allMenus[editMenuNum].addExitLoopKey(CTRL_I, DCTMENU_EDIT_INSERT_TOGGLE);
		doDCTESCMenu.allMenus[editMenuNum].addExitLoopKey(CTRL_N, DCTMENU_EDIT_FIND_TEXT);
		doDCTESCMenu.allMenus[editMenuNum].addExitLoopKey(CTRL_U, DCTMENU_EDIT_SETTINGS);

		// SysOp menu
		doDCTESCMenu.allMenus[sysopMenuNum] = new DCTMenu(doDCTESCMenu.mainMenuItemPositions.sysopX, doDCTESCMenu.mainMenuItemPositions.mainMenuY+1);
		doDCTESCMenu.allMenus[sysopMenuNum].addItem("&Import file      Ctrl-O", DCTMENU_SYSOP_IMPORT_FILE);
		doDCTESCMenu.allMenus[sysopMenuNum].addItem("E&xport to file   Ctrl-X", DCTMENU_SYSOP_EXPORT_FILE);
		doDCTESCMenu.allMenus[sysopMenuNum].addExitLoopKey(CTRL_O, DCTMENU_SYSOP_IMPORT_FILE);
		doDCTESCMenu.allMenus[sysopMenuNum].addExitLoopKey(CTRL_X, DCTMENU_SYSOP_EXPORT_FILE);

		// Help menu
		doDCTESCMenu.allMenus[helpMenuNum] = new DCTMenu(doDCTESCMenu.mainMenuItemPositions.helpX, doDCTESCMenu.mainMenuItemPositions.mainMenuY+1);
		doDCTESCMenu.allMenus[helpMenuNum].addItem("C&ommand List   Ctrl-P", DCTMENU_HELP_COMMAND_LIST);
		doDCTESCMenu.allMenus[helpMenuNum].addItem("&General Help   Ctrl-G", DCTMENU_HELP_GENERAL);
		doDCTESCMenu.allMenus[helpMenuNum].addItem("&Program Info   Ctrl-R", DCTMENU_HELP_PROGRAM_INFO);
		if (gConfigSettings.enableTextReplacements)
		{
			doDCTESCMenu.allMenus[helpMenuNum].addItem("&Text replcmts  Ctrl-T", DCTMENU_LIST_TXT_REPLACEMENTS);
			doDCTESCMenu.allMenus[helpMenuNum].addExitLoopKey(CTRL_T, DCTMENU_LIST_TXT_REPLACEMENTS);
		}
		doDCTESCMenu.allMenus[helpMenuNum].addExitLoopKey(CTRL_P, DCTMENU_HELP_COMMAND_LIST);
		doDCTESCMenu.allMenus[helpMenuNum].addExitLoopKey(CTRL_G, DCTMENU_HELP_GENERAL);
		doDCTESCMenu.allMenus[helpMenuNum].addExitLoopKey(CTRL_R, DCTMENU_HELP_PROGRAM_INFO);

		// For each menu, add KEY_LEFT, KEY_RIGHT, and ESC as loop-exit keys;
		// also, set the menu colors.
		for (var i = 0; i < doDCTESCMenu.allMenus.length; ++i)
		{
			doDCTESCMenu.allMenus[i].addExitLoopKey(KEY_LEFT);
			doDCTESCMenu.allMenus[i].addExitLoopKey(KEY_RIGHT);
			doDCTESCMenu.allMenus[i].addExitLoopKey(KEY_ESC);

			doDCTESCMenu.allMenus[i].colors.border = gConfigSettings.DCTColors.MenuBorders;
			doDCTESCMenu.allMenus[i].colors.selected = gConfigSettings.DCTColors.MenuSelectedItems;
			doDCTESCMenu.allMenus[i].colors.unselected = gConfigSettings.DCTColors.MenuUnselectedItems;
			doDCTESCMenu.allMenus[i].colors.hotkey = gConfigSettings.DCTColors.MenuHotkeys;
		}
	}

	// The chosen action will be returned from this function
	var chosenAction = ESC_MENU_EDIT_MESSAGE;

	// Boolean variables to keep track of which menus were displayed
	// (for refresh purposes later)
	var fileMenuDisplayed = false;
	var editMenuDisplayed = false;
	var sysopMenuDisplayed = false;
	var helpMenuDisplayed = false;

	// Display the top menu options with "File" highlighted.
	var menuNum = fileMenuNum; // 0: File, ..., 3: Help
	var subMenuItemNum = 0;
	var menuRetObj = displayTopMenuItems(doDCTESCMenu.mainMenuItemPositions, menuNum,
	                                     doDCTESCMenu.allMenus, pIsSysop);
	// Input loop
	var userInput = "";
	var matchedMenuRetval = false; // Whether one of the menu return values was matched
	var continueOn = true;
	while (continueOn)
	{
		matchedMenuRetval = valMatchesMenuCode(menuRetObj.returnVal, pIsSysop);
		// If a menu return value was matched, then set userInput to it.
		if (matchedMenuRetval)
			userInput = menuRetObj.returnVal;
		// If the user's input from the last menu was ESC, left, right, or one of the
		// characters from the menus, then set userInput to the last menu keypress.
		else if (inputMatchesMenuSelection(menuRetObj.userInput, pIsSysop))
			userInput = menuRetObj.userInput;
		// If nothing from the menu was matched, then get a key from the user.
		else
			userInput = getUserKey(K_UPPER|K_NOECHO|K_NOCRLF|K_NOSPIN, gConfigSettings);
		menuRetObj.userInput = "";

		// If a menu return code was matched or userInput is blank (the
		// timeout was hit), then exit out of the loop.
		if (matchedMenuRetval || (userInput == ""))
			break;

		// Take appropriate action based on the user's input.
		switch (userInput)
		{
			case KEY_LEFT:
				if (menuNum == 0)
					menuNum = 3;
				else
					--menuNum;
				// Don't allow the sysop menu for non-sysops.
				if ((menuNum == sysopMenuNum) && !pIsSysop)
					--menuNum;
				subMenuItemNum = 0;
				menuRetObj = displayTopMenuItems(doDCTESCMenu.mainMenuItemPositions, menuNum, doDCTESCMenu.allMenus, pIsSysop);
				break;
			case KEY_RIGHT:
				if (menuNum == 3)
					menuNum = 0;
				else
					++menuNum;
				// Don't allow the sysop menu for non-sysops.
				if ((menuNum == sysopMenuNum) && !pIsSysop)
					++menuNum;
				subMenuItemNum = 0;
				menuRetObj = displayTopMenuItems(doDCTESCMenu.mainMenuItemPositions, menuNum, doDCTESCMenu.allMenus, pIsSysop);
				break;
			case KEY_UP:
			case KEY_DOWN:
				break;
			case KEY_ENTER: // Selected an item from the menu
				// Set userInput to the return code from the menu so that it will
				// be returned from this function.
				userInput = menuRetObj.returnVal;
			case "S":       // Save
			case CTRL_Z:    // Save
			case "A":       // Abort
			case CTRL_A:    // Abort
			case "I":       // Import file for sysop, or Insert/Overwrite toggle for non-sysop
			case CTRL_O:    // Import file (for sysop)
			case "X":       // Export file (for sysop)
			case CTRL_X:    // Export file (for sysop)
			case CTRL_V:    // Insert/overwrite toggle
			case "F":       // Find text
			case CTRL_F:    // Find text
			case "N":       // User settings
			case CTRL_U:    // User settings
			case "O":       // Command List
			case "G":       // General help
			case "P":       // Program info
			case "E":       // Edit the message
			case KEY_ESC:   // Edit the message
				continueOn = false;
				break;
			case "C":       // Cross-post
			case CTRL_C:    // Cross-post
				if (pCanCrossPost)
					continueOn = false;
				break;
			case "T": // List text replacements
			case CTRL_T: // List text replacements
				if (gConfigSettings.enableTextReplacements)
					continueOn = false;
				break;
			case "L": // Spell checker
			case CTRL_W: // Spell checker
				continueOn = false;
				break;
			default:
				break;
		}
	}

	// We've exited the menu, so refresh the top menu border and the message text
	// on the screen.
	displayTextAreaTopBorder_DCTStyle(doDCTESCMenu.mainMenuItemPositions.mainMenuY, pEditLeft, pEditRight);

	// Set chosenAction based on the user's input
	// Save the message
	if ((userInput == "S") || (userInput == CTRL_Z) || (userInput == DCTMENU_FILE_SAVE))
		chosenAction = ESC_MENU_SAVE;
	// Abort
	else if ((userInput == "A") || (userInput == CTRL_A) || (userInput == DCTMENU_FILE_ABORT))
		chosenAction = ESC_MENU_ABORT;
	// Toggle insert/overwrite mode
	else if ((userInput == CTRL_V) || (userInput == DCTMENU_EDIT_INSERT_TOGGLE))
		chosenAction = ESC_MENU_INS_OVR_TOGGLE;
	// Import file (sysop only)
	else if (userInput == DCTMENU_SYSOP_IMPORT_FILE)
	{
		if (pIsSysop)
			chosenAction = ESC_MENU_SYSOP_IMPORT_FILE;
	}
	// Import file for sysop, or Insert/Overwrite toggle for non-sysop
	else if (userInput == "I")
	{
		if (pIsSysop)
			chosenAction = ESC_MENU_SYSOP_IMPORT_FILE;
		else
			chosenAction = ESC_MENU_INS_OVR_TOGGLE;
	}
	// Find text
	else if ((userInput == CTRL_F) || (userInput == "F") || (userInput == DCTMENU_EDIT_FIND_TEXT))
		chosenAction = ESC_MENU_FIND_TEXT;
	// Command List
	else if ((userInput == "O") || (userInput == DCTMENU_HELP_COMMAND_LIST))
		chosenAction = ESC_MENU_HELP_COMMAND_LIST;
	// General help
	else if ((userInput == "G") || (userInput == DCTMENU_HELP_GENERAL))
		chosenAction = ESC_MENU_HELP_GENERAL;
	// Program info
	else if ((userInput == "P") || (userInput == DCTMENU_HELP_PROGRAM_INFO))
		chosenAction = ESC_MENU_HELP_PROGRAM_INFO;
	// Export the message
	else if ((userInput == "X") || (userInput == DCTMENU_SYSOP_EXPORT_FILE))
	{
		if (pIsSysop)
			chosenAction = ESC_MENU_SYSOP_EXPORT_FILE;
	}
	// Edit the message
	else if ((userInput == "E") || (userInput == KEY_ESC))
		chosenAction = ESC_MENU_EDIT_MESSAGE;
	// Cross-post
	else if ((userInput == CTRL_C) || (userInput == "C") || (userInput == DCTMENU_CROSS_POST))
	{
		if (pCanCrossPost)
			chosenAction = ESC_MENU_CROSS_POST_MESSAGE;
	}
	// List text replacements
	else if ((userInput == CTRL_T) || (userInput == "T") || (userInput == DCTMENU_LIST_TXT_REPLACEMENTS))
	{
		if (gConfigSettings.enableTextReplacements)
			chosenAction = ESC_MENU_LIST_TEXT_REPLACEMENTS;
	}
	// User settings
	else if ((userInput == CTRL_U) || (userInput == "N") || (userInput == DCTMENU_EDIT_SETTINGS))
		chosenAction = ESC_MENU_USER_SETTINGS;
	// Spell checker
	else if ((userInput == CTRL_W) || (userInput == "L") || (userInput == DCTMENU_EDIT_SPELL_CHECKER))
		chosenAction = ESC_MENU_SPELL_CHECK;

	return chosenAction;
}

// This function returns whether a value matches any of the DCT Edit menu return
// values.  This is used in doDCTESCMenu()'s input loop;
//
// Parameters:
//  pVal: The value to test
//  pIsSysop: Whether or not the user is a sysop
//
// Return: Boolean - Whether or not the value matches a DCT Edit menu return value.
function valMatchesMenuCode(pVal, pIsSysop)
{
	var valMatches = false;
	valMatches = ((pVal == DCTMENU_FILE_SAVE) || (pVal == DCTMENU_FILE_ABORT) ||
	              (pVal == DCTMENU_FILE_EDIT) || (pVal == DCTMENU_EDIT_INSERT_TOGGLE) ||
	              (pVal == DCTMENU_EDIT_FIND_TEXT) || (pVal == DCTMENU_HELP_COMMAND_LIST) ||
	              (pVal == DCTMENU_HELP_GENERAL) || (pVal == DCTMENU_HELP_PROGRAM_INFO) ||
	              (pVal == DCTMENU_EDIT_SETTINGS) || (pVal == DCTMENU_EDIT_SPELL_CHECKER));
	if (gConfigSettings.enableTextReplacements)
		valMatches = (valMatches || (pVal == DCTMENU_LIST_TXT_REPLACEMENTS));
	if (pIsSysop)
		valMatches = (valMatches || (pVal == DCTMENU_SYSOP_IMPORT_FILE) || (pVal == DCTMENU_SYSOP_EXPORT_FILE));
	return valMatches;
}

// This function returns whether a user input matches a selection from one of the
// menus.  This is used in doDCTESCMenu()'s input loop;
//
// Parameters:
//  pInput: The user input to test
//  pIsSysop: Whether or not the user is a sysop
function inputMatchesMenuSelection(pInput, pIsSysop)
{
   return((pInput == KEY_ESC) || (pInput == KEY_LEFT) ||
           (pInput == KEY_RIGHT) || (pInput == KEY_ENTER) ||
           (pInput == "S") || (pInput == "A") || (pInput == "E") ||
           (pInput == "I") || (pIsSysop && (pInput == "X")) ||
           (pInput == "F") || (pInput == "C") || (pInput == "G") ||
           (pInput == "P") || (pInput == "T"));
}

//////////////////////////////////////////////////////////////////////////////////////////
// DCTMenu object functions

// Constructs a menu item for a DCT menu
function DCTMenuItem()
{
   this.text = "";        // The item text
   this.hotkeyIndex = -1; // Index of the hotkey in the text (-1 for no hotkey).
   this.hotkey = "";      // The shortcut key for the item (blank for no hotkey).
   this.returnVal = 0;    // Return value for the item
}

// DCTMenu constructor: Constructs a DCTEdit-style menu.
//
// Parameters:
//  pTopLeftX: The upper-left screen column
//  pTopLeftY: The upper-left screen row
function DCTMenu(pTopLeftX, pTopLeftY)
{
   this.colors = new Object();
   // Unselected item colors
   this.colors.unselected = "n7k";
   // Selected item colors
   this.colors.selected = "nw";
   // Other colors
   this.colors.hotkey = "hw";
   this.colors.border = "n7k";

   this.topLeftX = 1; // Upper-left screen column
   if ((pTopLeftX != null) && (pTopLeftX > 0) && (pTopLeftX <= console.screen_columns))
      this.topLeftX = pTopLeftX;
   this.topLeftY = 1; // Upper-left screen row
   if ((pTopLeftY != null) && (pTopLeftY > 0) && (pTopLeftY <= console.screen_rows))
      this.topLeftY = pTopLeftY;
   this.width = 0;
   this.height = 0;
   this.selectedItemIndex = 0;

   // this.menuItems will contain DCTMenuItem objects.
   this.menuItems = new Array();
   // hotkeyRetvals is an array, indexed by hotkey, that contains
   // the return values for each hotkey.
   this.hotkeyRetvals = new Array();

   // exitLoopKeys will contain keys that will exit the input loop
   // when pressed by the user, so that calling code can catch them.
   // It's indexed by the key, and the value is the return code that
   // should be returned for that key.
   this.exitLoopKeys = new Array();

   // Border style: "single" or "double"
   this.borderStyle = "single";

   // clearSpaceAroundMenu controls whether or not to clear one space
   // around the menu when it is drawn.
   this.clearSpaceAroundMenu = false;
   // clearSpaceColor controls which color to use when drawing the
   // clear space around the menu.
   this.clearSpaceColor = "n";
   // clearSpaceTopText specifies text to display above the top of the
   // menu when clearing space around it.
   this.clearSpaceTopText = "";

   // Timeout (in milliseconds) for the input loop.  Default to 1 minute.
   this.timeoutMS = 60000;

   // Member functions
   this.addItem = DCTMenu_AddItem;
   this.addExitLoopKey = DCTMenu_AddExitLoopKey;
   this.displayItem = DCTMenu_DisplayItem;
   this.doInputLoop = DCTMenu_DoInputLoop;
   this.numItems = DCTMenu_NumItems;
   this.removeAllItems = DCTMenu_RemoveAllItems;
}
// Adds an item to the DCTMenu.
//
// Parameters:
//  pText: The text of the menu item.  Note that a & precedes a hotkey.
//  pReturnVal: The value to return upon selection of the item
function DCTMenu_AddItem(pText, pReturnVal)
{
   if (pText == "")
      return;

   var item = new DCTMenuItem();
   item.returnVal = pReturnVal;
   // Look for a & in pText, and if one is found, use the next character as
   // its hotkey.
   var ampersandIndex = pText.indexOf("&");
   if (ampersandIndex > -1)
   {
      // If pText has text after ampersandIndex, then set up
      // the next character as the hotkey in the item.
      if (pText.length > ampersandIndex+1)
      {
         item.hotkeyIndex = ampersandIndex;
         item.hotkey = pText.substr(ampersandIndex+1, 1);
         // Set the text of the item.  The text should not include
         // the ampersand.
         item.text = pText.substr(0, ampersandIndex) + pText.substr(ampersandIndex+1);
         // Add the hotkey & return value to this.hotkeyRetvals
         this.hotkeyRetvals[item.hotkey.toUpperCase()] = pReturnVal;

         // If the menu is not wide enough for this item's text, then
         // update this.width.
         if (this.width < item.text.length + 2)
            this.width = item.text.length + 2;
         // Also update this.height
         if (this.height == 0)
            this.height = 3;
         else
            ++this.height;
      }
      else
      {
         // pText does not have text after ampersandIndex.
         item.text = pText.substr(0, ampersandIndex);
      }
   }
   else
   {
      // No ampersand was found in pText.
      item.text = pText;
   }

   // Add the item to this.menuItems
   this.menuItems.push(item);
}
// Adds a key that will exit the input loop when pressed by the user.
//
// Parameters:
//  pKey: The key that should cause the input loop to exit
//  pReturnValue: The return value of the input loop when the key is pressed.
//                If this is not specified, a default value of -1 will be used.
function DCTMenu_AddExitLoopKey(pKey, pReturnValue)
{
   var val = -1;
   if (typeof(pReturnValue) == "number")
      val = pReturnValue;

   this.exitLoopKeys[pKey] = val;
}
// Displays an item on the menu.
//
// Parameters:
//  pItemIndex: The index of the item in the menuItems array
//  pPrintBorders: Boolean - Whether or not to display the horizontal
//                 borders on each side of the menu item text.
function DCTMenu_DisplayItem(pItemIndex, pPrintBorders)
{
   var printBorders = false;
   if (pPrintBorders != null)
      printBorders = pPrintBorders;

   // Determine whether to use the selected item color or unselected
   // item color.
   var itemColor = "";
   if (pItemIndex == this.selectedItemIndex)
      itemColor = this.colors.selected;
   else
      itemColor = this.colors.unselected;

   // Print the menu item text on the screen.
   if (printBorders)
   {
      console.gotoxy(this.topLeftX, this.topLeftY + pItemIndex + 1);
      if (this.borderStyle == "single")
         console.print(this.colors.border + VERTICAL_SINGLE);
      else if (this.borderStyle == "double")
         console.print(this.colors.border + VERTICAL_DOUBLE);
   }
   else
      console.gotoxy(this.topLeftX + 1, this.topLeftY + pItemIndex + 1);
   // If the menu item has a hotkey, then write the appropriate character
   // in the hotkey color.
   if (this.menuItems[pItemIndex].hotkeyIndex > -1)
   {
      console.print(itemColor +
                    this.menuItems[pItemIndex].text.substr(0, this.menuItems[pItemIndex].hotkeyIndex) +
                    this.colors.hotkey + this.menuItems[pItemIndex].hotkey + itemColor +
                    this.menuItems[pItemIndex].text.substr(this.menuItems[pItemIndex].hotkeyIndex + 1));
   }
   else
   {
      console.print(itemColor + this.menuItems[pItemIndex].text);
   }
   // If the item text isn't wide enough to fill the entire inner width, then
   // clear the line up until the right border.
   var innerWidth = this.width - 2;
   if (this.menuItems[pItemIndex].text.length < innerWidth)
   {
      for (var i = this.menuItems[pItemIndex].text.length; i < innerWidth; ++i)
         console.print(" ");
   }
   // Print the right border character if specified.
   if (printBorders)
   {
      if (this.borderStyle == "single")
         console.print(this.colors.border + VERTICAL_SINGLE);
      else if (this.borderStyle == "double")
         console.print(this.colors.border + VERTICAL_DOUBLE);
   }
}
// Displays the DCT menu and enters the input loop.
//
// Return value: An object containing the following properties:
//  returnVal: The return code of the item selected, or -1 if no
//             item was selected.
//  userInput: The last user input
function DCTMenu_DoInputLoop()
{
   var returnObj = new Object();
   returnObj.returnVal = -1;
   returnObj.userInput = "";

   // If clearSpaceAroundMenu is true, then draw a blank row
   // above the menu.
   if (this.clearSpaceAroundMenu && (this.topLeftY > 1))
   {
      // If there is room, output a space to the left, diagonal
      // from the top-left corner of the menu.
      if (this.topLeftX > 1)
      {
         console.gotoxy(this.topLeftX-1, this.topLeftY-1);
         console.print(this.clearSpaceColor + " ");
      }
      else
         console.gotoxy(this.topLeftX, this.topLeftY-1);

      // Output this.clearSpaceTopText
      console.print(this.clearSpaceTopText);
      // Output the rest of the blank space
      var textLen = strip_ctrl(this.clearSpaceTopText).length;
      if (textLen < this.width)
      {
         var numSpaces = this.width - textLen;
         if (this.topLeftX + this.width < console.screen_columns)
            ++numSpaces;
         for (var i = 0; i < numSpaces; ++i)
            console.print(this.clearSpaceColor + " ");
      }
   }

   // Before drawing the top border, if clearSpaceAroundMenu is
   // true, put space before the border.
   if (this.clearSpaceAroundMenu && (this.topLeftY > 1))
   {
      console.gotoxy(this.topLeftX-1, this.topLeftY);
      console.print(this.clearSpaceColor + " ");
   }
   else
      console.gotoxy(this.topLeftX, this.topLeftY);
   // Draw the top border
   var innerWidth = this.width - 2;
   if (this.borderStyle == "single")
   {
      console.print(this.colors.border + UPPER_LEFT_SINGLE);
      for (var i = 0; i < innerWidth; ++i)
         console.print(HORIZONTAL_SINGLE);
      console.print(this.colors.border + UPPER_RIGHT_SINGLE);
   }
   else if (this.borderStyle == "double")
   {
      console.print(this.colors.border + UPPER_LEFT_DOUBLE);
      for (var i = 0; i < innerWidth; ++i)
         console.print(HORIZONTAL_DOUBLE);
      console.print(this.colors.border + UPPER_RIGHT_DOUBLE);
   }
   // If clearSpaceAroundMenu is true, then put a space after the border.
   if (this.clearSpaceAroundMenu && (this.topLeftX + this.width < console.screen_columns))
      console.print(this.clearSpaceColor + " ");

   // Print the menu items (and side spaces outside the menu if
   // clearSpaceAroundMenu is true).
   var itemColor = "";
   for (var i = 0; i < this.menuItems.length; ++i)
   {
      if (this.clearSpaceAroundMenu && (this.topLeftX > 1))
      {
         console.gotoxy(this.topLeftX-1, this.topLeftY + i + 1);
         console.print(this.clearSpaceColor + " ");
      }

      this.displayItem(i, true);

      if (this.clearSpaceAroundMenu && (this.topLeftX + this.width < console.screen_columns))
      {
         console.gotoxy(this.topLeftX + this.width, this.topLeftY + i + 1);
         console.print(this.clearSpaceColor + " ");
      }
   }

   // Before drawing the bottom border, if clearSpaceAroundMenu is
   // true, put space before the border.
   if (this.clearSpaceAroundMenu && (this.topLeftY > 1))
   {
      console.gotoxy(this.topLeftX - 1, this.topLeftY + this.height - 1);
      console.print(this.clearSpaceColor + " ");
   }
   else
      console.gotoxy(this.topLeftX, this.topLeftY + this.height - 1);
   // Draw the bottom border
   if (this.borderStyle == "single")
   {
      console.print(this.colors.border + LOWER_LEFT_SINGLE);
      for (var i = 0; i < innerWidth; ++i)
         console.print(HORIZONTAL_SINGLE);
      console.print(LOWER_RIGHT_SINGLE);
   }
   else if (this.borderStyle == "double")
   {
      console.print(this.colors.border + LOWER_LEFT_DOUBLE);
      for (var i = 0; i < innerWidth; ++i)
         console.print(HORIZONTAL_DOUBLE);
      console.print(LOWER_RIGHT_DOUBLE);
   }
   // If clearSpaceAroundMenu is true, then put a space after the border.
   if (this.clearSpaceAroundMenu && (this.topLeftX + this.width < console.screen_columns))
      console.print(this.clearSpaceColor + " ");

   // If clearSpaceAroundMenu is true, then draw a blank row
   // below the menu.
   if (this.clearSpaceAroundMenu && (this.topLeftY + this.height < console.screen_rows))
   {
      var numSpaces = this.width + 2;
      if (this.topLeftX > 1)
         console.gotoxy(this.topLeftX-1, this.topLeftY + this.height);
      else
      {
         console.gotoxy(this.topLeftX, this.topLeftY + this.height);
         --numSpaces;
      }

      if (this.topLeftX + this.width >= console.screen_columns)
         --numSpaces;

      for (var i = 0; i < numSpaces; ++i)
         console.print(this.clearSpaceColor + " ");
   }

   // Place the cursor on the line of the selected item
   console.gotoxy(this.topLeftX + 1, this.topLeftY + this.selectedItemIndex + 1);

   // Keep track of the current cursor position
   var curpos = new Object();
   curpos.x = this.topLeftX + 1;
   curpos.y = this.topLeftY + this.selectedItemIndex + 1;

   // Input loop
   const topItemLineNumber = this.topLeftY + 1;
   const bottomItemLineNumber = this.topLeftY + this.height - 1;
   var continueOn = true;
   while (continueOn)
   {
      // Get a key, (time out after the selected time), and take appropriate action.
		returnObj.userInput = getUserKey(K_UPPER|K_NOECHO|K_NOCRLF|K_NOSPIN, gConfigSettings);
		// If the user input is blank, then the input timed out, and we should quit.
		if (returnObj.userInput == "")
		{
         continueOn = false;
         break;
		}

      // Take appropriate action, depending on the user's keypress.
      switch (returnObj.userInput)
      {
         case KEY_ENTER:
            // Set returnObj.returnVal to the currently-selected item's returnVal,
            // and exit the input loop.
            returnObj.returnVal = this.menuItems[this.selectedItemIndex].returnVal;
            continueOn = false;
            break;
         case KEY_UP:
            // Go up one item
            if (this.menuItems.length > 1)
            {
               // If we're below the top menu item, then go up one item.  Otherwise,
               // go to the last menu item.
               var oldIndex = this.selectedItemIndex;
               if ((curpos.y > topItemLineNumber) && (this.selectedItemIndex > 0))
               {
                  --curpos.y;
                  --this.selectedItemIndex;
               }
               else
               {
                  curpos.y = bottomItemLineNumber - 1;
                  this.selectedItemIndex = this.menuItems.length - 1;
               }
               // Refresh the items on the screen so that the item colors
               // are updated.
               this.displayItem(oldIndex, false);
               this.displayItem(this.selectedItemIndex, false);
            }
            break;
         case KEY_DOWN:
            // Go down one item
            if (this.menuItems.length > 1)
            {
               // If we're above the bottom menu item, then go down one item.  Otherwise,
               // go to the first menu item.
               var oldIndex = this.selectedItemIndex;
               if ((curpos.y < bottomItemLineNumber) && (this.selectedItemIndex < this.menuItems.length - 1))
               {
                  ++curpos.y;
                  ++this.selectedItemIndex;
               }
               else
               {
                  curpos.y = this.topLeftY + 1;
                  this.selectedItemIndex = 0;
               }
               // Refresh the items on the screen so that the item colors
               // are updated.
               this.displayItem(oldIndex, false);
               this.displayItem(this.selectedItemIndex, false);
            }
            break;
         case KEY_ESC:
            continueOn = false;
            break;
         default:
            // If the user's input is one of the hotkeys, then stop the
            // input loop and return with the return code for the hotkey.
            if (typeof(this.hotkeyRetvals[returnObj.userInput]) != "undefined")
            {
               returnObj.returnVal = this.hotkeyRetvals[returnObj.userInput];
               continueOn = false;
            }
            // If the user's input is one of the loop-exit keys, then stop
            // the input loop.
            else if (typeof(this.exitLoopKeys[returnObj.userInput]) != "undefined")
            {
               returnObj.returnVal = this.exitLoopKeys[returnObj.userInput];
               continueOn = false;
            }
            break;
      }
   }

   return returnObj;
}
// Returns the number of items in the menu.
function DCTMenu_NumItems()
{
   return this.menuItems.length;
}
// Removes all items from a DCTMenu.
function DCTMenu_RemoveAllItems()
{
   this.width = 0;
   this.height = 0;
   this.selectedItemIndex = 0;
   this.menuItems = new Array();
   this.hotkeyRetvals = new Array();
   this.exitLoopKeys = new Array();
}

// Returns the the script's execution directory.
// The code in this function 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.
function getScriptDir()
{
   var startup_path = '.';
   try { throw dig.dist(dist); } catch(e) { startup_path = e.fileName; }
   return(backslash(startup_path.replace(/[\/\\][^\/\\]*$/,'')));
}