diff --git a/xtrn/DDAreaChoosers/DDFileAreaChooser.js b/xtrn/DDAreaChoosers/DDFileAreaChooser.js
index fc353d1f8e5ffc74eb952d44b7e67246f6f88000..1cb680793df6135ead4485831dee1892990c0428 100644
--- a/xtrn/DDAreaChoosers/DDFileAreaChooser.js
+++ b/xtrn/DDAreaChoosers/DDFileAreaChooser.js
@@ -52,6 +52,10 @@
  * 2023-05-14 Eric Oulashin   Version 1.35
  *                            Refactored the configuration reading code.
  *                            Fix: Displays correct file counts in directories when using directory name collapsing
+ * 2023-07-21 Eric Oulashin   Version 1.36
+ *                            Fix for directory collapsing mode with the lightbar interface: It now exits
+ *                            when the user chooses their same file directory instead of continuing the
+ *                            menu input loop.
  */
 
 // TODO: Failing silently when 1st argument is true
@@ -92,8 +96,8 @@ if (system.version_num < 31400)
 }
 
 // Version & date variables
-var DD_FILE_AREA_CHOOSER_VERSION = "1.35";
-var DD_FILE_AREA_CHOOSER_VER_DATE = "2023-05-14";
+var DD_FILE_AREA_CHOOSER_VERSION = "1.36";
+var DD_FILE_AREA_CHOOSER_VER_DATE = "2023-07-21";
 
 // Keyboard input key codes
 var CTRL_H = "\x08";
