diff --git a/exec/slyedcfg.js b/exec/slyedcfg.js
index 2def6620fe08329abac9ce6240f32741214fede7..76ab5021c95e462a40d5408666bf80b51dc59a78 100644
--- a/exec/slyedcfg.js
+++ b/exec/slyedcfg.js
@@ -25,9 +25,33 @@ var gHelpWrapWidth = uifc.screen_width - 10;
 // Read the SlyEdit configuration file
 var gCfgInfo = readSlyEditCfgFile();
 
-// Show the main menu and go from there
-doMainMenu();
-
+// Show the main menu and go from there.
+// This is in a loop so that if the user aborts from confirming to save
+// settings, they'll return to the main menu.
+var anyOptionChanged = false;
+var continueOn = true;
+while (continueOn)
+{
+	anyOptionChanged = doMainMenu(anyOptionChanged) || anyOptionChanged;
+	// If any configuration option changed, prompt the user & save if the user wants to
+	if (anyOptionChanged)
+	{
+		var userChoice = promptYesNo("Save configuration?", true, WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC);
+		if (typeof(userChoice) === "boolean")
+		{
+			if (userChoice)
+			{
+				if (saveSlyEditCfgFile())
+					uifc.msg("Changes were successfully saved (to mods dir)");
+				else
+					uifc.msg("Failed to save settings!");
+			}
+			continueOn = false;
+		}
+	}
+	else
+		continueOn = false;
+}
 
 
 ///////////////////////////////////////////////////////////
@@ -36,8 +60,7 @@ doMainMenu();
 function doMainMenu()
 {
 	// Create a CTX to specify the current selected item index
-	if (doMainMenu.ctx == undefined)
-		doMainMenu.ctx = uifc.list.CTX();
+	var ctx = uifc.list.CTX();
 	var helpText = "Behavior: Behavior settings\r\nIce Colors: Ice-related color settings\r\nDCT Colors: DCT-related color settings";
 	// Selection
 	var winMode = WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC;
@@ -47,8 +70,8 @@ function doMainMenu()
 	while (continueOn && !js.terminated)
 	{
 		uifc.help_text = word_wrap(helpText, gHelpWrapWidth);
-		var selection = uifc.list(winMode, menuTitle, ["Behavior", "Ice Colors", "DCT Colors"], doMainMenu.ctx);
-		doMainMenu.ctx.cur = selection; // Remember the current selected item
+		var selection = uifc.list(winMode, menuTitle, ["Behavior", "Ice Colors", "DCT Colors"], ctx);
+		ctx.cur = selection; // Remember the current selected item
 		switch (selection)
 		{
 			case -1: // ESC
@@ -65,19 +88,7 @@ function doMainMenu()
 				break;
 		}
 	}
-	
-	// If any configuration option changed, prompt the user & save if the user wants to
-	if (anyOptionChanged)
-	{
-		var userChoice = promptYesNo("Save configuration?", true, WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC);
-		if (typeof(userChoice) === "boolean" && userChoice)
-		{
-			if (saveSlyEditCfgFile())
-				uifc.msg("Changes were successfully saved (to mods dir)");
-			else
-				uifc.msg("Failed to save settings!");
-		}
-	}
+	return anyOptionChanged;
 }
 
 // Allows the user to change behavior settings.
@@ -112,8 +123,9 @@ function doBehaviorMenu()
 		"taglinePrefix",
 		"dictionaryFilenames"
 	];
-	// Menu item text for the toggle options:
-	var toggleOptItems = [
+	// Menu item text for the options:
+	var optionStrs = [
+		// Toggle options:
 		"Display end info screen",
 		"Enable user input timeout",
 		"Re-wrap quote lines",
@@ -127,20 +139,34 @@ function doBehaviorMenu()
 		"Enable taglines",
 		"Shuffle taglines",
 		"Double-quotes around tag lines",
-		"Allow/enable spell check"
+		"Allow/enable spell check",
+		// Other options:
+		"User input timeout (MS)",
+		"Enable text replacements",
+		"Tagline filename",
+		"Tagline prefix",
+		"Dictionary filenames"
 	];
 	// Build the array of items to be displayed on the menu
 	var menuItems = [];
 	// Toggle (on/off) settings
-	for (var i = 0; i < 14; ++i)
-		menuItems.push(formatCfgMenuText(itemTextMaxLen, toggleOptItems[i], gCfgInfo.cfgSections.BEHAVIOR[cfgOptProps[i]]));
+	var optionIdx = 0
+	for (; optionIdx < 14; ++optionIdx)
+		menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx], gCfgInfo.cfgSections.BEHAVIOR[cfgOptProps[optionIdx]]));
 	// Text input settings, etc.
-	menuItems.push(formatCfgMenuText(itemTextMaxLen, "User input timeout (MS)", gCfgInfo.cfgSections.BEHAVIOR.inputTimeoutMS));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.inputTimeoutMS));
 	// Text replacements can be a boolean true/false or "regex"
-	menuItems.push(formatCfgMenuText(itemTextMaxLen, "Enable text replacements", getTxtReplacementsVal()));
-	menuItems.push(formatCfgMenuText(itemTextMaxLen, "Tagline filename", gCfgInfo.cfgSections.BEHAVIOR.tagLineFilename));
-	menuItems.push(formatCfgMenuText(itemTextMaxLen, "Tagline prefix", gCfgInfo.cfgSections.BEHAVIOR.taglinePrefix));
-	menuItems.push("Dictionary filenames"); // dictionaryFilenames
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], getTxtReplacementsVal()));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.tagLineFilename));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.taglinePrefix));
+	//menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.dictionaryFilenames.substr(0,30)));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.dictionaryFilenames));
+
+	// A dictionary of help text for each option, indexed by the option name from the configuration file
+	if (doBehaviorMenu.optHelp == undefined)
+		doBehaviorMenu.optHelp = getOptionHelpText();
+	if (doBehaviorMenu.mainScreenHelp == undefined)
+		doBehaviorMenu.mainScreenHelp = getBehaviorScreenHelp(doBehaviorMenu.optHelp, cfgOptProps);
 
 	// Create a CTX to specify the current selected item index
 	if (doBehaviorMenu.ctx == undefined)
@@ -152,59 +178,20 @@ function doBehaviorMenu()
 	var continueOn = true;
 	while (continueOn && !js.terminated)
 	{
-		uifc.help_text = getBehaviorScreenHelp();
+		uifc.help_text = doBehaviorMenu.mainScreenHelp;
 		var optionMenuSelection = uifc.list(winMode, menuTitle, menuItems, doBehaviorMenu.ctx);
 		doBehaviorMenu.ctx.cur = optionMenuSelection; // Remember the current selected item
-		switch (optionMenuSelection)
+		if (optionMenuSelection == -1) // ESC
+			continueOn = false;
+		else
 		{
-			case -1: // ESC
-				continueOn = false;
-				break;
-			// 0-13 are boolean values
-			case 0:
-			case 1:
-			case 2:
-			case 3:
-			case 4:
-			case 5:
-			case 6:
-			case 7:
-			case 8:
-			case 9:
-			case 10:
-			case 11:
-			case 12:
-			case 13:
-				gCfgInfo.cfgSections.BEHAVIOR[cfgOptProps[optionMenuSelection]] = !gCfgInfo.cfgSections.BEHAVIOR[cfgOptProps[optionMenuSelection]];
-				anyOptionChanged = true;
-				menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, toggleOptItems[optionMenuSelection], gCfgInfo.cfgSections.BEHAVIOR[cfgOptProps[optionMenuSelection]]);
-				// With a separate window to prompt for toggling the item:
-				/*
-				if (inputCfgObjBoolean(cfgOptProps[optionMenuSelection], toggleOptItems[optionMenuSelection]))
-				{
-					anyOptionChanged = true;
-					menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, toggleOptItems[optionMenuSelection], gCfgInfo.cfgSections.BEHAVIOR[cfgOptProps[optionMenuSelection]]);
-				}
-				*/
-				break;
-			case 14: // User input timeout (MS)
-				uifc.help_text = "The user inactivity timeout, in milliseconds";
-				var userInput = uifc.input(WIN_MID, "User input timeout (MS)", gCfgInfo.cfgSections.BEHAVIOR.inputTimeoutMS.toString(), 0, K_NUMBER|K_EDIT);
-				if (typeof(userInput) === "string" && userInput.length > 0)
-				{
-					var value = parseInt(userInput);
-					if (!isNaN(value) && value > 0 && gCfgInfo.cfgSections.BEHAVIOR.inputTimeoutMS != value)
-					{
-						gCfgInfo.cfgSections.BEHAVIOR.inputTimeoutMS = value;
-						anyOptionChanged = true;
-						menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, "User input timeout (MS)", value);
-					}
-				}
-				break;
-			case 15: // Enable text replacements (yes/no/regex)
-				var helpText = "Whether or not to enable text replacements (AKA macros). Can be ";
-				helpText += "true, false, or 'regex' to use regular expressions.";
-				uifc.help_text = word_wrap(helpText, gHelpWrapWidth);
+			var optName = cfgOptProps[optionMenuSelection];
+			var itemType = typeof(gCfgInfo.cfgSections.BEHAVIOR[optName]);
+			uifc.help_text = doBehaviorMenu.optHelp[optName];
+			if (optName == "enableTextReplacements")
+			{
+				// Enable text replacements - This is above boolean because this
+				// could be mistaken for a boolean if it's true or false
 				// Store the current value wo we can see if it changes
 				var valBackup = gCfgInfo.cfgSections.BEHAVIOR.enableTextReplacements;
 				// Prompt the user
@@ -213,7 +200,7 @@ function doBehaviorMenu()
 					ctx.cur = gCfgInfo.cfgSections.BEHAVIOR.enableTextReplacements ? 0 : 1;
 				else if (gCfgInfo.cfgSections.BEHAVIOR.enableTextReplacements.toLowerCase() === "regex")
 					ctx.cur = 2;
-				var txtReplacementsSelection = uifc.list(winMode, "Enable text replacements", ["Yes", "No", "Regex"], ctx);
+				var txtReplacementsSelection = uifc.list(winMode, optionStrs[optionMenuSelection], ["Yes", "No", "Regex"], ctx);
 				switch (txtReplacementsSelection)
 				{
 					case 0:
@@ -231,35 +218,59 @@ function doBehaviorMenu()
 					anyOptionChanged = true;
 					menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, "Enable text replacements", getTxtReplacementsVal());
 				}
