diff --git a/xtrn/DDAreaChoosers/DDFileAreaChooser.cfg b/xtrn/DDAreaChoosers/DDFileAreaChooser.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..206bf56598396128d2339969efeded52975b9836
--- /dev/null
+++ b/xtrn/DDAreaChoosers/DDFileAreaChooser.cfg
@@ -0,0 +1,33 @@
+[BEHAVIOR]
+useLightbarInterface=true
+
+[COLORS]
+; Area number
+areaNum=nwh
+; Description
+desc=nc
+; Number of items
+numItems=bh
+; List header
+header=nyh
+; For the directory list header that has "Directories for" the group and
+; has the page number
+fileAreaHdr=ng
+; Mark character for areas that are currently selected
+areaMark=gh
+; Highlighted colors (for lightbar mode)
+bkgHighlight=4
+areaNumHighlight=wh
+descHighlight=c
+numItemsHighlight=wh
+
+; Colors for the lightbar help line text:
+; Background
+lightbarHelpLineBkg=7
+; The color for general text in the lightbar help line
+lightbarHelpLineGeneral=b
+; The color for the hotkeys in the lightbar help line
+lightbarHelpLineHotkey=r
+; The color for the ) separating the hotkeys from the general text in the
+; lightbar help line
+lightbarHelpLineParen=m
\ No newline at end of file
diff --git a/xtrn/DDAreaChoosers/DDFileAreaChooser.js b/xtrn/DDAreaChoosers/DDFileAreaChooser.js
new file mode 100644
index 0000000000000000000000000000000000000000..49560abba911f42fdd6d30d4437b3d8e2672ed7f
--- /dev/null
+++ b/xtrn/DDAreaChoosers/DDFileAreaChooser.js
@@ -0,0 +1,1782 @@
+/* This is a script that lets the user choose a file area,
+ * with either a lightbar or traditional user interface.
+ *
+ * Date       User          Version Description
+ * 2010-03-06 Eric Oulashin 0.90    Started (based on DDFileAreaChooser.js)
+ * 2010-03-08 Eric Oulashin         Continued work
+ * 2010-03-09 Eric Oulashin         Continued work
+ * 2010-03-11 Eric Oulashin         Continued work
+ * 2010-03-12 Eric Oulashin         Fixed a bug related to choosing a file area
+ *                                  by typing a number, in lightbar mode.
+ * 2010-03-13 Eric Oulashin 1.00    Fixed a bug where it was incorrectly displaying
+ *                                  the "currently selected" marker next to some
+ *                                  directories in some file libraries.
+ *                                  Updated to read settings from a configuration
+ *                                  file.
+ * 2012-10-06 Eric Oulashin 1.02    For lightbar mode, updated to display the
+ *                                  page number in the header at the top (in
+ *                                  addition to the total number of pages,
+ *                                  which it was already displaying).
+ * 2012-11-30 Eric Oulashin 1.03    Bug fix: After leaving the help screen
+ *                                  from the directory list, the top line
+ *                                  is now correctly written with the page
+ *                                  information as "Page # of #".
+ * 2013-05-04 Eric Oulashin 1.04    Updated to dynamically adjust the length
+ *                                  of the # files column based on the
+ *                                  greatest number of files of all
+ *                                  file dirs within a file library so
+ *                                  that the formatting still looks good.
+ * 2013-05-10 Eric Oulashin 1.05    Bug fix: In
+ *                                  DDFileAreaChooser_listDirsInFileLib_Traditional,
+ *                                  updated a couple lines of code to use
+ *                                  libIndex rather than pLibIndex, since
+ *                                  pLibIndex can sometimes be invalid.
+ *                                  Also updated that function to prepare
+ *                                  the associative array of directory
+ *                                  information for the file library.
+ * 2014-09-14 Eric Oulashin 1.06    Bug fix: Updated the highlight (lightbar)
+ *                                  format string in the
+ *                                  DDFileAreaChooser_buildFileDirPrintfInfoForLib()
+ *                                  function to include a normal attribute at
+ *                                  the end to avoid color issues when clearing
+ *                                  the screen, etc.  Bug reported by Psi-Jack.
+ * 2014-12-22 Eric Oulashin 1.07    Updated the verison number to match the
+ *                                  message area chooser, which had a couple of
+ *                                  bugs fixed.
+ * 2015-04-19 Eric Oulashin 1.08    Added color settings for the lightbar help text
+ *                                  at the bottom of the screen.  Also, added the
+ *									ability to use the PageUp & PageDown keys instead
+ *                                  of P and N in the lightbar lists.
+ */
+
+/* Command-line arguments:
+   1 (argv[0]): Boolean - Whether or not to run the area chooser (if false,
+                then this file will just provide the DDFileAreaChooser class).
+*/
+
+load("sbbsdefs.js");
+
+// This script requires Synchronet version 3.14 or higher.
+// Exit if the Synchronet version is below the minimum.
+if (system.version_num < 31400)
+{
+	var message = "\1n\1h\1y\1i* Warning:\1n\1h\1w Digital Distortion Message Lister "
+	             + "requires version \1g3.14\1w or\r\n"
+	             + "higher of Synchronet.  This BBS is using version \1g" + system.version
+	             + "\1w.  Please notify the sysop.";
+	console.crlf();
+	console.print(message);
+	console.crlf();
+	console.pause();
+	exit();
+}
+
+// Version & date variables
+var DD_FILE_AREA_CHOOSER_VERSION = "1.08";
+var DD_FILE_AREA_CHOOSER_VER_DATE = "2015-04-19";
+
+// Keyboard input key codes
+var CTRL_M = "\x0d";
+var KEY_ENTER = CTRL_M;
+var KEY_ESC = ascii(27);
+
+// PageUp & PageDown keys - Not real key codes, but codes defined
+// to be used & recognized in this script
+var KEY_PAGE_UP = "\1PgUp";
+var KEY_PAGE_DOWN = "\1PgDn";
+
+// Key codes for display
+var UP_ARROW = ascii(24);
+var DOWN_ARROW = ascii(25);
+
+// Determine whether or not to execute the message listing code, based
+// on the first program argument (a boolean).
+var executeThisScript = true;
+if (typeof(argv[0]) != "undefined")
+	executeThisScript = argv[0];
+
+// If executeThisScript is true, then create a DDFileAreaChooser object and use
+// it to let the user choose a message area.
+if (executeThisScript)
+{
+	var fileAreaChooser = new DDFileAreaChooser();
+	fileAreaChooser.SelectFileArea();
+}
+
+// End of script execution
+
+///////////////////////////////////////////////////////////////////////////////////
+// DDFileAreaChooser class stuff
+
+function DDFileAreaChooser()
+{
+	// Colors
+	this.colors = new Object();
+	this.colors.areaNum = "\1n\1w\1h";
+	this.colors.desc = "\1n\1c";
+	this.colors.numItems = "\1b\1h";
+	this.colors.header = "\1n\1y\1h";
+	this.colors.fileAreaHdr = "\1n\1g";
+	this.colors.areaMark = "\1g\1h";
+	// Highlighted colors (for lightbar mode)
+	this.colors.bkgHighlight = "\1" + "4"; // Blue background
+	this.colors.areaNumHighlight = "\1w\1h";
+	this.colors.descHighlight = "\1c";
+	this.colors.numItemsHighlight = "\1w\1h";
+	// Lightbar help line colors
+	this.colors.lightbarHelpLineBkg = "\1" + "7";
+	this.colors.lightbarHelpLineGeneral = "\1b";
+	this.colors.lightbarHelpLineHotkey = "\1r";
+	this.colors.lightbarHelpLineParen = "\1m";
+
+	// useLightbarInterface specifies whether or not to use the lightbar
+	// interface.  The lightbar interface will still only be used if the
+	// user's terminal supports ANSI.
+	this.useLightbarInterface = true;
+
+	// Store whether or not bbs.curlib and bbs.curdir are valid (they might not
+	// be for a new user).
+	this.curlibValid = ((bbs.curlib != null) && (typeof(bbs.curlib) == "number"));
+	this.curdirValid = ((bbs.curdir != null) && (typeof(bbs.curdir) == "number"));
+
+	this.areaNumLen = 4;
+	this.descFieldLen = 67; // Description field length
+
+	// Set the function pointers for the object
+	this.ReadConfigFile = DDFileAreaChooser_ReadConfigFile;
+	this.SelectFileArea = DDFileAreaChooser_selectFileArea;
+	this.SelectFileArea_Traditional = DDFileAreaChooser_selectFileArea_Traditional;
+	this.SelectDirWithinFileLib_Traditional = DDFileAreaChooser_selectDirWithinFileLib_Traditional;
+	this.ListFileLibs = DDFileAreaChooser_listFileLibs_Traditional;
+	this.ListDirsInFileLib = DDFileAreaChooser_listDirsInFileLib_Traditional;
+	this.WriteLibListHdrLine = DDFileAreaChooser_writeLibListTopHdrLine;
+	this.WriteDirListHdr1Line = DDFileAreaChooser_writeDirListHdr1Line;
+	// Lightbar-specific functions
+	this.SelectFileArea_Lightbar = DDFileAreaChooser_selectFileArea_Lightbar;
+	this.SelectDirWithinFileLib_Lightbar = DDFileAreaChooser_selectDirWithinFileLib_Lightbar;
+	this.WriteKeyHelpLine = DDFileAreaChooser_writeKeyHelpLine;
+	this.ListScreenfulOfFileLibs = DDFileAreaChooser_listScreenfulOfFileLibs;
+	this.WriteFileLibLine = DDFileAreaChooser_writeFileLibLine;
+	this.WriteFileLibDirLine = DDFileAreaChooser_writeFileLibDirLine;
+	this.updatePageNumInHeader = DDFileAreaChooser_updatePageNumInHeader;
+	this.ListScreenfulOfDirs = DDMsgAreaChooser_listScreenfulOfFileDirs;
+	// Help screen
+	this.ShowHelpScreen = DDFileAreaChooser_showHelpScreen;
+	// Misc. functions
+	this.NumFilesInDir = DDFileAreaChooser_NumFilesInDir;
+
+	// Function to build the directory printf information for a file lib
+	this.BuildFileDirPrintfInfoForLib = DDFileAreaChooser_buildFileDirPrintfInfoForLib;
+
+	// Read the settings from the config file.
+	this.ReadConfigFile();
+
+	// printf strings used for outputting the file libraries
+	this.fileLibPrintfStr = " " + this.colors.areaNum + "%4d "
+	                      + this.colors.desc + "%-" + this.descFieldLen
+	                      + "s " + this.colors.numItems + "%4d";
+	this.fileLibHighlightPrintfStr = "\1n" + this.colors.bkgHighlight + " "
+	                               + this.colors.areaNumHighlight + "%4d "
+	                               + this.colors.descHighlight + "%-" + this.descFieldLen
+	                               + "s " + this.colors.numItemsHighlight + "%4d";
+	this.fileLibListHdrPrintfStr = this.colors.header + " %5s %-"
+	                             + +(this.descFieldLen-2) + "s %6s";
+	this.fileDirHdrPrintfStr = this.colors.header + " %5s %-"
+	                         + +(this.descFieldLen-3) + "s %-7s";
+	// Lightbar mode key help line
+	this.lightbarKeyHelpText = "\1n" + this.colors.lightbarHelpLineHotkey
+	              + this.colors.lightbarHelpLineBkg + UP_ARROW
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + DOWN_ARROW
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "HOME"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "END"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "#"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "PgUp"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + "/"
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "Dn"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "F"
+				  + "\1n" + this.colors.lightbarHelpLineParen
+				  + this.colors.lightbarHelpLineBkg + ")"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + "irst pg, "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "L"
+				  + "\1n" + this.colors.lightbarHelpLineParen
+				  + this.colors.lightbarHelpLineBkg + ")"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + "ast pg, "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "Q"
+				  + "\1n" + this.colors.lightbarHelpLineParen
+				  + this.colors.lightbarHelpLineBkg + ")"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + "uit, "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "?";
+	// Pad the lightbar key help text on either side to center it on the screen
+	// (but leave off the last character to avoid screen drawing issues)
+	var helpTextLen = console.strlen(this.lightbarKeyHelpText);
+	var helpTextStartCol = (console.screen_columns/2) - (helpTextLen/2);
+	this.lightbarKeyHelpText = "\1n" + this.colors.lightbarHelpLineBkg
+	                         + format("%" + +(helpTextStartCol) + "s", "")
+							 + this.lightbarKeyHelpText + "\1n"
+							 + this.colors.lightbarHelpLineBkg;
+	var numTrailingChars = console.screen_columns - (helpTextStartCol+helpTextLen) - 1;
+	this.lightbarKeyHelpText += format("%" + +(numTrailingChars) + "s", "") + "\1n";
+
+	// this.fileDirListPrintfInfo will be an array of printf strings
+	// for the file directories in the file libraries.  The index is the
+	// file library index.  The file directory printf information is
+	// created on the fly the first time the user lists directories for
+	// a file library.
+	this.fileDirListPrintfInfo = new Array();
+}
+
+// For the DDFileAreaChooser class: Lets the user choose a file area.
+function DDFileAreaChooser_selectFileArea()
+{
+   if (this.useLightbarInterface && console.term_supports(USER_ANSI))
+		this.SelectFileArea_Lightbar();
+	else
+		this.SelectFileArea_Traditional();
+}
+
+// For the DDFileAreaChooser class: Traditional user interface for
+// letting the user choose a file area
+function DDFileAreaChooser_selectFileArea_Traditional()
+{
+   // If there are no file libraries, then don't let the user
+   // choose one.
+   if (file_area.lib_list.length == 0)
+   {
+      console.clear("\1n");
+      console.print("\1y\1hThere are no file libraries.\r\n\1p");
+      return;
+   }
+
+   // Show the file libraries & directories and let the user choose one.
+   var selectedLib = 0; // The user's selected file library
+   var selectedDir = 0; // The user's selected file directory
+   var continueChooseFileLib = true;
+   while (continueChooseFileLib)
+   {
+      // Clear the BBS command string to make sure there are no extra
+      // commands in there that could cause weird things to happen.
+      bbs.command_str = "";
+
+      console.clear("\1n");
+      this.ListFileLibs();
+      console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, or [\1h" + +(bbs.curlib+1) +
+                    "\1n\1c]:\1h ");
+      // Accept Q (quit) or a file library number
+      selectedLib = console.getkeys("Q", file_area.lib_list.length);
+
+      // If the user just pressed enter (selectedLib would be blank),
+      // default to the current library.
+      if (selectedLib.toString() == "")
+         selectedLib = bbs.curlib + 1;
+
+      // If the user chose to quit, then set continueChooseFileLib to
+      // false so we'll exit the loop.  Otherwise, let the user chose
+      // a dir within the library.
+      if (selectedLib.toString() == "Q")
+         continueChooseFileLib = false;
+      else
+         continueChooseFileLib = !this.SelectDirWithinFileLib_Traditional(selectedLib, selectedDir);
+   }
+}
+
+// For the DDFileAreaChooser class: Lets the user select a file area (directory)
+// within a specified file library - Traditional user interface.
+//
+// Parameters:
+//  pLibNumber: The file library number
+//  pSelectedDir: The currently-selected file area
+//
+// Return value: Boolean - Whether or not the user chose a file area.
+function DDFileAreaChooser_selectDirWithinFileLib_Traditional(pLibNumber, pSelectedDir)
+{
+   var userChoseAnArea = false;
+
+   // If the file library number is valid, then
+   // set it and let the user choose a file directory
+   // within the library.
+   if (pLibNumber > 0)
+   {
+      // Ensure that the file directory printf information is created for
+      // this file library.
+      this.BuildFileDirPrintfInfoForLib(pLibNumber-1);
+
+      // Set the default directory #: The current directory, or if the
+      // user chose a different file library, then this should be set
+      // to the first directory.
+      var defaultDir = bbs.curdir + 1;
+      if (pLibNumber-1 != bbs.curlib)
+         defaultDir = 1;
+
+      console.clear("\1n");
+      this.ListDirsInFileLib(pLibNumber - 1, defaultDir - 1);
+      console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, or [\1h" + defaultDir +
+                    "\1n\1c]: \1h");
+      // Accept Q (quit) or a file directory number
+      var selectedDir = console.getkeys("Q", file_area.lib_list[pLibNumber - 1].dir_list.length);
+
+      // If the user just pressed enter (selectedDir would be blank),
+      // default the selected directory.
+      if (selectedDir.toString() == "")
+         selectedDir = defaultDir;
+
+      // If the user chose a directory, then set bbs.curlib &
+      // bbs.curdir and quit the file library loop.
+      if ((pLibNumber.toString() != "Q") && (selectedDir > 0))
+      {
+         bbs.curlib = pLibNumber - 1;
+         bbs.curdir = selectedDir - 1;
+         userChoseAnArea = true;
+      }
+   }
+
+   return userChoseAnArea;
+}
+
+// For the DDFileAreaChooser class: Traditional user interface for listing
+// the file groups
+function DDFileAreaChooser_listFileLibs_Traditional()
+{
+   // See if bbs.curlib is valid (it might not be for brand-new users).
+   var curlibValid = ((bbs.curlib != null) && (typeof(bbs.curlib) != "undefined"));
+
+   // See if bbs.curlib and bbs.curdir are valid, since we'll be testing
+   // with them.
+   var curDirLibValid =  ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number"));
+
+   // Print the list header
+   printf(this.fileLibListHdrPrintfStr, "Lib #", "Description", "# Dirs");
+   console.crlf();
+   console.print("\1n");
+   // Print the information for each file library
+   var currentDir = false;
+   for (var i = 0; i < file_area.lib_list.length; ++i)
+   {
+      // Print the library information.
+      console.print(curDirLibValid && (i == bbs.curlib) ? this.colors.areaMark + "*" : " ");
+      printf(this.fileLibPrintfStr, +(i+1),
+             file_area.lib_list[i].description.substr(0, this.descFieldLen),
+             file_area.lib_list[i].dir_list.length);
+      console.crlf();
+   }
+}
+
+// For the DDFileAreaChooser class: Traditional user interface for listing
+// the directories in a file group
+//
+// Parameters:
+//  pLibIndex: The index of the file library (0-based)
+//  pMarkIndex: An index of a file library to display the "current" mark
+//              next to.  This is optional; if left off, this will default
+//              to the current directory.
+function DDFileAreaChooser_listDirsInFileLib_Traditional(pLibIndex, pMarkIndex)
+{
+   // set libIndex, the library index
+   var libIndex = bbs.curlib;
+   if (typeof(pLibIndex) == "number")
+      libIndex = pLibIndex;
+
+   // Set markIndex, the index of the item to highlight
+   var markIndex = bbs.curdir;
+   if (typeof(pMarkIndex) == "number")
+      markIndex = pMarkIndex;
+
+   // Make sure markIndex is valid (it might not be for brand-new users).
+   if ((markIndex == null) || (typeof(markIndex) == "undefined"))
+      markIndex = 0;
+
+   // See if bbs.curlib and bbs.curdir are valid, since we'll be testing with
+   // them.
+   var curDirLibValid = ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number"));
+
+   // Ensure that the file directory printf information is created for
+   // this file library.
+   this.BuildFileDirPrintfInfoForLib(libIndex);
+
+   // Print the header lines
+   console.print(this.colors.fileAreaHdr + "Directories of \1h" +
+                 file_area.lib_list[libIndex].description);
+   console.crlf();
+   printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Files");
+   console.crlf();
+   console.print("\1n");
+   // Print the file directories
+   var isSelectedDir = false;
+   for (var i = 0; i < file_area.lib_list[libIndex].dir_list.length; ++i)
+   {
+      // See if this is the currently-selected directory.
+      if (curDirLibValid)
+         isSelectedDir = ((libIndex == bbs.curlib) && (i == markIndex));
+      console.print(isSelectedDir ? this.colors.areaMark + "*" : " ");
+      printf(this.fileDirListPrintfInfo[libIndex].printfStr, +(i+1),
+             file_area.lib_list[libIndex].dir_list[i].description.substr(0, this.descFieldLen),
+             this.fileDirListPrintfInfo[libIndex].fileCounts[i]);
+      console.crlf();
+   }
+}
+
+// For the DDFileAreaChooser class: Outputs the header line to appear above
+// the list of file libraries.
+//
+// Parameters:
+//  pNumPages: The number of pages (a number).  This is optional; if this is
+//             not passed, then it won't be used.
+//  pPageNum: The page number.  This is optional; if this is not passed,
+//            then it won't be used.
+function DDFileAreaChooser_writeLibListTopHdrLine(pNumPages, pPageNum)
+{
+  var descStr = "Description";
+  if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
+    descStr += "    (Page " + pPageNum + " of " + pNumPages + ")";
+  else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
+    descStr += "    (Page " + pPageNum + ")";
+  else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
+    descStr += "    (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
+  printf(this.fileLibListHdrPrintfStr, "Lib #", descStr, "# Dirs");
+  console.cleartoeol("\1n");
+}
+
+// For the DDFileAreaChooser class: Outputs the first header line to appear
+// above the directory list for a file library.
+//
+// Parameters:
+//  pLibIndex: The index of the file library (assumed to be valid)
+//  pNumPages: The number of pages (a number).  This is optional; if this is
+//             not passed, then it won't be used.
+//  pPageNum: The page number.  This is optional; if this is not passed,
+//            then it won't be used.
+function DDFileAreaChooser_writeDirListHdr1Line(pLibIndex, pNumPages, pPageNum)
+{
+  var descLen = 40;
+  var descFormatStr = this.colors.fileAreaHdr + "Directories of \1h%-" + descLen + "s     \1n"
+                    + this.colors.fileAreaHdr;
+  if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
+    descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")";
+  else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
+    descFormatStr += "(Page " + pPageNum + ")";
+  else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
+    descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
+  printf(descFormatStr, file_area.lib_list[pLibIndex].description.substr(0, descLen));
+  console.cleartoeol("\1n");
+}
+
+// Lightbar functions
+
+// For the DDFileAreaChooser class: Lightbar interface for letting the user
+// choose a file library and group
+function DDFileAreaChooser_selectFileArea_Lightbar()
+{
+   // If there are file libraries, then don't let the user
+   // choose one.
+   if (file_area.lib_list.length == 0)
+   {
+      console.clear("\1n");
+      console.print("\1y\1hThere are no file libraries.\r\n\1p");
+      return;
+   }
+
+   // Returns the index of the bottommost file library that can be displayed
+   // on the screen.
+   //
+   // Parameters:
+   //  pTopLibIndex: The index of the topmost file library displayed on screen
+   //  pNumItemsPerPage: The number of items per page
+   function getBottommostLibIndex(pTopLibIndex, pNumItemsPerPage)
+   {
+      var bottomLibIndex = pTopLibIndex + pNumItemsPerPage - 1;
+      // If bottomLibIndex is beyond the last index, then adjust it.
+      if (bottomLibIndex >= file_area.lib_list.length)
+         bottomLibIndex = file_area.lib_list.length - 1;
+      return bottomLibIndex;
+   }
+
+
+   // Figure out the index of the user's currently-selected file library
+   var selectedLibIndex = 0;
+   if ((bbs.curlib != null) && (typeof(bbs.curlib) == "number"))
+      selectedLibIndex = bbs.curlib;
+
+   var listStartRow = 2;      // The row on the screen where the list will start
+   var listEndRow = console.screen_rows - 1; // Row on screen where list will end
+   var topFileLibIndex = 0;    // The index of the message group at the top of the list
+
+   // Figure out the index of the last message group to appear on the screen.
+   var numItemsPerPage = listEndRow - listStartRow + 1;
+   var bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+   // Figure out how many pages are needed to list all the file areas.
+   var numPages = Math.ceil(file_area.lib_list.length / numItemsPerPage);
+   var pageNum = 1;
+   // Figure out the top index for the last page.
+   var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage;
+
+   // If the highlighted row is beyond the current screen, then
+   // go to the appropriate page.
+   if (selectedLibIndex > bottomFileLibIndex)
+   {
+      var nextPageTopIndex = 0;
+      while (selectedLibIndex > bottomFileLibIndex)
+      {
+         nextPageTopIndex = topFileLibIndex + numItemsPerPage;
+         if (nextPageTopIndex < file_area.lib_list.length)
+         {
+            // Adjust topFileLibIndex and bottomFileLibIndex, and
+            // refresh the list on the screen.
+            topFileLibIndex = nextPageTopIndex;
+            pageNum = calcPageNum(topFileLibIndex, numItemsPerPage);
+            bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+         }
+         else
+            break;
+      }
+
+      // If we didn't find the correct page for some reason, then set the
+      // variables to display page 1 and select the first message group.
+      var foundCorrectPage = ((topFileLibIndex < file_area.lib_list.length) &&
+          (selectedLibIndex >= topFileLibIndex) && (selectedLibIndex <= bottomFileLibIndex));
+      if (!foundCorrectPage)
+      {
+         topFileLibIndex = 0;
+         pageNum = calcPageNum(topFileLibIndex, numItemsPerPage);
+         bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+         selectedLibIndex = 0;
+      }
+   }
+
+   // Clear the screen, write the help line and group list header, and output
+   // a screenful of message groups.
+   console.clear("\1n");
+   this.WriteKeyHelpLine();
+
+   var curpos = new Object();
+   curpos.x = 1;
+   curpos.y = 1;
+   console.gotoxy(curpos);
+   this.WriteLibListHdrLine(numPages, pageNum);
+   this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow, listEndRow, false,
+                                false);
+   // Start of the input loop.
+   var highlightScrenRow = 0; // The row on the screen for the highlighted group
+   var userInput = "";        // Will store a keypress from the user
+   var retObj = null;        // To store the return value of choosing a file area
+   var continueChoosingFileArea = true;
+   while (continueChoosingFileArea)
+   {
+      // Highlight the currently-selected message group
+      highlightScrenRow = listStartRow + (selectedLibIndex - topFileLibIndex);
+      curpos.y = highlightScrenRow;
+      if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows))
+      {
+         console.gotoxy(1, highlightScrenRow);
+         this.WriteFileLibLine(selectedLibIndex, true);
+      }
+
+      // Get a key from the user (upper-case) and take action based upon it.
+	  userInput = getKeyWithESCChars(K_UPPER | K_NOCRLF);
+      switch (userInput)
+      {
+         case KEY_UP: // Move up one message group in the list
+            if (selectedLibIndex > 0)
+            {
+               // If the previous group index is on the previous page, then
+               // display the previous page.
+               var previousGrpIndex = selectedLibIndex - 1;
+               if (previousGrpIndex < topFileLibIndex)
+               {
+                  // Adjust topFileLibIndex and bottomFileLibIndex, and
+                  // refresh the list on the screen.
+                  topFileLibIndex -= numItemsPerPage;
+                  pageNum = calcPageNum(topFileLibIndex, numItemsPerPage);
+                  bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+                  this.updatePageNumInHeader(pageNum, numPages, true, false);
+                  this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow,
+                                               listEndRow, false, true);
+               }
+               else
+               {
+                  // Display the current line un-highlighted.
+                  console.gotoxy(1, curpos.y);
+                  this.WriteFileLibLine(selectedLibIndex, false);
+               }
+               selectedLibIndex = previousGrpIndex;
+            }
+            break;
+         case KEY_DOWN: // Move down one message group in the list
+            if (selectedLibIndex < file_area.lib_list.length - 1)
+            {
+               // If the next group index is on the next page, then display
+               // the next page.
+               var nextGrpIndex = selectedLibIndex + 1;
+               if (nextGrpIndex > bottomFileLibIndex)
+               {
+                  // Adjust topFileLibIndex and bottomFileLibIndex, and
+                  // refresh the list on the screen.
+                  topFileLibIndex += numItemsPerPage;
+                  pageNum = calcPageNum(topFileLibIndex, numItemsPerPage);
+                  bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+                  this.updatePageNumInHeader(pageNum, numPages, true, false);
+                  this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow,
+                                               listEndRow, false, true);
+               }
+               else
+               {
+                  // Display the current line un-highlighted.
+                  console.gotoxy(1, curpos.y);
+                  this.WriteFileLibLine(selectedLibIndex, false);
+               }
+               selectedLibIndex = nextGrpIndex;
+            }
+            break;
+         case KEY_HOME: // Go to the top message group on the screen
+            if (selectedLibIndex > topFileLibIndex)
+            {
+               // Display the current line un-highlighted, then adjust
+               // selectedLibIndex.
+               console.gotoxy(1, curpos.y);
+               this.WriteFileLibLine(selectedLibIndex, false);
+               selectedLibIndex = topFileLibIndex;
+               // Note: curpos.y is set at the start of the while loop.
+            }
+            break;
+         case KEY_END: // Go to the bottom message group on the screen
+            if (selectedLibIndex < bottomFileLibIndex)
+            {
+               // Display the current line un-highlighted, then adjust
+               // selectedLibIndex.
+               console.gotoxy(1, curpos.y);
+               this.WriteFileLibLine(selectedLibIndex, false);
+               selectedLibIndex = bottomFileLibIndex;
+               // Note: curpos.y is set at the start of the while loop.
+            }
+            break;
+         case KEY_ENTER: // Select the currently-highlighted message group
+            retObj = this.SelectDirWithinFileLib_Lightbar(selectedLibIndex);
+            // If the user chose an area, then set bbs.curlib and
+            // bbs.curdir, and don't continue the input loop anymore.
+            if (retObj.fileDirChosen)
+            {
+               bbs.curlib = selectedLibIndex;
+               bbs.curdir = retObj.fileLibIndex;
+               continueChoosingFileArea = false;
+            }
+            else
+            {
+               // An area was not chosen, so we'll have to re-draw
+               // the header and list of message groups.
+               console.gotoxy(1, 1);
+               this.WriteLibListHdrLine(numPages, pageNum);
+               this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow, listEndRow,
+                                            false, true);
+            }
+            break;
+         case KEY_PAGE_DOWN: // Go to the next page
+            var nextPageTopIndex = topFileLibIndex + numItemsPerPage;
+            if (nextPageTopIndex < file_area.lib_list.length)
+            {
+               // Adjust topFileLibIndex and bottomFileLibIndex, and
+               // refresh the list on the screen.
+               topFileLibIndex = nextPageTopIndex;
+               pageNum = calcPageNum(topFileLibIndex, numItemsPerPage);
+               bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, true, false);
+               this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedLibIndex = topFileLibIndex;
+            }
+            break;
+         case KEY_PAGE_UP: // Go to the previous page
+            var prevPageTopIndex = topFileLibIndex - numItemsPerPage;
+            if (prevPageTopIndex >= 0)
+            {
+               // Adjust topFileLibIndex and bottomFileLibIndex, and
+               // refresh the list on the screen.
+               topFileLibIndex = prevPageTopIndex;
+               pageNum = calcPageNum(topFileLibIndex, numItemsPerPage);
+               bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, true, false);
+               this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedLibIndex = topFileLibIndex;
+            }
+            break;
+         case 'F': // Go to the first page
+            if (topFileLibIndex > 0)
+            {
+               topFileLibIndex = 0;
+               pageNum = calcPageNum(topFileLibIndex, numItemsPerPage);
+               bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, true, false);
+               this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow, listEndRow,
+                                            false, true);
+               selectedLibIndex = 0;
+            }
+            break;
+         case 'L': // Go to the last page
+            if (topFileLibIndex < topIndexForLastPage)
+            {
+               topFileLibIndex = topIndexForLastPage;
+               pageNum = calcPageNum(topFileLibIndex, numItemsPerPage);
+               bottomFileLibIndex = getBottommostLibIndex(topFileLibIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, true, false);
+               this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow, listEndRow,
+                                            false, true);
+               selectedLibIndex = topIndexForLastPage;
+            }
+            break;
+         case 'Q': // Quit
+            continueChoosingFileArea = false;
+            break;
+         case '?': // Show help
+            this.ShowHelpScreen(true, true);
+            console.pause();
+            // Refresh the screen
+            this.WriteKeyHelpLine();
+            console.gotoxy(1, 1);
+            this.WriteLibListHdrLine(numPages, pageNum);
+            this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow, listEndRow,
+                                         false, true);
+            break;
+         default:
+            // If the user entered a numeric digit, then treat it as
+            // the start of the message group number.
+            if (userInput.match(/[0-9]/))
+            {
+               var originalCurpos = curpos;
+
+               // Put the user's input back in the input buffer to
+               // be used for getting the rest of the message number.
+               console.ungetstr(userInput);
+               // Move the cursor to the bottom of the screen and
+               // prompt the user for the message number.
+               console.gotoxy(1, console.screen_rows);
+               console.clearline("\1n");
+               console.print("\1cChoose library #: \1h");
+               userInput = console.getnum(file_area.lib_list.length);
+               // If the user made a selection, then let them choose a
+               // directory from the file library.
+               if (userInput > 0)
+               {
+                  var selectedLibIndex = userInput - 1;
+                  var retObj = this.SelectDirWithinFileLib_Lightbar(selectedLibIndex);
+                  // If the user chose a sub-board, then set bbs.curlib and
+                  // bbs.curdir, and don't continue the input loop anymore.
+                  if (retObj.fileDirChosen)
+                  {
+                     bbs.curlib = selectedLibIndex;
+                     bbs.curdir = retObj.fileLibIndex;
+                     continueChoosingFileArea = false;
+                  }
+                  else
+                  {
+                     // A sub-board was not chosen, so we'll have to re-draw
+                     // the header and list of message groups.
+                     console.gotoxy(1, 1);
+                     this.WriteLibListHdrLine(numPages, pageNum);
+                     this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow, listEndRow,
+                                                  false, true);
+                  }
+               }
+               else
+               {
+                  // The user didn't make a selection.  So, we need to refresh
+                  // the screen due to everything being moved up one line.
+                  this.WriteKeyHelpLine();
+                  console.gotoxy(1, 1);
+                  this.WriteLibListHdrLine(numPages, pageNum);
+                  this.ListScreenfulOfFileLibs(topFileLibIndex, listStartRow, listEndRow,
+                                               false, true);
+               }
+            }
+            break;
+      }
+   }
+}
+
+// For the DDFileAreaChooser class: Lightbar interface for letting the user
+// choose a directory within a file library.
+//
+// Parameters:
+//  pLibIndex: The index of the message group to choose from.  This is
+//             optional; if not specified, bbs.curlib will be used.
+//  pHighlightIndex: An index of a message group to highlight.  This
+//                   is optional; if left off, this will default to
+//                   the current sub-board.
+//
+// Return value: An object containing the following values:
+//               fileDirChosen: Boolean - Whether or not a sub-board was chosen.
+//               fileLibIndex: Numeric - The sub-board that was chosen (if any).
+//                             Will be -1 if none chosen.
+function DDFileAreaChooser_selectDirWithinFileLib_Lightbar(pLibIndex, pHighlightIndex)
+{
+   // Create the return object.
+   var retObj = new Object();
+   retObj.fileDirChosen = false;
+   retObj.fileLibIndex = -1;
+
+   var libIndex = 0;
+   if (typeof(pLibIndex) == "number")
+      libIndex = pLibIndex;
+   else if ((bbs.curlib != null) && (typeof(bbs.curlib) == "number"))
+      libIndex = bbs.curlib;
+   // Double-check libIndex
+   if (libIndex < 0)
+      libIndex = 0;
+   else if (libIndex >= file_area.lib_list.length)
+      libIndex = file_area.lib_list.length - 1;
+
+   var highlightIndex = 0;
+   if ((pHighlightIndex != null) && (typeof(pHighlightIndex) == "number"))
+      highlightIndex = pHighlightIndex;
+   else if ((bbs.curdir != null) && (typeof(bbs.curdir) == "number") &&
+             (bbs.curlib == pLibIndex))
+   {
+      highlightIndex = bbs.curdir;
+   }
+   // Double-check highlightIndex
+   if (highlightIndex < 0)
+      highlightIndex = 0;
+   else if (highlightIndex >= file_area.lib_list[libIndex].dir_list.length)
+      highlightIndex = file_area.lib_list[libIndex].dir_list.length - 1;
+
+   // If there are no sub-boards in the given message group, then show
+   // an error and return.
+   if (file_area.lib_list[libIndex].dir_list.length == 0)
+   {
+      console.clear("\1n");
+      console.print("\1y\1hThere are no directories in the chosen library.\r\n\1p");
+      return retObj;
+   }
+   
+   // Ensure that the file directory printf information is created for
+   // this file library.
+   this.BuildFileDirPrintfInfoForLib(libIndex);
+
+   // Returns the index of the bottommost directory that can be displayed on
+   // the screen.
+   //
+   // Parameters:
+   //  pTopDirIndex: The index of the topmost directory displayed on screen
+   //  pNumItemsPerPage: The number of items per page
+   function getBottommostDirIndex(pTopDirIndex, pNumItemsPerPage)
+   {
+      var bottomDirIndex = pTopDirIndex + pNumItemsPerPage - 1;
+      // If bottomDirIndex is beyond the last index, then adjust it.
+      if (bottomDirIndex >= file_area.lib_list[libIndex].dir_list.length)
+         bottomDirIndex = file_area.lib_list[libIndex].dir_list.length - 1;
+      return bottomDirIndex;
+   }
+
+
+   // Figure out the index of the user's currently-selected sub-board.
+   var selectedDirIndex = 0;
+   if ((bbs.curdir != null) && (typeof(bbs.curdir) == "number"))
+   {
+      if ((bbs.curlib != null) && (typeof(bbs.curlib) == "number") &&
+          (bbs.curlib == pLibIndex))
+      {
+         selectedDirIndex = bbs.curdir;
+      }
+   }
+
+   var listStartRow = 3;      // The row on the screen where the list will start
+   var listEndRow = console.screen_rows - 1; // Row on screen where list will end
+   var topDirIndex = 0;      // The index of the message group at the top of the list
+   // Figure out the index of the last message group to appear on the screen.
+   var numItemsPerPage = listEndRow - listStartRow + 1;
+   var bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+   // Figure out how many pages are needed to list all the sub-boards.
+   var numPages = Math.ceil(file_area.lib_list[libIndex].dir_list.length / numItemsPerPage);
+   var pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+   // Figure out the top index for the last page.
+   var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage;
+
+   // If the highlighted row is beyond the current screen, then
+   // go to the appropriate page.
+   if (selectedDirIndex > bottomDirIndex)
+   {
+      var nextPageTopIndex = 0;
+      while (selectedDirIndex > bottomDirIndex)
+      {
+         nextPageTopIndex = topDirIndex + numItemsPerPage;
+         if (nextPageTopIndex < file_area.lib_list[libIndex].dir_list.length)
+         {
+            // Adjust topDirIndex and bottomDirIndex, and
+            // refresh the list on the screen.
+            topDirIndex = nextPageTopIndex;
+            pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+            bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+         }
+         else
+            break;
+      }
+
+      // If we didn't find the correct page for some reason, then set the
+      // variables to display page 1 and select the first message group.
+      var foundCorrectPage =
+          ((topDirIndex < file_area.lib_list[libIndex].dir_list.length) &&
+           (selectedDirIndex >= topDirIndex) && (selectedDirIndex <= bottomDirIndex));
+      if (!foundCorrectPage)
+      {
+         topDirIndex = 0;
+         pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+         bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+         selectedDirIndex = 0;
+      }
+   }
+
+   // Clear the screen, write the help line and group list header, and output
+   // a screenful of message groups.
+   console.clear("\1n");
+   this.WriteDirListHdr1Line(libIndex, numPages, pageNum);
+   this.WriteKeyHelpLine();
+
+   var curpos = new Object();
+   curpos.x = 1;
+   curpos.y = 2;
+   console.gotoxy(curpos);
+   printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Files");
+   this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow, listEndRow,
+                               false, false);
+   // Start of the input loop.
+   var highlightScrenRow = 0; // The row on the screen for the highlighted group
+   var userInput = "";        // Will store a keypress from the user
+   var continueChoosingFileDir = true;
+   while (continueChoosingFileDir)
+   {
+      // Highlight the currently-selected message group
+      highlightScrenRow = listStartRow + (selectedDirIndex - topDirIndex);
+      curpos.y = highlightScrenRow;
+      if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows))
+      {
+         console.gotoxy(1, highlightScrenRow);
+         this.WriteFileLibDirLine(libIndex, selectedDirIndex, true);
+      }
+
+      // Get a key from the user (upper-case) and take action based upon it.
+      userInput = getKeyWithESCChars(K_UPPER | K_NOCRLF);
+      switch (userInput)
+      {
+         case KEY_UP: // Move up one message group in the list
+            if (selectedDirIndex > 0)
+            {
+               // If the previous group index is on the previous page, then
+               // display the previous page.
+               var previousSubIndex = selectedDirIndex - 1;
+               if (previousSubIndex < topDirIndex)
+               {
+                  // Adjust topDirIndex and bottomDirIndex, and
+                  // refresh the list on the screen.
+                  topDirIndex -= numItemsPerPage;
+                  pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+                  bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+                  this.updatePageNumInHeader(pageNum, numPages, false, false);
+                  this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow,
+                                              listEndRow, false, true);
+               }
+               else
+               {
+                  // Display the current line un-highlighted.
+                  console.gotoxy(1, curpos.y);
+                  this.WriteFileLibDirLine(libIndex, selectedDirIndex, false);
+               }
+               selectedDirIndex = previousSubIndex;
+            }
+            break;
+         case KEY_DOWN: // Move down one message group in the list
+            if (selectedDirIndex < file_area.lib_list[libIndex].dir_list.length - 1)
+            {
+               // If the next group index is on the next page, then display
+               // the next page.
+               var nextGrpIndex = selectedDirIndex + 1;
+               if (nextGrpIndex > bottomDirIndex)
+               {
+                  // Adjust topDirIndex and bottomDirIndex, and
+                  // refresh the list on the screen.
+                  topDirIndex += numItemsPerPage;
+                  pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+                  bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+                  this.updatePageNumInHeader(pageNum, numPages, false, false);
+                  this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow,
+                                              listEndRow, false, true);
+               }
+               else
+               {
+                  // Display the current line un-highlighted.
+                  console.gotoxy(1, curpos.y);
+                  this.WriteFileLibDirLine(libIndex, selectedDirIndex, false);
+               }
+               selectedDirIndex = nextGrpIndex;
+            }
+            break;
+         case KEY_HOME: // Go to the top message group on the screen
+            if (selectedDirIndex > topDirIndex)
+            {
+               // Display the current line un-highlighted, then adjust
+               // selectedDirIndex.
+               console.gotoxy(1, curpos.y);
+               this.WriteFileLibDirLine(libIndex, selectedDirIndex, false);
+               selectedDirIndex = topDirIndex;
+               // Note: curpos.y is set at the start of the while loop.
+            }
+            break;
+         case KEY_END: // Go to the bottom message group on the screen
+            if (selectedDirIndex < bottomDirIndex)
+            {
+               // Display the current line un-highlighted, then adjust
+               // selectedDirIndex.
+               console.gotoxy(1, curpos.y);
+               this.WriteFileLibDirLine(libIndex, selectedDirIndex, false);
+               selectedDirIndex = bottomDirIndex;
+               // Note: curpos.y is set at the start of the while loop.
+            }
+            break;
+         case KEY_ENTER: // Select the currently-highlighted sub-board; and we're done.
+            continueChoosingFileDir = false;
+            retObj.fileDirChosen = true;
+            retObj.fileLibIndex = selectedDirIndex;
+            break;
+         case KEY_PAGE_DOWN: // Go to the next page
+            var nextPageTopIndex = topDirIndex + numItemsPerPage;
+            if (nextPageTopIndex < file_area.lib_list[libIndex].dir_list.length)
+            {
+               // Adjust topDirIndex and bottomDirIndex, and
+               // refresh the list on the screen.
+               topDirIndex = nextPageTopIndex;
+               pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+               bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, false, false);
+               this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedDirIndex = topDirIndex;
+            }
+            break;
+         case KEY_PAGE_UP: // Go to the previous page
+            var prevPageTopIndex = topDirIndex - numItemsPerPage;
+            if (prevPageTopIndex >= 0)
+            {
+               // Adjust topDirIndex and bottomDirIndex, and
+               // refresh the list on the screen.
+               topDirIndex = prevPageTopIndex;
+               pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+               bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, false, false);
+               this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedDirIndex = topDirIndex;
+            }
+            break;
+         case 'F': // Go to the first page
+            if (topDirIndex > 0)
+            {
+               topDirIndex = 0;
+               pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+               bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, false, false);
+               this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedDirIndex = 0;
+            }
+            break;
+         case 'L': // Go to the last page
+            if (topDirIndex < topIndexForLastPage)
+            {
+               topDirIndex = topIndexForLastPage;
+               pageNum = calcPageNum(topDirIndex, numItemsPerPage);
+               bottomDirIndex = getBottommostDirIndex(topDirIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, false, false);
+               this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedDirIndex = topIndexForLastPage;
+            }
+            break;
+         case 'Q': // Quit
+            continueChoosingFileDir = false;
+            break;
+         case '?': // Show help
+            this.ShowHelpScreen(true, true);
+            console.pause();
+            // Refresh the screen
+            console.gotoxy(1, 1);
+            this.WriteDirListHdr1Line(libIndex, numPages, pageNum);
+            console.cleartoeol("\1n");
+            this.WriteKeyHelpLine();
+            console.gotoxy(1, 2);
+            printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Files");
+            this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow,
+                                      listEndRow, false, true);
+            break;
+         default:
+            // If the user entered a numeric digit, then treat it as
+            // the start of the message group number.
+            if (userInput.match(/[0-9]/))
+            {
+               var originalCurpos = curpos;
+
+               // Put the user's input back in the input buffer to
+               // be used for getting the rest of the message number.
+               console.ungetstr(userInput);
+               // Move the cursor to the bottom of the screen and
+               // prompt the user for the message number.
+               console.gotoxy(1, console.screen_rows);
+               console.clearline("\1n");
+               console.print("\1cDir #: \1h");
+               userInput = console.getnum(file_area.lib_list[libIndex].dir_list.length);
+               // If the user made a selection, then set it in the
+               // return object and don't continue the input loop.
+               if (userInput > 0)
+               {
+                  continueChoosingFileDir = false;
+                  retObj.fileDirChosen = true;
+                  retObj.fileLibIndex = userInput - 1;
+               }
+               else
+               {
+                  // The user didn't enter a selection.  Now we need to
+                  // re-draw the screen due to everything being moved
+                  // up one line.
+                  console.gotoxy(1, 1);
+                  this.WriteDirListHdr1Line(libIndex, numPages, pageNum);
+                  console.cleartoeol("\1n");
+                  this.WriteKeyHelpLine();
+                  console.gotoxy(1, 2);
+                  printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Files");
+                  this.ListScreenfulOfDirs(libIndex, topDirIndex, listStartRow,
+                                            listEndRow, false, true);
+               }
+            }
+            break;
+      }
+   }
+
+   return retObj;
+}
+
+// Displays a screenful of file libraries (for the lightbar interface).
+//
+// Parameters:
+//  pStartIndex: The file library index to start at (0-based)
+//  pStartScreenRow: The row on the screen to start at (1-based)
+//  pEndScreenRow: The row on the screen to end at (1-based)
+//  pClearScreenFirst: Boolean - Whether or not to clear the screen first
+//  pBlankToEndRow: Boolean - Whether or not to write blank lines to the end
+//                  screen row if there aren't enough message groups to fill
+//                  the screen.
+function DDFileAreaChooser_listScreenfulOfFileLibs(pStartIndex, pStartScreenRow,
+                                                    pEndScreenRow, pClearScreenFirst,
+                                                    pBlankToEndRow)
+{
+   // Check the parameters; If they're bad, then just return.
+   if ((typeof(pStartIndex) != "number") ||
+       (typeof(pStartScreenRow) != "number") ||
+       (typeof(pEndScreenRow) != "number"))
+   {
+      return;
+   }
+   if ((pStartIndex < 0) || (pStartIndex >= file_area.lib_list.length))
+      return;
+   if ((pStartScreenRow < 1) || (pStartScreenRow > console.screen_rows))
+      return;
+   if ((pEndScreenRow < 1) || (pEndScreenRow > console.screen_rows))
+      return;
+
+   // If pStartScreenRow is greather than pEndScreenRow, then swap them.
+   if (pStartScreenRow > pEndScreenRow)
+   {
+      var temp = pStartScreenRow;
+      pStartScreenRow = pEndScreenRow;
+      pEndScreenRow = temp;
+   }
+
+   // Calculate the ending index to use for the file libraries array.
+   var endIndex = pStartIndex + (pEndScreenRow-pStartScreenRow);
+   if (endIndex >= file_area.lib_list.length)
+      endIndex = file_area.lib_list.length - 1;
+   var onePastEndIndex = endIndex + 1;
+
+   // Check to make sure bbs.curlib is valid (it might not be for brand-new users).
+   var curlibValid = ((bbs.curlib != null) && (typeof(bbs.curlib) != "undefined"));
+
+   // Clear the screen, go to the specified screen row, and display the message
+   // group information.
+   if (pClearScreenFirst)
+      console.clear("\1n");
+   console.gotoxy(1, pStartScreenRow);
+   var libIndex = pStartIndex;
+   for (; libIndex < onePastEndIndex; ++libIndex)
+   {
+      this.WriteFileLibLine(libIndex, false);
+      if (libIndex < endIndex)
+         console.crlf();
+   }
+
+   // If pBlankToEndRow is true and we're not at the end row yet, then
+   // write blank lines to the end row.
+   if (pBlankToEndRow)
+   {
+      var screenRow = pStartScreenRow + (endIndex - pStartIndex) + 1;
+      if (screenRow <= pEndScreenRow)
+      {
+         for (; screenRow <= pEndScreenRow; ++screenRow)
+         {
+            console.gotoxy(1, screenRow);
+            console.clearline("\1n");
+         }
+      }
+   }
+}
+
+// For the DDFileAreaChooser class - Writes a file library information line.
+//
+// Parameters:
+//  pLibIndex: The index of the file library to write (assumed to be valid)
+//  pHighlight: Boolean - Whether or not to write the line highlighted.
+function DDFileAreaChooser_writeFileLibLine(pLibIndex, pHighlight)
+{
+   console.print("\1n");
+   // Write the highlight background color if pHighlight is true.
+   if (pHighlight)
+      console.print(this.colors.bkgHighlight);
+
+   var curlibValid = (typeof(bbs.curlib) == "number");
+
+   // Write the message group information line
+   console.print((curlibValid && (pLibIndex == bbs.curlib)) ? this.colors.areaMark + "*" : " ");
+   printf((pHighlight ? this.fileLibHighlightPrintfStr : this.fileLibPrintfStr),
+          +(pLibIndex+1),
+          file_area.lib_list[pLibIndex].description.substr(0, this.descFieldLen),
+          file_area.lib_list[pLibIndex].dir_list.length);
+   console.cleartoeol("\1n");
+}
+
+// For the DDFileAreaChooser class: Writes a file directory information line.
+//
+// Parameters:
+//  pLibIndex: The index of the file library (assumed to be valid)
+//  pDirIndex: The index of the directory within the file library to write (assumed to be valid)
+//  pHighlight: Boolean - Whether or not to write the line highlighted.
+function DDFileAreaChooser_writeFileLibDirLine(pLibIndex, pDirIndex, pHighlight)
+{
+   console.print("\1n");
+   // Write the highlight background color if pHighlight is true.
+   if (pHighlight)
+      console.print(this.colors.bkgHighlight);
+
+   // Determine if pLibIndex and pDirIndex specify the user's
+   // currently-selected file library & directory.
+   var currentDir = false;
+   if ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number"))
+      currentDir = ((pLibIndex == bbs.curlib) && (pDirIndex == bbs.curdir));
+
+   // Print the directory information line
+   console.print(currentDir ? this.colors.areaMark + "*" : " ");
+   printf((pHighlight ? this.fileDirListPrintfInfo[pLibIndex].highlightPrintfStr : this.fileDirListPrintfInfo[pLibIndex].printfStr),
+          +(pDirIndex+1),
+          file_area.lib_list[pLibIndex].dir_list[pDirIndex].description.substr(0, this.descFieldLen),
+          this.fileDirListPrintfInfo[pLibIndex].fileCounts[pDirIndex]);
+}
+
+// Updates the page number text in the file group/area list header line on the screen.
+//
+// Parameters:
+//  pPageNum: The page number
+//  pNumPages: The total number of pages
+//  pFileLib: Boolean - Whether or not this is for the file library header.  If so,
+//            then this will go to the right location for the file group page text
+//            and use this.colors.header for the text.  Otherwise, this will
+//            go to the right place for the file area page text and use the
+//            file area header color.
+//  pRestoreCurPos: Optional - Boolean - If true, then move the cursor back
+//                  to the position where it was before this function was called
+function DDFileAreaChooser_updatePageNumInHeader(pPageNum, pNumPages, pFileLib, pRestoreCurPos)
+{
+  var originalCurPos = null;
+  if (pRestoreCurPos)
+    originalCurPos = console.getxy();
+
+  if (pFileLib)
+  {
+    console.gotoxy(30, 1);
+    console.print("\1n" + this.colors.header + pPageNum + " of " + pNumPages + ")   ");
+  }
+  else
+  {
+    console.gotoxy(67, 1);
+    console.print("\1n" + this.colors.fileAreaHdr + pPageNum + " of " + pNumPages + ")   ");
+  }
+
+  if (pRestoreCurPos)
+    console.gotoxy(originalCurPos);
+}
+
+// Displays a screenful of file directories, for the lightbar interface.
+//
+// Parameters:
+//  pLibIndex: The index of the file library (0-based)
+//  pStartDirIndex: The file directory index to start at (0-based)
+//  pStartScreenRow: The row on the screen to start at (1-based)
+//  pEndScreenRow: The row on the screen to end at (1-based)
+//  pClearScreenFirst: Boolean - Whether or not to clear the screen first
+//  pBlankToEndRow: Boolean - Whether or not to write blank lines to the end
+//                  screen row if there aren't enough message groups to fill
+//                  the screen.
+function DDMsgAreaChooser_listScreenfulOfFileDirs(pLibIndex, pStartDirIndex,
+                                                  pStartScreenRow, pEndScreenRow,
+                                                  pClearScreenFirst, pBlankToEndRow)
+{
+   // Check the parameters; If they're bad, then just return.
+   if ((typeof(pLibIndex) != "number") ||
+       (typeof(pStartDirIndex) != "number") ||
+       (typeof(pStartScreenRow) != "number") ||
+       (typeof(pEndScreenRow) != "number"))
+   {
+      return;
+   }
+   if ((pLibIndex < 0) || (pLibIndex >= file_area.lib_list.length))
+      return;
+   if ((pStartDirIndex < 0) ||
+       (pStartDirIndex >= file_area.lib_list[pLibIndex].dir_list.length))
+   {
+      return;
+   }
+   if ((pStartScreenRow < 1) || (pStartScreenRow > console.screen_rows))
+      return;
+   if ((pEndScreenRow < 1) || (pEndScreenRow > console.screen_rows))
+      return;
+   // If pStartScreenRow is greather than pEndScreenRow, then swap them.
+   if (pStartScreenRow > pEndScreenRow)
+   {
+      var temp = pStartScreenRow;
+      pStartScreenRow = pEndScreenRow;
+      pEndScreenRow = temp;
+   }
+
+   // Calculate the ending index to use for the sub-board array.
+   var endIndex = pStartDirIndex + (pEndScreenRow-pStartScreenRow);
+   if (endIndex >= file_area.lib_list[pLibIndex].dir_list.length)
+      endIndex = file_area.lib_list[pLibIndex].dir_list.length - 1;
+   var onePastEndIndex = endIndex + 1;
+
+   // Clear the screen and go to the specified screen row.
+   if (pClearScreenFirst)
+      console.clear("\1n");
+   console.gotoxy(1, pStartScreenRow);
+
+   // Start listing the file  directories.
+
+   var dirIndex = pStartDirIndex;
+   for (; dirIndex < onePastEndIndex; ++dirIndex)
+   {
+      this.WriteFileLibDirLine(pLibIndex, dirIndex, false);
+      if (dirIndex < endIndex)
+         console.crlf();
+   }
+
+   // If pBlankToEndRow is true and we're not at the end row yet, then
+   // write blank lines to the end row.
+   if (pBlankToEndRow)
+   {
+      var screenRow = pStartScreenRow + (endIndex - pStartDirIndex) + 1;
+      if (screenRow <= pEndScreenRow)
+      {
+         for (; screenRow <= pEndScreenRow; ++screenRow)
+         {
+            console.gotoxy(1, screenRow);
+            console.clearline("\1n");
+         }
+      }
+   }
+}
+
+function DDFileAreaChooser_writeKeyHelpLine()
+{
+	console.gotoxy(1, console.screen_rows);
+	console.print(this.lightbarKeyHelpText);
+}
+
+// For the DDFileAreaChooser class: Reads the configuration file.
+function DDFileAreaChooser_ReadConfigFile()
+{
+   // 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 startup_path = '.';
+   try { throw dig.dist(dist); } catch(e) { startup_path = e.fileName; }
+   startup_path = backslash(startup_path.replace(/[\/\\][^\/\\]*$/,''));
+
+   // Open the configuration file
+   var cfgFile = new File(startup_path + "DDFileAreaChooser.cfg");
+   if (cfgFile.open("r"))
+   {
+      var settingsMode = "behavior";
+      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;        // 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 in the "behavior" section, then set the behavior-related variables.
+         if (fileLine.toUpperCase() == "[BEHAVIOR]")
+         {
+            settingsMode = "behavior";
+            continue;
+         }
+         else if (fileLine.toUpperCase() == "[COLORS]")
+         {
+            settingsMode = "colors";
+            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);
+
+            if (settingsMode == "behavior")
+            {
+               // Set the appropriate value in the settings object.
+               if (settingUpper == "USELIGHTBARINTERFACE")
+                  this.useLightbarInterface = (value.toUpperCase() == "TRUE");
+            }
+            else if (settingsMode == "colors")
+               this.colors[setting] = value;
+         }
+      }
+   
+      cfgFile.close();
+   }
+}
+
+// Misc. functions
+
+// For the DDFileAreaChooser class: Shows the help screen
+//
+// Parameters:
+//  pLightbar: Boolean - Whether or not to show lightbar help.  If
+//             false, then this function will show regular help.
+//  pClearScreen: Boolean - Whether or not to clear the screen first
+function DDFileAreaChooser_showHelpScreen(pLightbar, pClearScreen)
+{
+	if (pClearScreen)
+		console.clear("\1n");
+	else
+		console.print("\1n");
+	console.center("\1c\1hDigital Distortion File Area Chooser");
+	console.center("\1k������������������������������������");
+	console.center("\1n\1cVersion \1g" + DD_FILE_AREA_CHOOSER_VERSION +
+	               " \1w\1h(\1b" + DD_FILE_AREA_CHOOSER_VER_DATE + "\1w)");
+	console.crlf();
+	console.print("\1n\1cFirst, a listing of file libraries is displayed.  One can be chosen by typing");
+	console.crlf();
+	console.print("its number.  Then, a listing of directories within that library will be");
+	console.crlf();
+	console.print("shown, and one can be chosen by typing its number.");
+	console.crlf();
+
+	if (pLightbar)
+	{
+		console.crlf();
+		console.print("\1n\1cThe lightbar interface also allows up & down navigation through the lists:");
+		console.crlf();
+		console.print("\1k\1h��������������������������������������������������������������������������");
+		console.crlf();
+		console.print("\1n\1c\1hUp arrow\1n\1c: Move the cursor up one line");
+		console.crlf();
+		console.print("\1hDown arrow\1n\1c: Move the cursor down one line");
+		console.crlf();
+		console.print("\1hENTER\1n\1c: Select the current library/dir");
+		console.crlf();
+		console.print("\1hHOME\1n\1c: Go to the first item on the screen");
+		console.crlf();
+		console.print("\1hEND\1n\1c: Go to the last item on the screen");
+		console.crlf();
+		console.print("\1hPageUp\1n\1c/\1hPageDown\1n\1c: Go to the previous/next page");
+		console.crlf();
+		console.print("\1hF\1n\1c/\1hL\1n\1c: Go to the first/last page");
+		console.crlf();
+	}
+
+	console.crlf();
+	console.print("Additional keyboard commands:");
+	console.crlf();
+	console.print("\1k\1h�����������������������������");
+	console.crlf();
+	console.print("\1n\1c\1h?\1n\1c: Show this help screen");
+	console.crlf();
+	console.print("\1hQ\1n\1c: Quit");
+	console.crlf();
+}
+
+// Returns the number of files in a directory for one of the file libraries.
+// Note that returns the number of files in the directory on the hard drive,
+// which isn't necessarily the number of files in the database for the directory.
+//
+// Paramters:
+//  pLibNum: The file library number (0-based)
+//  pDirNum: The file directory number (0-based)
+//
+// Returns: The number of files in the directory
+function DDFileAreaChooser_NumFilesInDir(pLibNum, pDirNum)
+{
+   var numFiles = 0;
+
+   // Count the files in the directory.  If it's not a directory, then
+   // increment numFiles.
+   var files = directory(file_area.lib_list[pLibNum].dir_list[pDirNum].path + "*.*");
+   numFiles = files.length;
+   // Make sure directories aren't counted: Go through the files array, and
+   // for each directory, decrement numFiles.
+   for (var i in files)
+   {
+      if (file_isdir(files[i]))
+         --numFiles;
+   }
+
+   return numFiles;
+}
+
+// Builds file directory printf format information for a file library.
+// The widths of the description & # files columns are calculated
+// based on the greatest number of files in a directory for the
+// file library.
+//
+// Parameters:
+//  pLibIndex: The index of the file library
+function DDFileAreaChooser_buildFileDirPrintfInfoForLib(pLibIndex)
+{
+  if (typeof(this.fileDirListPrintfInfo[pLibIndex]) == "undefined")
+  {
+    // Create the file directory listing object and set some defaults
+    this.fileDirListPrintfInfo[pLibIndex] = new Object();
+    this.fileDirListPrintfInfo[pLibIndex].numFilesLen = 4;
+    // Get information about the number of files in each directory
+    // and the greatest number of files and set up the according
+    // information in the file directory list object
+    var fileDirInfo = getGreatestNumFiles(pLibIndex);
+    if (fileDirInfo != null)
+    {
+      this.fileDirListPrintfInfo[pLibIndex].numFilesLen = fileDirInfo.greatestNumFiles.toString().length;
+      this.fileDirListPrintfInfo[pLibIndex].fileCounts = fileDirInfo.fileCounts.slice(0);
+    }
+    else
+    {
+      // fileDirInfo is null.  We still want to create
+      // the fileCounts array in the file directory object
+      // so that it's valid.
+      this.fileDirListPrintfInfo[pLibIndex].fileCounts = new Array(file_area.lib_list[pLibIndex].length);
+      for (var i = 0; i < file_area.lib_list[pLibIndex].length; ++i)
+        this.fileDirListPrintfInfo[pLibIndex].fileCounts[i] == 0;
+    }
+
+    // Set the description field length and printf strings for
+    // this file library
+    this.fileDirListPrintfInfo[pLibIndex].descFieldLen =
+                        console.screen_columns - this.areaNumLen
+                        - this.fileDirListPrintfInfo[pLibIndex].numFilesLen - 5;
+    this.fileDirListPrintfInfo[pLibIndex].printfStr =
+                        this.colors.areaNum + " %" + this.areaNumLen + "d "
+                        + this.colors.desc + "%-"
+                        + this.fileDirListPrintfInfo[pLibIndex].descFieldLen
+                        + "s " + this.colors.numItems + "%"
+                        + this.fileDirListPrintfInfo[pLibIndex].numFilesLen + "d";
+    this.fileDirListPrintfInfo[pLibIndex].highlightPrintfStr =
+                        "\1n" + this.colors.bkgHighlight
+                        + this.colors.areaNumHighlight + " %" + this.areaNumLen
+                        + "d " + this.colors.descHighlight + "%-"
+                        + this.fileDirListPrintfInfo[pLibIndex].descFieldLen
+                        + "s " + this.colors.numItemsHighlight + "%"
+                        + this.fileDirListPrintfInfo[pLibIndex].numFilesLen +"d\1n";
+  }
+}
+
+// Removes multiple, leading, and/or trailing spaces
+// The search & replace regular expressions used in this
+// function came from the following URL:
+//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
+//
+// Parameters:
+//  pString: The string to trim
+//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
+//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
+//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
+function trimSpaces(pString, pLeading, pMultiple, pTrailing)
+{
+	var leading = true;
+	var multiple = true;
+	var trailing = true;
+	if(typeof(pLeading) != "undefined")
+		leading = pLeading;
+	if(typeof(pMultiple) != "undefined")
+		multiple = pMultiple;
+	if(typeof(pTrailing) != "undefined")
+		trailing = pTrailing;
+		
+	// To remove both leading & trailing spaces:
+	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");
+
+	if (leading)
+		pString = pString.replace(/(^\s*)/gi,"");
+	if (multiple)
+		pString = pString.replace(/[ ]{2,}/gi," ");
+	if (trailing)
+		pString = pString.replace(/(\s*$)/gi,"");
+
+	return pString;
+}
+
+// Calculates & returns a page number.
+//
+// Parameters:
+//  pTopIndex: The index (0-based) of the topmost item on the page
+//  pNumPerPage: The number of items per page
+//
+// Return value: The page number
+function calcPageNum(pTopIndex, pNumPerPage)
+{
+  return ((pTopIndex / pNumPerPage) + 1);
+}
+
+// For a given file library index, returns an object containing
+// the greatest number of files of all directories within a file
+// library and an array containing the number of files in each
+// directory.  If the given library index is invalid, this
+// function will return null.
+//
+// Parameters:
+//  pLibIndex: The index of the file library
+//
+// Returns: An object containing the following properties:
+//          greatestNumFiles: The greatest number of files of all
+//                            directories within the file library
+//          fileCounts: An array, indexed by directory index,
+//                      containing the number of files in each
+//                      directory within the file library
+function getGreatestNumFiles(pLibIndex)
+{
+  // Sanity checking
+  if (typeof(pLibIndex) != "number")
+    return null;
+  if (typeof(file_area.lib_list[pLibIndex]) == "undefined")
+    return null;
+
+  var retObj = new Object();
+  retObj.greatestNumFiles = 0;
+  retObj.fileCounts = new Array(file_area.lib_list[pLibIndex].dir_list.length);
+  for (var dirIndex = 0; dirIndex < file_area.lib_list[pLibIndex].dir_list.length; ++dirIndex)
+  {
+    retObj.fileCounts[dirIndex] = DDFileAreaChooser_NumFilesInDir(pLibIndex, dirIndex);
+    if (retObj.fileCounts[dirIndex] > retObj.greatestNumFiles)
+      retObj.greatestNumFiles = retObj.fileCounts[dirIndex];
+  }
+  return retObj;
+}
+
+// Inputs a keypress from the user and handles some ESC-based
+// characters such as PageUp, PageDown, and ESC.  If PageUp
+// or PageDown are pressed, this function will return the
+// string "\1PgUp" (KEY_PAGE_UP) or "\1Pgdn" (KEY_PAGE_DOWN),
+// respectively.  Also, F1-F5 will be returned as "\1F1"
+// through "\1F5", respectively.
+// Thanks goes to Psi-Jack for the original impementation
+// of this function.
+//
+// Parameters:
+//  pGetKeyMode: Optional - The mode bits for console.getkey().
+//               If not specified, K_NONE will be used.
+//
+// Return value: The user's keypress
+function getKeyWithESCChars(pGetKeyMode)
+{
+   var getKeyMode = K_NONE;
+   if (typeof(pGetKeyMode) == "number")
+      getKeyMode = pGetKeyMode;
+
+   var userInput = console.getkey(getKeyMode);
+   if (userInput == KEY_ESC) {
+      switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+         case '[':
+            switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+               case 'V':
+                  userInput = KEY_PAGE_UP;
+                  break;
+               case 'U':
+                  userInput = KEY_PAGE_DOWN;
+                  break;
+           }
+           break;
+         case 'O':
+           switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+              case 'P':
+                 userInput = "\1F1";
+                 break;
+              case 'Q':
+                 userInput = "\1F2";
+                 break;
+              case 'R':
+                 userInput = "\1F3";
+                 break;
+              case 'S':
+                 userInput = "\1F4";
+                 break;
+              case 't':
+                 userInput = "\1F5";
+                 break;
+           }
+         default:
+           break;
+      }
+   }
+
+   return userInput;
+}
\ No newline at end of file
diff --git a/xtrn/DDAreaChoosers/DDMsgAreaChooser.cfg b/xtrn/DDAreaChoosers/DDMsgAreaChooser.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..7e5cd45fe00f5dfb2be5d5d6f78af43fd0b96a6d
--- /dev/null
+++ b/xtrn/DDAreaChoosers/DDMsgAreaChooser.cfg
@@ -0,0 +1,43 @@
+[BEHAVIOR]
+useLightbarInterface=true
+; If showImportDates is true, then the date shown for the latest date
+; will be the messaeg import date.  If false, the date will represent
+; the timestamp in the message.
+showImportDates=true
+
+[COLORS]
+; Area number
+areaNum=nwh
+; Description
+desc=nc
+; Number of items
+numItems=bh
+; List header
+header=nyh
+; For the sub-board list header that has "Sub-boards for" the group and
+; has the page number
+subBoardHeader=ng
+; Mark character for areas that are currently selected
+areaMark=gh
+; Latest message date
+latestDate=ng
+; Latest message time
+latestTime=nm
+; Highlighted colors (for lightbar mode)
+bkgHighlight=4
+areaNumHighlight=wh
+descHighlight=c
+dateHighlight=wh
+timeHighlight=wh
+numItemsHighlight=wh
+
+; Colors for the lightbar help line text:
+; Background
+lightbarHelpLineBkg=7
+; The color for general text in the lightbar help line
+lightbarHelpLineGeneral=b
+; The color for the hotkeys in the lightbar help line
+lightbarHelpLineHotkey=r
+; The color for the ) separating the hotkeys from the general text in the
+; lightbar help line
+lightbarHelpLineParen=m
\ No newline at end of file
diff --git a/xtrn/DDAreaChoosers/DDMsgAreaChooser.js b/xtrn/DDAreaChoosers/DDMsgAreaChooser.js
new file mode 100644
index 0000000000000000000000000000000000000000..62fd3ffe4b0869d36f1668df2a13a19265faf45c
--- /dev/null
+++ b/xtrn/DDAreaChoosers/DDMsgAreaChooser.js
@@ -0,0 +1,1900 @@
+/* This is a script that lets the user choose a message area,
+ * with either a lightbar or traditional user interface.
+ *
+ * Date       User          Version Description
+ * 2010-02-05 Eric Oulashin 0.90    Started
+ * 2010-02-18 to
+ * 2010-02-27 Eric Oulashin         Continued work.
+ *                                  Added the first & lasg page functionality
+ *                                  to the lightbar interface.
+ * 2010-03-13 Eric Oulashin 1.00    Added the ability to load settings from a
+ *                                  configuration file.
+ * 2011-04-22 Eric Oulashin 1.01    Fixed the wording when choosing a message
+ *                                  group - It now says "group #" instead
+ *                                  of "sub-board #".
+ * 2012-10-06 Eric Oulashin 1.02    For lightbar mode, updated to display the
+ *                                  page number in the header at the top (in
+ *                                  addition to the total number of pages,
+ *                                  which it was already displaying).
+ * 2012-11-30 Eric Oulashin 1.03    Bug fix: After leaving the help screen
+ *                                  from the sub-board list, the top line is
+ *                                  now correctly written with the page
+ *                                  information as "Page # of #".
+ * 2013-05-04 Eric Oulashin 1.04    Updated to dynamically adjust the length
+ *                                  of the # messages column based on the
+ *                                  greatest number of messages of all
+ *                                  sub-boards within a message group so
+ *                                  that the formatting still looks good.
+ * 2013-05-10 Eric Oulashin 1.05    Updated the version to match the
+ *                                  version in DDFileAreaChooser (a bug
+ *                                  was fixed there, but DDMsgAreaChooser
+ *                                  didn't have the corresponding bug).
+ * 2014-09-14 Eric Oulashin 1.06    Bug fix: Updated the highlight (lightbar)
+ *                                  format string in the
+ *                                  DDMsgAreaChooser_buildSubBoardPrintfInfoForGrp()
+ *                                  function to include a normal attribute at
+ *                                  the end to avoid color issues when clearing
+ *                                  the screen, etc.  Bug reported by Psi-Jack.
+ * 2014-12-22 Eric Oulashin 1.07    Bug fix: Made this.colors.subBoardHeader apply
+ *                                  to the whole line rather than just the page
+ *                                  number.
+ *                                  Bug fix: The initial display of the page number
+ *                                  is now correct (previously, it would start out
+ *                                  saying page 1, even if on another page).
+ * 2015-04-19 Eric Oulashin 1.08    Added color settings for the lightbar help text
+ *                                  at the bottom of the screen.  Also, added the
+ *									ability to use the PageUp & PageDown keys instead
+ *                                  of P and N in the lightbar lists.
+*/
+
+/* Command-line arguments:
+   1 (argv[0]): Boolean - Whether or not to run the area chooser (if false,
+                then this file will just provide the DDMsgAreaChooser class).
+*/
+
+load("sbbsdefs.js");
+
+// This script requires Synchronet version 3.14 or higher.
+// Exit if the Synchronet version is below the minimum.
+if (system.version_num < 31400)
+{
+	var message = "\1n\1h\1y\1i* Warning:\1n\1h\1w Digital Distortion Message Lister "
+	             + "requires version \1g3.14\1w or\r\n"
+	             + "higher of Synchronet.  This BBS is using version \1g" + system.version
+	             + "\1w.  Please notify the sysop.";
+	console.crlf();
+	console.print(message);
+	console.crlf();
+	console.pause();
+	exit();
+}
+
+// Version & date variables
+var DD_MSG_AREA_CHOOSER_VERSION = "1.08";
+var DD_MSG_AREA_CHOOSER_VER_DATE = "2015-04-19";
+
+// Keyboard input key codes
+var CTRL_M = "\x0d";
+var KEY_ENTER = CTRL_M;
+var KEY_ESC = ascii(27);
+// PageUp & PageDown keys - Not real key codes, but codes defined
+// to be used & recognized in this script
+var KEY_PAGE_UP = "\1PgUp";
+var KEY_PAGE_DOWN = "\1PgDn";
+
+// Key codes for display
+var UP_ARROW = ascii(24);
+var DOWN_ARROW = ascii(25);
+
+// Determine whether or not to execute the message listing code, based
+// on the first program argument (a boolean).
+var executeThisScript = true;
+if (typeof(argv[0]) != "undefined")
+	executeThisScript = argv[0];
+
+// If executeThisScript is true, then create a DDMsgAreaChooser object and use
+// it to let the user choose a message area.
+if (executeThisScript)
+{
+	var msgAreaChooser = new DDMsgAreaChooser();
+	msgAreaChooser.SelectMsgArea();
+}
+
+// End of script execution
+
+///////////////////////////////////////////////////////////////////////////////////
+// DDMsgAreaChooser class stuff
+
+function DDMsgAreaChooser()
+{
+	// this.colors will be an associative array of colors (indexed by their
+	// usage) used for the message group/sub-board lists.
+	// Colors for the file & message area lists
+	this.colors = new Object();
+	this.colors.areaNum = "\1n\1w\1h";
+	this.colors.desc = "\1n\1c";
+	this.colors.numItems = "\1b\1h";
+	this.colors.header = "\1n\1y\1h";
+	this.colors.subBoardHeader = "\1n\1g";
+	this.colors.areaMark = "\1g\1h";
+	this.colors.latestDate = "\1n\1g";
+	this.colors.latestTime = "\1n\1m";
+	// Highlighted colors (for lightbar mode)
+	this.colors.bkgHighlight = "\1" + "4"; // Blue background
+	this.colors.areaNumHighlight = "\1w\1h";
+	this.colors.descHighlight = "\1c";
+	this.colors.dateHighlight = "\1w\1h";
+	this.colors.timeHighlight = "\1w\1h";
+	this.colors.numItemsHighlight = "\1w\1h";
+	// Lightbar help line colors
+	this.colors.lightbarHelpLineBkg = "\1" + "7";
+	this.colors.lightbarHelpLineGeneral = "\1b";
+	this.colors.lightbarHelpLineHotkey = "\1r";
+	this.colors.lightbarHelpLineParen = "\1m";
+
+	// showImportDates is a boolean to specify whether or not to display the
+	// message import dates.  If false, the message written dates will be
+	// displayed instead.
+	this.showImportDates = true;
+
+	// useLightbarInterface specifies whether or not to use the lightbar
+	// interface.  The lightbar interface will still only be used if the
+	// user's terminal supports ANSI.
+	this.useLightbarInterface = true;
+
+	// These variables store the lengths of the various columns displayed in
+	// the message group/sub-board lists.
+	// Sub-board info field lengths
+	this.areaNumLen = 4;
+	this.numItemsLen = 4;
+	this.dateLen = 10; // i.e., YYYY-MM-DD
+	this.timeLen = 8;  // i.e., HH:MM:SS
+	// Sub-board name length - This should be 47 for an 80-column display.
+	this.subBoardNameLen = console.screen_columns - this.areaNumLen -
+	this.numItemsLen - this.dateLen - this.timeLen - 7;
+	// Message group description length (67 chars on an 80-column screen)
+	this.msgGrpDescLen = console.screen_columns - this.areaNumLen -
+	this.numItemsLen - 5;
+
+	// Set the function pointers for the object
+	this.ReadConfigFile = DDMsgAreaChooser_ReadConfigFile;
+	this.WriteKeyHelpLine = DDMsgAreaChooser_writeKeyHelpLine;
+	this.WriteGrpListHdrLine = DDMsgAreaChooser_writeGrpListTopHdrLine;
+	this.WriteSubBrdListHdr1Line = DMsgAreaChooser_writeSubBrdListHdr1Line;
+	this.SelectMsgArea = DDMsgAreaChooser_selectMsgArea;
+	this.SelectMsgArea_Lightbar = DDMsgAreaChooser_selectMsgArea_Lightbar;
+	this.SelectSubBoard_Lightbar = DDMsgAreaChooser_selectSubBoard_Lightbar;
+	this.SelectMsgArea_Traditional = DDMsgAreaChooser_selectMsgArea_Traditional;
+	this.ListMsgGrps = DDMsgAreaChooser_listMsgGrps_Traditional;
+	this.ListSubBoardsInMsgGroup = DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional;
+	// Lightbar-specific functions
+	this.ListScreenfulOfMsgGrps = DDMsgAreaChooser_listScreenfulOfMsgGrps;
+	this.WriteMsgGroupLine = DDMsgAreaChooser_writeMsgGroupLine;
+	this.updatePageNumInHeader = DDMsgAreaChooser_updatePageNumInHeader;
+	this.ListScreenfulOfSubBrds = DDMsgAreaChooser_listScreenfulOfSubBrds;
+	this.WriteMsgSubBoardLine = DDMsgAreaChooser_writeMsgSubBrdLine;
+	// Help screen
+	this.ShowHelpScreen = DDMsgAreaChooser_showHelpScreen;
+	// Function to build the sub-board printf information for a message
+	// group
+	this.BuildSubBoardPrintfInfoForGrp = DDMsgAreaChooser_buildSubBoardPrintfInfoForGrp;
+
+	// Read the settings from the config file.
+	this.ReadConfigFile();
+
+	// printf strings for various things
+	// Message group information (printf strings)
+	this.msgGrpListPrintfStr = "\1n " + this.colors.areaNum + "%" + this.areaNumLen
+	                         + "d " + this.colors.desc + "%-"
+	                         + this.msgGrpDescLen + "s " + this.colors.numItems
+	                         + "%" + this.numItemsLen + "d";
+	this.msgGrpListHilightPrintfStr = "\1n" + this.colors.bkgHighlight + " "
+	                                + "\1n" + this.colors.bkgHighlight
+	                                + this.colors.areaNumHighlight + "%" + this.areaNumLen
+	                                + "d \1n" + this.colors.bkgHighlight
+	                                + this.colors.descHighlight + "%-"
+	                                + this.msgGrpDescLen + "s \1n" + this.colors.bkgHighlight
+	                                + this.colors.numItemsHighlight + "%" + this.numItemsLen
+	                                + "d";
+	// Message group list header (printf string)
+	this.msgGrpListHdrPrintfStr = this.colors.header + "%6s %-"
+	                            + +(this.msgGrpDescLen-8) + "s %-12s";
+	// Sub-board information header (printf string)
+	this.subBoardListHdrPrintfStr = this.colors.header + " %5s %-"
+	                              + +(this.subBoardNameLen-3) + "s %-7s %-19s";
+	// Lightbar mode key help line
+	this.lightbarKeyHelpText = "\1n" + this.colors.lightbarHelpLineHotkey
+	              + this.colors.lightbarHelpLineBkg + UP_ARROW
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + DOWN_ARROW
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "HOME"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "END"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "#"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "PgUp"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + "/"
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "Dn"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + ", "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "F"
+				  + "\1n" + this.colors.lightbarHelpLineParen
+				  + this.colors.lightbarHelpLineBkg + ")"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + "irst pg, "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "L"
+				  + "\1n" + this.colors.lightbarHelpLineParen
+				  + this.colors.lightbarHelpLineBkg + ")"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + "ast pg, "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "Q"
+				  + "\1n" + this.colors.lightbarHelpLineParen
+				  + this.colors.lightbarHelpLineBkg + ")"
+				  + "\1n" + this.colors.lightbarHelpLineGeneral
+				  + this.colors.lightbarHelpLineBkg + "uit, "
+				  + "\1n" + this.colors.lightbarHelpLineHotkey
+				  + this.colors.lightbarHelpLineBkg + "?";
+	// Pad the lightbar key help text on either side to center it on the screen
+	// (but leave off the last character to avoid screen drawing issues)
+	var helpTextLen = console.strlen(this.lightbarKeyHelpText);
+	var helpTextStartCol = (console.screen_columns/2) - (helpTextLen/2);
+	this.lightbarKeyHelpText = "\1n" + this.colors.lightbarHelpLineBkg
+	                         + format("%" + +(helpTextStartCol) + "s", "")
+							 + this.lightbarKeyHelpText + "\1n"
+							 + this.colors.lightbarHelpLineBkg;
+	var numTrailingChars = console.screen_columns - (helpTextStartCol+helpTextLen) - 1;
+	this.lightbarKeyHelpText += format("%" + +(numTrailingChars) + "s", "") + "\1n";
+	// this.subBoardListPrintfInfo will be an array of printf strings
+	// for the sub-boards in the message groups.  The index is the
+	// message group index.  The sub-board printf information is created
+	// on the fly the first time the user lists sub-boards for a message
+	// group.
+	this.subBoardListPrintfInfo = new Array();
+}
+
+// For the DDMsgAreaChooser class: Writes the line of key help at the bottom
+// row of the screen.
+function DDMsgAreaChooser_writeKeyHelpLine()
+{
+	console.gotoxy(1, console.screen_rows);
+	console.print(this.lightbarKeyHelpText);
+}
+
+// For the DDMsgAreaChooser class: Outputs the header line to appear above
+// the list of message groups.
+//
+// Parameters:
+//  pNumPages: The number of pages.  This is optional; if this is
+//             not passed, then it won't be used.
+//  pPageNum: The page number.  This is optional; if this is not passed,
+//            then it won't be used.
+function DDMsgAreaChooser_writeGrpListTopHdrLine(pNumPages, pPageNum)
+{
+  var descStr = "Description";
+  if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
+    descStr += "    (Page " + pPageNum + " of " + pNumPages + ")";
+  else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
+    descStr += "    (Page " + pPageNum + ")";
+  else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
+    descStr += "    (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
+  printf(this.msgGrpListHdrPrintfStr, "Group#", descStr, "# Sub-Boards");
+  console.cleartoeol("\1n");
+}
+
+// For the DDMsgAreaChooser class: Outputs the first header line to appear
+// above the sub-board list for a message group.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group (assumed to be valid)
+//  pNumPages: The number of pages.  This is optional; if this is
+//             not passed, then it won't be used.
+//  pPageNum: The page number.  This is optional; if this is not passed,
+//            then it won't be used.
+function DMsgAreaChooser_writeSubBrdListHdr1Line(pGrpIndex, pNumPages, pPageNum)
+{
+  var descFormatStr = "\1n" + this.colors.subBoardHeader + "Sub-boards of \1h%-25s     \1n"
+                     + this.colors.subBoardHeader;
+  if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
+    descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")";
+  else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
+    descFormatStr += "(Page " + pPageNum + ")";
+  else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
+    descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
+  printf(descFormatStr, msg_area.grp_list[pGrpIndex].description.substr(0, 25));
+  console.cleartoeol("\1n");
+}
+
+// For the DDMsgAreaChooser class: Lets the user choose a message group and
+// sub-board via numeric input, using a lightbar interface (if enabled and
+// if the user's terminal uses ANSI) or a traditional user interface.
+function DDMsgAreaChooser_selectMsgArea()
+{
+	if (this.useLightbarInterface && console.term_supports(USER_ANSI))
+		this.SelectMsgArea_Lightbar();
+	else
+		this.SelectMsgArea_Traditional();
+}
+
+// For the DDMsgAreaChooser class: Lets the user choose a message group and
+// sub-board via numeric input, using a lightbar user interface.
+function DDMsgAreaChooser_selectMsgArea_Lightbar()
+{
+   // If there are no message groups, then don't let the user
+   // choose one.
+   if (msg_area.grp_list.length == 0)
+   {
+      console.clear("\1n");
+      console.print("\1y\1hThere are no message groups.\r\n\1p");
+      return;
+   }
+
+   // Returns the index of the bottommost message group that can be displayed
+   // on the screen.
+   //
+   // Parameters:
+   //  pTopGrpIndex: The index of the topmost message group displayed on screen
+   //  pNumItemsPerPage: The number of items per page
+   function getBottommostGrpIndex(pTopGrpIndex, pNumItemsPerPage)
+   {
+      var bottomGrpIndex = pTopGrpIndex + pNumItemsPerPage - 1;
+      // If bottomGrpIndex is beyond the last index, then adjust it.
+      if (bottomGrpIndex >= msg_area.grp_list.length)
+         bottomGrpIndex = msg_area.grp_list.length - 1;
+      return bottomGrpIndex;
+   }
+
+
+   // Figure out the index of the user's currently-selected message group
+   var selectedGrpIndex = 0;
+   if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number"))
+      selectedGrpIndex = bbs.curgrp;
+
+   var listStartRow = 2;      // The row on the screen where the list will start
+   var listEndRow = console.screen_rows - 1; // Row on screen where list will end
+   var topMsgGrpIndex = 0;    // The index of the message group at the top of the list
+
+   // Figure out the index of the last message group to appear on the screen.
+   var numItemsPerPage = listEndRow - listStartRow + 1;
+   var bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+   // Figure out how many pages are needed to list all the sub-boards.
+   var numPages = Math.ceil(msg_area.grp_list.length / numItemsPerPage);
+   // Figure out the top index for the last page.
+   var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage;
+
+   // If the highlighted row is beyond the current screen, then
+   // go to the appropriate page.
+   if (selectedGrpIndex > bottomMsgGrpIndex)
+   {
+      var nextPageTopIndex = 0;
+      while (selectedGrpIndex > bottomMsgGrpIndex)
+      {
+         nextPageTopIndex = topMsgGrpIndex + numItemsPerPage;
+         if (nextPageTopIndex < msg_area.grp_list.length)
+         {
+            // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
+            // refresh the list on the screen.
+            topMsgGrpIndex = nextPageTopIndex;
+            bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+         }
+         else
+            break;
+      }
+
+      // If we didn't find the correct page for some reason, then set the
+      // variables to display page 1 and select the first message group.
+      var foundCorrectPage = ((topMsgGrpIndex < msg_area.grp_list.length) &&
+          (selectedGrpIndex >= topMsgGrpIndex) && (selectedGrpIndex <= bottomMsgGrpIndex));
+      if (!foundCorrectPage)
+      {
+         topMsgGrpIndex = 0;
+         bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+         selectedGrpIndex = 0;
+      }
+   }
+
+   // Clear the screen, write the help line and group list header, and output
+   // a screenful of message groups.
+   console.clear("\1n");
+   this.WriteKeyHelpLine();
+
+   var curpos = new Object();
+   curpos.x = 1;
+   curpos.y = 1;
+   console.gotoxy(curpos);
+   var pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
+   this.WriteGrpListHdrLine(numPages, pageNum);
+   this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, false);
+   // Start of the input loop.
+   var highlightScrenRow = 0; // The row on the screen for the highlighted group
+   var userInput = "";        // Will store a keypress from the user
+   var retObj = null;        // To store the return value of choosing a sub-board
+   var continueChoosingMsgArea = true;
+   while (continueChoosingMsgArea)
+   {
+      // Highlight the currently-selected message group
+      highlightScrenRow = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
+      curpos.y = highlightScrenRow;
+      if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows))
+      {
+         console.gotoxy(1, highlightScrenRow);
+         this.WriteMsgGroupLine(selectedGrpIndex, true);
+      }
+
+      // Get a key from the user (upper-case) and take action based upon it.
+	  userInput = getKeyWithESCChars(K_UPPER | K_NOCRLF);
+      switch (userInput)
+      {
+         case KEY_UP: // Move up one message group in the list
+            if (selectedGrpIndex > 0)
+            {
+               // If the previous group index is on the previous page, then
+               // display the previous page.
+               var previousGrpIndex = selectedGrpIndex - 1;
+               if (previousGrpIndex < topMsgGrpIndex)
+               {
+                  // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
+                  // refresh the list on the screen.
+                  topMsgGrpIndex -= numItemsPerPage;
+                  bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+                  this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
+                                              listEndRow, false, true);
+               }
+               else
+               {
+                  // Display the current line un-highlighted.
+                  console.gotoxy(1, curpos.y);
+                  this.WriteMsgGroupLine(selectedGrpIndex, false);
+               }
+               selectedGrpIndex = previousGrpIndex;
+            }
+            break;
+         case KEY_DOWN: // Move down one message group in the list
+            if (selectedGrpIndex < msg_area.grp_list.length - 1)
+            {
+               // If the next group index is on the next page, then display
+               // the next page.
+               var nextGrpIndex = selectedGrpIndex + 1;
+               if (nextGrpIndex > bottomMsgGrpIndex)
+               {
+                  // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
+                  // refresh the list on the screen.
+                  topMsgGrpIndex += numItemsPerPage;
+                  bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+                  this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
+                                              listEndRow, false, true);
+               }
+               else
+               {
+                  // Display the current line un-highlighted.
+                  console.gotoxy(1, curpos.y);
+                  this.WriteMsgGroupLine(selectedGrpIndex, false);
+               }
+               selectedGrpIndex = nextGrpIndex;
+            }
+            break;
+         case KEY_HOME: // Go to the top message group on the screen
+            if (selectedGrpIndex > topMsgGrpIndex)
+            {
+               // Display the current line un-highlighted, then adjust
+               // selectedGrpIndex.
+               console.gotoxy(1, curpos.y);
+               this.WriteMsgGroupLine(selectedGrpIndex, false);
+               selectedGrpIndex = topMsgGrpIndex;
+               // Note: curpos.y is set at the start of the while loop.
+            }
+            break;
+         case KEY_END: // Go to the bottom message group on the screen
+            if (selectedGrpIndex < bottomMsgGrpIndex)
+            {
+               // Display the current line un-highlighted, then adjust
+               // selectedGrpIndex.
+               console.gotoxy(1, curpos.y);
+               this.WriteMsgGroupLine(selectedGrpIndex, false);
+               selectedGrpIndex = bottomMsgGrpIndex;
+               // Note: curpos.y is set at the start of the while loop.
+            }
+            break;
+         case KEY_ENTER: // Select the currently-highlighted message group
+            retObj = this.SelectSubBoard_Lightbar(selectedGrpIndex);
+            // If the user chose a sub-board, then set bbs.curgrp and
+            // bbs.cursub, and don't continue the input loop anymore.
+            if (retObj.subBoardChosen)
+            {
+               bbs.curgrp = selectedGrpIndex;
+               bbs.cursub = retObj.subBoardIndex;
+               continueChoosingMsgArea = false;
+            }
+            else
+            {
+               // A sub-board was not chosen, so we'll have to re-draw
+               // the header and list of message groups.
+               console.gotoxy(1, 1);
+               this.WriteGrpListHdrLine(numPages, pageNum);
+               this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
+                                            false, true);
+            }
+            break;
+		 case KEY_PAGE_DOWN: // Go to the next page
+            var nextPageTopIndex = topMsgGrpIndex + numItemsPerPage;
+            if (nextPageTopIndex < msg_area.grp_list.length)
+            {
+               // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
+               // refresh the list on the screen.
+               topMsgGrpIndex = nextPageTopIndex;
+               pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
+               bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, true, false);
+               this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
+                                           listEndRow, false, true);
+               selectedGrpIndex = topMsgGrpIndex;
+            }
+            break;
+		 case KEY_PAGE_UP: // Go to the previous page
+            var prevPageTopIndex = topMsgGrpIndex - numItemsPerPage;
+            if (prevPageTopIndex >= 0)
+            {
+               // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
+               // refresh the list on the screen.
+               topMsgGrpIndex = prevPageTopIndex;
+               pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
+               bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, true, false);
+               this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
+                                           listEndRow, false, true);
+               selectedGrpIndex = topMsgGrpIndex;
+            }
+            break;
+         case 'F': // Go to the first page
+            if (topMsgGrpIndex > 0)
+            {
+               topMsgGrpIndex = 0;
+               pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
+               bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, true, false);
+               this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
+                                           false, true);
+               selectedGrpIndex = 0;
+            }
+            break;
+         case 'L': // Go to the last page
+            if (topMsgGrpIndex < topIndexForLastPage)
+            {
+               topMsgGrpIndex = topIndexForLastPage;
+               pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
+               bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, true, false);
+               this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
+                                           false, true);
+               selectedGrpIndex = topIndexForLastPage;
+            }
+            break;
+         case 'Q': // Quit
+            continueChoosingMsgArea = false;
+            break;
+         case '?': // Show help
+            this.ShowHelpScreen(true, true);
+            console.pause();
+            // Refresh the screen
+            this.WriteKeyHelpLine();
+            console.gotoxy(1, 1);
+            this.WriteGrpListHdrLine(numPages, pageNum);
+            this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
+                                         false, true);
+            break;
+         default:
+            // If the user entered a numeric digit, then treat it as
+            // the start of the message group number.
+            if (userInput.match(/[0-9]/))
+            {
+               var originalCurpos = curpos;
+
+               // Put the user's input back in the input buffer to
+               // be used for getting the rest of the message number.
+               console.ungetstr(userInput);
+               // Move the cursor to the bottom of the screen and
+               // prompt the user for the message number.
+               console.gotoxy(1, console.screen_rows);
+               console.clearline("\1n");
+               console.print("\1cChoose group #: \1h");
+               userInput = console.getnum(msg_area.grp_list.length);
+               // If the user made a selection, then let them choose a
+               // sub-board from the group.
+               if (userInput > 0)
+               {
+                  var msgGroupIndex = userInput - 1;
+                  retObj = this.SelectSubBoard_Lightbar(msgGroupIndex);
+                  // If the user chose a sub-board, then set bbs.curgrp and
+                  // bbs.cursub, and don't continue the input loop anymore.
+                  if (retObj.subBoardChosen)
+                  {
+                     bbs.curgrp = msgGroupIndex;
+                     bbs.cursub = retObj.subBoardIndex;
+                     continueChoosingMsgArea = false;
+                  }
+                  else
+                  {
+                     // A sub-board was not chosen, so we'll have to re-draw
+                     // the header and list of message groups.
+                     console.gotoxy(1, 1);
+                     this.WriteGrpListHdrLine(numPages, pageNum);
+                     this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
+                                                  false, true);
+                  }
+               }
+               else
+               {
+                  // The user didn't make a selection.  So, we need to refresh
+                  // the screen due to everything being moved up one line.
+                  this.WriteKeyHelpLine();
+                  console.gotoxy(1, 1);
+                  this.WriteGrpListHdrLine(numPages, pageNum);
+                  this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
+                                               false, true);
+               }
+            }
+            break;
+      }
+   }
+}
+
+// For the DDMsgAreaChooser class: Lets the user choose a sub-board within a
+// message group, with a lightbar interface.  Does not set bbs.cursub.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group to choose from.  This is
+//             optional; if not specified, bbs.curgrp will be used.
+//  pMarkIndex: An index of a message group to display the "current" mark
+//              next to.  This is optional; if left off, this will default to
+//              the current sub-board.
+//
+// Return value: An object containing the following values:
+//               subBoardChosen: Boolean - Whether or not a sub-board was chosen.
+//               subBoardIndex: Numeric - The sub-board that was chosen (if any).
+//                              Will be -1 if none chosen.
+function DDMsgAreaChooser_selectSubBoard_Lightbar(pGrpIndex, pMarkIndex)
+{
+   // Create the return object.
+   var retObj = new Object();
+   retObj.subBoardChosen = false;
+   retObj.subBoardIndex = -1;
+
+   var grpIndex = 0;
+   if (typeof(pGrpIndex) == "number")
+      grpIndex = pGrpIndex;
+   else if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number"))
+      grpIndex = bbs.curgrp;
+   // Double-check grpIndex
+   if (grpIndex < 0)
+      grpIndex = 0;
+   else if (grpIndex >= msg_area.grp_list.length)
+      grpIndex = msg_area.grp_list.length - 1;
+
+   var markIndex = 0;
+   if ((pMarkIndex != null) && (typeof(pMarkIndex) == "number"))
+      markIndex = pMarkIndex;
+   else if ((bbs.cursub != null) && (typeof(bbs.cursub) == "number") &&
+             (bbs.curgrp == pGrpIndex))
+   {
+      markIndex = bbs.cursub;
+   }
+   // Double-check markIndex
+   if (markIndex < 0)
+      markIndex = 0;
+   else if (markIndex >= msg_area.grp_list[grpIndex].sub_list.length)
+      markIndex = msg_area.grp_list[grpIndex].sub_list.length - 1;
+
+
+   // Ensure that the sub-board printf information is created for
+   // this message group.
+   this.BuildSubBoardPrintfInfoForGrp(grpIndex);
+
+
+   // If there are no sub-boards in the given message group, then show
+   // an error and return.
+   if (msg_area.grp_list[grpIndex].sub_list.length == 0)
+   {
+      console.clear("\1n");
+      console.print("\1y\1hThere are no sub-boards in the chosen group.\r\n\1p");
+      return retObj;
+   }
+
+   // Returns the index of the bottommost sub-board that can be displayed on
+   // the screen.
+   //
+   // Parameters:
+   //  pTopSubIndex: The index of the topmost sub-board displayed on screen
+   //  pNumItemsPerPage: The number of items per page
+   function getBottommostSubIndex(pTopSubIndex, pNumItemsPerPage)
+   {
+      var bottomGrpIndex = topSubIndex + pNumItemsPerPage - 1;
+      // If bottomGrpIndex is beyond the last index, then adjust it.
+      if (bottomGrpIndex >= msg_area.grp_list[grpIndex].sub_list.length)
+         bottomGrpIndex = msg_area.grp_list[grpIndex].sub_list.length - 1;
+      return bottomGrpIndex;
+   }
+
+
+   // Figure out the index of the user's currently-selected sub-board.
+   var selectedSubIndex = 0;
+   if ((bbs.cursub != null) && (typeof(bbs.cursub) == "number"))
+   {
+      if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number") &&
+          (bbs.curgrp == pGrpIndex))
+      {
+         selectedSubIndex = bbs.cursub;
+      }
+   }
+
+   var listStartRow = 3;      // The row on the screen where the list will start
+   var listEndRow = console.screen_rows - 1; // Row on screen where list will end
+   var topSubIndex = 0;      // The index of the message group at the top of the list
+   // Figure out the index of the last message group to appear on the screen.
+   var numItemsPerPage = listEndRow - listStartRow + 1;
+   var bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+   // Figure out how many pages are needed to list all the sub-boards.
+   var numPages = Math.ceil(msg_area.grp_list[grpIndex].sub_list.length / numItemsPerPage);
+   // Figure out the top index for the last page.
+   var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage;
+
+   // If the highlighted row is beyond the current screen, then
+   // go to the appropriate page.
+   if (selectedSubIndex > bottomSubIndex)
+   {
+      var nextPageTopIndex = 0;
+      while (selectedSubIndex > bottomSubIndex)
+      {
+         nextPageTopIndex = topSubIndex + numItemsPerPage;
+         if (nextPageTopIndex < msg_area.grp_list[grpIndex].sub_list.length)
+         {
+            // Adjust topSubIndex and bottomSubIndex, and
+            // refresh the list on the screen.
+            topSubIndex = nextPageTopIndex;
+            bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+         }
+         else
+            break;
+      }
+
+      // If we didn't find the correct page for some reason, then set the
+      // variables to display page 1 and select the first message group.
+      var foundCorrectPage =
+          ((topSubIndex < msg_area.grp_list[grpIndex].sub_list.length) &&
+           (selectedSubIndex >= topSubIndex) && (selectedSubIndex <= bottomSubIndex));
+      if (!foundCorrectPage)
+      {
+         topSubIndex = 0;
+         bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+         selectedSubIndex = 0;
+      }
+   }
+
+   // Clear the screen, write the help line and group list header, and output
+   // a screenful of message groups.
+   console.clear("\1n");
+   var pageNum = calcPageNum(topSubIndex, numItemsPerPage);
+   this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum);
+   this.WriteKeyHelpLine();
+
+   var curpos = new Object();
+   curpos.x = 1;
+   curpos.y = 2;
+   console.gotoxy(curpos);
+   printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
+   this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow,
+                               false, false);
+   // Start of the input loop.
+   var highlightScrenRow = 0; // The row on the screen for the highlighted group
+   var userInput = "";        // Will store a keypress from the user
+   var continueChoosingSubBrd = true;
+   while (continueChoosingSubBrd)
+   {
+      // Highlight the currently-selected message group
+      highlightScrenRow = listStartRow + (selectedSubIndex - topSubIndex);
+      curpos.y = highlightScrenRow;
+      if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows))
+      {
+         console.gotoxy(1, highlightScrenRow);
+         this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, true);
+      }
+
+      // Get a key from the user (upper-case) and take action based upon it.
+	  userInput = getKeyWithESCChars(K_UPPER | K_NOCRLF);
+      switch (userInput)
+      {
+         case KEY_UP: // Move up one message group in the list
+            if (selectedSubIndex > 0)
+            {
+               // If the previous group index is on the previous page, then
+               // display the previous page.
+               var previousSubIndex = selectedSubIndex - 1;
+               if (previousSubIndex < topSubIndex)
+               {
+                  // Adjust topSubIndex and bottomSubIndex, and
+                  // refresh the list on the screen.
+                  topSubIndex -= numItemsPerPage;
+                  bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+                  pageNum = calcPageNum(topSubIndex, numItemsPerPage);
+                  this.updatePageNumInHeader(pageNum, numPages, false, false);
+                  this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow,
+                                              listEndRow, false, true);
+               }
+               else
+               {
+                  // Display the current line un-highlighted.
+                  console.gotoxy(1, curpos.y);
+                  this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
+               }
+               selectedSubIndex = previousSubIndex;
+            }
+            break;
+         case KEY_DOWN: // Move down one message group in the list
+            if (selectedSubIndex < msg_area.grp_list[grpIndex].sub_list.length - 1)
+            {
+               // If the next group index is on the next page, then display
+               // the next page.
+               var nextGrpIndex = selectedSubIndex + 1;
+               if (nextGrpIndex > bottomSubIndex)
+               {
+                  // Adjust topSubIndex and bottomSubIndex, and
+                  // refresh the list on the screen.
+                  topSubIndex += numItemsPerPage;
+                  bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+                  pageNum = calcPageNum(topSubIndex, numItemsPerPage);
+                  this.updatePageNumInHeader(pageNum, numPages, false, false);
+                  this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow,
+                                              listEndRow, false, true);
+               }
+               else
+               {
+                  // Display the current line un-highlighted.
+                  console.gotoxy(1, curpos.y);
+                  this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
+               }
+               selectedSubIndex = nextGrpIndex;
+            }
+            break;
+         case KEY_HOME: // Go to the top message group on the screen
+            if (selectedSubIndex > topSubIndex)
+            {
+               // Display the current line un-highlighted, then adjust
+               // selectedSubIndex.
+               console.gotoxy(1, curpos.y);
+               this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
+               selectedSubIndex = topSubIndex;
+               // Note: curpos.y is set at the start of the while loop.
+            }
+            break;
+         case KEY_END: // Go to the bottom message group on the screen
+            if (selectedSubIndex < bottomSubIndex)
+            {
+               // Display the current line un-highlighted, then adjust
+               // selectedSubIndex.
+               console.gotoxy(1, curpos.y);
+               this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
+               selectedSubIndex = bottomSubIndex;
+               // Note: curpos.y is set at the start of the while loop.
+            }
+            break;
+         case KEY_ENTER: // Select the currently-highlighted sub-board; and we're done.
+            continueChoosingSubBrd = false;
+            retObj.subBoardChosen = true;
+            retObj.subBoardIndex = selectedSubIndex;
+            break;
+         case KEY_PAGE_DOWN: // Go to the next page
+            var nextPageTopIndex = topSubIndex + numItemsPerPage;
+            if (nextPageTopIndex < msg_area.grp_list[grpIndex].sub_list.length)
+            {
+               // Adjust topSubIndex and bottomSubIndex, and
+               // refresh the list on the screen.
+               topSubIndex = nextPageTopIndex;
+               pageNum = calcPageNum(topSubIndex, numItemsPerPage);
+               bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, false, false);
+               this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedSubIndex = topSubIndex;
+            }
+            break;
+         case KEY_PAGE_UP: // Go to the previous page
+            var prevPageTopIndex = topSubIndex - numItemsPerPage;
+            if (prevPageTopIndex >= 0)
+            {
+               // Adjust topSubIndex and bottomSubIndex, and
+               // refresh the list on the screen.
+               topSubIndex = prevPageTopIndex;
+               pageNum = calcPageNum(topSubIndex, numItemsPerPage);
+               bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, false, false);
+               this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedSubIndex = topSubIndex;
+            }
+            break;
+         case 'F': // Go to the first page
+            if (topSubIndex > 0)
+            {
+               topSubIndex = 0;
+               pageNum = calcPageNum(topSubIndex, numItemsPerPage);
+               bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, false, false);
+               this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedSubIndex = 0;
+            }
+            break;
+         case 'L': // Go to the last page
+            if (topSubIndex < topIndexForLastPage)
+            {
+               topSubIndex = topIndexForLastPage;
+               pageNum = calcPageNum(topSubIndex, numItemsPerPage);
+               bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+               this.updatePageNumInHeader(pageNum, numPages, false, false);
+               this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow,
+                                            listEndRow, false, true);
+               selectedSubIndex = topIndexForLastPage;
+            }
+            break;
+         case 'Q': // Quit
+            continueChoosingSubBrd = false;
+            break;
+         case '?': // Show help
+            this.ShowHelpScreen(true, true);
+            console.pause();
+            // Refresh the screen
+            console.gotoxy(1, 1);
+            this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum);
+            console.cleartoeol("\1n");
+            this.WriteKeyHelpLine();
+            console.gotoxy(1, 2);
+            printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts",
+                   "Latest date & time");
+            this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow,
+                                        listEndRow, false, true);
+            break;
+         default:
+            // If the user entered a numeric digit, then treat it as
+            // the start of the message group number.
+            if (userInput.match(/[0-9]/))
+            {
+               var originalCurpos = curpos;
+
+               // Put the user's input back in the input buffer to
+               // be used for getting the rest of the message number.
+               console.ungetstr(userInput);
+               // Move the cursor to the bottom of the screen and
+               // prompt the user for the message number.
+               console.gotoxy(1, console.screen_rows);
+               console.clearline("\1n");
+               console.print("\1cSub-board #: \1h");
+               userInput = console.getnum(msg_area.grp_list[grpIndex].sub_list.length);
+               // If the user made a selection, then set it in the
+               // return object and don't continue the input loop.
+               if (userInput > 0)
+               {
+                  continueChoosingSubBrd = false;
+                  retObj.subBoardChosen = true;
+                  retObj.subBoardIndex = userInput - 1;
+               }
+               else
+               {
+                  // The user didn't enter a selection.  Now we need to
+                  // re-draw the screen due to everything being moved
+                  // up one line.
+                  console.gotoxy(1, 1);
+                  this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum);
+                  console.cleartoeol("\1n");
+                  this.WriteKeyHelpLine();
+                  console.gotoxy(1, 2);
+                  printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts",
+                         "Latest date & time");
+                  this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow,
+                                              listEndRow, false, true);
+               }
+            }
+            break;
+      }
+   }
+
+   return retObj;
+}
+
+// For the DDMsgAreaChooser class: Lets the user choose a message group and
+// sub-board via numeric input, using a traditional user interface.
+function DDMsgAreaChooser_selectMsgArea_Traditional()
+{
+   // If there are no message groups, then don't let the user
+   // choose one.
+   if (msg_area.grp_list.length == 0)
+   {
+      console.clear("\1n");
+      console.print("\1y\1hThere are no message groups.\r\n\1p");
+      return;
+   }
+
+   // Show the message groups & sub-boards and let the user choose one.
+   var selectedGrp = 0;      // The user's selected message group
+   var selectedSubBoard = 0; // The user's selected sub-board
+   var continueChoosingMsgArea = true;
+   while (continueChoosingMsgArea)
+   {
+      // Clear the BBS command string to make sure there are no extra
+      // commands in there that could cause weird things to happen.
+      bbs.command_str = "";
+
+      console.clear("\1n");
+      this.ListMsgGrps();
+      console.crlf();
+      console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, or [\1h" +
+                    +(bbs.curgrp+1) + "\1n\1c]: \1h");
+      // Accept Q (quit) or a file library number
+      selectedGrp = console.getkeys("Q", msg_area.grp_list.length);
+
+      // If the user just pressed enter (selectedGrp would be blank),
+      // default to the current group.
+      if (selectedGrp.toString() == "")
+         selectedGrp = bbs.curgrp + 1;
+
+      if (selectedGrp.toString() == "Q")
+         continueChoosingMsgArea = false;
+      else
+      {
+         // If the user specified a message group number, then
+         // set it and let the user choose a sub-board within
+         // the group.
+         if (selectedGrp > 0)
+         {
+            // Set the default sub-board #: The current sub-board, or if the
+            // user chose a different group, then this should be set
+            // to the first sub-board.
+            var defaultSubBoard = bbs.cursub + 1;
+            if (selectedGrp-1 != bbs.curgrp)
+               defaultSubBoard = 1;
+
+            console.clear("\1n");
+            this.ListSubBoardsInMsgGroup(selectedGrp-1, defaultSubBoard-1);
+            console.crlf();
+            console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, or [\1h" +
+                          defaultSubBoard + "\1n\1c]: \1h");
+            // Accept Q (quit) or a sub-board number
+            selectedSubBoard = console.getkeys("Q", msg_area.grp_list[selectedGrp - 1].sub_list.length);
+
+            // If the user just pressed enter (selectedSubBoard would be blank),
+            // default the selected directory.
+            if (selectedSubBoard.toString() == "")
+               selectedSubBoard = defaultSubBoard;
+
+            // If the user chose a directory, then set bbs.curlib &
+            // bbs.curdir and quit the file library loop.
+            if ((selectedGrp.toString() != "Q") && (selectedSubBoard > 0))
+            {
+               bbs.curgrp = selectedGrp - 1;
+               bbs.cursub = selectedSubBoard - 1;
+               continueChoosingMsgArea = false;
+            }
+         }
+      }
+   }
+}
+
+// For the DDMsgAreaChooser class: Lists all message groups (for the traditional
+// user interface).
+function DDMsgAreaChooser_listMsgGrps_Traditional()
+{
+   // Print the header
+   this.WriteGrpListHdrLine();
+   console.print("\1n");
+   // List the message groups
+   for (var i = 0; i < msg_area.grp_list.length; ++i)
+   {
+      console.crlf();
+      this.WriteMsgGroupLine(i, false);
+   }
+}
+
+// For the DDMsgAreaChooser class: Lists the sub-boards in a message group,
+// for the traditional user interface.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group (0-based)
+//  pMarkIndex: An index of a message group to highlight.  This
+//                   is optional; if left off, this will default to
+//                   the current sub-board.
+//  pSortType: Optional - A string describing how to sort the list (if desired):
+//             "none": Default behavior - Sort by sub-board #
+//             "dateAsc": Sort by date, ascending
+//             "dateDesc": Sort by date, descending
+//             "description": Sort by description
+function DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIndex, pSortType)
+{
+   // Default to the current message group & sub-board if pGrpIndex
+   // and pMarkIndex aren't specified.
+   var grpIndex = bbs.curgrp;
+   if ((pGrpIndex != null) && (typeof(pGrpIndex) == "number"))
+      grpIndex = pGrpIndex;
+   var highlightIndex = bbs.cursub;
+   if ((pMarkIndex != null) && (typeof(pMarkIndex) == "number"))
+      highlightIndex = pMarkIndex;
+
+   // Make sure grpIndex and highlightIndex are valid (they might not be for
+   // brand-new users).
+   if ((grpIndex == null) || (typeof(grpIndex) == "undefined"))
+      grpIndex = 0;
+   if ((highlightIndex == null) || (typeof(highlightIndex) == "undefined"))
+      highlightIndex = 0;
+
+   // Ensure that the sub-board printf information is created for
+   // this message group.
+   this.BuildSubBoardPrintfInfoForGrp(grpIndex);
+
+   // Print the headers
+   this.WriteSubBrdListHdr1Line(grpIndex);
+   console.crlf();
+   printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
+   console.print("\1n");
+
+   // List each sub-board in the message group.
+   var subBoardArray = null;       // For sorting, if desired
+   var newestDate = new Object(); // For storing the date of the newest post in a sub-board
+   var msgBase = null;    // For opening the sub-boards with a MsgBase object
+   var msgHeader = null;  // For getting the date & time of the newest post in a sub-board
+   var subBoardNum = 0;   // 0-based sub-board number (because the array index is the number as a str)
+   // If a sort type is specified, then add the sub-board information to
+   // subBoardArray so that it can be sorted.
+   if ((typeof(pSortType) == "string") && (pSortType != "") && (pSortType != "none"))
+   {
+      subBoardArray = new Array();
+      var subBoardInfo = null;
+      for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list)
+      {
+         // Open the current sub-board with the msgBase object.
+         msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
+         if (msgBase.open())
+         {
+            subBoardInfo = new MsgSubBoardInfo();
+            subBoardInfo.subBoardNum = +(arrSubBoardNum);
+            subBoardInfo.description = msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description;
+            subBoardInfo.numPosts = msgBase.total_msgs;
+
+            // Get the date & time when the last message was imported.
+            if (msgBase.total_msgs > 0)
+            {
+               msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
+               if (this.showImportDates)
+                  subBoardInfo.newestPostDate = msgHeader.when_imported_time
+               else
+                  subBoardInfo.newestPostDate = msgHeader.when_written_time;
+            }
+         }
+         msgBase.close();
+         subBoardArray.push(subBoardInfo);
+      }
+      // Free some memory?
+      delete msgBase;
+
+      // Sort sub-board list.
+      if (pSortType == "dateAsc")
+      {
+         subBoardArray.sort(function(pA, pB)
+         {
+            // Return -1, 0, or 1, depending on whether pA's date comes
+            // before, is equal to, or comes after pB's date.
+            var returnValue = 0;
+            if (pA.newestPostDate < pB.newestPostDate)
+               returnValue = -1;
+            else if (pA.newestPostDate > pB.newestPostDate)
+               returnValue = 1;
+            return returnValue;
+         });
+      }
+      else if (pSortType == "dateDesc")
+      {
+         subBoardArray.sort(function(pA, pB)
+         {
+            // Return -1, 0, or 1, depending on whether pA's date comes
+            // after, is equal to, or comes before pB's date.
+            var returnValue = 0;
+            if (pA.newestPostDate > pB.newestPostDate)
+               returnValue = -1;
+            else if (pA.newestPostDate < pB.newestPostDate)
+               returnValue = 1;
+            return returnValue;
+         });
+      }
+      else if (pSortType == "description")
+      {
+         // Binary safe string comparison  
+         // 
+         // version: 909.322
+         // discuss at: http://phpjs.org/functions/strcmp    // +   original by: Waldo Malqui Silva
+         // +      input by: Steve Hilder
+         // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+         // +    revised by: gorthaur
+         // *     example 1: strcmp( 'waldo', 'owald' );    // *     returns 1: 1
+         // *     example 2: strcmp( 'owald', 'waldo' );
+         // *     returns 2: -1
+         subBoardArray.sort(function(pA, pB)
+         {
+            return ((pA.description == pB.description) ? 0 : ((pA.description > pB.description) ? 1 : -1));
+         });
+      }
+
+      // Display the sub-board list.
+      for (var i = 0; i < subBoardArray.length; ++i)
+      {
+         console.crlf();
+         console.print((subBoardArray[i].subBoardNum == highlightIndex) ? "\1n" + this.colors.areaMark + "*" : " ");
+         printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1),
+                subBoardArray[i].description.substr(0, this.subBoardNameLen),
+                subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate),
+                strftime("%H:%M:%S", subBoardArray[i].newestPostDate));
+      }
+   }
+   // If no sort type is specified, then output the sub-board information in
+   // order of sub-board number.
+   else
+   {
+      for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list)
+      {
+         // Open the current sub-board with the msgBase object.
+         msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
+         if (msgBase.open())
+         {
+            // Get the date & time when the last message was imported.
+            if (msgBase.total_msgs > 0)
+            {
+               msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
+               // Construct the date & time strings of the latest post
+               if (this.showImportDates)
+               {
+                  newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time);
+                  newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time);
+               }
+               else
+               {
+                  newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
+                  newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
+               }
+            }
+            else
+               newestDate.date = newestDate.time = "";
+
+            // Print the sub-board information
+            subBoardNum = +(arrSubBoardNum);
+            console.crlf();
+            console.print((subBoardNum == highlightIndex) ? "\1n" + this.colors.areaMark + "*" : " ");
+            printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
+                   msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.substr(0, this.subBoardListPrintfInfo[grpIndex].nameLen),
+                   msgBase.total_msgs, newestDate.date, newestDate.time);
+   
+            msgBase.close();
+         }
+   
+         // Free some memory?
+         delete msgBase;
+      }
+   }
+}
+
+//////////////////////////////////////////////
+// Message group list stuff (lightbar mode) //
+//////////////////////////////////////////////
+
+// Displays a screenful of message groups, for the lightbar interface.
+//
+// Parameters:
+//  pStartIndex: The message group index to start at (0-based)
+//  pStartScreenRow: The row on the screen to start at (1-based)
+//  pEndScreenRow: The row on the screen to end at (1-based)
+//  pClearScreenFirst: Boolean - Whether or not to clear the screen first
+//  pBlankToEndRow: Boolean - Whether or not to write blank lines to the end
+//                  screen row if there aren't enough message groups to fill
+//                  the screen.
+function DDMsgAreaChooser_listScreenfulOfMsgGrps(pStartIndex, pStartScreenRow,
+                                                  pEndScreenRow, pClearScreenFirst,
+                                                  pBlankToEndRow)
+{
+   // Check the parameters; If they're bad, then just return.
+   if ((typeof(pStartIndex) != "number") ||
+       (typeof(pStartScreenRow) != "number") ||
+       (typeof(pEndScreenRow) != "number"))
+   {
+      return;
+   }
+   if ((pStartIndex < 0) || (pStartIndex >= msg_area.grp_list.length))
+      return;
+   if ((pStartScreenRow < 1) || (pStartScreenRow > console.screen_rows))
+      return;
+   if ((pEndScreenRow < 1) || (pEndScreenRow > console.screen_rows))
+      return;
+
+   // If pStartScreenRow is greather than pEndScreenRow, then swap them.
+   if (pStartScreenRow > pEndScreenRow)
+   {
+      var temp = pStartScreenRow;
+      pStartScreenRow = pEndScreenRow;
+      pEndScreenRow = temp;
+   }
+
+   // Calculate the ending index to use for the message groups array.
+   var endIndex = pStartIndex + (pEndScreenRow-pStartScreenRow);
+   if (endIndex >= msg_area.grp_list.length)
+      endIndex = msg_area.grp_list.length - 1;
+   var onePastEndIndex = endIndex + 1;
+
+   // Check to make sure bbs.curgrp is valid (it might not be for brand-new users).
+   var curgrpValid = ((bbs.curgrp != null) && (typeof(bbs.curgrp) != "undefined"));
+
+   // Clear the screen, go to the specified screen row, and display the message
+   // group information.
+   if (pClearScreenFirst)
+      console.clear("\1n");
+   console.gotoxy(1, pStartScreenRow);
+   var grpIndex = pStartIndex;
+   for (; grpIndex < onePastEndIndex; ++grpIndex)
+   {
+      this.WriteMsgGroupLine(grpIndex, false);
+      if (grpIndex < endIndex)
+         console.crlf();
+   }
+
+   // If pBlankToEndRow is true and we're not at the end row yet, then
+   // write blank lines to the end row.
+   if (pBlankToEndRow)
+   {
+      var screenRow = pStartScreenRow + (endIndex - pStartIndex) + 1;
+      if (screenRow <= pEndScreenRow)
+      {
+         for (; screenRow <= pEndScreenRow; ++screenRow)
+         {
+            console.gotoxy(1, screenRow);
+            console.clearline("\1n");
+         }
+      }
+   }
+}
+
+// For the DDMsgAreaChooser class - Writes a message group information line.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group to write (assumed to be valid)
+//  pHighlight: Boolean - Whether or not to write the line highlighted.
+function DDMsgAreaChooser_writeMsgGroupLine(pGrpIndex, pHighlight)
+{
+   console.print("\1n");
+   // Write the highlight background color if pHighlight is true.
+   if (pHighlight)
+      console.print(this.colors.bkgHighlight);
+
+   // Write the message group information line
+   console.print(((typeof(bbs.curgrp) == "number") && (pGrpIndex == bbs.curgrp)) ? this.colors.areaMark + "*" : " ");
+   printf((pHighlight ? this.msgGrpListHilightPrintfStr : this.msgGrpListPrintfStr),
+          +(pGrpIndex+1),
+          msg_area.grp_list[pGrpIndex].description.substr(0, this.msgGrpDescLen),
+          msg_area.grp_list[pGrpIndex].sub_list.length);
+   console.cleartoeol("\1n");
+}
+
+//////////////////////////////////////////////////
+// Message sub-board list stuff (lightbar mode) //
+//////////////////////////////////////////////////
+
+// Updates the page number text in the group list header line on the screen.
+//
+// Parameters:
+//  pPageNum: The page number
+//  pNumPages: The total number of pages
+//  pGroup: Boolean - Whether or not this is for the group header.  If so,
+//          then this will go to the right location for the group page text
+//          and use this.colors.header for the text.  Otherwise, this will
+//          go to the right place for the sub-board page text and use the
+//          sub-board header color.
+//  pRestoreCurPos: Optional - Boolean - If true, then move the cursor back
+//                  to the position where it was before this function was called
+function DDMsgAreaChooser_updatePageNumInHeader(pPageNum, pNumPages, pGroup, pRestoreCurPos)
+{
+  var originalCurPos = null;
+  if (pRestoreCurPos)
+    originalCurPos = console.getxy();
+
+  if (pGroup)
+  {
+    console.gotoxy(29, 1);
+    console.print("\1n" + this.colors.header + pPageNum + " of " + pNumPages + ")   ");
+  }
+  else
+  {
+    console.gotoxy(51, 1);
+    console.print("\1n" + this.colors.subBoardHeader + pPageNum + " of " + pNumPages + ")   ");
+  }
+
+  if (pRestoreCurPos)
+    console.gotoxy(originalCurPos);
+}
+
+// Displays a screenful of message sub-boards, for the lightbar interface.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group (0-based)
+//  pStartSubIndex: The message sub-board index to start at (0-based)
+//  pStartScreenRow: The row on the screen to start at (1-based)
+//  pEndScreenRow: The row on the screen to end at (1-based)
+//  pClearScreenFirst: Boolean - Whether or not to clear the screen first
+//  pBlankToEndRow: Boolean - Whether or not to write blank lines to the end
+//                  screen row if there aren't enough message groups to fill
+//                  the screen.
+function DDMsgAreaChooser_listScreenfulOfSubBrds(pGrpIndex, pStartSubIndex,
+                                                  pStartScreenRow, pEndScreenRow,
+                                                  pClearScreenFirst, pBlankToEndRow)
+{
+   // Check the parameters; If they're bad, then just return.
+   if ((typeof(pGrpIndex) != "number") ||
+       (typeof(pStartSubIndex) != "number") ||
+       (typeof(pStartScreenRow) != "number") ||
+       (typeof(pEndScreenRow) != "number"))
+   {
+      return;
+   }
+   if ((pGrpIndex < 0) || (pGrpIndex >= msg_area.grp_list.length))
+      return;
+   if ((pStartSubIndex < 0) ||
+       (pStartSubIndex >= msg_area.grp_list[pGrpIndex].sub_list.length))
+   {
+      return;
+   }
+   if ((pStartScreenRow < 1) || (pStartScreenRow > console.screen_rows))
+      return;
+   if ((pEndScreenRow < 1) || (pEndScreenRow > console.screen_rows))
+      return;
+   // If pStartScreenRow is greather than pEndScreenRow, then swap them.
+   if (pStartScreenRow > pEndScreenRow)
+   {
+      var temp = pStartScreenRow;
+      pStartScreenRow = pEndScreenRow;
+      pEndScreenRow = temp;
+   }
+
+   // Calculate the ending index to use for the sub-board array.
+   var endIndex = pStartSubIndex + (pEndScreenRow-pStartScreenRow);
+   if (endIndex >= msg_area.grp_list[pGrpIndex].sub_list.length)
+      endIndex = msg_area.grp_list[pGrpIndex].sub_list.length - 1;
+   var onePastEndIndex = endIndex + 1;
+
+   // Clear the screen and go to the specified screen row.
+   if (pClearScreenFirst)
+      console.clear("\1n");
+   console.gotoxy(1, pStartScreenRow);
+
+   // Start listing the sub-boards.
+
+   var subIndex = pStartSubIndex;
+   for (; subIndex < onePastEndIndex; ++subIndex)
+   {
+      this.WriteMsgSubBoardLine(pGrpIndex, subIndex, false);
+      if (subIndex < endIndex)
+         console.crlf();
+   }
+
+   // If pBlankToEndRow is true and we're not at the end row yet, then
+   // write blank lines to the end row.
+   if (pBlankToEndRow)
+   {
+      var screenRow = pStartScreenRow + (endIndex - pStartSubIndex) + 1;
+      if (screenRow <= pEndScreenRow)
+      {
+         for (; screenRow <= pEndScreenRow; ++screenRow)
+         {
+            console.gotoxy(1, screenRow);
+            console.clearline("\1n");
+         }
+      }
+   }
+}
+
+// For the DDMsgAreaChooser class: Writes a message sub-board information line.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group (assumed to be valid)
+//  pSubIndex: The index of the sub-board within the message group to write (assumed to be valid)
+//  pHighlight: Boolean - Whether or not to write the line highlighted.
+function DDMsgAreaChooser_writeMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight)
+{
+   console.print("\1n");
+   // Write the highlight background color if pHighlight is true.
+   if (pHighlight)
+      console.print(this.colors.bkgHighlight);
+
+   // Determine if pGrpIndex and pSubIndex specify the user's
+   // currently-selected group and sub-board.
+   var currentSub = false;
+   if ((typeof(bbs.curgrp) == "number") && (typeof(bbs.cursub) == "number"))
+      currentSub = ((pGrpIndex == bbs.curgrp) && (pSubIndex == bbs.cursub));
+
+   // Open the current sub-board with the msgBase object (so that we can get
+   // the date & time of the last imporeted message).
+   var msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code);
+   if (msgBase.open())
+   {
+      var newestDate = new Object(); // For storing the date of the newest post
+      // Get the date & time when the last message was imported.
+      if (msgBase.total_msgs > 0)
+      {
+         msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
+         // Construct the date & time strings of the latest post
+         if (this.showImportDates)
+         {
+            newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time);
+            newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time);
+         }
+         else
+         {
+            newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
+            newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
+         }
+      }
+      else
+         newestDate.date = newestDate.time = "";
+
+      // Print the sub-board information line.
+      console.print(currentSub ? this.colors.areaMark + "*" : " ");
+      printf((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr),
+             +(pSubIndex+1),
+             msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, this.subBoardListPrintfInfo[pGrpIndex].nameLen),
+             msgBase.total_msgs, newestDate.date, newestDate.time);
+      msgBase.close();
+
+      // Free some memory?
+      delete msgBase;
+   }
+}
+
+///////////////////////////////////////////////
+// Other functions for the msg. area chooser //
+///////////////////////////////////////////////
+
+// For the DDMsgAreaChooser class: Reads the configuration file.
+function DDMsgAreaChooser_ReadConfigFile()
+{
+   // 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 startup_path = '.';
+   try { throw dig.dist(dist); } catch(e) { startup_path = e.fileName; }
+   startup_path = backslash(startup_path.replace(/[\/\\][^\/\\]*$/,''));
+
+   // Open the configuration file
+   var cfgFile = new File(startup_path + "DDMsgAreaChooser.cfg");
+   if (cfgFile.open("r"))
+   {
+      var settingsMode = "behavior";
+      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;        // 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 in the "behavior" section, then set the behavior-related variables.
+         if (fileLine.toUpperCase() == "[BEHAVIOR]")
+         {
+            settingsMode = "behavior";
+            continue;
+         }
+         else if (fileLine.toUpperCase() == "[COLORS]")
+         {
+            settingsMode = "colors";
+            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);
+
+            if (settingsMode == "behavior")
+            {
+               // Set the appropriate value in the settings object.
+               if (settingUpper == "USELIGHTBARINTERFACE")
+                  this.useLightbarInterface = (value.toUpperCase() == "TRUE");
+               else if (settingUpper == "SHOWIMPORTDATES")
+                  this.showImportDates = (value.toUpperCase() == "TRUE");
+            }
+            else if (settingsMode == "colors")
+               this.colors[setting] = value;
+         }
+      }
+   
+      cfgFile.close();
+   }
+}
+
+// For the DDMsgAreaChooser class: Shows the help screen
+//
+// Parameters:
+//  pLightbar: Boolean - Whether or not to show lightbar help.  If
+//             false, then this function will show regular help.
+//  pClearScreen: Boolean - Whether or not to clear the screen first
+function DDMsgAreaChooser_showHelpScreen(pLightbar, pClearScreen)
+{
+	if (pClearScreen)
+		console.clear("\1n");
+	else
+		console.print("\1n");
+	console.center("\1c\1hDigital Distortion Message Area Chooser");
+	console.center("\1k���������������������������������������");
+	console.center("\1n\1cVersion \1g" + DD_MSG_AREA_CHOOSER_VERSION +
+	               " \1w\1h(\1b" + DD_MSG_AREA_CHOOSER_VER_DATE + "\1w)");
+	console.crlf();
+	console.print("\1n\1cFirst, a listing of message groups is displayed.  One can be chosen by typing");
+	console.crlf();
+	console.print("its number.  Then, a listing of sub-boards within that message group will be");
+	console.crlf();
+	console.print("shown, and one can be chosen by typing its number.");
+	console.crlf();
+
+	if (pLightbar)
+	{
+		console.crlf();
+		console.print("\1n\1cThe lightbar interface also allows up & down navigation through the lists:");
+		console.crlf();
+		console.print("\1k\1h��������������������������������������������������������������������������");
+		console.crlf();
+		console.print("\1n\1c\1hUp arrow\1n\1c: Move the cursor up one line");
+		console.crlf();
+		console.print("\1hDown arrow\1n\1c: Move the cursor down one line");
+		console.crlf();
+		console.print("\1hENTER\1n\1c: Select the current group/sub-board");
+		console.crlf();
+		console.print("\1hHOME\1n\1c: Go to the first item on the screen");
+		console.crlf();
+		console.print("\1hEND\1n\1c: Go to the last item on the screen");
+		console.crlf();
+		console.print("\1hPageUp\1n\1c/\1hPageDown\1n\1c: Go to the previous/next page");
+		console.crlf();
+		console.print("\1hF\1n\1c/\1hL\1n\1c: Go to the first/last page");
+		console.crlf();
+	}
+
+	console.crlf();
+	console.print("Additional keyboard commands:");
+	console.crlf();
+	console.print("\1k\1h�����������������������������");
+	console.crlf();
+	console.print("\1n\1c\1h?\1n\1c: Show this help screen");
+	console.crlf();
+	console.print("\1hQ\1n\1c: Quit");
+	console.crlf();
+}
+
+// Builds sub-board printf format information for a message group.
+// The widths of the description & # messages columns are calculated
+// based on the greatest number of messages in a sub-board for the
+// message group.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group
+function DDMsgAreaChooser_buildSubBoardPrintfInfoForGrp(pGrpIndex)
+{
+   // If the array of sub-board printf strings doesn't contain the printf
+   // strings for this message group, then figure out the largest number
+   // of messages in the message group and add the printf strings.
+   if (typeof(this.subBoardListPrintfInfo[pGrpIndex]) == "undefined")
+   {
+      var greatestNumMsgs = getGreatestNumMsgs(pGrpIndex);
+
+      this.subBoardListPrintfInfo[pGrpIndex] = new Object();
+      this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen = greatestNumMsgs.toString().length;
+      // Sub-board name length: With a # items length of 4, this should be
+      // 47 for an 80-column display.
+      this.subBoardListPrintfInfo[pGrpIndex].nameLen = console.screen_columns -
+                                   this.areaNumLen -
+                                   this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen -
+                                   this.dateLen - this.timeLen - 7;
+      // Create the printf strings
+      this.subBoardListPrintfInfo[pGrpIndex].printfStr =
+               " " + this.colors.areaNum
+               + "%" + this.areaNumLen + "d "
+               + this.colors.desc + "%-"
+               + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s "
+               + this.colors.numItems + "%"
+               + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d "
+               + this.colors.latestDate + "%" + this.dateLen + "s "
+               + this.colors.latestTime + "%" + this.timeLen + "s";
+      this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr =
+                              "\1n" + this.colors.bkgHighlight + " "
+                              + "\1n" + this.colors.bkgHighlight
+                              + this.colors.areaNumHighlight
+                              + "%" + this.areaNumLen + "d \1n"
+                              + this.colors.bkgHighlight
+                              + this.colors.descHighlight + "%-"
+                              + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s \1n"
+                              + this.colors.bkgHighlight
+                              + this.colors.numItemsHighlight + "%"
+                              + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d \1n"
+                              + this.colors.bkgHighlight
+                              + this.colors.dateHighlight + "%" + this.dateLen + "s \1n"
+                              + this.colors.bkgHighlight
+                              + this.colors.timeHighlight + "%" + this.timeLen + "s\1n";
+   }
+}
+
+// Removes multiple, leading, and/or trailing spaces.
+// The search & replace regular expressions used in this
+// function came from the following URL:
+//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
+//
+// Parameters:
+//  pString: The string to trim
+//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
+//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
+//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
+function trimSpaces(pString, pLeading, pMultiple, pTrailing)
+{
+	var leading = true;
+	var multiple = true;
+	var trailing = true;
+	if(typeof(pLeading) != "undefined")
+		leading = pLeading;
+	if(typeof(pMultiple) != "undefined")
+		multiple = pMultiple;
+	if(typeof(pTrailing) != "undefined")
+		trailing = pTrailing;
+		
+	// To remove both leading & trailing spaces:
+	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");
+
+	if (leading)
+		pString = pString.replace(/(^\s*)/gi,"");
+	if (multiple)
+		pString = pString.replace(/[ ]{2,}/gi," ");
+	if (trailing)
+		pString = pString.replace(/(\s*$)/gi,"");
+
+	return pString;
+}
+
+// Calculates & returns a page number.
+//
+// Parameters:
+//  pTopIndex: The index (0-based) of the topmost item on the page
+//  pNumPerPage: The number of items per page
+//
+// Return value: The page number
+function calcPageNum(pTopIndex, pNumPerPage)
+{
+  return ((pTopIndex / pNumPerPage) + 1);
+}
+
+// Returns the greatest number of messages of all sub-boards within
+// a message group.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group
+//
+// Returns: The greatest number of messages of all sub-boards within
+//          the message group
+function getGreatestNumMsgs(pGrpIndex)
+{
+  // Sanity checking
+  if (typeof(pGrpIndex) != "number")
+    return 0;
+  if (typeof(msg_area.grp_list[pGrpIndex]) == "undefined")
+    return 0;
+
+  var greatestNumMsgs = 0;
+  var msgBase = null;
+  for (var subIndex = 0; subIndex < msg_area.grp_list[pGrpIndex].sub_list.length; ++subIndex)
+  {
+    msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[subIndex].code);
+    if (msgBase == null) continue;
+    if (msgBase.open())
+    {
+      if (msgBase.total_msgs > greatestNumMsgs)
+        greatestNumMsgs = msgBase.total_msgs;
+      msgBase.close();
+    }
+  }
+  return greatestNumMsgs;
+}
+
+// Inputs a keypress from the user and handles some ESC-based
+// characters such as PageUp, PageDown, and ESC.  If PageUp
+// or PageDown are pressed, this function will return the
+// string "\1PgUp" (KEY_PAGE_UP) or "\1Pgdn" (KEY_PAGE_DOWN),
+// respectively.  Also, F1-F5 will be returned as "\1F1"
+// through "\1F5", respectively.
+// Thanks goes to Psi-Jack for the original impementation
+// of this function.
+//
+// Parameters:
+//  pGetKeyMode: Optional - The mode bits for console.getkey().
+//               If not specified, K_NONE will be used.
+//
+// Return value: The user's keypress
+function getKeyWithESCChars(pGetKeyMode)
+{
+   var getKeyMode = K_NONE;
+   if (typeof(pGetKeyMode) == "number")
+      getKeyMode = pGetKeyMode;
+
+   var userInput = console.getkey(getKeyMode);
+   if (userInput == KEY_ESC) {
+      switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+         case '[':
+            switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+               case 'V':
+                  userInput = KEY_PAGE_UP;
+                  break;
+               case 'U':
+                  userInput = KEY_PAGE_DOWN;
+                  break;
+           }
+           break;
+         case 'O':
+           switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+              case 'P':
+                 userInput = "\1F1";
+                 break;
+              case 'Q':
+                 userInput = "\1F2";
+                 break;
+              case 'R':
+                 userInput = "\1F3";
+                 break;
+              case 'S':
+                 userInput = "\1F4";
+                 break;
+              case 't':
+                 userInput = "\1F5";
+                 break;
+           }
+         default:
+           break;
+      }
+   }
+
+   return userInput;
+}
\ No newline at end of file
diff --git a/xtrn/DDAreaChoosers/FILE_ID.DIZ b/xtrn/DDAreaChoosers/FILE_ID.DIZ
new file mode 100644
index 0000000000000000000000000000000000000000..2f671a872aaac1513b7967567819a8ed51e4e44d
--- /dev/null
+++ b/xtrn/DDAreaChoosers/FILE_ID.DIZ
@@ -0,0 +1,10 @@
+   Digital Distortion Area Choosers v1.08
+�����������������������������������������
+           For Synchronet 3.14+
+These are a couple of JavaScript scripts
+that let the user choose their file & message
+area, with a lightbar interface and
+configurable colors. The message area chooser
+shows latest post dates & times in the
+sub-board lists.
+Release date: 2015-04-19
\ No newline at end of file
diff --git a/xtrn/DDAreaChoosers/Read Me.txt b/xtrn/DDAreaChoosers/Read Me.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b6670d06a90ea5a397198c0b2377c3ce27c6aaa8
--- /dev/null
+++ b/xtrn/DDAreaChoosers/Read Me.txt	
@@ -0,0 +1,438 @@
+                     Digital Distortion Area Choosers
+                              Version 1.08
+                        Release date: 2015-04-19
+
+                                  by
+
+                             Eric Oulashin
+                     Sysop of Digital Distortion BBS
+                 BBS internet address: digdist.bbsindex.com
+                     Email: eric.oulashin@gmail.com
+
+
+
+This file describes the Digital Distortion area chooser scripts.
+
+Contents
+========
+1. Disclaimer
+2. Introduction
+3. Installation & Setup
+4. Configuration file
+5. DDMsgAreaChooser class: Properties & methods
+6. DDFileAreaChooser class: Properties & methods
+7. Revision History
+
+
+1. Disclaimer
+=============
+The only guarantee that I can make about these scripts is that they will take
+up space on your computer.  I have tested these with the Windows verison of
+Synchronet 3.15 and 3.16 in Windows 2000 and Windows XP.
+I created these scripts to customize the message & file area selection on my
+BBS and am providing them to the Synchronet BBS community in case other
+Synchronet sysops might find them useful.
+
+
+2. Introduction
+===============
+The Digital Distortion message & file area chooser scripts provide a lightbar
+or traditional user interface to let the user choose their message and file
+area.  Also, the colors are customizable.  Additionally, the message area
+chooser will show the date & time of the latest post for each of the
+sub-boards.
+
+The file & message area chooser scripts can be run in several ways:
+- Executed from a JavaScript/Baja script
+- Loaded in a JavaScript script, then use the DDMsgAreaChooser or
+  DDFileAreaChooser object to let the user choose their message/file area
+- Set up as a door
+
+These scripts require Synchronet version 3.14 or higher.
+
+
+3. Installation & Setup
+=======================
+Step 1: Copy the following files to a directory of your choice (i.e., sbbs/exec
+or sbbs/mods):
+DDFileAreaChooser.js
+DDMsgAreaChooser.js
+DDMsgAreaChooser.cfg
+DDFileAreaChooser.cfg
+
+Step 2: Set up the scripts to run on your BBS.  To do so, you will need to
+update your command shell to execute the included script instead of
+Synchronet's built-in area chooser functionality.  Synchronet does not use a
+traditional menu system like some other BBS packages do; rather, Synchronet
+uses a "command shell", which is a script that handles user input, performs
+actions based on the user's input, displays menu files, etc.  If you have not
+modified your Synchronet installation much, you may be using the default
+command shell, which (at the time of this writing) is default.src and its
+compiled version, default.bin, which is written in Baja (Synchronet's own
+scripting language).  It's also possible to use a command shell written in
+JavaScript - Synchronet includes a few examples, such as classic_shell.js.
+
+For these examples, I will use DDMsgAreaChooser.js.
+
+To use these scripts in a Baja script (such as the 'default' script mentioned
+above), you can run the area chooser as follows:
+exec "?DDMsgAreaChooser.js"
+
+If you're using a JavaScript command shell, you can run it this way:
+bbs.exec("?DDMsgAreaChooser.js");
+
+To use the message area chooser in the 'default' command shell, search for the
+text "msg_select_area" (without the double-quotes).  It should be underneath a
+line that says "cmdkey J" (meaning the J key is used to jump to another message
+area).  Replace msg_select_area with this line:
+exec "?DDMsgAreaChooser.js"
+
+The process is similar for the file area chooser - Search for file_select_area
+and replace that with the following:
+exec "?DDFileAreaChooser.js"
+
+
+If you are using the JavaScript command shell classic_shell.js, do the
+following for message area selection (some knowledge of the JavaScript language
+will be helpful):
+- Search for the text "case 'J':" (without the double-quotes)
+- Comment out the text from the next line until (and not including) the next
+  case statement
+- Underneath the "case 'J':" line, put the following two lines:
+bbs.exec("?DDMsgAreaChooser.js");
+break;
+
+In classic_shell.js, the process will be similar for file area selection.
+
+
+
+Advanced installation notes (optional)
+--------------------------------------
+The area chooser functionality is encapsulated into JavaScript objects so
+that you can customize the settings & colors within your script if you like.
+To do this, follow these steps:
+ 1. Include the following line in your JavaScript script (preferably near the
+    top):
+ load("DDMsgAreaChooser.js", false);
+ 2. Where you want to have area choosing functionality, instantiate the object
+    and call its SelectMsgArea() function (for the message area chooser) or its
+    SelectFileArea() function (for the file area chooser).  An example:
+    var msgAreaChooser = new DDMsgAreaChooser();
+	 msgAreaChooser.SelectMsgArea();
+You can also list message groups (using the same colors as the chooser) as
+follows:
+  msgAreaChooser.ListMsgGrps();
+You can also use the message lister object to list the sub-boards in the
+current message group (using the same colors as the chooser) as follows:
+  msgAreaChooser.ListSubBoardsInMsgGroup();
+For listing file libraries and file directories inside of a library, you can
+do the following:
+  var fileAreaChooser = new DDFileAreaChooser();
+  fileAreaChooser.ListFileLibs(); // List file libraries
+  fileAreaChooser.ListDirsInFileLib(); // List directories inside the current library
+
+
+If you would like to set up these scripts as doors, the following is an example
+setup of the message area chooser (assuming it is placed in sbbs/exec or
+sbbs/mods):
++[�][?]----------------------------------------------------+
+�                   Message Area Chooser                   �
+�----------------------------------------------------------�
+� �Name                       Message Area Chooser         �
+� �Internal Code              MSGARCHO                     �
+� �Start-up Directory                                      �
+� �Command Line               ?DDMsgAreaChooser.js         �
+� �Clean-up Command Line                                   �
+� �Execution Cost             None                         �
+� �Access Requirements                                     �
+� �Execution Requirements                                  �
+� �Multiple Concurrent Users  Yes                          �
+� �Intercept Standard I/O     No                           �
+� �Native (32-bit) Executable No                           �
+� �Use Shell to Execute       No                           �
+� �Modify User Data           No                           �
+� �Execute on Event           No                           �
+� �Pause After Execution      No                           �
+� �BBS Drop File Type         None                         �
+� �Place Drop File In         Node Directory               �
++----------------------------------------------------------+
+To run that from a JavaScript, include this line:
+bbs.exec_xtrn("MSGARCHO");
+To run that from a Baja script, include this line:
+exec_xtrn MSGARCHO
+
+
+4. Configuration file
+=====================
+If you want to change the default beavior and colors for one of these scripts,
+you can edit its configuration file, which is a plain text file.  The
+configuration files have two sections: A behavior section (denoted by
+[BEHAVIOR]) and a colors section (denoted by [COLORS]).  For each setting or
+color, the syntax is as folows:
+
+setting=value
+
+where "setting" is the behavior setting or color, and "value" is the corresponding
+value for the setting/color.  The colors are Synchronet color codes.
+
+Also, comments are allowed in the configuration file.  Comments begin with a
+semicolon (;).
+
+Behavior section: Message area chooser
+--------------------------------------
+Setting                               Description
+-------                               -----------
+useLightbarInterface                  true/false: Whether or not to use a
+                                      lightbar user interface.
+
+showImportDates                       true/false: Whether or not to show the
+                                      import dates (rather than message dates)
+                                      in the latest date & time column in the
+                                      sub-board lists.
+
+Colors section: Message area chooser
+------------------------------------
+Color setting                        Description
+-------------                        -----------
+areaNum                              The color to use for area numbers
+
+desc                                 The color to use for descriptions
+
+numItems                             The color to use for the item counts
+
+header                               The color to use for list headers
+
+subBoardHeader                       The color to use for the header in the
+                                     sub-board list showing "Sub-boards of"
+                                     with the group description and page number
+                                     (note that the group description will have
+                                     he bright attribute applied)
+
+areaMark                             The color to use for the marker character
+                                     used to show the area that is currently
+                                     selected
+
+latestDate                           The color to use for the latest post date
+
+latestTime                           The color to use for the latest post time
+
+bkgHighlight                         The background highlight color for
+                                     lightbar mode
+
+areaNumHighlight                     The color to use for an area number for
+                                     a selected item in lightbar mode
+
+descHighlight                        The color to use for a description for
+                                     a selected item in lightbar mode
+
+dateHighlight                        The color to use for the date for a
+                                     selected item in lightbar mode
+
+timeHighlight                        The color to use for the time for a
+                                     selected item in lightbar mode
+
+numItemsHighlight                    The color to use for the number of items
+                                     for a selected item in lightbar mode
+
+lightbarHelpLineBkg                  The background color to use for the help
+                                     text line displayed at the bottom of the
+                                     screen in lightbar mode
+
+lightbarHelpLineGeneral              The color to use for general text in the
+                                     help text line displayed at the bottom of
+                                     the screen in lightbar mode
+
+lightbarHelpLineHotkey               The color to use for hotkeys in the help
+                                     text line displayed at the bottom of the
+                                     screen in lightbar mode
+
+lightbarHelpLineParen                The color to use for the ) characters in
+                                     the help text line displayed at the bottom
+                                     of the screen in lightbar mode
+
+Behavior section: File area chooser
+-----------------------------------
+Setting                               Description
+-------                               -----------
+useLightbarInterface                  true/false: Whether or not to use a
+                                      lightbar user interface.
+
+Colors section: File area chooser
+------------------------------------
+Color setting                        Description
+-------------                        -----------
+areaNum                              The color to use for area numbers
+
+desc                                 The color to use for descriptions
+
+numItems                             The color to use for the item counts
+
+header                               The color to use for list headers
+
+fileAreaHdr                          The color to use for the header in the
+                                     directory list showing "Directories of"
+                                     with the group description and page number
+                                     (note that the group description will have
+                                     the bright attribute applied)
+
+areaMark                             The color to use for the marker character
+                                     used to show the area that is currently
+                                     selected
+
+bkgHighlight                         The background highlight color for
+                                     lightbar mode
+
+areaNumHighlight                     The color to use for an area number for
+                                     a selected item in lightbar mode
+
+descHighlight                        The color to use for a description for
+                                     a selected item in lightbar mode
+
+numItemsHighlight                    The color to use for the number of items
+                                     for a selected item in lightbar mode
+
+lightbarHelpLineBkg                  The background color to use for the help
+                                     text line displayed at the bottom of the
+                                     screen in lightbar mode
+
+lightbarHelpLineGeneral              The color to use for general text in the
+                                     help text line displayed at the bottom of
+                                     the screen in lightbar mode
+
+lightbarHelpLineHotkey               The color to use for hotkeys in the help
+                                     text line displayed at the bottom of the
+                                     screen in lightbar mode
+
+lightbarHelpLineParen                The color to use for the ) characters in
+                                     the help text line displayed at the bottom
+                                     of the screen in lightbar mode
+
+
+5. DDMsgAreaChooser class: Properties & methods
+===============================================
+The following are the properties and methods of the DDMsgAreaChooser class, which
+is the class used for letting the user choose a message area:
+Property name                         Description
+-------------                         -----------
+showImportDates                       Boolean: Whether or not to show the
+                                      import dates (rather than message dates)
+                                      in the latest date & time column in the
+                                      sub-board lists.
+
+useLightbarInterface                  Boolean: Whether or not to use a
+                                      lightbar user interface.
+
+Methods
+-------
+Method name                           Description
+-----------                           -----------
+DDMsgAreaChooser()                    Constructor
+
+SelectMsgArea()                       Lets the user choose a message sub-board.
+                                      If the useLightbarInterface property is
+                                      true and the user's terminal supports
+                                      ANSI, it will use the lightbar interface;
+                                      otherwise, it will use traditional
+                                      interface.
+
+SelectMsgArea_Lightbar()              Lets the user choose a message sub-board,
+                                      with a lightbar user interface.
+
+SelectMsgArea_Traditional()           Lets the user choose a message sub-board,
+                                      with a traditional user interface.
+
+ListMsgGrps()                         Lists the message groups
+
+ListSubBoardsInMsgGroup(pGrpIndex,    Lists the sub-boards in the user's
+                        pMarkIndex,   currently-selected message group.
+                        pSortType)    The parameters are all optional.  They
+                                      specify the index of the message group,
+                                      the index of the sub-board to mark with
+                                      the "chosen" character, and a sort type,
+                                      which can be "none" (default sorting),
+                                      "dateAsc" for date ascending,
+                                      "dateDesc" for date descending, or
+                                      "description" for description.
+
+6. DDFileAreaChooser class: Properties & methods
+===============================================
+The following are the properties and methods of the DDMsgAreaChooser class, which
+is the class used for letting the user choose a message area:
+Property name                         Description
+-------------                         -----------
+useLightbarInterface                  Boolean: Whether or not to use a
+                                      lightbar user interface.
+
+Methods
+-------
+Method name                           Description
+-----------                           -----------
+DDFileAreaChooser()                   Constructor
+
+SelectFileArea()                      Lets the user choose a file directory.
+                                      If the useLightbarInterface property is
+                                      true and the user's terminal supports
+                                      ANSI, it will use the lightbar interface;
+                                      otherwise, it will use traditional
+                                      interface.
+
+SelectFileArea_Lightbar()             Lets the user choose a file directory,
+                                      with a lightbar user interface.
+
+SelectFileArea_Traditional()          Lets the user choose a file directory,
+                                      with a traditional user interface.
+
+ListFileLibs()                        Lists the file libraries
+
+ListDirsInFileLib(pLibIndex,          Lists the directories in the user's
+                  pMarkIndex)         currently-selected file library.
+                                      The parameters are optional.  They
+                                      specify the index of the file library
+                                      and the index of the directory to mark
+                                      with the "chosen" character.
+7. Revision History
+===================
+Version  Date         Description
+-------  ----         -----------
+1.08     2015-04-19   Added customizable color settings for the key help text
+                      line displayed at the bottom of the screen in lightbar
+                      mode.  Also, updated to allow the PageUp and PageDown
+                      keys to be used instead of the P and N keys to go to the
+                      previous & next pages in lightbar mode.
+1.07     2014-12-22   Message area chooser:
+                      Bug fix: Made this.colors.subBoardHeader apply to the
+                      whole line rather than just the page number.
+                      Bug fix: The initial display of the page number is now
+                      correct (previously, it would start out saying page 1,
+                      even if on another page).
+                      Documentation & example configuration files:
+                      Added the color options subBoardHeader (for the message
+                      area chooser) and fileAreaHdr (for the file area chooser)
+                      to the documentation and example configuration files.
+1.06     2014-09-14   Bug fix: Updated the lightbar highlight format string to
+                      include a normal attribute at the end to avoid the
+                      highlight color to be used when clearing the screen,
+                      etc.  Bug reported by Psi-Jack.
+1.05     2013-05-10   Bug fix in the file area chooser: When listing
+                      directories in a file group, it would sometimes
+                      crash due to an incorrect array index used, and
+                      the array was not set up.  Those have been fixed.
+1.04     2013-05-04   Updated to properly format message sub-boards and
+                      file directories with more than 9999 entries.  The
+                      formatting is now dynamically adjusted depending
+                      on the greatest number of entries in a sub-board
+                      for a message group or file directory in a file
+                      library (the descriptions will shrink as the
+                      text length of the greatest number of entries
+                      increases).
+1.03     2012-11-30   Bug fix: After leaving the help screen from the
+                      sub-board/directory list, the top line is now
+                      correctly written with the page information as "Page
+                      # of #".
+1.02     2012-10-06   For the lightbar interface, the current page number is
+                      now displayed at the top of the screen (along with the
+                      total number of pages) and is updated when going to a
+                      new page.
+1.01     2011-04-22   Fixed the wording when choosing a message sub-board and
+                      file library.
+1.00     2010-03-13   First public release
\ No newline at end of file