@@ -1059,8 +1063,8 @@ function DDFileAreaChooser_SelectFileArea_Lightbar(pLevel, pLibIdx, pDirIdx, pCa
 	// 3: Choose a subdirectory within a directory, for directory name collapsing
 	else if ((level == 2) || (level == 3))
 	{
-		if (typeof(pLibIdx) != "number")
-			return;
+		// If there are no directories in the given library index, then see if there's a next library with
+		// directories (and wrap around)
 		if (file_area.lib_list[pLibIdx].dir_list.length == 0)
 		{
 			console.clear("\x01n");
@@ -1386,27 +1390,36 @@ function DDFileAreaChooser_SelectFileArea_Lightbar(pLevel, pLibIdx, pDirIdx, pCa
 				// enabled)
 				var dirCodeBackup = bbs.curdir_code;
 				var chosenFileDirIdx = this.SelectFileArea_Lightbar(level+1, chosenIdx, null, true);
-				if (chosenFileDirIdx > -1)
-				{
-					// Set the current file directory
-					if (this.useDirCollapsing)
-						bbs.curdir_code = this.lib_list[chosenIdx].dir_list[chosenFileDirIdx].code;
-					else
-						bbs.curdir_code = file_area.lib_list[chosenIdx].dir_list[chosenFileDirIdx].code;
-					continueOn = false;
-				}
-				else
+				// chosenFileDirIdx could actually be a boolean and could be false (returned
+				// when pLevel is 3 and the user chose a directory), so check its type and
+				// act accordingly.
+				var retValType = typeof(chosenFileDirIdx);
+				if (retValType === "boolean")
+					continueOn = chosenFileDirIdx;
+				else if (retValType === "number")
 				{
-					// If the dir changed (probably at level 3 because directory
-					// collapsing is enabled), then exit here.
-					if (bbs.curdir_code != dirCodeBackup)
+					if (chosenFileDirIdx > -1)
+					{
+						// Set the current file directory
+						if (this.useDirCollapsing)
+							bbs.curdir_code = this.lib_list[chosenIdx].dir_list[chosenFileDirIdx].code;
+						else
+							bbs.curdir_code = file_area.lib_list[chosenIdx].dir_list[chosenFileDirIdx].code;
 						continueOn = false;
+					}
 					else
 					{
-						// A file directory was not chosen, so we'll have to re-draw
-						// the header and key help line
-						displayListHdrLines(level, this, pLibIdx, pDirIdx);
-						this.WriteKeyHelpLine();
+						// If the dir changed (probably at level 3 because directory
+						// collapsing is enabled), then exit here.
+						if (bbs.curdir_code != dirCodeBackup)
+							continueOn = false;
+						else
+						{
+							// A file directory was not chosen, so we'll have to re-draw
+							// the header and key help line
+							displayListHdrLines(level, this, pLibIdx, pDirIdx);
+							this.WriteKeyHelpLine();
+						}
 					}
 				}
 			}
@@ -1427,10 +1440,12 @@ function DDFileAreaChooser_SelectFileArea_Lightbar(pLevel, pLibIdx, pDirIdx, pCa
 						if (chosenSubdirIdx > -1)
 						{
 							// Set the current file directory
-							// TODO: This doesn't seem to be exiting, it's just
-							// going back to the library menu
 							bbs.curdir_code = this.lib_list[pLibIdx].dir_list[chosenIdx].subdir_list[chosenSubdirIdx].code;
 							continueOn = false;
+							// Return a false here so that after this function is called by itself when
+							// pLevel is 2, it will know that a sub-board has been chosen and will set
+							// continueOn to false so that the loop won't continue from there.
+							return false;
 						}
 						else
 						{
@@ -2826,3 +2841,35 @@ function attrCodeStr(pAttrCodeCharStr)
 	return str;
 }
 
+// Finds the index of a file library AFTER the given library index which contains
+// directories.  If there are none, this will return -1.
+//
+// Parameters:
+//  pLibIdx: An index of a file library; this function will start searching AFTER this one
+//
+// Return value: An index of a file library after the given group index that contains directories,
+//               or -1 if there are none
+function findNextLibIdxWithDirs(pLibIdx)
+{
+	if (typeof(pLibIdx) !== "number")
+		return -1;
+	var nextLibIdx = -1;
+	//file_area.lib_list[pLibIdx].dir_list
+	if (pLibIdx < file_area.lib_list.length - 1)
+	{
+		for (var i = pLibIdx + 1; i < file_area.lib_list.length && nextLibIdx == -1; ++i)
+		{
+			if (file_area.lib_list[i].dir_list.length > 0)
+				nextLibIdx = i;
+		}
+	}
+	if (nextLibIdx == -1 && pLibIdx > 0)
+	{
+		for (var i = 0; i < pLibIdx && nextLibIdx == -1; ++i)
+		{
+			if (file_area.lib_list[i].dir_list.length > 0)
+				nextLibIdx = i;
+		}
+	}
+	return nextLibIdx;
+}
diff --git a/xtrn/DDAreaChoosers/readme.txt b/xtrn/DDAreaChoosers/readme.txt
index 718134157c69c709d6449d59327e5313738bdfd4..cddc6cc77ca49781269e0bd40a00bc9716e940f7 100644
--- a/xtrn/DDAreaChoosers/readme.txt
+++ b/xtrn/DDAreaChoosers/readme.txt
@@ -1,6 +1,6 @@
                      Digital Distortion Area Choosers
-                              Version 1.35
-                        Release date: 2023-05-14
+                              Version 1.36
+                        Release date: 2023-07-21
 
                                   by
 
diff --git a/xtrn/DDAreaChoosers/revision_history.txt b/xtrn/DDAreaChoosers/revision_history.txt
index d8debf37974d4ae87138f7284b373b61863e1033..96ef17f9c3d2a71aacb04de8420a2014033be164 100644
--- a/xtrn/DDAreaChoosers/revision_history.txt
+++ b/xtrn/DDAreaChoosers/revision_history.txt
@@ -5,6 +5,11 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.36     2023-07-21   Message area chooser fix for not allowing to change sub-
+                      board if the first group is empty. File area chooser fix:
+                      When using directory collapsing mode with the lightbar
+                      interface, it now exits when the user chooses their same
+                      file directory instead of continuing the menu input loop.
 1.35     2023-05-14   File area chooser fix: Displays correct file counts in
                       directories when using directory name collapsing.
                       Both: Internal refactor for the code that reads the
diff --git a/xtrn/ddfilelister.js b/xtrn/ddfilelister.js
deleted file mode 100644
index 7451fba24d5c83328374bf14c0a61d9b6aa70aa6..0000000000000000000000000000000000000000
--- a/xtrn/ddfilelister.js
+++ /dev/null
@@ -1,4212 +0,0 @@
-/* This is a file lister door for Synchronet.
- *
- * Author: Eric Oulashin (AKA Nightfox)
- * BBS: Digital Distortion
- * BBS address: digitaldistortionbbs.com (or digdist.synchro.net)
- *
- * Date       Author            Description
- * 2022-01-17 Eric Oulashin     Version 0.01
- *                              Started work on this script
- * 2022-02-06 Eric Oulashin     Version 2.00
- *                              Functionality implemented (for lightbar/ANSI terminal).
- *                              Seems to work as expected.  Releasing this version.
- *                              I'm calling this version 2.00 because I had already
- *                              released a file lister mod years ago (modding the stock
- *                              Synchronet file list interface).
- * 2022-02-07 Eric Oulashin     Version 2.01
- *                              Fixed file description being undefined when viewing
- *                              file info.  Fixed command bar refreshing when pressing
- *                              the hotkeys.  Added an option to pause after viewing a
- *                              file (defaults to true).
- * 2022-02-13 Eric Oulashin     Version 2.02
- *                              Things overall look good. Releasing this version.  Added
- *                              the ability to do searching via filespec, description, and
- *                              new file search (started working on this 2022-02-08).
- * 2022-02-27 Eric Oulashin     Version 2.03
- *                              For terminals over 25 rows tall, the file info window will
- *                              now be up to 45 rows tall.  Also, fixed the display of the
- *                              trailing blocks for the list header for wide terminals (over
- *                              80 columns).
- * 2022-03-09 Eric Oulashin     Version 2.04
- *                              Bug fix: Now successfully formats filenames without extensions
- *                              when listing files.
- * 2022-03-12 Eric Oulashin     Version 2.05
- *                              Now makes use of the user's extended file description setting:
- *                              If the user's extended file description setting is enabled,
- *                              the lister will now show extended file descriptions on the
- *                              main screen in a split format, with the lightbar file list
- *                              on the left and the extended file description for the
- *                              highlighted file on the right.  Also, made the file info
- *                              window taller for terminals within 25 lines high.
- *                              I had started work on this on March 9, 2022.
- * 2022-03-13 Eric Oulashin     Version 2.05a
- *                              Fix for "fileDesc is not defined" error when displaying
- *                              the file description on the main screen.  Also made a
- *                              small refactor to the main screen refresh function.
- * 2022-04-13 Eric Oulashin     Version 2.06
- *                              When extended file descriptions are enabled, the file
- *                              date is now shown with the file description on the last
- *                              line.
- * 2022-12-02 Eric Oulashin     Version 2.07
- *                              In a file's extended description, added the number of times
- *                              downloaded and date/time last downloaded.  Also, fixed a bug
- *                              where some descriptions were blank in the Frame object because
- *                              of a leading normal attribute (the fix may be a kludge though).
- * 2023-01-18 Eric Oulashin     Version 2.08
- *                              When doing a file search in multiple directories, the file
- *                              library & directory is now shown in the header as the user
- *                              scrolls through the file list/search results.  Also,
- *                              used lfexpand() to ensure the extended description has
- *                              CRLF endings, useful for splitting it into multiple lines properly.
- * 2023-02-25 Eric Oulashin     Version 2.09
- *                              Now supports being used as a loadable module for
- *                              Scan Dirs and List Files
- * 2023-02-27 Eric Oulashin     Version 2.10
- *                              Now allows downloading a single selected file with the D key.
- *                              Also, ddfilelister now checks whether the user has permission (based
- *                              on ARS) to download before allowing adding files to their batch
- *                              download queue (and downloading a single file as well).
-*/
-
-"use strict";
-
-if (typeof(require) === "function")
-{
-	require("sbbsdefs.js", "K_UPPER");
-	require('key_defs.js', 'KEY_UP');
-	require("text.js", "Email"); // Text string definitions (referencing text.dat)
-	require("dd_lightbar_menu.js", "DDLightbarMenu");
-	require("frame.js", "Frame");
-	require("scrollbar.js", "ScrollBar");
-	require("mouse_getkey.js", "mouse_getkey");
-	require("attr_conv.js", "convertAttrsToSyncPerSysCfg");
-}
-else
-{
-	load("sbbsdefs.js");
-	load('key_defs.js');
-	load("text.js"); // Text string definitions (referencing text.dat)
-	load("dd_lightbar_menu.js");
-	load("frame.js");
-	load("scrollbar.js");
-	load("mouse_getkey.js");
-	load("attr_conv.js");
-}
-
-
-/*
-Configured in SCFG->System->Loadable Modules:
-Scan Dirs:      User scans one or more directories for (e.g. new) files
-List Files:     User lists files within a file directory
-View File Info: User views detailed information on files in a directory
-
-This addresses/fixes feature request #521 for Nightfox
-
-Will need to document the mode argument bit values on the wiki, but
-it's the usual suspects: FL_* for scandirs and listfiles and FI_* for
-fileinfo. The scandirs_mod will be passed an extra bool (0/1) arg that
-indicates whether or not the user is scanning *all* directories.
-*/
-
-
-// This script requires Synchronet version 3.19 or newer.
-// If the Synchronet version is below the minimum, then exit.
-if (system.version_num < 31900)
-{
-	if (user.is_sysop)
-	{
-		var message = "\x01n\x01h\x01y\x01i* Warning:\x01n\x01h\x01w Digital Distortion File Lister "
-		            + "requires version \x01g3.19\x01w or\r\n"
-		            + "newer of Synchronet.  This BBS is using version \x01g" + system.version
-		            + "\x01w.\x01n";
-		console.crlf();
-		console.print(message);
-		console.crlf();
-		console.pause();
-	}
-	exit();
-}
-
-// Lister version information
-var LISTER_VERSION = "2.10";
-var LISTER_DATE = "2023-02-27";
-
-
-///////////////////////////////////////////////////////////////////////////////
-// Global variables
-
-var KEY_BACKSPACE = CTRL_H;
-
-// Block characters
-var BLOCK1 = "\xB0"; // Dimmest block
-var BLOCK2 = "\xB1";
-var BLOCK3 = "\xB2";
-var BLOCK4 = "\xDB"; // Brightest block
-var THIN_RECTANGLE_LEFT = "\xDD";
-var THIN_RECTANGLE_RIGHT = "\xDE";
-var RIGHT_T_HDOUBLE_VSINGLE = "\xB5";
-var LEFT_T_HDOUBLE_VSINGLE = "\xcC6";
-
-// For file sizes
-//var BYTES_PER_TB = 1099511627776; // Seems to be too big for JS
-var BYTES_PER_GB = 1073741824;
-var BYTES_PER_MB = 1048576;
-var BYTES_PER_KB = 1024;
-
-// File list column indexes (0-based).  The end indexes are one past the last index.
-// These defaults assume an 80-character wide terminal.
-var gListIdxes = {
-	filenameStart: 0
-};
-// The end index of each column includes the trailing space so that
-// highlight colors will highlight the whole field
-gListIdxes.filenameEnd = gListIdxes.filenameStart + 13;
-gListIdxes.fileSizeStart = gListIdxes.filenameEnd;
-gListIdxes.fileSizeEnd = gListIdxes.fileSizeStart + 7;
-gListIdxes.descriptionStart = gListIdxes.fileSizeEnd;
-gListIdxes.descriptionEnd = console.screen_columns - 1; // Leave 1 character remaining on the screen
-// Colors
-var gColors = {
-	filename: "\x01n\x01b\x01h",
-	fileSize: "\x01n\x01m\x01h",
-	desc: "\x01n\x01w",
-	bkgHighlight: "\x01n\x01" + "4",
-	filenameHighlight: "\x01c\x01h",
-	fileSizeHighlight: "\x01c\x01h",
-	descHighlight: "\x01c\x01h",
-	fileTimestamp: "\x01g\x01h",
-	fileInfoWindowBorder: "\x01r",
-	fileInfoWindowTitle: "\x01g",
-	errorBoxBorder: "\x01g\x01h",
-	errorMessage: "\x01y\x01h",
-	successMessage: "\x01c",
-
-	batchDLInfoWindowBorder: "\x01r",
-	batchDLInfoWindowTitle: "\x01g",
-	confirmFileActionWindowBorder: "\x01r",
-	confirmFileActionWindowWindowTitle: "\x01g",
-
-	fileAreaMenuBorder: "\x01b",
-	fileNormalBkg: "\x01" + "4",
-	fileAreaNum: "\x01w",
-	fileAreaDesc: "\x01w",
-	fileAreaNumItems: "\x01w",
-
-	fileAreaMenuHighlightBkg: "\x01" + "7",
-	fileAreaNumHighlight: "\x01b",
-	fileAreaDescHighlight: "\x01b",
-	fileAreaNumItemsHighlight: "\x01b"
-};
-
-
-// Actions
-var FILE_VIEW_INFO = 1;
-var FILE_VIEW = 2;
-var FILE_ADD_TO_BATCH_DL = 3;
-var FILE_DOWNLOAD_SINGLE = 4;
-var HELP = 5;
-var QUIT = 6;
-var FILE_MOVE = 7;   // Sysop action
-var FILE_DELETE = 8; // Sysop action
-
-// Search/list modes
-var MODE_LIST_DIR = 1;
-var MODE_SEARCH_FILENAME = 2;
-var MODE_SEARCH_DESCRIPTION = 3;
-var MODE_NEW_FILE_SEARCH = 4;
-
-// Sort orders (not included in FileBase.SORT)
-var SORT_FL_ULTIME = 50; // Sort by upload time
-var SORT_FL_DLTIME = 51; // Sort by download time
-
-// The searc/list mode for the current run
-var gScriptMode = MODE_LIST_DIR; // Default
-var gListBehavior = FL_NONE; // From sbbsdefs.js
-
-// The directory internal code to list
-var gDirCode = bbs.curdir_code;
-
-
-
-// This will store the number of header lines that were displayed.  This will control
-// the starting row of the file list menu.
-var gNumHeaderLinesDisplayed = 0;
-
-// The number of milliseconds to wait after displaying an error message
-var gErrorMsgWaitMS = 1500;
-// The upper-left position, width, & size of the error message box
-var gErrorMsgBoxULX = 2;
-var gErrorMsgBoxULY = 4;
-var gErrorMsgBoxWidth = console.screen_columns - 2;
-var gErrorMsgBoxHeight = 3;
-
-// Whether or not to pause after viewing a file
-var gPauseAfterViewingFile = true;
-
-///////////////////////////////////////////////////////////////////////////////
-// Script execution code
-
-// The filename pattern to match
-var gFilespec = "*";
-
-// The sort order to use for the file list
-var gFileSortOrder = FileBase.SORT.NATURAL; // Natural sort order, same as DATE_A (import date ascending)
-
-var gSearchVerbose = false;
-
-// When called as a lodable module, one of the options is to scan all dirs
-var gScanAllDirs = false;
-
-// Read the configuration file and set the settings
-readConfigFile();
-
-// Parse command-line arguments (which sets program options)
-parseArgs(argv);
-
-// If the user's terminal doesn't support ANSI, then just call the standard Synchronet
-// file list function and exit now
-if (!console.term_supports(USER_ANSI))
-{
-	var exitCode = 0;
-	if (gScriptMode == MODE_SEARCH_FILENAME || gScriptMode == MODE_SEARCH_DESCRIPTION || gScriptMode == MODE_NEW_FILE_SEARCH)
-		bbs.scan_dirs(gListBehavior, gScanAllDirs);
-	else
-		exitCode = bbs.list_files(gDirCode, gFilespec, gListBehavior);
-	exit(exitCode);
-}
-
-// This array will contain file metadata objects
-var gFileList = [];
-
-// Populate the file list based on the script mode (list/search).
-// It's important that this is called before createFileListMenu(),
-// since this adjusts gListIdxes.filenameEnd based on the longest
-// filename length and terminal width.
-var listPopRetObj = populateFileList(gScriptMode);
-if (listPopRetObj.exitNow)
-	exit(listPopRetObj.exitCode);
-
-// If there are no files, then say so and exit.
-if (gFileList.length == 0)
-{
-	console.crlf();
-	console.print("\x01n\x01c");
-	if (gScriptMode == MODE_LIST_DIR)
-	{
-		if (gFilespec == "*" || gFilespec == "*.*")
-			console.print("There are no files in the directory.");
-		else
-			console.print("No files in the directory were found matching " + gFilespec);
-	}
-	else
-		console.print("No files were found.");
-	console.print("\x01n");
-	console.crlf();
-	console.pause();
-	exit(0);
-}
-
-
-// Clear the screen and display the header lines
-console.clear("\x01n");
-if ((gListBehavior & FL_NO_HDR) != FL_NO_HDR)
-	displayFileLibAndDirHeader();
-// Construct and display the menu/command bar at the bottom of the screen
-var fileMenuBar = new DDFileMenuBar({ x: 1, y: console.screen_rows });
-fileMenuBar.writePromptLine();
-// Create the file list menu
-var gFileListMenu = createFileListMenu(fileMenuBar.getAllActionKeysStr(true, true) + KEY_LEFT + KEY_RIGHT + KEY_DEL);
-// In a loop, show the file list menu, allowing the user to scroll the file list,
-// and respond to user input until the user decides to quit.
-gFileListMenu.Draw({});
-// If using extended descriptions, write the first file's description on the screen
-if (extendedDescEnabled())
-	displayFileExtDescOnMainScreen(0);
-var continueDoingFileList = true;
-var drawFileListMenu = false; // For screen refresh optimization
-while (continueDoingFileList)
-{
-	// Clear the menu's selected item indexes so it's 'fresh' for this round
-	for (var prop in gFileListMenu.selectedItemIndexes)
-		delete gFileListMenu.selectedItemIndexes[prop];
-	var actionRetObj = null;
-	var currentActionVal = null;
-	var userChoice = gFileListMenu.GetVal(drawFileListMenu, gFileListMenu.selectedItemIndexes);
-	drawFileListMenu = false; // For screen refresh optimization
-	var lastUserInputUpper = gFileListMenu.lastUserInput != null ? gFileListMenu.lastUserInput.toUpperCase() : null;
-	if (lastUserInputUpper == null || lastUserInputUpper == "Q")
-		continueDoingFileList = false;
-	else if (lastUserInputUpper == KEY_LEFT)
-		fileMenuBar.decrementMenuItemAndRefresh();
-	else if (lastUserInputUpper == KEY_RIGHT)
-		fileMenuBar.incrementMenuItemAndRefresh();
-	else if (lastUserInputUpper == KEY_ENTER)
-	{
-		currentActionVal = fileMenuBar.getCurrentSelectedAction();
-		fileMenuBar.setCurrentActionCode(currentActionVal);
-		actionRetObj = doAction_ANSI(currentActionVal, gFileList, gFileListMenu);
-	}
-	// Allow the delete key as a special key for sysops to delete the selected file(s). Also allow backspace
-	// due to some terminals returning backspace for delete.
-	else if (lastUserInputUpper == KEY_DEL || lastUserInputUpper == KEY_BACKSPACE)
-	{
-		if (user.is_sysop)
-		{
-			fileMenuBar.setCurrentActionCode(FILE_DELETE, true);
-			actionRetObj = doAction_ANSI(FILE_DELETE, gFileList, gFileListMenu);
-			currentActionVal = FILE_DELETE;
-		}
-	}
-	else
-	{
-		currentActionVal = fileMenuBar.getActionFromChar(lastUserInputUpper, false);
-		fileMenuBar.setCurrentActionCode(currentActionVal, true);
-		actionRetObj = doAction_ANSI(currentActionVal, gFileList, gFileListMenu);
-	}
-	// If an action was done (actionRetObj is not null), then look at actionRetObj and
-	// do what's needed.  Note that quit (for the Q key) is already handled.
-	if (actionRetObj != null)
-	{
-		if (actionRetObj.exitNow)
-			continueDoingFileList = false;
-		else
-		{
-			if ((gListBehavior & FL_NO_HDR) != FL_NO_HDR)
-			{
-				if (actionRetObj.reDrawHeaderTextOnly)
-				{
-					console.print("\x01n");
-					displayFileLibAndDirHeader(true); // Will move the cursor where it needs to be
-				}
-				else if (actionRetObj.reDrawListerHeader)
-				{
-					console.print("\x01n");
-					console.gotoxy(1, 1);
-					displayFileLibAndDirHeader();
-				}
-			}
-			if (actionRetObj.reDrawCmdBar) // Could call fileMenuBar.constructPromptText(); if needed
-				fileMenuBar.writePromptLine();
-			var redrewPartOfFileListMenu = false;
-			// If we are to re-draw the main screen content, then
-			// enable the flag to draw the file list menu on the next
-			// GetVal(); also, if extended descriptions are being shown,
-			// write the current file's extended description too.
-			if (actionRetObj.reDrawMainScreenContent)
-			{
-				drawFileListMenu = true;
-				if (extendedDescEnabled())
-					displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx);
-			}
-			else
-			{
-				// If there is partial redraw information available, then use it
-				// to re-draw that part of the main screen
-				if (actionRetObj.fileListPartialRedrawInfo != null)
-				{
-					drawFileListMenu = false;
-					var startX = actionRetObj.fileListPartialRedrawInfo.absStartX;
-					var startY = actionRetObj.fileListPartialRedrawInfo.absStartY;
-					var width = actionRetObj.fileListPartialRedrawInfo.width;
-					var height = actionRetObj.fileListPartialRedrawInfo.height;
-					refreshScreenMainContent(startX, startY, width, height, true);
-					actionRetObj.refreshedSelectedFilesAlready = true;
-					redrewPartOfFileListMenu = true;
-				}
-				else
-				{
-					// Partial screen re-draw information was not returned.
-					continueDoingFileList = actionRetObj.continueFileLister;
-					drawFileListMenu = actionRetObj.reDrawMainScreenContent;
-					// If displaying extended descriptions and the user deleted some files, then
-					// refresh the file description area to erase the delete confirmation text
-					if (extendedDescEnabled()/* && currentActionVal == FILE_DELETE*/)
-					{
-						if (actionRetObj.hasOwnProperty("filesDeleted") && actionRetObj.filesDeleted)
-						{
-							var numFiles = gFileListMenu.NumItems();
-							if (numFiles > 0 && gFileListMenu.selectedItemIdx >= 0 && gFileListMenu.selectedItemIdx < numFiles)
-								displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx);
-						}
-						else
-						{
-							var firstLine = startY + gFileListMenu.pos.y;
-							var lastLine = console.screen_rows - 1;
-							var width = console.screen_columns - gFileListMenu.size.width - 1;
-							displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx, firstLine, lastLine, width);
-						}
-					}
-				}
-			}
-			// Remove checkmarks from any selected files in the file menu.
-			// For efficiency, we'd probably only do this if not re-drawing the wohle
-			// menu, but that's not working for now.
-			if (!actionRetObj.refreshedSelectedFilesAlready && /*!drawFileListMenu &&*/ gFileListMenu.numSelectedItemIndexes() > 0)
-			{
-				var bottomItemIdx = gFileListMenu.GetBottomItemIdx();
-				var redrawTopY = -1;
-				var redrawBottomY = -1;
-				if (actionRetObj.fileListPartialRedrawInfo != null)
-				{
-					redrawTopY = actionRetObj.fileListPartialRedrawInfo.absStartY;
-					redrawBottomY = actionRetObj.fileListPartialRedrawInfo.height + height - 1;
-				}
-				for (var idx in gFileListMenu.selectedItemIndexes)
-				{
-					var idxNum = +idx;
-					if (idxNum >= gFileListMenu.topItemIdx && idxNum <= bottomItemIdx)
-					{
-						var drawItem = true;
-						if (redrawTopY > -1 && redrawBottomY > redrawTopY)
-						{
-							var screenRowForItem = gFileListMenu.ScreenRowForItem(idxNum);
-							drawItem = (screenRowForItem < redrawTopY || screenRowForItem > redrawBottomY)
-						}
-						if (drawItem)
-						{
-							var isSelected = (idxNum == gFileListMenu.selectedItemIdx);
-							gFileListMenu.WriteItemAtItsLocation(idxNum, isSelected, false);
-						}
-						else
-							console.print("\x01n\r\nNot drawing idx " + idxNum + "\r\n\x01p");
-					}
-				}
-			}
-			// If part of the file list menu was re-drawn (partially, not completely), move the cursor
-			// to the lower-right corner of the screen so that it's out of the way
-			if (redrewPartOfFileListMenu)
-				console.gotoxy(console.screen_columns-1, console.screen_rows);
-		}
-	}
-}
-
-
-
-
-///////////////////////////////////////////////////////////////////////////////
-// Functions: File actions
-
-// Performs a specified file action based on an action code. For the ANSI user interface.
-//
-// Parameters:
-//  pActionCode: A code specifying an action to do.  Must be one of the global
-//               action codes.
-//  pFileList: The list of file metadata objects, as retrieved from the filebase
-//  pFileListMenu: The file list menu
-//
-// Return value: An object with values to indicate status & screen refresh actions; see
-//               getDefaultActionRetObj() for details.
-function doAction_ANSI(pActionCode, pFileList, pFileListMenu)
-{
-	if (typeof(pActionCode) !== "number")
-		return getDefaultActionRetObj();
-
-	var fileMetadata = pFileList[pFileListMenu.selectedItemIdx];
-
-	var retObj = null;
-	switch (pActionCode)
-	{
-		case FILE_VIEW_INFO:
-			retObj = showFileInfo_ANSI(fileMetadata);
-			break;
-		case FILE_VIEW:
-			retObj = viewFile_ANSI(fileMetadata);
-			break;
-		case FILE_ADD_TO_BATCH_DL:
-			if (userCanDownloadFromFileArea_ShowErrorIfNot(fileMetadata.dirCode))
-				retObj = addSelectedFilesToBatchDLQueue_ANSI(fileMetadata, pFileList);
-			else
-			{
-				retObj = getDefaultActionRetObj();
-				retObj.reDrawListerHeader = true;
-				retObj.reDrawHeaderTextOnly = false;
-				retObj.reDrawMainScreenContent = true;
-				retObj.reDrawCmdBar = true;
-			}
-			break;
-		case FILE_DOWNLOAD_SINGLE:
-			if (userCanDownloadFromFileArea_ShowErrorIfNot(fileMetadata.dirCode) && pFileListMenu.selectedItemIdx >= 0 && pFileListMenu.selectedItemIdx < pFileListMenu.NumItems())
-				retObj = letUserDownloadSelectedFile_ANSI(fileMetadata);
-			else
-			{
-				retObj = getDefaultActionRetObj();
-				retObj = getDefaultActionRetObj();
-				retObj.reDrawListerHeader = true;
-				retObj.reDrawHeaderTextOnly = false;
-				retObj.reDrawMainScreenContent = true;
-				retObj.reDrawCmdBar = true;
-			}
-			break;
-		case HELP:
-			retObj = displayHelpScreen();
-			break;
-		case QUIT:
-			retObj = getDefaultActionRetObj();
-			retObj.continueFileLister = false;
-			break;
-		case FILE_MOVE: // Sysop action
-			if (user.is_sysop)
-				retObj = chooseFilebaseAndMoveFileToOtherFilebase_ANSI(pFileList, pFileListMenu);
-			break;
-		case FILE_DELETE: // Sysop action
-			if (user.is_sysop)
-				retObj = confirmAndRemoveFilesFromFilebase_ANSI(pFileList, pFileListMenu);
-			break;
-	}
-
-	return retObj;
-}
-
-// Returns an object for use for returning from performing a file action,
-// with default values.
-//
-// Return value: An object with the following properties:
-//               continueFileLister: Boolean - Whether or not the file lister should continue, or exit
-//               reDrawMainScreenContent: Boolean - Whether or not to re-draw the main screen content
-//                                        (file list, and extended description area if applicable)
-//               reDrawListerHeader: Boolean - Whether or not to re-draw the header at the top of the screen
-//               reDrawHeaderTextOnly: Boolean - Whether or not to re-draw the header text only.  This should
-//                                     take precedence over reDrawListerHeader.
-//               reDrawCmdBar: Boolean - Whether or not to re-draw the command bar at the bottom of the screen
-//               fileListPartialRedrawInfo: If part of the file list menu needs to be re-drawn,
-//                                          this will be an object that includes the following properties:
-//                                          startX: The starting X coordinate for where to re-draw
-//                                          startY: The starting Y coordinate for where to re-draw
-//                                          width: The width to re-draw
-//                                          height: The height to re-draw
-//               refreshedSelectedFilesAlready: Whether or not selected file checkmark items
-//                                              have already been refreshed (boolean)
-//               exitNow: Exit the file lister now (boolean)
-//               If no part of the file list menu needs to be re-drawn, this will be null.
-function getDefaultActionRetObj()
-{
-	return {
-		continueFileLister: true,
-		reDrawMainScreenContent: false,
-		reDrawListerHeader: false,
-		reDrawHeaderTextOnly: false,
-		reDrawCmdBar: false,
-		fileListPartialRedrawInfo: null,
-		refreshedSelectedFilesAlready: false,
-		exitNow: false
-	};
-}
-
-// Shows extended information about a file to the user.
-//
-// Parameters:
-//  pFileMetadata: The file metadata object for the file to view information about
-//
-// Return value: An object with values to indicate status & screen refresh actions; see
-//               getDefaultActionRetObj() for details.
-function showFileInfo_ANSI(pFileMetadata)
-{
-	var retObj = getDefaultActionRetObj();
-
-	// The width of the frame to display the file info (including borders).  This
-	// is declared early so that it can be used for string length adjustment.
-	//var frameWidth = pFileListMenu.size.width - 4; // TODO: Remove?
-	var frameWidth = console.screen_columns - 4;
-
-	// pFileList[pFileListMenu.selectedItemIdx] has a file metadata object without
-	// extended information.  Get a metadata object with extended information so we
-	// can display the extended description.
-	// The metadata object in pFileList should have a dirCode added by this script.
-	var dirCode = gDirCode;
-	if (pFileMetadata.hasOwnProperty("dirCode"))
-		dirCode = pFileMetadata.dirCode;
-	var fileMetadata = null;
-	if (extendedDescEnabled())
-		fileMetadata = pFileMetadata;
-	else
-		fileMetadata = getFileInfoFromFilebase(dirCode, pFileMetadata.name, FileBase.DETAIL.EXTENDED);
-	// Build a string with the file information
-	// Make sure the displayed filename isn't too crazy long
-	var frameInnerWidth = frameWidth - 2; // Without borders
-	var adjustedFilename = shortenFilename(fileMetadata.name, frameInnerWidth, false);
-	var fileInfoStr = "\x01n\x01wFilename";
-	if (adjustedFilename.length < fileMetadata.name.length)
-		fileInfoStr += " (shortened)";
-	fileInfoStr += ":\r\n";
-	fileInfoStr += gColors.filename + adjustedFilename +  "\x01n\x01w\r\n";
-	// Note: File size can also be retrieved by calling a FileBase's get_size(fileMetadata.name)
-	// TODO: Shouldn't need the max length here
-	fileInfoStr += "Size: " + gColors.fileSize + getFileSizeStr(fileMetadata.size, 99999) + "\x01n\x01w\r\n";
-	fileInfoStr += "Timestamp: " + gColors.fileTimestamp + strftime("%Y-%m-%d %H:%M:%S", fileMetadata.time) + "\x01n\x01w\r\n";
-	fileInfoStr += "\r\n";
-
-	// File library/directory information
-	var libIdx = file_area.dir[dirCode].lib_index;
-	var dirIdx = file_area.dir[dirCode].index;
-	var libDesc = file_area.lib_list[libIdx].description;
-	var dirDesc =  file_area.dir[dirCode].description;
-	fileInfoStr += "\x01c\x01hLib\x01g: \x01n\x01c" + libDesc.substr(0, frameInnerWidth-5) + "\x01n\x01w\r\n";
-	fileInfoStr += "\x01c\x01hDir\x01g: \x01n\x01c" + dirDesc.substr(0, frameInnerWidth-5) + "\x01n\x01w\r\n";
-	fileInfoStr += "\r\n";
-
-	// fileMetadata should have extdDesc, but check just in case
-	var fileDesc = "";
-	if (fileMetadata.hasOwnProperty("extdesc") && fileMetadata.extdesc.length > 0)
-		fileDesc = fileMetadata.extdesc;
-	else
-		fileDesc = fileMetadata.desc;
-	// It's possible for fileDesc to be undefined (due to extDesc or desc being undefined),
-	// so make sure it's a string.
-	// Also, if it's a string, reformat certain types of strings that don't look good in a
-	// Frame object
-	if (typeof(fileDesc) === "string")
-	{
-		// Check to see if it starts with a normal attribute and remove if so,
-		// since that seems to cause problems with displaying the description in a Frame object.  This
-		// may be a kludge, and perhaps there's a better solution..
-		fileDesc = fileDesc.replace(/^\x01[nN]/, "");
-		// Fix line endings if necessary
-		fileDesc = lfexpand(fileDesc);
-	}
-	else
-		fileDesc = "";
-	// This might be overkill, but just in case, convert any non-Synchronet
-	// attribute codes to Synchronet attribute codes in the description.
-	if (!fileMetadata.hasOwnProperty("attrsConverted"))
-	{
-		fileDesc = convertAttrsToSyncPerSysCfg(fileDesc);
-		fileMetadata.attrsConverted = true;
-		if (fileMetadata.hasOwnProperty("extdesc"))
-			fileMetadata.extdesc = fileDesc;
-		else
-			fileMetadata.desc = fileDesc;
-	}
-
-	fileInfoStr += gColors.desc;
-	if (fileDesc.length > 0)
-		fileInfoStr += "Description:\r\n" + fileDesc; // Don't want to use strip_ctrl(fileDesc)
-	else
-		fileInfoStr += "No description available";
-	fileInfoStr += "\r\n";
-	// # of times downloaded and last downloaded date/time
-	var fieldFormatStr = "\r\n\x01n\x01c\x01h%s\x01g:\x01n\x01c %s";
-	var timesDownloaded = fileMetadata.hasOwnProperty("times_downloaded") ? fileMetadata.times_downloaded : 0;
-	fileInfoStr += format(fieldFormatStr, "Times downloaded", timesDownloaded);
-	if (fileMetadata.hasOwnProperty("last_downloaded"))
-		fileInfoStr += format(fieldFormatStr, "Last downloaded", strftime("%Y-%m-%d %H:%M", fileMetadata.last_downloaded));
-	// Some more fields for the sysop
-	if (user.is_sysop)
-	{
-		var sysopFields = [ "from", "cost", "added"];
-		for (var sI = 0; sI < sysopFields.length; ++sI)
-		{
-			var prop = sysopFields[sI];
-			if (fileMetadata.hasOwnProperty(prop))
-			{
-				if (typeof(fileMetadata[prop]) === "string" && fileMetadata[prop].length == 0)
-					continue;
-				var propName = prop.charAt(0).toUpperCase() + prop.substr(1);
-				var infoValue = "";
-				if (prop == "added")
-					infoValue = strftime("%Y-%m-%d %H:%M:%S", fileMetadata.added);
-				else
-					infoValue = fileMetadata[prop].toString().substr(0, frameInnerWidth);
-				fileInfoStr += format(fieldFormatStr, propName, infoValue);
-				fileInfoStr += "\x01n\x01w";
-			}
-		}
-	}
-	fileInfoStr += "\x01n\x01w";
-
-	// Construct & draw a frame with the file information & do the input loop
-	// for the frame until the user closes the frame.
-	var frameUpperLeftX = 3;
-	var frameUpperLeftY = gNumHeaderLinesDisplayed + 3;
-	// Note: frameWidth is declared earlier
-	var frameHeight = console.screen_rows - 4 - frameUpperLeftY;
-	// If the user's console is more than 25 rows high, then make the info window
-	// taller so that its bottom row is 10 from the bottom, but only up to 45 rows tall.
-	if (console.screen_rows > 25)
-	{
-		var frameBottomRow = console.screen_rows - 4;
-		frameHeight = frameBottomRow - frameUpperLeftY + 1;
-		if (frameHeight > 45)
-			frameHeight = 45;
-	}
-	var frameTitle = "File Info";
-	displayBorderedFrameAndDoInputLoop(frameUpperLeftX, frameUpperLeftY, frameWidth, frameHeight,
-	                                   gColors.fileInfoWindowBorder, frameTitle,
-	                                   gColors.fileInfoWindowTitle, fileInfoStr);
-
-	// Construct the file list redraw info.  Note that the X and Y are relative
-	// to the file list menu, not absolute screen coordinates.
-	retObj.fileListPartialRedrawInfo = {
-		startX: frameUpperLeftX - gFileListMenu.pos.x + 1, // Relative to the file menu
-		startY: frameUpperLeftY - gFileListMenu.pos.y + 1, // Relative to the file menu
-		absStartX: frameUpperLeftX,
-		absStartY: frameUpperLeftY,
-		width: frameWidth,
-		height: frameHeight
-	};
-
-	return retObj;
-}
-// Splits a string on a given string and then re-combines the string with \r\n (carriage return & newline)
-// at the end of each line
-//
-// Parameters:
-//  pStr: The string to split & recombine
-//  pSplitStr: The string to split the first string on
-//
-// Return value: The string split on pSplitStr and re-combined with \r\n at the end of each line
-function splitStrAndCombineWithRN(pStr, pSplitStr)
-{
-	if (typeof(pStr) !== "string")
-		return "";
-	if (typeof(pSplitStr) !== "string")
-		return pStr;
-
-	var newStr = "";
-	var strs = pStr.split(pSplitStr);
-	if (strs.length > 0)
-	{
-		for (var i = 0; i < strs.length; ++i)
-			newStr += strs[i] + "\r\n";
-		newStr = newStr.replace(/\r\n$/, "");
-	}
-	else
-		newStr = pStr;
-	return newStr;
-}
-
-// Lets the user view a file.
-//
-// Parameters:
-//  pFileMetadata: The file metadata object for the file to view
-//
-// Return value: An object with values to indicate status & screen refresh actions; see
-//               getDefaultActionRetObj() for details.
-function viewFile_ANSI(pFileMetadata)
-{
-	var retObj = getDefaultActionRetObj();
-
-	// Open the filebase & get the fully pathed filename
-	var fullyPathedFilename = "";
-	var filebase = new FileBase(pFileMetadata.dirCode);
-	if (filebase.open())
-	{
-		fullyPathedFilename = filebase.get_path(pFileMetadata);
-		filebase.close();
-	}
-	else
-	{
-		displayMsg("Failed to open the filebase!", true, true);
-		return retObj;
-	}
-
-	// View the file
-	console.gotoxy(1, console.screen_rows);
-	console.print("\x01n");
-	console.crlf();
-	var successfullyViewed = bbs.view_file(fullyPathedFilename);
-	console.print("\x01n");
-	if (gPauseAfterViewingFile || !successfullyViewed)
-		console.pause();
-
-	retObj.reDrawListerHeader = true;
-	retObj.reDrawMainScreenContent = true;
-	retObj.reDrawCmdBar = true;
-	return retObj;
-}
-
-// Allows the user to add their selected file to their batch downloaded queue
-//
-// Parameters:
-//  pFileMetadata: The file metadata object for the file
-//  pFileList: The list of file metadata objects from the file directory
-//
-// Return value: An object with values to indicate status & screen refresh actions; see
-//               getDefaultActionRetObj() for details.
-function addSelectedFilesToBatchDLQueue_ANSI(pFileMetadata, pFileList)
-{
-	var retObj = getDefaultActionRetObj();
-	if (!userCanDownloadFromFileArea_ShowErrorIfNot(pFileMetadata.dirCode))
-		return retObj;
-
-	// Confirm with the user to add the file(s) to their batch queue.  If they don't want to,
-	// then just return now.
-	var filenames = [];
-	var metadataObjects = [];
-	if (gFileListMenu.numSelectedItemIndexes() > 0)
-	{
-		for (var idx in gFileListMenu.selectedItemIndexes)
-		{
-			var idxNum = +idx;
-			filenames.push(pFileList[idxNum].name);
-			metadataObjects.push(pFileList[idxNum]);
-		}
-	}
-	else
-	{
-		filenames.push(pFileMetadata.name);
-		metadataObjects.push(pFileMetadata);
-	}
-	// Note that confirmFileActionWithUser() will re-draw the parts of the file
-	// list menu that are necessary.
-	var addFilesConfirmed = confirmFileActionWithUser(filenames, "Batch DL add", false);
-	retObj.refreshedSelectedFilesAlready = true;
-	if (addFilesConfirmed)
-	{
-		var batchDLQueueStats = getUserDLQueueStats();
-		var filenamesFailed = []; // To store filenames that failed to get added to the queue
-		var batchDLFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".dnload";
-		var batchDLFile = new File(batchDLFilename);
-		if (batchDLFile.open(batchDLFile.exists ? "r+" : "w+"))
-		{
-			displayMsg("Adding file(s) to batch DL queue..", false, false);
-			for (var i = 0; i < metadataObjects.length; ++i)
-			{
-				// If the file isn't in the user's batch DL queue already, then add it.
-				var fileAlreadyInQueue = false;
-				for (var fIdx = 0; fIdx < batchDLQueueStats.filenames.length && !fileAlreadyInQueue; ++fIdx)
-					fileAlreadyInQueue = (batchDLQueueStats.filenames[fIdx].filename == metadataObjects[i].name);
-				if (!fileAlreadyInQueue)
-				{
-					var addToQueueSuccessful = true;
-					batchDLFile.writeln("");
-
-					// Add the required "dir" and "desc" properties to the user's batch download
-					// queue file.  The section is the filename.  Also, this script should add a
-					// dirCode property to each metadata object in the list.
-					addToQueueSuccessful = batchDLFile.iniSetValue(metadataObjects[i].name, "dir", metadataObjects[i].dirCode);
-					if (addToQueueSuccessful)
-					{
-						addToQueueSuccessful = batchDLFile.iniSetValue(metadataObjects[i].name, "desc", metadataObjects[i].desc);
-						// Update the batch DL queue stats object
-						++(batchDLQueueStats.numFilesInQueue);
-						batchDLQueueStats.filenames.push({ filename: metadataObjects[i].name, desc: metadataObjects[i].desc });
-						batchDLQueueStats.totalSize += +(metadataObjects[i].size);
-						batchDLQueueStats.totalCost += +(metadataObjects[i].cost);
-					}
-
-					if (!addToQueueSuccessful)
-						filenamesFailed.push(metadataObjects[i].name);
-				}
-			}
-
-			batchDLFile.close();
-		}
-
-		// Frame location & size for batch DL queue stats or filenames that failed
-		var frameUpperLeftX = gFileListMenu.pos.x + 2;
-		var frameUpperLeftY = gFileListMenu.pos.y + 2;
-		var frameWidth = console.screen_columns - 4; // Used to be gFileListMenu.size.width - 4;
-		var frameInnerWidth = frameWidth - 2; // Without borders
-		var frameHeight = 8;
-
-		// If there were no failures, then show a success message & prompt the user if they
-		// want to download their batch queue.  Otherwise, show the filenames that failed to
-		// get added.
-		if (filenamesFailed.length == 0)
-		{
-			displayMsg("Your batch DL queue was sucessfully updated", false, true);
-			// Prompt if the user wants to download their batch queue
-			if (bbs.batch_dnload_total > 0)
-			{
-				// Clear most of the screen area so the user has focus on the batch DL queue stats
-				var fullLineFormatStr = "%" + console.screen_columns + "s";
-				var leftFormatStr = "%" + frameUpperLeftX + "s";
-				var rightFormatStr = "%" + +(frameUpperLeftX+frameWidth-1) + "s";
-				var lastFrameRow = frameUpperLeftY + frameHeight - 1;
-				var lastRow = console.screen_rows - 1;
-				console.print("\x01n");
-				for (var screenRow = gNumHeaderLinesDisplayed+1; screenRow <= lastRow; ++screenRow)
-				{
-					console.gotoxy(1, screenRow);
-					if (screenRow < frameUpperLeftY || screenRow > lastFrameRow)
-						printf(fullLineFormatStr, "");
-					else
-					{
-						printf(leftFormatStr, "");
-						console.gotoxy(frameUpperLeftX+frameWidth, screenRow);
-						printf(rightFormatStr, "");
-					}
-				}
-
-				// Build a frame with batch DL queue stats and prompt the user if they want to
-				// download their batch DL queue
-				var frameTitle = "Download your batch queue (Y/N)?";
-				// \x01cFiles: \x01h1 \x01n\x01c(\x01h100 \x01n\x01cMax)  Credits: 0  Bytes: \x01h2,228,254 \x01n\x01c Time: 00:09:40
-				// Note: The maximum number of allowed files in the batch download queue doesn't seem to
-				// be available to JavaScript.
-				var totalQueueSize = batchDLQueueStats.totalSize + pFileMetadata.size;
-				var totalQueueCost = batchDLQueueStats.totalCost + pFileMetadata.cost;
-				var queueStats = "\x01n\x01cFiles: \x01h" + batchDLQueueStats.numFilesInQueue + "  \x01n\x01cCredits: \x01h"
-				               + totalQueueCost + "\x01n\x01c  Bytes: \x01h" + numWithCommas(totalQueueSize) + "\x01n\x01w\r\n";
-				for (var i = 0; i < batchDLQueueStats.filenames.length; ++i)
-				{
-					queueStats += shortenFilename(batchDLQueueStats.filenames[i].filename, frameInnerWidth, false) + "\r\n";
-					queueStats += batchDLQueueStats.filenames[i].desc.substr(0, frameInnerWidth) + "\r\n";
-					if (i < batchDLQueueStats.filenames.length-1)
-						queueStats += "\r\n";
-				}
-				var additionalQuitKeys = "yYnN";
-				var lastUserInput = displayBorderedFrameAndDoInputLoop(frameUpperLeftX, frameUpperLeftY, frameWidth,
-				                                                       frameHeight, gColors.batchDLInfoWindowBorder,
-				                                                       frameTitle, gColors.batchDLInfoWindowTitle,
-				                                                       queueStats, additionalQuitKeys);
-				// The main screen content (file list & extended description if applicable)
-				// will need to be redrawn after this.
-				retObj.reDrawMainScreenContent = true;
-				// If the user chose to download their file queue, then send it to the user.
-				// And the lister headers will need to be re-drawn as well.
-				if (lastUserInput.toUpperCase() == "Y")
-				{
-					retObj.reDrawListerHeader = true;
-					retObj.reDrawCmdBar = true;
-					console.print("\x01n");
-					console.gotoxy(1, console.screen_rows);
-					console.crlf();
-					bbs.batch_download();
-					// If the user is still online (chose not to hang up after transfer),
-					// then pause so that the user can see the batch download status
-					if (bbs.online > 0)
-						console.pause();
-				}
-			}
-		}
-		else
-		{
-			eraseMsgBoxScreenArea();
-			// Build a frame object to show the names of the files that failed to be added to the
-			// user's batch DL queue
-			var frameTitle = "Failed to add these files to batch DL queue";
-			var fileListStr = "\x01n\x01w";
-			for (var i = 0; i < filenamesFailed.length; ++i)
-				fileListStr += shortenFilename(filenamesFailed[i], frameInnerWidth, false) + "\r\n";
-			var lastUserInput = displayBorderedFrameAndDoInputLoop(frameUpperLeftX, frameUpperLeftY, frameWidth,
-																   frameHeight, gColors.batchDLInfoWindowBorder,
-																   frameTitle, gColors.batchDLInfoWindowTitle,
-																   fileListStr, "");
-			// Add the file list redraw info.  Note that the X and Y are relative
-			// to the file list menu, not absolute screen coordinates.
-			// To make the list refresh info to return to the main script loop
-			retObj.fileListPartialRedrawInfo = {
-				startX: 3,
-				startY: 3,
-				absStartX: gFileListMenu.pos.x + 3 - 1, // 1-based
-				absStartY: gFileListMenu.pos.y + 3 - 1, // 1-based
-				width: frameWidth + 1,
-				height: frameHeight
-			};
-		}
-	}
-
-	return retObj;
-}
-// Gets stats about the user's batch download queue.
-//
-// Return value: An object containing the following properties:
-//               numFilesInQueue: The number of files already in the queue
-//               totalSize: The total size of the files in the queue
-//               totalCost: The total cost of the files in the queue
-//               filenames: An array of objects, each containing the filename and
-//                          descriptions (desc) of the files in the download queue
-function getUserDLQueueStats()
-{
-	var retObj = {
-		numFilesInQueue: 0,
-		totalSize: 0,
-		totalCost: 0,
-		filenames: []
-	};
-
-	var batchDLFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".dnload";
-	var batchDLFile = new File(batchDLFilename);
-	if (batchDLFile.open(batchDLFile.exists ? "r+" : "w+"))
-	{
-		// See if a section exists for the filename
-		//File.iniGetAllObjects([name_property] [,prefix=none] [,lowercase=false] [,blanks=false])
-		var allIniObjs = batchDLFile.iniGetAllObjects();
-		console.attributes = "N";
-		console.crlf();
-		for (var i = 0; i < allIniObjs.length; ++i)
-		{
-			if (typeof(allIniObjs[i]) === "object")
-			{
-				++(retObj.numFilesInQueue);
-				//allIniObjs[i].name
-				//allIniObjs[i].dir
-				//allIniObjs[i].desc
-				retObj.filenames.push({ filename: allIniObjs[i].name, desc: allIniObjs[i].desc });
-				// dir is the internal directory code
-				if (allIniObjs[i].dir.length > 0)
-				{
-					var filebase = new FileBase(allIniObjs[i].dir);
-					if (filebase.open())
-					{
-						var fileInfo = filebase.get(allIniObjs[i].name);
-						if (typeof(fileInfo) === "object")
-						{
-							retObj.totalSize += +(fileInfo.size);
-							retObj.totalCost += +(fileInfo.cost);
-						}
-						filebase.close();
-					}
-				}
-			}
-		}
-		batchDLFile.close();
-	}
-
-	return retObj;
-}
-
-// Lets the user download the currently selected file on the file list menu
-//
-// Parameters:
-//  pFileMetadata: The file metadata object for the file to download
-//
-// Return value: An object with values to indicate status & screen refresh actions; see
-//               getDefaultActionRetObj() for details.
-function letUserDownloadSelectedFile_ANSI(pFileMetadata)
-{
-	var retObj = getDefaultActionRetObj();
-	console.attributes = "N";
-	console.crlf();
-	console.crlf();
-	// If the user has the security level to download the file, let them do so
-	if (userCanDownloadFromFileArea_ShowErrorIfNot(pFileMetadata.dirCode))
-	{
-		console.print("\x01cDownloading \x01h" + pFileMetadata.name + "\x01n");
-		console.crlf();
-		var selectedFilanmeFullPath = backslash(file_area.dir[pFileMetadata.dirCode].path) + pFileMetadata.name;
-		bbs.send_file(selectedFilanmeFullPath);
-	}
-
-	retObj.reDrawListerHeader = true;
-	retObj.reDrawHeaderTextOnly = false;
-	retObj.reDrawMainScreenContent = true;
-	retObj.reDrawCmdBar = true;
-
-	return retObj;
-}
-
-// Displays the help screen.
-function displayHelpScreen()
-{
-	var retObj = getDefaultActionRetObj();
-
-	console.clear("\x01n");
-	// Display program information
-	displayTextWithLineBelow("Digital Distortion File Lister", true, "\x01n\x01c\x01h", "\x01k\x01h")
-	console.center("\x01n\x01cVersion \x01g" + LISTER_VERSION + " \x01w\x01h(\x01b" + LISTER_DATE + "\x01w)");
-	console.crlf();
-
-	// If listing files in a directory, display information about the current file directory.
-	if (gScriptMode == MODE_LIST_DIR)
-	{
-		var libIdx = file_area.dir[gDirCode].lib_index;
-		var dirIdx = file_area.dir[gDirCode].index;
-		console.print("\x01n\x01cCurrent file library: \x01g" + file_area.lib_list[libIdx].description);
-		console.crlf();
-		console.print("\x01cCurrent file directory: \x01g" + file_area.dir[gDirCode].description);
-		console.crlf();
-		console.print("\x01cThere are \x01g" + file_area.dir[gDirCode].files + " \x01cfiles in this directory.");
-	}
-	else if (gScriptMode == MODE_SEARCH_FILENAME)
-		console.print("\x01n\x01cCurrently performing a filename search");
-	else if (gScriptMode == MODE_SEARCH_DESCRIPTION)
-		console.print("\x01n\x01cCurrently performing a description search");
-	else if (gScriptMode == MODE_NEW_FILE_SEARCH)
-		console.print("\x01n\x01cCurrently performing a new file search");
-	console.crlf();
-	console.crlf();
-
-	// Display information about the lister
-	var helpStr = "This lists files in your current file directory with a lightbar interface (for an ANSI terminal).  ";
-	helpStr += "The file list can be navigated using the up & down arrow keys, PageUp, PageDown, Home, and End keys.  "
-	helpStr += "The currently highlighted file in the menu is used by default for the various actions.  For batch download "
-	helpStr += "selection, ";
-	if (user.is_sysop)
-		helpStr += "moving, and deleting, ";
-	helpStr += "you can select multiple files by using the spacebar.  ";
-	helpStr += "There is also a command bar accross the bottom of the screen - You can select an action on the ";
-	helpStr += "action bar by using the left & right arrow keys and pressing enter to choose an action.  Alternately, ";
-	helpStr += "you can press the first character of the action word to perform the action.";
-	helpStr += "  Also, the following actions are available:";
-	// Wrap the help string to the user's terminal width, and replace all instances of
-	// newlines with carriage return + newline, then display the help text.
-	helpStr = word_wrap(helpStr, console.screen_columns - 1).replace(/\n/g, "\r\n");
-	console.print(helpStr);
-	// Display the commands available
-	var commandStrWidth = 8;
-	var printfStr = "\x01n\x01c\x01h%-" + commandStrWidth + "s\x01g: \x01n\x01c%s\r\n";
-	printf(printfStr, "I", "Display extended file information");
-	printf(printfStr, "V", "View the file");
-	printf(printfStr, "B", "Flag the selected file(s) for batch download");
-	printf(printfStr, "D", "Download the highlighted (selected) file");
-	if (user.is_sysop)
-	{
-		printf(printfStr, "M", "Move the file(s) to another directory");
-		printf(printfStr, "DEL", "Delete the file(s)");
-	}
-	printf(printfStr, "?", "Show this help screen");
-	printf(printfStr, "Q", "Quit back to the BBS");
-	console.print("\x01n");
-	console.crlf();
-	//console.pause();
-
-	retObj.reDrawListerHeader = true;
-	retObj.reDrawMainScreenContent = true;
-	retObj.reDrawCmdBar = true;
-	return retObj;
-}
-
-// Allows the user to move the selected file to another filebase.  Only for sysops!
-//
-// Parameters:
-//  pFileList: The list of file metadata objects from the file directory
-//  pFileListMenu: The menu object for the file diretory
-//
-// Return value: An object with values to indicate status & screen refresh actions; see
-//               getDefaultActionRetObj() for details.
-function chooseFilebaseAndMoveFileToOtherFilebase_ANSI(pFileList, pFileListMenu)
-{
-	var retObj = getDefaultActionRetObj();
-
-	var fileMetadata = pFileList[pFileListMenu.selectedItemIdx];
-
-	// Confirm with the user to move the file(s).  If they don't want to,
-	// then just return now.
-	var filenames = [];
-	if (pFileListMenu.numSelectedItemIndexes() > 0)
-	{
-		for (var idx in pFileListMenu.selectedItemIndexes)
-			filenames.push(pFileList[+idx].name);
-	}
-	else
-		filenames.push(fileMetadata.name);
-	// Note that confirmFileActionWithUser() will re-draw the parts of the file
-	// list menu that are necessary.
-	var moveFilesConfirmed = confirmFileActionWithUser(filenames, "Move", false);
-	retObj.refreshedSelectedFilesAlready = true;
-	if (!moveFilesConfirmed)
-		return retObj;
-
-
-	// Create a file library menu for the user to choose a file library (and then directory)
-	var fileLibMenu = createFileLibMenu();
-	// For screen refresh purposes, construct the file list redraw info.  Note that the X and Y are relative
-	// to the file list menu, not absolute screen coordinates.
-	var topYForRefresh = fileLibMenu.pos.y - 1; // - 1 because of the label above the menu
-	var fileListPartialRedrawInfo = {
-		startX: fileLibMenu.pos.x - pFileListMenu.pos.x + 1,
-		startY: topYForRefresh - pFileListMenu.pos.y + 1,
-		absStartX: fileLibMenu.pos.x,
-		absStartY: topYForRefresh,
-		width: fileLibMenu.size.width + 1,
-		height: fileLibMenu.size.height + 1 // + 1 because of the label above the menu
-	};
-	console.gotoxy(fileLibMenu.pos.x, fileLibMenu.pos.y-1);
-	printf("\x01n\x01c\x01h|\x01n\x01c%-" + +(fileLibMenu.size.width-1) + "s\x01n", "Choose a destination area");
-	// Prompt the user which directory to move the file to
-	var chosenDirCode = null;
-	var continueOn = true;
-	while (continueOn)
-	{
-		var chosenLibIdx = fileLibMenu.GetVal();
-		if (typeof(chosenLibIdx) === "number")
-		{
-			// The file dir menu will be created at the same position & with the same size
-			// as the file library menu
-			var fileDirMenu = createFileDirMenu(chosenLibIdx);
-			chosenDirCode = fileDirMenu.GetVal();
-			if (typeof(chosenDirCode) === "string")
-			{
-				if (chosenDirCode != fileMetadata.dirCode)
-					continueOn = false;
-				else
-				{
-					chosenDirCode = "";
-					displayMsg("Can't move to the same directory", true);
-				}
-			}
-		}
-		else
-			continueOn = false;
-	}
-	// If the user chose a directory, then move the file there.
-	if (typeof(chosenDirCode) === "string" && chosenDirCode.length > 0)
-	{
-		// For logging
-		var libIdx = file_area.dir[chosenDirCode].lib_index;
-		var dirIdx = file_area.dir[chosenDirCode].index;
-		var libDesc = file_area.lib_list[libIdx].description;
-		var dirDesc =  file_area.dir[chosenDirCode].description;
-		var destLibAndDirDesc = libDesc + " - " + dirDesc;
-
-		// Build an array of file indexes and sort the array
-		var fileIndexes = [];
-		if (pFileListMenu.numSelectedItemIndexes() > 0)
-		{
-			for (var idx in pFileListMenu.selectedItemIndexes)
-				fileIndexes.push(+idx);
-		}
-		else
-			fileIndexes.push(+(pFileListMenu.selectedItemIdx));
-		// Ensure the file indexes are sorted in numerical order
-		fileIndexes.sort(function(a, b) { return a - b});
-
-		// Go through the list of files and move each of them
-		var moveAllSucceeded = true;
-		for (var i = 0; i < fileIndexes.length; ++i)
-		{
-			var fileIdx = fileIndexes[i];
-			// For logging
-			libIdx = file_area.dir[pFileList[fileIdx].dirCode].lib_index;
-			dirIdx = file_area.dir[pFileList[fileIdx].dirCode].index;
-			libDesc = file_area.lib_list[libIdx].description;
-			dirDesc =  file_area.dir[pFileList[fileIdx].dirCode].description;
-			var srcLibAndDirDesc = libDesc + " - " + dirDesc;
-
-			var moveRetObj = moveFileToOtherFilebase(pFileList[fileIdx], chosenDirCode);
-			var logMsg = "";
-			var logLevel = LOG_INFO;
-			if (moveRetObj.moveSucceeded)
-			{
-				logMsg = "Digital Distotion File Lister: Successfully moved " + pFileList[fileIdx].name
-				       + " from " + srcLibAndDirDesc + " to " + destLibAndDirDesc;
-
-				// If we're listing files in the user's current directory, then remove
-				// the file info object from the file list array.  Otherwise, update
-				// the metadata object in the list.
-				if (gScriptMode == MODE_LIST_DIR)
-				{
-					pFileList.splice(fileIdx, 1);
-					// Subtract 1 from the remaining indexes in the fileIndexes array
-					for (var j = i+1; j < fileIndexes.length; ++j)
-						fileIndexes[j] = fileIndexes[j] - 1;
-					// Have the file list menu set up its description width, colors, and format
-					// string again in case it no longer needs to use its scrollbar
-					pFileListMenu.SetItemWidthsColorsAndFormatStr();
-					retObj.reDrawMainScreenContent = true;
-				}
-				else
-				{
-					// Note: getFileInfoFromFilebase() will add dirCode to the metadata object
-					var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM);
-					pFileList[fileIdx] = getFileInfoFromFilebase(chosenDirCode, pFileList[fileIdx].name, fileDetail);
-				}
-			}
-			else
-			{
-				moveAllSucceeded = false;
-				logLevel = LOG_ERR;
-				logMsg = "Digital Distotion File Lister: Failed to move " + pFileList[fileIdx].name
-				       + " from " + srcLibAndDirDesc + " to " + destLibAndDirDesc;
-			}
-			log(logLevel, logMsg);
-			bbs.log_str(logMsg);
-		}
-		// Adjust the selected item index in the file list menu if necesary
-		if (pFileListMenu.NumItems() == 0)
-			pFileListMenu.selectedItemIdx = 0;
-		else if (pFileListMenu.selectedItemIdx >= pFileListMenu.NumItems() - 1)
-			pFileListMenu.selectedItemIdx = pFileListMenu.NumItems() - 1;
-		// If doing a search (not listing files in the user's current directory), then
-		// if all files were in the same directory, then we'll need to update the header
-		// lines at the top of the file list.  If there's only one file in the list,
-		// the header lines will need to display the correct directory.  Otherwise,
-		// set allSameDir to false so the header lines will now say "various".
-		// However, if not all files were in the same directory, check to see if they
-		// are now, and if so, we'll need to re-draw the header lines.
-		if (gScriptMode != MODE_LIST_DIR && typeof(pFileList.allSameDir) == "boolean")
-		{
-			if (pFileList.allSameDir)
-			{
-				if (pFileList.length > 1)
-					pFileList.allSameDir = false;
-				retObj.reDrawHeaderTextOnly = true;
-			}
-			else
-			{
-				pFileList.allSameDir = true; // Until we find it's not true
-				for (var fileListIdx = 1; fileListIdx < pFileList.length && pFileList.allSameDir; ++fileListIdx)
-					pFileList.allSameDir = (pFileList[fileListIdx].dirCode == pFileList[0].dirCode);
-				retObj.reDrawHeaderTextOnly =  pFileList.allSameDir;
-			}
-		}
-		// Display a success/fail message
-		if (moveAllSucceeded)
-		{
-			var msg = "Successfully moved the file(s) to " + destLibAndDirDesc;
-			displayMsg(msg, false, true);
-		}
-		else
-		{
-			displayMsg("Failed to move the file(s)!", true, true);
-		}
-		// After moving the files, if there are no more files (in the directory or otherwise),
-		// say so and exit now.
-		if (gScriptMode == MODE_LIST_DIR && file_area.dir[gDirCode].files == 0)
-		{
-			displayMsg("There are no more files in the directory.", false);
-			retObj.exitNow = true;
-		}
-		else if (pFileList.length == 0)
-		{
-			displayMsg("There are no more files.", false);
-			retObj.exitNow = true;
-		}
-	}
-
-	// If not exiting now, we'll want to re-draw part of the file list to erase the
-	// area chooser menu.
-	if (!retObj.exitNow)
-		retObj.fileListPartialRedrawInfo = fileListPartialRedrawInfo;
-
-	return retObj;
-}
-
-// Allows the user to remove the selected file(s) from the filebase.  Only for sysops!
-//
-// Parameters:
-//  pFileList: The list of file metadata objects from the file directory
-//  pFileListMenu: The menu object for the file diretory
-//
-// Return value: An object with values to indicate status & screen refresh actions; see
-//               getDefaultActionRetObj() for details.  For this function, the object
-//               returned will have the following additional properties:
-//               filesDeleted: Boolean - Whether or not files were actually deleted (after
-//                             confirmation)
-function confirmAndRemoveFilesFromFilebase_ANSI(pFileList, pFileListMenu)
-{
-	var retObj = getDefaultActionRetObj();
-	retObj.filesDeleted = false;
-
-	// Confirm the action with the user.  If the user confirms, then remove the file(s).
-	// If there are multiple selected files, then prompt to remove each of them.
-	// Otherwise, prompt for the one selected file.
-	var filenames = [];
-	if (pFileListMenu.numSelectedItemIndexes() > 0)
-	{
-		for (var idx in pFileListMenu.selectedItemIndexes)
-			filenames.push(pFileList[+idx].name);
-	}
-	else
-		filenames.push(pFileList[pFileListMenu.selectedItemIdx].name);
-	// Note that confirmFileActionWithUser() will re-draw the parts of the file list menu
-	// that are necessary.
-	var removeFilesConfirmed = confirmFileActionWithUser(filenames, "Remove", false);
-	retObj.refreshedSelectedFilesAlready = true;
-	if (removeFilesConfirmed)
-	{
-		retObj.filesDeleted = true; // Assume true even if some deletions may fail
-
-		var fileIndexes = [];
-		if (pFileListMenu.numSelectedItemIndexes() > 0)
-		{
-			for (var idx in pFileListMenu.selectedItemIndexes)
-				fileIndexes.push(+idx);
-		}
-		else
-			fileIndexes.push(+(pFileListMenu.selectedItemIdx));
-		// Ensure the file indexes are sorted in numerical order
-		fileIndexes.sort(function(a, b) { return a - b});
-
-		// Go through all the selected files and remove them.
-		// Note: Going through the list of indexes in reverse order so that
-		// removing each one from pFileList (gFileList) is simpler.
-		var removeAllSucceeded = true;
-		//for (var i = 0; i < fileIndexes.length; ++i)
-		for (var i = fileIndexes.length-1; i >= 0; --i)
-		{
-			var fileIdx = fileIndexes[i];
-			if (typeof(pFileList[fileIdx]) === "undefined")
-			{
-				removeAllSucceeded = false;
-				continue;
-			}
-			// For logging
-			var libIdx = file_area.dir[pFileList[fileIdx].dirCode].lib_index;
-			var dirIdx = file_area.dir[pFileList[fileIdx].dirCode].index;
-			var libDesc = file_area.lib_list[libIdx].description;
-			var dirDesc =  file_area.dir[pFileList[fileIdx].dirCode].description;
-			var libAndDirDesc = libDesc + " - " + dirDesc;
-
-			// Open the filebase and remove the file
-			var removeFileSucceeded = false;
-			var numFilesRemaining = 0;
-			var filebase = new FileBase(pFileList[fileIdx].dirCode);
-			if (filebase.open())
-			{
-				var filenameFullPath = filebase.get_path(pFileList[fileIdx].name); // For logging
-				try
-				{
-					removeFileSucceeded = filebase.remove(pFileList[fileIdx].name, true);
-				}
-				catch (error)
-				{
-					removeFileSucceeded = false;
-					// Make an entry in the BBS log that deleting the file failed
-					var logMsg = "ddfilelister: " + error;
-					log(LOG_ERR, logMsg);
-					bbs.log_str(logMsg);
-				}
-				// If the remove failed with deleting the file, then try without deleting the file
-				if (!removeFileSucceeded)
-				{
-					removeFileSucceeded = filebase.remove(pFileList[fileIdx].name, false);
-					if (removeFileSucceeded)
-					{
-						var logMsg = "ddfilelister: Removed " + filenameFullPath + " from the "
-						           + "filebase but couldn't actually delete the file";
-						log(LOG_INFO, logMsg);
-						bbs.log_str(logMsg);
-					}
-				}
-				if (removeFileSucceeded)
-				{
-					if (gScriptMode == MODE_LIST_DIR)
-						numFilesRemaining = filebase.files;
-				}
-				filebase.close();
-			}
-			else
-				removeAllSucceeded = false;
-
-			// Log a success/error message
-			var logMsg = "";
-			var logLevel = LOG_INFO;
-			if (removeFileSucceeded)
-			{
-				logMsg = "Digital Distortion File Lister: Successfully removed " + pFileList[fileIdx].name
-				       + " from " + libAndDirDesc + " (by " + user.alias + ")";
-				// Remove the file info object from the file list array
-				pFileList.splice(fileIdx, 1);
-				// If we were going through the list in forward order, we'd have to
-				// subtract 1 from the remaining indexes:
-				/*
-				// Subtract 1 from the remaining indexes in the fileIndexes array
-				for (var j = i+1; j < fileIndexes.length; ++j)
-					fileIndexes[j] = fileIndexes[j] - 1;
-				*/
-			}
-			else
-			{
-				removeAllSucceeded = false;
-				logMsg = "Digital Distortion File Lister: Failed to remove " + pFileList[fileIdx].name
-				       + " from " + libAndDirDesc + " (by " + user.alias + ")";
-				logLevel = LOG_ERR;
-			}
-			log(logLevel, logMsg);
-			bbs.log_str(logMsg);
-		}
-		// Display a success/failure message
-		if (removeAllSucceeded)
-			displayMsg("Successfully removed the file(s)", false, true);
-		else
-			displayMsg("Failed to remove 1 or more files", true, true);
-		// Adjust the selected item index in the file list menu if necesary
-		if (pFileListMenu.NumItems() == 0)
-			pFileListMenu.selectedItemIdx = 0;
-		else if (pFileListMenu.selectedItemIdx >= pFileListMenu.NumItems() - 1)
-			pFileListMenu.selectedItemIdx = pFileListMenu.NumItems() - 1;
-		// If the file list still has files in it, have the menu redraw
-		// itself to refresh with the missing entry.  Otherwise (no files left),
-		// say so and have the lister exit now.
-		numFilesRemaining = pFileList.length;
-		if (numFilesRemaining > 0)
-		{
-			// Have the file list menu set up its description width, colors, and format
-			// string again in case it no longer needs to use its scrollbar
-			pFileListMenu.SetItemWidthsColorsAndFormatStr();
-			retObj.reDrawMainScreenContent = true;
-			// If all files were not in the same directory, then check to see if all
-			// remaining files are now.  If so, we'll need to update the header lines
-			// at the top of the file list.
-			if (typeof(pFileList.allSameDir) == "boolean")
-			{
-				if (!pFileList.allSameDir)
-				{
-					pFileList.allSameDir = true; // Until we find it's not true
-					for (var i = 1; i < pFileList.length && pFileList.allSameDir; ++i)
-						pFileList.allSameDir = (pFileList[i].dirCode == pFileList[0].dirCode);
-					//retObj.reDrawListerHeader =  pFileList.allSameDir;
-					retObj.reDrawHeaderTextOnly =  pFileList.allSameDir;
-				}
-			}
-			// Also, if the file list menu can now show all its items on one
-			// page (not needing the scrollbar), set its top item index to 0.
-			if (pFileListMenu.CanShowAllItemsInWindow())
-				pFileListMenu.topItemIdx = 0;
-		}
-		else
-		{
-			if (gScriptMode == MODE_LIST_DIR)
-				displayMsg("The directory now has no files.", false, true);
-			else
-				displayMsg("There are no more files to show.", false, true);
-			retObj.exitNow = true;
-		}
-	}
-
-	return retObj;
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// DDFileMenuBar stuff
-
-function DDFileMenuBar(pPos)
-{
-	// Member functions
-	this.constructPromptText = DDFileMenuBar_constructPromptText;
-	this.getItemTextFromIdx = DDFileMenuBar_getItemTextFromIdx;
-	this.writePromptLine = DDFileMenuBar_writePromptLine;
-	this.refreshWithNewAction = DDFileMenuBar_refreshWithNewAction;
-	this.getDDFileMenuBarItemText = DDFileMenuBar_getDDFileMenuBarItemText;
-	this.incrementMenuItemAndRefresh = DDFileMenuBar_incrementMenuItemAndRefresh;
-	this.decrementMenuItemAndRefresh = DDFileMenuBar_decrementMenuItemAndRefresh;
-	this.getCurrentSelectedAction = DDFileMenuBar_getCurrentSelectedAction;
-	this.getActionFromChar = DDFileMenuBar_getActionFromChar;
-	this.setCurrentActionCode = DDFileMenuBar_setCurrentActionCode;
-	this.getAllActionKeysStr = DDFileMenuBar_getAllActionKeysStr;
-
-	// Member variables
-	this.pos = {
-		x: 1,
-		y: 1
-	};
-	if (typeof(pPos) === "object" && pPos.hasOwnProperty("x") && pPos.hasOwnProperty("y") && typeof(pPos.x) === "number" && typeof(pPos.y) === "number")
-	{
-		if (pPos.x >= 1 && pPos.x <= console.screen_columns)
-			this.pos.x = pPos.x;
-		if (pPos.y >= 1 && pPos.y <= console.screen_rows)
-			this.pos.y = pPos.y;
-	}
-
-	this.currentCommandIdx = 0; // The index of the current command for the menu array
-	this.lastCommandIdx = 0;    // To keep track of the previous command index
-
-	// Construct this.cmdArray: An array of the options
-	this.cmdArray = [];
-	this.cmdArray.push(new DDFileMenuBarItem("Info", 0, FILE_VIEW_INFO));
-	this.cmdArray.push(new DDFileMenuBarItem("View", 0, FILE_VIEW));
-	this.cmdArray.push(new DDFileMenuBarItem("Batch", 0, FILE_ADD_TO_BATCH_DL));
-	this.cmdArray.push(new DDFileMenuBarItem("DL", 0, FILE_DOWNLOAD_SINGLE));
-	if (user.is_sysop)
-	{
-		this.cmdArray.push(new DDFileMenuBarItem("Move", 0, FILE_MOVE));
-		//this.cmdArray.push(new DDFileMenuBarItem("Del", 0, FILE_DELETE));
-		this.cmdArray.push(new DDFileMenuBarItem("DEL", 0, FILE_DELETE, KEY_DEL));
-	}
-	this.cmdArray.push(new DDFileMenuBarItem("?", 0, HELP));
-	this.cmdArray.push(new DDFileMenuBarItem("Quit", 0, QUIT));
-
-	// Construct the prompt text (this must happen after this.cmdArray is built)
-	this.promptText = "";
-	this.constructPromptText(); // this.promptText will be constructed here
-}
-// For the DDFileMenuBar class: Constructs the prompt text.  This must be called after
-// this.cmdArray is built.
-//
-// Return value: The number of additional solid blocks used to fill the whole screen row
-function DDFileMenuBar_constructPromptText()
-{
-	var totalItemTextLen = 0;
-	for (var i = 0; i < this.cmdArray.length; ++i)
-		totalItemTextLen += this.cmdArray[i].itemText.length;
-	// The number of inner characters (without the outer solid blocks) is the total text
-	// length of all the items + 2 characters for each item except the last one
-	var numInnerChars = totalItemTextLen + (2 * (this.cmdArray.length-1));
-	// The number of solid blocks: Subtracting 11 because there will be 5 block characters on each side,
-	// and subtract 1 extra so it doesn't fill the last character on the screen
-	var numSolidBlocks = console.screen_columns - numInnerChars - 11;
-	var numSolidBlocksPerSide = Math.floor(numSolidBlocks / 2);
-	// Build the prompt text: Start with the left blocks
-	this.promptText = "\x01n\x01w" + BLOCK1 + BLOCK2 + BLOCK3 + BLOCK4;
-	for (var i = 0; i < numSolidBlocksPerSide; ++i)
-		this.promptText += BLOCK4;
-	this.promptText += THIN_RECTANGLE_LEFT;
-	// Add the menu item text & block characters
-	var menuItemXPos = 6 + numSolidBlocksPerSide; // The X position of the start of item text for each item
-	for (var i = 0; i < this.cmdArray.length; ++i)
-	{
-		this.cmdArray[i].pos = menuItemXPos;
-		var numTrailingBlockChars = 0;
-		var selected = (i == this.currentCommandIdx);
-		var withTrailingBlock = false;
-		if (i < this.cmdArray.length-1)
-		{
-			withTrailingBlock = true;
-			numTrailingBlockChars = 2;
-		}
-		menuItemXPos += this.cmdArray[i].itemText.length + numTrailingBlockChars;
-		this.promptText += this.getDDFileMenuBarItemText(this.cmdArray[i].itemText, selected, withTrailingBlock);
-	}
-	// Add the right-side blocks
-	this.promptText += "\x01w" + THIN_RECTANGLE_RIGHT;
-	for (var i = 0; i < numSolidBlocksPerSide; ++i)
-		this.promptText += BLOCK4;
-	this.promptText += BLOCK3 + BLOCK2 + BLOCK1 + "\x01n";
-}
-// For the DDFileMenuBar class: Gets the text for a prompt item based on its index
-function DDFileMenuBar_getItemTextFromIdx(pIdx)
-{
-	if (typeof(pIdx) !== "number" || pIdx < 0 || pIdx >= this.cmdArray.length)
-		return "";
-	return this.cmdArray[pIdx].itemText;
-}
-// For the DDFileMenuBar class: Writes the prompt text at the defined location
-function DDFileMenuBar_writePromptLine()
-{
-	// Place the cursor at the defined location, then write the prompt text
-	console.gotoxy(this.pos.x, this.pos.y);
-	console.print(this.promptText);
-}
-// For the DDFileMenuBar class: Refreshes 2 items in the command bar text line
-//
-// Parameters:
-//  pCmdIdx: The index of the new/current command
-function DDFileMenuBar_refreshWithNewAction(pCmdIdx)
-{
-	if (typeof(pCmdIdx) !== "number")
-		return;
-	if (pCmdIdx == this.currentCommandIdx)
-		return;
-
-	// Refresh the prompt area for the previous index with regular colors
-	// Re-draw the last item text with regular colors
-	var itemText = this.getItemTextFromIdx(this.currentCommandIdx);
-	console.gotoxy(this.cmdArray[this.currentCommandIdx].pos, this.pos.y);
-	console.print("\x01n" + this.getDDFileMenuBarItemText(itemText, false, false));
-	// Draw the new item text with selected colors
-	itemText = this.getItemTextFromIdx(pCmdIdx);
-	console.gotoxy(this.cmdArray[pCmdIdx].pos, this.pos.y);
-	console.print("\x01n" + this.getDDFileMenuBarItemText(itemText, true, false));
-	console.gotoxy(this.pos.x+strip_ctrl(this.promptText).length-1, this.pos.y);
-
-	this.lastCommandIdx = this.currentCommandIdx;
-	this.currentCommandIdx = pCmdIdx;
-
-	// Re-construct the bar text to make sure it's up to date with the selected action
-	this.constructPromptText();
-}
-// For the DDFileMenuBar class: Returns a string containing a piece of text for the
-// menu bar text with its color attributes.
-//
-// Parameters:
-//  pText: The text for the item
-//  pSelected: Boolean - Whether or not the item is selected
-//  pWithTrailingBlock: Boolean - Whether or not to include the trailing block
-//
-// Return value: A string containing the item text for the action bar
-function DDFileMenuBar_getDDFileMenuBarItemText(pText, pSelected, pWithTrailingBlock)
-{
-	if (typeof(pText) !== "string" || pText.length == 0)
-		return "";
-
-	var selected = (typeof(pSelected) === "boolean" ? pSelected : false);
-	var withTrailingBlock = (typeof(pWithTrailingBlock) === "boolean" ? pWithTrailingBlock : false);
-
-	// Separate the first character from the rest of the text
-	var firstChar = pText.length > 0 ? pText.charAt(0) : "";
-	var restOfText = pText.length > 1 ? pText.substr(1, pText.length - 1) : "";
-	// Build the item text and return it
-	var itemText = "\x01n";
-	if (selected)
-		itemText += "\x01" + "1\x01r\x01h" + firstChar + "\x01n\x01" + "1\x01k" + restOfText;
-	else
-		itemText += "\x01" + "6\x01c\x01h" + firstChar + "\x01n\x01" + "6\x01k" + restOfText;
-	itemText += "\x01n";
-	if (withTrailingBlock)
-		itemText += "\x01w" + THIN_RECTANGLE_RIGHT + THIN_RECTANGLE_LEFT + "\x01n";
-	return itemText;
-}
-// For the DDFileMenuBar class: Increments to the next menu item and refreshes the
-// menu bar on the screen
-function DDFileMenuBar_incrementMenuItemAndRefresh()
-{
-	var newCmdIdx = this.currentCommandIdx + 1;
-	if (newCmdIdx >= this.cmdArray.length)
-		newCmdIdx = 0;
-	// Will set this.currentCommandIdx
-	this.refreshWithNewAction(newCmdIdx);
-}
-// For the DDFileMenuBar class: Decrements to the previous menu item and refreshes the
-// menu bar on the screen
-function DDFileMenuBar_decrementMenuItemAndRefresh()
-{
-	var newCmdIdx = this.currentCommandIdx - 1;
-	if (newCmdIdx < 0)
-		newCmdIdx = this.cmdArray.length - 1;
-	// Will set this.currentCommandIdx
-	this.refreshWithNewAction(newCmdIdx);
-}
-// For the DDFileMenuBar class: Gets the return code for the currently selected action
-function DDFileMenuBar_getCurrentSelectedAction()
-{
-	return this.cmdArray[this.currentCommandIdx].retCode;
-}
-// For the DDFileMenuBar class: Gets the return code matching a given character.
-// If there is no match, this will return -1.
-//
-// Parameters:
-//  pChar: The character to match
-//  pCaseSensitive: Optional - Boolean - Whether or not to do a case-sensitive match.
-//                  This defaults to false.
-function DDFileMenuBar_getActionFromChar(pChar, pCaseSensitive)
-{
-	if (typeof(pChar) !== "string" || pChar.length == 0)
-		return -1;
-
-	var caseSensitive = (typeof(pCaseSensitive) === "boolean" ? pCaseSensitive : false);
-
-	var retCode = -1;
-	if (caseSensitive)
-	{
-		for (var i = 0; i < this.cmdArray.length && retCode == -1; ++i)
-		{
-			if (this.cmdArray[i].hotkeyOverride != null && typeof(this.cmdArray[i].hotkeyOverride) !== "undefined")
-			{
-				if (pChar == this.cmdArray[i].hotkeyOverride)
-					retCode = this.cmdArray[i].retCode;
-			}
-			else if (this.cmdArray[i].itemText.length > 0 && this.cmdArray[i].itemText.charAt(0) == pChar)
-				retCode = this.cmdArray[i].retCode;
-		}
-	}
-	else
-	{
-		// Not case sensitive
-		var charUpper = pChar.toUpperCase();
-		for (var i = 0; i < this.cmdArray.length && retCode == -1; ++i)
-		{
-			if (this.cmdArray[i].hotkeyOverride != null && typeof(this.cmdArray[i].hotkeyOverride) !== "undefined")
-			{
-				if (pChar == this.cmdArray[i].hotkeyOverride)
-					retCode = this.cmdArray[i].retCode;
-			}
-			else if (this.cmdArray[i].itemText.length > 0 && this.cmdArray[i].itemText.charAt(0).toUpperCase() == charUpper)
-				retCode = this.cmdArray[i].retCode;
-		}
-	}
-	return retCode;
-}
-// For the DDFileMenuBar class: Sets the current command item in the menu bar based on its
-// action code
-//
-// Parameters:
-//  pActionCode: The code of the action
-//  pRefreshOnScreen: Optional boolean - Whether or not to refresh the command bar on the screen.
-//                    Defaults to false.
-function DDFileMenuBar_setCurrentActionCode(pActionCode, pRefreshOnScreen)
-{
-	if (typeof(pActionCode) !== "number")
-		return;
-
-	var refreshOnScreen = (typeof(pRefreshOnScreen) === "boolean" ? pRefreshOnScreen : false);
-
-	for (var i = 0; i < this.cmdArray.length; ++i)
-	{
-		if (this.cmdArray[i].retCode == pActionCode)
-		{
-			if (refreshOnScreen)
-				this.refreshWithNewAction(i);
-			else
-			{
-				this.currentCommandIdx = i;
-				this.lastCommandIdx = i;
-			}
-			break;
-		}
-	}
-}
-// For the DDFileMenuBar: Gets all the action hotkeys as a string
-//
-// Parameters:
-//  pLowercase: Boolean - Whether or not to include letters as lowercase
-//  pUppercase: Boolean - Whether or not to include letters as uppercase
-//
-// Return value: All the action hotkeys as a string
-function DDFileMenuBar_getAllActionKeysStr(pLowercase, pUppercase)
-{
-	var hotkeysStr = "";
-	for (var i = 0; i < this.cmdArray.length; ++i)
-	{
-		if (this.cmdArray[i].itemText.length > 0)
-			hotkeysStr += this.cmdArray[i].itemText.charAt(0);
-	}
-	var hotkeysToReturn = "";
-	if (pLowercase)
-		hotkeysToReturn += hotkeysStr.toLowerCase();
-	if (pUppercase)
-		hotkeysToReturn += hotkeysStr.toUpperCase();
-	return hotkeysToReturn;
-}
-
-// Consctructor for an DDFileMenuBarItem
-//
-// Parameters:
-//  pItemText: The text of the item
-//  pPos: Horizontal (or vertical) starting location in the bar
-//  pRetCode: The item's return code
-//  pHotkeyOverride: Optional: A key to use for the action instead of the first character in pItemText
-function DDFileMenuBarItem(pItemText, pPos, pRetCode, pHotkeyOverride)
-{
-	this.itemText = pItemText;
-	this.pos = pPos;
-	this.retCode = pRetCode;
-	this.hotkeyOverride = null;
-	if (pHotkeyOverride != null && typeof(pHotkeyOverride) !== "undefined")
-		this.hotkeyOverride = pHotkeyOverride;
-}
-
-
-///////////////////////////////////////////////////////////////////////////////
-// Helper functions
-
-// Gets file metadata & its file time from a filebase.
-//
-// Parameters:
-//  pDirCode: The internal code of the directory the file is in
-//  pFilename: The name of the file (without the full path)
-//  pDetail: The detail level of the metadata object to get - See FileBase.DETAIL in JS docs
-//
-// Return value: An object containing the following properties:
-//               fileMetadataObj: An object with extended file metadata from the filebase.
-//                                This will have a dirCode property added.
-//               fileTime: The timestamp of the file
-function getFileInfoFromFilebase(pDirCode, pFilename, pDetail)
-{
-	if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pFilename) !== "string" || pFilename.length == 0)
-		return null;
-
-	var fileMetadataObj = null;
-	var filebase = new FileBase(pDirCode);
-	if (filebase.open())
-	{
-		// Just in case the file has the full path, get just the filename from it.
-		var filename = file_getname(pFilename);
-		var fileDetail = (typeof(pDetail) === "number" ? pDetail : FileBase.DETAIL.NORM);
-		if (typeof(pDetail) === "number")
-			fileDetail = pDetail;
-		else
-			fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM);
-		fileMetadataObj = filebase.get(filename, fileDetail);
-		fileMetadataObj.dirCode = pDirCode;
-		//fileMetadataObj.size = filebase.get_size(filename);
-		if (!fileMetadataObj.hasOwnProperty("time"))
-			fileMetadataObj.time = filebase.get_time(filename);
-		filebase.close();
-	}
-
-	return fileMetadataObj;
-}
-
-// Moves a file from one filebase to another
-//
-// Parameters:
-//  pSrcFileMetadata: Metadata for the source file.  This is assumed to contain 'normal' detail (not extended)
-//  pDestDirCode: The internal code of the destination filebase to move to the file to
-//
-// Return value: An object containing the following properties:
-//               moveSucceeded: Boolean - Whether or not the move succeeded
-//               failReason: If the move failed, this is a string that specifies why it failed
-function moveFileToOtherFilebase(pSrcFileMetadata, pDestDirCode)
-{
-	var retObj = {
-		moveSucceeded: false,
-		failReason: ""
-	};
-
-	// pSrcFileMetadata is assumed to be a basic file metadata object, without extended
-	// information.  Get a metadata object with maximum information so we have all
-	// metadata available.
-	var srcFilebase = new FileBase(pSrcFileMetadata.dirCode);
-	if (srcFilebase.open())
-	{
-		var extdFileInfo = srcFilebase.get(pSrcFileMetadata, FileBase.DETAIL.MAX);
-		// Move the file over, remove it from the original filebase, and add it to the new filebase
-		var srcFilenameFull = srcFilebase.get_path(pSrcFileMetadata);
-		var destFilenameFull = file_area.dir[pDestDirCode].path + pSrcFileMetadata.name;
-		if (file_rename(srcFilenameFull, destFilenameFull))
-		{
-			if (srcFilebase.remove(pSrcFileMetadata.name, false))
-			{
-				// Add the file to the other directory
-				var destFilebase = new FileBase(pDestDirCode);
-				if (destFilebase.open())
-				{
-					retObj.moveSucceeded = destFilebase.add(extdFileInfo);
-					destFilebase.close();
-				}
-				else
-				{
-					retObj.failReason = "Failed to open the destination filebase";
-					// Try to add the file back to the source filebase
-					var moveBackSucceeded = false;
-					if (file_rename(destFilenameFull, srcFilenameFull))
-						moveBackSucceeded = srcFilebase.add(extdFileInfo);
-					if (!moveBackSucceeded)
-						retObj.failReason += " & moving the file back failed";
-				}
-			}
-			else
-				retObj.failReason = "Failed to remove the file from the source directory";
-		}
-		else
-			retObj.failReason = "Failed to move the file to the new filebase directory";
-
-		srcFilebase.close();
-	}
-	else
-	{
-		var libIdx = file_area.dir[pSrcFileMetadata.dirCode].lib_index;
-		var dirIdx = file_area.dir[pSrcFileMetadata.dirCode].index;
-		var libDesc = file_area.lib_list[libIdx].description;
-		var dirDesc =  file_area.dir[pSrcFileMetadata.dirCode].description;
-		retObj.failreason = "Failed to open filebase: " + libDesc + " - " + dirDesc;
-	}
-
-	return retObj;
-}
-
-// Counts the number of occurrences of a substring within a string
-//
-// Parameters:
-//  pStr: The string to count occurences in
-//  pSubstr: The string to look for within pStr
-//
-// Return value: The number of occurrences of pSubstr found in pStr
-function countOccurrencesInStr(pStr, pSubstr)
-{
-	if (typeof(pStr) !== "string" || typeof(pSubstr) !== "string") return 0;
-	if (pStr.length == 0 || pSubstr.length == 0) return 0;
-
-	var count = 0;
-	var strIdx = pStr.indexOf(pSubstr);
-	while (strIdx > -1 && strIdx < pStr.length)
-	{
-		++count;
-		strIdx = pStr.indexOf(pSubstr, strIdx+1);
-	}
-	return count;
-}
-
-// Constructs & displays a frame with a border around it, and performs a user input loop
-// until the user quits out of the input loop.
-//
-// Parameters:
-//  pFrameX: The X coordinate of the upper-left corner of the frame (including border)
-//  pFrameY: The Y coordinate of the upper-left corner of the frame (including border)
-//  pFrameWidth: The width of the frame (including border)
-//  pFrameHeight: The height of the frame (including border)
-//  pBorderColor: The attribute codes for the border color
-//  pFrameTitle: The title (text) to use in the frame border
-//  pTitleColor: Optional string - The attribute codes for the color to use for the frame title
-//  pFrameContents: The contents to display in the frame
-//  pAdditionalQuitKeys: Optional - A string containing additional keys to quit the
-//                       input loop.  This is case-sensitive.
-//
-// Return value: The last keypress/input from the user
-function displayBorderedFrameAndDoInputLoop(pFrameX, pFrameY, pFrameWidth, pFrameHeight, pBorderColor, pFrameTitle, pTitleColor, pFrameContents, pAdditionalQuitKeys)
-{
-	if (typeof(pFrameX) !== "number" || typeof(pFrameY) !== "number" || typeof(pFrameWidth) !== "number" || typeof(pFrameHeight) !== "number")
-		return;
-
-	// Display the border for the frame
-	var keyHelpStr = "\x01n\x01c\x01hQ\x01b/\x01cEnter\x01b/\x01cESC\x01y: \x01gClose\x01b";
-	var scrollLoopNavHelp = "\x01c\x01hUp\x01b/\x01cDn\x01b/\x01cHome\x01b/\x01cEnd\x01b/\x01cPgup\x01b/\x01cPgDn\x01y: \x01gNav";
-	if (console.screen_columns >= 80)
-		keyHelpStr += ", " + scrollLoopNavHelp;
-	var borderColor = (typeof(pBorderColor) === "string" ? pBorderColor : "\x01r");
-	drawBorder(pFrameX, pFrameY, pFrameWidth, pFrameHeight, borderColor, "double", pFrameTitle, pTitleColor, keyHelpStr);
-
-	// Construct the frame window for the file info
-	// Create a Frame here with the full filename, extended description, etc.
-	var frameX = pFrameX + 1;
-	var frameY = pFrameY + 1;
-	var frameWidth = pFrameWidth - 2;
-	var frameHeight = pFrameHeight - 2;
-	var frameObj = new Frame(frameX, frameY, frameWidth, frameHeight, BG_BLACK);
-	frameObj.attr &=~ HIGH;
-	frameObj.v_scroll = true;
-	frameObj.h_scroll = false;
-	frameObj.scrollbars = true;
-	var scrollbarObj = new ScrollBar(frameObj, {bg: BG_BLACK, fg: LIGHTGRAY, orientation: "vertical", autohide: false});
-	// Put the file info string in the frame window, then start the
-	// user input loop for the frame
-	frameObj.putmsg(pFrameContents, "\x01n");
-	var lastUserInput = doFrameInputLoop(frameObj, scrollbarObj, pFrameContents, pAdditionalQuitKeys);
-	//infoFrame.bottom();
-
-	return lastUserInput;
-}
-
-// Displays a Frame object and handles the input loop for navigation until
-// the user presses Q, Enter, or ESC To quit the input loop
-//
-// Parameters:
-//  pFrame: The Frame object
-//  pScrollbar: The Scrollbar object for the Frame
-//  pFrameContentStr: The string content that was added to the Frame
-//  pAdditionalQuitKeys: Optional - A string containing additional keys to quit the
-//                       input loop.  This is case-sensitive.
-//
-// Return value: The last keypress/input from the user
-function doFrameInputLoop(pFrame, pScrollbar, pFrameContentStr, pAdditionalQuitKeys)
-{
-	var checkAdditionalQuitKeys = (typeof(pAdditionalQuitKeys) === "string" && pAdditionalQuitKeys.length > 0);
-
-	// Input loop for the frame to let the user scroll it
-	var frameContentTopYOffset = 0;
-	//var maxFrameYOffset = pFrameContentStr.split("\r\n").length - pFrame.height;
-	var maxFrameYOffset = countOccurrencesInStr(pFrameContentStr, "\r\n") - pFrame.height;
-	if (maxFrameYOffset < 0) maxFrameYOffset = 0;
-	var userInput = "";
-	var continueOn = true;
-	do
-	{
-		pFrame.scrollTo(0, frameContentTopYOffset);
-		pFrame.invalidate();
-		pScrollbar.cycle();
-		pFrame.cycle();
-		pFrame.draw();
-		// Note: getKeyWithESCChars() is defined in dd_lightbar_menu.js.
-		userInput = getKeyWithESCChars(K_NOECHO|K_NOSPIN|K_NOCRLF, 30000).toUpperCase();
-		if (userInput == KEY_UP)
-		{
-			if (frameContentTopYOffset > 0)
-				--frameContentTopYOffset;
-		}
-		else if (userInput == KEY_DOWN)
-		{
-			if (frameContentTopYOffset < maxFrameYOffset)
-				++frameContentTopYOffset;
-		}
-		else if (userInput == KEY_PAGEUP)
-		{
-			frameContentTopYOffset -= pFrame.height;
-			if (frameContentTopYOffset < 0)
-				frameContentTopYOffset = 0;
-		}
-		else if (userInput == KEY_PAGEDN)
-		{
-			frameContentTopYOffset += pFrame.height;
-			if (frameContentTopYOffset > maxFrameYOffset)
-				frameContentTopYOffset = maxFrameYOffset;
-		}
-		else if (userInput == KEY_HOME)
-			frameContentTopYOffset = 0;
-		else if (userInput == KEY_END)
-			frameContentTopYOffset = maxFrameYOffset;
-
-		// Check for whether to continue the input loop
-		continueOn = (userInput != "Q" && userInput != KEY_ENTER && userInput != KEY_ESC);
-		// If the additional quit keys does not contain the user's keypress, then continue
-		// the input loop.
-		// In other words, if the additional quit keys includes the user's keypress, then
-		// don't continue.
-		if (continueOn && checkAdditionalQuitKeys)
-			continueOn = (pAdditionalQuitKeys.indexOf(userInput) < 0);
-	} while (continueOn);
-
-	return userInput;
-}
-
-// Displays the header lines for showing above the file list
-//
-// Parameters:
-//  pTextOnly: Only draw the library & directory text (no decoration or other text).
-//             This is optional & defaults to false.
-//  pDirCodeOverride: Optional string: If this is valid, this will be used for the library & directory name
-function displayFileLibAndDirHeader(pTextOnly, pDirCodeOverride)
-{
-	// If the behavior flags include no header, then just return immediately
-	if ((gListBehavior & FL_NO_HDR) == FL_NO_HDR)
-		return;
-
-	var textOnly = (typeof(pTextOnly) === "boolean" ? pTextOnly : false);
-
-	// Determine if this is the first time this function has been called.  If so,
-	// we'll want to update gNumHeaderLinesDisplayed at the end.
-	var dispHdrFirstRun = false;
-	if (typeof(displayFileLibAndDirHeader.firstRun) == "undefined")
-	{
-		dispHdrFirstRun = true;
-		displayFileLibAndDirHeader.firstRun = true;
-	}
-
-	// For the library & directory text to display, if we're just listing the user's
-	// current directory, use that.  Otherwise, if all search results are in the same
-	// directory, then use a directory code from the file list.
-	var libIdx = 0;
-	var dirIdx = 0;
-	var libDesc = "";
-	var dirDesc =  "";
-	var dirCode = "";
-	if (gScriptMode == MODE_LIST_DIR)
-		dirCode = gDirCode;
-	else if (typeof(gFileList.allSameDir) === "boolean" && gFileList.allSameDir && gFileList.length > 0)
-		dirCode = gFileList[0].dirCode;
-	if (dirCode.length > 0)
-	{
-		libIdx = file_area.dir[dirCode].lib_index;
-		dirIdx = file_area.dir[dirCode].index;
-		libDesc = file_area.lib_list[libIdx].description;
-		dirDesc =  file_area.dir[dirCode].description;
-	}
-	else if (typeof(pDirCodeOverride) === "string" && file_area.dir.hasOwnProperty(pDirCodeOverride))
-	{
-		libIdx = file_area.dir[pDirCodeOverride].lib_index;
-		dirIdx = file_area.dir[pDirCodeOverride].index;
-		libDesc = file_area.lib_list[libIdx].description;
-		dirDesc =  file_area.dir[pDirCodeOverride].description;
-	}
-	else
-	{
-		libIdx = -1;
-		dirIdx = -1;
-		libDesc = "Various";
-		dirDesc = "Various";
-	}
-
-	var hdrTextWidth = console.screen_columns - 21;
-	var descWidth = hdrTextWidth - 11;
-	var libText = format("\x01cLib \x01w\x01h#\x01b%4d\x01c: \x01n\x01c%-" + descWidth + "s\x01n", +(libIdx+1), libDesc.substr(0, descWidth));
-	var dirText = format("\x01cDir \x01w\x01h#\x01b%4d\x01c: \x01n\x01c%-" + descWidth + "s\x01n", +(dirIdx+1), dirDesc.substr(0, descWidth));
-
-	// Library line
-	if (textOnly)
-	{
-		console.gotoxy(6, 1);
-		console.print("\x01n" + libText);
-		console.gotoxy(6, 2);
-		console.print("\x01n" + dirText);
-	}
-	else
-	{
-		console.print("\x01n\x01w" + BLOCK1 + BLOCK2 + BLOCK3 + BLOCK4 + THIN_RECTANGLE_LEFT);
-		console.print(libText);
-		console.print("\x01w" + THIN_RECTANGLE_RIGHT + "\x01k\x01h" + BLOCK4 + "\x01n\x01w" + THIN_RECTANGLE_LEFT +
-					  "\x01g\x01hDD File\x01n\x01w");
-		console.print(THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1);
-		console.crlf();
-		// Directory line
-		console.print("\x01n\x01w" + BLOCK1 + BLOCK2 + BLOCK3 + BLOCK4 + THIN_RECTANGLE_LEFT);
-		console.print(dirText);
-		console.print("\x01w" + THIN_RECTANGLE_RIGHT + "\x01k\x01h" + BLOCK4 + "\x01n\x01w" + THIN_RECTANGLE_LEFT +
-					  "\x01g\x01hLister \x01n\x01w");
-		console.print(THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1);
-		console.print("\x01n");
-
-		// List header
-		console.crlf();
-		displayListHdrLine(false);
-
-		if (dispHdrFirstRun)
-		{
-			gNumHeaderLinesDisplayed = 3;
-			gErrorMsgBoxULY = gNumHeaderLinesDisplayed; // Note: console.screen_rows is 1-based
-		}
-	}
-}
-// Displays the header line with the column headers for the file list
-//
-// Parameters:
-//  pMoveToLocationFirst: Boolean - Whether to move the cursor to the required location first.
-function displayListHdrLine(pMoveToLocationFirst)
-{
-	if (pMoveToLocationFirst && console.term_supports(USER_ANSI))
-		console.gotoxy(1, 3);
-	var filenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart;
-	var fileSizeLen = gListIdxes.fileSizeEnd - gListIdxes.fileSizeStart -1;
-	var descLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1;
-	var formatStr = "\x01n\x01w\x01h%-" + filenameLen + "s %" + fileSizeLen + "s %-"
-	              + +(descLen-7) + "s\x01n\x01w%5s\x01n";
-	var listHdrEndText = THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1;
-	printf(formatStr, "Filename", "Size", "Description", listHdrEndText);
-}
-
-// Creates the menu for displaying the file list
-//
-// Parameters:
-//  pQuitKeys: A string containing hotkeys to use as the menu's quit keys
-//
-// Return value: The DDLightbarMenu object for the file list in the file directory
-function createFileListMenu(pQuitKeys)
-{
-	//DDLightbarMenu(pX, pY, pWidth, pHeight)
-	// Create the menu object.  Place it below the header lines (which should have been written
-	// before this), and also leave 1 row at the bottom for the prompt line
-	var startRow = gNumHeaderLinesDisplayed > 0 ? gNumHeaderLinesDisplayed + 1 : 1;
-	// If we'll be displaying short (one-line) file descriptions, then use the whole width
-	// of the terminal (minus 1) for the menu width.  But if the user has extended (multi-line)
-	// file descriptions enabled, then set the menu width only up through the file size, since
-	// the extended file description will be displayed to the right of the menu.
-	var menuWidth = console.screen_columns - 1;
-	if (extendedDescEnabled())
-		menuWidth = gListIdxes.fileSizeEnd + 1;
-	var menuHeight = console.screen_rows - (startRow-1) - 1;
-	var fileListMenu = new DDLightbarMenu(1, startRow, menuWidth, menuHeight);
-	fileListMenu.scrollbarEnabled = true;
-	fileListMenu.borderEnabled = false;
-	fileListMenu.multiSelect = true;
-	fileListMenu.ampersandHotkeysInItems = false;
-	fileListMenu.wrapNavigation = false;
-
-	fileListMenu.extdDescEnabled = extendedDescEnabled();
-
-	// Add additional keypresses for quitting the menu's input loop so we can
-	// respond to these keys.
-	if (typeof(pQuitKeys) === "string")
-		fileListMenu.AddAdditionalQuitKeys(pQuitKeys);
-
-	// Define the menu's function to get the number of items.  This must be done here
-	// in order for the menu's CanShowAllItemsInWindow() to work propertly.
-	fileListMenu.NumItems = function() {
-		return gFileList.length;
-	};
-
-	// Define a function for setting the description width, colors, and format string
-	// based on whether the menu's scrollbar will be used.
-	//
-	// Return value: Boolean: Whether or not the width changed
-	fileListMenu.SetItemWidthsColorsAndFormatStr = function() {
-		var widthChanged = false;
-		var oldDescriptionEnd = gListIdxes.descriptionEnd;
-		gListIdxes.descriptionEnd = console.screen_columns - 1; // Leave 1 character remaining on the screen
-		// If the file list menu will use its scrollbar, then reduce the description width by 1
-		if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
-			gListIdxes.descriptionEnd -= 1;
-		widthChanged = (gListIdxes.descriptionEnd != oldDescriptionEnd);
-		this.SetColors({
-			itemColor: [{start: gListIdxes.filenameStart, end: gListIdxes.filenameEnd, attrs: gColors.filename},
-			            {start: gListIdxes.fileSizeStart, end: gListIdxes.fileSizeEnd, attrs: gColors.fileSize},
-			            {start: gListIdxes.descriptionStart, end: gListIdxes.descriptionEnd, attrs: gColors.desc}],
-			selectedItemColor: [{start: gListIdxes.filenameStart, end: gListIdxes.filenameEnd, attrs: gColors.bkgHighlight + gColors.filenameHighlight},
-			                    {start: gListIdxes.fileSizeStart, end: gListIdxes.fileSizeEnd, attrs: gColors.bkgHighlight + gColors.fileSizeHighlight},
-			                    {start: gListIdxes.descriptionStart, end: gListIdxes.descriptionEnd, attrs: gColors.bkgHighlight + gColors.descHighlight}]
-		});
-
-		this.filenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart;
-		this.fileSizeLen = gListIdxes.fileSizeEnd - gListIdxes.fileSizeStart -1;
-		this.shortDescLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1;
-		// If extended descriptions are enabled, then we won't be writing a description here
-		if (this.extdDescEnabled)
-			this.fileFormatStr = "%-" + this.filenameLen + "s %" + this.fileSizeLen + "s";
-		else
-		{
-			this.fileFormatStr = "%-" + this.filenameLen + "s %" + this.fileSizeLen
-			                   + "s %-" + this.shortDescLen + "s";
-		}
-		return widthChanged;
-	};
-	// Set up the menu's description width, colors, and format string
-	fileListMenu.SetItemWidthsColorsAndFormatStr();
-
-	fileListMenu.lastFileDirCode = "";
-	// Define the menu function for getting an item
-	fileListMenu.GetItem = function(pIdx) {
-		// In here, 'this' refers to the fileListMenu object
-		// If doing a file search, then update the header with the file library & directory
-		// name of the currently selected file (instead of displaying "Various"). This seems
-		// like a bit of a hack, but it works.
-		var allSameDir = (typeof(gFileList.allSameDir) === "boolean" ? gFileList.allSameDir : false);
-		if (isDoingFileSearch() && !allSameDir && gFileList[pIdx].dirCode != this.lastFileDirCode)
-		{
-			if ((gListBehavior & FL_NO_HDR) != FL_NO_HDR)
-			{
-				var originalCurPos = console.getxy();
-				displayFileLibAndDirHeader(true, gFileList[pIdx].dirCode);
-				console.gotoxy(originalCurPos);
-			}
-		}
-		this.lastFileDirCode = gFileList[pIdx].dirCode;
-
-		var menuItemObj = this.MakeItemWithRetval(pIdx);
-		var filename = shortenFilename(gFileList[pIdx].name, this.filenameLen, true);
-		// FileBase.format_name() could also be called to format the filename for display:
-		/*
-		var filebase = new FileBase(gFileList[pIdx].dirCode);
-		if (filebase.open())
-		{
-			filename = filebase.format_name(gFileList[pIdx].name, this.filenameLen, true);
-			filebase.close();
-		}
-		*/
-		var desc = (typeof(gFileList[pIdx].desc) === "string" ? gFileList[pIdx].desc : "");
-		menuItemObj.text = format(this.fileFormatStr,
-		                          filename,
-								  getFileSizeStr(gFileList[pIdx].size, this.fileSizeLen),
-		                          desc.substr(0, this.shortDescLen));
-		return menuItemObj;
-	}
-
-	fileListMenu.selectedItemIndexes = {};
-	fileListMenu.numSelectedItemIndexes = function() {
-		return Object.keys(this.selectedItemIndexes).length;
-	};
-
-	// If extended file descriptions are enabled, set the OnItemNav function for
-	// when the user navigates to a new item, to display the file description
-	// next to the file menu.
-	if (extendedDescEnabled())
-	{
-		fileListMenu.OnItemNav = function(pOldItemIdx, pNewItemIdx) {
-			displayFileExtDescOnMainScreen(pNewItemIdx);
-		};
-	}
-
-	return fileListMenu;
-}
-
-// Creates the menu for choosing a file library (for moving a message to another message area).
-// The return value of the chosen item is the file library index.
-//
-// Return value: The DDLightbarMenu object for choosing a file library
-function createFileLibMenu()
-{
-	// This probably shouldn't happen, but check to make sure there are file libraries
-	if (file_area.lib_list.length == 0)
-	{
-		console.crlf();
-		console.print("\x01n\x01y\x01hThere are no file libraries available\x01n");
-		console.crlf();
-		console.pause();
-		return;
-	}
-
-	//DDLightbarMenu(pX, pY, pWidth, pHeight)
-	// Create the menu object
-	var startRow = gNumHeaderLinesDisplayed + 4;
-	var fileLibMenu = new DDLightbarMenu(5, startRow, console.screen_columns - 10, console.screen_rows - startRow - 5);
-	fileLibMenu.scrollbarEnabled = true;
-	fileLibMenu.borderEnabled = true;
-	fileLibMenu.multiSelect = false;
-	fileLibMenu.ampersandHotkeysInItems = false;
-	fileLibMenu.wrapNavigation = false;
-
-	// Add additional keypresses for quitting the menu's input loop.
-	// Q: Quit
-	var additionalQuitKeys = "qQ";
-	fileLibMenu.AddAdditionalQuitKeys(additionalQuitKeys);
-
-	// Re-define the menu's function for getting the number of items.  This is necessary
-	// here in order for the menu's CanShowAllItemsInWindow() to work properly.
-	fileLibMenu.NumItems = function() {
-		return file_area.lib_list.length;
-	};
-
-	// Construct a format string for the file libraries
-	var largestNumDirs = getLargestNumDirsWithinFileLibs();
-	fileLibMenu.libNumLen = file_area.lib_list.length.toString().length;
-	fileLibMenu.numDirsLen = largestNumDirs.toString().length;
-	var menuInnerWidth = fileLibMenu.size.width - 2; // Menu width excluding borders
-	// If the scrollbar will be showing, reduce the inner width by 1
-	if (fileLibMenu.scrollbarEnabled && !fileLibMenu.CanShowAllItemsInWindow())
-		--menuInnerWidth;
-	// Allow 2 for spaces
-	fileLibMenu.libDescLen = menuInnerWidth - fileLibMenu.libNumLen - fileLibMenu.numDirsLen - 2;
-	fileLibMenu.libFormatStr = "%" + fileLibMenu.libNumLen + "d %-" + fileLibMenu.libDescLen + "s %" + fileLibMenu.numDirsLen + "d";
-
-	// Colors and their indexes
-	fileLibMenu.borderColor = gColors.fileAreaMenuBorder;
-	var libNumStart = 0;
-	var libNumEnd = fileLibMenu.libNumLen;
-	var descStart = libNumEnd;
-	var descEnd = descStart + fileLibMenu.libDescLen;
-	var numDirsStart = descEnd;
-	//var numDirsEnd = numDirsStart + fileLibMenu.numDirsLen;
-	fileLibMenu.SetColors({
-		itemColor: [{start: libNumStart, end: libNumEnd, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaNum},
-		            {start: descStart, end:descEnd, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaDesc},
-		            {start: numDirsStart, end: -1, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaNumItems}],
-		selectedItemColor: [{start: libNumStart, end: libNumEnd, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumHighlight},
-		                    {start: descStart, end:descEnd, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaDescHighlight},
-		                    {start: numDirsStart, end: -1, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumItemsHighlight}]
-	});
-
-	fileLibMenu.topBorderText = "\x01y\x01hFile Libraries";
-	// Define the menu function for getting an item
-	fileLibMenu.GetItem = function(pIdx) {
-		var menuItemObj = this.MakeItemWithRetval(pIdx);
-		menuItemObj.text = format(this.libFormatStr,
-		                          pIdx + 1,//file_area.lib_list[pIdx].number + 1,
-								  file_area.lib_list[pIdx].description.substr(0, this.libDescLen),
-								  file_area.lib_list[pIdx].dir_list.length);
-		return menuItemObj;
-	}
-
-	return fileLibMenu;
-}
-// Helper for createFileLibMenu(): Returns the largest number of directories within the file libraries
-function getLargestNumDirsWithinFileLibs()
-{
-	var largestNumDirs = 0;
-	for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx)
-	{
-		if (file_area.lib_list[libIdx].dir_list.length > largestNumDirs)
-			largestNumDirs = file_area.lib_list[libIdx].dir_list.length;
-	}
-	return largestNumDirs;
-}
-
-// Creates the menu for choosing a file directory within a file library (for moving
-// a message to another message area).
-// The return value of the chosen item is the internal code for the file directory.
-//
-// Parameters:
-//  pLibIdx: The file directory index
-//
-// Return value: The DDLightbarMenu object for choosing a file directory within the
-// file library at the given index
-function createFileDirMenu(pLibIdx)
-{
-	if (typeof(pLibIdx) !== "number")
-		return null;
-
-	var startRow = gNumHeaderLinesDisplayed + 4;
-	// Make sure there are directories in this library
-	if (file_area.lib_list[pLibIdx].dir_list.length == 0)
-	{
-		displayMsg("There are no directories in this file library", true, true);
-		return null;
-	}
-
-	//DDLightbarMenu(pX, pY, pWidth, pHeight)
-	// Create the menu object
-	var fileDirMenu = new DDLightbarMenu(5, startRow, console.screen_columns - 10, console.screen_rows - startRow - 5);
-	fileDirMenu.scrollbarEnabled = true;
-	fileDirMenu.borderEnabled = true;
-	fileDirMenu.multiSelect = false;
-	fileDirMenu.ampersandHotkeysInItems = false;
-	fileDirMenu.wrapNavigation = false;
-
-	// Add additional keypresses for quitting the menu's input loop.
-	// Q: Quit
-	var additionalQuitKeys = "qQ";
-	fileDirMenu.AddAdditionalQuitKeys(additionalQuitKeys);
-
-	// Re-define the menu's function for getting the number of items.  This is necessary
-	// here in order for the menu's CanShowAllItemsInWindow() to work properly.
-	fileDirMenu.NumItems = function() {
-		return file_area.lib_list[this.libIdx].dir_list.length;
-	};
-
-	fileDirMenu.libIdx = pLibIdx;
-	// Construct a format string for the file libraries
-	var largestNumFiles = getLargestNumFilesInLibDirs(pLibIdx);
-	fileDirMenu.dirNumLen = file_area.lib_list[pLibIdx].dir_list.length.toString().length;
-	fileDirMenu.numFilesLen = largestNumFiles.toString().length;
-	var menuInnerWidth = fileDirMenu.size.width - 2; // Menu width excluding borders
-	// If the scrollbar will be showing, reduce the inner width by 1
-	if (fileDirMenu.scrollbarEnabled && !fileDirMenu.CanShowAllItemsInWindow())
-		--menuInnerWidth;
-	// Allow 2 for spaces
-	fileDirMenu.dirDescLen = menuInnerWidth - fileDirMenu.dirNumLen - fileDirMenu.numFilesLen - 2;
-	fileDirMenu.dirFormatStr = "%" + fileDirMenu.dirNumLen + "d %-" + fileDirMenu.dirDescLen + "s %" + fileDirMenu.numFilesLen + "d";
-
-	// Colors and their indexes
-	fileDirMenu.borderColor = gColors.fileAreaMenuBorder;
-	var dirNumStart = 0;
-	var dirNumEnd = fileDirMenu.dirNumLen;
-	var descStart = dirNumEnd;
-	var descEnd = descStart + fileDirMenu.dirDescLen;
-	var numDirsStart = descEnd;
-	//var numDirsEnd = numDirsStart + fileDirMenu.numDirsLen;
-	fileDirMenu.SetColors({
-		itemColor: [{start: dirNumStart, end: dirNumEnd, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaNum},
-		            {start: descStart, end:descEnd, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaDesc},
-		            {start: numDirsStart, end: -1, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaNumItems}],
-		selectedItemColor: [{start: dirNumStart, end: dirNumEnd, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumHighlight},
-		                    {start: descStart, end:descEnd, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaDescHighlight},
-		                    {start: numDirsStart, end: -1, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumItemsHighlight}]
-	});
-
-	fileDirMenu.topBorderText = "\x01y\x01h" + ("File directories of " + file_area.lib_list[pLibIdx].description).substr(0, fileDirMenu.size.width-2);
-	// Define the menu function for ggetting an item
-	fileDirMenu.GetItem = function(pIdx) {
-		// Return the internal code for the directory for the item
-		var menuItemObj = this.MakeItemWithRetval(file_area.lib_list[this.libIdx].dir_list[pIdx].code);
-		menuItemObj.text = format(this.dirFormatStr,
-		                          pIdx + 1,//file_area.lib_list[this.libIdx].dir_list[pIdx].number + 1,
-								  file_area.lib_list[this.libIdx].dir_list[pIdx].description.substr(0, this.dirDescLen),
-								  file_area.lib_list[this.libIdx].dir_list[pIdx].files);
-		return menuItemObj;
-	}
-
-	return fileDirMenu;
-}
-
-// Returns the largest number of files in all directories in a file library
-//
-// Parameters:
-//  pLibIdx: The index of a file library
-//
-// Return value: The largest number of files of all the directories in the file library
-function getLargestNumFilesInLibDirs(pLibIdx)
-{
-	var largestNumFiles = 0;
-	for (var dirIdx = 0; dirIdx < file_area.lib_list[pLibIdx].dir_list.length; ++dirIdx)
-	{
-		var numFilesInDir = file_area.lib_list[pLibIdx].dir_list[dirIdx].files;
-		if (numFilesInDir > largestNumFiles)
-			largestNumFiles = numFilesInDir;
-	}
-	return largestNumFiles;
-}
-
-// Returns a formatted string representation of a file size.  Tries
-// to put a size designation at the end if possible.
-//
-// Parameters:
-//  pFileSize: The size of the file in bytes
-//  pMaxLen: Optional - The maximum length of the string
-//
-// Return value: A formatted string representation of the file size
-function getFileSizeStr(pFileSize, pMaxLen)
-{
-	var fileSizeStr = "?";
-	if (typeof(pFileSize) !== "number" || pFileSize < 0)
-		return fileSizeStr;
-
-	// TODO: Improve
-	if (pFileSize >= BYTES_PER_GB) // Gigabytes
-	{
-		fileSizeStr = format("%.02fG", +(pFileSize / BYTES_PER_GB));
-		if (typeof(pMaxLen) === "number" && pMaxLen > 0 && fileSizeStr.length > pMaxLen)
-		{
-			fileSizeStr = format("%.1fG", +(pFileSize / BYTES_PER_GB));
-			if (fileSizeStr.length > pMaxLen)
-			{
-				// If there's a decimal point, then put the size designation after it
-				var dotIdx = fileSizeStr.lastIndexOf(".");
-				if (dotIdx > 0)
-				{
-					if (/\.$/.test(fileSizeStr))
-						fileSizeStr = fileSizeStr.substr(0, dotIdx) + "G";
-					else
-						fileSizeStr = fileSizeStr.substr(0, fileSizeStr.length-1) + "G";
-				}
-			}
-			fileSizeStr = fileSizeStr.substr(0, pMaxLen);
-		}
-	}
-	else if (pFileSize >= BYTES_PER_MB) // Megabytes
-	{
-		fileSizeStr = format("%.02fM", +(pFileSize / BYTES_PER_MB));
-		if (typeof(pMaxLen) === "number" && pMaxLen > 0 && fileSizeStr.length > pMaxLen)
-		{
-			fileSizeStr = format("%.1fM", +(pFileSize / BYTES_PER_MB));
-			if (fileSizeStr.length > pMaxLen)
-			{
-				// If there's a decimal point, then put the size designation after it
-				var dotIdx = fileSizeStr.lastIndexOf(".");
-				if (dotIdx > 0)
-				{
-					if (/\.$/.test(fileSizeStr))
-						fileSizeStr = fileSizeStr.substr(0, dotIdx) + "M";
-					else
-						fileSizeStr = fileSizeStr.substr(0, fileSizeStr.length-1) + "M";
-				}
-			}
-			fileSizeStr = fileSizeStr.substr(0, pMaxLen);
-		}
-	}
-	else if (pFileSize >= BYTES_PER_KB) // Kilobytes
-	{
-		fileSizeStr = format("%.02fK", +(pFileSize / BYTES_PER_KB));
-		if (typeof(pMaxLen) === "number" && pMaxLen > 0 && fileSizeStr.length > pMaxLen)
-		{
-			fileSizeStr = format("%.1fK", +(pFileSize / BYTES_PER_KB));
-			if (fileSizeStr.length > pMaxLen)
-			{
-				// If there's a decimal point, then put the size designation after it
-				var dotIdx = fileSizeStr.lastIndexOf(".");
-				if (dotIdx > 0)
-				{
-					if (/\.$/.test(fileSizeStr))
-						fileSizeStr = fileSizeStr.substr(0, dotIdx) + "K";
-					else
-						fileSizeStr = fileSizeStr.substr(0, fileSizeStr.length-1) + "K";
-				}
-			}
-			fileSizeStr = fileSizeStr.substr(0, pMaxLen);
-		}
-	}
-	else
-	{
-		fileSizeStr = pFileSize.toString();
-		if (typeof(pMaxLen) === "number" && pMaxLen > 0 && fileSizeStr.length > pMaxLen)
-			fileSizeStr = fileSizeStr.substr(0, pMaxLen);
-	}
-
-	return fileSizeStr;
-}
-
-// Displays some text with a solid horizontal line on the next line.
-//
-// Parameters:
-//  pText: The text to display
-//  pCenter: Whether or not to center the text.  Optional; defaults
-//           to false.
-//  pTextColor: The color to use for the text.  Optional; by default,
-//              normal white will be used.
-//  pLineColor: The color to use for the line underneath the text.
-//              Optional; by default, bright black will be used.
-function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor)
-{
-	var centerText = (typeof(pCenter) == "boolean" ? pCenter : false);
-	var textColor = (typeof(pTextColor) == "string" ? pTextColor : "\x01n\x01w");
-	var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\x01n\x01k\x01h");
-
-	// Output the text and a solid line on the next line.
-	if (centerText)
-	{
-		console.center(textColor + pText);
-		var solidLine = "";
-		var textLength = console.strlen(pText);
-		for (var i = 0; i < textLength; ++i)
-			solidLine += HORIZONTAL_SINGLE;
-		console.center(lineColor + solidLine);
-	}
-	else
-	{
-		console.print(textColor + pText);
-		console.crlf();
-		console.print(lineColor);
-		var textLength = console.strlen(pText);
-		for (var i = 0; i < textLength; ++i)
-			console.print(HORIZONTAL_SINGLE);
-		console.crlf();
-	}
-}
-
-// Returns a string for a number with commas added every 3 places
-//
-// Parameters:
-//  pNum: The number to format
-//
-// Return value: A string with the number formatted with commas every 3 places
-function numWithCommas(pNum)
-{
-	var numStr = "";
-	if (typeof(pNum) === "number")
-		numStr = pNum.toString();
-	else if (typeof(pNum) === "string")
-		numStr = pNum;
-	else
-		return "";
-
-	// Check for a decimal point in the number
-	var afterDotSuffix = "";
-	var dotIdx = numStr.lastIndexOf(".");
-	if (dotIdx > -1)
-	{
-		afterDotSuffix = numStr.substr(dotIdx+1);
-		numStr = numStr.substr(0, dotIdx);
-	}
-	// First, build an array containing sections of the number containing
-	// 3 digits each (the last may contain less than 3)
-	var numParts = [];
-	var i = numStr.length - 1;
-	var continueOn = true;
-	while (continueOn/*i >= 0*/)
-	{
-		if (i >= 3)
-		{
-			numParts.push(numStr.substr(i-2, 3));
-			i -= 3;
-		}
-		else
-		{
-			numParts.push(numStr.substr(0, i+1));
-			i -= i;
-			continueOn = false;
-		}
-	}
-	// Reverse the array so the sections of digits are in forward order
-	numParts.reverse();
-	// Re-build the number string with commas
-	numStr = "";
-	for (var i = 0; i < numParts.length; ++i)
-		numStr += numParts[i] + ",";
-	if (/,$/.test(numStr))
-		numStr = numStr.substr(0, numStr.length-1);
-	// Append back the value after the decimal place if there was one
-	if (afterDotSuffix.length > 0)
-		numStr += "." + afterDotSuffix;
-	return numStr;
-}
-
-// Displays a set of messages at the status location on the screen, along with a border.  Then
-// refreshes the area of the screen to erase the message box.
-//
-// Parameters:
-//  pMsgArray: An array containing the messages to display
-//  pIsError: Boolean - Whether or not the messages are an error
-//  pWaitAndErase: Optional boolean - Whether or not to automatically wait a moment and then
-//                 erase the message box after drawing.  Defaults to true.
-function displayMsgs(pMsgArray, pIsError, pWaitAndErase)
-{
-	if (typeof(pMsgArray) !== "object" || pMsgArray.length == 0)
-		return;
-
-	var waitAndErase = (typeof(pWaitAndErase) === "boolean" ? pWaitAndErase : true);
-
-	// Draw the box border, then write the messages
-	var title = pIsError ? "Error" : "Message";
-	var titleColor = pIsError ? gColors.errorMessage : gColors.successMessage;
-	drawBorder(gErrorMsgBoxULX, gErrorMsgBoxULY, gErrorMsgBoxWidth, gErrorMsgBoxHeight,
-	           gColors.errorBoxBorder, "single", title, titleColor, "");
-	var msgColor = "\x01n" + (pIsError ? gColors.errorMessage : gColors.successMessage);
-	var innerWidth = gErrorMsgBoxWidth - 2;
-	var msgFormatStr = msgColor + "%-" + innerWidth + "s\x01n";
-	for (var i = 0; i < pMsgArray.length; ++i)
-	{
-		console.gotoxy(gErrorMsgBoxULX+1, gErrorMsgBoxULY+1);
-		printf(msgFormatStr, pMsgArray[i].substr(0, innerWidth));
-		if (waitAndErase)
-		{
-			// Wait for the error wait duration
-			mswait(gErrorMsgWaitMS);
-		}
-	}
-	if (waitAndErase)
-		eraseMsgBoxScreenArea();
-}
-function displayMsg(pMsg, pIsError, pWaitAndErase)
-{
-	if (typeof(pMsg) !== "string")
-		return;
-	displayMsgs([ pMsg ], pIsError, pWaitAndErase);
-}
-// Erases the message box screen area by re-drawing the necessary components
-function eraseMsgBoxScreenArea()
-{
-	// Refresh the list header line and have the file list menu refresh itself over
-	// the error message window
-	displayListHdrLine(true);
-	// This used to call gFileListMenu.DrawPartialAbs
-	refreshScreenMainContent(gErrorMsgBoxULX, gErrorMsgBoxULY+1, gErrorMsgBoxWidth, gErrorMsgBoxHeight-2);
-}
-
-// Draws a border
-//
-// Parameters:
-//  pX: The X location of the upper left corner
-//  pY: The Y location of the upper left corner
-//  pWidth: The width of the box
-//  pHeight: The height of the box
-//  pColor: A string containing color/attribute codes for the border characters
-//  pLineStyle: A string specifying the border character style, either "single" or "double"
-//  pTitle: Optional - A string specifying title text for the top border
-//  pTitleColor: Optional - Attribute codes for the color to use for the title text
-//  pBottomBorderText: Optional - A string specifying text to include in the bottom border
-function drawBorder(pX, pY, pWidth, pHeight, pColor, pLineStyle, pTitle, pTitleColor, pBottomBorderText)
-{
-	if (typeof(pX) !== "number" || typeof(pY) !== "number" || typeof(pWidth) !== "number" || typeof(pHeight) !== "number")
-		return;
-	if (typeof(pColor) !== "string")
-		return;
-
-	var borderChars = {
-		UL: UPPER_LEFT_SINGLE,
-		UR: UPPER_RIGHT_SINGLE,
-		LL: LOWER_LEFT_SINGLE,
-		LR: LOWER_RIGHT_SINGLE,
-		preText: RIGHT_T_SINGLE,
-		postText: LEFT_T_SINGLE,
-		horiz: HORIZONTAL_SINGLE,
-		vert: VERTICAL_SINGLE
-	};
-	if (typeof(pLineStyle) === "string" && pLineStyle.toUpperCase() == "DOUBLE")
-	{
-		borderChars.UL = UPPER_LEFT_DOUBLE;
-		borderChars.UR = UPPER_RIGHT_DOUBLE;
-		borderChars.LL = LOWER_LEFT_DOUBLE;
-		borderChars.LR = LOWER_RIGHT_DOUBLE;
-		borderChars.preText = RIGHT_T_DOUBLE;
-		borderChars.postText = LEFT_T_DOUBLE
-		borderChars.horiz = HORIZONTAL_DOUBLE;
-		borderChars.vert = VERTICAL_DOUBLE;
-	}
-
-	// Top border
-	console.gotoxy(pX, pY);
-	console.print("\x01n" + pColor);
-	console.print(borderChars.UL);
-	var innerWidth = pWidth - 2;
-	// Include the title text in the top border, if there is any specified
-	var titleTextWithoutAttrs = strip_ctrl(pTitle); // Possibly used twice, so only call strip_ctrl() once
-	if (typeof(pTitle) === "string" && titleTextWithoutAttrs.length > 0)
-	{
-		var titleLen = strip_ctrl(pTitle).length;
-		if (titleLen > pWidth - 4)
-			titleLen = pWidth - 4;
-		innerWidth -= titleLen;
-		innerWidth -= 2; // ?? Correctional
-		// Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js
-		var titleText = pTitle;
-		if (typeof(pTitleColor) === "string")
-			titleText = "\x01n" + pTitleColor + titleTextWithoutAttrs;
-		console.print(borderChars.preText + "\x01n" + substrWithAttrCodes(titleText, 0, titleLen) +
-		              "\x01n" + pColor + borderChars.postText);
-		if (innerWidth > 0)
-			console.print(pColor);
-	}
-	for (var i = 0; i < innerWidth; ++i)
-		console.print(borderChars.horiz);
-	console.print(borderChars.UR);
-	// Side borders
-	var rightCol = pX + pWidth - 1;
-	var endScreenRow = pY + pHeight - 1;
-	for (var screenRow = pY + 1; screenRow < endScreenRow; ++screenRow)
-	{
-		console.gotoxy(pX, screenRow);
-		console.print(borderChars.vert);
-		console.gotoxy(rightCol, screenRow);
-		console.print(borderChars.vert);
-	}
-	// Bottom border
-	console.gotoxy(pX, endScreenRow);
-	console.print(borderChars.LL);
-	innerWidth = pWidth - 2;
-	// Include the bottom border text in the top border, if there is any specified
-	if (typeof(pBottomBorderText) === "string" && pBottomBorderText.length > 0)
-	{
-		var textLen = strip_ctrl(pBottomBorderText).length;
-		if (textLen > pWidth - 4)
-			textLen = pWidth - 4;
-		innerWidth -= textLen;
-		innerWidth -= 2; // ?? Correctional
-		// Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js
-		console.print(borderChars.preText + "\x01n" + substrWithAttrCodes(pBottomBorderText, 0, textLen) +
-		              "\x01n" + pColor + borderChars.postText);
-		if (innerWidth > 0)
-			console.print(pColor);
-	}
-	for (var i = 0; i < innerWidth; ++i)
-		console.print(borderChars.horiz);
-	console.print(borderChars.LR);
-}
-
-// Draws a horizontal separator line on the screen, in high green
-//
-// Parameters:
-//  pX: The X coordinate to start at
-//  pY: The Y coordinate to start at
-//  pWidth: The width of the line to draw
-function drawSeparatorLine(pX, pY, pWidth)
-{
-	if (typeof(pX) !== "number" || typeof(pY) !== "number" || typeof(pWidth) !== "number")
-		return;
-	if (pX < 1 || pX > console.screen_columns)
-		return;
-	if (pY < 1 || pY > console.screen_rows)
-		return;
-	if (pWidth < 1)
-		return;
-
-	var width = pWidth;
-	var maxWidth = console.screen_columns - pX + 1;
-	if (width > maxWidth)
-		width = maxWidth;
-
-	console.gotoxy(pX, pY);
-	console.print("\x01n\x01g\x01h");
-	for (var i = 0; i < width; ++i)
-		console.print(HORIZONTAL_SINGLE);
-	console.print("\x01n");
-}
-
-// Confirms with the user to perform an action with a file or set of files
-//
-// Parameters:
-//  pFilenames: An array of filenames (as strings), or a string containing a filename
-//  pActionName: String - The name of the action to confirm
-//  pDefaultYes: Boolean - True if the default is to be yes, or false if no
-//
-// Return value: Boolean - True if the user confirmed, or false if not
-function confirmFileActionWithUser(pFilenames, pActionName, pDefaultYes)
-{
-	if (typeof(pFilenames) !== "object" && typeof(pFilenames) !== "string")
-		return false;
-	if (typeof(pActionName) !== "string")
-		return false;
-
-	var actionConfirmed = false;
-
-	var numFilenames = 1;
-	if (typeof(pFilenames) === "object")
-		numFilenames = pFilenames.length;
-	if (numFilenames < 1)
-		return false;
-	// If there is only 1 filename, then prompt the user near the bottom of the screen
-	else if (numFilenames == 1)
-	{
-		var filename = (typeof(pFilenames) === "string" ? pFilenames : pFilenames[0]);
-		drawSeparatorLine(1, console.screen_rows-2, console.screen_columns-1);
-		console.gotoxy(1, console.screen_rows-1);
-		console.cleartoeol("\x01n");
-		console.gotoxy(1, console.screen_rows-1);
-		var shortFilename = shortenFilename(filename, Math.floor(console.screen_columns/2), false);
-		if (pDefaultYes)
-			actionConfirmed = console.yesno(pActionName + " " + shortFilename);
-		else
-			actionConfirmed = !console.noyes(pActionName + " " + shortFilename);
-		// Refresh the main screen content, to erase the confirmation prompt
-		refreshScreenMainContent(1, console.screen_rows-2, console.screen_columns, 2, true);
-	}
-	else
-	{
-		// Construct & draw a frame with the file list & display the frame to confirm with the
-		// user to delete the files
-		var frameUpperLeftX = gFileListMenu.pos.x + 2;
-		var frameUpperLeftY = gFileListMenu.pos.y + 2;
-		//var frameWidth = gFileListMenu.size.width - 4;
-		var frameWidth = console.screen_columns - 4;
-		var frameHeight = 10;
-		var frameTitle = pActionName + " files? (Y/N)";
-		var additionalQuitKeys = "yYnN";
-		var frameInnerWidth = frameWidth - 2; // Without borders; for filename lengths
-		var fileListStr = "\x01n\x01w";
-		for (var i = 0; i < pFilenames.length; ++i)
-			fileListStr += shortenFilename(pFilenames[i], frameInnerWidth, false) + "\r\n";
-		var lastUserInput = displayBorderedFrameAndDoInputLoop(frameUpperLeftX, frameUpperLeftY, frameWidth,
-		                                                       frameHeight, gColors.confirmFileActionWindowBorder,
-		                                                       frameTitle, gColors.confirmFileActionWindowWindowTitle,
-		                                                       fileListStr, additionalQuitKeys);
-		actionConfirmed = (lastUserInput.toUpperCase() == "Y");
-		// Refresh the main screen content, to erase the confirmation window
-		refreshScreenMainContent(frameUpperLeftX, frameUpperLeftY, frameWidth, frameHeight, true);
-	}
-
-	return actionConfirmed;
-}
-
-
-// Reads the configuration file and sets the settings accordingly
-function readConfigFile()
-{
-	this.cfgFileSuccessfullyRead = false;
-
-	var themeFilename = ""; // In case a theme filename is specified
-
-	// Determine the script's startup directory.
-	// This code is a trick that was created by Deuce, suggested by Rob Swindell
-	// as a way to detect which directory the script was executed in.  I've
-	// shortened the code a little.
-	var startupPath = '.';
-	try { throw dig.dist(dist); } catch(e) { startupPath = e.fileName; }
-	startupPath = backslash(startupPath.replace(/[\/\\][^\/\\]*$/,''));
-
-	// Open the main configuration file.  First look for it in the sbbs/mods
-	// directory, then sbbs/ctrl, then in the same directory as this script.
-	var cfgFilename = "ddfilelister.cfg";
-	var cfgFilenameFullPath = file_cfgname(system.mods_dir, cfgFilename);
-	if (!file_exists(cfgFilenameFullPath))
-		cfgFilenameFullPath = file_cfgname(system.ctrl_dir, cfgFilename);
-	if (!file_exists(cfgFilenameFullPath))
-		cfgFilenameFullPath = file_cfgname(startupPath, cfgFilename);
-	var cfgFile = new File(cfgFilenameFullPath);
-	if (cfgFile.open("r"))
-	{
-		this.cfgFileSuccessfullyRead = true;
-
-		var fileLine = null;     // A line read from the file
-		var equalsPos = 0;       // Position of a = in the line
-		var commentPos = 0;      // Position of the start of a comment
-		var setting = null;      // A setting name (string)
-		var settingUpper = null; // Upper-case setting name
-		var value = null;        // To store a value for a setting (string)
-		while (!cfgFile.eof)
-		{
-			// Read the next line from the config file.
-			fileLine = cfgFile.readln(2048);
-
-			// fileLine should be a string, but I've seen some cases
-			// where it isn't, so check its type.
-			if (typeof(fileLine) != "string")
-				continue;
-
-			// If the line starts with with a semicolon (the comment
-			// character) or is blank, then skip it.
-			if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
-				continue;
-
-			// If the line has a semicolon anywhere in it, then remove
-			// everything from the semicolon onward.
-			commentPos = fileLine.indexOf(";");
-			if (commentPos > -1)
-				fileLine = fileLine.substr(0, commentPos);
-
-			// Look for an equals sign, and if found, separate the line
-			// into the setting name (before the =) and the value (after the
-			// equals sign).
-			equalsPos = fileLine.indexOf("=");
-			if (equalsPos > 0)
-			{
-				// Read the setting & value, and trim leading & trailing spaces.
-				setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true);
-				settingUpper = setting.toUpperCase();
-				value = trimSpaces(fileLine.substr(equalsPos+1), true, false, true);
-				var valueUpper = value.toUpperCase();
-
-				// Set the appropriate valueUpper in the settings object.
-				if (settingUpper == "SORTORDER")
-				{
-					// FileBase.SORT properties
-					// Name		Type	Description
-					// NATURAL	number	Natural sort order (same as DATE_A)
-					// NAME_AI	number	Filename ascending, case insensitive sort order
-					// NAME_DI	number	Filename descending, case insensitive sort order
-					// NAME_AS	number	Filename ascending, case sensitive sort order
-					// NAME_DS	number	Filename descending, case sensitive sort order
-					// DATE_A	number	Import date/time ascending sort order
-					// DATE_D	number	Import date/time descending sort order
-					if (valueUpper == "NATURAL")
-						gFileSortOrder = FileBase.SORT.NATURAL;
-					else if (valueUpper == "NAME_AI")
-						gFileSortOrder = FileBase.SORT.NAME_AI;
-					else if (valueUpper == "NAME_DI")
-						gFileSortOrder = FileBase.SORT.NAME_DI;
-					else if (valueUpper == "NAME_AS")
-						gFileSortOrder = FileBase.SORT.NAME_AS;
-					else if (valueUpper == "NAME_DS")
-						gFileSortOrder = FileBase.SORT.NAME_DS;
-					else if (valueUpper == "DATE_A")
-						gFileSortOrder = FileBase.SORT.DATE_A;
-					else if (valueUpper == "DATE_D")
-						gFileSortOrder = FileBase.SORT.DATE_D;
-					else if (valueUpper == "ULTIME")
-						gFileSortOrder = SORT_FL_ULTIME;
-					else if (valueUpper == "DLTIME")
-						gFileSortOrder = SORT_FL_DLTIME;
-					else // Default
-						gFileSortOrder = FileBase.SORT.NATURAL;
-				}
-				else if (settingUpper == "PAUSEAFTERVIEWINGFILE")
-					gPauseAfterViewingFile = (value.toUpperCase() == "TRUE");
-				else if (settingUpper == "THEMEFILENAME")
-				{
-					// First look for the theme config file in the sbbs/mods
-					// directory, then sbbs/ctrl, then the same directory as
-					// this script.
-					themeFilename = system.mods_dir + value;
-					if (!file_exists(themeFilename))
-						themeFilename = system.ctrl_dir + value;
-					if (!file_exists(themeFilename))
-						themeFilename = startupPath + value;
-				}
-			}
-		}
-
-		cfgFile.close();
-	}
-	else
-	{
-		// Was unable to read the configuration file.  Output a warning to the user
-		// that defaults will be used and to notify the sysop.
-		console.print("\x01n");
-		console.crlf();
-		console.print("\x01w\x01hUnable to open the configuration file: \x01y" + cfgFilename);
-		console.crlf();
-		console.print("\x01wDefault settings will be used.  Please notify the sysop.");
-		mswait(2000);
-	}
-	
-	// If a theme filename was specified, then read the colors & strings
-	// from it.
-	if (themeFilename.length > 0)
-	{
-		var themeFile = new File(themeFilename);
-		if (themeFile.open("r"))
-		{
-			var fileLine = null;     // A line read from the file
-			var equalsPos = 0;       // Position of a = in the line
-			var commentPos = 0;      // Position of the start of a comment
-			var setting = null;      // A setting name (string)
-			var value = null;        // To store a value for a setting (string)
-			while (!themeFile.eof)
-			{
-				// Read the next line from the config file.
-				fileLine = themeFile.readln(2048);
-
-				// fileLine should be a string, but I've seen some cases
-				// where it isn't, so check its type.
-				if (typeof(fileLine) != "string")
-					continue;
-
-				// If the line starts with with a semicolon (the comment
-				// character) or is blank, then skip it.
-				if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
-					continue;
-
-				// If the line has a semicolon anywhere in it, then remove
-				// everything from the semicolon onward.
-				commentPos = fileLine.indexOf(";");
-				if (commentPos > -1)
-					fileLine = fileLine.substr(0, commentPos);
-
-				// Look for an equals sign, and if found, separate the line
-				// into the setting name (before the =) and the value (after the
-				// equals sign).
-				equalsPos = fileLine.indexOf("=");
-				if (equalsPos > 0)
-				{
-					// Read the setting (without leading/trailing spaces) & value
-					setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true);
-					value = fileLine.substr(equalsPos+1);
-
-					if (gColors.hasOwnProperty(setting))
-					{
-						// Trim leading & trailing spaces from the value when
-						// setting a color.  Also, replace any instances of "\x01" or "\1"
-						// with the Synchronet attribute control character.
-						gColors[setting] = trimSpaces(value, true, false, true).replace(/\\[xX]01/g, "\x01").replace(/\\1/g, "\x01");
-					}
-				}
-			}
-
-			themeFile.close();
-		}
-		else
-		{
-			// Was unable to read the theme file.  Output a warning to the user
-			// that defaults will be used and to notify the sysop.
-			this.cfgFileSuccessfullyRead = false;
-			console.print("\x01n");
-			console.crlf();
-			console.print("\x01w\x01hUnable to open the theme file: \x01y" + themeFilename);
-			console.crlf();
-			console.print("\x01wDefault colors will be used.  Please notify the sysop.");
-			mswait(2000);
-		}
-	}
-}
-
-// Removes multiple, leading, and/or trailing spaces
-// The search & replace regular expressions used in this
-// function came from the following URL:
-//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
-//
-// Parameters:
-//  pString: The string to trim
-//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
-//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
-//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
-//
-// Return value: The string with whitespace trimmed
-function trimSpaces(pString, pLeading, pMultiple, pTrailing)
-{
-	if (typeof(pString) !== "string")
-		return "";
-
-	var leading = (typeof(pLeading) === "boolean" ? pLeading : true);
-	var multiple = (typeof(pMultiple) === "boolean" ? pMultiple : true);
-	var trailing = (typeof(pTrailing) === "boolean" ? pTrailing : true);
-
-	// To remove both leading & trailing spaces:
-	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");
-
-	var string = pString;
-	if (leading)
-		string = string.replace(/(^\s*)/gi,"");
-	if (multiple)
-		string = string.replace(/[ ]{2,}/gi," ");
-	if (trailing)
-		string = string.replace(/(\s*$)/gi,"");
-
-	return string;
-}
-
-// Given a filename, this returns a filename shortened to a maximum length, preserving the
-// filename extension.
-//
-// Parameters:
-//  pFilename: The filename to shorten
-//  pMaxLen: The maximum length of the shortened filename
-//  pFillWidth: Boolean - Whether or not to fill the width specified by pMaxLen
-//
-// Return value: A string with the shortened filename
-function shortenFilename(pFilename, pMaxLen, pFillWidth)
-{
-	if (typeof(pFilename) !== "string")
-		return "";
-	if (typeof(pMaxLen) !== "number" || pMaxLen < 1)
-		return "";
-
-	var shortenedFilename = ""; // Will contain the shortened filename
-	// Get the filename extension.  And the way we shorten the filename
-	// will depend on whether the filename actually has an extension or not.
-	var filenameExt = file_getext(pFilename);
-	if (typeof(filenameExt) === "string")
-	{
-		var filenameWithoutExt = file_getname(pFilename);
-		var extIdx = filenameWithoutExt.indexOf(filenameExt);
-		if (extIdx >= 0)
-			filenameWithoutExt = filenameWithoutExt.substr(0, extIdx);
-
-		var maxWithoutExtLen = pMaxLen - filenameExt.length;
-		if (filenameWithoutExt.length > maxWithoutExtLen)
-			filenameWithoutExt = filenameWithoutExt.substr(0, maxWithoutExtLen);
-
-		var fillWidth = (typeof(pFillWidth) === "boolean" ? pFillWidth : false);
-		if (fillWidth)
-			shortenedFilename = format("%-" + maxWithoutExtLen + "s%s", filenameWithoutExt, filenameExt);
-		else
-			shortenedFilename = filenameWithoutExt + filenameExt;
-	}
-	else
-		shortenedFilename = pFilename.substr(0, pMaxLen);
-	return shortenedFilename;
-}
-
-
-// Parses command-line arguments, where each argument in the given array is in
-// the format -arg=val.  If the value is the string "true" or "false", then the
-// value will be a boolean.  Otherwise, the value will be a string.
-//
-// Parameters:
-//  argv: An array of strings containing values in the format -arg=val
-//
-// Return value: Boolean: Whether or not this script was run as a loadable
-//                        module (depending on the arguments used).
-function parseArgs(argv)
-{
-	// Default program options
-	gScriptMode = MODE_LIST_DIR;
-
-	// Sanity checking for argv - Make sure it's an array
-	if ((typeof(argv) != "object") || (typeof(argv.length) != "number"))
-		return false;
-
-	// This script accepts arguments in -arg=val format; See if there are any of those.
-	// If so, process arguments with that assumption; otherwise, we'll check for args
-	// passed when Synchronet runs this as a loadable module.
-	var scriptRanAsLoadableModule = false;
-	var anySettingEqualsVal = false;
-	for (var i = 0; i < argv.length && !anySettingEqualsVal; ++i)
-		anySettingEqualsVal = (typeof(argv[i]) === "string" && argv[i].length > 0 && argv[i].charAt(0) == "-" && argv[i].indexOf("=") > 1);
-	if (anySettingEqualsVal)
-	{
-		// Go through argv looking for strings in the format -arg=val and parse them
-		// into objects in the argVals array.
-		var equalsIdx = 0;
-		var argName = "";
-		var argVal = "";
-		var argValUpper = ""; // For case-insensitive matching
-		for (var i = 0; i < argv.length; ++i)
-		{
-			// We're looking for strings that start with "-", except strings that are
-			// only "-".
-			if (typeof(argv[i]) !== "string" || argv[i].length == 0 || argv[i].charAt(0) != "-" || argv[i] == "-")
-				continue;
-
-			// Look for an = and if found, split the string on the =
-			equalsIdx = argv[i].indexOf("=");
-			// If a = is found, then split on it and add the argument name & value
-			// to the array.  Otherwise (if the = is not found), then treat the
-			// argument as a boolean and set it to true (to enable an option).
-			if (equalsIdx > -1)
-			{
-				argName = argv[i].substring(1, equalsIdx).toUpperCase();
-				argVal = argv[i].substr(equalsIdx+1);
-				argValUpper = argVal.toUpperCase();
-				if (argName === "MODE")
-				{
-					if (argValUpper === "SEARCH_FILENAME")
-						gScriptMode = MODE_SEARCH_FILENAME;
-					else if (argValUpper === "SEARCH_DESCRIPTION")
-						gScriptMode = MODE_SEARCH_DESCRIPTION;
-					else if (argValUpper === "NEW_FILE_SEARCH")
-						gScriptMode = MODE_NEW_FILE_SEARCH;
-					else if (argValUpper === "LIST_CURDIR")
-						gScriptMode = MODE_LIST_DIR;
-				}
-			}
-			// Nothing to do if an = was not found
-		}
-	}
-	else
-	{
-		// Check for arguments as if this was run by Synchronet as a loadable module
-		// (for either Scan Dirs or List Files)
-
-		/*
-		// bbs.list_files() & bbs.scan_dirs()
-		//********************************************
-		var FL_NONE			=0;			// No special behavior
-		var FL_ULTIME		=(1<<0);	// List files by upload time
-		var FL_DLTIME		=(1<<1);	// List files by download time
-		var FL_NO_HDR		=(1<<2);	// Don't list directory header
-		var FL_FINDDESC		=(1<<3);	// Find text in description
-		var FL_EXFIND		=(1<<4);	// Find text in description - extended info
-		var FL_VIEW			=(1<<5);	// View ZIP/ARC/GIF etc. info
-		*/
-
-		// There must be either 2 or 3 arguments
-		if (argv.length < 2)
-			return false;
-		// The 2nd argument is the mode/behavior bits in either case
-		var FLBehavior = parseInt(argv[1]);
-		if (isNaN(FLBehavior))
-			return false;
-		else
-			gListBehavior = FLBehavior;
-		scriptRanAsLoadableModule = true;
-
-		// Default gScriptmode to MODE_LIST_DIR; for FLBehavior as FL_NONE, no special behavior
-		gScriptMode = MODE_LIST_DIR;
-
-		// 2 args - Scanning/searching
-		if (argv.length == 2)
-		{
-			// - 0: Bool (scanning all directories): 0/1
-			// - 1: FL_ mode value
-			gScanAllDirs = (argv[0] == "1");
-			if ((FLBehavior & FL_ULTIME) == FL_ULTIME)
-				gScriptMode = MODE_NEW_FILE_SEARCH;
-			else if ((FLBehavior & FL_FINDDESC) == FL_FINDDESC || (FLBehavior & FL_EXFIND) == FL_EXFIND)
-				gScriptMode = MODE_SEARCH_DESCRIPTION;
-			if ((FLBehavior & FL_VIEW) == FL_VIEW)
-			{
-				// View ZIP/ARC/GIF etc. info
-				// TODO: Not sure what to do with this
-			}
-		}
-		// 3 args - Listing
-		else if (argv.length >= 3) //==3
-		{
-			// - 0: Directory internal code
-			// - 1: FL_ mode value
-			// - 2: Filespec (i.e., *, *.zip, etc.)
-			if (!file_area.dir.hasOwnProperty(argv[0]))
-				return false;
-			gDirCode = argv[0];
-			gFilespec = argv[2];
-			if ((FLBehavior & FL_VIEW) == FL_VIEW)
-			{
-				// View ZIP/ARC/GIF etc. info
-				// TODO: Not sure what to do with this
-			}
-		}
-
-		// Options that apply to both searching and listing
-		if ((FLBehavior & FL_ULTIME) == FL_ULTIME)
-			gFileSortOrder = SORT_FL_ULTIME;
-		else if ((FLBehavior & FL_DLTIME) == FL_DLTIME)
-			gFileSortOrder = SORT_FL_DLTIME;
-
-	}
-
-	return scriptRanAsLoadableModule;
-}
-
-// Populates the file list (gFileList).
-//
-// Parameters:
-//  pSearchMode: The search mode
-//
-// Return value: An obmect with the following properties:
-//               exitNow: Boolean: Whether or not this script should exit after calling this function
-//               exitCode: The exit code to use if needing to exit after calling this function
-function populateFileList(pSearchMode)
-{
-	var retObj = {
-		exitNow: false,
-		exitCode: 0
-	};
-	
-	var dirErrors = [];
-	var allSameDir = true;
-
-	// Do the things for list or search, depending on the specified mode
-	if (pSearchMode == MODE_LIST_DIR) // This is the default
-	{
-		var filebase = new FileBase(gDirCode);
-		if (filebase.open())
-		{
-			// If there are no files in the filebase, then say so and exit now.
-			if (filebase.files == 0)
-			{
-				filebase.close();
-				var libIdx = file_area.dir[gDirCode].lib_index;
-				console.crlf();
-				console.print("\x01n\x01cThere are no files in \x01h" + file_area.lib_list[libIdx].description + "\x01n\x01c - \x01h" +
-							  file_area.dir[gDirCode].description + "\x01n");
-				console.crlf();
-				console.pause();
-				retObj.exitNow = true;
-				retObj.exitCode = 0;
-				return retObj;
-			}
-
-			// Get a list of file data
-			var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM);
-			if (gFileSortOrder == SORT_FL_ULTIME || gFileSortOrder == SORT_FL_DLTIME)
-			{
-				gFileList = filebase.get_list(gFilespec, fileDetail, 0, true, FileBase.SORT.NATURAL);
-				if (gFileSortOrder == SORT_FL_ULTIME)
-					gFileList.sort(fileInfoSortULTime);
-				else if (gFileSortOrder == SORT_FL_DLTIME)
-					gFileList.sort(fileInfoSortDLTime);
-			}
-			else
-				gFileList = filebase.get_list(gFilespec, fileDetail, 0, true, gFileSortOrder);
-			filebase.close();
-			// Add a dirCode property to the file metadata objects (for consistency,
-			// as file search results may contain files from multiple directories).
-			// Also, if the metadata objects have an extdesc, remove any trailing CRLF
-			// from the end.
-			for (var i = 0; i < gFileList.length; ++i)
-			{
-				gFileList[i].dirCode = gDirCode;
-				if (gFileList[i].hasOwnProperty("extdesc") && /\r\n$/.test(gFileList[i].extdesc))
-				{
-					gFileList[i].extdesc = gFileList[i].extdesc.substr(0, gFileList[i].extdesc.length-2);
-					// Fix line endings if necessary
-					gFileList[i].extdesc = lfexpand(gFileList[i].extdesc);
-				}
-			}
-		}
-		else
-		{
-			console.crlf();
-			console.print("\x01n\x01h\x01yUnable to open \x01w" + file_area.dir[gDirCode].description + "\x01n");
-			console.crlf();
-			console.pause();
-			retObj.exitNow = true;
-			retObj.exitCode = 1;
-			return retObj;
-		}
-	}
-	else if (pSearchMode == MODE_SEARCH_FILENAME)
-	{
-		var lastDirCode = "";
-
-		// If not searching all already, prompt the user for directory, library, or all
-		var validInputOptions = "DLA";
-		var userInputDLA = "";
-		if (gScanAllDirs)
-			userInputDLA = "A";
-		else
-		{
-			console.attributes = "N";
-			console.crlf();
-			console.mnemonics(bbs.text(DirLibOrAll));
-			userInputDLA = console.getkeys(validInputOptions, -1, K_UPPER);
-		}
-		var userFilespec = "";
-		if (userInputDLA.length > 0 && validInputOptions.indexOf(userInputDLA) > -1)
-		{
-			// Prompt the user for a filespec to search for
-			console.mnemonics(bbs.text(FileSpecStarDotStar));
-			userFilespec = console.getstr();
-			if (userFilespec.length == 0)
-				userFilespec = "*"; // Should this be *.*?
-			console.print("Searching....");
-		}
-		var searchRetObj = searchDirGroupOrAll(userInputDLA, function(pDirCode) {
-			return searchDirWithFilespec(pDirCode, userFilespec);
-		});
-		allSameDir = searchRetObj.allSameDir;
-		for (var i = 0; i < searchRetObj.errors.length; ++i)
-			dirErrors.push(searchRetObj.errors[i]);
-	}
-	else if (pSearchMode == MODE_SEARCH_DESCRIPTION)
-	{
-		var lastDirCode = "";
-
-		// If not saerching all already, prompt the user for directory, library, or all
-		var validInputOptions = "DLA";
-		var userInputDLA = "";
-		if (gScanAllDirs)
-			userInputDLA = "A";
-		else
-		{
-			console.print("\x01n");
-			console.crlf();
-			//console.print("\r\n\x01c\x01hFind Text in File Descriptions (no wildcards)\x01n\r\n");
-			console.mnemonics(bbs.text(DirLibOrAll));
-			console.print("\x01n");
-			userInputDLA = console.getkeys(validInputOptions, -1, K_UPPER);
-		}
-		var searchDescription = "";
-		if (userInputDLA.length > 0 && validInputOptions.indexOf(userInputDLA) > -1)
-		{
-			// Prompt the user for a description to search for
-			console.mnemonics(bbs.text(SearchStringPrompt));
-			searchDescription = console.getstr(40, K_LINE|K_UPPER);
-			if (searchDescription.length > 0)
-				console.print("Searching....");
-			else
-			{
-				retObj.exitNow = true;
-				retObj.exitCode = 0;
-				return retObj;
-			}
-		}
-		var searchRetObj = searchDirGroupOrAll(userInputDLA, function(pDirCode) {
-			return searchDirWithDescUpper(pDirCode, searchDescription);
-		});
-		allSameDir = searchRetObj.allSameDir;
-		for (var i = 0; i < searchRetObj.errors.length; ++i)
-			dirErrors.push(searchRetObj.errors[i]);
-	}
-	else if (pSearchMode == MODE_NEW_FILE_SEARCH)
-	{
-		// New file search
-		var lastDirCode = "";
-
-		// 2022-02-011 - Digital Man said:
-		/*
-		Upon logon to the terminal server, bbs.new_file_time and bbs.last_new_file_time
-		are set to the current user.new_file_time value.
-
-		A new file scan displays files uploaded (added/imported) since the current
-		bbs.new_file_time value. The bbs.new_file_time value can be manipulated by the
-		user, e.g. they want to review files that have been uploaded over the past
-		month or whatever.
-
-		When the user executes a new file scan, bbs.last_new_file_time is updated to
-		the current time (this happens automatically when using the built-in file
-		listing logic). The bbs.last_new_file_time isn't actually used for anything
-		until the user logs-off and its value is then copied to the user.new_file_time
-		to be used for the next logon (this copy/sync of the last_new_file_time ->
-		user.new_file_time is built-in to the BBS's logout logci and nothing a script
-		would need to do).
-
-		This scheme insures that a user will never "miss" the display of new files on
-		the BBS and that they would not normally see the same/repeat "new" files
-		between successive sessions.
-		*/
-
-		// If not searching all dirs already, prompt the user for directory, library, or all
-		var userInputDLA = "";
-		if (gScanAllDirs)
-			userInputDLA = "A";
-		else
-		{
-			console.print("\x01n");
-			console.crlf();
-			console.mnemonics(bbs.text(DirLibOrAll));
-			var validInputOptions = "DLA";
-			userInputDLA = console.getkeys(validInputOptions, -1, K_UPPER);
-			console.print("\x01n");
-			console.crlf();
-		}
-		if (userInputDLA == "D" || userInputDLA == "L" || userInputDLA == "A")
-		{
-			console.print("\x01n\x01cSearching for files uploaded after \x01h" + system.timestr(bbs.new_file_time) + "\x01n");
-			console.crlf();
-		}
-		var searchRetObj = searchDirGroupOrAll(userInputDLA, function(pDirCode) {
-			return searchDirNewFiles(pDirCode, bbs.new_file_time);
-		});
-		// Now bbs.last_new_file_time needs to be updated with the current time
-		bbs.last_new_file_time = time();
-		// user.new_file_time should be updated with the value of bbs.last_new_file_time
-		// when the user logs off.
-		allSameDir = searchRetObj.allSameDir;
-		for (var i = 0; i < searchRetObj.errors.length; ++i)
-			dirErrors.push(searchRetObj.errors[i]);
-	}
-	else
-	{
-		retObj.exitNow = true;
-		retObj.exitCode = 1;
-		return retObj;
-	}
-
-	if (pSearchMode != MODE_LIST_DIR)
-		gFileList.allSameDir = allSameDir;
-
-	if (dirErrors.length > 0)
-	{
-		console.print("\x01n\x01y\x01h");
-		for (var i = 0; i < dirErrors.length; ++i)
-		{
-			console.print(dirErrors[i]);
-			console.crlf();
-		}
-		console.print("\x01n");
-		console.pause();
-		retObj.exitNow = true;
-		retObj.exitCode = 1;
-		return retObj;
-	}
-
-	// Figure out the longest filename in the list.
-	var longestFilenameLen = 0;
-	for (var i = 0; i < gFileList.length; ++i)
-	{
-		if (gFileList[i].name.length > longestFilenameLen)
-			longestFilenameLen = gFileList[i].name.length;
-	}
-	var displayFilenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart + 1;
-	// If the user has extended descriptions enabled, then allow 47 characters for the
-	// description and adjust the filename length accordingly
-	gListIdxes.descriptionEnd = console.screen_columns - 1; // Leave 1 character remaining on the screen
-	if (extendedDescEnabled())
-	{
-		gListIdxes.descriptionStart = gListIdxes.descriptionEnd - 47 + 1;
-		gListIdxes.fileSizeEnd = gListIdxes.descriptionStart;
-		gListIdxes.fileSizeStart = gListIdxes.fileSizeEnd - 7;
-		gListIdxes.filenameEnd = gListIdxes.fileSizeStart;
-	}
-	// If not displaying extended descriptions, then if the longest filename
-	// is longer than the current display filename length and the user's
-	// terminal is at least 100 columns wide, then increase the filename length
-	// for the list by 20;
-	else if (longestFilenameLen > displayFilenameLen && console.screen_columns >= 100)
-	{
-		gListIdxes.filenameEnd += 20;
-		gListIdxes.fileSizeStart = gListIdxes.filenameEnd;
-		gListIdxes.fileSizeEnd = gListIdxes.fileSizeStart + 7;
-		gListIdxes.descriptionStart = gListIdxes.fileSizeEnd;
-	}
-
-	return retObj;
-}
-
-// Searches files in a directory by filespec.
-//
-// Parameters:
-//  pDirCode: The internal code of a directory to search
-//  pFilespec: A filespec describing files to search for
-//
-// Return value: An object with the following properties:
-//               foundFiles: Boolean - Whether or not any files were found
-//               dirErrors: An array of strings that will be populated with any errors that occur
-function searchDirWithFilespec(pDirCode, pFilespec)
-{
-	var retObj = {
-		foundFiles: false,
-		dirErrors: []
-	};
-	if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pFilespec) !== "string")
-		return retObj;
-
-	var filebase = new FileBase(pDirCode);
-	if (filebase.open())
-	{
-		var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM);
-		var fileList;
-		if (gFileSortOrder == SORT_FL_ULTIME || gFileSortOrder == SORT_FL_DLTIME)
-		{
-			fileList = filebase.get_list(pFilespec, fileDetail, 0, true, FileBase.SORT.NATURAL);
-			if (gFileSortOrder == SORT_FL_ULTIME)
-				fileList.sort(fileInfoSortULTime);
-			else if (gFileSortOrder == SORT_FL_DLTIME)
-				fileList.sort(fileInfoSortDLTime);
-		}
-		else
-			fileList = filebase.get_list(pFilespec, fileDetail, 0, true, gFileSortOrder);
-		retObj.foundFiles = (fileList.length > 0);
-		filebase.close();
-		for (var i = 0; i < fileList.length; ++i)
-		{
-			// Fix line endings in extdesc if necessary
-			if (fileList[i].hasOwnProperty("extdesc") && /\r\n$/.test(fileList[i].extdesc))
-				fileList[i].extdesc = lfexpand(fileList[i].extdesc);
-			fileList[i].dirCode = pDirCode;
-			gFileList.push(fileList[i]);
-		}
-	}
-	else
-	{
-		var libAndDirDesc = file_area.lib_list[libIdx].description + " - "
-						  + file_area.lib_list[libIdx].dir_list[dirIdx].description + " ("
-						  + file_area.lib_list[libIdx].dir_list[dirIdx].code + ")";
-		retObj.dirErrors.push("Failed to open " + libAndDirDesc);
-	}
-	return retObj;
-}
-
-// Searches files in a directory by description.
-//
-// Parameters:
-//  pDirCode: The internal code of a directory to search
-//  pDescUpper: The description to search for.  This is assumed to be uppercase.
-//
-// Return value: An object with the following properties:
-//               foundFiles: Boolean - Whether or not any files were found
-//               dirErrors: An array of strings that will be populated with any errors that occur
-function searchDirWithDescUpper(pDirCode, pDescUpper)
-{
-	var retObj = {
-		foundFiles: false,
-		dirErrors: []
-	};
-	if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pDescUpper) !== "string" || pDescUpper.length == 0)
-		return retObj;
-
-	var filebase = new FileBase(pDirCode);
-	if (filebase.open())
-	{
-		//var fileList = filebase.get_list(gFilespec, FileBase.DETAIL.EXTENDED, 0, true, gFileSortOrder);
-		var fileList;
-		if (gFileSortOrder == SORT_FL_ULTIME || gFileSortOrder == SORT_FL_DLTIME)
-		{
-			fileList = filebase.get_list(gFilespec, FileBase.DETAIL.EXTENDED, 0, true, FileBase.SORT.NATURAL);
-			if (gFileSortOrder == SORT_FL_ULTIME)
-				fileList.sort(fileInfoSortULTime);
-			else if (gFileSortOrder == SORT_FL_DLTIME)
-				fileList.sort(fileInfoSortDLTime);
-		}
-		else
-			fileList = filebase.get_list(gFilespec, FileBase.DETAIL.EXTENDED, 0, true, gFileSortOrder);
-		filebase.close();
-		for (var i = 0; i < fileList.length; ++i)
-		{
-			// Search both 'desc' and 'extdesc', if available in the file metadata object
-			var fileIsMatch = false;
-			if (fileList[i].hasOwnProperty("desc"))
-			{
-				var fileDesc = strip_ctrl(fileList[i].desc).toUpperCase();
-				fileIsMatch = (fileDesc.indexOf(pDescUpper) > -1);
-			}
-			if (!fileIsMatch && fileList[i].hasOwnProperty("extdesc"))
-			{
-				// Fix line endings if necessary
-				fileList[i].extdesc = lfexpand(fileList[i].extdesc);
-				var descLines = fileList[i].extdesc.split("\r\n");
-				var fileDesc = strip_ctrl(descLines.join(" ")).toUpperCase();
-				fileIsMatch = (fileDesc.indexOf(pDescUpper) > -1);
-			}
-			if (fileIsMatch)
-			{
-				retObj.foundFiles = true;
-				fileList[i].dirCode = pDirCode;
-				gFileList.push(fileList[i]);
-			}
-		}
-	}
-	else
-	{
-		var libAndDirDesc = file_area.lib_list[libIdx].description + " - "
-						  + file_area.lib_list[libIdx].dir_list[dirIdx].description + " ("
-						  + file_area.lib_list[libIdx].dir_list[dirIdx].code + ")";
-		retObj.dirErrors.push("Failed to open " + libAndDirDesc);
-	}
-	return retObj;
-}
-
-// Searches files in a directory that were added later than a given time.
-//
-// Parameters:
-//  pDirCode: The internal code of a directory to search
-//  pSinceTime: The time after which to look for newer files
-//
-// Return value: An object with the following properties:
-//               foundFiles: Boolean - Whether or not any files were found
-//               dirErrors: An array of strings that will be populated with any errors that occur
-function searchDirNewFiles(pDirCode, pSinceTime)
-{
-	var retObj = {
-		foundFiles: false,
-		dirErrors: []
-	};
-	if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pSinceTime) !== "number")
-		return retObj;
-
-	var filebase = new FileBase(pDirCode);
-	if (filebase.open())
-	{
-		var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM);
-		var fileList;
-		if (gFileSortOrder == SORT_FL_ULTIME || gFileSortOrder == SORT_FL_DLTIME)
-		{
-			fileList = filebase.get_list(gFilespec, fileDetail, 0, true, FileBase.SORT.NATURAL);
-			if (gFileSortOrder == SORT_FL_ULTIME)
-				fileList.sort(fileInfoSortULTime);
-			else if (gFileSortOrder == SORT_FL_DLTIME)
-				fileList.sort(fileInfoSortDLTime);
-		}
-		else
-			fileList = filebase.get_list(gFilespec, fileDetail, 0, true, gFileSortOrder);
-		filebase.close();
-		for (var i = 0; i < fileList.length; ++i)
-		{
-			if (fileList[i].added >= pSinceTime)
-			{
-				retObj.foundFiles = true;
-				// Fix line endings in extdesc if necessary
-				if (fileList[i].hasOwnProperty("extdesc"))
-					fileList[i].extdesc = lfexpand(fileList[i].extdesc);
-				fileList[i].dirCode = pDirCode;
-				gFileList.push(fileList[i]);
-			}
-		}
-	}
-	else
-	{
-		var libAndDirDesc = file_area.lib_list[libIdx].description + " - "
-						  + file_area.lib_list[libIdx].dir_list[dirIdx].description + " ("
-						  + file_area.lib_list[libIdx].dir_list[dirIdx].code + ")";
-		retObj.dirErrors.push("Failed to open " + libAndDirDesc);
-	}
-	return retObj;
-}
-
-// Searches either the user's current directory, library, or all dirs in all
-// libraries, using a search function that populages gFileList.
-//
-// Parameters:
-//  pSearchOption: A string that specifies "D" for directory, "L" for library, or "A" for all
-//  pDirSearchFn: A search function that searches a file directory for criteria and populates gFileList.
-//                This function must return an object with the following properties:
-//                foundFiles: Boolean - Whether any files were found
-//                dirErrors: An array containing strings for any errors found
-//
-// Return value: An object with the following properties:
-//               allSameDir: Boolean - Whethre or not all files found are in the same directory
-//               errors: An array containing strings for any errors found
-function searchDirGroupOrAll(pSearchOption, pDirSearchFn)
-{
-	var retObj = {
-		allSameDir: true,
-		errors: []
-	};
-
-	if (typeof(pSearchOption) !== "string")
-	{
-		retObj.errors.push("Invalid search option");
-		return retObj;
-	}
-	if (typeof(pDirSearchFn) !== "function")
-	{
-		retObj.errors.push("No search function");
-		return retObj;
-	}
-
-	var searchOptionUpper = pSearchOption.toUpperCase();
-	if (searchOptionUpper == "D")
-	{
-		// Current directory
-		var searchRetObj = pDirSearchFn(gDirCode);
-		retObj.allSameDir = true;
-		for (var i = 0; i < searchRetObj.dirErrors.length; ++i)
-			retObj.errors.push(searchRetObj.dirErrors[i]);
-	}
-	else if (searchOptionUpper == "L")
-	{
-		// Current file library
-		var libIdx = file_area.dir[gDirCode].lib_index;
-		for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx)
-		{
-			if (file_area.lib_list[libIdx].dir_list[dirIdx].can_access) // And can_download?
-			{
-				if (gSearchVerbose)
-				{
-					console.print(" " + file_area.lib_list[libIdx].dir_list[dirIdx].description + "..");
-					console.crlf();
-				}
-				var lastDirCode = (gFileList.length > 0 ? gFileList[gFileList.length-1].dirCode : "");
-				var dirCode = file_area.lib_list[libIdx].dir_list[dirIdx].code;
-				var searchRetObj = pDirSearchFn(dirCode);
-				if (retObj.allSameDir && searchRetObj.foundFiles && lastDirCode.length > 0)
-					retObj.allSameDir = (dirCode == lastDirCode);
-				for (var i = 0; i < searchRetObj.dirErrors.length; ++i)
-					retObj.errors.push(searchRetObj.dirErrors[i]);
-			}
-		}
-	}
-	else if (searchOptionUpper == "A")
-	{
-		// All file libraries & directories
-		for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx)
-		{
-			if (gSearchVerbose)
-			{
-				console.print("Searching " + file_area.lib_list[libIdx].description + "..");
-				console.crlf();
-			}
-			for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx)
-			{
-				if (file_area.lib_list[libIdx].dir_list[dirIdx].can_access) // And can_download?
-				{
-					if (gSearchVerbose)
-					{
-						console.print(" " + file_area.lib_list[libIdx].dir_list[dirIdx].description + "..");
-						console.crlf();
-					}
-					var lastDirCode = (gFileList.length > 0 ? gFileList[gFileList.length-1].dirCode : "");
-					var dirCode = file_area.lib_list[libIdx].dir_list[dirIdx].code;
-					var searchRetObj = pDirSearchFn(dirCode);
-					if (retObj.allSameDir && searchRetObj.foundFiles && lastDirCode.length > 0)
-						retObj.allSameDir = (dirCode == lastDirCode);
-					for (var i = 0; i < searchRetObj.dirErrors.length; ++i)
-						retObj.errors.push(searchRetObj.dirErrors[i]);
-				}
-			}
-		}
-	}
-	else
-	{
-		//retObj.errors.push("Invalid search option" + (pSearchOption.length > 0 ? ": " + pSearchOption : ""));
-		retObj.errors.push("Aborted");
-	}
-
-	return retObj;
-}
-
-// Returns whether the user has their extended file description setting enabled
-// and if it can be supported in the user's terminal mode.  Extended descriptions
-// will be displayed in the main screen if the user has that option enabled and
-// the user's terminal is at least 80 columns wide.
-function extendedDescEnabled()
-{
-	var userExtDescEnabled = ((user.settings & USER_EXTDESC) == USER_EXTDESC);
-	return userExtDescEnabled && console.screen_columns >= 80;
-}
-
-// Displays a file's extended description on the main screen, next to the
-// file list menu.  This is to be used when the user's extended file description
-// option is enabled (where the menu would take up about the left half of
-// the screen).
-//
-// Parameters:
-//  pFileIdx: The index of the file metadata object in gFileList to use
-//  pStartScreenRow: Optional - The screen row number to start printing, for partial
-//                   screen refreshing (can be in the middle of the extended description)
-//  pEndScreenRow: Optional - The screen row number to stop printing, for partial
-//                   screen refreshing (can be in the middle of the extended description)
-//  pMaxWidth: Optional - The maximum width to use for printing the description lines
-function displayFileExtDescOnMainScreen(pFileIdx, pStartScreenRow, pEndScreenRow, pMaxWidth)
-{
-	if (typeof(pFileIdx) !== "number")
-		return;
-	if (pFileIdx < 0 || pFileIdx >= gFileList.length)
-		return;
-
-	// Get the file description from its metadata object
-	var fileMetadata = gFileList[pFileIdx];
-	var fileDesc = "";
-	if (fileMetadata.hasOwnProperty("extdesc") && fileMetadata.extdesc.length > 0)
-		fileDesc = fileMetadata.extdesc;
-	else
-		fileDesc = fileMetadata.desc;
-	if (typeof(fileDesc) != "string")
-		fileDesc = "";
-
-	// This might be overkill, but just in case, convert any non-Synchronet
-	// attribute codes to Synchronet attribute codes in the description.
-	// This will help simplify getting substrings for formatting.  Then for
-	// efficiency, put the converted description back into the metadata
-	// object in the array so that it doesn't have to be converted again.
-	if (!fileMetadata.hasOwnProperty("attrsConverted"))
-	{
-		fileDesc = convertAttrsToSyncPerSysCfg(fileDesc);
-		fileMetadata.attrsConverted = true;
-		if (fileMetadata.hasOwnProperty("extdesc"))
-			fileMetadata.extdesc = fileDesc;
-		else
-			fileMetadata.desc = fileDesc;
-	}
-
-	// Calculate where to write the description on the screen
-	var startX = gFileListMenu.size.width + 1; // Assuming the file menu starts at the leftmost column
-	var maxDescLen = console.screen_columns - startX;
-	if (typeof(pMaxWidth) === "number" && pMaxWidth >= 0 && pMaxWidth < maxDescLen)
-		maxDescLen = pMaxWidth;
-	// Go to the location on the screen and write the file description
-	var formatStr = "%-" + maxDescLen + "s";
-	// firstScreenRow is the first row on the screen where the extended description
-	// should start at.  lastScreenRow is the last row (inclusive) to use for
-	// printing the extended description
-	var firstScreenRow = gNumHeaderLinesDisplayed + 1;
-	var lastScreenRow = console.screen_rows - 1; // This is inclusive
-	// screenRowForPrinting will be used for the actual screen row we're at while
-	// printing the extended description lines
-	var screenRowForPrinting = firstScreenRow;
-	// If pStartScreenRow or pEndScreenRow are specified, then use
-	// them to specify the start & end screen rows to actually print
-	if (typeof(pStartScreenRow) === "number" && pStartScreenRow >= firstScreenRow && pStartScreenRow <= lastScreenRow)
-		screenRowForPrinting = pStartScreenRow;
-	if (typeof(pEndScreenRow) === "number" && pEndScreenRow > firstScreenRow && pStartScreenRow <= lastScreenRow)
-		lastScreenRow = pEndScreenRow;
-	var fileDescArray = fileDesc.split("\r\n");
-	console.print("\x01n");
-	// screenRowNum is to keep track of the row on the screen where the
-	// description line would be placed, in case the start row is after that
-	var screenRowNum = firstScreenRow;
-	for (var i = 0; i < fileDescArray.length; ++i)
-	{
-		if (screenRowForPrinting > screenRowNum++)
-			continue;
-		// Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js
-		// Normally it would be handy to use printf() to print the text line:
-		//printf(formatStr, substrWithAttrCodes(fileDescArray[i], 0, maxDescLen));
-		// However, printf() doesn't account for attribute codes and thus may not
-		// fill the rest of the width.  So, we do that manually.
-		var descLine = substrWithAttrCodes(fileDescArray[i], 0, maxDescLen);
-		var lineTextLength = console.strlen(descLine);
-		if (lineTextLength > 0)
-		{
-			console.gotoxy(startX, screenRowForPrinting++);
-			console.print(descLine);
-			var remainingLen = maxDescLen - lineTextLength;
-			if (remainingLen > 0)
-				printf("%" + remainingLen + "s", "");
-		}
-		// Stop printing the description lines when we reach the last line on
-		// the screen where we want to print.
-		if (screenRowForPrinting > lastScreenRow)
-			break;
-	}
-	// If there is room, shoe the file date on the next line
-	if (screenRowForPrinting <= lastScreenRow && fileMetadata.hasOwnProperty("time"))
-	{
-		console.print("\x01n");
-		console.gotoxy(startX, screenRowForPrinting++);
-		var dateStr = "Date: " + strftime("%Y-%m-%d", fileMetadata.time);
-		printf("%-" + maxDescLen + "s", dateStr.substr(0, maxDescLen));
-	}
-	// Clear the rest of the lines to the bottom of the list area
-	console.print("\x01n");
-	while (screenRowForPrinting <= lastScreenRow)
-	{
-		console.gotoxy(startX, screenRowForPrinting++);
-			printf(formatStr, "");
-	}
-}
-
-// Refreshes (re-draws) the main content of the screen (file list menu,
-// and extended description area if enabled).  The coordinates are absolute
-// screen coordinates.
-//
-// Parameters:
-//  pUpperLeftX: The X coordinate of the upper-left corner of the area to re-draw
-//  pUpperLeftY: The Y coordinate of the upper-left corner of the area to re-draw
-//  pWidth: The width of the area to re-draw
-//  pHeight: The height of the area to re-draw
-//  pSelectedItemIdxes: Optional: An object with selected item indexes for the file menu.
-//                                If not passed, an empty object will be used.
-//                      This can also be a boolean, and if true, will refresh the
-//                      selected items on the file menu (with checkmarks) outside the
-//                      given top & bottom screen rows.
-function refreshScreenMainContent(pUpperLeftX, pUpperLeftY, pWidth, pHeight, pSelectedItemIdxes)
-{
-	// Have the file list menu partially re-draw itself if necessary
-	var startXWithinFileList = (pUpperLeftX >= gFileListMenu.pos.x && pUpperLeftX < gFileListMenu.pos.x + gFileListMenu.size.width);
-	var startYWithinFileList = (pUpperLeftY >= gFileListMenu.pos.y && pUpperLeftY < gFileListMenu.pos.y + gFileListMenu.size.height);
-	if (startXWithinFileList && startYWithinFileList)
-	{
-		var selectedItemIdxesIsValid = (typeof(pSelectedItemIdxes) === "object");
-		var selectedItemIdxes = (selectedItemIdxesIsValid ? pSelectedItemIdxes : {});
-		gFileListMenu.DrawPartialAbs(pUpperLeftX, pUpperLeftY, pWidth, pHeight, selectedItemIdxes);
-	}
-	// If pSelectedItemIdxes is a bool instead of an object and is true,
-	// refresh the selected items (with checkmarks) outside the top & bottom
-	// lines on the file menu
-	if (!selectedItemIdxesIsValid && typeof(pSelectedItemIdxes) === "boolean" && pSelectedItemIdxes && gFileListMenu.numSelectedItemIndexes() > 0)
-	{
-		var bottomScreenRow = pUpperLeftY + pHeight - 1;
-		for (var idx in gFileListMenu.selectedItemIndexes)
-		{
-			var idxNum = +idx;
-			var itemScreenRow = gFileListMenu.ScreenRowForItem(idxNum);
-			if (itemScreenRow == -1)
-				continue;
-			if (itemScreenRow < pUpperLeftY || itemScreenRow > bottomScreenRow)
-			{
-				var isSelected = (idxNum == gFileListMenu.selectedItemIdx);
-				gFileListMenu.WriteItemAtItsLocation(idxNum, isSelected, false);
-			}
-		}
-	}
-	// If the user has extended descriptions enabled, then the file menu
-	// is only taking up about half the screen on the left, and we'll also
-	// have to refresh the description area.
-	if (extendedDescEnabled())
-	{
-		var fileMenuRightX = gFileListMenu.pos.x + gFileListMenu.size.width - 1;
-		var width = pWidth - (fileMenuRightX - pUpperLeftX + 1);
-		if (width > 0)
-		{
-			var firstRow = pUpperLeftY;
-			// The last row is inclusive.  It seems like there might be an off-by-1
-			// problem here?  I thought 1 would need to be subtracted from lastRow
-			var lastRow = pUpperLeftY + pHeight;
-			// We don't want to overwrite the last row on the screen, since that's
-			// used for the command bar
-			if (lastRow == console.screen_rows)
-				--lastRow;
-			displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx, firstRow, lastRow, width);
-		}
-	}
-}
-
-// Returns whether or not the lister is doing a file search
-function isDoingFileSearch()
-{
-	return (gScriptMode == MODE_SEARCH_FILENAME || gScriptMode == MODE_SEARCH_DESCRIPTION || gScriptMode == MODE_NEW_FILE_SEARCH);
-}
-
-// Custom file info sort function for upload time
-function fileInfoSortULTime(pA, pB)
-{
-	if (pA.hasOwnProperty("added") && pB.hasOwnProperty("added"))
-	{
-		if (pA.added < pB.added)
-			return -1;
-		else if (pA.added > pB.added)
-			return 1;
-		else
-			return 0;
-	}
-	else
-	{
-		if (pA.time < pB.time)
-			return -1;
-		else if (pA.time > pB.time)
-			return 1;
-		else
-			return 0;
-	}
-}
-
-// Custom file info sort function for download time
-function fileInfoSortDLTime(pA, pB)
-{
-	if (pA.hasOwnProperty("last_downloaded") && pB.hasOwnProperty("last_downloaded"))
-	{
-		if (pA.last_downloaded < pB.last_downloaded)
-			return -1;
-		else if (pA.last_downloaded > pB.last_downloaded)
-			return 1;
-		else
-			return 0;
-	}
-	else
-		return 0;
-}
-
-// Returns whether or not a user can download from a file directory, and writes an error if not.
-//
-// Parameters:
-//  pDirCode: The internal directory code of a file directory
-//
-// Return value: Boolean - Whether or not the user can download from the file directory given
-function userCanDownloadFromFileArea_ShowErrorIfNot(pDirCode)
-{
-	var userCanDownload = bbs.compare_ars(file_area.dir[pDirCode].download_ars);
-	if (!userCanDownload)
-	{
-		// The user doesn't have permission to download from this directory
-		//file_area.dir[pDirCode].name
-		var areaFullDesc = file_area.dir[pDirCode].lib_name + ": "
-						 + file_area.dir[pDirCode].description;
-		areaFullDesc = word_wrap(areaFullDesc, console.screen_columns-1, areaFullDesc.length).replace(/\r|\n/g, "\r\n");
-		while (areaFullDesc.lastIndexOf("\r\n") == areaFullDesc.length-2)
-			areaFullDesc = areaFullDesc.substr(0, areaFullDesc.length-2);
-		console.crlf();
-		console.print(areaFullDesc);
-		console.crlf();
-		console.mnemonics(bbs.text(CantDownloadFromDir));
-		console.crlf();
-		console.pause();
-	}
-	return userCanDownload;
-}
\ No newline at end of file