-				break;
-			case 16: // Tagline filename
-				var helpText = "The name of the file where tag lines are stored. This file is loaded from the sbbs ctrl directory.";
-				uifc.help_text = word_wrap(helpText, gHelpWrapWidth);
-				var userInput = uifc.input(WIN_MID, "Tagline filename", gCfgInfo.cfgSections.BEHAVIOR.tagLineFilename, 60, K_EDIT);
+			}
+			else if (itemType === "boolean")
+			{
+				gCfgInfo.cfgSections.BEHAVIOR[optName] = !gCfgInfo.cfgSections.BEHAVIOR[optName];
+				anyOptionChanged = true;
+				menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, optionStrs[optionMenuSelection], gCfgInfo.cfgSections.BEHAVIOR[optName]);
+			}
+			else if (itemType === "number")
+			{
+				var promptStr = optionStrs[optionMenuSelection];
+				var initialVal = gCfgInfo.cfgSections.BEHAVIOR[optName].toString();
+				var minVal = 1;
+				var userInput = uifc.input(WIN_MID, promptStr, initialVal, 0, K_NUMBER|K_EDIT);
+				if (typeof(userInput) === "string" && userInput.length > 0)
+				{
+					var value = parseInt(userInput);
+					if (!isNaN(value) && value >= minVal && gCfgInfo.cfgSections.BEHAVIOR[optName] != value)
+					{
+						gCfgInfo.cfgSections.BEHAVIOR[optName] = value;
+						anyOptionChanged = true;
+						menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, optionStrs[optionMenuSelection], gCfgInfo.cfgSections.BEHAVIOR[optName]);
+					}
+				}
+			}
+			else if (optName == "tagLineFilename")
+			{
+				// Tagline filename
+				var userInput = uifc.input(WIN_MID, optionStrs[optionMenuSelection], gCfgInfo.cfgSections.BEHAVIOR.tagLineFilename, 60, K_EDIT);
 				if (typeof(userInput) === "string" && userInput != gCfgInfo.cfgSections.BEHAVIOR.tagLineFilename)
 				{
 					gCfgInfo.cfgSections.BEHAVIOR.tagLineFilename = userInput;
 					anyOptionChanged = true;
 					menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, "Tagline filename", userInput);
 				}
-				break;
-			case 17: // Tagline prefix
-				var helpText = "Text to add to the front of a tagline when adding it to the message. This ";
-				helpText += "can be blank (nothing after the =) if no prefix is desired.";
-				uifc.help_text = word_wrap(helpText, gHelpWrapWidth);
-				var userInput = uifc.input(WIN_MID, "Tagline prefix", gCfgInfo.cfgSections.BEHAVIOR.taglinePrefix, 0, K_EDIT);
+			}
+			else if (optName == "taglinePrefix")
+			{
+				// Tagline prefix
+				var userInput = uifc.input(WIN_MID, optionStrs[optionMenuSelection], gCfgInfo.cfgSections.BEHAVIOR.taglinePrefix, 0, K_EDIT);
 				if (typeof(userInput) === "string" && userInput != gCfgInfo.cfgSections.BEHAVIOR.taglinePrefix)
 				{
 					gCfgInfo.cfgSections.BEHAVIOR.tagLineFilename = userInput;
 					anyOptionChanged = true;
 					menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, "Tagline prefix", userInput);
 				}
-				break;
-			case 18: // Dictionary filenames
+			}
+			else if (optName == "dictionaryFilenames")
+			{
+				// Dictionary filenames
 				var userInput = promptDictionaries();
 				anyOptionChanged = (userInput != gCfgInfo.cfgSections.BEHAVIOR.dictionaryFilenames);
 				gCfgInfo.cfgSections.BEHAVIOR.dictionaryFilenames = userInput;
-				break;
+			}
 		}
 	}
 
@@ -529,64 +540,85 @@ function promptYesNo(pQuestion, pInitialVal, pWinMode)
 ///////////////////////////////////////////////////
 // Help text functions
 
-// Returns help text for the behavior configuration screen
-function getBehaviorScreenHelp()
+// Returns a dictionary of help text, indexed by the option name from the configuration file
+function getOptionHelpText()
 {
-	if (getBehaviorScreenHelp.help == undefined)
-	{
-		getBehaviorScreenHelp.help = "This screen allows you to configure behavior options for SlyEdit.\r\n\r\n";
+	var optionHelpText = {};
+	optionHelpText["displayEndInfoScreen"] = "Display end info screen: Whether or not to display editor info when exiting";
 
-		getBehaviorScreenHelp.help += "Display end info screen: Whether or not to display editor info when exiting\r\n\r\n";
+	optionHelpText["userInputTimeout"] = "User input timeout: Whether or not to enable user inactivity timeout";
 
-		getBehaviorScreenHelp.help += "User input timeout: Whether or not to enable user inactivity timeout\r\n\r\n";
+	optionHelpText["reWrapQuoteLines"] = "Re-wrap quote lines: If true, quote lines will be re-wrapped so that they are complete ";
+	optionHelpText["reWrapQuoteLines"] += "but still look good when quoted.  If this option is disabled, then quote lines will ";
+	optionHelpText["reWrapQuoteLines"] += "simply be trimmed to fit into the message.";
 
-		getBehaviorScreenHelp.help += "Re-wrap quote lines: If true, quote lines will be re-wrapped so that they are complete ";
-		getBehaviorScreenHelp.help += "but still look good when quoted.  If this option is disabled, then quote lines will ";
-		getBehaviorScreenHelp.help += "simply be trimmed to fit into the message.\r\n\r\n";
-		
-		getBehaviorScreenHelp.help += "Use author initials in quoted lines: Whether or not to prefix the quote ";
-		getBehaviorScreenHelp.help += "lines with the last author's initials. Users can change this for themselves too.\r\n\r\n";
+	optionHelpText["useQuoteLineInitials"] = "Use author initials in quoted lines: Whether or not to prefix the quote ";
+	optionHelpText["useQuoteLineInitials"] += "lines with the last author's initials. Users can change this for themselves too.";
 
-		getBehaviorScreenHelp.help += "Indent quoted lines with author initials: When prefixing quote lines with the last author's initials, ";
-		getBehaviorScreenHelp.help += "whether or not to indent the quote lines with a space. Users can change this for themselves too.\r\n\r\n";
+	optionHelpText["indentQuoteLinesWithInitials"] = "Indent quoted lines with author initials: When prefixing quote lines with the last author's initials, ";
+	optionHelpText["indentQuoteLinesWithInitials"] += "whether or not to indent the quote lines with a space. Users can change this for themselves too.";
 
-		getBehaviorScreenHelp.help += "Allow editing quote lines: Whether or not to allow editing quote lines\r\n\r\n";
+	optionHelpText["allowEditQuoteLines"] = "Allow editing quote lines: Whether or not to allow editing quote lines";
 
-		getBehaviorScreenHelp.help += "Allow user settings: Whether or not to allow users to change their user settings.\r\n\r\n";
+	optionHelpText["allowUserSettings"] = "Allow user settings: Whether or not to allow users to change their user settings.";
 
-		getBehaviorScreenHelp.help += "Allow color selection: Whether or not to let the user change the text color\r\n\r\n";
+	optionHelpText["allowColorSelection"] = "Allow color selection: Whether or not to let the user change the text color";
 
-		getBehaviorScreenHelp.help += "Save colors as ANSI: Whether or not to save message color/attribute codes as ANSI ";
-		getBehaviorScreenHelp.help += "(if not, they will be saved as Synchronet attribute codes)\r\n\r\n";
+	optionHelpText["saveColorsAsANSI"] = "Save colors as ANSI: Whether or not to save message color/attribute codes as ANSI ";
+	optionHelpText["saveColorsAsANSI"] += "(if not, they will be saved as Synchronet attribute codes)";
 
-		getBehaviorScreenHelp.help += "Allow cross-posting: Whether or not to allow cross-posting to multiple sub-boards\r\n\r\n";
+	optionHelpText["allowCrossPosting"] = "Allow cross-posting: Whether or not to allow cross-posting to multiple sub-boards";
 
-		getBehaviorScreenHelp.help += "Enable taglines: Whether or not to enable the option to add a tagline.\r\n\r\n";
+	optionHelpText["enableTaglines"] = "Enable taglines: Whether or not to enable the option to add a tagline.";
 
-		getBehaviorScreenHelp.help += "Shuffle taglines: Whether or not to shuffle (randomize) the list of taglines when they are ";
-		getBehaviorScreenHelp.help += "displayed for the user to choose from\r\n\r\n";
+	optionHelpText["shuffleTaglines"] = "Shuffle taglines: Whether or not to shuffle (randomize) the list of taglines when they are ";
+	optionHelpText["shuffleTaglines"] += "displayed for the user to choose from";
 
-		getBehaviorScreenHelp.help += "Double-quotes around tag lines: Whether or not to add double-quotes around taglines\r\n\r\n";
+	optionHelpText["quoteTaglines"] = "Double-quotes around tag lines: Whether or not to add double-quotes around taglines";
 
-		getBehaviorScreenHelp.help += "Allow/enable spell check: Whether or not to allow spell check\r\n\r\n";
+	optionHelpText["allowSpellCheck"] = "Allow/enable spell check: Whether or not to allow spell check";
 
-		getBehaviorScreenHelp.help += "User input timeout (MS): The user inactivity timeout, in milliseconds\r\n\r\n";
+	optionHelpText["inputTimeoutMS"] = "User input timeout (MS): The user inactivity timeout, in milliseconds";
 
-		getBehaviorScreenHelp.help += "Enable text replacements: Whether or not to enable text replacements (AKA macros). Can be ";
-		getBehaviorScreenHelp.help += "true, false, or 'regex' to use regular expressions.\r\n\r\n";
+	optionHelpText["enableTextReplacements"] = "Enable text replacements: Whether or not to enable text replacements (AKA macros). Can be ";
+	optionHelpText["enableTextReplacements"] += "true, false, or 'regex' to use regular expressions.";
 
-		getBehaviorScreenHelp.help += "Tagline filename: The name of the file where tag lines are stored. This file is loaded ";
-		getBehaviorScreenHelp.help += "from the sbbs ctrl directory.\r\n\r\n";
+	optionHelpText["tagLineFilename"] = "Tagline filename: The name of the file where tag lines are stored. This file is loaded ";
+	optionHelpText["tagLineFilename"] += "from the sbbs ctrl directory.";
 
-		getBehaviorScreenHelp.help += "Tagline prefix: Text to add to the front of a tagline when adding it to the message. This ";
-		getBehaviorScreenHelp.help += "can be blank (nothing after the =) if no prefix is desired.\r\n\r\n";
+	optionHelpText["taglinePrefix"] = "Tagline prefix: Text to add to the front of a tagline when adding it to the message. This ";
+	optionHelpText["taglinePrefix"] += "can be blank (nothing after the =) if no prefix is desired.";
 
-		getBehaviorScreenHelp.help +=  "Dictionary filenames: These are dictionaries to use for spell check.  ";
-		getBehaviorScreenHelp.help += "The dictionary filenames are in the format dictionary_<language>.txt, where ";
-		getBehaviorScreenHelp.help += "<language> is the language name.  The dictionary files are located in either ";
-		getBehaviorScreenHelp.help += "sbbs/mods, sbbs/ctrl, or the same directory as SlyEdit. Users can change ";
-		getBehaviorScreenHelp.help += "this for themselves too.";
+	optionHelpText["dictionaryFilenames"] = "Dictionary filenames: These are dictionaries to use for spell check.  ";
+	optionHelpText["dictionaryFilenames"] += "The dictionary filenames are in the format dictionary_<language>.txt, where ";
+	optionHelpText["dictionaryFilenames"] += "<language> is the language name.  The dictionary files are located in either ";
+	optionHelpText["dictionaryFilenames"] += "sbbs/mods, sbbs/ctrl, or the same directory as SlyEdit. Users can change ";
+	optionHelpText["dictionaryFilenames"] += "this for themselves too.";
 
+	// Word-wrap the help text items
+	for (var prop in optionHelpText)
+		optionHelpText[prop] = word_wrap(optionHelpText[prop], gHelpWrapWidth);
+
+	return optionHelpText;
+}
+
+// Returns help text for the behavior configuration screen
+//
+// Parameters:
+//  pOptionHelpText: An object of help text for each option, indexed by the option name from the configuration file
+//  pCfgOptProps: An array specifying the properties to include in the help text and their order
+//
+// Return value: Help text for the behavior options screen
+function getBehaviorScreenHelp(pOptionHelpText, pCfgOptProps)
+{
+	if (getBehaviorScreenHelp.help == undefined)
+	{
+		getBehaviorScreenHelp.help = "This screen allows you to configure behavior options for SlyEdit.\r\n\r\n";
+		for (var i = 0; i < pCfgOptProps.length; ++i)
+		{
+			var optName = pCfgOptProps[i];
+			getBehaviorScreenHelp.help += pOptionHelpText[optName] + "\r\n\r\n";
+		}
 		getBehaviorScreenHelp.help = word_wrap(getBehaviorScreenHelp.help, gHelpWrapWidth);
 	}
 	return getBehaviorScreenHelp.help;
@@ -720,10 +752,8 @@ function readSlyEditCfgFile()
 // Return value: Boolean - Whether or not the save fully succeeded
 function saveSlyEditCfgFile()
 {
-	var saveSucceeded = false;
-
 	// If SlyEdit.cfg doesn't exist in the sbbs/mods directory, then copy it
-	// from sbbs/ctrl
+	// from sbbs/ctrl to sbbs/mods
 	// gCfgInfo.cfgFilename contains the full path & filename of the configuration
 	// file
 	var originalCfgFilename = "";
@@ -734,185 +764,26 @@ function saveSlyEditCfgFile()
 	var modsSlyEditCfgFilename = system.mods_dir + gSlyEdCfgFileName;
 	var modsSlyEditCfgFileExists = file_exists(modsSlyEditCfgFilename);
 	if (!modsSlyEditCfgFileExists && file_exists(originalCfgFilename))
-		modsSlyEditCfgFileExists = file_copy(originalCfgFilename, modsSlyEditCfgFilename);
-
-	var cfgFile = new File(modsSlyEditCfgFilename);
-	if (modsSlyEditCfgFileExists)
 	{
-		if (cfgFile.open("r+")) // Reading and writing (file must exist)
-		{
-			for (var settingName in gCfgInfo.cfgSections.BEHAVIOR)
-				cfgFile.iniSetValue("BEHAVIOR", settingName, gCfgInfo.cfgSections.BEHAVIOR[settingName]);
-			for (var settingName in gCfgInfo.cfgSections.ICE_COLORS)
-				cfgFile.iniSetValue("ICE_COLORS", settingName, gCfgInfo.cfgSections.ICE_COLORS[settingName]);
-			for (var settingName in gCfgInfo.cfgSections.DCT_COLORS)
-				cfgFile.iniSetValue("DCT_COLORS", settingName, gCfgInfo.cfgSections.DCT_COLORS[settingName]);
-
-			cfgFile.close();
-			saveSucceeded = true;
-		}
+		if (!file_copy(originalCfgFilename, modsSlyEditCfgFilename))
+			return false;
 	}
-	else
+
+	// Open the configuration file and save the current settings to it
+	var saveSucceeded = false;
+	var cfgFile = new File(modsSlyEditCfgFilename);
+	if (cfgFile.open("r+")) // Reading and writing (file must exist)
 	{
-		// Creae a new SlyEdit.cfg in sbbs/mods
-		if (cfgFile.open("w"))
-		{
-			saveSucceeded = true;
-			// Behavior section
-			if (!cfgFile.writeln("[BEHAVIOR]"))
-				saveSucceeded = false;
-			for (var settingName in gCfgInfo.cfgSections.BEHAVIOR)
-			{
-				// Write any comments for this setting
-				var comments = getIniFileCommentsForOpt(settingName, "BEHAVIOR");
-				for (var i = 0; i < comments.length; ++i)
-				{
-					if (!cfgFile.writeln(comments[i]))
-						saveSucceeded = false;
-				}
-				// Write the setting
-				var settingLine = settingName + "=" + gCfgInfo.cfgSections.BEHAVIOR[settingName];
-				if (!cfgFile.writeln(settingLine))
-					saveSucceeded = false;
-			}
-			// ICE_COLORS section
-			if (!cfgFile.writeln("[ICE_COLORS]"))
-				saveSucceeded = false;
-			for (var settingName in gCfgInfo.cfgSections.ICE_COLORS)
-			{
-				// Write any comments for this setting
-				var comments = getIniFileCommentsForOpt(settingName, "ICE_COLORS");
-				for (var i = 0; i < comments.length; ++i)
-				{
-					if (!cfgFile.writeln(comments[i]))
-						saveSucceeded = false;
-				}
-				// Write the setting
-				var settingLine = settingName + "=" + gCfgInfo.cfgSections.ICE_COLORS[settingName];
-				if (!cfgFile.writeln(settingLine))
-					saveSucceeded = false;
-			}
-			// DCT_COLORS section
-			if (!cfgFile.writeln("[DCT_COLORS]"))
-				saveSucceeded = false;
-			for (var settingName in gCfgInfo.cfgSections.DCT_COLORS)
-			{
-				// Write any comments for this setting
-				var comments = getIniFileCommentsForOpt(settingName, "DCT_COLORS");
-				for (var i = 0; i < comments.length; ++i)
-				{
-					if (!cfgFile.writeln(comments[i]))
-						saveSucceeded = false;
-				}
-				// Write the setting
-				var settingLine = settingName + "=" + gCfgInfo.cfgSections.DCT_COLORS[settingName];
-				if (!cfgFile.writeln(settingLine))
-					saveSucceeded = false;
-			}
+		for (var settingName in gCfgInfo.cfgSections.BEHAVIOR)
+			cfgFile.iniSetValue("BEHAVIOR", settingName, gCfgInfo.cfgSections.BEHAVIOR[settingName]);
+		for (var settingName in gCfgInfo.cfgSections.ICE_COLORS)
+			cfgFile.iniSetValue("ICE_COLORS", settingName, gCfgInfo.cfgSections.ICE_COLORS[settingName]);
+		for (var settingName in gCfgInfo.cfgSections.DCT_COLORS)
+			cfgFile.iniSetValue("DCT_COLORS", settingName, gCfgInfo.cfgSections.DCT_COLORS[settingName]);
 
-			cfgFile.close();
-		}
+		cfgFile.close();
+		saveSucceeded = true;
 	}
 
 	return saveSucceeded;
 }
-
-// Returns an array of INI file comments for a particular option (and section name)
-function getIniFileCommentsForOpt(pOptName, pSectionName)
-{
-	var commentLines = [];
-	if (pSectionName == "BEHAVIOR")
-	{
-		if (pOptName == "reWrapQuoteLines")
-		{
-			commentLines.push("; If the reWrapQuoteLines option is set to true, quote lines will be re-wrapped");
-			commentLines.push("; so that they are complete but still look good when quoted.  If this option is");
-			commentLines.push("; disabled, then quote lines will simply be trimmed to fit into the message.");
-		}
-		else if (pOptName == "allowColorSelection")
-			commentLines.push("; Whether or not to let the user change the text color");
-		else if (pOptName == "saveColorsAsANSI")
-		{
-			commentLines.push("; Whether or not to save message color/attribute codes as ANSI (if not, they");
-			commentLines.push("; will be saved as Synchronet attribute codes)");
-		}
-		else if (pOptName == "allowCrossPosting")
-			commentLines.push("; Whether or not to allow cross-posting");
-		else if (pOptName == "enableTextReplacements")
-		{
-			commentLines.push("; Whether or not to enable text replacements (AKA macros).");
-			commentLines.push("; enableTextReplacements can have one of the following values:");
-			commentLines.push("; false : Text replacement is disabled");
-			commentLines.push("; true  : Text replacement is enabled and performed as literal search and replace");
-			commentLines.push("; regex : Text replacement is enabled using regular expressions");
-		}
-		else if (pOptName == "tagLineFilename")
-			commentLines.push("; The name of the file where tag lines are stored");
-		else if (pOptName == "taglinePrefix")
-		{
-			commentLines.push("; Text to add to the front of a tagline when adding it to the message.");
-			commentLines.push("; This can be blank (nothing after the =) if no prefix is desired.");
-		}
-		else if (pOptName == "quoteTaglines")
-			commentLines.push("; Whether or not to add double-quotes around taglines");
-		else if (pOptName == "shuffleTaglines")
-		{
-			commentLines.push("; Whether or not to shuffle (randomize) the list of taglines when they are");
-			commentLines.push("; displayed for the user to choose from");
-		}
-		else if (pOptName == "allowUserSettings")
-			commentLines.push("; Whether or not to allow users to change their user settings.");
-		//; The following settings serve as defaults for the user settings, which
-		//; each user can change for themselves:
-		else if (pOptName == "useQuoteLineInitials")
-		{
-			commentLines.push("; Whether or not to prefix the quote lines with the last author's initials");
-			commentLines.push("; This also has a user option that takes precedence over this setting.");
-		}
-		else if (pOptName == "indentQuoteLinesWithInitials")
-		{
-			commentLines.push("; When prefixing quote lines with the last author's initials, whether or not");
-			commentLines.push("; to indent the quote lines with a space.");
-			commentLines.push("; This also has a user option that takes precedence over this setting.");
-		}
-		else if (pOptName == "enableTaglines")
-		{
-			commentLines.push("; Whether or not to enable the option to add a tagline");
-			commentLines.push("; This also has a user option that takes precedence over this setting.");
-		}
-		else if (pOptName == "allowEditQuoteLine")
-		{
-			commentLines.push("; Whether or not to allow editing quote lines");
-			commentLines.push("; This also has a user option that takes precedence over this setting.");
-		}
-		else if (pOptName == "allowSpellCheck")
-		{
-			commentLines.push("; Whether or not to allow spell check");
-			commentLines.push("; This also has a user option that takes precedence over this setting.");
-		}
-		else if (pOptName == "dictionaryFilenames")
-		{
-			commentLines.push("; Dictionary filenames (used for spell check): This is a comma-separated list of");
-			commentLines.push("; dictionary filenames.  The dictionary filenames are in the format");
-			commentLines.push("; dictionary_<language>.txt, where <language> is the language name.  In this");
-			commentLines.push("; list, the filenames can be in that format, or just <language>.txt, or just");
-			commentLines.push("; <language>.  Leave blank to use all dictionary files that exist on the");
-			commentLines.push("; system.  The dictionary files are located in either sbbs/mods, sbbs/ctrl, or");
-			commentLines.push("; the same directory as SlyEdit.");
-			commentLines.push("; This also has a user option that takes precedence over this setting.");
-		}
-	}
-	else if (pSectionName == "ICE_COLORS")
-	{
-		if (pOptName == "ThemeFilename")
-			commentLines.push("; The filename of the theme file (no leading path necessary)");
-		else if (pOptName == "menuOptClassicColors")
-			commentLines.push("; Whether or not to use all classic IceEdit colors (true/false)");
-	}
-	else if (pSectionName == "DCT_COLORS")
-	{
-		if (pOptName == "ThemeFilename")
-			commentLines.push("; The filename of the theme file (no leading path necessary)");
-	}
-	return commentLines;
-}
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/ddmr_cfg.js b/xtrn/DDMsgReader/ddmr_cfg.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa572bff46ddb260aa62a666f9813aacfeaefe97
--- /dev/null
+++ b/xtrn/DDMsgReader/ddmr_cfg.js
@@ -0,0 +1,765 @@
+// Configurator for Digital Distortion Message Reader: This is a menu-driven configuration
+// program/script for Digital Distortion Message reader.  If you're running DDMsgReader from
+// xtrn/DDMsgReader (the standard location), then any changes are saved to DDMsgReader.cfg in
+// sbbs/mods, so that custom changes don't get overridden due to an update.
+// If you have DDMsgReader in a directory other than xtrn/DDMsgReader, then the changes to
+// DDMsgReader.cfg will be saved in that directory (assuming you're running ddmr_cfg.js from
+// that same directory).
+// Currently for DDMsgReader 1.85.
+//
+// If you're running DDMsgReader from xtrn/DDMsgReader (the standard location) and you want
+// to save the configuration file there (rather than sbbs/mods), you can use one of the
+// following command-line options: noMods, -noMods, no_mods, or -no_mods
+
+"use strict";
+
+
+require("sbbsdefs.js", "P_NONE");
+require("uifcdefs.js", "UIFC_INMSG");
+
+
+if (!uifc.init("DigDist. Message Reader 1.85 Configurator"))
+{
+	print("Failed to initialize uifc");
+	exit(1);
+}
+js.on_exit("uifc.bail()");
+
+
+// DDMsgReader base configuration filename, and help text wrap width
+var gDDMRCfgFileName = "DDMsgReader.cfg";
+var gHelpWrapWidth = uifc.screen_width - 10;
+
+// When saving the configuration file, always save it in the same directory
+// as DDMsgReader (or this script); don't copy to mods
+var gAlwaysSaveCfgInOriginalDir = false;
+
+// Parse command-line arguments
+parseCmdLineArgs();
+
+// Read the DDMsgReader configuration file
+var gCfgInfo = readDDMsgReaderCfgFile();
+
+// Show the main menu and go from there.
+// This is in a loop so that if the user aborts from confirming to save
+// settings, they'll return to the main menu.
+var anyOptionChanged = false;
+var continueOn = true;
+while (continueOn)
+{
+	anyOptionChanged = doMainMenu() || anyOptionChanged;
+	// If any option changed, then let the user save the configuration if they want to
+	if (anyOptionChanged)
+	{
+		var userChoice = promptYesNo("Save configuration?", true, WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC);
+		if (typeof(userChoice) === "boolean")
+		{
+			if (userChoice)
+			{
+				var saveRetObj = saveDDMsgReaderCfgFile();
+				if (saveRetObj.saveSucceeded)
+				{
+					var msg = "Changes were successfully saved"; 
+					if (saveRetObj.savedToModsDir)
+						msg += " (saved to the mods dir)";
+					uifc.msg(msg);
+				}
+				else
+					uifc.msg("Failed to save settings!");
+			}
+			continueOn = false;
+		}
+	}
+	else
+		continueOn = false;
+}
+
+
+
+///////////////////////////////////////////////////////////
+// Functions
+
+function doMainMenu()
+{
+	// For menu item text formatting
+	var itemTextMaxLen = 50;
+
+	// Configuration option object properties
+	// cfgOptProps must correspond exactly with optionStrs & menuItems
+	var cfgOptProps = [
+		"listInterfaceStyle", // String (Lightbar/Traditional)
+		"reverseListOrder", // Boolean
+		"readerInterfaceStyle", // String (Scrollable/Traditional)
+		//"readerInterfaceStyleForANSIMessages", // String (Scrollable/Traditional)
+		"displayBoardInfoInHeader", // Boolean
+		"promptToContinueListingMessages", // Boolean
+		"promptConfirmReadMessage", // Boolean
+		"msgListDisplayTime", // String (written/imported)
+		"msgAreaList_lastImportedMsg_time", // String (written/imported)
+		"startMode", // String (Reader/List)
+		"tabSpaces", // Number
+		"pauseAfterNewMsgScan", // Boolean
+		"readingPostOnSubBoardInsteadOfGoToNext", // Boolean
+		"areaChooserHdrFilenameBase", // String
+		"areaChooserHdrMaxLines", // Number
+		"displayAvatars", // Boolean
+		"rightJustifyAvatars", // Boolean
+		"msgListSort", // String (Written/Received)
+		"convertYStyleMCIAttrsToSync", // Boolean
+		"prependFowardMsgSubject", // Boolean
+		"enableIndexedModeMsgListCache", // Boolean
+		"quickUserValSetIndex", // Number (can be -1)
+		"saveAllHdrsWhenSavingMsgToBBSPC", // Boolean
+		"useIndexedModeForNewscan", // Boolean
+		"themeFilename" // String
+	];
+	// Strings for the options to display on the menu
+	var optionStrs = [
+		"List Interface Style",
+		"Reverse List Order",
+		"Reader Interface Style",
+		//"readerInterfaceStyleForANSIMessages",
+		"Display Board Info In Header",
+		"Prompt to Continue Listing Messages",
+		"Prompt to Confirm Reading Message",
+		"Message List Display Time",
+		"Message Area List: Last Imported Message Time",
+		"Start Mode",
+		"Number of Spaces for Tabs",
+		"Pause After New Message Scan",
+		"Reading Post On Sub-Board Instead Of Go To Next",
+		"Area Chooser Header Filename Base",
+		"Area Chooser Header Max # of Lines",
+		"Display Avatars",
+		"Right-Justify Avatars",
+		"Message List Sort",
+		"Convert Y-Style MCI Attributes To Sync",
+		"Prepend Forwarded Message Subject with \"Fwd\"",
+		"Enable Indexed Mode Message List Cache",
+		"Quick User Val Set Index",
+		"Save All Headers When Saving Message To BBS PC",
+		"Use Indexed Mode For Newscan",
+		"Theme Filename"
+	];
+	// Build an array of formatted string to be displayed on the menu
+	// (the value formatting will depend on the variable type)
+	var menuItems = [];
+	for (var i = 0; i < cfgOptProps.length; ++i)
+	{
+		var propName = cfgOptProps[i];
+		menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[i], gCfgInfo.cfgOptions[propName]));
+	}
+
+	// Help text
+	// A dictionary of help text for each option, indexed by the option name from the configuration file
+	if (doMainMenu.optHelp == undefined)
+		doMainMenu.optHelp = getOptionHelpText();
+	if (doMainMenu.mainScreenHelp == undefined)
+		doMainMenu.mainScreenHelp = getMainHelp(doMainMenu.optHelp, cfgOptProps);
+
+	// Create a CTX to specify the current selected item index
+	if (doMainMenu.ctx == undefined)
+		doMainMenu.ctx = uifc.list.CTX();
+	// Selection
+	var winMode = WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC;
+	var menuTitle = "DD Message Reader Behavior Configuration";
+	var anyOptionChanged = false;
+	var continueOn = true;
+	while (continueOn && !js.terminated)
+	{
+		uifc.help_text = doMainMenu.mainScreenHelp;
+		var optionMenuSelection = uifc.list(winMode, menuTitle, menuItems, doMainMenu.ctx);
+		doMainMenu.ctx.cur = optionMenuSelection; // Remember the current selected item
+		if (optionMenuSelection == -1) // ESC
+			continueOn = false;
+		else
+		{
+			var optName = cfgOptProps[optionMenuSelection];
+			var itemType = typeof(gCfgInfo.cfgOptions[optName]);
+			uifc.help_text = doMainMenu.optHelp[optName];
+			if (optName == "quickUserValSetIndex")
+			{
+				// User quick-validation set index
+				var selectedValSetIdx = promptQuickValSet();
+				if (selectedValSetIdx > -1)
+				{
+					gCfgInfo.cfgOptions.quickUserValSetIndex = selectedValSetIdx;
+					anyOptionChanged = true;
+					menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, optionStrs[optionMenuSelection], gCfgInfo.cfgOptions[optName]);
+				}
+			}
+			else if (itemType === "boolean")
+			{
+				gCfgInfo.cfgOptions[optName] = !gCfgInfo.cfgOptions[optName];
+				anyOptionChanged = true;
+				menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, optionStrs[optionMenuSelection], gCfgInfo.cfgOptions[optName]);
+			}
+			else if (itemType === "number")
+			{
+				var promptStr = optionStrs[optionMenuSelection];
+				var initialVal = gCfgInfo.cfgOptions[optName].toString();
+				var minVal = 1;
+				var userInput = uifc.input(WIN_MID, promptStr, initialVal, 0, K_NUMBER|K_EDIT);
+				if (typeof(userInput) === "string" && userInput.length > 0)
+				{
+					var value = parseInt(userInput);
+					if (!isNaN(value) && value >= minVal && gCfgInfo.cfgOptions[optName] != value)
+					{
+						gCfgInfo.cfgOptions[optName] = value;
+						anyOptionChanged = true;
+						menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, optionStrs[optionMenuSelection], gCfgInfo.cfgOptions[optName]);
+					}
+				}
+			}
+			else
+			{
+				if (optName == "areaChooserHdrFilenameBase")
+				{
+					// Area chooser header filename base
+					var promptStr = optionStrs[optionMenuSelection];
+					var userInput = uifc.input(WIN_MID, promptStr, gCfgInfo.cfgOptions[optName], 60, K_EDIT);
+					if (typeof(userInput) === "string" && userInput.length > 0)
+					{
+						gCfgInfo.cfgOptions[optName] = userInput;
+						anyOptionChanged = true;
+						menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, optionStrs[optionMenuSelection], gCfgInfo.cfgOptions[optName]);
+					}
+				}
+				else if (optName == "themeFilename")
+				{
+					// Theme filename
+					var userInput = promptThemeFilename();
+					if (typeof(userInput) === "string" && userInput != gCfgInfo.cfgOptions[optName])
+					{
+						gCfgInfo.cfgOptions[optName] = userInput;
+						anyOptionChanged = true;
+						menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, optionStrs[optionMenuSelection], gCfgInfo.cfgOptions[optName]);
+					}
+				}
+				else
+				{
+					// Multiple-choice
+					var options = [];
+					if (optName == "listInterfaceStyle")
+						options = ["Lightbar", "Traditional"];
+					else if (optName == "readerInterfaceStyle")
+						options = ["Scrollable", "Traditional"];
+					else if (optName == "msgListDisplayTime" || optName == "msgAreaList_lastImportedMsg_time")
+						options = ["Written", "Imported"];
+					else if (optName == "startMode")
+						options = ["Reader", "List"];
+					else if (optName == "msgListSort")
+						options = ["Written", "Received"];
+					var promptStr = optionStrs[optionMenuSelection];
+					var userChoice = promptMultipleChoice(promptStr, options, gCfgInfo.cfgOptions[optName]);
+					if (userChoice != null && userChoice != undefined && userChoice != gCfgInfo.cfgOptions[optName])
+					{
+						gCfgInfo.cfgOptions[optName] = userChoice;
+						anyOptionChanged = true;
+						menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, optionStrs[optionMenuSelection], gCfgInfo.cfgOptions[optName]);
+					}
+				}
+			}
+		}
+	}
+
+	return anyOptionChanged;
+}
+
+// Formats text for a behavior option
+//
+// Parameters:
+//  pItemTextMaxLen: The maximum length for menu item text
+//  pItemText: The text of the item to be displayed on the menu
+//  pVal: The value of the option
+//
+// Return value: The formatted text for the menu item, with a Yes/No value indicating whether it's enabled
+function formatCfgMenuText(pItemTextMaxLen, pItemText, pVal)
+{
+	if (formatCfgMenuText.formatStr == undefined)
+		formatCfgMenuText.formatStr = "%-" + pItemTextMaxLen + "s %s";
+	// Determine what should be displayed for the value
+	var valType = typeof(pVal);
+	var value = "";
+	if (valType === "boolean")
+		value = pVal ? "Yes" : "No";
+	else
+		value = pVal.toString();
+	return format(formatCfgMenuText.formatStr, pItemText.substr(0, pItemTextMaxLen), value);
+}
+
+
+// Prompts the user for which dictionaries to use (for spell check)
+function promptThemeFilename()
+{
+	// Find theme filenames. There should be a DefaultTheme.cfg; also, look
+	// for theme .cfg filenames starting with DDMR_Theme_
+	var defaultThemeFilename = js.exec_dir + "DefaultTheme.cfg";
+	var themeFilenames = [];
+	if (file_exists(defaultThemeFilename))
+		themeFilenames.push(defaultThemeFilename);
+	themeFilenames = themeFilenames.concat(directory(js.exec_dir + "DDMR_Theme_*.cfg"));
+
+	// Abbreviated theme file names: Get just the filename without the full path,
+	// and remove the trailing .cfg
+	var abbreviatedThemeFilenames = [];
+	for (var i = 0; i < themeFilenames.length; ++i)
+	{
+		var themeFilename = file_getname(themeFilenames[i]).replace(/\.cfg$/, "");
+		abbreviatedThemeFilenames.push(themeFilename);
+	}
+	// Add an option at the end to let the user type it themselves
+	abbreviatedThemeFilenames.push("Type your own filename");
+
+	// Create a context, and look for the current theme filename & set the
+	// current index
+	var ctx = uifc.list.CTX();
+	if (gCfgInfo.cfgOptions.themeFilename.length > 0)
+	{
+		var themeFilenameUpper = gCfgInfo.cfgOptions.themeFilename.toUpperCase();
+		for (var i = 0; i < themeFilenames.length; ++i)
+		{
+			if (themeFilenames[i].toUpperCase() == themeFilenameUpper)
+			{
+				ctx.cur = i;
+				break;
+			}
+		}
+	}
+	// User input
+	var chosenThemeFilename = null;
+	var selection = uifc.list(WIN_MID, "Theme Filename", abbreviatedThemeFilenames, ctx);
+	if (selection == -1)
+	{
+		// User quit/aborted; do nothing
+	}
+	// Last item: Let the user input the filename themselves
+	else if (selection == abbreviatedThemeFilenames.length-1) 
+	{
+		var userInput = uifc.input(WIN_MID, "Theme filename", "", 0, K_EDIT);
+		if (typeof(userInput) === "string")
+			chosenThemeFilename = userInput;
+	}
+	else
+		chosenThemeFilename = file_getname(themeFilenames[selection]);
+
+	return chosenThemeFilename;
+}
+
+
+// Prompts the user to select one of multiple values for an option
+//
+// Parameters:
+//  pPrompt: The prompt text
+//  pChoices: An array of the choices
+//  pCurrentVal: The current value (to set the index in the menu)
+//
+// Return value: The user's chosen value, or null if the user aborted
+function promptMultipleChoice(pPrompt, pChoices, pCurrentVal)
+{
+	//uifc.help_text = pHelpText;
+	// Create a context object with the current value index
+	var currentValUpper = pCurrentVal.toUpperCase();
+	var ctx = uifc.list.CTX();
+	for (var i = 0; i < pChoices.length; ++i)
+	{
+		if (pChoices[i].toUpperCase() == currentValUpper)
+		{
+			ctx.cur = i;
+			break;
+		}
+	}
+	// Prompt the user and return their chosen value
+	var chosenValue = null;
+	//var winMode = WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC;
+	var winMode = WIN_MID;
+	var userSelection = uifc.list(winMode, pPrompt, pChoices, ctx);
+	if (userSelection >= 0 && userSelection < pChoices.length)
+		chosenValue = pChoices[userSelection];
+	return chosenValue;
+}
+
+// Prompts the user Yes/No for a boolean response
+//
+// Parameters:
+//  pQuestion: The question/prompt for the user
+//  pInitialVal: Boolean - Whether the initial selection in the menu should
+//               be Yes (true) or No (false)
+//  pWinMode: Optional window mode bits.  If not specified, WIN_MID will be used.
+//
+//  Return value: Boolean true (yes), false (no), or null if the user aborted
+function promptYesNo(pQuestion, pInitialVal, pWinMode)
+{
+	var chosenVal = null;
+	var winMode = typeof(pWinMode) === "number" ? pWinMode : WIN_MID;
+	// Create a CTX to specify the current selected item index
+	var ctx = uifc.list.CTX();
+	ctx.cur = typeof(pInitialVal) === "boolean" && pInitialVal ? 0 : 1;
+	switch (uifc.list(winMode, pQuestion, ["Yes", "No"], ctx))
+	{
+		case -1: // User quit/aborted - Leave chosenVal as null
+			break;
+		case 0: // User chose Yes
+			chosenVal = true;
+			break;
+		case 1: // User chose No
+			chosenVal = false;
+			break;
+		default:
+			break;
+	}
+	return chosenVal;
+}
+
+// Prompts the user for a quick-validation set. Returns the index of the
+// validation set, or -1 if none chosen.
+function promptQuickValSet()
+{
+	var chosenQuickValSetIdx = -1;
+
+	var promptStr = "Quick-Validation Values";
+	// Create a context object with the current value index
+	var ctx = uifc.list.CTX();
+	if (gCfgInfo.cfgOptions.quickUserValSetIndex >= 0 && gCfgInfo.cfgOptions.quickUserValSetIndex < quickValSets.length)
+		ctx.cur = gCfgInfo.cfgOptions.quickUserValSetIndex;
+
+	// Choices: If main.ini exists (i.e., Synchronet 3.20+), then get the
+	// quick validation sets from that. Otherwise, just make a list of indexes 0-9.
+	var menuStrs = [];
+	if (file_exists(system.ctrl_dir + "main.ini"))
+	{
+		var quickValSets = getQuickValidationVals();
+		var formatStr = "%-d: SL: %-3d F1: %s";
+		for (var i = 0; i < quickValSets.length; ++i)
+			menuStrs.push(format(formatStr, i, quickValSets[i].level, getUserValFlagsStr(quickValSets[i].flags1)));
+	}
+	else
+	{
+		for (var i = 0; i < 10; ++i)
+			menuStrs.push(i.toString());
+	}
+
+	// Prompt the user and return their chosen value
+	var chosenValue = null;
+	//var winMode = WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC;
+	var winMode = WIN_MID;
+	var userSelection = uifc.list(winMode, promptStr, menuStrs, ctx);
+	if (userSelection >= 0 && userSelection < quickValSets.length)
+		chosenQuickValSetIdx = userSelection;
+	return chosenQuickValSetIdx;
+}
+
+
+///////////////////////////////////////////////////
+// Help text functions
+
+
+// Returns a dictionary of help text, indexed by the option name from the configuration file
+function getOptionHelpText()
+{
+	var optionHelpText = {};
+	optionHelpText["listInterfaceStyle"] = "List Interface Style: Either Lightbar or Traditional";
+
+	optionHelpText["reverseListOrder"] = "Reverse List Order: Whether or not message lists are to be shown in reverse order. This is a default for ";
+	optionHelpText["reverseListOrder"] += "a user setting.";
+
+	optionHelpText["readerInterfaceStyle"] = "Reader Interface Style: This can be either Scrollable (allowing scrolling up & down) or Traditional. ";
+	optionHelpText["readerInterfaceStyle"] += "The scrollable interface only works if the user's terminal supports ANSI. The reader will fall back ";
+	optionHelpText["readerInterfaceStyle"] += "to a traditional interface if the usser's terminal doesn't support ANSI.";
+
+	//optionHelpText["readerInterfaceStyleForANSIMessages"] = "";
+
+	optionHelpText["displayBoardInfoInHeader"] = "Display Board Info In Header: Whether or not to display the message group and sub-board lines in the ";
+	optionHelpText["displayBoardInfoInHeader"] += "header at the top of the screen (an additional 2 lines).";
+
+	optionHelpText["promptToContinueListingMessages"] = "Prompt to Continue Listing Messages: Whether or not to ask the user if they want to continue listing ";
+	optionHelpText["promptToContinueListingMessages"] += "messages after they read a message";
+
+	optionHelpText["promptConfirmReadMessage"] = "Prompt to Confirm Reading Message: Whether or not to prompt the user to confirm to read a message ";
+	optionHelpText["promptConfirmReadMessage"] += "when a message is selected from the message list";
+
+	optionHelpText["msgListDisplayTime"] = "Message List Display Time: Whether to display the message import time or the written time in the ";
+	optionHelpText["msgListDisplayTime"] += "message lists.  Valid values are imported and written";
+
+	optionHelpText["msgAreaList_lastImportedMsg_time"] = "Message Area List Last Imported Message Time: For the last message time in the message lists, ";
+	optionHelpText["msgAreaList_lastImportedMsg_time"] += "whether to use message import time or written time. Valid values are imported and written";
+
+	optionHelpText["startMode"] = "Start Mode: Specifies the default startup mode (Reader/Read or Lister/List)";
+
+	optionHelpText["tabSpaces"] = "Number of Spaces for Tabs: This specifies how many spaces to use for tabs (if a message has tabs)";
+
+	optionHelpText["pauseAfterNewMsgScan"] = "Pause After Nes Message Scan: Whether or not to pause at the end of a newscan";
+
+	optionHelpText["readingPostOnSubBoardInsteadOfGoToNext"] = "Reading Post On Sub-Board Instead Of Go To Next: When reading messages (but not for a newscan, etc.): ";
+	optionHelpText["readingPostOnSubBoardInsteadOfGoToNext"] += "Whether or not to ask the user whether to post on the sub-board in reader mode after reading the last ";
+	optionHelpText["readingPostOnSubBoardInsteadOfGoToNext"] += "message instead of prompting to go to the next sub-board.  This is  like the stock Synchronet behavior.";
+
+	optionHelpText["areaChooserHdrFilenameBase"] = "Area Chooser Header Filename Base: If you'd like to have an ANSI displayed above the lists of the area ";
+	optionHelpText["areaChooserHdrFilenameBase"] += "chooser, you can specify the 'base' of the filename (without the .ans/.asc) here. The file must be in the ";
+	optionHelpText["areaChooserHdrFilenameBase"] += "same directory as DDMsgReader.js.";
+
+	optionHelpText["areaChooserHdrMaxLines"] = "Area Chooser Header Max # of Lines: The maximum number of lines to use from the area chooser header file";
+
+	optionHelpText["displayAvatars"] = "Display Avatars: Whether or not to display user avatars (the small user-specified ANSIs) when reading messages";
+
+	optionHelpText["rightJustifyAvatars"] = "Right-Justify Avatars: Whether or not to display user avatars on the right. If false, they will be displayed ";
+	optionHelpText["rightJustifyAvatars"] += "on the left.";
+
+	optionHelpText["msgListSort"] = "Message List Sort: How to sort the message lists - Either Received (by date/time received, which is faster), ";
+	optionHelpText["msgListSort"] += "or Written (by date/time written, which takes time due to sorting)";
+
+	optionHelpText["convertYStyleMCIAttrsToSync"] = "Convert Y-Style MCI Attributes to Sync: Whether or not to convert Y-Style MCI attribute/color codes to ";
+	optionHelpText["convertYStyleMCIAttrsToSync"] += "Synchronet attribute codes (if disabled, these codes will appear as-is rather than as the colors or ";
+	optionHelpText["convertYStyleMCIAttrsToSync"] += "attributes they represent)";
+
+	optionHelpText["prependFowardMsgSubject"] = "Prepend Forwarded Message Subject with \"Fwd\" Whether or not to prepend the subject for forwarded messages ";
+	optionHelpText["prependFowardMsgSubject"] += "with \"Fwd: \"";
+
+	optionHelpText["enableIndexedModeMsgListCache"] = "Enable Indexed Mode Message List Cache: For indexed reader mode, whether or not to enable caching the message ";
+	optionHelpText["enableIndexedModeMsgListCache"] += "header lists for performance";
+
+	optionHelpText["quickUserValSetIndex"] = "Quick User Val Set Index: An index of a quick-validation set from SCFG > System > Security Options > ";
+	optionHelpText["quickUserValSetIndex"] += "Quick-Validation Values to be used by the sysop to quick-validate a local user who has posted a message while ";
+	optionHelpText["quickUserValSetIndex"] += "reading the  message. This index is 0-based, as they appear in SCFG. Normally there are 10 quick-validation ";
+	optionHelpText["quickUserValSetIndex"] += "values, so valid values for this index are 0 through 9. If you would rather DDMsgReader display a menu of ";
+	optionHelpText["quickUserValSetIndex"] += "quick-validation sets, you can set this to an invalid index (such as -1).";
+
+	optionHelpText["saveAllHdrsWhenSavingMsgToBBSPC"] = "Save All Headers When Saving Message to BBS PC: As the sysop, you can save messages to the BBS PC. This ";
+	optionHelpText["saveAllHdrsWhenSavingMsgToBBSPC"] += "option specifies whether or not to save all the message headers along with the message. If disabled, ";
+	optionHelpText["saveAllHdrsWhenSavingMsgToBBSPC"] += "only a few relevant headers will be saved (such as From, To, Subject, and message time).";
+
+	optionHelpText["useIndexedModeForNewscan"] = "Used Indexed Mode for Newscan: Whether or not to use indexed mode for message newscans (not for new-to-you ";
+	optionHelpText["useIndexedModeForNewscan"] += "scans). This is a default for a user setting. When indexed mode is enabled for newscans, the reader displays ";
+	optionHelpText["useIndexedModeForNewscan"] += "a menu showing each sub-board and the number of new messages and total messages in each. When disabled, ";
+	optionHelpText["useIndexedModeForNewscan"] += "the reader will do a traditional newscan where it will scan through the sub-boards and go into reader ";
+	optionHelpText["useIndexedModeForNewscan"] += "mode when there are new messages in a sub-board.";
+
+	optionHelpText["themeFilename"] = "Theme filename: The name of a file for a color theme to use";
+
+	// Word-wrap the help text items
+	for (var prop in optionHelpText)
+		optionHelpText[prop] = word_wrap(optionHelpText[prop], gHelpWrapWidth);
+
+	return optionHelpText;
+}
+
+// Returns help text for the main configuration screen (behavior settings)
+//
+// Parameters:
+//  pOptionHelpText: An object of help text for each option, indexed by the option name from the configuration file
+//  pCfgOptProps: An array specifying the properties to include in the help text and their order
+//
+// Return value: Help text for the main configuration screen (behavior settings)
+function getMainHelp(pOptionHelpText, pCfgOptProps)
+{
+	var helpText = "This screen allows you to configure behavior options for Digital Distortion Message Reader.\r\n\r\n";
+	for (var i = 0; i < pCfgOptProps.length; ++i)
+	{
+		var optName = pCfgOptProps[i];
+		helpText += pOptionHelpText[optName] + "\r\n\r\n";
+	}
+	return word_wrap(helpText, gHelpWrapWidth);
+}
+
+
+///////////////////////////////////////////////////
+// Non-UI utility functions
+
+// Returns an array of the quick-validation sets configured in
+// SCFG > System > Security > Quick-Validation Values.  This reads
+// from main.ini, which exists with Synchronet 3.20 and newer.
+// In SCFG:
+//
+// Level                 60     |
+// Flag Set #1                  |
+// Flag Set #2                  |
+// Flag Set #3                  |
+// Flag Set #4                  |
+// Exemptions                   |
+// Restrictions                 |
+// Extend Expiration     0 days |
+// Additional Credits    0      |
+//
+// Each object in the returned array will have the following properties:
+//  level (numeric)
+//  expire
+//  flags1
+//  flags2
+//  flags3
+//  flags4
+//  credits
+//  exemptions
+//  restrictions
+function getQuickValidationVals()
+{
+	var validationValSets = [];
+	// In SCFG > System > Security > Quick-Validation Values, there are 10 sets of
+	// validation values.  These are in main.ini as [valset:0] through [valset:9]
+	// This reads from main.ini, which exists with Synchronet 3.20 and newer.
+	//system.version_num >= 32000
+	var mainIniFile = new File(system.ctrl_dir + "main.ini");
+	if (mainIniFile.open("r"))
+	{
+		for (var i = 0; i < 10; ++i)
+		{
+			// Flags:
+			// AZ is 0x2000001
+			// A is 1
+			// Z is 0x2000000
+			var valSection = mainIniFile.iniGetObject(format("valset:%d", i));
+			if (valSection != null)
+				validationValSets.push(valSection);
+		}
+		mainIniFile.close();
+	}
+	return validationValSets;
+}
+// Generates a string based on user quick-validation flags
+//
+// Parameters:
+//  pFlags: A bitfield of user quick-validation flags
+//
+// Return value: A string representing the quick-validation flags (as would appear in SCFG)
+function getUserValFlagsStr(pFlags)
+{
+	var flagLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+	var flagsStr = "";
+	for (var i = 0; i < flagLetters.length; ++i)
+		flagsStr += (Boolean((1 << i) & pFlags) ? flagLetters[i] : " ");
+	return truncsp(flagsStr);
+}
+
+// Parses command-line arguments & sets any applicable values
+function parseCmdLineArgs()
+{
+	for (var i = 0; i < argv.length; ++i)
+	{
+		var argUpper = argv[i].toUpperCase();
+		if (argUpper == "NOMODS" || argUpper == "NO_MODS" || argUpper == "-NOMODS" || argUpper == "-NO_MODS")
+			gAlwaysSaveCfgInOriginalDir = true;
+	}
+}
+
+// Reads the DDMsgReader configuration file
+//
+// Return value: An object with the following properties:
+//               cfgFilename: The full path & filename of the DDMsgReader configuration file that was read
+//               cfgOptions: A dictionary of the configuration options and their values
+function readDDMsgReaderCfgFile()
+{
+	var retObj = {
+		cfgFilename: "",
+		cfgOptions: {}
+	};
+
+	// Determine the location of the configuration file.  First look for it
+	// in the sbbs/mods directory, then sbbs/ctrl, then in the same directory
+	// as this script.
+	var cfgFilename = file_cfgname(system.mods_dir, gDDMRCfgFileName);
+	if (!file_exists(cfgFilename))
+		cfgFilename = file_cfgname(system.ctrl_dir, gDDMRCfgFileName);
+	if (!file_exists(cfgFilename))
+		cfgFilename = file_cfgname(js.exec_dir, gDDMRCfgFileName);
+	retObj.cfgFilename = cfgFilename;
+
+	// Open and read the configuration file
+	var cfgFile = new File(retObj.cfgFilename);
+	if (cfgFile.open("r"))
+	{
+		retObj.cfgOptions = cfgFile.iniGetObject();
+		cfgFile.close();
+	}
+
+	// In case some settings weren't loaded, add defaults
+	if (!retObj.cfgOptions.hasOwnProperty("listInterfaceStyle"))
+		retObj.cfgOptions.listInterfaceStyle = "Lightbar";
+	if (!retObj.cfgOptions.hasOwnProperty("reverseListOrder"))
+		retObj.cfgOptions.reverseListOrder = false;
+	if (!retObj.cfgOptions.hasOwnProperty("readerInterfaceStyle"))
+		retObj.cfgOptions.readerInterfaceStyle = "Scrollable";
+	if (!retObj.cfgOptions.hasOwnProperty("readerInterfaceStyleForANSIMessages"))
+		retObj.cfgOptions.readerInterfaceStyleForANSIMessages = "Scrollable";
+	if (!retObj.cfgOptions.hasOwnProperty("displayBoardInfoInHeader"))
+		retObj.cfgOptions.displayBoardInfoInHeader = false;
+	if (!retObj.cfgOptions.hasOwnProperty("promptToContinueListingMessages"))
+		retObj.cfgOptions.promptToContinueListingMessages = false;
+	if (!retObj.cfgOptions.hasOwnProperty("promptConfirmReadMessage"))
+		retObj.cfgOptions.promptConfirmReadMessage = false;
+	if (!retObj.cfgOptions.hasOwnProperty("msgListDisplayTime"))
+		retObj.cfgOptions.msgListDisplayTime = "written";
+	if (!retObj.cfgOptions.hasOwnProperty("msgAreaList_lastImportedMsg_time"))
+		retObj.cfgOptions.msgAreaList_lastImportedMsg_time = "written";
+	if (!retObj.cfgOptions.hasOwnProperty("startMode"))
+		retObj.cfgOptions.startMode = "Reader";
+	if (!retObj.cfgOptions.hasOwnProperty("tabSpaces"))
+		retObj.cfgOptions.tabSpaces = 3;
+	if (!retObj.cfgOptions.hasOwnProperty("pauseAfterNewMsgScan"))
+		retObj.cfgOptions.pauseAfterNewMsgScan = true;
+	if (!retObj.cfgOptions.hasOwnProperty("readingPostOnSubBoardInsteadOfGoToNext"))
+		retObj.cfgOptions.readingPostOnSubBoardInsteadOfGoToNext = false;
+	if (!retObj.cfgOptions.hasOwnProperty("areaChooserHdrFilenameBase"))
+		retObj.cfgOptions.areaChooserHdrFilenameBase = "";
+	if (!retObj.cfgOptions.hasOwnProperty("areaChooserHdrMaxLines"))
+		retObj.cfgOptions.areaChooserHdrMaxLines = 12;
+	if (!retObj.cfgOptions.hasOwnProperty("displayAvatars"))
+		retObj.cfgOptions.displayAvatars = true;
+	if (!retObj.cfgOptions.hasOwnProperty("rightJustifyAvatars"))
+		retObj.cfgOptions.rightJustifyAvatars = true;
+	if (!retObj.cfgOptions.hasOwnProperty("msgListSort"))
+		retObj.cfgOptions.msgListSort = "Received";
+	if (!retObj.cfgOptions.hasOwnProperty("convertYStyleMCIAttrsToSync"))
+		retObj.cfgOptions.convertYStyleMCIAttrsToSync = true;
+	if (!retObj.cfgOptions.hasOwnProperty("prependFowardMsgSubject"))
+		retObj.cfgOptions.prependFowardMsgSubject = true;
+	if (!retObj.cfgOptions.hasOwnProperty("enableIndexedModeMsgListCache"))
+		retObj.cfgOptions.enableIndexedModeMsgListCache = true;
+	if (!retObj.cfgOptions.hasOwnProperty("quickUserValSetIndex"))
+		retObj.cfgOptions.quickUserValSetIndex = -1;
+	if (!retObj.cfgOptions.hasOwnProperty("saveAllHdrsWhenSavingMsgToBBSPC"))
+		retObj.cfgOptions.saveAllHdrsWhenSavingMsgToBBSPC = false;
+	if (!retObj.cfgOptions.hasOwnProperty("useIndexedModeForNewscan"))
+		retObj.cfgOptions.useIndexedModeForNewscan = false;
+	if (!retObj.cfgOptions.hasOwnProperty("themeFilename"))
+		retObj.cfgOptions.themeFilename = "DefaultTheme.cfg";
+
+	return retObj;
+}
+
+// Saves the DDMsgReader configuration file using the settings in gCfgInfo
+//
+// Return value: An object with the following properties:
+//               saveSucceeded: Boolean - Whether or not the save fully succeeded
+//               savedToModsDir: Boolean - Whether or not the .cfg file was saved to the mods directory
+function saveDDMsgReaderCfgFile()
+{
+	var retObj = {
+		saveSucceeded: false,
+		savedToModsDir: false
+	};
+
+	// If the configuration file was loaded from the standard location in
+	// the Git repository (xtrn/DDMsgReader), and the option to always save
+	// in the original directory is not set, then the configuration file
+	// should be copied to sbbs/mods to avoid custom settings being overwritten
+	// with an update.
+	var defaultDirRE = new RegExp("xtrn[\\\\/]DDMsgReader[\\\\/]" + gDDMRCfgFileName + "$");
+	var cfgFilename = gCfgInfo.cfgFilename;
+	if (defaultDirRE.test(cfgFilename) && !gAlwaysSaveCfgInOriginalDir)
+	{
+		cfgFilename = system.mods_dir + gDDMRCfgFileName;
+		if (!file_copy(gCfgInfo.cfgFilename, cfgFilename))
+			return false;
+		retObj.savedToModsDir = true;
+	}
+
+	var cfgFile = new File(cfgFilename);
+	if (cfgFile.open("r+")) // Reading and writing (file must exist)
+	{
+		for (var settingName in gCfgInfo.cfgOptions)
+			cfgFile.iniSetValue(null, settingName, gCfgInfo.cfgOptions[settingName]);
+		cfgFile.close();
+		retObj.saveSucceeded = true;
+	}
+
+	return retObj;
+}