From 8eec123ef08bce07312a8dadef00612d005780f5 Mon Sep 17 00:00:00 2001
From: nightfox <>
Date: Sat, 22 Dec 2012 07:37:47 +0000
Subject: [PATCH] Adding SlyEdit: This is a full-screen message editor for
 Synchronet that mimics the look & feel of IceEdit and DCTEdit.

---
 docs/SlyEdit_ReadMe.txt  |  600 +++++++
 exec/SlyEdit.js          | 3561 ++++++++++++++++++++++++++++++++++++++
 exec/SlyEdit_DCTStuff.js | 1432 +++++++++++++++
 exec/SlyEdit_IceStuff.js |  783 +++++++++
 exec/SlyEdit_Misc.js     | 1636 +++++++++++++++++
 5 files changed, 8012 insertions(+)
 create mode 100644 docs/SlyEdit_ReadMe.txt
 create mode 100644 exec/SlyEdit.js
 create mode 100644 exec/SlyEdit_DCTStuff.js
 create mode 100644 exec/SlyEdit_IceStuff.js
 create mode 100644 exec/SlyEdit_Misc.js

diff --git a/docs/SlyEdit_ReadMe.txt b/docs/SlyEdit_ReadMe.txt
new file mode 100644
index 0000000000..ca7137a040
--- /dev/null
+++ b/docs/SlyEdit_ReadMe.txt
@@ -0,0 +1,600 @@
+                         SlyEdit message editor
+                              Version 1.16
+                        Release date: 2012-12-21
+
+                                  by
+
+                             Eric Oulashin
+                     Sysop of Digital Distortion BBS
+                 BBS internet address: digdist.bbsindex.com
+                     Email: eric.oulashin@gmail.com
+
+
+
+This file describes SlyEdit, a message editor for Synchronet.
+Note: For sysops who already have a previous version of SlyEdit
+installed and are upgrading to this version, please see the file
+Upgrading.txt.
+
+Contents
+========
+1. Disclaimer
+2. Introduction
+3. Installation & Setup
+4. Features
+5. Configuration file
+6. Ice-style Color Theme Settings
+7. DCT-style Color Theme Settings
+8. Revision History (change log)
+
+
+1. Disclaimer
+=============
+Although I have tested SlyEdit, I cannot guarantee that it is 100% bug free or
+will work as expected in all environments in all cases.  That said, I hope you
+find SlyEdit useful and enjoy using it.
+
+
+2. Introduction
+===============
+SlyEdit is a message editor that I wrote in JavaScript for Synchronet, which
+can mimic the look and feel of IceEdit or DCT Edit.  SlyEdit also supports
+customization of colors via theme files.
+
+The motivation for creating this was that IceEdit and DCT Edit were always
+my two favorite BBS message editors, but in a world where 32-bit (and 64-bit)
+Windows and *nix platforms are common, 16-bit DOS emulation is required to
+run IceEdit and DCT Edit, which results in slow (and sometimes unreliable)
+operation.  Since SlyEdit is written in JavaScript, it is faster and does
+not require a 16-bit virtual DOS machine.  Also, being written in JavaScript,
+it should run on any platform where Synchronet runs, so all Synchronet sysops
+can use it.
+
+SlyEdit will recognize the user's terminal size and set up the screen
+accordingly.  The width of the edit area will always be 80 characters; however,
+an increased terminal size will provide more room for message information to be
+displayed, as well as a taller edit area (if the terminal size is taller).
+
+Thanks goes out to Nick of Lightning BBS (lightningbbs.dyndns.org) for testing.
+
+3. Installation & Setup
+=======================
+These are the steps for installation:
+ 1. Unzip the archive.  If you're viewing this file, then you've probably
+    already done this. :)
+ 2. There are 2 ways SlyEdit's files can be copied onto your Synchronet system:
+    1. Copy the JavaScript files into your sbbs/exec directory and the .cfg files
+       into your sbbs/ctrl directory
+    2. Copy all files together into their own directory of your choice
+ 3. Set up SlyEdit on your BBS with Synchronet's configration program (SCFG).
+
+SlyEdit can be set to mimic IceEdit or DCT Edit via a command-line parameter.
+The values for this parameter are as follows:
+  ICE: Mimic the IceEdit look & feel
+  DCT: Mimic the DCT Edit look & feel
+  RANDOM: Randomly select either the IceEdit or DCT Edit look & feel
+
+To add SlyEdit to Synchronet's list of external editors, run Synchronet's
+configuration program (SCFG) and select "External Programs", and then
+"External Editors".  The following describes setting up SlyEdit using the
+ICE parameter for IceEdit emulation:
+ 1. Scroll down to the empty slot in the editor list and press Enter to
+    select it.
+ 2. For the external editor name, enter "SlyEdit (Ice style)" (without the quotes)
+    (or similar, depending on your personal preference)
+ 3. For the internal code, use SLYEDICE (or whatever you want, depending on your
+    personal preference)
+ 4. Press Enter to select and edit the new entry.  Asuming that the .js files
+    are in the sbbs/exect directory, the settings should be as follows:
+      Command line: ?SlyEdit.js %f ICE
+      Access requirement string: ANSI
+      Intercept standard I/O: No
+      Native (32-bit) Executable: No
+      Use Shell to Execute: No
+      Quoted Text: All
+      Editor Information Files: QuickBBS MSGINF/MSGTMP
+      Expand Line Feeds to CRLF: Yes
+      Strip FidoNet Kludge Lines: No
+      BBS Drop File Type: None
+    After you've added SlyEdit, your Synchronet configuration window should look
+    like this:
+    +[�][?]--------------------------------------------------------------+
+    �                      SlyEdit (Ice style) Editor                    �
+    �--------------------------------------------------------------------�
+    � �Name                            SlyEdit  (Ice style)              �
+    � �Internal Code                   SLYEDICE                          �
+    � �Remote Command Line             ?SlyEdit.js %f ICE                �
+    � �Access Requirements             ANSI                              �
+    � �Intercept Standard I/O          No                                �
+    � �Native (32-bit) Executable      No                                �
+    � �Use Shell to Execute            No                                �
+    � �Quoted Text                     All                               �
+    � �Editor Information Files        QuickBBS MSGINF/MSGTMP            �
+    � �Expand Line Feeds to CRLF       Yes                               �
+    � �Strip FidoNet Kludge Lines      No                                �
+    � �BBS Drop File Type              None                              �
+    +--------------------------------------------------------------------+
+
+    For DCT Edit mode, use DCT in place of ICE on the command line.  For
+    random mode, use RANDOM in place of ICE.
+
+    Note that if you placed the files in a different directory, then the
+    command line should include the full path to SlyEdit.js.  For example,
+    if SlyEdit was placed in sbbs/xtrn/SlyEdit, then the command line would
+    be /BBS/sbbs/xtrn/DigDist/SlyEdit/SlyEdit.js %f ICE
+
+
+4. Features
+===========
+As mentioned earlier, SlyEdit is can mimic the look & feel of IceEdit or
+DCT Edit.  It also has the following features:
+- Text search: Allows the user to search for text in the message.  If
+  the text is found, the message area will scroll to it, and it will be
+  highlighted.  Repeated searches for the same text will look for the
+  next occurrance of that text.
+- Navigation: Page up/down, home/end of line, and arrow keys
+- Slash commands (at the start of the line):
+  /A: Abort
+  /S: Save
+  /Q: Quote message
+- Sysops can import a file (from the BBS machine) into the message
+- Sysops can export the current message to a file (on the BBS machine)
+- Configuration file with behavior and color settings.  The configuration
+  settings are fairly limited right now, and I plan to add more color
+  settings in future versions.  See section 4 (Configuration File) for
+  more information.
+
+The following is a summary of the keyboard shortcuts (from SlyEdit's command
+help screen):
+
+Help keys                                     Slash commands (@ line start)
+---------                                     -----------------------------
+Ctrl-G       : General help                 | /A     : Abort
+Ctrl-P       : Command key help             | /S     : Save
+Ctrl-R       : Program information          | /Q     : Quote message
+
+Command/edit keys
+-----------------
+Ctrl-A       : Abort message                | Ctrl-W : Page up
+Ctrl-Z       : Save message                 | Ctrl-S : Page down
+Ctrl-Q       : Quote message                | Ctrl-N : Find text
+Insert/Ctrl-I: Toggle insert/overwrite mode | ESC    : Command menu
+Ctrl-O       : Import a file                | Ctrl-X : Export to file
+Ctrl-D       : Delete line
+
+
+5. Configuration file
+=====================
+The configuration file, SlyEdit.cfg, is split up into 3 sections -
+Behavior, Ice colors, and DCT colors.  These sections are designated
+by [BEHAVIOR], [ICE_COLORS], and [DCT_COLORS], respectively.  These
+settings are described below:
+
+Behavior settings
+-----------------
+Setting                           Description
+-------                           -----------
+displayEndInfoScreen              Whether or not to display the info
+                                  screen when SlyEdit exits.  Valid values
+                                  are true and false.  If this option is
+                                  not specified, this feature will be
+                                  enabled by default.
+
+userInputTimeout                  Whether or not to use an input timeout
+                                  for users.  Valid values are true and
+                                  false.  Note: The input timeout is not
+                                  used for sysops.  If this option is not
+                                  specified, this feature will be enabled
+                                  by default.
+
+inputTimeoutMS                    The amount of time (in milliseconds) to
+                                  use for the input timeout.  If this option
+                                  is not specified, this option will default
+                                  to 300000.
+
+reWrapQuoteLines                  Whether or not to re-wrap quote lines. Valid
+                                  values are true and false, and this feature
+                                  is enabled by default.  With this feature
+                                  enabled, SlyEdit will re-wrap quote lines
+                                  to still be complete and readable after the
+                                  quote prefix character is added to the front
+                                  of the quote lines.  SlyEdit is able to
+                                  recognize quote lines beginning with >
+                                  or 2 letters and a > (such as EO>).  If this
+                                  feature is disabled, quote lines will simply
+                                  be trimmed to make room for the quote prefix
+                                  character to be added to the front.
+
+add3rdPartyStartupScript          Add a 3rd-party JavaScript script to execute
+                                  (via loading) upon startup of SlyEdit.  The
+                                  parameter must specify the full path & filename
+                                  of the JavaScript script.  For example (using
+                                  the excellent Desafortunadamente add-on by Art
+                                  of Fat Cats BBS):
+                                  add3rdPartyStartupScript=D:/BBS/sbbs/xtrn/desafortunadamente/desafortunadamente.js
+
+addJSOnStart                      Add a JavaScript command to run on startup.  Any
+                                  commands added this way will be executed after
+                                  3rd-party scripts are loaded.
+                                  Example (using the excellent Desafortunadamente
+                                  add-on by Art of Fat Cats BBS):
+                                  addJSOnStart=fortune_load();
+
+add3rdPartyExitScript             Add a 3rd-party JavaScript script to execute
+                                  (via loading) upon exit of SlyEdit.  The
+                                  parameter must specify the full path & filename
+                                  of the JavaScript script.
+
+addJSOnExit                       Add a JavaScript command to run on exit.
+                                  Example (don't actually do this):
+                                  addJSOnStart=console.print("Hello\r\n\1p");
+
+Ice colors
+----------
+Setting                           Description
+-------                           -----------
+ThemeFilename                     The name of the color theme file to use.
+                                  Note: Ice-style theme settings are described
+                                  in Section 5: Ice-style Color Theme Settings.
+                                  If no theme file is specified, then default
+                                  colors will be used.
+
+DCT colors
+----------
+Setting                           Description
+-------                           -----------
+ThemeFilename                     The name of the color theme file to use.
+                                  Note: DCT-style theme settings are described
+                                  in Section 6: DCT-style Color Theme Settings.
+                                  If no theme file is specified, then default
+                                  colors will be used.
+
+The color theme files are plain text files that can be edited with a
+text editor.
+
+
+6. Ice-style Color Theme Settings
+=================================
+The following options are valid for Ice-style theme files:
+----------------------------------------------------------
+TextEditColor                     The color for the message text
+
+QuoteLineColor                    The color for quoted lines in the message
+
+BorderColor1                      The first color to use for borders
+                                  (for alternating border colors)
+
+BorderColor2                      The other color to use for borders
+                                  (for alternating border colors)
+
+KeyInfoLabelColor                 The color to use for key information
+                                  labels (displayed on the bottom border)
+
+TopInfoBkgColor                   The color to use for the background in
+                                  the informational area at the top
+
+TopLabelColor                     The color to use for informational labels
+                                  in the informational area at the top
+
+TopLabelColonColor                The color to use for the colons (:) in the
+                                  informational area at the top
+
+TopToColor                        The color to use for the "To" name in the
+                                  informational area at the top
+
+TopFromColor                      The color to use for the "From" name in the
+                                  informational area at the top
+
+TopSubjectColor                   The color to use for the subject in the
+                                  informational area at the top
+
+TopTimeColor                      The color to use for the time left in the
+                                  informational area at the top
+
+TopTimeLeftColor                  The color to use for the time left in the
+                                  informational area at the top
+
+EditMode                          The color to use for the edit mode text
+
+QuoteWinText                      The color for non-highlighted text in
+                                  the quote window
+
+QuoteLineHighlightColor           The color for highlighted text in the
+                                  quote window
+
+QuoteWinBorderTextColor           The color for the quote window borders
+
+; Colors for the multi-choice options
+SelectedOptionBorderColor         The color to use for the borders around
+                                  text for selected multi-choice options
+
+SelectedOptionTextColor           The color to use for the text for selected
+                                  multi-choice options
+
+UnselectedOptionBorderColor       The color to use for the borders around
+                                  text for unselected multi-choice options
+
+UnselectedOptionTextColor         The color to use for the text for unselected
+                                  multi-choice options
+
+
+7. DCT-style Color Theme Settings
+=================================
+The following options are valid for DCT-style theme files:
+----------------------------------------------------------
+TextEditColor                     The color for the message text
+
+QuoteLineColor                    The color for quoted lines in the message
+
+TopBorderColor1                   The first color to use for the
+                                  top borders (for alternating border
+                                  colors)
+
+TopBorderColor2                   The other color to use for the
+                                  top borders (for alternating border
+                                  colors)
+
+EditAreaBorderColor1              The first color to use for the
+                                  edit area borders (for alternating border
+                                  colors)
+
+EditAreaBorderColor2              The other color to use for the
+                                  edit area borders (for alternating border
+                                  colors)
+
+EditModeBrackets                  The color to use for the square brackets
+                                  around the edit mode text that appears
+                                  in the bottom border (the [ and ] around
+                                  the "INS"/"OVR")
+
+EditMode                          The color to use for the edit mode text
+
+TopLabelColor                     The color to use for the informational labels
+                                  in the informational area at the top
+
+TopLabelColonColor                The color to use for the colons (:) in the
+                                  informational area at the top
+
+TopFromColor                      The color to use for the "From" name in the
+                                  informational area at the top
+
+TopFromFillColor                  The color to use for the filler dots in the
+                                  "From" name in the informational area at the top
+
+TopToColor                        The color to use for the "To" name in the
+                                  informational area at the top
+
+TopToFillColor                    The color to use for the filler dots in the
+                                  "To" name in the informational area at the top
+
+TopSubjColor                      The color to use for the subject in the informational
+                                  area at the top
+
+TopSubjFillColor                  The color to use for the filler dots in the subject
+                                  in the informational area at the top
+
+TopAreaColor                      The color to use for the "Area" text in the
+                                  informational area at the top
+
+TopAreaFillColor                  The color to use for the filler dots in the "Area"
+                                  field in the informational area at the top
+
+TopTimeColor                      The color to use for the "Time" text in the
+                                  informational area at the top
+
+TopTimeFillColor                  The color to use for the filler dots in the "Time"
+                                  field in the informational area at the top
+
+TopTimeLeftColor                  The color to use for the "Time left" text in the
+                                  informational area at the top
+
+TopTimeLeftFillColor              The color to use for the filler dots in the "Time left"
+                                  field in the informational area at the top
+
+TopInfoBracketColor               The color to use for the square brackets in the
+                                  informational area at the top
+
+QuoteWinText                      The color for non-highlighted text in
+                                  the quote window
+
+QuoteLineHighlightColor           The color for highlighted text in the
+                                  quote window
+
+QuoteWinBorderTextColor           The color to use for the text in the quote window
+                                  borders
+
+QuoteWinBorderColor               The color to use for the quote window borders
+
+BottomHelpBrackets                The color to use for the brackets displayed in
+                                  the line of help text at the bottom
+
+BottomHelpKeys                    The color to use for the key names written in
+                                  the line of help text at the botom
+
+BottomHelpFill                    The color to use for the filler dots in the line of
+                                  help text at the bottom
+
+BottomHelpKeyDesc                 The color to use for the key descriptions in the
+                                  line of help text at the bottom
+
+TextBoxBorder                     The color to use for text box borders (i.e., the
+                                  abort confirmation prompt)
+
+TextBoxBorderText                 The color to use for text in the borders of text
+                                  boxes (i.e., the abort confirmation prompt)
+
+TextBoxInnerText                  The color to use for text inside text boxes
+
+YesNoBoxBrackets                  The color to use for the square brackets used for
+                                  yes/no confirmation prompt boxes
+
+YesNoBoxYesNoText                 The color to use for the actual "Yes"/"No" text in
+                                  yes/no confirmation prompt boxes
+
+SelectedMenuLabelBorders          The color to use for the border characters for the
+                                  labels of currently active drop-down menus
+
+SelectedMenuLabelText             The color to use for the text for the labels of
+                                  currently active drop-down menus
+
+UnselectedMenuLabelText           The color to use for the text for the labels of
+                                  inactive drop-down menus
+
+MenuBorders                       The color to use for the drop-down menu borders
+
+MenuSelectedItems                 The color to use for selected items on the drop-down
+                                  menus
+
+MenuUnselectedItems               The color to use for unselected items on the
+                                  drop-down menus
+
+MenuHotkeys                       The color to use for the hotkey characters in the
+                                  menu items on the drop-down menus
+
+
+8. Revision History (change log)
+================================
+Version  Date         Description
+-------  ----         -----------
+1.16     2012-12-21   Updated to look for the .cfg files first in
+                      the sbbs/ctrl directory, and if they're not
+                      found there, assume they're in the same
+                      directory as the .js files.
+1.15     2012-04-22   Improved quoting with the ability to re-wrap quote lines
+                      so that they are complete but still look good when
+                      quoted.  SlyEdit recognizes quote lines beginning with >
+                      or 1 or 2 intials followed by a >.  The configuration
+                      option "splitLongQuoteLines" was replaced by
+                      "reWrapQuoteLines", and it is enabled by default.
+                      Also, added the following configuration options and capabilities:
+                      add3rdPartyStartupScript:
+                                  Add a 3rd-party JavaScript script to execute
+                                  (via loading) upon startup of SlyEdit.  The
+                                  parameter must specify the full path & filename
+                                  of the JavaScript script.  For example (using
+                                  the excellent Desafortunadamente add-on by Art
+                                  of Fat Cats BBS):
+                                  add3rdPartyStartupScript=D:/BBS/sbbs/xtrn/desafortunadamente/desafortunadamente.js
+                      addJSOnStart:
+                                  Add a JavaScript command to run on startup.  Any
+                                  commands added this way will be executed after
+                                  3rd-party scripts are loaded.
+                                  Example (using the excellent Desafortunadamente
+                                  add-on by Art of Fat Cats BBS):
+                                  addJSOnStart=fortune_load();
+                      add3rdPartyExitScript:
+                                  Add a 3rd-party JavaScript script to execute
+                                  (via loading) upon exit of SlyEdit.  The
+                                  parameter must specify the full path & filename
+                                  of the JavaScript script.
+                      addJSOnExit:
+                                  Add a JavaScript command to run on exit.
+                                  Example (don't actually do this):
+                                  addJSOnStart=console.print("Hello\n\1p");
+1.145    2011-02-07   The time on the screen will now be updated.  The
+                      time is checked every 5 keystrokes and will be
+                      updated on the screen when it changes.
+1.144    2010-11-21   Minor bug fix: In DCT mode, if the top or bottom border
+                      of one of the menus or the abort confirmation box is
+                      on the first or last line of text on the screen and the
+                      text line ends before the box border ends, the box border
+                      is now fully erased when it disappears.
+1.143    2010-06-19   Minor bug fix: When typing an entire line of text that
+                      doesn't have any spaces, the last character was being
+                      discarded when wrapping to the next line.
+1.142    2010-02-04   Minor bug fix: When reading quote lines and the
+                      splitLongQuoteLines is disabled, it will no longer
+                      (incorrectly) insert "null" as the last quote line
+                      (as is done when the splitLongQuoteLines option is
+                      enabled).
+1.141    2010-01-23   Bug fix: The screen wouldn't update when pressing the Delete
+                      key on a blank line, which would remove the line.
+1.14     2010-01-19   Bug fix: The screen wouldn't update when pressing the Delete
+                      key at the end of a line (specifically, with a blank line
+                      below it followed by a non-blank line).
+                      Also, updated to allow combining quote lines by pressing
+                      the Delete key at the end of a quote line.
+1.131    2010-01-10   Minor update - The option for splitting long quote
+                      lines, which was enabled by default in the previous
+                      version, is now disabled by default in this verison.
+                      It seems that there may be more sysops that don't
+                      like it than those who do like it.
+                      The code in the .js files in this version is also
+                      a little more refactored.
+1.13     2010-01-04   Includes the ability to split up quote lines that
+                      are too long, rather than truncating them.  This
+                      is an option that can be toggled and is enabled by
+                      default.
+                      Includes several bug fixes related to message editing
+                      (i.e., such as word wrapping for the last word on a
+                      line) and other behind-the-scenes bug fixes.
+                      Efficiency of screen updates has been improved somewhat
+                      in this release.  This is more noticeable on slower
+                      connections.
+1.12     2009-12-14   Behavior change: Now never removes any spaces from
+                      the beginning of a line when the user presses enter
+                      at the beginning of a line.
+1.11     2009-12-10   Added the ability to customize the quote line color
+                      in the color theme files (QuoteLineColor).
+                      Fixed a bug where the text color temporarily went
+                      back to default (not using the customized text
+                      color) when moving to a new line when typing a
+                      message.
+                      Updated to (hopefully) fixed a bug that could
+                      cause the script to abort when adding a line to
+                      the message in rare circumstances.
+1.10     2009-12-03   Added support for customizable color themes.
+                      Fixed a couple of text editing bugs/annoyances.
+1.08     2009-11-10   Changed the way the message is saved back to the
+                      way SlyEdit was saving in 1.06 and earlier, as
+                      this simplifies the code.  The "Expand Line Feeds
+                      to CRLF" option needs to be enabled in SCFG for
+                      messages to be saved properly in all platforms.
+                      Added configuration options to enable/disable the
+                      user input timeout, and to specify the input timeout
+                      time (in MS).
+                      The sysop is now always exempt from the input timeout.
+                      Also, started to work on improving the efficiency
+                      of refreshing the message area.
+1.07     2009-10-23   Bug fix: Changed how the end-of-line newline
+                      characters are written to the message file so
+                      that the message text lines are saved correctly
+                      in Linux.  The bug in previous versions was causing
+                      messages going across certain networks to lose their
+                      end-of-line characters so that text lines weren't
+                      terminated where they were supposed to be.  Thanks
+                      goes to Tracker1 (sysop of The Roughnecks BBS) for
+                      the tip of how to fix this.
+                      New feature: Configuration file with settings
+                      for whether or not to display the ending info
+                      screen, as well as quote window colors for both
+                      Ice and DCT modes.
+1.06     2009-09-12   Bug fix: Updated the way it checks for printable
+                      characters.  This should fix the problem I've seen
+                      with some BBSs where it wouldn't allow typing an
+                      upper-case letter.
+1.05     2009-08-30   Bug fix: When editing an existing message, the cursor
+                      would be at the bottom of the edit area, and it would
+                      appear stuck there, unable to move up.  This has been
+                      fixed - Now the cursor is always initially placed at
+                      the start of the edit area.
+                      Bug fix: When saving a message, blank lines are now
+                      removed from the end of a message before saving.
+1.04     2009-08-27   Bug fix: When wrapping a text line, it would place
+                      individual words on lines by themselves, due to a
+                      change in 1.03.  This has been fixed.
+1.03     2009-08-27   Bug fix: With a small message (less than one screenful),
+                      Ctrl-S (page down) now doesn't crash SlyEdit.
+                      Bug fix: When typing and the end of the line and it has to
+                      wrap the word to the next line, it now always inserts a new
+                      line below the current line, pushing the rest of the message
+                      down.
+1.02     2009-08-26   Bug fix: Now prevents invalid text lines from quotes.txt or
+                      the message file from being used.
+1.01     2009-08-23   Bug fix: Blank edit lines would be removed
+                      when they weren't supposed to if the user
+                      used the /S or /A commands on a line that
+                      wasn't the last line.
+1.00     2009-08-22   First public release
+0.99     2009-08-13-  Test release.  Finishing up features, testing,
+         2009-08-20   and fixing bugs before general release.
\ No newline at end of file
diff --git a/exec/SlyEdit.js b/exec/SlyEdit.js
new file mode 100644
index 0000000000..126b2fe380
--- /dev/null
+++ b/exec/SlyEdit.js
@@ -0,0 +1,3561 @@
+/* This is a text editor for Synchronet designed to mimic the look & feel of
+ * DCTEdit and IceEdit, since neither of those editors have been developed
+ * for quite a while and still exist only as 16-bit DOS applications.
+ *
+ * Author: Eric Oulashin (AKA Nightfox)
+ * BBS: Digital Distortion
+ * BBS address: digdist.bbsindex.com
+ *
+ * Date       Author            Description
+ * 2009-05-11 Eric Oulashin     Started development
+ * 2009-06-11 Eric Oulashin     Taking a break from development
+ * 2009-08-09 Eric Oulashin     Started more development & testing
+ * 2009-08-22 Eric Oulashin     Version 1.00
+ *                              Initial public release
+ * ....Removed some comments... Note: I've started working on Ctrl-A
+ * color selection (for message text colorization), but that feature
+ * is not complete yet.
+ * 2010-11-19 Eric Oulashin     For version 1.144: Added a parameter to
+ *                              displayMessageRectangle() to optionally
+ *                              enable the following behavior:
+ *                              When erasing the menu box, if the box width
+ *                              is longer than the text that was written,
+ *                              then it will output spaces to clear the rest
+ *                              of the line to erase the rest of the box line.
+ * 2010-11-21 Eric Oulashin     Version 1.144
+ *                              Bug fix release: In DCT mode, now erases the
+ *                              whole upper/lower border of the menu & abort
+ *                              confirm boxes on the first/last line of the
+ *                              message text when the box disappears.
+ * 2011-02-03 Eric Oulashin     Started updating to update the time on the
+ *                              screen periodically.
+ * 2011-02-07 Eric Oulashin     Version 1.145
+ *                              Updated to update the time on the screen
+ *                              when it changes.  Checks every 5 keystrokes.
+ * 2012-02-18 Eric Oulashin     Version 1.146 beta X
+ *                              Working on enhanced quote line wrapping
+ * 2012-03-31 Eric Oulashin     Added the ability to run 3rd-party JavaScript
+ *                              scripts upon startup & exit (via load()) and
+ *                              to run extra JavaScript commands upon startup
+ *                              & exit (configurable in SlyEdit.cfg).
+ * 2012-04-01 Eric Oulashin     Version 1.146 beta 4
+ *                              Worked on wrapping quote lines smarter: Added
+ *                              handling for quotes with 1 or 2 initials (i.e.,
+ *                              "EO>" or "E>").
+ * 2012-04-11 Eric Oulashin     Fixed a bug with quote line wrapping where it
+ *                              wasn't properly dealing with quote lines that
+ *                              were blank after the quote text.
+ * 2012-04-22 Eric Oulashin     Version 1.15
+ *                              Quoting update looks good, so I'm releasing
+ *                              this version.
+ * 2012-12-21 Eric Oulashin     Version 1.16
+ *                              Updated to look for the .cfg files first in
+ *                              the sbbs/ctrl directory, and if they're not
+ *                              found there, assume they're in the same
+ *                              directory as the .js files.
+ */
+
+/* Command-line arguments:
+ 1 (argv[0]): Filename to read/edit
+ 2 (argv[1]): Editor mode ("DCT", "ICE", or "RANDOM")
+*/
+
+// Determine the location of this script (its startup directory).
+// The code for figuring this out is a trick that was created by Deuce,
+// suggested by Rob Swindell.  I've shortened the code a little.
+var gStartupPath = '.';
+try { throw dig.dist(dist); } catch(e) { gStartupPath = e.fileName; }
+gStartupPath = backslash(gStartupPath.replace(/[\/\\][^\/\\]*$/,''));
+
+// Load sbbsdefs.js and SlyEdit's misc. defs first
+load("sbbsdefs.js");
+load(gStartupPath + "SlyEdit_Misc.js");
+
+// Load program settings from SlyEdit.cfg
+var gConfigSettings = ReadSlyEditConfigFile();
+// Load any specified 3rd-party startup scripts
+for (var i = 0; i < gConfigSettings.thirdPartyLoadOnStart.length; ++i)
+  load(gConfigSettings.thirdPartyLoadOnStart[i]);
+// Execute any provided startup JavaScript commands
+for (var i = 0; i < gConfigSettings.runJSOnStart.length; ++i)
+  eval(gConfigSettings.runJSOnStart[i]);
+
+// Now, load the DCT & Ice scripts (they use settings gConfigSettings)
+load(gStartupPath + "SlyEdit_DCTStuff.js");
+load(gStartupPath + "SlyEdit_IceStuff.js");
+
+const EDITOR_PROGRAM_NAME = "SlyEdit";
+
+// This script requires Synchronet version 3.14 or higher.
+// Exit if the Synchronet version is below the minimum.
+if (system.version_num < 31400)
+{
+  console.print("n");
+	console.crlf();
+	console.print("nhyi* Warning:nhw " + EDITOR_PROGRAM_NAME);
+	console.print(" " + "requires version g3.14w or");
+	console.crlf();
+	console.print("higher of Synchronet.  This BBS is using version g");
+	console.print(system.version + "w.  Please notify the sysop.");
+	console.crlf();
+	console.pause();
+	exit(1); // 1: Aborted
+}
+// If the user's terminal doesn't support ANSI, then exit.
+if (!console.term_supports(USER_ANSI))
+{
+	console.print("n\r\nhyERROR: w" + EDITOR_PROGRAM_NAME +
+	              " requires an ANSI terminal.n\r\np");
+	exit(1); // 1: Aborted
+}
+
+// Constants
+const EDITOR_VERSION = "1.16";
+const EDITOR_VER_DATE = "2012-12-21";
+
+
+// Program variables
+var gIsSysop = user.compare_ars("SYSOP"); // Whether or not the user is a sysop
+var gEditTop = 6;                         // The top line of the edit area
+var gEditBottom = console.screen_rows-2;  // The last line of the edit area
+// gEditLeft and gEditRight are the rightmost and leftmost columns of the edit
+// area, respectively.  They default to an edit area 80 characters wide
+// in the center of the screen, but for IceEdit mode, the edit area will
+// be on the left side of the screen to match up with the screen header.
+// gEditLeft and gEditRight are 1-based.
+var gEditLeft = (console.screen_columns/2).toFixed(0) - 40 + 1;
+var gEditRight = gEditLeft + 79; // Based on gEditLeft being 1-based
+// If the screen has less than 80 columns, then use the whole screen.
+if (console.screen_columns < 80)
+{
+	gEditLeft = 1;
+	gEditRight = console.screen_columns;
+}
+// Note: gQuotePrefix is declared in SlyEdit_Misc.js.
+
+// Colors
+var gQuoteWinTextColor = "n7k";   // Normal text color for the quote window (DCT default)
+var gQuoteLineHighlightColor = "nw"; // Highlighted text color for the quote window (DCT default)
+var gTextAttrs = "nw";               // The text color for edit mode
+var gQuoteLineColor = "nc";          // The text color for quote lines
+var gUseTextAttribs = false;              // Will be set to true if text colors start to be used
+
+// EDITOR_STYLE: Can be changed to mimic the look of DCT Edit or IceEdit.
+// The following are supported:
+//  "DCT": DCT Edit style
+//  "ICE": IceEdit style
+//  "RANDOM": Randomly choose a style
+var EDITOR_STYLE = "DCT";
+// The second command-line argument (argv[1]) can change this.
+if (typeof(argv[1]) != "undefined")
+{
+	var styleUpper = argv[1].toUpperCase();
+	// Make sure styleUpper is valid before setting EDITOR_STYLE.
+	if (styleUpper == "DCT")
+		EDITOR_STYLE = "DCT";
+	else if (styleUpper == "ICE")
+		EDITOR_STYLE = "ICE";
+	else if (styleUpper == "RANDOM")
+      EDITOR_STYLE = (Math.floor(Math.random()*2) == 0) ? "DCT" : "ICE";
+}
+// Set variables properly for the different editor styles.  Also, set up
+// function pointers for functionality common to the IceEdit & DCTedit styles.
+var fpDrawQuoteWindowTopBorder = DrawQuoteWindowTopBorder_DCTStyle;
+var fpDisplayTextAreaBottomBorder = DisplayTextAreaBottomBorder_DCTStyle;
+var fpDrawQuoteWindowBottomBorder = DrawQuoteWindowBottomBorder_DCTStyle;
+var fpRedrawScreen = redrawScreen_DCTStyle;
+var fpUpdateInsertModeOnScreen = updateInsertModeOnScreen_DCTStyle;
+var fpDisplayBottomHelpLine = DisplayBottomHelpLine_DCTStyle;
+var fpHandleESCMenu = handleDCTESCMenu;
+var fpDisplayTime = displayTime_DCTStyle;
+if (EDITOR_STYLE == "DCT")
+{
+	gEditTop = 6;
+	gQuoteWinTextColor = gConfigSettings.DCTColors.QuoteWinText;
+	gQuoteLineHighlightColor = gConfigSettings.DCTColors.QuoteLineHighlightColor;
+	gTextAttrs = gConfigSettings.DCTColors.TextEditColor;
+	gQuoteLineColor = gConfigSettings.DCTColors.QuoteLineColor;
+}
+else if (EDITOR_STYLE == "ICE")
+{
+	gEditTop = 5;
+	gQuoteWinTextColor = gConfigSettings.iceColors.QuoteWinText;
+	gQuoteLineHighlightColor = gConfigSettings.iceColors.QuoteLineHighlightColor;
+	gTextAttrs = gConfigSettings.iceColors.TextEditColor;
+	gQuoteLineColor = gConfigSettings.iceColors.QuoteLineColor;
+
+	// Function pointers for the styled screen update functions
+	fpDrawQuoteWindowTopBorder = DrawQuoteWindowTopBorder_IceStyle;
+	fpDisplayTextAreaBottomBorder = DisplayTextAreaBottomBorder_IceStyle;
+	fpDrawQuoteWindowBottomBorder = DrawQuoteWindowBottomBorder_IceStyle;
+	fpRedrawScreen = redrawScreen_IceStyle;
+	fpUpdateInsertModeOnScreen = updateInsertModeOnScreen_IceStyle;
+	fpDisplayBottomHelpLine = DisplayBottomHelpLine_IceStyle;
+	fpHandleESCMenu = handleIceESCMenu;
+	fpDisplayTime = displayTime_IceStyle;
+}
+
+// Temporary (for testing): Make the edit area small
+//gEditLeft = 25;
+//gEditRight = 45;
+//gEditBottom = gEditTop + 1;
+// End Temporary
+
+// Calculate the edit area width & height
+const gEditWidth = gEditRight - gEditLeft + 1;
+const gEditHeight = gEditBottom - gEditTop + 1;
+
+// Message display & edit variables
+var gInsertMode = "INS";       // Insert (INS) or overwrite (OVR) mode
+var gQuoteLines = new Array(); // Array of quote lines loaded from file, if in quote mode
+var gQuoteLinesTopIndex = 0;   // Index of the first displayed quote line
+var gQuoteLinesIndex = 0;      // Index of the current quote line
+// The gEditLines array will contain TextLine objects storing the line
+// information.
+var gEditLines = new Array();
+var gEditLinesIndex = 0;      // Index into gEditLines for the line being edited
+var gTextLineIndex = 0;       // Index into the current text line being edited
+// Format strings used for printf() to display text in the edit area
+const gFormatStr = "%-" + gEditWidth + "s";
+const gFormatStrWithAttr = "%s%-" + gEditWidth + "s";
+
+// gEditAreaBuffer will be an array of strings for the edit area, which
+// will be checked by displayEditLines() before outputting text lines
+// to optimize the update of message text on the screen. displayEditLines()
+// will also update this array after writing a line of text to the screen.
+// The indexes in this array are the absolute screen lines.
+var gEditAreaBuffer = new Array();
+function clearEditAreaBuffer()
+{
+   for (var lineNum = gEditTop; lineNum <= gEditBottom; ++lineNum)
+      gEditAreaBuffer[lineNum] = "";
+}
+clearEditAreaBuffer();
+
+// Set some stuff up for message editing
+var gUseQuotes = true;
+var gInputFilename = file_getcase(system.node_dir + "QUOTES.TXT");
+if (gInputFilename == undefined)
+{
+	gUseQuotes = false;
+	if ((argc > 0) && (gInputFilename == undefined))
+		gInputFilename = argv[0];
+}
+else
+{
+	var all_files = directory(system.node_dir + "*");
+	var newest_filedate = -Infinity;
+	var newest_filename;
+	for (var file in all_files)
+	{
+		if (all_files[file].search(/quotes.txt$/i) != -1)
+		{
+			var this_date = file_date(all_files[file]);
+			if (this_date > newest_filedate)
+			{
+				newest_filename = all_files[file];
+				newest_filedate = this_date;
+			}
+		}
+	}
+	if (newest_filename != undefined)
+		gInputFilename = newest_filename;
+}
+
+var gOldStatus = bbs.sys_status;
+bbs.sys_status &=~SS_PAUSEON;
+bbs.sys_status |= SS_PAUSEOFF;
+var gOldPassthru = console.ctrlkey_passthru;
+console.ctrlkey_passthru = "+ACGKLOPQRTUVWXYZ_";
+// Enable delete line in SyncTERM (Disabling ANSI Music in the process)
+console.write("\033[=1M");
+console.clear();
+// Open the quote file / message file
+var inputFile = new File(gInputFilename);
+if (inputFile.open("r", false))
+{
+	// Read into the gQuoteLines or gEditLines array, depending on the value
+	// of gUseQuotes.  Use a buffer size that should be long enough.
+	if (gUseQuotes)
+	{
+      var textLine = null;  // Line of text read from the quotes file
+      while (!inputFile.eof)
+      {
+        textLine = inputFile.readln(2048);
+        // Only use textLine if it's actually a string.
+        if (typeof(textLine) == "string")
+        {
+           textLine = strip_ctrl(textLine);
+           gQuoteLines.push(textLine);
+        }
+     }
+     // If the setting to re-wrap quote lines is enabled, then do it.
+     if (gConfigSettings.reWrapQuoteLines && (gQuoteLines.length > 0))
+       wrapQuoteLines();
+	}
+	else
+	{
+		var textLine = null;
+		while (!inputFile.eof)
+		{
+			textLine = new TextLine();
+			textLine.text = inputFile.readln(2048);
+			if (typeof(textLine.text) == "string")
+				textLine.text = strip_ctrl(textLine.text);
+			else
+				textLine.text = "";
+			textLine.hardNewlineEnd = true;
+			// If there would still be room on the line for at least
+			// 1 more character, then add a space to the end of the
+			// line.
+			if (textLine.text.length < console.screen_columns-1)
+				textLine.text += " ";
+			gEditLines.push(textLine);
+		}
+
+		// If the last edit line is undefined (which is possible after reading the end
+		// of the quotes file), then remove it from gEditLines.
+		if (gEditLines.length > 0)
+		{
+			if (gEditLines.length > 0)
+			{
+				var lastQuoteLineIndex = gEditLines.length - 1;
+				if (gEditLines[lastQuoteLineIndex].text == undefined)
+					gEditLines.splice(lastQuoteLineIndex, 1);
+			}
+		}
+	}
+	inputFile.close();
+}
+
+// Read the message from name, to name, and subject from the drop file
+// (msginf in the node directory).
+var gMsgSubj = "";
+var gFromName = user.alias;
+var gToName = gInputFilename;
+var gMsgArea = "";
+var dropFileTime = -Infinity;
+var dropFileName = file_getcase(system.node_dir + "msginf");
+if (dropFileName != undefined)
+{
+   if (file_date(dropFileName) >= dropFileTime)
+	{
+		var dropFile = new File(dropFileName);
+		if (dropFile.exists && dropFile.open("r"))
+		{
+         dropFileTime = dropFile.date;
+			info = dropFile.readAll();
+			dropFile.close();
+
+         gFromName = info[0];
+         gToName = info[1];
+			gMsgSubj = info[2];
+			gMsgArea = info[4];
+		}
+	}
+	file_remove(dropFileName);
+}
+
+// If the subject is blank, set it to something.
+if (gMsgSubj == "")
+	gMsgSubj = gToName.replace(/^.*[\\\/]/,'');
+// Store a copy of the current subject (possibly allowing the user to
+// change the subject in the future)
+var gOldSubj = gMsgSubj;
+
+// Now it's edit time.
+var exitCode = doEditLoop();
+
+// Remove any extra blank lines that may be at the end of
+// the message (in gEditLines).
+if ((exitCode == 0) && (gEditLines.length > 0))
+{
+   var lineIndex = gEditLines.length - 1;
+   while ((lineIndex > 0) && (lineIndex < gEditLines.length) &&
+           (gEditLines[lineIndex].length() == 0))
+   {
+      gEditLines.splice(lineIndex, 1);
+      --lineIndex;
+   }
+}
+
+// If the user wrote & saved a message, then output the message
+// lines to a file with the passed-in input filename.
+var savedTheMessage = false;
+if ((exitCode == 0) && (gEditLines.length > 0))
+{
+  // Open the output filename.  If no arguments were passed, then use
+  // INPUT.MSG in the node's temporary directory; otherwise, use the
+  // first program argument.
+  var msgFile = new File((argc == 0 ? system.temp_dir + "INPUT.MSG" : argv[0]));
+  if (msgFile.open("w"))
+  {
+    // Write each line of the message to the file.  Note: The
+    // "Expand Line Feeds to CRLF" option should be turned on
+    // in SCFG for this to work properly for all platforms.
+    for (var i = 0; i < gEditLines.length; ++i)
+      msgFile.writeln(gEditLines[i].text);
+    msgFile.close();
+    savedTheMessage = true;
+  }
+  else
+    console.print("nrh* Unable to save the message!n\r\n");
+}
+
+/*
+// Note: If we were using WWIV editor.inf/result.ed drop files, we
+// could allow the user to change the subject and write the new
+// subject in result.ed..
+if (savedTheMessage)
+{
+  gMsgSubj = "New subject";
+  if (gMsgSubj != gOldSubj)
+  {
+    var dropFile = new File(system.node_dir + "result.ed");
+    if (dropFile.open("w"))
+    {
+      dropFile.writeln("0");
+      dropFile.writeln(gMsgSubj);
+      dropFile.close();
+    }
+  }
+}
+*/
+
+// Set the original ctrlkey_passthru and sys_status settins back.
+console.ctrlkey_passthru = gOldPassthru;
+bbs.sys_status = gOldStatus;
+
+// Set the end-of-program status message.
+var endStatusMessage = "";
+if (exitCode == 1)
+   endStatusMessage = "nmhMessage aborted.";
+else if (exitCode == 0)
+{
+   if (gEditLines.length > 0)
+      endStatusMessage = "nchThe message has been saved.";
+   else
+      endStatusMessage = "nmhEmpty message not sent.";
+}
+// We shouldn't hit this else case, but it's here just to be safe.
+else
+   endStatusMessage = "nmhPossible message error.";
+
+// Display the end-of-program information (if the setting is enabled) and
+// the ending program status.
+console.clear("n");
+if (gConfigSettings.displayEndInfoScreen)
+{
+   displayProgramExitInfo(false);
+   console.crlf();
+}
+console.print(endStatusMessage);
+console.crlf();
+
+// If the user's setting to pause after every screenful is disabled, then
+// pause here so that they can see the exit information.
+if (user.settings & USER_PAUSE == 0)
+   mswait(1000);
+
+// Load any specified 3rd-party exit scripts and execute any provided exit
+// JavaScript commands.
+for (var i = 0; i < gConfigSettings.thirdPartyLoadOnExit.length; ++i)
+  load(gConfigSettings.thirdPartyLoadOnExit[i]);
+for (var i = 0; i < gConfigSettings.runJSOnExit.length; ++i)
+  eval(gConfigSettings.runJSOnExit[i]);
+
+exit(exitCode);
+
+// End of script execution
+
+
+///////////////////////////////////////////////////////////////////////////////////
+// Functions
+
+// Edit mode & input loop
+function doEditLoop()
+{
+	// Return codes:
+	// 0: Success
+	// 1: Aborted
+	var returnCode = 0;
+
+   // Set the shortcut keys.
+	const ABORT_KEY             = CTRL_A;
+	const DELETE_LINE_KEY       = CTRL_D;
+	const GENERAL_HELP_KEY      = CTRL_G;
+	const TOGGLE_INSERT_KEY     = CTRL_I;
+	const CHANGE_COLOR_KEY      = CTRL_K;
+	const FIND_TEXT_KEY         = CTRL_N;
+	const IMPORT_FILE_KEY       = CTRL_O;
+	const CMDLIST_HELP_KEY      = CTRL_P;
+	const QUOTE_KEY             = CTRL_Q;
+	const PROGRAM_INFO_HELP_KEY = CTRL_R;
+	const PAGE_DOWN_KEY         = CTRL_S;
+	const PAGE_UP_KEY           = CTRL_W;
+	const EXPORT_FILE_KEY       = CTRL_X;
+	const SAVE_KEY              = CTRL_Z;
+
+   // Draw the screen.
+   // Note: This is purposefully drawing the top of the message.  We
+   // want to place the cursor at the first character on the top line,
+   // too.  This is for the case where we're editin an existing message -
+   // we want to start editigng it at the top.
+	fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+		             gInsertMode, gUseQuotes, 0, displayEditLines);
+
+	var curpos = new Object();
+	curpos.x = gEditLeft;
+	curpos.y = gEditTop;
+	console.gotoxy(curpos);
+
+	// Input loop
+	var userInput = "";
+	var currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+	var numKeysPressed = 0; // Used only to determine when to call updateTime()
+	var continueOn = true;
+	while (continueOn)
+	{
+		// Get a key, and time out after 5 minutes.
+		// Get a keypress from the user.  If the setting for using the
+		// input timeout is enabled and the user is not a sysop, then use
+		// the input timeout specified in the config file.  Otherwise,
+		// don't use a timeout.
+		if (gConfigSettings.userInputTimeout && !gIsSysop)
+			userInput = console.inkey(K_NOCRLF|K_NOSPIN, gConfigSettings.inputTimeoutMS);
+		else
+			userInput = console.getkey(K_NOCRLF|K_NOSPIN);
+		// If userInput is blank, then the input timeout was probably
+		// reached, so abort.
+		if (userInput == "")
+		{
+			returnCode = 1; // Aborted
+			continueOn = false;
+			console.crlf();
+			console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
+			continue;
+		}
+
+		// If we reach this code, the timeout wasn't reached.
+		++numKeysPressed;
+
+		// If gEditLines currently has 1 less line than we need,
+		// then add a new line to gEditLines.
+		if (gEditLines.length == gEditLinesIndex)
+			gEditLines.push(new TextLine());
+
+		// Take the appropriate action for the key pressed.
+		switch (userInput)
+		{
+			case ABORT_KEY:
+            // Before aborting, ask they user if they really want to abort.
+            if (promptYesNo("Abort message", false, "Abort"))
+            {
+               returnCode = 1; // Aborted
+               continueOn = false;
+            }
+            else
+            {
+               // Make sure the edit color attribute is set.
+               //console.print("n" + gTextAttrs);
+               console.print(chooseEditColor());
+            }
+				break;
+			case SAVE_KEY:
+				returnCode = 0; // Save
+				continueOn = false;
+				break;
+			case CMDLIST_HELP_KEY:
+				displayCommandList(true, true, true, gIsSysop);
+				clearEditAreaBuffer();
+            fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+		                     gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+		                     displayEditLines);
+				break;
+			case GENERAL_HELP_KEY:
+				displayGeneralHelp(true, true, true);
+				clearEditAreaBuffer();
+				fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+				               gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+				               displayEditLines);
+				break;
+			case PROGRAM_INFO_HELP_KEY:
+				displayProgramInfo(true, true, true);
+				clearEditAreaBuffer();
+				fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+				               gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+				               displayEditLines);
+            break;
+			case QUOTE_KEY:
+            // Let the user choose & insert quote lines into the message.
+            if (gUseQuotes)
+            {
+               var retObject = doQuoteSelection(curpos, currentWordLength);
+               curpos.x = retObject.x;
+               curpos.y = retObject.y;
+               currentWordLength = retObject.currentWordLength;
+               // If user input timed out, then abort.
+               if (retObject.timedOut)
+               {
+                  returnCode = 1; // Aborted
+                  continueOn = false;
+                  console.crlf();
+                  console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
+                  continue;
+               }
+            }
+            break;
+         case CHANGE_COLOR_KEY:
+				    /*
+            // Let the user change the text color.
+            if (gConfigSettings.allowColorSelection)
+            {
+               var retObject = doColorSelection(curpos, currentWordLength);
+               curpos.x = retObject.x;
+               curpos.y = retObject.y;
+               currentWordLength = retObject.currentWordLength;
+               // If user input timed out, then abort.
+               if (retObject.timedOut)
+               {
+                  returnCode = 1; // Aborted
+                  continueOn = false;
+                  console.crlf();
+                  console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
+                  continue;
+               }
+            }
+            */
+            break;
+			case KEY_UP:
+				// Move the cursor up one line.
+				if (gEditLinesIndex > 0)
+				{
+					--gEditLinesIndex;
+
+					// gTextLineIndex should containg the index in the text
+					// line where the cursor would add text.  If the previous
+					// line is shorter than the one we just left, then
+					// gTextLineIndex and curpos.x need to be adjusted.
+					if (gTextLineIndex > gEditLines[gEditLinesIndex].length())
+					{
+						gTextLineIndex = gEditLines[gEditLinesIndex].length();
+						curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
+					}
+					// Figure out the vertical coordinate of where the
+					// cursor should be.
+					// If the cursor is at the top of the edit area,
+					// then scroll up through the message by 1 line.
+					if (curpos.y == gEditTop)
+						displayEditLines(gEditTop, gEditLinesIndex, gEditBottom, true, /*true*/false);
+					else
+						--curpos.y;
+
+					console.gotoxy(curpos);
+					currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+					console.print(chooseEditColor()); // Make sure the edit color is correct
+				}
+				break;
+			case KEY_DOWN:
+				// Move the cursor down one line.
+				if (gEditLinesIndex < gEditLines.length-1)
+				{
+					++gEditLinesIndex;
+					// gTextLineIndex should containg the index in the text
+					// line where the cursor would add text.  If the next
+					// line is shorter than the one we just left, then
+					// gTextLineIndex and curpos.x need to be adjusted.
+					if (gTextLineIndex > gEditLines[gEditLinesIndex].length())
+					{
+						gTextLineIndex = gEditLines[gEditLinesIndex].length();
+						curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
+					}
+					// Figure out the vertical coordinate of where the
+					// cursor should be.
+					// If the cursor is at the bottom of the edit area,
+					// then scroll down through the message by 1 line.
+					if (curpos.y == gEditBottom)
+					{
+						displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+						                 gEditBottom, true, /*true*/false);
+					}
+					else
+						++curpos.y;
+
+					console.gotoxy(curpos);
+					currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+					console.print(chooseEditColor()); // Make sure the edit color is correct
+				}
+				break;
+			case KEY_LEFT:
+				// If the horizontal cursor position is right of the
+				// leftmost edit position, then let it move left.
+				if (curpos.x > gEditLeft)
+				{
+					--curpos.x;
+					console.gotoxy(curpos);
+					if (gTextLineIndex > 0)
+						--gTextLineIndex;
+				}
+				else
+				{
+					// The cursor is at the leftmost position in the
+					// edit area.  If there are text lines above the
+					// current line, then move the cursor to the end
+					// of the previous line.
+					if (gEditLinesIndex > 0)
+					{
+						--gEditLinesIndex;
+						curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
+						// Move the cursor up or scroll up by one line
+						if (curpos.y > 1)
+							--curpos.y;
+						else
+							displayEditLines(gEditTop, gEditLinesIndex, gEditBottom, true, /*true*/false);
+						gTextLineIndex = gEditLines[gEditLinesIndex].length();
+						console.gotoxy(curpos);
+					}
+				}
+				console.print(chooseEditColor());
+
+				// Update the current word length.
+				currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+				// Make sure the edit color is correct
+				console.print(chooseEditColor());
+				break;
+			case KEY_RIGHT:
+				// If the horizontal cursor position is left of the
+				// rightmost edit position, then the cursor can move
+				// to the right.
+				if (curpos.x < gEditRight)
+				{
+					// The current line index must be within bounds
+					// before we can move the cursor to the right.
+					if (gTextLineIndex < gEditLines[gEditLinesIndex].length())
+					{
+						++curpos.x;
+						console.gotoxy(curpos);
+						++gTextLineIndex;
+					}
+					else
+					{
+						// The cursor is at the rightmost position on the
+						// line.  If there are text lines below the current
+						// line, then move the cursor to the start of the
+						// next line.
+						if (gEditLinesIndex < gEditLines.length-1)
+						{
+							++gEditLinesIndex;
+							curpos.x = gEditLeft;
+							// Move the cursor down or scroll down by one line
+							if (curpos.y < gEditBottom)
+								++curpos.y;
+							else
+								displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+								                 gEditBottom, true, /*true*/false);
+							gTextLineIndex = 0;
+							console.gotoxy(curpos);
+						}
+					}
+				}
+				else
+				{
+					// The cursor is at the rightmost position in the
+					// edit area.  If there are text lines below the
+					// current line, then move the cursor to the start
+					// of the next line.
+					if (gEditLinesIndex < gEditLines.length-1)
+					{
+						++gEditLinesIndex;
+						curpos.x = gEditLeft;
+						// Move the cursor down or scroll down by one line
+						if (curpos.y < gEditBottom)
+							++curpos.y;
+						else
+							displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+							                 gEditBottom, true, false);
+						gTextLineIndex = 0;
+						console.gotoxy(curpos);
+					}
+				}
+				console.print(chooseEditColor());
+
+				// Update the current word length.
+				currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+				// Make sure the edit color is correct
+				console.print(chooseEditColor());
+				break;
+			case KEY_HOME:
+				// Go to the beginning of the line
+				gTextLineIndex = 0;
+				curpos.x = gEditLeft;
+				console.gotoxy(curpos);
+				// Update the current word length.
+            currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+				break;
+			case KEY_END:
+				// Go to the end of the line
+				if (gEditLinesIndex < gEditLines.length)
+				{
+					gTextLineIndex = gEditLines[gEditLinesIndex].length();
+					curpos.x = gEditLeft + gTextLineIndex;
+					// If the cursor position would be to the right of the edit
+					// area, then place it at gEditRight.
+					if (curpos.x > gEditRight)
+					{
+                  var difference = curpos.x - gEditRight;
+                  curpos.x -= difference;
+                  gTextLineIndex -= difference;
+					}
+					// Place the cursor where it should be.
+					console.gotoxy(curpos);
+
+					// Update the current word length.
+               currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+				}
+				break;
+			case BACKSPACE:
+				// Delete the previous character
+				var retObject = doBackspace(curpos, currentWordLength);
+				curpos.x = retObject.x;
+				curpos.y = retObject.y;
+				currentWordLength = retObject.currentWordLength;
+				// Make sure the edit color is correct
+				console.print(chooseEditColor());
+				break;
+			case KEY_DEL:
+				// Delete the next character
+				var retObject = doDeleteKey(curpos, currentWordLength);
+				curpos.x = retObject.x;
+				curpos.y = retObject.y;
+				currentWordLength = retObject.currentWordLength;
+				// Make sure the edit color is correct
+				console.print(chooseEditColor());
+				break;
+			case KEY_ENTER:
+				var retObject = doEnterKey(curpos, currentWordLength);
+				curpos.x = retObject.x;
+				curpos.y = retObject.y;
+				currentWordLength = retObject.currentWordLength;
+				returnCode = retObject.returnCode;
+            continueOn = retObject.continueOn;
+            // Check for whether we should do quote selection or
+            // show the help screen (if the user entered /Q or /?)
+            if (continueOn)
+            {
+               if (retObject.doQuoteSelection)
+               {
+                  if (gUseQuotes)
+                  {
+                     retObject = doQuoteSelection(curpos, currentWordLength);
+                     curpos.x = retObject.x;
+                     curpos.y = retObject.y;
+                     currentWordLength = retObject.currentWordLength;
+                     // If user input timed out, then abort.
+                     if (retObject.timedOut)
+                     {
+                        returnCode = 1; // Aborted
+                        continueOn = false;
+                        console.crlf();
+                        console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
+                        continue;
+                     }
+                  }
+               }
+               else if (retObject.showHelp)
+               {
+                  displayProgramInfo(true, true, false);
+                  displayCommandList(false, false, true, gIsSysop);
+                  clearEditAreaBuffer();
+                  fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+                                 gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+                                 displayEditLines);
+                  console.gotoxy(curpos);
+               }
+            }
+            // Make sure the edit color is correct
+				console.print(chooseEditColor());
+				break;
+         // Insert/overwrite mode toggle
+         case KEY_INSERT:
+         case TOGGLE_INSERT_KEY:
+            toggleInsertMode(null);
+            //console.print("n" + gTextAttrs);
+            console.print(chooseEditColor());
+            console.gotoxy(curpos);
+            break;
+			case KEY_ESC:
+            // Do the ESC menu
+            var retObj = fpHandleESCMenu(curpos, currentWordLength);
+            returnCode = retObj.returnCode;
+            continueOn = retObj.continueOn;
+            curpos.x = retObj.x;
+            curpos.y = retObj.y;
+            currentWordLength = retObj.currentWordLength;
+            // If we can continue on, put the cursor back
+            // where it should be.
+            if (continueOn)
+            {
+               //console.print("n" + gTextAttrs);
+               console.print(chooseEditColor());
+               console.gotoxy(curpos);
+            }
+				break;
+         case FIND_TEXT_KEY:
+            var retObj = findText(curpos);
+            curpos.x = retObj.x;
+            curpos.y = retObj.y;
+				console.print(chooseEditColor()); // Make sure the edit color is correct
+            break;
+         case IMPORT_FILE_KEY:
+            // Only let sysops import files.
+            if (gIsSysop)
+            {
+               var retObj = importFile(gIsSysop, curpos);
+               curpos.x = retObj.x;
+               curpos.y = retObj.y;
+               currentWordLength = retObj.currentWordLength;
+               console.print(chooseEditColor()); // Make sure the edit color is correct
+            }
+            break;
+         case EXPORT_FILE_KEY:
+            // Only let sysops export files.
+            if (gIsSysop)
+            {
+               exportToFile(gIsSysop);
+               console.gotoxy(curpos);
+            }
+            break;
+         case DELETE_LINE_KEY:
+            var retObj = doDeleteLine(curpos);
+            curpos.x = retObj.x;
+            curpos.y = retObj.y;
+            currentWordLength = retObj.currentWordLength;
+            console.print(chooseEditColor()); // Make sure the edit color is correct
+            break;
+         case PAGE_UP_KEY: // Move 1 page up in the message
+            // Calculate the index of the message line shown at the top
+            // of the edit area.
+            var topEditIndex = gEditLinesIndex-(curpos.y-gEditTop);
+            // If topEditIndex is > 0, then we can page up.
+            if (topEditIndex > 0)
+            {
+               // Calculate the new top edit line index.
+               // If there is a screenful or more of lines above the top,
+               // then set topEditIndex to what it would need to be for the
+               // previous page.  Otherwise, set topEditIndex to 0.
+               if (topEditIndex >= gEditHeight)
+                  topEditIndex -= gEditHeight;
+               else
+                  topEditIndex = 0;
+               // Refresh the edit area
+               displayEditLines(gEditTop, topEditIndex, gEditBottom, true, /*true*/false);
+               // Set the cursor to the last place on the last line.
+               gEditLinesIndex = topEditIndex + gEditHeight - 1;
+               gTextLineIndex = gEditLines[gEditLinesIndex].length();
+               if ((gTextLineIndex > 0) && (gEditLines[gEditLinesIndex].length == gEditWidth))
+                  --gTextLineIndex;
+               curpos.x = gEditLeft + gTextLineIndex;
+               curpos.y = gEditBottom;
+               console.gotoxy(curpos);
+
+               // Update the current word length.
+               currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+            }
+            else
+            {
+               // topEditIndex is 0.  If gEditLinesIndex is not already 0,
+               // then make it 0 and place the cursor at the first line.
+               if (gEditLinesIndex > 0)
+               {
+                  gEditLinesIndex = 0;
+                  gTextLineIndex = 0;
+                  curpos.x = gEditLeft;
+                  curpos.y = gEditTop;
+                  console.gotoxy(curpos);
+
+                  // Update the current word length.
+                  currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+               }
+            }
+            console.print(chooseEditColor()); // Make sure the edit color is correct
+            break;
+         case PAGE_DOWN_KEY: // Move 1 page down in the message
+            // Calculate the index of the message line shown at the top
+            // of the edit area, and the index of the line that would be
+            // shown at the bottom of the edit area.
+            var topEditIndex = gEditLinesIndex-(curpos.y-gEditTop);
+            var bottomEditIndex = topEditIndex + gEditHeight - 1;
+            // If bottomEditIndex is less than the last index, then we can
+            // page down.
+            var lastEditLineIndex = gEditLines.length-1;
+            if (bottomEditIndex < lastEditLineIndex)
+            {
+               // Calculate the new top edit line index.
+               // If there is a screenful or more of lines below the bottom,
+               // then set topEditIndex to what it would need to be for the
+               // next page.  Otherwise, set topEditIndex to the right
+               // index to display the last full page.
+               if (gEditLines.length - gEditHeight > bottomEditIndex)
+                  topEditIndex += gEditHeight;
+               else
+                  topEditIndex = gEditLines.length - gEditHeight;
+               // Refresh the edit area
+               displayEditLines(gEditTop, topEditIndex, gEditBottom, true, /*true*/false);
+               // Set the cursor to the first place on the first line.
+               gEditLinesIndex = topEditIndex;
+               gTextLineIndex = 0;
+               curpos.x = gEditLeft;
+               curpos.y = gEditTop;
+               console.gotoxy(curpos);
+
+               // Update the current word length.
+               currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+            }
+            else
+            {
+               // bottomEditIndex >= the last edit line index.
+               // If gEditLinesIndex is not already equal to bottomEditIndex,
+               // make it so and put the cursor at the end of the last line.
+               if (gEditLinesIndex < bottomEditIndex)
+               {
+                  var oldEditLinesIndex = gEditLinesIndex;
+
+                  // Make sure gEditLinesIndex is valid.  It should be set to the
+                  // last edit line index.  It's possible that bottomEditIndex is
+                  // beyond the last edit line index, so we need to be careful here.
+                  if (bottomEditIndex == lastEditLineIndex)
+                     gEditLinesIndex = bottomEditIndex;
+                  else
+                     gEditLinesIndex = lastEditLineIndex;
+                  gTextLineIndex = gEditLines[gEditLinesIndex].length();
+                  if ((gTextLineIndex > 0) && (gEditLines[gEditLinesIndex].length == gEditWidth))
+                     --gTextLineIndex;
+                  curpos.x = gEditLeft + gTextLineIndex;
+                  curpos.y += (gEditLinesIndex-oldEditLinesIndex);
+                  console.gotoxy(curpos);
+
+                  // Update the current word length.
+                  currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+               }
+            }
+            console.print(chooseEditColor()); // Make sure the edit color is correct
+            break;
+			default:
+            // For the tab character, insert 3 spaces.  Otherwise,
+            // if it's a printabel character, add the character.
+				if (/\t/.test(userInput))
+				{
+               var retObject;
+               for (var i = 0; i < 3; ++i)
+               {
+                  retObject = doPrintableChar(" ", curpos, currentWordLength);
+                  curpos.x = retObject.x;
+                  curpos.y = retObject.y;
+                  currentWordLength = retObject.currentWordLength;
+               }
+				}
+				else
+				{
+               if (isPrintableChar(userInput))
+               {
+                  var retObject = doPrintableChar(userInput, curpos, currentWordLength);
+                  curpos.x = retObject.x;
+                  curpos.y = retObject.y;
+                  currentWordLength = retObject.currentWordLength;
+               }
+            }
+				break;
+		}
+
+		// For every 5 keys pressed, dheck the current time and update
+		// it on the screen if necessary.
+		if (numKeysPressed % 5 == 0)
+			updateTime();
+	}
+
+   // If gEditLines has only 1 line in it and it's blank, then
+   // remove it so that we can test to see if the message is empty.
+   if (gEditLines.length == 1)
+   {
+      if (gEditLines[0].length() == 0)
+         gEditLines.splice(0, 1);
+   }
+
+	return returnCode;
+}
+// Helper function for doEditLoop(): Handles the backspace behavior.
+//
+// Parameters:
+//  pCurpos: An object containing x and y values representing the
+//           cursor position.
+//  pCurrentWordLength: The length of the current word that has been typed
+//
+// Return value: An object containing x and y values representing the cursor
+//               position and currentLength, the current word length.
+function doBackspace(pCurpos, pCurrentWordLength)
+{
+   // Create the return object.
+	var retObj = new Object();
+	retObj.x = pCurpos.x;
+	retObj.y = pCurpos.y;
+	retObj.currentWordLength = pCurrentWordLength;
+
+	var didBackspace = false;
+	// For later, store a backup of the current edit line index and
+	// cursor position.
+	var originalCurrentLineIndex = gEditLinesIndex;
+	var originalX = pCurpos.x;
+	var originalY = pCurpos.y;
+	var originallyOnLastLine = (gEditLinesIndex == gEditLines.length-1);
+
+	// If the cursor is beyond the leftmost position in
+	// the edit area, then we can simply remove the last
+	// character in the current line and move the cursor
+	// over to the left.
+	if (retObj.x > gEditLeft)
+	{
+		if (gTextLineIndex > 0)
+		{
+			console.print(BACKSPACE);
+			console.print(" ");
+			--retObj.x;
+			console.gotoxy(retObj.x, retObj.y);
+
+			// Remove the previous character from the text line
+			var textLineLength = gEditLines[gEditLinesIndex].length();
+			if (textLineLength > 0)
+			{
+				var textLine = gEditLines[gEditLinesIndex].text.substr(0, gTextLineIndex-1)
+				             + gEditLines[gEditLinesIndex].text.substr(gTextLineIndex);
+				gEditLines[gEditLinesIndex].text = textLine;
+				didBackspace = true;
+				--gTextLineIndex;
+			}
+		}
+	}
+	else
+	{
+		// The cursor is at the leftmost position in the edit area.
+		// If we are beyond the first text line, then move as much of
+		// the current text line as possible up to the previous line,
+		// if there's room (if not, don't do anything).
+		if (gEditLinesIndex > 0)
+		{
+         var prevLineIndex = gEditLinesIndex - 1;
+         if (gEditLines[gEditLinesIndex].length() > 0)
+         {
+            // Store the previous line's original length
+            var originalPrevLineLen = gEditLines[prevLineIndex].length();
+
+            // See how much space is at the end of the previous line
+            var previousLineEndSpace = gEditWidth - gEditLines[prevLineIndex].length();
+            if (previousLineEndSpace > 0)
+            {
+               var index = previousLineEndSpace - 1;
+               // If that index is valid for the current line, then find the first
+               // space in the current line so that the text would fit at the end
+               // of the previous line.  Otherwise, set index to the length of the
+               // current line so that we'll move the whole current line up to the
+               // previous line.
+               if (index < gEditLines[gEditLinesIndex].length())
+               {
+                  for (; index >= 0; --index)
+                  {
+                     if (gEditLines[gEditLinesIndex].text.charAt(index) == " ")
+                        break;
+                  }
+               }
+               else
+                  index = gEditLines[gEditLinesIndex].length();
+               // If we found a space, then move the part of the current line before
+               // the space to the end of the previous line.
+               if (index > 0)
+               {
+                  var linePart = gEditLines[gEditLinesIndex].text.substr(0, index);
+                  gEditLines[gEditLinesIndex].text = gEditLines[gEditLinesIndex].text.substr(index);
+                  gEditLines[prevLineIndex].text += linePart;
+                  gEditLines[prevLineIndex].hardNewlineEnd = gEditLines[gEditLinesIndex].hardNewlineEnd;
+
+                  // If the current line is now blank, then remove it from gEditLines.
+                  if (gEditLines[gEditLinesIndex].length() == 0)
+                     gEditLines.splice(gEditLinesIndex, 1);
+
+                  // Update the global edit variables so that the cursor is placed
+                  // on the previous line.
+                  --gEditLinesIndex;
+                  // Search for linePart in the line - If found, the cursor should
+                  // be placed where it starts.  If it' snot found, place the cursor
+                  // at the end of the line.
+                  var linePartIndex = gEditLines[gEditLinesIndex].text.indexOf(linePart);
+                  if (linePartIndex > -1)
+                     gTextLineIndex = linePartIndex;
+                  else
+                     gTextLineIndex = gEditLines[gEditLinesIndex].length();
+
+                  retObj.x = gEditLeft + gTextLineIndex;
+                  if (retObj.y > gEditTop)
+                     --retObj.y;
+
+                  didBackspace = true;
+               }
+            }
+         }
+         else
+         {
+            // The current line's length is 0.
+            // If there's enough room on the previous line, remove the
+            // current line and place the cursor at the end of the
+            // previous line.
+            if (gEditLines[prevLineIndex].length() <= gEditWidth-1)
+            {
+               gEditLines.splice(gEditLinesIndex, 1);
+
+               --gEditLinesIndex;
+               gTextLineIndex = gEditLines[prevLineIndex].length();
+               retObj.x = gEditLeft + gEditLines[prevLineIndex].length();
+               if (retObj.y > gEditTop)
+                  --retObj.y;
+
+               didBackspace = true;
+            }
+         }
+		}
+	}
+
+	// If the backspace was performed, then re-adjust the text lines
+	// and refresh the screen.
+	if (didBackspace)
+	{
+      // Store the previous line of text now so we can compare it later
+      var prevTextline = "";
+      if (gEditLinesIndex > 0)
+         prevTextline = gEditLines[gEditLinesIndex-1].text;
+
+      // Re-adjust the text lines
+		reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth);
+
+      // If the previous line's length increased, that probably means that the
+      // user backspaced to the beginning of the current line and the word was
+      // moved to the end of the previous line.  If so, then move the cursor to
+      // the end of the previous line.
+      //var scrolled = false;
+      if ((gEditLinesIndex > 0) &&
+          (gEditLines[gEditLinesIndex-1].length() > prevTextline.length))
+      {
+         // Update the text index variables and cusor position variables.
+         --gEditLinesIndex;
+         gTextLineIndex = gEditLines[gEditLinesIndex].length();
+         retObj.x = gEditLeft + gTextLineIndex;
+         if (retObj.y > gEditTop)
+            --retObj.y;
+      }
+
+      // If the cursor was at the leftmost position in the edit area,
+      // update the edit lines from the currently-set screen line #.
+      if (originalX == gEditLeft)
+      {
+         // Since the original X position was at the left edge of the edit area,
+         // display the edit lines starting with the previous line if possible.
+         if ((gEditLinesIndex > 0) && (retObj.y > gEditTop))
+            displayEditLines(retObj.y-1, gEditLinesIndex-1, gEditBottom, true, true);
+         else
+            displayEditLines(retObj.y, gEditLinesIndex, gEditBottom, true, true);
+      }
+      // If the original horizontal cursor position was in the middle of
+      // the line, and the line is the last line on the screen, then
+      // only refresh that one line on the screen.
+      else if ((originalX > gEditLeft) && (originalX < gEditLeft + gEditWidth - 1) && originallyOnLastLine)
+         displayEditLines(originalY, originalCurrentLineIndex, originalY, false);
+		// If scrolling was to be done, then refresh the entire
+		// current message text on the screen from the top of the
+		// edit area.  Otherwise, only refresh starting from the
+		// original horizontal position and message line.
+      else
+      {
+         // Display the edit lines starting with the previous line if possible.
+         if ((gEditLinesIndex > 0) && (retObj.y > gEditTop))
+            displayEditLines(retObj.y-1, gEditLinesIndex-1, gEditBottom, true, true);
+         else
+            displayEditLines(retObj.y, gEditLinesIndex, gEditBottom, true, true);
+      }
+
+      // Make sure the current word length is correct.
+      retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+	}
+
+   // Make sure the cursor is placed where it should be.
+   console.gotoxy(retObj.x, retObj.y);
+	return retObj;
+}
+
+// Helper function for doEditLoop(): Handles the delete key behavior.
+//
+// Parameters:
+//  pCurpos: An object containing x and y values representing the
+//           cursor position.
+//  pCurrentWordLength: The length of the current word that has been typed
+//
+// Return value: An object containing x and y values representing the cursor
+//               position and currentLength, the current word length.
+function doDeleteKey(pCurpos, pCurrentWordLength)
+{
+   // Create the return object
+  var returnObject = new Object();
+	returnObject.x = pCurpos.x;
+	returnObject.y = pCurpos.y;
+	returnObject.currentWordLength = pCurrentWordLength;
+
+  // Store the original line text (for testing to see if we should update the screen).
+  var originalLineText = gEditLines[gEditLinesIndex].text;
+
+  // If gEditLinesIndex is invalid, then return without doing anything.
+  if ((gEditLinesIndex < 0) || (gEditLinesIndex >= gEditLines.length))
+     return returnObject;
+
+  // If the text line index is within bounds, then we can
+  // delete the next character and refresh the screen.
+  if (gTextLineIndex < gEditLines[gEditLinesIndex].length())
+  {
+     var lineText = gEditLines[gEditLinesIndex].text.substr(0, gTextLineIndex)
+                   + gEditLines[gEditLinesIndex].text.substr(gTextLineIndex+1);
+     gEditLines[gEditLinesIndex].text = lineText;
+     // If the current character is a space, then reset the current word length.
+     // to 0.  Otherwise, set it to the current word length.
+     if (gTextLineIndex < gEditLines[gEditLinesIndex].length())
+     {
+        if (gEditLines[gEditLinesIndex].text.charAt(gTextLineIndex) == " ")
+           returnObject.currentWordLength = 0;
+        else
+        {
+           var spacePos = gEditLines[gEditLinesIndex].text.indexOf(" ", gTextLineIndex);
+           if (spacePos > -1)
+              returnObject.currentWordLength = spacePos - gTextLineIndex;
+           else
+              returnObject.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+        }
+     }
+
+     // Re-adjust the line lengths and refresh the edit area.
+     var textChanged = reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length,
+                                         gEditWidth);
+
+     // If the line text changed, then update the message area from the
+     // current line on down.
+     textChanged = textChanged || (gEditLines[gEditLinesIndex].text != originalLineText);
+     if (textChanged)
+     {
+        // Calculate the bottommost edit area row to update, and then
+        // refresh the edit area.
+        var bottommostRow = calcBottomUpdateRow(returnObject.y, gEditLinesIndex);
+        displayEditLines(returnObject.y, gEditLinesIndex, bottommostRow, true, true);
+     }
+  }
+  else
+  {
+     // The textChanged variable will be used by this code to store whether or
+     // not any text changed so we'll know if the screen needs to be refreshed.
+     var textChanged = false;
+
+     // The text line index is at the end of the line.
+     // Set the current line's hardNewlineEnd property to false
+     // so that we can bring up text from the next line,
+     // if possible.
+     gEditLines[gEditLinesIndex].hardNewlineEnd = false;
+     // Also, temporarily set the line's isQuoteLine property to false
+     // so that text from the next line can be brought up.  Store the
+     // current isQuoteLine value so it can be restored later.
+     var lineIsQuoteLine = gEditLines[gEditLinesIndex].isQuoteLine;
+     gEditLines[gEditLinesIndex].isQuoteLine = false;
+
+     // If the current line is blank and is not the last line, then remove it.
+     if (gEditLines[gEditLinesIndex].length() == 0)
+     {
+        if (gEditLinesIndex < gEditLines.length-1)
+        {
+           gEditLines.splice(gEditLinesIndex, 1);
+           textChanged = true;
+        }
+     }
+     // If the next line is blank, then set its
+     // hardNewlineEnd to false too, so that lower
+     // text lines can be brought up.
+     else if (gEditLinesIndex < gEditLines.length-1)
+     {
+        var nextLineIndex = gEditLinesIndex + 1;
+        if (gEditLines[nextLineIndex].length() == 0)
+           gEditLines[nextLineIndex].hardNewlineEnd = false;
+     }
+
+     // Re-adjust the text lines, update textChanged, restore the line's
+     // isQuoteLine property, and set a few other things.
+     textChanged = textChanged || reAdjustTextLines(gEditLines, gEditLinesIndex,
+                                                    gEditLines.length, gEditWidth);
+     gEditLines[gEditLinesIndex].isQuoteLine = lineIsQuoteLine;
+     returnObject.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+     var startRow = returnObject.y;
+     var startEditLinesIndex = gEditLinesIndex;
+     if (returnObject.y > gEditTop)
+     {
+       --startRow;
+       --startEditLinesIndex;
+     }
+
+     // If text changed, then refresh the edit area.
+     textChanged = textChanged || (gEditLines[gEditLinesIndex].text != originalLineText);
+     if (textChanged)
+     {
+        // Calculate the bottommost edit area row to update, and then
+        // refresh the edit area.
+        var bottommostRow = calcBottomUpdateRow(startRow, startEditLinesIndex);
+        displayEditLines(startRow, startEditLinesIndex, bottommostRow, true, true);
+     }
+  }
+
+  // Move the cursor where it should be.
+  console.gotoxy(returnObject.x, returnObject.y);
+
+  return returnObject;
+}
+
+// Helper function for doEditLoop(): Handles printable characters.
+//
+// Parameters:
+//  pUserInput: The user's input
+//  pCurpos: An object containing x and y values representing the
+//           cursor position.
+//  pCurrentWordLength: The length of the current word that has been typed
+//
+// Return value: An object containing the following properties:
+//               x: The horizontal component of the cursor position
+//               y: The vertical component of the cursor position
+//               currentLength: The length of the current word
+function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
+{
+   // Create the return object.
+   var retObj = new Object();
+   retObj.x = pCurpos.x;
+	retObj.y = pCurpos.y;
+	retObj.currentWordLength = pCurrentWordLength;
+
+   // Note: gTextLineIndex is where the new character will appear in the line.
+   // If gTextLineIndex is somehow past the end of the current line, then
+   // fill it with spaces up to gTextLineIndex.
+   if (gTextLineIndex > gEditLines[gEditLinesIndex].length())
+   {
+      var numSpaces = gTextLineIndex - gEditLines[gEditLinesIndex].length();
+      if (numSpaces > 0)
+         gEditLines[gEditLinesIndex].text += format("%" + numSpaces + "s", "");
+      gEditLines[gEditLinesIndex].text += pUserInput;
+   }
+   // If gTextLineIndex is at the end of the line, then just append the char.
+   else if (gTextLineIndex == gEditLines[gEditLinesIndex].length())
+      gEditLines[gEditLinesIndex].text += pUserInput;
+   else
+   {
+      // gTextLineIndex is at the beginning or in the middle of the line.
+      if (inInsertMode())
+      {
+         gEditLines[gEditLinesIndex].text = spliceIntoStr(gEditLines[gEditLinesIndex].text,
+                                                          gTextLineIndex, pUserInput);
+      }
+      else
+      {
+         gEditLines[gEditLinesIndex].text = gEditLines[gEditLinesIndex].text.substr(0, gTextLineIndex)
+                                          + pUserInput + gEditLines[gEditLinesIndex].text.substr(gTextLineIndex+1);
+      }
+   }
+
+   // Store a copy of the current line so that we can compare it later to see
+   // if it was modified by reAdjustTextLines().
+   var originalAfterCharApplied = gEditLines[gEditLinesIndex].text;
+
+   // If the line is now too long to fit in the edit area, then we will have
+   // to re-adjust the text lines.
+   var reAdjusted = false;
+   if (gEditLines[gEditLinesIndex].length() >= gEditWidth)
+      reAdjusted = reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth);
+
+   // placeCursorAtEnd specifies whether or not to place the cursor at its
+   // spot using console.gotoxy() at the end.  This is an optimization.
+   var placeCursorAtEnd = true;
+
+   // If the current text line is now different (modified by reAdjustTextLines()),
+   // then we'll need to refresh multiple lines on the screen.
+   if (reAdjusted && (gEditLines[gEditLinesIndex].text != originalAfterCharApplied))
+   {
+      // TODO: In case the user entered a whole line of text without any spaces,
+      // the new character would appear on the next line, so we need to figure
+      // out where the cursor location should be.
+
+      // If gTextLineIndex is >= gEditLines[gEditLinesIndex].length(), then
+      // we know the current word was wrapped to the next line.  Figure out what
+      // retObj.x, retObj.currentWordLength, gEditLinesIndex, and gTextLineIndex
+      // should be, and increment retObj.y.  Also figure out what lines on the
+      // screen to update, and deal with scrolling if necessary.
+      if (gTextLineIndex >= gEditLines[gEditLinesIndex].length())
+      {
+         // TODO: I changed this on 2010-02-14 to (hopefully) place the cursor
+         // where it should be 
+         // Old line (prior to 2010-02-14):
+         //var numChars = gTextLineIndex - gEditLines[gEditLinesIndex].length();
+         // New (2010-02-14):
+         var numChars = 0;
+         // Special case: If the current line's length is exactly the longest
+         // edit with, then the # of chars should be 0 or 1, depending on whether the
+         // entered character was a space or not.  Otherwise, calculate numChars
+         // normally.
+         if (gEditLines[gEditLinesIndex].length() == gEditWidth-1)
+            numChars = ((pUserInput == " ") ? 0 : 1);
+         else
+            numChars = gTextLineIndex - gEditLines[gEditLinesIndex].length();
+         retObj.x = gEditLeft + numChars;
+         var originalEditLinesIndex = gEditLinesIndex++;
+         gTextLineIndex = numChars;
+         // The following line is now done at the end:
+         //retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+
+         // Figure out which lines we need to update on the screen and whether
+         // to do scrolling and what retObj.y should be.
+         if (retObj.y < gEditBottom)
+         {
+            // We're above the last line on the screen, so we can go one
+            // line down.
+            var originalY = retObj.y++;
+            // Update the lines on the screen.
+            var bottommostRow = calcBottomUpdateRow(originalY, originalEditLinesIndex);
+            displayEditLines(originalY, originalEditLinesIndex, bottommostRow, true, true);
+         }
+         else
+         {
+            // We're on the last line in the edit area, so we need to scroll
+            // the text lines up on the screen.
+            var editLinesTopIndex = gEditLinesIndex - (pCurpos.y - gEditTop);
+            displayEditLines(gEditTop, editLinesTopIndex, gEditBottom, true, true);
+         }
+      }
+      else
+      {
+         // gTextLineIndex is < the line's length.  Update the lines on the
+         // screen from the current line down.  Increment retObj.x,
+         // retObj.currentWordLength, and gTextLineIndex.
+         var bottommostRow = calcBottomUpdateRow(retObj.y, gEditLinesIndex);
+         displayEditLines(retObj.y, gEditLinesIndex, bottommostRow, true, true);
+         if (pUserInput == " ")
+            retObj.currentWordLength = 0;
+         else
+            ++retObj.currentWordLength;
+         ++retObj.x;
+         ++gTextLineIndex;
+      }
+   }
+   else
+   {
+      // The text line wasn't changed by reAdjustTextLines.
+
+      // If gTextLineIndex is not the last index of the line, then refresh the
+      // entire line on the screen.  Otherwise, just output the character that
+      // the user typed.
+      if (gTextLineIndex < gEditLines[gEditLinesIndex].length()-1)
+         displayEditLines(retObj.y, gEditLinesIndex, retObj.y, false, true);
+      else
+      {
+         console.print(pUserInput);
+         placeCursorAtEnd = false; // Since we just output the character
+      }
+
+      // Keep housekeeping variables up to date.
+      ++retObj.x;
+      ++gTextLineIndex;
+      /* retObj.currentWordLength is now calculated at the end, but we could do this:
+      if (pUserInput == " ")
+         retObj.currentWordLength = 0;
+      else
+         ++retObj.currentWordLength;
+      */
+   }
+
+   // Make sure the current word length is correct.
+   retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+
+   // Make sure the cursor is placed where it should be.
+   if (placeCursorAtEnd)
+      console.gotoxy(retObj.x, retObj.y);
+
+   return retObj;
+}
+
+// Helper function for doEditLoop(): Performs the action for when the user
+// presses the enter key.
+//
+// Parameters:
+//  pCurpos: An object containing x and y values representing the
+//           cursor position.
+//  pCurrentWordLength: The length of the current word that has been typed
+//
+// Return value: An object containing the following values:
+//               x: The horizontal component of the cursor position
+//               y: The vertical component of the cursor position
+//               currentWordLength: The current word length
+//               returnCode: The return code for the program (in case the
+//                           user saves or aborts)
+//               continueOn: Whether or not the edit loop should continue
+//               doQuoteSelection: Whether or not the user typed the command
+//                                 to do quote selection.
+//               showHelp: Whether or not the user wants to show the help screen
+function doEnterKey(pCurpos, pCurrentWordLength)
+{
+   // Create the return object
+   var retObj = new Object();
+	retObj.x = pCurpos.x;
+	retObj.y = pCurpos.y;
+	retObj.currentWordLength = pCurrentWordLength;
+	retObj.returnCode = 0;
+	retObj.continueOn = true;
+	retObj.doQuoteSelection = false;
+	retObj.showHelp = false;
+
+   // Check for slash commands (/S, /A, /?).  If the user has
+   // typed one of them by itself at the beginning of the line,
+   // then save, abort, or show help, respectively.
+   if (gEditLines[gEditLinesIndex].length() == 2)
+   {
+      var lineUpper = gEditLines[gEditLinesIndex].text.toUpperCase();
+      // /S: Save
+      if (lineUpper == "/S")
+      {
+         // If the current text line is the last one, remove it; otherwise,
+         // blank it out.
+         if (gEditLinesIndex == gEditLines.length-1)
+            gEditLines.splice(gEditLinesIndex, 1);
+         else
+            gEditLines[gEditLinesIndex].text = "";
+
+         retObj.continueOn = false;
+         return(retObj);
+      }
+      // /A: Abort
+      else if (lineUpper == "/A")
+      {
+         // Confirm with the user
+         if (promptYesNo("Abort message", false, "Abort"))
+         {
+            retObj.returnCode = 1; // 1: Abort
+            retObj.continueOn = false;
+            return(retObj);
+         }
+         else
+         {
+            // Make sure the edit color attribute is set back.
+            //console.print("n" + gTextAttrs);
+            console.print(chooseEditColor());
+
+            // Blank out the data in the text line, set the data in
+            // retObj, and return it.
+            gEditLines[gEditLinesIndex].text = "";
+            retObj.currentWordLength = 0;
+            gTextLineIndex = 0;
+            retObj.x = gEditLeft;
+            retObj.y = pCurpos.y;
+            // Blank out the /A on the screen
+            //console.print("n" + gTextAttrs);
+            console.print(chooseEditColor());
+            console.gotoxy(retObj.x, retObj.y);
+            console.print("  ");
+            // Put the cursor where it should be and return.
+            console.gotoxy(retObj.x, retObj.y);
+            return(retObj);
+         }
+      }
+      // /Q: Do quote selection, and /?: Show help
+      else if ((lineUpper == "/Q") || (lineUpper == "/?"))
+      {
+         retObj.doQuoteSelection = (lineUpper == "/Q");
+         retObj.showHelp = (lineUpper == "/?");
+         retObj.currentWordLength = 0;
+         gTextLineIndex = 0;
+         gEditLines[gEditLinesIndex].text = "";
+         // Blank out the /? on the screen
+         //console.print("n" + gTextAttrs);
+         console.print(chooseEditColor());
+         retObj.x = gEditLeft;
+         console.gotoxy(retObj.x, retObj.y);
+         console.print("  ");
+         // Put the cursor where it should be and return.
+         console.gotoxy(retObj.x, retObj.y);
+         return(retObj);
+      }
+   }
+
+	// Store the current screen row position and gEditLines index.
+	var initialScreenLine = pCurpos.y;
+	var initialEditLinesIndex = gEditLinesIndex;
+
+	// If we're currently on the last line, then we'll need to append
+	// a new line.  Otherwise, we'll need to splice a new line into
+	// gEditLines where appropriate.
+
+	var appendLineToEnd = (gEditLinesIndex == gEditLines.length-1);
+	var retObject = enterKey_InsertOrAppendNewLine(pCurpos, pCurrentWordLength, appendLineToEnd);
+	retObj.x = retObject.x;
+	retObj.y = retObject.y;
+	retObj.currentWordLength = retObject.currentWordLength;
+
+   // If a line was added to gEditLines, then set the hardNewlineEnd property
+   // to true for both lines.
+   if (retObject.addedATextLine)
+   {
+      gEditLines[initialEditLinesIndex].hardNewlineEnd = true;
+      gEditLines[gEditLinesIndex].hardNewlineEnd = true;
+   }
+
+	// Refresh the message text on the screen if that wasn't done by
+	// enterKey_InsertOrAppendNewLine().
+	if (!retObject.displayedEditlines)
+      displayEditLines(initialScreenLine, initialEditLinesIndex, gEditBottom, true, true);
+
+	console.gotoxy(retObj.x, retObj.y);
+
+	return retObj;
+}
+
+// Helper function for doEnterKey(): Appends/inserts a line to gEditLines
+// and returns the position of where the cursor shoul dbe.
+//
+// Parameters:
+//  pCurpos: An object containing x and y values representing the
+//           cursor position.
+//  pCurrentWordLength: The length of the current word that has been typed
+//  pAppendLine: Whether or not to append the new line (true/false).  If false,
+//               then the new line will be spliced into the middle of the array
+//               where it belongs rather than appended to the end.
+//
+// Return value: An object containing the following values:
+//               - x and y values representing the cursor position
+//               - currentLength: The current word length
+//               - displayedEditlines: Whether or not the edit lines were refreshed
+//               - addedATextLine: Whether or not a line of text was added to gEditLines
+//               - addedTextLineBelow: If addedATextLine is true, whether or not
+//                 the line was added below the line
+function enterKey_InsertOrAppendNewLine(pCurpos, pCurrentWordLength, pAppendLine)
+{
+   var returnObject = new Object();
+   returnObject.displayedEditlines = false;
+   returnObject.addedATextLine = false;
+   returnObject.addedTextLineBelow = false;
+
+	// If we're at the end of the line, then we can simply
+	// add a new blank line & set the cursor there.
+	// Otherwise, we need to split the current line, and
+	// the text to the right of the cursor will go on the new line.
+	if (gTextLineIndex == gEditLines[gEditLinesIndex].length())
+	{
+		if (pAppendLine)
+		{
+			// Add a new blank line to the end of the message, and set
+			// the cursor there.
+			gEditLines.push(new TextLine());
+			++gEditLinesIndex;
+			returnObject.addedATextLine = true;
+         returnObject.addedTextLineBelow = true;
+		}
+		else
+		{
+			// Increment gEditLinesIndex and add a new line there.
+			++gEditLinesIndex;
+			gEditLines.splice(gEditLinesIndex, 0, new TextLine());
+         returnObject.addedATextLine = true;
+		}
+
+		gTextLineIndex = 0;
+		pCurrentWordLength = 0;
+		pCurpos.x = gEditLeft;
+		// Update the vertical cursor position.
+		// If the cursor is at the bottom row, then we need
+		// to scroll the message down by 1 line.  Otherwise,
+		// we can simply increment pCurpos.y.
+		if (pCurpos.y == gEditBottom)
+		{
+			displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+			                 gEditBottom, true, true);
+         returnObject.displayedEditlines = true;
+      }
+		else
+			++pCurpos.y;
+	}
+	else
+	{
+		// We're in the middle of the line.
+		// Get the text to the end of the current line.
+		var lineEndText = gEditLines[gEditLinesIndex].text.substr(gTextLineIndex);
+		// Remove that text from the current line.
+		gEditLines[gEditLinesIndex].text = gEditLines[gEditLinesIndex].text.substr(0, gTextLineIndex);
+
+		if (pAppendLine)
+		{
+			// Create a new line containing lineEndText and append it to
+			// gEditLines.  Then place the cursor at the start of that line.
+			var newTextLine = new TextLine();
+			newTextLine.text = lineEndText;
+			newTextLine.hardNewlineEnd = gEditLines[gEditLinesIndex].hardNewlineEnd;
+			newTextLine.isQuoteLine = gEditLines[gEditLinesIndex].isQuoteLine;
+			gEditLines.push(newTextLine);
+			++gEditLinesIndex;
+			returnObject.addedATextLine = true;
+         returnObject.addedTextLineBelow = true;
+		}
+		else
+		{
+			// Create a new line containing lineEndText and splice it into
+			// gEditLines on the next line.  Then place the cursor at the
+			// start of that line.
+			var oldIndex = gEditLinesIndex++;
+			var newTextLine = new TextLine();
+			newTextLine.text = lineEndText;
+			newTextLine.hardNewlineEnd = gEditLines[oldIndex].hardNewlineEnd;
+			newTextLine.isQuoteLine = gEditLines[oldIndex].isQuoteLine;
+			// If the user pressed enter at the beginning of a line, then a new
+			// blank line will be inserted above, so we want to make sure its
+			// isQuoteLine property is set to false.
+			if (gTextLineIndex == 0)
+            gEditLines[oldIndex].isQuoteLine = false;
+         // Splice the new text line into gEditLines at gEditLinesIndex.
+			gEditLines.splice(gEditLinesIndex, 0, newTextLine);
+         returnObject.addedATextLine = true;
+		}
+
+		gTextLineIndex = 0;
+		pCurpos.x = gEditLeft;
+		// Update the vertical cursor position.
+		// If the cursor is at the bottom row, then we need
+		// to scroll the message down by 1 line.  Otherwise,
+		// we can simply increment pCurpos.y.
+		if (pCurpos.y == gEditBottom)
+		{
+			displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+			                 gEditBottom, true, true);
+			returnObject.displayedEditlines = true;
+      }
+		else
+			++pCurpos.y;
+		// Figure out the current word length.
+		// Look for a space in lineEndText.  If a space is found,
+		// the word length is the length of the word up until the
+		// space.  If a space is not found, then the word length
+		// is the entire length of lineEndText.
+		var spacePos = lineEndText.indexOf(" ");
+		if (spacePos > -1)
+			pCurrentWordLength = spacePos;
+		else
+			pCurrentWordLength = lineEndText.length;
+	}
+
+	// Set some stuff in the return object, and return it.
+	returnObject.x = pCurpos.x;
+	returnObject.y = pCurpos.y;
+	returnObject.currentWordLength = pCurrentWordLength;
+	return returnObject;
+}
+
+// This function handles quote selection and is called by doEditLoop().
+//
+// Parameters:
+//  pCurpos: An object containing x and y values representing the
+//           cursor position.
+//  pCurrentWordLength: The length of the current word that has been typed
+//
+// Return value: An object containing the following properties:
+//               x and y: The horizontal and vertical cursor position
+//               timedOut: Whether or not the user input timed out (boolean)
+//               currentWordLength: The length of the current word
+function doQuoteSelection(pCurpos, pCurrentWordLength)
+{
+   // Create the return object
+   var retObj = new Object();
+	retObj.x = pCurpos.x;
+	retObj.y = pCurpos.y;
+	retObj.timedOut = false;
+	retObj.currentWordLength = pCurrentWordLength;
+
+   // Note: Quote lines are in the gQuoteLines array, where each element is
+   // a string.
+
+   // If gQuoteLines is empty, then we have nothing to do, so just return.
+   if ((gQuoteLines.length == 0) || !gUseQuotes)
+      return retObj;
+
+   // Set up some variables
+   var curpos = new Object();
+   curpos.x = pCurpos.x;
+   curpos.y = pCurpos.y;
+   const quoteWinHeight = 8;
+   // The first and last lines on the screen where quote lines are written
+   const quoteTopScreenRow = console.screen_rows - quoteWinHeight + 2;
+   const quoteBottomScreenRow = console.screen_rows - 2;
+   // Quote window parameters
+   const quoteWinTopScreenRow = quoteTopScreenRow-1;
+   const quoteWinWidth = gEditRight - gEditLeft + 1;
+
+   // Display the top border of the quote window.
+   fpDrawQuoteWindowTopBorder(quoteWinHeight, gEditLeft, gEditRight);
+
+   // Display the remainder of the quote window, with the quote lines in it.
+   displayQuoteWindowLines(gQuoteLinesTopIndex, quoteWinHeight, quoteWinWidth, true, gQuoteLinesIndex);
+
+   // Position the cursor at the currently-selected quote line.
+   var screenLine = quoteTopScreenRow + (gQuoteLinesIndex - gQuoteLinesTopIndex);
+   console.gotoxy(gEditLeft, screenLine);
+
+   // User input loop
+   var quoteLine = getQuoteTextLine(gQuoteLinesIndex, quoteWinWidth);
+   retObj.timedOut = false;
+   var userInput = null;
+   var continueOn = true;
+   while (continueOn)
+   {
+		// Get a key, and time out after 1 minute.
+		userInput = console.inkey(0, 100000);
+		if (userInput == "")
+		{
+			// The input timeout was reached.  Abort.
+			retObj.timedOut = true;
+			continueOn = false;
+			break;
+		}
+
+		// If we got here, that means the user input didn't time out.
+		switch (userInput)
+		{
+         case KEY_UP:
+            // Go up 1 quote line
+            if (gQuoteLinesIndex > 0)
+            {
+               // If the cursor is at the topmost position, then
+               // we need to scroll up 1 line in gQuoteLines.
+               if (screenLine == quoteTopScreenRow)
+               {
+                  --gQuoteLinesIndex;
+                  --gQuoteLinesTopIndex;
+                  quoteLine = getQuoteTextLine(gQuoteLinesIndex, quoteWinWidth);
+                  // Redraw the quote lines in the quote window.
+                  displayQuoteWindowLines(gQuoteLinesIndex, quoteWinHeight, quoteWinWidth,
+                                          true, gQuoteLinesIndex);
+                  // Put the cursor back where it should be.
+                  console.gotoxy(gEditLeft, screenLine);
+               }
+               // If the cursor is below the topmost position, then
+               // we can just go up 1 line.
+               else if (screenLine > quoteTopScreenRow)
+               {
+                  // Write the current quote line using the normal color
+                  // Note: This gets the quote line again using getQuoteTextLine()
+                  // so that the color codes in the line will be correct.
+                  quoteLine = getQuoteTextLine(gQuoteLinesIndex, quoteWinWidth);
+                  console.gotoxy(gEditLeft, screenLine);
+                  printf(gFormatStrWithAttr, gQuoteWinTextColor, quoteLine);
+
+                  // Go up one line and display that quote line in the
+                  // highlighted color.
+                  --screenLine;
+                  --gQuoteLinesIndex;
+                  quoteLine = strip_ctrl(getQuoteTextLine(gQuoteLinesIndex, quoteWinWidth));
+                  console.gotoxy(gEditLeft, screenLine);
+                  printf(gFormatStrWithAttr, gQuoteLineHighlightColor, quoteLine);
+
+                  // Make sure the cursor is where it should be.
+                  console.gotoxy(gEditLeft, screenLine);
+               }
+            }
+            break;
+         case KEY_DOWN:
+            // Go down 1 line in the quote window.
+            var downRetObj = moveDownOneQuoteLine(gQuoteLinesIndex, screenLine,
+                                                  quoteWinHeight, quoteWinWidth,
+                                                  quoteBottomScreenRow);
+            gQuoteLinesIndex = downRetObj.quoteLinesIndex;
+            screenLine = downRetObj.screenLine;
+            quoteLine = downRetObj.quoteLine;
+            break;
+         case KEY_ENTER:
+            // numTimesToMoveDown specifies how many times to move the cursor
+            // down after inserting the quote line into the message.
+            var numTimesToMoveDown = 1;
+
+            // Insert the quote line into gEditLines after the current gEditLines index.
+            var insertedBelow = insertLineIntoMsg(gEditLinesIndex, quoteLine, true, true);
+            if (insertedBelow)
+            {
+               // The cursor will need to be moved down 1 more line.
+               // So, increment numTimesToMoveDown, and set curpos.x
+               // and gTextLineIndex to the beginning of the line.
+               ++numTimesToMoveDown;
+               curpos.x = gEditLeft;
+               gTextLineIndex = 0;
+               retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+            }
+            else
+               retObj.currentWordLength = 0;
+
+            // Refresh the part of the message that needs to be refreshed on the
+            // screen (above the quote window).
+            if (curpos.y < quoteTopScreenRow-1)
+               displayEditLines(curpos.y, gEditLinesIndex, quoteTopScreenRow-2, false, true);
+
+            gEditLinesIndex += numTimesToMoveDown;
+
+            // Go down one line in the quote window.
+            var tempReturnObj = moveDownOneQuoteLine(gQuoteLinesIndex, screenLine,
+                                              quoteWinHeight, quoteWinWidth,
+                                              quoteBottomScreenRow);
+            gQuoteLinesIndex = tempReturnObj.quoteLinesIndex;
+            screenLine = tempReturnObj.screenLine;
+            quoteLine = tempReturnObj.quoteLine;
+
+            // Move the cursor down as specified by numTimesToMoveDown.  If
+            // the cursor is at the bottom of the edit area, then refresh
+            // the message on the screen, scrolled down by one line.
+            for (var i = 0; i < numTimesToMoveDown; ++i)
+            {
+               if (curpos.y == gEditBottom)
+               {
+                  // Refresh the message on the screen, scrolled down by
+                  // one line, but only if this is the last time we're
+                  // doing this (for efficiency).
+                  if (i == numTimesToMoveDown-1)
+                  {
+                     displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+                                      quoteTopScreenRow-2, false, true);
+                  }
+               }
+               else
+                  ++curpos.y;
+            }
+            break;
+         // ESC or CTRL-Q: Stop quoting
+         case KEY_ESC:
+         case CTRL_Q:
+            // Quit out of the input loop (get out of quote mode).
+            continueOn = false;
+            break;
+		}
+   }
+
+   // We've exited quote mode.  Refresh the message text on the screen.  Note:
+   // This will refresh only the quote window portion of the screen if the
+   // cursor row is at or below the top of the quote window, and it will also
+   // refresh the screen if the cursor row is above the quote window.
+   displayEditLines(quoteWinTopScreenRow, gEditLinesIndex-(curpos.y-quoteWinTopScreenRow),
+                    gEditBottom, true, true);
+
+   // Draw the bottom edit border to erase the bottom border of the
+   // quote window.
+   fpDisplayTextAreaBottomBorder(gEditBottom+1, gUseQuotes, gEditLeft, gEditRight,
+                                 gInsertMode, gConfigSettings.allowColorSelection);
+
+   // Make sure the color is correct for editing.
+   //console.print("n" + gTextAttrs);
+   console.print(chooseEditColor());
+   // Put the cursor where it should be.
+   console.gotoxy(curpos);
+
+   // Set the settings in the return object, and return it.
+	retObj.x = curpos.x;
+	retObj.y = curpos.y;
+	return retObj;
+}
+
+// Helper for doQuoteSelection(): This function moves the quote selection
+// down one line and updates the quote window.
+//
+// Parameters:
+//  pQuoteLinesIndex: The index of the current line in gQuoteLines
+//  pScreenLine: The vertical position of the cursor on the screen
+//  pQuoteWinHeight: The height of the quote window
+//  pQuoteWinWidth: The width of the quote window
+//  pQuoteBottomScreenLine: The bottommost screen line where quote lines are displayed
+function moveDownOneQuoteLine(pQuoteLinesIndex, pScreenLine, pQuoteWinHeight, pQuoteWinWidth,
+                               pQuoteBottomScreenLine)
+{
+   // Create the return object
+   var returnObj = new Object();
+   returnObj.quoteLinesIndex = pQuoteLinesIndex;
+   returnObj.screenLine = pScreenLine;
+   returnObj.quoteLine = "";
+
+   // If the current quote line is above the last one, then we can
+   // move down one quote line.
+   if (pQuoteLinesIndex < gQuoteLines.length-1)
+   {
+      // If the cursor is at the bottommost position, then
+      // we need to scroll up 1 line in gQuoteLines.
+      if (pScreenLine == pQuoteBottomScreenLine)
+      {
+         ++pQuoteLinesIndex;
+         ++gQuoteLinesTopIndex;
+         returnObj.quoteLine = getQuoteTextLine(pQuoteLinesIndex, pQuoteWinWidth);
+         // Redraw the quote lines in the quote window.
+         var topQuoteIndex = pQuoteLinesIndex - pQuoteWinHeight + 4;
+         displayQuoteWindowLines(topQuoteIndex, pQuoteWinHeight, pQuoteWinWidth, true,
+                                 pQuoteLinesIndex);
+         // Put the cursor back where it should be.
+         console.gotoxy(gEditLeft, pScreenLine);
+      }
+      // If the cursor is above the bottommost position, then
+      // we can just go down 1 line.
+      else if (pScreenLine < pQuoteBottomScreenLine)
+      {
+         // Write the current quote line using the normal color.
+         // Note: This gets the quote line again using getQuoteTextLine()
+         // so that the color codes in the line will be correct.
+         console.gotoxy(gEditLeft, pScreenLine);
+         returnObj.quoteLine = getQuoteTextLine(pQuoteLinesIndex, pQuoteWinWidth);
+         printf(gFormatStrWithAttr, gQuoteWinTextColor, returnObj.quoteLine);
+
+         // Go down one line and display that quote line in the
+         // highlighted color.
+         ++pScreenLine;
+         ++pQuoteLinesIndex;
+         returnObj.quoteLine = getQuoteTextLine(pQuoteLinesIndex, pQuoteWinWidth);
+         console.gotoxy(gEditLeft, pScreenLine);
+         printf(gFormatStrWithAttr, gQuoteLineHighlightColor, returnObj.quoteLine);
+
+         // Put the cursor back where it should be.
+         console.gotoxy(gEditLeft, pScreenLine);
+      }
+   }
+   else // This else case is for when we're already on the last quote line.
+      returnObj.quoteLine = getQuoteTextLine(pQuoteLinesIndex, pQuoteWinWidth);
+
+   // Make sure the properties of returnObj have the correct
+   // values (except quoteLine, which is already set), and
+   // return the returnObj.
+   returnObj.quoteLinesIndex = pQuoteLinesIndex;
+   returnObj.screenLine = pScreenLine;
+   return returnObj;
+}
+
+// Helper for doQuoteSelection(): This displays the quote window, except for its
+// top border.
+//
+// Parameters:
+//  pQuoteLinesIndex: The index into gQuoteLines to start at.  The quote line
+//                    at this index will be displayed at the top of the quote
+//                    window.
+//  pQuoteWinHeight: The height of the quote window
+//  pQuoteWinWidth: The width of the quote window
+//  pDrawBottomBorder: Whether or not to draw the bottom border of the quote
+//                     window.
+//  pHighlightIndex: Optional - An index of a quote line to highlight.
+function displayQuoteWindowLines(pQuoteLinesIndex, pQuoteWinHeight, pQuoteWinWidth, pDrawBottomBorder, pHighlightIndex)
+{
+   var quoteLinesIndex = pQuoteLinesIndex;
+   var quoteLine = ""; // A line of text from gQuoteLines
+   var screenLine = console.screen_rows - pQuoteWinHeight + 2;
+   if (gQuoteLines.length > 0)
+   {
+      var color = "";     // The color to use when writing the text
+      var lineLength = 0; // Length of a quote line
+      while ((quoteLinesIndex < gQuoteLines.length) && (screenLine < console.screen_rows-1))
+      {
+         quoteLine = getQuoteTextLine(quoteLinesIndex, pQuoteWinWidth);
+         // Go to the line on screen and display the quote line text.
+         console.gotoxy(gEditLeft, screenLine);
+         // If pHighlightIndex is valid, and if quoteLinesIndex matches
+         // pHighlightIndex, then use the highlight color for this quote line.
+         if ((pHighlightIndex != null) && (pHighlightIndex >= 0) && (pHighlightIndex < gQuoteLines.length))
+         {
+            if (quoteLinesIndex == pHighlightIndex)
+            {
+               color = gQuoteLineHighlightColor;
+               quoteLine = quoteLine;
+            }
+            else
+               color = gQuoteWinTextColor;
+         }
+         else
+         {
+            color = gQuoteWinTextColor;
+            quoteLine = quoteLine;
+         }
+         // Write the quote line, and fill the rest of the line with spaces.
+         printf(gFormatStrWithAttr, color, quoteLine);
+
+         ++quoteLinesIndex;
+         ++screenLine;
+      }
+   }
+   // Fill the remainder of the quote window area
+   for (; screenLine < console.screen_rows-1; ++screenLine)
+   {
+      console.gotoxy(gEditLeft, screenLine);
+      printf(gFormatStrWithAttr, gQuoteWinTextColor, "");
+   }
+
+   // If pDrawBottomBorder is true, then display the bottom border of the
+   // quote window.
+   if (pDrawBottomBorder)
+   {
+      console.gotoxy(gEditLeft, screenLine);
+      fpDrawQuoteWindowBottomBorder(gEditLeft, gEditRight);
+   }
+}
+
+// This function returns a line of text from gQuoteLines, with "> "
+// added  to the front if it's not blank.  Also, the text line will
+// be limited in length by the screen width.
+//
+// Parameters:
+//  pIndex: The index of the quote line to retrieve
+//  pMaxWidth: The maximum width of the line
+//
+// Return value: The line of text from gQuoteLines
+function getQuoteTextLine(pIndex, pMaxWidth)
+{
+   var textLine = "";
+   if ((pIndex >= 0) && (pIndex < gQuoteLines.length))
+   {
+      if ((gQuoteLines[pIndex] != null) && (gQuoteLines[pIndex].length > 0))
+         textLine = quote_msg(gQuoteLines[pIndex], pMaxWidth-1, gQuotePrefix);
+   }
+   return textLine;
+}
+
+// This function deletes the current edit line.  This function is called
+// by doEditLoop().
+//
+// Parameters:
+//  pCurpos: An object containing the x and y cursor position.
+//
+// Return value: An object containing the following properties:
+//               x: The horizontal component of the cursor location
+//               y: The vertical component of the cursor location
+//               currentWordLength: The length of the current word
+function doDeleteLine(pCurpos)
+{
+   // Construct the object that we'll be returning
+   var retObj = new Object();
+   retObj.x = pCurpos.x;
+   retObj.y = pCurpos.y;
+   retObj.currentWordLength = 0;
+
+   // Remove the current line from gEditLines.  If we're on the last line,
+   // then we'll need to add a blank line to gEditLines.  We'll also need
+   // to refresh the edit lines on the screen.
+   if (gEditLinesIndex == gEditLines.length-1)
+   {
+      // We're on the last line.  Remove it & replace it with a new line.
+      gEditLines.splice(gEditLinesIndex, 1, new TextLine());
+      // Refresh (clear) the line on the screen
+      displayEditLines(pCurpos.y, gEditLinesIndex, pCurpos.y, true, true);
+      console.gotoxy(gEditLeft, pCurpos.y);
+      printf(gFormatStr, "");
+   }
+   else
+   {
+      // We weren't on the last line.  Remove the current line and get the
+      // word length, and then refresh the message on the screen.
+      gEditLines.splice(gEditLinesIndex, 1);
+      displayEditLines(pCurpos.y, gEditLinesIndex, gEditBottom, true, true);
+      // Update the current word length
+      retObj.currentWordLength = getWordLength(gEditLinesIndex, 0);
+   }
+
+   // Adjust global message parameters, make sure the cursor position is
+   // correct in retObj, and place the cursor where it's supposed to be.
+   gTextLineIndex = 0;
+   retObj.x = gEditLeft;
+   console.gotoxy(retObj.x, retObj.y);
+
+   return retObj;
+}
+
+// Toggles insert mode between insert and overwrite mode and updates it
+// on the screen.  Insert/overwrite mode is signified by gInsertMode
+// (either "INS" or "OVR");
+//
+// Parameters:
+//  pCurpos: An object containing the cursor's position (X and Y coordinates).
+//           The cursor will be returned here when finished.
+function toggleInsertMode(pCurpos)
+{
+   // Change gInsertMode, and then refresh it on the screen.
+   gInsertMode = inInsertMode() ? "OVR" : "INS";
+   fpUpdateInsertModeOnScreen(gEditRight, gEditBottom, gInsertMode);
+   if ((pCurpos != null) && (typeof(pCurpos) != "undefined"))
+      console.gotoxy(pCurpos);
+}
+
+// Displays the contents of the gEditLines array, starting at a given
+// line on the screen and index into the array.
+//
+// Parameters:
+//  pStartScreenRow: The line on the screen at which to start printing the
+//               message lines (1-based)
+//  pArrayIndex: The starting index to use for the message lines array
+//               (0-based)
+//  pEndScreenRow: Optional.  This specifies the row on the screen to stop
+//                 at.  If this is not specified, this function will stop
+//                 at the edit area's bottom row (gEditBottom).
+//  pClearRemainingScreenRows: Optional.  This is a boolean that specifies
+//                             whether or not to clear the remaining lines
+//                             on the screen between the end of the message
+//                             text and the last row on the screen.
+//  pIgnoreEditAreaBuffer: Optional.  This is a boolean that specifies whether
+//                         to always write the edit text regardless of gEditAreaBuffer.
+//                         By default, gEditAreaBuffer is always checked.
+function displayEditLines(pStartScreenRow, pArrayIndex, pEndScreenRow, pClearRemainingScreenRows,
+                           pIgnoreEditAreaBuffer)
+{
+   // Make sure the array has lines in it, the given array index is valid, and
+   // that the given line # is valid.  If not, then just return.
+   if ((gEditLines.length == 0) || (pArrayIndex < 0) || (pStartScreenRow < 1) || (pStartScreenRow > gEditBottom))
+      return;
+
+   // Choose which ending screen row to use for displaying text,
+   // pEndScreenRow or gEditBottom.
+   var endScreenRow = (pEndScreenRow != null ? pEndScreenRow : gEditBottom);
+
+	// Display the message lines
+	console.print("n" + gTextAttrs);
+	var screenLine = pStartScreenRow;
+	var arrayIndex = pArrayIndex;
+	while ((screenLine <= endScreenRow) && (arrayIndex < gEditLines.length))
+	{
+		// Print the text from the current line in gEditLines.  Note: Lines starting
+		// with " >" are assumed to be quote lines - Display those lines with cyan
+		// color and the normal lines with gTextAttrs.
+		var color = gTextAttrs;
+		// Note: gEditAreaBuffer is also used in clearMsgAreaToBottom().
+		if ((gEditAreaBuffer[screenLine] != gEditLines[arrayIndex].text) || pIgnoreEditAreaBuffer)
+      {
+         // Choose the quote line color or the normal color for the line, then
+         // display the line on the screen.
+         color = (isQuoteLine(gEditLines, arrayIndex) ? gQuoteLineColor : gTextAttrs);
+         console.gotoxy(gEditLeft, screenLine);
+         printf(gFormatStrWithAttr, color, gEditLines[arrayIndex].text);
+         gEditAreaBuffer[screenLine] = gEditLines[arrayIndex].text;
+      }
+
+		++screenLine;
+		++arrayIndex;
+	}
+	if (arrayIndex > 0)
+		--arrayIndex;
+	// incrementLineBeforeClearRemaining stores whether or not we
+	// should increment screenLine before clearing the remaining
+	// lines in the edit area.
+	var incrementLineBeforeClearRemaining = true;
+	// If the array index is valid, and if the current line is shorter
+	// than the edit area width, then place the cursor after the last
+	// character in the line.
+	if ((arrayIndex >= 0) && (arrayIndex < gEditLines.length) &&
+	    (gEditLines[arrayIndex] != undefined) && (gEditLines[arrayIndex].text != undefined))
+	{
+      var lineLength = gEditLines[arrayIndex].length();
+      if (lineLength < gEditWidth)
+      {
+         --screenLine;
+         console.gotoxy(gEditLeft + gEditLines[arrayIndex].length(), screenLine);
+      }
+      else if ((lineLength == gEditWidth) || (lineLength == 0))
+         incrementLineBeforeClearRemaining = false;
+	}
+	else
+      incrementLineBeforeClearRemaining = false;
+
+	// Edge case: If the current screen line is below the last line, then
+	// clear the lines up until that point.
+	var clearRemainingScreenLines = (pClearRemainingScreenRows != null ? pClearRemainingScreenRows : true);
+	if (clearRemainingScreenLines && (screenLine <= endScreenRow))
+	{
+		console.print("n" + gTextAttrs);
+		var screenLineBackup = screenLine; // So we can move the cursor back
+		clearMsgAreaToBottom(incrementLineBeforeClearRemaining ? screenLine+1 : screenLine,
+		                     pIgnoreEditAreaBuffer);
+		// Move the cursor back to the end of the current text line.
+		if (typeof(gEditLines[arrayIndex]) != "undefined")
+         console.gotoxy(gEditLeft + gEditLines[arrayIndex].length(), screenLineBackup);
+      else
+         console.gotoxy(gEditLeft, screenLineBackup);
+	}
+
+	// Make sure the correct color is set for the current line.
+	console.print(chooseEditColor());
+}
+
+// Clears the lines in the message area from a given line to the bottom.
+//
+// Parameters:
+//  pStartLine: The line number at which to start clearing.
+//  pIgnoreEditAreaBuffer: Optional.  This is a boolean that specifies whether
+//                         to always write the edit text regardless of gEditAreaBuffer.
+//                         By default, gEditAreaBuffer is always checked.
+function clearMsgAreaToBottom(pStartLine, pIgnoreEditAreaBuffer)
+{
+   for (var screenLine = pStartLine; screenLine <= gEditBottom; ++screenLine)
+   {
+		// Note: gEditAreaBuffer is also used in displayEditLines().
+      if ((gEditAreaBuffer[screenLine].length > 0) || pIgnoreEditAreaBuffer)
+      {
+         console.gotoxy(gEditLeft, screenLine);
+         printf(gFormatStr, "");
+         gEditAreaBuffer[screenLine] = "";
+      }
+   }
+}
+
+// Returns whether or not the message is empty (gEditLines may have lines in
+// it, and this tests to see if they are all empty).
+function messageIsEmpty()
+{
+	var msgEmpty = true;
+	
+	for (var i = 0; i < gEditLines.length; ++i)
+	{
+		if (gEditLines[i].length() > 0)
+		{
+			msgEmpty = false;
+			break;
+		}
+	}
+
+	return msgEmpty;
+}
+
+// Displays a part of the message text in a rectangle on the screen.  This
+// is useful for refreshing part of the message area that may have been
+// written over (i.e., by a text dialog).
+//
+// Parameters:
+//  pX: The upper-left X coordinate
+//  pY: The upper-left Y coordinate
+//  pWidth: The width of the rectangle
+//  pHeight: The height of the rectangle
+//  pEditLinesIndex: The starting index to use with gEditLines
+//  pClearExtraWidth: Boolean - Optional.  If true, then space after the end of the line
+//                    up to the specified width will be cleared.  Defaults to false.
+function displayMessageRectangle(pX, pY, pWidth, pHeight, pEditLinesIndex, pClearExtraWidth)
+{
+   // If any of the parameters are out of bounds, then just return without
+   // doing anything.
+   if ((pX < gEditLeft) || (pY < gEditTop) || (pWidth < 0) || (pHeight < 0) || (pEditLinesIndex < 0))
+      return;
+
+   // If pWidth is too long with the given pX, then fix it.
+   if (pWidth > (gEditRight - pX + 1))
+      pWidth = gEditRight - pX + 1;
+   // If pHeight is too much with the given pY, then fix it.
+   if (pHeight > (gEditBottom - pY + 1))
+      pHeight = gEditBottom - pY + 1;
+
+   // Calculate the index into the edit line using pX and gEditLeft.  This
+   // assumes that pX is within the edit area (and it should be).
+   const editLineIndex = pX - gEditLeft;
+
+   // Go to the given position on the screen and output the message text.
+   var messageStr = ""; // Will contain a portion of the message text
+   var screenY = pY;
+   var editLinesIndex = pEditLinesIndex;
+   var formatStr = "%-" + pWidth + "s";
+   for (var rectangleLine = 0; rectangleLine < pHeight; ++rectangleLine)
+   {
+      // Output the correct color for the line
+      console.print("n" + (isQuoteLine(gEditLines, editLinesIndex) ? gQuoteLineColor : gTextAttrs));
+      // Go to the position on the screen
+      screenY = pY + rectangleLine;
+      console.gotoxy(pX, screenY);
+      // Display the message text.  If the current edit line is valid,
+      // then print it; otherwise, just print spaces to blank out the line.
+      if (typeof(gEditLines[editLinesIndex]) != "undefined")
+      {
+         //printf(formatStr, gEditLines[editLinesIndex].text.substr(editLineIndex, pWidth));
+         //printEditLine(pIndex, pUseColors, pStart, pLength)
+         // TODO: Change the false to the parameter for whether or not to allow
+         // message colors
+         printEditLine(editLinesIndex, false, editLineIndex, pWidth);
+         // If pClearExtraWidth is true, then if the box width is longer than
+         // the text that was written, then output spaces to clear the rest
+         // of the line to erase the rest of the box line.
+         if (pClearExtraWidth)
+         {
+            var displayedTextLen = gEditLines[editLinesIndex].text.length - editLineIndex;
+            if (pWidth > displayedTextLen)
+               printf("%" + (pWidth-displayedTextLen) + "s", "");
+         }
+      }
+      else
+         printf(formatStr, "");
+
+      ++editLinesIndex;
+   }
+}
+
+// Displays the DCTEdit-style ESC menu and handles user input from that menu.
+// This is used by the main input loop.
+//
+// Parameters:
+//  curpos: The current cursor position
+//  pEditLineDiff: The difference between the current edit line and the top of
+//                 the edit area.
+//  pCurrentWordLength: The length of the current word
+//
+// Return value: An object containing values to be used by the main input loop.
+//               The object will contain these values:
+//                 returnCode: The value to use as the editor's return code
+//                 continueOn: Whether or not the input loop should continue
+//                 x: The horizontal component of the cursor position
+//                 y: The vertical component of the cursor position
+//                 currentWordLength: The length of the current word
+function handleDCTESCMenu(pCurpos, pCurrentWordLength)
+{
+   var returnObj = new Object();
+   returnObj.returnCode = 0;
+   returnObj.continueOn = true;
+   returnObj.x = pCurpos.x;
+   returnObj.y = pCurpos.y;
+   returnObj.currentWordLength = pCurrentWordLength;
+
+   // Call doDCTMenu() to display the DCT Edit menu and get the
+   // user's choice.
+   var editLineDiff = pCurpos.y - gEditTop;
+   var menuChoice = doDCTMenu(gEditLeft, gEditRight, gEditTop,
+                              displayMessageRectangle, gEditLinesIndex,
+                              editLineDiff, gIsSysop);
+   // Take action according to the user's choice.
+   // Save
+   if ((menuChoice == "S") || (menuChoice == CTRL_Z) ||
+       (menuChoice == DCTMENU_FILE_SAVE))
+   {
+      returnObj.returnCode = 0;
+      returnObj.continueOn = false;
+   }
+   // Abort
+   else if ((menuChoice == "A") || (menuChoice == CTRL_A) ||
+             (menuChoice == DCTMENU_FILE_ABORT))
+   {
+      // Before aborting, ask they user if they really want to abort.
+      if (promptYesNo("Abort message", false, "Abort"))
+      {
+         returnObj.returnCode = 1; // Aborted
+         returnObj.continueOn = false;
+      }
+      else
+      {
+         // Make sure the edit color attribute is set back.
+         //console.print("n" + gTextAttrs);
+         console.print(chooseEditColor());
+      }
+   }
+   // Toggle insert/overwrite mode
+   else if ((menuChoice == CTRL_V) || (menuChoice == DCTMENU_EDIT_INSERT_TOGGLE))
+      toggleInsertMode(pCurpos);
+   // Import file (sysop only)
+   else if (menuChoice == DCTMENU_SYSOP_IMPORT_FILE)
+   {
+      var retval = importFile(gIsSysop, pCurpos);
+      returnObj.x = retval.x;
+      returnObj.y = retval.y;
+      returnObj.currentWordLength = retval.currentWordLength;
+   }
+   // Import file for sysop, or Insert/Overwrite toggle for non-sysop
+   else if (menuChoice == "I")
+   {
+      if (gIsSysop)
+      {
+         var retval = importFile(gIsSysop, pCurpos);
+         returnObj.x = retval.x;
+         returnObj.y = retval.y;
+         returnObj.currentWordLength = retval.currentWordLength;
+      }
+      else
+         toggleInsertMode(pCurpos);
+   }
+   // Find text
+   else if ((menuChoice == CTRL_F) || (menuChoice == "F") ||
+             (menuChoice == DCTMENU_EDIT_FIND_TEXT))
+   {
+      var retval = findText(pCurpos);
+      returnObj.x = retval.x;
+      returnObj.y = retval.y;
+   }
+   // Command List
+   else if ((menuChoice == "C") || (menuChoice == DCTMENU_HELP_COMMAND_LIST))
+   {
+      displayCommandList(true, true, true, gIsSysop);
+      clearEditAreaBuffer();
+      fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+		               gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
+		               displayEditLines);
+   }
+   // General help
+   else if ((menuChoice == "G") || (menuChoice == DCTMENU_HELP_GENERAL))
+   {
+      displayGeneralHelp(true, true, true);
+      clearEditAreaBuffer();
+      fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+		               gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
+		               displayEditLines);
+   }
+   // Program info
+   else if ((menuChoice == "P") || (menuChoice == DCTMENU_HELP_PROGRAM_INFO))
+   {
+      displayProgramInfo(true, true, true);
+      clearEditAreaBuffer();
+      fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+		               gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
+		               displayEditLines);
+   }
+   // Export the message
+   else if ((menuChoice == "X") || (menuChoice == DCTMENU_SYSOP_EXPORT_FILE))
+   {
+      if (gIsSysop)
+      {
+         exportToFile(gIsSysop);
+         console.gotoxy(returnObj.x, returnObj.y);
+      }
+   }
+   // Edit the message
+   else if ((menuChoice == "E") || (menuChoice == KEY_ESC))
+   {
+      // We don't need to do do anything in here.
+   }
+
+   // Make sure the edit color attribute is set back.
+   //console.print("n" + gTextAttrs);
+   console.print(chooseEditColor());
+
+   return returnObj;
+}
+
+// Displays the IceEdit-style ESC menu and handles user input from that menu.
+// This is used by the main input loop.
+//
+// Parameters:
+//  curpos: The current cursor position
+//  pEditLineDiff: The difference between the current edit line and the top of
+//                 the edit area.
+//  pCurrentWordLength: The length of the current word
+//
+// Return value: An object containing values to be used by the main input loop.
+//               The object will contain these values:
+//                 returnCode: The value to use as the editor's return code
+//                 continueOn: Whether or not the input loop should continue
+//                 x: The horizontal component of the cursor position
+//                 y: The vertical component of the cursor position
+//                 currentWordLength: The length of the current word
+function handleIceESCMenu(pCurpos, pCurrentWordLength)
+{
+   var returnObj = new Object();
+   returnObj.returnCode = 0;
+   returnObj.continueOn = true;
+   returnObj.x = pCurpos.x;
+   returnObj.y = pCurpos.y;
+   returnObj.currentWordLength = pCurrentWordLength;
+
+   // Call doIceESCMenu() to display the choices, and then take the
+   // chosen action.
+   var userChoice = doIceESCMenu(console.screen_rows);
+   switch (userChoice)
+   {
+      case ICE_ESC_MENU_SAVE:
+         returnObj.returnCode = 0;
+         returnObj.continueOn = false;
+         break;
+      case ICE_ESC_MENU_ABORT:
+         // Before aborting, ask they user if they really want to abort.
+         if (promptYesNo("Abort message", false, "Abort"))
+         {
+            returnObj.returnCode = 1; // Aborted
+            returnObj.continueOn = false;
+         }
+         break;
+      case ICE_ESC_MENU_EDIT:
+         // Nothing needs to be done for this option.
+         break;
+      case ICE_ESC_MENU_HELP:
+         displayProgramInfo(true, true, false);
+         displayCommandList(false, false, true, gIsSysop);
+         clearEditAreaBuffer();
+         fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+                        gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
+                        displayEditLines);
+         break;
+   }
+
+   // If the user didn't choose help, then we only need to refresh the bottom
+   // row on the screen.
+   if (userChoice != ICE_ESC_MENU_HELP)
+      fpDisplayBottomHelpLine(console.screen_rows, gUseQuotes);
+
+   // Make sure the edit color attribute is set back.
+   //console.print("n" + gTextAttrs);
+   console.print(chooseEditColor());
+
+   return returnObj;
+}
+
+// Figures out and returns the length of a word in the message text,based on
+// a given edit lines index and text line index.
+//
+// Parameters:
+//  pEditLinesIndex: The index into the gEditLines array
+//  pTextLineIndex: The index into the line's text
+//
+// Return value: The length of the word at the given indexes
+function getWordLength(pEditLinesIndex, pTextLineIndex)
+{
+   // pEditLinesIndex and pTextLineIndex should be >= 0 before we can do
+   // anything in this function.
+   if ((pEditLinesIndex < 0) || (pTextLineIndex < 0))
+      return 0;
+   // Also, make sure gEditLines[pEditLinesIndex] is valid.
+   if ((gEditLines[pEditLinesIndex] == null) || (typeof(gEditLines[pEditLinesIndex]) == "undefined"))
+      return 0;
+
+   // This function counts and returns the number of non-whitespace characters
+   // before the current character.
+   function countBeforeCurrentChar()
+   {
+      var charCount = 0;
+
+      for (var i = pTextLineIndex-1; i >= 0; --i)
+      {
+         if (!/\s/.test(gEditLines[pEditLinesIndex].text.charAt(i)))
+            ++charCount;
+         else
+            break;
+      }
+
+      return charCount;
+   }
+
+   var wordLen = 0;
+
+   // If there are only characters to the left, or if the current
+   // character is a space, then count before the current character.
+   if ((pTextLineIndex == gEditLines[pEditLinesIndex].length()) ||
+       (gEditLines[pEditLinesIndex].text.charAt(gTextLineIndex) == " "))
+      wordLen = countBeforeCurrentChar();
+   // If there are charactrs to the left and at the current line index,
+   // then count to the left only if the current character is not whitespace.
+   else if (pTextLineIndex == gEditLines[pEditLinesIndex].length()-1)
+   {
+      if (!/\s/.test(gEditLines[pEditLinesIndex].text.charAt(pTextLineIndex)))
+         wordLen = countBeforeCurrentChar() + 1;
+   }
+   // If there are characters to the left and right, then count to the left
+   // and right only if the current character is not whitespace.
+   else if (pTextLineIndex < gEditLines[pEditLinesIndex].length()-1)
+   {
+      if (!/\s/.test(gEditLines[pEditLinesIndex].text.charAt(pTextLineIndex)))
+      {
+         // Count non-whitespace characters to the left, and include the current one.
+         wordLen = countBeforeCurrentChar() + 1;
+         // Count characters to the right.
+         for (var i = pTextLineIndex+1; i < gEditLines[pEditLinesIndex].length(); ++i)
+         {
+            if (!/\s/.test(gEditLines[pEditLinesIndex].text.charAt(i)))
+               ++wordLen;
+            else
+               break;
+         }
+      }
+   }
+
+   return wordLen;
+}
+
+// Inserts a string into gEditLines after a given index.
+//
+// Parameters:
+//  pInsertLineIndex: The index for gEditLines at which to insert the string.
+//  pString: The string to insert
+//  pHardNewline: Whether or not to enable the hard newline flag for the line
+//  pIsQuoteLine: Whether or not the line is a quote line
+//
+// Return value: Whether or not the line was inserted below the given index
+//               (as opposed to above).
+function insertLineIntoMsg(pInsertLineIndex, pString, pHardNewline, pIsQuoteLine)
+{
+   var insertedBelow = false;
+
+   // Create the new text line
+   var line = new TextLine();
+   line.text = pString;
+   line.hardNewlineEnd = false;
+   if ((pHardNewline != null) && (typeof(pHardNewline) != "undefined"))
+      line.hardNewlineEnd = pHardNewline;
+   if ((pIsQuoteLine != null) && (typeof(pIsQuoteLine) != "undefined"))
+      line.isQuoteLine = pIsQuoteLine;
+
+   // If the current message line is empty, insert the quote line above
+   // the current line.  Otherwise, insert the quote line below the
+   // current line.
+   if (typeof(gEditLines[pInsertLineIndex]) == "undefined")
+      gEditLines.splice(pInsertLineIndex, 0, line);
+   // Note: One time, I noticed an error with the following test:
+   // gEditLines[pInsertLineIndex] has no properties
+   // Thus, I added the above test to see if the edit line is valid.
+   else if (gEditLines[pInsertLineIndex].length() == 0)
+      gEditLines.splice(pInsertLineIndex, 0, line);
+   else
+   {
+      // Insert the quote line below the given line index
+      gEditLines.splice(pInsertLineIndex + 1, 0, line);
+      // The current message line should have its hardNewlineEnd set
+      // true so that the quote line won't get wrapped up.
+      gEditLines[pInsertLineIndex].hardNewlineEnd = true;
+      insertedBelow = true;
+   }
+
+   return insertedBelow;
+}
+
+// Prompts the user for a filename on the BBS computer and loads its contents
+// into the message.  This is for sysops only!
+//
+// Parameters:
+//  pIsSysop: Whether or not the user is the sysop
+//  pCurpos: The current cursor position (with x and y properties)
+//
+// Return value: An object containing the following information:
+//               x: The horizontal component of the cursor's location
+//               y: The vertical component of the cursor's location
+//               currentWordLength: The length of the current word
+function importFile(pIsSysop, pCurpos)
+{
+   // Create the return object
+   var retObj = new Object();
+   retObj.x = pCurpos.x;
+   retObj.y = pCurpos.y;
+   retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+
+   // Don't let non-sysops do this.
+   if (!pIsSysop)
+      return retObj;
+
+   var loadedAFile = false;
+   // This loop continues to prompt the user until they enter a valid
+   // filename or a blank string.
+   var continueOn = true;
+   while (continueOn)
+   {
+      // Go to the last row on the screen and prompt the user for a filename
+      var promptText = "ncFile:h";
+      var promptTextLen = strip_ctrl(promptText).length;
+      console.gotoxy(1, console.screen_rows);
+      console.cleartoeol("n");
+      console.print(promptText);
+      var filename = console.getstr(console.screen_columns-promptTextLen-1, K_NOCRLF);
+      continueOn = (filename != "");
+      if (continueOn)
+      {
+         filename = file_getcase(filename);
+         if (filename != undefined)
+         {
+            // Open the file and insert its contents into the message.
+            var inFile = new File(filename);
+            if (inFile.exists && inFile.open("r"))
+            {
+               const maxLineLength = gEditWidth - 1; // Don't insert lines longer than this
+               var fileLine;
+               while (!inFile.eof)
+               {
+                  fileLine = inFile.readln(1024);
+                  // fileLine should always be a string, but there seem to be
+                  // situations where it isn't.  So if it's a string, we can
+                  // insert text into gEditLines as normal.  If it's not a
+                  // string, insert a blank line.
+                  if (typeof(fileLine) == "string")
+                  {
+                     // Tab characters can cause problems, so replace tabs with 3 spaces.
+                     fileLine = fileLine.replace(/\t/, "   ");
+                     // Insert the line into the message, splitting up the line,
+                     // if the line is longer than the edit area.
+                     do
+                     {
+                        insertLineIntoMsg(gEditLinesIndex, fileLine.substr(0, maxLineLength),
+                                          true, false);
+                        fileLine = fileLine.substr(maxLineLength);
+                        ++gEditLinesIndex;
+                     } while (fileLine.length > maxLineLength);
+                     // Edge case, if the line still has characters in it
+                     if (fileLine.length > 0)
+                     {
+                        insertLineIntoMsg(gEditLinesIndex, fileLine, true, false);
+                        ++gEditLinesIndex;
+                     }
+                  }
+                  else
+                  {
+                     insertLineIntoMsg(gEditLinesIndex, "", true, false);
+                     ++gEditLinesIndex;
+                  }
+               }
+               inFile.close();
+
+               // If the last text line is blank, then remove it.
+               if (gEditLines[gEditLinesIndex].length() == 0)
+               {
+                  gEditLines.splice(gEditLinesIndex, 1);
+                  --gEditLinesIndex;
+               }
+
+               loadedAFile = true;
+               continueOn = false;
+            }
+            else // Unable to open the file
+               writeWithPause(1, console.screen_rows, "yhUnable to open the file!", 1500);
+         }
+         else // Could not find the correct case for the file (it doesn't exist?)
+            writeWithPause(1, console.screen_rows, "yhUnable to locate the file!", 1500);
+      }
+   }
+
+   // Refresh the help line on the bottom of the screen
+   fpDisplayBottomHelpLine(console.screen_rows, gUseQuotes);
+
+   // If we loaded a file, then refresh the message text.
+   if (loadedAFile)
+   {
+      // Insert a blank line into gEditLines so that the user ends up on a new
+      // blank line.
+      //displayEditLines(pScreenLine, pArrayIndex, pEndScreenRow, pClearRemainingScreenRows)
+      // Figure out the index to start at in gEditLines
+      var startIndex = 0;
+      if (gEditLines.length > gEditHeight)
+         startIndex = gEditLines.length - gEditHeight;
+      // Refresh the message on the screen
+      displayEditLines(gEditTop, startIndex, gEditBottom, true, true);
+
+      // Set up the edit lines & text line index for the last line, and
+      // place the cursor at the beginning of the last edit line.
+      // If the last line is short enough, place the cursor at the end
+      // of it.  Otherwise, append a new line and place the cursor there.
+      if (gEditLines[gEditLinesIndex].length() < gEditWidth-1)
+      {
+         gEditLinesIndex = gEditLines.length - 1;
+         gTextLineIndex = gEditLines[gEditLinesIndex].length();
+         retObj.x = gEditLeft + gTextLineIndex;
+         retObj.y = gEditBottom;
+         if (gEditLines.length < gEditHeight)
+            retObj.y = gEditTop + gEditLines.length - 1;
+         retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+      }
+      else
+      {
+         // Append a new line and place the cursor there
+         gEditLines.push(new TextLine());
+         gEditLinesIndex = gEditLines.length - 1;
+         gTextLineIndex = 0;
+         retObj.x = gEditLeft;
+         retObj.y = gEditBottom;
+         if (gEditLines.length < gEditHeight)
+            retObj.y = gEditTop + gEditLines.length - 1;
+         retObj.currentWordLength = 0;
+      }
+   }
+
+   // Make sure the cursor is where it's supposed to be.
+   console.gotoxy(retObj.x, retObj.y);
+
+   return retObj;
+}
+
+// This function lets sysops export (save) the current message to
+// a file.
+//
+// Parameters:
+//  pIsSysop: Whether or not the user is the sysop
+function exportToFile(pIsSysop)
+{
+   // Don't let non-sysops do this.
+   if (!pIsSysop)
+      return;
+
+   // Go to the last row on the screen and prompt the user for a filename
+   var promptText = "ncFile:h";
+   var promptTextLen = strip_ctrl(promptText).length;
+   console.gotoxy(1, console.screen_rows);
+   console.cleartoeol("n");
+   console.print(promptText);
+   var filename = console.getstr(console.screen_columns-promptTextLen-1, K_NOCRLF);
+   if (filename != "")
+   {
+      var outFile = new File(filename);
+      if (outFile.open("w"))
+      {
+         const lastLineIndex = gEditLines.length - 1;
+         for (var i = 0; i < gEditLines.length; ++i)
+         {
+            // Use writeln to write all lines with CRLF except the last line.
+            if (i < lastLineIndex)
+               outFile.writeln(gEditLines[i].text);
+            else
+               outFile.write(gEditLines[i].text);
+         }
+         outFile.close();
+         writeWithPause(1, console.screen_rows, "mhMessage exported.", 1500);
+      }
+      else // Could not open the file for writing
+         writeWithPause(1, console.screen_rows, "yhUnable to open the file for writing!", 1500);
+   }
+   else // No filename specified
+      writeWithPause(1, console.screen_rows, "mhMessage not exported.", 1500);
+
+   // Refresh the help line on the bottom of the screen
+   fpDisplayBottomHelpLine(console.screen_rows, gUseQuotes);
+}
+
+// Performs a text search.
+//
+// Parameters:
+//  pCurpos: The current cursor position (with x and y properties)
+//
+// Return value: An object containing the following properties:
+//               x: The horizontal component of the cursor position
+//               y: The vertical component of the cursor position
+function findText(pCurpos)
+{
+   // Create the return object.
+   var returnObj = new Object();
+   returnObj.x = pCurpos.x;
+   returnObj.y = pCurpos.y;
+
+   // This function makes use of the following "static" variables:
+   //  lastSearchText: The text searched for last
+   //  searchStartIndex: The starting index for gEditLines that should
+   //                    be used for the search
+   if (typeof(findText.lastSearchText) == "undefined")
+      findText.lastSearchText = "";
+   if (typeof(findText.searchStartIndex) == "undefined")
+      findText.searchStartIndex = 0;
+
+   // Go to the last row on the screen and prompt the user for text to find
+   var promptText = "ncText:h";
+   var promptTextLen = strip_ctrl(promptText).length;
+   console.gotoxy(1, console.screen_rows);
+   console.cleartoeol("n");
+   console.print(promptText);
+   var searchText = console.getstr(console.screen_columns-promptTextLen-1, K_NOCRLF);
+
+   // If the user's search is text is different from last time, then set the
+   // starting gEditLines index to 0.  Also, update the last search text.
+   if (searchText != findText.lastSearchText)
+      findText.searchStartIndex = 0;
+   findText.lastSearchText = searchText;
+
+   // Search for the text.
+   var caseSensitive = false; // Case-sensitive search?
+   var textIndex = 0; // The index of the text in the edit lines
+   if (searchText.length > 0)
+   {
+      // editLinesTopIndex is the index of the line currently displayed
+      // at the top of the edit area, and also the line to be displayed
+      // at the top of the edit area.
+      var editLinesTopIndex = gEditLinesIndex - (pCurpos.y - gEditTop);
+
+      // Look for the text in gEditLines
+      var textFound = false;
+      for (var i = findText.searchStartIndex; i < gEditLines.length; ++i)
+      {
+         if (caseSensitive)
+            textIndex = gEditLines[i].text.indexOf(searchText);
+         else
+            textIndex = gEditLines[i].text.toUpperCase().indexOf(searchText.toUpperCase());
+         // If the text was found in this line, then highlight it and
+         // exit the search loop.
+         if (textIndex > -1)
+         {
+            gTextLineIndex = textIndex;
+            textFound = true;
+
+            // If the line is above or below the edit area, then we'll need
+            // to refresh the edit lines on the screen.  We also need to set
+            // the cursor position to the proper place.
+            returnObj.x = gEditLeft + gTextLineIndex;
+            var refresh = false;
+            if (i < editLinesTopIndex)
+            {
+               // The line is above the edit area.
+               refresh = true;
+               returnObj.y = gEditTop;
+               editLinesTopIndex = i;
+            }
+            else if (i >= editLinesTopIndex + gEditHeight)
+            {
+               // The line is below the edit area.
+               refresh = true;
+               returnObj.y = gEditBottom;
+               editLinesTopIndex = i - gEditHeight + 1;
+            }
+            else
+            {
+               // The line is inside the edit area.
+               returnObj.y = pCurpos.y + (i - gEditLinesIndex);
+            }
+
+            gEditLinesIndex = i;
+
+            if (refresh)
+               displayEditLines(gEditTop, editLinesTopIndex, gEditBottom, true, true);
+
+            // Highlight the found text on the line by briefly displaying it in a
+            // different color.
+            var highlightText = gEditLines[i].text.substr(textIndex, searchText.length);
+            console.gotoxy(returnObj.x, returnObj.y);
+            console.print("nk4" + highlightText);
+            mswait(1500);
+            console.gotoxy(returnObj.x, returnObj.y);
+            //console.print(gTextAttrs + highlightText);
+            console.print(chooseEditColor() + highlightText);
+
+            // The next time the user searches with the same text, we'll want
+            // to start searching at the next line.  Wrap around if necessary.
+            findText.searchStartIndex = i + 1;
+            if (findText.searchStartIndex >= gEditLines.length)
+               findText.searchStartIndex = 0;
+
+            break;
+         }
+      }
+
+      // If the text wasn't found, tell the user.  Also, make sure searchStartIndex
+      // is reset to 0.
+      if (!textFound)
+      {
+         console.gotoxy(1, console.screen_rows);
+         console.cleartoeol("n");
+         console.print("yhThe text wasn't found!");
+         mswait(1500);
+
+         findText.searchStartIndex = 0;
+      }
+   }
+
+   // Refresh the help line on the bottom of the screen
+   fpDisplayBottomHelpLine(console.screen_rows, gUseQuotes);
+
+   // Make sure the cursor is positioned where it should be.
+   console.gotoxy(returnObj.x, returnObj.y);
+
+   return returnObj;
+}
+
+// Returns whether we're in insert mode (if not, we're in overwrite mode).
+function inInsertMode()
+{
+   return (gInsertMode == "INS");
+}
+
+// Returns either the normal edit color (gTextAttrs) or the quote line
+// color (gQuoteLineColor), depending on whether the current edit line
+// is a normal line or a quote line.
+function chooseEditColor()
+{
+   return ("n" + (isQuoteLine(gEditLines, gEditLinesIndex) ? gQuoteLineColor : gTextAttrs));
+}
+
+// This function calculates the row on the screen to stop updating the
+// message text.
+//
+// Parameters:
+//  pY: The topmost row at which we'll start writing
+//  pTopIndex: The topmost index in gEditLines
+//
+// Return value: The row on the screen to stop updating the
+//               message text.
+function calcBottomUpdateRow(pY, pTopIndex)
+{
+   var bottomScreenRow = gEditBottom;
+   // Note: This is designed to return the screen row #
+   // below the last message line.  To return the exact
+   // bottommost screen row, subtract 1 from gEditLines.length-pTopIndex.
+   var bottommost = (pY + (gEditLines.length-pTopIndex));
+   if (bottomScreenRow > bottommost)
+      bottomScreenRow = bottommost;
+   return bottomScreenRow;
+}
+
+// This function updates the time on the screen and puts
+// the cursor back to where it was.
+function updateTime()
+{
+	if (typeof(updateTime.timeStr) == "undefined")
+		updateTime.timeStr = getCurrentTimeStr();
+
+	// If the current time has changed since the last time this
+	// function was called, then update the time on the screen.
+	var currentTime = getCurrentTimeStr();
+	if (currentTime != updateTime.timeStr)
+	{
+		// Get the current cursor position so we can move
+		// the cursor back there when we're done.
+		var curpos = console.getxy();
+		// Display the current time on the screen
+		fpDisplayTime(currentTime);
+		// Make sure the edit color attribute is set.
+		console.print("n" + gTextAttrs);
+		// Move the cursor back to where it was
+		console.gotoxy(curpos);
+		// Update this function's time variable
+		updateTime.timeStr = currentTime;
+	}
+}
+
+// This function lets the user change the text color and is called by doEditLoop().
+//
+// Parameters:
+//  pCurpos: An object containing x and y values representing the
+//           cursor position.
+//  pCurrentWordLength: The length of the current word that has been typed
+//
+// Return value: An object containing the following properties:
+//               x and y: The horizontal and vertical cursor position
+//               timedOut: Whether or not the user input timed out (boolean)
+//               currentWordLength: The length of the current word
+function doColorSelection(pCurpos, pCurrentWordLength)
+{
+   // Create the return object
+   var retObj = new Object();
+	retObj.x = pCurpos.x;
+	retObj.y = pCurpos.y;
+	retObj.timedOut = false;
+	retObj.currentWordLength = pCurrentWordLength;
+
+   // Note: The current text color is stored in gTextAttrs
+
+   var colorSelTopLine = console.screen_rows - 2;
+   var curpos = new Object();
+   curpos.x = 1;
+   curpos.y = colorSelTopLine;
+   console.gotoxy(curpos);
+   console.print("nForeground: whK:nkBlack whR:nrRed whG:ngGreen whY:nyYellow whB:nbBlue whM:nmMagenta whC:ncCyan whW:nwWhite");
+   console.cleartoeol("n");
+   console.crlf();
+   console.print("nBackground: wh0:n" + gTextAttrs + "0Blackn wh1:n" + gTextAttrs + "1Redn wh2:n" + gTextAttrs + "2Greenn wh3:n" + gTextAttrs + "3Yellown wh4:n" + gTextAttrs + "4Bluen wh5:n" + gTextAttrs + "5Magentan wh6:n" + gTextAttrs + "6Cyann wh7:n" + gTextAttrs + "7White");
+   console.cleartoeol("n");
+   console.crlf();
+   console.clearline("n");
+   console.print("Special: whH:n" + gTextAttrs + "hHigh Intensity wI:n" + gTextAttrs + "iBlinking nwhN:nNormal c� nChoose Color: ");
+   var attr = FORE_ATTR;
+   var toggle = true;
+   //var key = console.getkeys("KRGYBMCW01234567HIN").toString(); // Outputs a CR..  bad
+   var key = console.getkey(K_UPPER|K_NOCRLF);
+   switch (key)
+   {
+      // Foreground colors:
+      case 'K': // Black
+      case 'R': // Red
+      case 'G': // Green
+      case 'Y': // Yellow
+      case 'B': // Blue
+      case 'M': // Magenta
+      case 'C': // Cyan
+      case 'W': // White
+         attr = FORE_ATTR;
+         break;
+      // Background colors:
+      case '0': // Black
+      case '1': // Red
+      case '2': // Green
+      case '3': // Yellow
+      case '4': // Blue
+      case '5': // Magenta
+      case '6': // Cyan
+      case '7': // White
+         attr = BKG_ATTR;
+         break;
+      // Special attributes:
+      case 'H': // High intensity
+      case 'I': // Blinking
+         attr = SPECIAL_ATTR;
+         break;
+      case 'N': // Normal
+         gTextAttrs = "N";
+         toggle = false;
+         break;
+      default:
+         toggle = false;
+         break;
+   }
+   if (key != "Q")
+   {
+      if (toggle)
+      {
+         gTextAttrs = toggleAttr(attr, gTextAttrs, key);
+         // TODO: Set the attribute in the current text line
+      }
+   }
+
+
+   // Display the parts of the screen text that we covered up with the
+   // color selection: Message edit lines, bottom border, and bottom help line.
+   displayEditLines(colorSelTopLine, gEditLinesIndex-(gEditBottom-colorSelTopLine),
+                    gEditBottom, true, true);
+   fpDisplayTextAreaBottomBorder(gEditBottom+1, gUseQuotes, gEditLeft, gEditRight,
+                                 gInsertMode, gConfigSettings.allowColorSelection);
+   fpDisplayBottomHelpLine(console.screen_rows, gUseQuotes);
+
+   console.print(gTextAttrs);
+
+   // Move the cursor to where it should be before returning
+   curpos.x = pCurpos.x;
+   curpos.y = pCurpos.y;
+   console.gotoxy(curpos);
+
+   // This code was copied from doQuoteSelection().
+/*
+   // Set up some variables
+   var curpos = new Object();
+   curpos.x = pCurpos.x;
+   curpos.y = pCurpos.y;
+   const quoteWinHeight = 8;
+   // The first and last lines on the screen where quote lines are written
+   const quoteTopScreenRow = console.screen_rows - quoteWinHeight + 2;
+   const quoteBottomScreenRow = console.screen_rows - 2;
+   // Quote window parameters
+   const quoteWinTopScreenRow = quoteTopScreenRow-1;
+   const quoteWinWidth = gEditRight - gEditLeft + 1;
+
+   // Display the top border of the quote window.
+   fpDrawQuoteWindowTopBorder(quoteWinHeight, gEditLeft, gEditRight);
+
+   // Display the remainder of the quote window, with the quote lines in it.
+   displayQuoteWindowLines(gQuoteLinesTopIndex, quoteWinHeight, quoteWinWidth, true, gQuoteLinesIndex);
+
+   // Position the cursor at the currently-selected quote line.
+   var screenLine = quoteTopScreenRow + (gQuoteLinesIndex - gQuoteLinesTopIndex);
+   console.gotoxy(gEditLeft, screenLine);
+
+   // User input loop
+   var quoteLine = getQuoteTextLine(gQuoteLinesIndex, quoteWinWidth);
+   retObj.timedOut = false;
+   var userInput = null;
+   var continueOn = true;
+   while (continueOn)
+   {
+		// Get a key, and time out after 1 minute.
+		userInput = console.inkey(0, 100000);
+		if (userInput == "")
+		{
+			// The input timeout was reached.  Abort.
+			retObj.timedOut = true;
+			continueOn = false;
+			break;
+		}
+
+		// If we got here, that means the user input didn't time out.
+		switch (userInput)
+		{
+         case KEY_UP:
+            // Go up 1 quote line
+            if (gQuoteLinesIndex > 0)
+            {
+               // If the cursor is at the topmost position, then
+               // we need to scroll up 1 line in gQuoteLines.
+               if (screenLine == quoteTopScreenRow)
+               {
+                  --gQuoteLinesIndex;
+                  --gQuoteLinesTopIndex;
+                  quoteLine = getQuoteTextLine(gQuoteLinesIndex, quoteWinWidth);
+                  // Redraw the quote lines in the quote window.
+                  displayQuoteWindowLines(gQuoteLinesIndex, quoteWinHeight, quoteWinWidth,
+                                          true, gQuoteLinesIndex);
+                  // Put the cursor back where it should be.
+                  console.gotoxy(gEditLeft, screenLine);
+               }
+               // If the cursor is below the topmost position, then
+               // we can just go up 1 line.
+               else if (screenLine > quoteTopScreenRow)
+               {
+                  // Write the current quote line using the normal color
+                  // Note: This gets the quote line again using getQuoteTextLine()
+                  // so that the color codes in the line will be correct.
+                  quoteLine = getQuoteTextLine(gQuoteLinesIndex, quoteWinWidth);
+                  console.gotoxy(gEditLeft, screenLine);
+                  printf(gFormatStrWithAttr, gQuoteWinTextColor, quoteLine);
+
+                  // Go up one line and display that quote line in the
+                  // highlighted color.
+                  --screenLine;
+                  --gQuoteLinesIndex;
+                  quoteLine = strip_ctrl(getQuoteTextLine(gQuoteLinesIndex, quoteWinWidth));
+                  console.gotoxy(gEditLeft, screenLine);
+                  printf(gFormatStrWithAttr, gQuoteLineHighlightColor, quoteLine);
+
+                  // Make sure the cursor is where it should be.
+                  console.gotoxy(gEditLeft, screenLine);
+               }
+            }
+            break;
+         case KEY_DOWN:
+            // Go down 1 line in the quote window.
+            var downRetObj = moveDownOneQuoteLine(gQuoteLinesIndex, screenLine,
+                                                  quoteWinHeight, quoteWinWidth,
+                                                  quoteBottomScreenRow);
+            gQuoteLinesIndex = downRetObj.quoteLinesIndex;
+            screenLine = downRetObj.screenLine;
+            quoteLine = downRetObj.quoteLine;
+            break;
+         case KEY_ENTER:
+            // numTimesToMoveDown specifies how many times to move the cursor
+            // down after inserting the quote line into the message.
+            var numTimesToMoveDown = 1;
+
+            // Insert the quote line into gEditLines after the current gEditLines index.
+            var insertedBelow = insertLineIntoMsg(gEditLinesIndex, quoteLine, true, true);
+            if (insertedBelow)
+            {
+               // The cursor will need to be moved down 1 more line.
+               // So, increment numTimesToMoveDown, and set curpos.x
+               // and gTextLineIndex to the beginning of the line.
+               ++numTimesToMoveDown;
+               curpos.x = gEditLeft;
+               gTextLineIndex = 0;
+               retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+            }
+            else
+               retObj.currentWordLength = 0;
+
+            // Refresh the part of the message that needs to be refreshed on the
+            // screen (above the quote window).
+            if (curpos.y < quoteTopScreenRow-1)
+               displayEditLines(curpos.y, gEditLinesIndex, quoteTopScreenRow-2, false, true);
+
+            gEditLinesIndex += numTimesToMoveDown;
+
+            // Go down one line in the quote window.
+            var tempReturnObj = moveDownOneQuoteLine(gQuoteLinesIndex, screenLine,
+                                              quoteWinHeight, quoteWinWidth,
+                                              quoteBottomScreenRow);
+            gQuoteLinesIndex = tempReturnObj.quoteLinesIndex;
+            screenLine = tempReturnObj.screenLine;
+            quoteLine = tempReturnObj.quoteLine;
+
+            // Move the cursor down as specified by numTimesToMoveDown.  If
+            // the cursor is at the bottom of the edit area, then refresh
+            // the message on the screen, scrolled down by one line.
+            for (var i = 0; i < numTimesToMoveDown; ++i)
+            {
+               if (curpos.y == gEditBottom)
+               {
+                  // Refresh the message on the screen, scrolled down by
+                  // one line, but only if this is the last time we're
+                  // doing this (for efficiency).
+                  if (i == numTimesToMoveDown-1)
+                  {
+                     displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+                                      quoteTopScreenRow-2, false, true);
+                  }
+               }
+               else
+                  ++curpos.y;
+            }
+            break;
+         // ESC or CTRL-Q: Stop quoting
+         case KEY_ESC:
+         case CTRL_Q:
+            // Quit out of the input loop (get out of quote mode).
+            continueOn = false;
+            break;
+		}
+   }
+
+   // We've exited quote mode.  Refresh the message text on the screen.  Note:
+   // This will refresh only the quote window portion of the screen if the
+   // cursor row is at or below the top of the quote window, and it will also
+   // refresh the screen if the cursor row is above the quote window.
+   displayEditLines(quoteWinTopScreenRow, gEditLinesIndex-(curpos.y-quoteWinTopScreenRow),
+                    gEditBottom, true, true);
+
+   // Draw the bottom edit border to erase the bottom border of the
+   // quote window.
+   fpDisplayTextAreaBottomBorder(gEditBottom+1, gUseQuotes, gEditLeft, gEditRight,
+                                 gInsertMode, gConfigSettings.allowColorSelection);
+
+   // Make sure the color is correct for editing.
+   //console.print("n" + gTextAttrs);
+   console.print(chooseEditColor());
+   // Put the cursor where it should be.
+   console.gotoxy(curpos);
+*/
+   // Set the settings in the return object, and return it.
+	retObj.x = curpos.x;
+	retObj.y = curpos.y;
+	return retObj;
+}
+
+// Writes a line in the edit lines array
+//
+// Parameters:
+//  pIndex: Integer - The index of the line to write. Required.
+//  pUseColors: Boolean - Whether or not to use the line's colors.
+//              Optional.  If omitted, the colors will be used.
+//  pStart: Integer - The index in the line of where to start.
+//          Optional.  If omitted, 0 will be used.
+//  pLength: Integer - The length to write.  Optional.  If
+//           omitted, the entire line will be written.  <= 0 can be
+//           passed to write the entire string.
+function printEditLine(pIndex, pUseColors, pStart, pLength)
+{
+   if (typeof(pIndex) != "number")
+      return;
+   var useColors = true;
+   var start = 0;
+   var length = -1;
+   if (typeof(pUseColors) == "boolean")
+      useColors = pUseColors;
+   if (typeof(pStart) == "number")
+      start = pStart;
+   if (typeof(pLength) == "number")
+      length = pLength;
+   // Validation of variable values
+   if (pIndex < 0)
+      pIndex = 0;
+   else if (pIndex >= gEditLines.length)
+   {
+      // Before returning, write spaces for the length specified so
+      // that the screen is updated correctly
+      for (var i = 0; i < length; ++i)
+         console.print(" ");
+      return;
+   }
+   if (start < 0)
+      start = 0;
+   else if (start >= gEditLines[pIndex].text.length)
+   {
+      // Before returning, write spaces for the length specified so
+      // that the screen is updated correctly
+      for (var i = 0; i < length; ++i)
+         console.print(" ");
+      return;
+   }
+   //if (length > (gEditLines[pIndex].text.length - start))
+   //   length = gEditLines[pIndex].text.length - start;
+
+   if (useColors)
+   {
+   }
+   else
+   {
+      // Don't use the line colors
+      // Cases where the start index is at the beginning of the line
+      if (start == 0)
+      {
+         // Simplest case: start is 0 and length is negative -
+         // Just print the entire line.
+         if (length <= 0)
+            console.print(gEditLines[pIndex].text);
+         else
+            console.print(gEditLines[pIndex].text.substr(start, length));
+      }
+      else
+      {
+         // Start is > 0
+         if (length <= 0)
+            console.print(gEditLines[pIndex].text.substr(start));
+         else
+            console.print(gEditLines[pIndex].text.substr(start, length));
+      }
+   }
+}
\ No newline at end of file
diff --git a/exec/SlyEdit_DCTStuff.js b/exec/SlyEdit_DCTStuff.js
new file mode 100644
index 0000000000..f6cce88a99
--- /dev/null
+++ b/exec/SlyEdit_DCTStuff.js
@@ -0,0 +1,1432 @@
+/* This contains some DCTEdit-specific functions for the Digital Distortion
+ * Editor which don't rely on any global variables.
+ *
+ * Author: Eric Oulashin (AKA Nightfox)
+ * BBS: Digital Distortion
+ * BBS address: digdist.bbsindex.com
+ *
+ * Date       User              Description
+ * 2009-05-11 Eric Oulashin     Started development
+ * 2009-08-09 Eric Oulashin     More development & testing
+ * 2009-08-22 Eric Oulashin     Version 1.00
+ *                              Initial public release
+ * 2009-12-03 Eric Oulashin     Added support for color schemes.
+ *                              Added readColorConfig().
+ * 2009-12-31 Eric Oulashin     Updated promptYesNo_DCTStyle()
+ *                              so that the return variable,
+ *                              userResponse, defaults to the
+ *                              value of pDefaultYes.
+ * 2010-01-02 Eric Oulashin     Removed abortConfirm_DCTStyle(),
+ *                              since it's no longer used anymore.
+ * 2010-11-19 Eric Oulashin     Updated doDCTMenu() so that when the
+ *                              pDisplayMessageRectangle() function is
+ *                              called, it passes true at the end to
+ *                              tell it to clear extra spaces between
+ *                              the end of the text lines up to the
+ *                              width given.
+ * 2010-11-21 Eric Oulashin     Updated promptYesNo_DCTStyle() so that when the
+ *                              pDisplayMessageRectangle() function is
+ *                              called, it passes true at the end to
+ *                              tell it to clear extra spaces between
+ *                              the end of the text lines up to the
+ *                              width given.
+ * 2011-02-02 Eric Oulashin     Moved the time displaying code into
+ *                              a new function, displayTime_DCTStyle().
+ * 2012-12-21 Eric Oulashin     Removed gStartupPath from the beginning
+ *                              of the theme filename, since the path is
+ *                              now set in ReadSlyEditConfigFile() in
+ *                              SlyEdit_Misc.js.
+ */
+
+load("sbbsdefs.js");
+load(getScriptDir() + "SlyEdit_Misc.js");
+
+// DCTEdit menu item return values
+var DCTMENU_FILE_SAVE = 0;
+var DCTMENU_FILE_ABORT = 1;
+var DCTMENU_FILE_EDIT = 2;
+var DCTMENU_EDIT_INSERT_TOGGLE = 3;
+var DCTMENU_EDIT_FIND_TEXT = 4;
+//var DCTMENU_EDIT_SPELL_CHECKER = 5;
+//var DCTMENU_EDIT_SETUP = 6;
+var DCTMENU_SYSOP_IMPORT_FILE = 7;
+var DCTMENU_SYSOP_EXPORT_FILE = 11;
+var DCTMENU_HELP_COMMAND_LIST = 8;
+var DCTMENU_HELP_GENERAL = 9;
+var DCTMENU_HELP_PROGRAM_INFO = 10;
+
+// Read the color configuration file
+readColorConfig(gConfigSettings.DCTColors.ThemeFilename);
+
+///////////////////////////////////////////////////////////////////////////////////
+// Functions
+
+// This function reads the color configuration for DCT style.
+//
+// Parameters:
+//  pFilename: The name of the color configuration file
+function readColorConfig(pFilename)
+{
+   var colors = readValueSettingConfigFile(pFilename, 512);
+   if (colors != null)
+      gConfigSettings.DCTColors = colors;
+}
+
+// Re-draws the screen, in the style of DCTEdit.
+//
+// Parameters:
+//  pEditLeft: The leftmost column of the edit area
+//  pEditRight: The rightmost column of the edit area
+//  pEditTop: The topmost row of the edit area
+//  pEditBottom: The bottommost row of the edit area
+//  pEditColor: The edit color
+//  pInsertMode: The insert mode ("INS" or "OVR")
+//  pUseQuotes: Whether or not message quoting is enabled
+//  pEditLinesIndex: The index of the message line at the top of the edit area
+//  pDisplayEditLines: The function that displays the edit lines
+function redrawScreen_DCTStyle(pEditLeft, pEditRight, pEditTop, pEditBottom, pEditColor,
+                                pInsertMode, pUseQuotes, pEditLinesIndex, pDisplayEditLines)
+{
+	// Top header
+	// Generate & display the top border line (Note: Generate this
+	// border only once, for efficiency).
+	if (typeof(redrawScreen_DCTStyle.topBorder) == "undefined")
+	{
+      var innerWidth = console.screen_columns - 2;
+      redrawScreen_DCTStyle.topBorder = UPPER_LEFT_SINGLE;
+      for (var i = 0; i < innerWidth; ++i)
+         redrawScreen_DCTStyle.topBorder += HORIZONTAL_SINGLE;
+      redrawScreen_DCTStyle.topBorder += UPPER_RIGHT_SINGLE;
+      redrawScreen_DCTStyle.topBorder = randomTwoColorString(redrawScreen_DCTStyle.topBorder,
+                                                        gConfigSettings.DCTColors.TopBorderColor1,
+                                                        gConfigSettings.DCTColors.TopBorderColor2);
+   }
+   // Print the border line on the screen
+   console.clear();
+   console.print(redrawScreen_DCTStyle.topBorder);
+
+   // Next line
+	// From name
+	var lineNum = 2;
+	console.gotoxy(1, lineNum);
+	// Calculate the width of the from name field: 28 characters, based
+	// on an 80-column screen width.
+	var fieldWidth = (console.screen_columns * (28/80)).toFixed(0);
+	screenText = gFromName.substr(0, fieldWidth);
+	console.print(randomTwoColorString(VERTICAL_SINGLE, gConfigSettings.DCTColors.TopBorderColor1,
+	                                   gConfigSettings.DCTColors.TopBorderColor2) +
+				  " " + gConfigSettings.DCTColors.TopLabelColor + "From " +
+				  gConfigSettings.DCTColors.TopLabelColonColor + ": " +
+				  gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
+				  gConfigSettings.DCTColors.TopFromFillColor + DOT_CHAR + 
+				  gConfigSettings.DCTColors.TopFromColor + screenText +
+				  gConfigSettings.DCTColors.TopFromFillColor);
+	fieldWidth -= (screenText.length+1);
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(DOT_CHAR);
+	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "]");
+
+	// Message area
+	fieldWidth = (console.screen_columns * (27/80)).toFixed(0);
+	screenText = gMsgArea.substr(0, fieldWidth);
+	var startX = console.screen_columns - fieldWidth - 9;
+	console.gotoxy(startX, lineNum);
+	console.print(gConfigSettings.DCTColors.TopLabelColor + "Area" +
+	              gConfigSettings.DCTColors.TopLabelColonColor + ": " +
+	              gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
+	              gConfigSettings.DCTColors.TopAreaFillColor + DOT_CHAR +
+	              gConfigSettings.DCTColors.TopAreaColor + screenText +
+	              gConfigSettings.DCTColors.TopAreaFillColor);
+	fieldWidth -= (screenText.length+1);
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(DOT_CHAR);
+	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "] " +
+	              randomTwoColorString(VERTICAL_SINGLE,
+	                                   gConfigSettings.DCTColors.TopBorderColor1,
+	                                   gConfigSettings.DCTColors.TopBorderColor2));
+
+	// Next line: To, Time, time Left
+	++lineNum;
+	console.gotoxy(1, lineNum);
+	// To name
+	fieldWidth = (console.screen_columns * (28/80)).toFixed(0);
+	screenText = gToName.substr(0, fieldWidth);
+	console.print(randomTwoColorString(VERTICAL_SINGLE, gConfigSettings.DCTColors.TopBorderColor1,
+	                                   gConfigSettings.DCTColors.TopBorderColor2) +
+				  " " + gConfigSettings.DCTColors.TopLabelColor + "To   " +
+				  gConfigSettings.DCTColors.TopLabelColonColor + ": " +
+				  gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
+				  gConfigSettings.DCTColors.TopToFillColor + DOT_CHAR +
+				  gConfigSettings.DCTColors.TopToColor + screenText +
+				  gConfigSettings.DCTColors.TopToFillColor);
+	fieldWidth -= (screenText.length+1);
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(DOT_CHAR);
+	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "]");
+
+	// Current time
+	console.gotoxy(startX, lineNum);
+	console.print(gConfigSettings.DCTColors.TopLabelColor + "Time" +
+	              gConfigSettings.DCTColors.TopLabelColonColor + ": " +
+	              gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
+	              gConfigSettings.DCTColors.TopTimeFillColor + DOT_CHAR);
+	displayTime_DCTStyle();
+	console.print(gConfigSettings.DCTColors.TopTimeFillColor + DOT_CHAR +
+	              gConfigSettings.DCTColors.TopInfoBracketColor + "]");
+
+	// Time left
+	fieldWidth = (console.screen_columns * (7/80)).toFixed(0);
+	startX = console.screen_columns - fieldWidth - 9;
+	console.gotoxy(startX, lineNum);
+	var timeStr = Math.floor(bbs.time_left / 60).toString();
+	console.print(gConfigSettings.DCTColors.TopLabelColor + "Left" +
+	              gConfigSettings.DCTColors.TopLabelColonColor + ": " +
+	              gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
+	              gConfigSettings.DCTColors.TopTimeLeftFillColor + DOT_CHAR +
+	              gConfigSettings.DCTColors.TopTimeLeftColor + timeStr +
+	              gConfigSettings.DCTColors.TopTimeLeftFillColor);
+	fieldWidth -= (timeStr.length+1);
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(DOT_CHAR);
+	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "] " +
+	              randomTwoColorString(VERTICAL_SINGLE,
+	              gConfigSettings.DCTColors.TopBorderColor1,
+	              gConfigSettings.DCTColors.TopBorderColor2));
+
+	// Line 3: Subject
+	++lineNum;
+	console.gotoxy(1, lineNum);
+	//fieldWidth = (console.screen_columns * (66/80)).toFixed(0);
+	fieldWidth = console.screen_columns - 13;
+	screenText = gMsgSubj.substr(0, fieldWidth);
+	console.print(randomTwoColorString(LOWER_LEFT_SINGLE, gConfigSettings.DCTColors.TopBorderColor1,
+	                                   gConfigSettings.DCTColors.TopBorderColor1) +
+                 " " + gConfigSettings.DCTColors.TopLabelColor + "Subj " +
+                 gConfigSettings.DCTColors.TopLabelColonColor + ": " +
+				     gConfigSettings.DCTColors.TopInfoBracketColor + "[" +
+				     gConfigSettings.DCTColors.TopSubjFillColor + DOT_CHAR +
+				     gConfigSettings.DCTColors.TopSubjColor + screenText +
+				     gConfigSettings.DCTColors.TopSubjFillColor);
+	fieldWidth -= (screenText.length+1);
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(DOT_CHAR);
+	console.print(gConfigSettings.DCTColors.TopInfoBracketColor + "] " +
+	              randomTwoColorString(LOWER_RIGHT_SINGLE,
+	              gConfigSettings.DCTColors.TopBorderColor1,
+	              gConfigSettings.DCTColors.TopBorderColor2));
+	
+	// Line 4: Top border for message area
+	++lineNum;
+	displayTextAreaTopBorder_DCTStyle(lineNum, pEditLeft, pEditRight);
+	// If the screen is at least 82 characters wide, display horizontal
+	// lines around the message editing area.
+	if (console.screen_columns >= 82)
+	{
+		for (lineNum = pEditTop; lineNum <= pEditBottom; ++lineNum)
+		{
+			console.gotoxy(pEditLeft-1, lineNum);
+			console.print(randomTwoColorString(VERTICAL_SINGLE,
+			                                   gConfigSettings.DCTColors.EditAreaBorderColor1,
+			                                   gConfigSettings.DCTColors.EditAreaBorderColor2));
+			console.gotoxy(pEditRight+1, lineNum);
+			console.print(randomTwoColorString(VERTICAL_SINGLE,
+			                                   gConfigSettings.DCTColors.EditAreaBorderColor1,
+			                                   gConfigSettings.DCTColors.EditAreaBorderColor2));
+		}
+	}
+
+	// Display the bottom message area border and help line
+	DisplayTextAreaBottomBorder_DCTStyle(pEditBottom+1, null, pEditLeft, pEditRight, pInsertMode);
+	DisplayBottomHelpLine_DCTStyle(console.screen_rows, pUseQuotes);
+	
+	// Go to the start of the edit area
+	console.gotoxy(pEditLeft, pEditTop);
+
+	// Write the message text that has been entered thus far.
+	pDisplayEditLines(pEditTop, pEditLinesIndex);
+	console.print(pEditColor);
+}
+
+// Displays the top border of the message area, in the style of DCTEdit.
+//
+// Parameters:
+//  pLineNum: The line number on the screen at which to draw the border
+//  pEditLeft: The leftmost edit area column on the screen
+//  pEditRight: The rightmost edit area column on the screen
+function displayTextAreaTopBorder_DCTStyle(pLineNum, pEditLeft, pEditRight)
+{
+   // The border will use random bright/normal colors.  The colors
+   // should stay the same each time we draw it, so a "static"
+   // variable is used for the border text.  If that variable has
+   // not been defined yet, then build it.
+   if (typeof(displayTextAreaTopBorder_DCTStyle.border) == "undefined")
+   {
+      var numHorizontalChars = pEditRight - pEditLeft - 1;
+      if (console.screen_columns >= 82)
+         numHorizontalChars += 2;
+
+      displayTextAreaTopBorder_DCTStyle.border = UPPER_LEFT_SINGLE;
+      for (var i = 0; i < numHorizontalChars; ++i)
+         displayTextAreaTopBorder_DCTStyle.border += HORIZONTAL_SINGLE;
+      displayTextAreaTopBorder_DCTStyle.border += UPPER_RIGHT_SINGLE;
+      displayTextAreaTopBorder_DCTStyle.border =
+               randomTwoColorString(displayTextAreaTopBorder_DCTStyle.border,
+                                    gConfigSettings.DCTColors.EditAreaBorderColor1,
+                                    gConfigSettings.DCTColors.EditAreaBorderColor2);
+   }
+
+	// Draw the line on the screen
+	console.gotoxy((console.screen_columns >= 82 ? pEditLeft-1 : pEditLeft), pLineNum);
+	console.print(displayTextAreaTopBorder_DCTStyle.border);
+}
+
+// Displays the bottom border of the message area, in the style of DCTEdit.
+//
+// Parameters:
+//  pLineNum: The line number on the screen at which to draw the border
+//  pUseQuotes: This is not used; this is only here so that the signatures of
+//              the IceEdit and DCTEdit versions match.
+//  pEditLeft: The leftmost edit area column on the screen
+//  pEditRight: The rightmost edit area column on the screen
+//  pInsertMode: The insert mode ("INS" or "OVR")
+//  pCanChgMsgColor: Whether or not changing the text color is allowed
+function DisplayTextAreaBottomBorder_DCTStyle(pLineNum, pUseQuotes, pEditLeft, pEditRight,
+                                               pInsertMode, pCanChgMsgColor)
+{
+   // The border will use random bright/normal colors.  The colors
+   // should stay the same each time we draw it, so a "static"
+   // variable is used for the border text.  If that variable has
+   // not been defined yet, then build it.
+   if (typeof(DisplayTextAreaBottomBorder_DCTStyle.border) == "undefined")
+   {
+      var innerWidth = pEditRight - pEditLeft - 1;
+      // If the screen is at least 82 characters wide, add 2 to innerWidth
+      // to make room for the vertical lines around the text area.
+      if (console.screen_columns >= 82)
+         innerWidth += 2;
+
+      DisplayTextAreaBottomBorder_DCTStyle.border = LOWER_LEFT_SINGLE;
+
+      // This loop uses innerWidth-6 to make way for the insert mode
+      // text.
+      for (var i = 0; i < innerWidth-6; ++i)
+         DisplayTextAreaBottomBorder_DCTStyle.border += HORIZONTAL_SINGLE;
+      DisplayTextAreaBottomBorder_DCTStyle.border =
+               randomTwoColorString(DisplayTextAreaBottomBorder_DCTStyle.border,
+                                    gConfigSettings.DCTColors.EditAreaBorderColor1,
+                                    gConfigSettings.DCTColors.EditAreaBorderColor2);
+      // Insert mode
+      DisplayTextAreaBottomBorder_DCTStyle.border += gConfigSettings.DCTColors.EditModeBrackets
+                                                   + "[" + gConfigSettings.DCTColors.EditMode
+                                                   + pInsertMode
+                                                   + gConfigSettings.DCTColors.EditModeBrackets
+                                                   + "]";
+      // The last 2 border characters
+      DisplayTextAreaBottomBorder_DCTStyle.border +=
+                     randomTwoColorString(HORIZONTAL_SINGLE + LOWER_RIGHT_SINGLE,
+                                          gConfigSettings.DCTColors.EditAreaBorderColor1,
+                                          gConfigSettings.DCTColors.EditAreaBorderColor2);
+   }
+
+   // Draw the border line on the screen.
+   console.gotoxy((console.screen_columns >= 82 ? pEditLeft-1 : pEditLeft), pLineNum);
+   console.print(DisplayTextAreaBottomBorder_DCTStyle.border);
+}
+
+// Displays the help line at the bottom of the screen, in the style
+// of DCTEdit.
+//
+// Parameters:
+//  pLineNum: The line number on the screen at which to draw the help line
+//  pUsingQuotes: Boolean - Whether or not message quoting is enabled.
+function DisplayBottomHelpLine_DCTStyle(pLineNum, pUsingQuotes)
+{
+   // For efficiency, define the help line variable only once.
+   if (typeof(DisplayBottomHelpLine_DCTStyle.helpText) == "undefined")
+   {
+      DisplayBottomHelpLine_DCTStyle.helpText = gConfigSettings.DCTColors.BottomHelpBrackets +
+                     "[" + gConfigSettings.DCTColors.BottomHelpKeys + "CTRL" +
+                     gConfigSettings.DCTColors.BottomHelpFill + DOT_CHAR +
+                     gConfigSettings.DCTColors.BottomHelpKeys + "Z" +
+                     gConfigSettings.DCTColors.BottomHelpBrackets + "]n " +
+                     gConfigSettings.DCTColors.BottomHelpKeyDesc + "Saven      " +
+                     gConfigSettings.DCTColors.BottomHelpBrackets + "[" +
+                     gConfigSettings.DCTColors.BottomHelpKeys + "CTRL" +
+                     gConfigSettings.DCTColors.BottomHelpFill + DOT_CHAR +
+                     gConfigSettings.DCTColors.BottomHelpKeys + "A" +
+                     gConfigSettings.DCTColors.BottomHelpBrackets + "]n " +
+                     gConfigSettings.DCTColors.BottomHelpKeyDesc + "Abort";
+      // If we can allow message quoting, then add a text to show Ctrl-Q for
+      // quoting.
+      if (pUsingQuotes)
+         DisplayBottomHelpLine_DCTStyle.helpText += "n      " +
+                          gConfigSettings.DCTColors.BottomHelpBrackets + "[" +
+                          gConfigSettings.DCTColors.BottomHelpKeys + "CTRL" +
+                          gConfigSettings.DCTColors.BottomHelpFill + DOT_CHAR +
+                          gConfigSettings.DCTColors.BottomHelpKeys + "Q" +
+                          gConfigSettings.DCTColors.BottomHelpBrackets + "]n " +
+                          gConfigSettings.DCTColors.BottomHelpKeyDesc + "Quote";
+      DisplayBottomHelpLine_DCTStyle.helpText += "n      " +
+                     gConfigSettings.DCTColors.BottomHelpBrackets + "[" +
+                     gConfigSettings.DCTColors.BottomHelpKeys + "ESC" +
+                     gConfigSettings.DCTColors.BottomHelpBrackets + "]n " +
+                     gConfigSettings.DCTColors.BottomHelpKeyDesc + "Menu";
+      // Center the text by padding it in the front with spaces.  This is done instead
+      // of using console.center() because console.center() will output a newline,
+      // which would not be good on the last line of the screen.
+      var numSpaces = (console.screen_columns/2).toFixed(0)
+                     - (strip_ctrl(DisplayBottomHelpLine_DCTStyle.helpText).length/2).toFixed(0);
+      for (var i = 0; i < numSpaces; ++i)
+         DisplayBottomHelpLine_DCTStyle.helpText = " " + DisplayBottomHelpLine_DCTStyle.helpText;
+   }
+
+   // Display the help line on the screen
+   var lineNum = console.screen_rows;
+	if ((typeof(pLineNum) != "undefined") && (pLineNum != null))
+		lineNum = pLineNum;
+   console.gotoxy(1, lineNum);
+	console.print(DisplayBottomHelpLine_DCTStyle.helpText);
+}
+
+// Updates the insert mode displayd on the screen, for DCT Edit style.
+//
+// Parameters:
+//  pEditRight: The rightmost column on the screen for the edit area
+//  pEditBottom: The bottommost row on the screen for the edit area
+//  pInsertMode: The insert mode ("INS" or "OVR")
+function updateInsertModeOnScreen_DCTStyle(pEditRight, pEditBottom, pInsertMode)
+{
+	console.gotoxy(pEditRight-6, pEditBottom+1);
+	console.print(gConfigSettings.DCTColors.EditModeBrackets + "[" +
+	              gConfigSettings.DCTColors.EditMode + pInsertMode +
+	              gConfigSettings.DCTColors.EditModeBrackets + "]");
+}
+
+// Draws the top border of the quote window, DCT Edit style.
+//
+// Parameters:
+//  pQuoteWinHeight: The height of the quote window
+//  pEditLeft: The leftmost column on the screen for the edit window
+//  pEditRight: The rightmost column on the screen for the edit window
+function DrawQuoteWindowTopBorder_DCTStyle(pQuoteWinHeight, pEditLeft, pEditRight)
+{
+   // Generate the top border vairable only once.
+   if (typeof(DrawQuoteWindowTopBorder_DCTStyle.border) == "undefined")
+   {
+      DrawQuoteWindowTopBorder_DCTStyle.border = gConfigSettings.DCTColors.QuoteWinBorderColor
+                                + UPPER_LEFT_SINGLE + HORIZONTAL_SINGLE + " "
+                                + gConfigSettings.DCTColors.QuoteWinBorderTextColor
+                                + "Quote Window " + gConfigSettings.DCTColors.QuoteWinBorderColor;
+      var curLength = strip_ctrl(DrawQuoteWindowTopBorder_DCTStyle.border).length;
+      var endCol = console.screen_columns-1;
+      for (var i = curLength; i < endCol; ++i)
+         DrawQuoteWindowTopBorder_DCTStyle.border += HORIZONTAL_SINGLE;
+      DrawQuoteWindowTopBorder_DCTStyle.border += UPPER_RIGHT_SINGLE;
+   }
+
+   // Draw the top border line
+   var screenLine = console.screen_rows - pQuoteWinHeight + 1;
+   console.gotoxy(pEditLeft, screenLine);
+   console.print(DrawQuoteWindowTopBorder_DCTStyle.border);
+}
+
+// Draws the bottom border of the quote window, DCT Edit style.  Note:
+// The cursor should be placed at the start of the line (leftmost screen
+// column on the screen line where the border should be drawn).
+//
+// Parameters:
+//  pEditLeft: The leftmost column of the edit area
+//  pEditRight: The rightmost column of the edit area
+function DrawQuoteWindowBottomBorder_DCTStyle(pEditLeft, pEditRight)
+{
+   // Generate the bottom border vairable only once.
+   if (typeof(DrawQuoteWindowBottomBorder_DCTStyle.border) == "undefined")
+   {
+      // Create a string containing the quote help text.
+      var quoteHelpText = " " + gConfigSettings.DCTColors.QuoteWinBorderTextColor
+                         + "[Enter] Quote Line " + gConfigSettings.DCTColors.QuoteWinBorderColor
+                         + " ";
+      for (var i = 0; i < 3; ++i)
+         quoteHelpText += HORIZONTAL_SINGLE;
+      quoteHelpText += " " +  gConfigSettings.DCTColors.QuoteWinBorderTextColor
+                     + "[ESC] Stop Quoting " + gConfigSettings.DCTColors.QuoteWinBorderColor
+                     + " ";
+      for (var i = 0; i < 3; ++i)
+         quoteHelpText += HORIZONTAL_SINGLE;
+      quoteHelpText += " " + gConfigSettings.DCTColors.QuoteWinBorderTextColor
+                     + "[Up/Down] Scroll Lines ";
+
+      // Figure out the starting horizontal position on the screen so that
+      // the quote help text line can be centered.
+      var helpTextStartX = ((console.screen_columns/2) - (strip_ctrl(quoteHelpText).length/2)).toFixed(0);
+
+      // Start creating DrawQuoteWindowBottomBorder_DCTStyle.border with the
+      // bottom border lines, up until helpTextStartX.
+      DrawQuoteWindowBottomBorder_DCTStyle.border = gConfigSettings.DCTColors.QuoteWinBorderColor
+                                                  + LOWER_LEFT_SINGLE;
+      for (var XPos = pEditLeft+2; XPos < helpTextStartX; ++XPos)
+         DrawQuoteWindowBottomBorder_DCTStyle.border += HORIZONTAL_SINGLE;
+      // Add the help text, then display the rest of the bottom border characters.
+      DrawQuoteWindowBottomBorder_DCTStyle.border += quoteHelpText
+                                                   + gConfigSettings.DCTColors.QuoteWinBorderColor;
+      for (var XPos = pEditLeft+2+strip_ctrl(quoteHelpText).length; XPos <= pEditRight-1; ++XPos)
+         DrawQuoteWindowBottomBorder_DCTStyle.border += HORIZONTAL_SINGLE;
+      DrawQuoteWindowBottomBorder_DCTStyle.border += LOWER_RIGHT_SINGLE;
+   }
+
+   // Print the border text on the screen
+   console.print(DrawQuoteWindowBottomBorder_DCTStyle.border);
+}
+
+// Prompts the user for a yes/no question, DCTEdit-style.
+//
+// Parameters:
+//  pQuestion: The question to ask, without the trailing "?"
+//  pBoxTitle: The text to appear in the top border of the question box
+//  pDefaultYes: Whether to default to "yes" (true/false).  If false, this
+//               will default to "no".
+//  pParamObj: An object containing the following members:
+//   displayMessageRectangle: The function used for re-drawing the portion of the
+//                            message on the screen when the yes/no box is no longer
+//                            needed
+//   editTop: The topmost row of the edit area
+//   editBottom: The bottom row of the edit area
+//   editWidth: The edit area width
+//   editHeight: The edit area height
+//   editLinesIndex: The current index into the edit lines array
+function promptYesNo_DCTStyle(pQuestion, pBoxTitle, pDefaultYes, pParamObj)
+{
+   var userResponse = pDefaultYes;
+
+   // Get the current cursor position, so that we can place the cursor
+   // back there later.
+   const originalCurpos = console.getxy();
+
+   // Set up the abort confirmation box dimensions, and calculate
+   // where on the screen to display it.
+   //const boxWidth = 27;
+   const boxWidth = pQuestion.length + 14;
+   const boxHeight = 6;
+   const boxX = (console.screen_columns/2).toFixed(0) - (boxWidth/2).toFixed(0);
+   const boxY = +pParamObj.editTop + +(pParamObj.editHeight/2).toFixed(0) - +(boxHeight/2).toFixed(0);
+   const innerBoxWidth = boxWidth - 2;
+
+   // Display the question box
+   // Upper-left corner, 1 horizontal line, and "Abort" text
+   console.gotoxy(boxX, boxY);
+   console.print(gConfigSettings.DCTColors.TextBoxBorder + UPPER_LEFT_SINGLE +
+                 HORIZONTAL_SINGLE + " " + gConfigSettings.DCTColors.TextBoxBorderText +
+                 pBoxTitle + gConfigSettings.DCTColors.TextBoxBorder + " ");
+   // Remaining top box border
+   for (var i = pBoxTitle.length + 5; i < boxWidth; ++i)
+      console.print(HORIZONTAL_SINGLE);
+   console.print(UPPER_RIGHT_SINGLE);
+   // Inner box: Blank
+   var endScreenLine = boxY + boxHeight - 2;
+   for (var screenLine = boxY+1; screenLine < endScreenLine; ++screenLine)
+   {
+      console.gotoxy(boxX, screenLine);
+      console.print(VERTICAL_SINGLE);
+      for (var i = 0; i < innerBoxWidth; ++i)
+         console.print(" ");
+      console.print(VERTICAL_SINGLE);
+   }
+   // Bottom box border
+   console.gotoxy(boxX, screenLine);
+   console.print(LOWER_LEFT_SINGLE);
+   for (var i = 0; i < innerBoxWidth; ++i)
+      console.print(HORIZONTAL_SINGLE);
+   console.print(LOWER_RIGHT_SINGLE);
+
+   // Prompt the user whether or not to abort: Move the cursor to
+   // the proper location on the screen, output the propmt text,
+   // and get user input.
+   console.gotoxy(boxX+3, boxY+2);
+   console.print(gConfigSettings.DCTColors.TextBoxInnerText + pQuestion + "?  " +
+                 gConfigSettings.DCTColors.YesNoBoxBrackets + "[" +
+                 gConfigSettings.DCTColors.YesNoBoxYesNoText);
+   // Default to yes/no, depending on the value of pDefaultYes.
+   if (pDefaultYes)
+   {
+      console.print("Yes");
+      userResponse = true;
+   }
+   else
+   {
+      console.print("No ");
+      userResponse = false;
+   }
+   console.print(gConfigSettings.DCTColors.YesNoBoxBrackets + "]");
+
+   // Input loop
+   var userInput = "";
+   var continueOn = true;
+   while (continueOn)
+   {
+      // Move the cursor where it needs to be to write the "Yes"
+      // or "No"
+      console.gotoxy(boxX+20, boxY+2);
+      // Get a key, (time out after 1 minute), and take appropriate action.
+		  userInput = console.inkey(0, 100000).toUpperCase();
+      if (userInput == KEY_ENTER)
+         continueOn = false;
+      else if (userInput == "Y")
+      {
+         userResponse = true;
+         continueOn = false;
+      }
+      else if ((userInput == "N") || (userInput == KEY_ESC))
+      {
+         userResponse = false;
+         continueOn = false;
+      }
+      // Arrow keys: Toggle back and forth
+      else if ((userInput == KEY_UP) || (userInput == KEY_DOWN) ||
+                (userInput == KEY_LEFT) || (userInput == KEY_RIGHT))
+      {
+         // Toggle the userResponse variable
+         userResponse = !userResponse;
+         // Write "Yes" or "No", depending on the userResponse variable
+         if (userResponse)
+         {
+            console.print(gConfigSettings.DCTColors.YesNoBoxYesNoText + "Yes" +
+                          gConfigSettings.DCTColors.YesNoBoxBrackets + "]");
+         }
+         else
+         {
+            console.print(gConfigSettings.DCTColors.YesNoBoxYesNoText + "No " +
+                          gConfigSettings.DCTColors.YesNoBoxBrackets + "]");
+         }
+      }
+   }
+
+   // If the user chose not to abort, then erase the confirmation box and
+   // put the cursor back where it originally was.
+   if (!userResponse)
+   {
+      // Calculate the difference in the edit lines index we'll need to use
+      // to erase the confirmation box.  This will be the difference between
+      // the cursor position between boxY and the current cursor row.
+      const editLinesIndexDiff = boxY - originalCurpos.y;
+      pParamObj.displayMessageRectangle(boxX, boxY, boxWidth, boxHeight,
+                                        pParamObj.editLinesIndex + editLinesIndexDiff,
+                                        true);
+      // Put the cursor back where it was
+      console.gotoxy(originalCurpos);
+   }
+
+   return userResponse;
+}
+
+// Displays the time on the screen.
+//
+// Parameters:
+//  pTimeStr: The time to display (optional).  If this is omitted,
+//            then this funtion will get the current time.
+function displayTime_DCTStyle(pTimeStr)
+{
+	console.gotoxy(52, 3);
+	if (pTimeStr == null)
+		console.print(gConfigSettings.DCTColors.TopTimeColor + getCurrentTimeStr());
+	else
+		console.print(gConfigSettings.DCTColors.TopTimeColor + pTimeStr);
+}
+
+// Displays & handles the input loop for the DCT Edit menu.
+//
+// Parameters:
+//  pEditLeft: The leftmost column of the edit area
+//  pEditRight: The rightmost column of the edit area
+//  pEditTop: The topmost row of the edit area
+//  pDisplayMessageRectangle: The function for drawing part of the
+//                            edit text on the screen.  This is used
+//                            for refreshing the screen after a menu
+//                            box disappears.
+//  pEditLinesIndex: The current index into the edit lines array.  This
+//                   is is required to pass to the pDisplayMessageRectangle
+//                   function.
+//  pEditLineDiff: The difference between the current edit line and the top of
+//                 the edit area.
+//  pIsSysop: Whether or not the user is a sysop.
+//
+// Return value: An object containing the following properties:
+//               userInput: The user's input from the menu loop.
+//               returnVal: The return code from the menu.
+function doDCTMenu(pEditLeft, pEditRight, pEditTop, pDisplayMessageRectangle,
+                    pEditLinesIndex, pEditLineDiff, pIsSysop)
+{
+   // This function displays the top menu options, with a given one highlighted.
+   //
+   // Parameters:
+   //  pItemPositions: An object containing the menu item positions.
+   //  pHighlightedItemNum: The index (0-based) of the menu item to be highlighted.
+   //  pMenus: An array containing the menus
+   //  pIsSysop: Whether or not the user is the sysop.
+   //
+   // Return value: The user's last keypress during the menu's input loop.
+   function displayTopMenuItems(pItemPositions, pHighlightedItemNum, pMenus, pIsSysop)
+   {
+      // File
+      console.gotoxy(pItemPositions.fileX, pItemPositions.mainMenuY);
+      if (pHighlightedItemNum == 0)
+      {
+         console.print(gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_RIGHT +
+                       gConfigSettings.DCTColors.SelectedMenuLabelText + "File" +
+                       gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_LEFT);
+      }
+      else
+      {
+         console.print("n " + gConfigSettings.DCTColors.UnselectedMenuLabelText + "File" +
+                       "n ");
+      }
+      // Edit
+      console.gotoxy(pItemPositions.editX, pItemPositions.mainMenuY);
+      if (pHighlightedItemNum == 1)
+      {
+         console.print(gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_RIGHT +
+                       gConfigSettings.DCTColors.SelectedMenuLabelText + "Edit" +
+                       gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_LEFT);
+      }
+      else
+      {
+         console.print("n " + gConfigSettings.DCTColors.UnselectedMenuLabelText + "Edit" +
+                       "n ");
+      }
+      // SysOp
+      if (pIsSysop)
+      {
+         console.gotoxy(pItemPositions.sysopX, pItemPositions.mainMenuY);
+         if (pHighlightedItemNum == 2)
+         {
+            console.print(gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_RIGHT +
+                          gConfigSettings.DCTColors.SelectedMenuLabelText + "SysOp" +
+                          gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_LEFT);
+         }
+         else
+         {
+            console.print("n " + gConfigSettings.DCTColors.UnselectedMenuLabelText + "SysOp" +
+                          "n ");
+         }
+      }
+      // Help
+      console.gotoxy(pItemPositions.helpX, pItemPositions.mainMenuY);
+      if (pHighlightedItemNum == 3)
+      {
+         console.print(gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_RIGHT +
+                       gConfigSettings.DCTColors.SelectedMenuLabelText + "Help" +
+                       gConfigSettings.DCTColors.SelectedMenuLabelBorders + THIN_RECTANGLE_LEFT);
+      }
+      else
+      {
+         console.print("n " + gConfigSettings.DCTColors.UnselectedMenuLabelText + "Help" +
+                       "n ");
+      }
+
+      // Display the menu (and capture the return object so that we can
+      // also return it here).
+      var returnObj = pMenus[pHighlightedItemNum].doInputLoop();
+
+      // Refresh the part of the edit text on the screen where the menu was.
+      pDisplayMessageRectangle(pMenus[pHighlightedItemNum].topLeftX,
+                               pMenus[pHighlightedItemNum].topLeftY,
+                               pMenus[pHighlightedItemNum].width,
+                               pMenus[pHighlightedItemNum].height,
+                               pEditLinesIndex-pEditLineDiff, true);
+
+      return returnObj;
+   }
+
+   // Set up an object containing the menu item positions.
+   // Only create this object once.
+   if (typeof(doDCTMenu.mainMenuItemPositions) == "undefined")
+   {
+      doDCTMenu.mainMenuItemPositions = new Object();
+      // Vertical position on the screen for the main menu items
+      doDCTMenu.mainMenuItemPositions.mainMenuY = pEditTop - 1;
+      // Horizontal position of the "File" text
+      doDCTMenu.mainMenuItemPositions.fileX = gEditLeft + 5;
+      // Horizontal position of the "Edit" text
+      doDCTMenu.mainMenuItemPositions.editX = doDCTMenu.mainMenuItemPositions.fileX + 11;
+      // Horizontal position of the "SysOp" text
+      doDCTMenu.mainMenuItemPositions.sysopX = doDCTMenu.mainMenuItemPositions.editX + 11;
+      // Horizontal position of of the "Help" text
+      if (pIsSysop)
+         doDCTMenu.mainMenuItemPositions.helpX = doDCTMenu.mainMenuItemPositions.sysopX + 12;
+      else
+         doDCTMenu.mainMenuItemPositions.helpX = doDCTMenu.mainMenuItemPositions.sysopX;
+   }
+
+   // Variables for the menu numbers
+   const fileMenuNum = 0;
+   const editMenuNum = 1;
+   const sysopMenuNum = 2;
+   const helpMenuNum = 3;
+
+   // Set up the menu objects.  Only create these objects once.
+   if (typeof(doDCTMenu.allMenus) == "undefined")
+   {
+      doDCTMenu.allMenus = new Array();
+      // File menu
+      doDCTMenu.allMenus[fileMenuNum] = new DCTMenu(doDCTMenu.mainMenuItemPositions.fileX, doDCTMenu.mainMenuItemPositions.mainMenuY+1);
+      doDCTMenu.allMenus[fileMenuNum].addItem("&Save    Ctrl-Z", DCTMENU_FILE_SAVE);
+      doDCTMenu.allMenus[fileMenuNum].addItem("&Abort   Ctrl-A", DCTMENU_FILE_ABORT);
+      doDCTMenu.allMenus[fileMenuNum].addItem("&Edit       ESC", DCTMENU_FILE_EDIT);
+      doDCTMenu.allMenus[fileMenuNum].addExitLoopKey(CTRL_Z, DCTMENU_FILE_SAVE);
+      doDCTMenu.allMenus[fileMenuNum].addExitLoopKey(CTRL_A, DCTMENU_FILE_ABORT);
+      doDCTMenu.allMenus[fileMenuNum].addExitLoopKey(KEY_ESC, DCTMENU_FILE_EDIT);
+
+      // Edit menu
+      doDCTMenu.allMenus[editMenuNum] = new DCTMenu(doDCTMenu.mainMenuItemPositions.editX, doDCTMenu.mainMenuItemPositions.mainMenuY+1);
+      doDCTMenu.allMenus[editMenuNum].addItem("&Insert Mode    Ctrl-I", DCTMENU_EDIT_INSERT_TOGGLE);
+      doDCTMenu.allMenus[editMenuNum].addItem("&Find Text      Ctrl-N", DCTMENU_EDIT_FIND_TEXT);
+      //doDCTMenu.allMenus[editMenuNum].addItem("Spell &Checker  Ctrl-W", DCTMENU_EDIT_SPELL_CHECKER);
+      //doDCTMenu.allMenus[editMenuNum].addItem("&Setup          Ctrl-U", DCTMENU_EDIT_SETUP);
+      doDCTMenu.allMenus[editMenuNum].addExitLoopKey(CTRL_I, DCTMENU_EDIT_INSERT_TOGGLE);
+      doDCTMenu.allMenus[editMenuNum].addExitLoopKey(CTRL_N, DCTMENU_EDIT_FIND_TEXT);
+
+      // SysOp menu
+      doDCTMenu.allMenus[sysopMenuNum] = new DCTMenu(doDCTMenu.mainMenuItemPositions.sysopX, doDCTMenu.mainMenuItemPositions.mainMenuY+1);
+      doDCTMenu.allMenus[sysopMenuNum].addItem("&Import file      Ctrl-O", DCTMENU_SYSOP_IMPORT_FILE);
+      doDCTMenu.allMenus[sysopMenuNum].addItem("E&xport to file   Ctrl-X", DCTMENU_SYSOP_EXPORT_FILE);
+      doDCTMenu.allMenus[sysopMenuNum].addExitLoopKey(CTRL_O, DCTMENU_SYSOP_IMPORT_FILE);
+      doDCTMenu.allMenus[sysopMenuNum].addExitLoopKey(CTRL_X, DCTMENU_SYSOP_EXPORT_FILE);
+
+      // Help menu
+      doDCTMenu.allMenus[helpMenuNum] = new DCTMenu(doDCTMenu.mainMenuItemPositions.helpX, doDCTMenu.mainMenuItemPositions.mainMenuY+1);
+      doDCTMenu.allMenus[helpMenuNum].addItem("&Command List   Ctrl-P", DCTMENU_HELP_COMMAND_LIST);
+      doDCTMenu.allMenus[helpMenuNum].addItem("&General Help   Ctrl-G", DCTMENU_HELP_GENERAL);
+      doDCTMenu.allMenus[helpMenuNum].addItem("&Program Info   Ctrl-R", DCTMENU_HELP_PROGRAM_INFO);
+      doDCTMenu.allMenus[helpMenuNum].addExitLoopKey(CTRL_P, DCTMENU_HELP_COMMAND_LIST);
+      doDCTMenu.allMenus[helpMenuNum].addExitLoopKey(CTRL_G, DCTMENU_HELP_GENERAL);
+      doDCTMenu.allMenus[helpMenuNum].addExitLoopKey(CTRL_R, DCTMENU_HELP_PROGRAM_INFO);
+
+      // For each menu, add KEY_LEFT, KEY_RIGHT, and ESC as loop-exit keys;
+      // also, set the menu colors.
+      for (var i = 0; i < doDCTMenu.allMenus.length; ++i)
+      {
+         doDCTMenu.allMenus[i].addExitLoopKey(KEY_LEFT);
+         doDCTMenu.allMenus[i].addExitLoopKey(KEY_RIGHT);
+         doDCTMenu.allMenus[i].addExitLoopKey(KEY_ESC);
+
+         doDCTMenu.allMenus[i].colors.border = gConfigSettings.DCTColors.MenuBorders;
+         doDCTMenu.allMenus[i].colors.selected = gConfigSettings.DCTColors.MenuSelectedItems;
+         doDCTMenu.allMenus[i].colors.unselected = gConfigSettings.DCTColors.MenuUnselectedItems;
+         doDCTMenu.allMenus[i].colors.hotkey = gConfigSettings.DCTColors.MenuHotkeys;
+      }
+   }
+
+   // Boolean variables to keep track of which menus were displayed
+   // (for refresh purposes later)
+   var fileMenuDisplayed = false;
+   var editMenuDisplayed = false;
+   var sysopMenuDisplayed = false;
+   var helpMenuDisplayed = false;
+
+   // Display the top menu options with "File" highlighted.
+   var menuNum = fileMenuNum; // 0: File, ..., 3: Help
+   var subMenuItemNum = 0;
+   var menuRetObj = displayTopMenuItems(doDCTMenu.mainMenuItemPositions, menuNum,
+                                        doDCTMenu.allMenus, pIsSysop);
+   // Input loop
+   var userInput = "";
+   var matchedMenuRetval = false; // Whether one of the menu return values was matched
+   var continueOn = true;
+   while (continueOn)
+   {
+      matchedMenuRetval = valMatchesMenuCode(menuRetObj.returnVal, pIsSysop);
+      // If a menu return value was matched, then set userInput to it.
+      if (matchedMenuRetval)
+         userInput = menuRetObj.returnVal;
+      // If the user's input from the last menu was ESC, left, right, or one of the
+      // characters from the menus, then set userInput to the last menu keypress.
+      else if (inputMatchesMenuSelection(menuRetObj.userInput, pIsSysop))
+         userInput = menuRetObj.userInput;
+      // If nothing from the menu was matched, then get a key from the user.
+      else
+         userInput = console.inkey(K_UPPER, 60000);
+      menuRetObj.userInput = "";
+
+		// If a menu return code was matched or userInput is blank (the
+		// timeout was hit), then exit out of the loop.
+		if (matchedMenuRetval || (userInput == ""))
+         break;
+
+		// Take appropriate action based on the user's input.
+		switch (userInput)
+		{
+         case KEY_LEFT:
+            if (menuNum == 0)
+               menuNum = 3;
+            else
+               --menuNum;
+            // Don't allow the sysop menu for non-sysops.
+            if ((menuNum == sysopMenuNum) && !pIsSysop)
+               --menuNum;
+            subMenuItemNum = 0;
+            menuRetObj = displayTopMenuItems(doDCTMenu.mainMenuItemPositions, menuNum, doDCTMenu.allMenus, pIsSysop);
+            break;
+         case KEY_RIGHT:
+            if (menuNum == 3)
+               menuNum = 0;
+            else
+               ++menuNum;
+            // Don't allow the sysop menu for non-sysops.
+            if ((menuNum == sysopMenuNum) && !pIsSysop)
+               ++menuNum;
+            subMenuItemNum = 0;
+            menuRetObj = displayTopMenuItems(doDCTMenu.mainMenuItemPositions, menuNum, doDCTMenu.allMenus, pIsSysop);
+            break;
+         case KEY_UP:
+         case KEY_DOWN:
+            break;
+         case KEY_ENTER: // Selected an item from the menu
+            //displayDebugText(1, 1, "Pressed Enter", null, false); // Temporary
+            // Set userInput to the return code from the menu so that it will
+            // be returned from this function.
+            userInput = menuRetObj.returnVal;
+         case "S":       // Save
+         case CTRL_Z:    // Save
+         case "A":       // Abort
+         case CTRL_A:    // Abort
+         case "I":       // Import file for sysop, or Insert/Overwrite toggle for non-sysop
+         case CTRL_O:    // Import file (for sysop)
+         case "X":       // Export file (for sysop)
+         case CTRL_X:    // Export file (for sysop)
+         case CTRL_V:    // Insert/overwrite toggle
+         case "F":       // Find text
+         case CTRL_F:    // Find text
+         case "C":       // Command List
+         case "G":       // General help
+         case "P":       // Program info
+         case "E":       // Edit the message
+         case KEY_ESC:   // Edit the message
+            continueOn = false;
+            break;
+         default:
+            //displayDebugText(1, 1, "Last key:" + userInput + ":", null, true); // Temporary
+            break;
+		}
+   }
+
+   // We've exited the menu, so refresh the top menu border and the message text
+   // on the screen.
+   displayTextAreaTopBorder_DCTStyle(doDCTMenu.mainMenuItemPositions.mainMenuY, pEditLeft, pEditRight);
+
+   // Return the user's input from the menu loop.
+   return userInput;
+}
+
+// This function returns whether a value matches any of the DCT Edit menu return
+// values.  This is used in doDCTMenu()'s input loop;
+//
+// Parameters:
+//  pVal: The value to test
+//  pIsSysop: Whether or not the user is a sysop
+//
+// Return: Boolean - Whether or not the value matches a DCT Edit menu return value.
+function valMatchesMenuCode(pVal, pIsSysop)
+{
+   var valMatches = false;
+   if (pIsSysop)
+   {
+      valMatches = ((pVal == DCTMENU_FILE_SAVE) || (pVal == DCTMENU_FILE_ABORT) ||
+                   (pVal == DCTMENU_FILE_EDIT) || (pVal == DCTMENU_EDIT_INSERT_TOGGLE) ||
+                   (pVal == DCTMENU_EDIT_FIND_TEXT) || (pVal == DCTMENU_SYSOP_IMPORT_FILE) ||
+                   (pVal == DCTMENU_SYSOP_EXPORT_FILE) || (pVal == DCTMENU_HELP_COMMAND_LIST) ||
+                   (pVal == DCTMENU_HELP_GENERAL) || (pVal == DCTMENU_HELP_PROGRAM_INFO));
+   }
+   else
+   {
+      valMatches = ((pVal == DCTMENU_FILE_SAVE) || (pVal == DCTMENU_FILE_ABORT) ||
+                   (pVal == DCTMENU_FILE_EDIT) || (pVal == DCTMENU_EDIT_INSERT_TOGGLE) ||
+                   (pVal == DCTMENU_EDIT_FIND_TEXT) || (pVal == DCTMENU_HELP_COMMAND_LIST) ||
+                   (pVal == DCTMENU_HELP_GENERAL) || (pVal == DCTMENU_HELP_PROGRAM_INFO));
+   }
+   return valMatches;
+}
+
+// This function returns whether a user input matches a selection from one of the
+// menus.  This is used in doDCTMenu()'s input loop;
+//
+// Parameters:
+//  pInput: The user input to test
+//  pIsSysop: Whether or not the user is a sysop
+function inputMatchesMenuSelection(pInput, pIsSysop)
+{
+   return((pInput == KEY_ESC) || (pInput == KEY_LEFT) ||
+           (pInput == KEY_RIGHT) || (pInput == KEY_ENTER) ||
+           (pInput == "S") || (pInput == "A") || (pInput == "E") ||
+           (pInput == "I") || (pIsSysop && (pInput == "X")) ||
+           (pInput == "F") || (pInput == "C") || (pInput == "G") ||
+           (pInput == "P"));
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// DCTMenu object functions
+
+// Constructs a menu item for a DCT menu
+function DCTMenuItem()
+{
+   this.text = "";        // The item text
+   this.hotkeyIndex = -1; // Index of the hotkey in the text (-1 for no hotkey).
+   this.hotkey = "";      // The shortcut key for the item (blank for no hotkey).
+   this.returnVal = 0;    // Return value for the item
+}
+
+// DCTMenu constructor: Constructs a DCTEdit-style menu.
+//
+// Parameters:
+//  pTopLeftX: The upper-left screen column
+//  pTopLeftY: The upper-left screen row
+function DCTMenu(pTopLeftX, pTopLeftY)
+{
+   this.colors = new Object();
+   // Unselected item colors
+   this.colors.unselected = "n7k";
+   // Selected item colors
+   this.colors.selected = "nw";
+   // Other colors
+   this.colors.hotkey = "hw";
+   this.colors.border = "n7k";
+
+   this.topLeftX = 1; // Upper-left screen column
+   if ((pTopLeftX != null) && (pTopLeftX > 0) && (pTopLeftX <= console.screen_columns))
+      this.topLeftX = pTopLeftX;
+   this.topLeftY = 1; // Upper-left screen row
+   if ((pTopLeftY != null) && (pTopLeftY > 0) && (pTopLeftY <= console.screen_rows))
+      this.topLeftY = pTopLeftY;
+   this.width = 0;
+   this.height = 0;
+   this.selectedItemIndex = 0;
+
+   // this.menuItems will contain DCTMenuItem objects.
+   this.menuItems = new Array();
+   // hotkeyRetvals is an array, indexed by hotkey, that contains
+   // the return values for each hotkey.
+   this.hotkeyRetvals = new Array();
+
+   // exitLoopKeys will contain keys that will exit the input loop
+   // when pressed by the user, so that calling code can catch them.
+   // It's indexed by the key, and the value is the return code that
+   // should be returned for that key.
+   this.exitLoopKeys = new Array();
+
+   // Border style: "single" or "double"
+   this.borderStyle = "single";
+
+   // clearSpaceAroundMenu controls whether or not to clear one space
+   // around the menu when it is drawn.
+   this.clearSpaceAroundMenu = false;
+   // clearSpaceColor controls which color to use when drawing the
+   // clear space around the menu.
+   this.clearSpaceColor = "n";
+   // clearSpaceTopText specifies text to display above the top of the
+   // menu when clearing space around it.
+   this.clearSpaceTopText = "";
+
+   // Timeout (in milliseconds) for the input loop.  Default to 1 minute.
+   this.timeoutMS = 60000;
+
+   // Member functions
+   this.addItem = DCTMenu_AddItem;
+   this.addExitLoopKey = DCTMenu_AddExitLoopKey;
+   this.displayItem = DCTMenu_DisplayItem;
+   this.doInputLoop = DCTMenu_DoInputLoop;
+   this.numItems = DCTMenu_NumItems;
+   this.removeAllItems = DCTMenu_RemoveAllItems;
+}
+// Adds an item to the DCTMenu.
+//
+// Parameters:
+//  pText: The text of the menu item.  Note that a & precedes a hotkey.
+//  pReturnVal: The value to return upon selection of the item
+function DCTMenu_AddItem(pText, pReturnVal)
+{
+   if (pText == "")
+      return;
+
+   var item = new DCTMenuItem();
+   item.returnVal = pReturnVal;
+   // Look for a & in pText, and if one is found, use the next character as
+   // its hotkey.
+   var ampersandIndex = pText.indexOf("&");
+   if (ampersandIndex > -1)
+   {
+      // If pText has text after ampersandIndex, then set up
+      // the next character as the hotkey in the item.
+      if (pText.length > ampersandIndex+1)
+      {
+         item.hotkeyIndex = ampersandIndex;
+         item.hotkey = pText.substr(ampersandIndex+1, 1);
+         // Set the text of the item.  The text should not include
+         // the ampersand.
+         item.text = pText.substr(0, ampersandIndex) + pText.substr(ampersandIndex+1);
+         // Add the hotkey & return value to this.hotkeyRetvals
+         this.hotkeyRetvals[item.hotkey.toUpperCase()] = pReturnVal;
+
+         // If the menu is not wide enough for this item's text, then
+         // update this.width.
+         if (this.width < item.text.length + 2)
+            this.width = item.text.length + 2;
+         // Also update this.height
+         if (this.height == 0)
+            this.height = 3;
+         else
+            ++this.height;
+      }
+      else
+      {
+         // pText does not have text after ampersandIndex.
+         item.text = pText.substr(0, ampersandIndex);
+      }
+   }
+   else
+   {
+      // No ampersand was found in pText.
+      item.text = pText;
+   }
+
+   // Add the item to this.menuItems
+   this.menuItems.push(item);
+}
+// Adds a key that will exit the input loop when pressed by the user.
+//
+// Parameters:
+//  pKey: The key that should cause the input loop to exit
+//  pReturnValue: The return value of the input loop when the key is pressed.
+//                If this is not specified, a default value of -1 will be used.
+function DCTMenu_AddExitLoopKey(pKey, pReturnValue)
+{
+   var val = -1;
+   if ((pReturnValue != null) && (typeof(pReturnValue) != "undefined"))
+      val = pReturnValue;
+
+   this.exitLoopKeys[pKey] = val;
+}
+// Displays an item on the menu.
+//
+// Parameters:
+//  pItemIndex: The index of the item in the menuItems array
+//  pPrintBorders: Boolean - Whether or not to display the horizontal
+//                 borders on each side of the menu item text.
+function DCTMenu_DisplayItem(pItemIndex, pPrintBorders)
+{
+   var printBorders = false;
+   if (pPrintBorders != null)
+      printBorders = pPrintBorders;
+
+   // Determine whether to use the selected item color or unselected
+   // item color.
+   var itemColor = "";
+   if (pItemIndex == this.selectedItemIndex)
+      itemColor = this.colors.selected;
+   else
+      itemColor = this.colors.unselected;
+
+   // Print the menu item text on the screen.
+   if (printBorders)
+   {
+      console.gotoxy(this.topLeftX, this.topLeftY + pItemIndex + 1);
+      if (this.borderStyle == "single")
+         console.print(this.colors.border + VERTICAL_SINGLE);
+      else if (this.borderStyle == "double")
+         console.print(this.colors.border + VERTICAL_DOUBLE);
+   }
+   else
+      console.gotoxy(this.topLeftX + 1, this.topLeftY + pItemIndex + 1);
+   // If the menu item has a hotkey, then write the appropriate character
+   // in the hotkey color.
+   if (this.menuItems[pItemIndex].hotkeyIndex > -1)
+   {
+      console.print(itemColor +
+                    this.menuItems[pItemIndex].text.substr(0, this.menuItems[pItemIndex].hotkeyIndex) +
+                    this.colors.hotkey + this.menuItems[pItemIndex].hotkey + itemColor +
+                    this.menuItems[pItemIndex].text.substr(this.menuItems[pItemIndex].hotkeyIndex + 1));
+   }
+   else
+   {
+      console.print(itemColor + this.menuItems[pItemIndex].text);
+   }
+   // If the item text isn't wide enough to fill the entire inner width, then
+   // clear the line up until the right border.
+   var innerWidth = this.width - 2;
+   if (this.menuItems[pItemIndex].text.length < innerWidth)
+   {
+      for (var i = this.menuItems[pItemIndex].text.length; i < innerWidth; ++i)
+         console.print(" ");
+   }
+   // Print the right border character if specified.
+   if (printBorders)
+   {
+      if (this.borderStyle == "single")
+         console.print(this.colors.border + VERTICAL_SINGLE);
+      else if (this.borderStyle == "double")
+         console.print(this.colors.border + VERTICAL_DOUBLE);
+   }
+}
+// Displays the DCT menu and enters the input loop.
+//
+// Return value: An object containing the following properties:
+//  returnVal: The return code of the item selected, or -1 if no
+//             item was selected.
+//  userInput: The last user input
+function DCTMenu_DoInputLoop()
+{
+   var returnObj = new Object();
+   returnObj.returnVal = -1;
+   returnObj.userInput = "";
+
+   // If clearSpaceAroundMenu is true, then draw a blank row
+   // above the menu.
+   if (this.clearSpaceAroundMenu && (this.topLeftY > 1))
+   {
+      // If there is room, output a space to the left, diagonal
+      // from the top-left corner of the menu.
+      if (this.topLeftX > 1)
+      {
+         console.gotoxy(this.topLeftX-1, this.topLeftY-1);
+         console.print(this.clearSpaceColor + " ");
+      }
+      else
+         console.gotoxy(this.topLeftX, this.topLeftY-1);
+
+      // Output this.clearSpaceTopText
+      console.print(this.clearSpaceTopText);
+      // Output the rest of the blank space
+      var textLen = strip_ctrl(this.clearSpaceTopText).length;
+      if (textLen < this.width)
+      {
+         var numSpaces = this.width - textLen;
+         if (this.topLeftX + this.width < console.screen_columns)
+            ++numSpaces;
+         for (var i = 0; i < numSpaces; ++i)
+            console.print(this.clearSpaceColor + " ");
+      }
+   }
+
+   // Before drawing the top border, if clearSpaceAroundMenu is
+   // true, put space before the border.
+   if (this.clearSpaceAroundMenu && (this.topLeftY > 1))
+   {
+      console.gotoxy(this.topLeftX-1, this.topLeftY);
+      console.print(this.clearSpaceColor + " ");
+   }
+   else
+      console.gotoxy(this.topLeftX, this.topLeftY);
+   // Draw the top border
+   var innerWidth = this.width - 2;
+   if (this.borderStyle == "single")
+   {
+      console.print(this.colors.border + UPPER_LEFT_SINGLE);
+      for (var i = 0; i < innerWidth; ++i)
+         console.print(HORIZONTAL_SINGLE);
+      console.print(this.colors.border + UPPER_RIGHT_SINGLE);
+   }
+   else if (this.borderStyle == "double")
+   {
+      console.print(this.colors.border + UPPER_LEFT_DOUBLE);
+      for (var i = 0; i < innerWidth; ++i)
+         console.print(HORIZONTAL_DOUBLE);
+      console.print(this.colors.border + UPPER_RIGHT_DOUBLE);
+   }
+   // If clearSpaceAroundMenu is true, then put a space after the border.
+   if (this.clearSpaceAroundMenu && (this.topLeftX + this.width < console.screen_columns))
+      console.print(this.clearSpaceColor + " ");
+
+   // Print the menu items (and side spaces outside the menu if
+   // clearSpaceAroundMenu is true).
+   var itemColor = "";
+   for (var i = 0; i < this.menuItems.length; ++i)
+   {
+      if (this.clearSpaceAroundMenu && (this.topLeftX > 1))
+      {
+         console.gotoxy(this.topLeftX-1, this.topLeftY + i + 1);
+         console.print(this.clearSpaceColor + " ");
+      }
+
+      this.displayItem(i, true);
+
+      if (this.clearSpaceAroundMenu && (this.topLeftX + this.width < console.screen_columns))
+      {
+         console.gotoxy(this.topLeftX + this.width, this.topLeftY + i + 1);
+         console.print(this.clearSpaceColor + " ");
+      }
+   }
+
+   // Before drawing the bottom border, if clearSpaceAroundMenu is
+   // true, put space before the border.
+   if (this.clearSpaceAroundMenu && (this.topLeftY > 1))
+   {
+      console.gotoxy(this.topLeftX - 1, this.topLeftY + this.height - 1);
+      console.print(this.clearSpaceColor + " ");
+   }
+   else
+      console.gotoxy(this.topLeftX, this.topLeftY + this.height - 1);
+   // Draw the bottom border
+   if (this.borderStyle == "single")
+   {
+      console.print(this.colors.border + LOWER_LEFT_SINGLE);
+      for (var i = 0; i < innerWidth; ++i)
+         console.print(HORIZONTAL_SINGLE);
+      console.print(LOWER_RIGHT_SINGLE);
+   }
+   else if (this.borderStyle == "double")
+   {
+      console.print(this.colors.border + LOWER_LEFT_DOUBLE);
+      for (var i = 0; i < innerWidth; ++i)
+         console.print(HORIZONTAL_DOUBLE);
+      console.print(LOWER_RIGHT_DOUBLE);
+   }
+   // If clearSpaceAroundMenu is true, then put a space after the border.
+   if (this.clearSpaceAroundMenu && (this.topLeftX + this.width < console.screen_columns))
+      console.print(this.clearSpaceColor + " ");
+
+   // If clearSpaceAroundMenu is true, then draw a blank row
+   // below the menu.
+   if (this.clearSpaceAroundMenu && (this.topLeftY + this.height < console.screen_rows))
+   {
+      var numSpaces = this.width + 2;
+      if (this.topLeftX > 1)
+         console.gotoxy(this.topLeftX-1, this.topLeftY + this.height);
+      else
+      {
+         console.gotoxy(this.topLeftX, this.topLeftY + this.height);
+         --numSpaces;
+      }
+
+      if (this.topLeftX + this.width >= console.screen_columns)
+         --numSpaces;
+
+      for (var i = 0; i < numSpaces; ++i)
+         console.print(this.clearSpaceColor + " ");
+   }
+
+   // Place the cursor on the line of the selected item
+   console.gotoxy(this.topLeftX + 1, this.topLeftY + this.selectedItemIndex + 1);
+
+   // Keep track of the current cursor position
+   var curpos = new Object();
+   curpos.x = this.topLeftX + 1;
+   curpos.y = this.topLeftY + this.selectedItemIndex + 1;
+
+   // Input loop
+   const topItemLineNumber = this.topLeftY + 1;
+   const bottomItemLineNumber = this.topLeftY + this.height - 1;
+   var continueOn = true;
+   while (continueOn)
+   {
+      // Get a key, (time out after the selected time), and take appropriate action.
+		//returnObj.userInput = console.inkey(0, this.timeoutMS).toUpperCase();
+		returnObj.userInput = console.getkey(K_NONE);
+		// If the user input is blank, then the input timed out, and we should quit.
+		if (returnObj.userInput == "")
+		{
+         continueOn = false;
+         break;
+		}
+
+      // Take appropriate action, depending on the user's keypress.
+      switch (returnObj.userInput)
+      {
+         case KEY_ENTER:
+            // Set returnObj.returnVal to the currently-selected item's returnVal,
+            // and exit the input loop.
+            returnObj.returnVal = this.menuItems[this.selectedItemIndex].returnVal;
+            continueOn = false;
+            break;
+         case KEY_UP:
+            // Go up one item
+            if (this.menuItems.length > 1)
+            {
+               // If we're below the top menu item, then go up one item.  Otherwise,
+               // go to the last menu item.
+               var oldIndex = this.selectedItemIndex;
+               if ((curpos.y > topItemLineNumber) && (this.selectedItemIndex > 0))
+               {
+                  --curpos.y;
+                  --this.selectedItemIndex;
+               }
+               else
+               {
+                  curpos.y = bottomItemLineNumber - 1;
+                  this.selectedItemIndex = this.menuItems.length - 1;
+               }
+               // Refresh the items on the screen so that the item colors
+               // are updated.
+               this.displayItem(oldIndex, false);
+               this.displayItem(this.selectedItemIndex, false);
+            }
+            break;
+         case KEY_DOWN:
+            // Go down one item
+            if (this.menuItems.length > 1)
+            {
+               // If we're above the bottom menu item, then go down one item.  Otherwise,
+               // go to the first menu item.
+               var oldIndex = this.selectedItemIndex;
+               if ((curpos.y < bottomItemLineNumber) && (this.selectedItemIndex < this.menuItems.length - 1))
+               {
+                  ++curpos.y;
+                  ++this.selectedItemIndex;
+               }
+               else
+               {
+                  curpos.y = this.topLeftY + 1;
+                  this.selectedItemIndex = 0;
+               }
+               // Refresh the items on the screen so that the item colors
+               // are updated.
+               this.displayItem(oldIndex, false);
+               this.displayItem(this.selectedItemIndex, false);
+            }
+            break;
+         case KEY_ESC:
+            continueOn = false;
+            break;
+         default:
+            // If the user's input is one of the hotkeys, then stop the
+            // input loop and return with the return code for the hotkey.
+            if (typeof(this.hotkeyRetvals[returnObj.userInput]) != "undefined")
+            {
+               returnObj.returnVal = this.hotkeyRetvals[returnObj.userInput];
+               continueOn = false;
+            }
+            // If the user's input is one of the loop-exit keys, then stop
+            // the input loop.
+            else if (typeof(this.exitLoopKeys[returnObj.userInput]) != "undefined")
+            {
+               returnObj.returnVal = this.exitLoopKeys[returnObj.userInput];
+               continueOn = false;
+            }
+            break;
+      }
+   }
+
+   return returnObj;
+}
+// Returns the number of items in the menu.
+function DCTMenu_NumItems()
+{
+   return this.menuItems.length;
+}
+// Removes all items from a DCTMenu.
+function DCTMenu_RemoveAllItems()
+{
+   this.width = 0;
+   this.height = 0;
+   this.selectedItemIndex = 0;
+   this.menuItems = new Array();
+   this.hotkeyRetvals = new Array();
+   this.exitLoopKeys = new Array();
+}
+
+// Returns the the script's execution directory.
+// The code in this function is a trick that was created by Deuce, suggested
+// by Rob Swindell as a way to detect which directory the script was executed
+// in.  I've shortened the code a little.
+function getScriptDir()
+{
+   var startup_path = '.';
+   try { throw dig.dist(dist); } catch(e) { startup_path = e.fileName; }
+   return(backslash(startup_path.replace(/[\/\\][^\/\\]*$/,'')));
+}
\ No newline at end of file
diff --git a/exec/SlyEdit_IceStuff.js b/exec/SlyEdit_IceStuff.js
new file mode 100644
index 0000000000..7e193e987f
--- /dev/null
+++ b/exec/SlyEdit_IceStuff.js
@@ -0,0 +1,783 @@
+/* This contains some IceEdit-specific functions for the Digital Distortion
+ * Editor which don't rely on any global variables.
+ *
+ * Author: Eric Oulashin (AKA Nightfox)
+ * BBS: Digital Distortion
+ * BBS address: digdist.bbsindex.com
+ *
+ * Date       User              Description
+ * 2009-06-06 Eric Oulashin     Started development
+ * 2009-08-09 Eric Oulashin     More development & testing
+ * 2009-08-22 Eric Oulashin     Version 1.00
+ *                              Initial public release
+ * 2009-12-03 Eric Oulashin     Added support for color schemes.
+ *                              Added displayIceYesNoText() and
+ *                              readColorConfig().
+ * 2010-01-02 Eric Oulashin     Removed abortConfirm_DCTStyle(),
+ *                              since it's no longer used anymore.
+ * 2011-02-02 Eric Oulashin     Moved the time displaying code into
+ *                              a new function, displayTime_IceStyle().
+ * 2012-02-18 Eric Oulashin     Changed the copyright year to 2012
+ * 2012-12-21 Eric Oulashin     Removed gStartupPath from the beginning
+ *                              of the theme filename, since the path is
+ *                              now set in ReadSlyEditConfigFile() in
+ *                              SlyEdit_Misc.js.
+ */
+
+load("sbbsdefs.js");
+load(gStartupPath + "SlyEdit_Misc.js");
+
+// IceEdit ESC menu item return values
+var ICE_ESC_MENU_SAVE = 0;
+var ICE_ESC_MENU_ABORT = 1;
+var ICE_ESC_MENU_EDIT = 2;
+var ICE_ESC_MENU_HELP = 3;
+
+// Read the color configuration file
+readColorConfig(gConfigSettings.iceColors.ThemeFilename);
+
+
+///////////////////////////////////////////////////////////////////////////////////
+// Functions
+
+// This function reads the color configuration for Ice style.
+//
+// Parameters:
+//  pFilename: The name of the color configuration file
+function readColorConfig(pFilename)
+{
+   var colors = readValueSettingConfigFile(pFilename, 512);
+   if (colors != null)
+      gConfigSettings.iceColors = colors;
+}
+
+// Re-draws the screen, in the style of IceEdit.
+//
+// Parameters:
+//  pEditLeft: The leftmost column of the edit area
+//  pEditRight: The rightmost column of the edit area
+//  pEditTop: The topmost row of the edit area
+//  pEditBottom: The bottommost row of the edit area
+//  pEditColor: The edit color
+//  pInsertMode: The insert mode ("INS" or "OVR")
+//  pUseQuotes: Whether or not message quoting is enabled
+//  pEditLinesIndex: The index of the message line at the top of the edit area
+//  pDisplayEditLines: The function that displays the edit lines
+function redrawScreen_IceStyle(pEditLeft, pEditRight, pEditTop, pEditBottom, pEditColor,
+                                pInsertMode, pUseQuotes, pEditLinesIndex, pDisplayEditLines)
+{
+	// Top header
+	// Generate & display the top border line (Note: Generate this
+	// border line only once, for efficiency).
+	if (typeof(redrawScreen_IceStyle.topBorder) == "undefined")
+	{
+      redrawScreen_IceStyle.topBorder = UPPER_LEFT_SINGLE;
+      var innerWidth = console.screen_columns - 2;
+      for (var i = 0; i < innerWidth; ++i)
+         redrawScreen_IceStyle.topBorder += HORIZONTAL_SINGLE;
+      redrawScreen_IceStyle.topBorder += UPPER_RIGHT_SINGLE;
+      redrawScreen_IceStyle.topBorder = randomTwoColorString(redrawScreen_IceStyle.topBorder,
+                                                             gConfigSettings.iceColors.BorderColor1,
+                                                             gConfigSettings.iceColors.BorderColor2);
+   }
+   // Print the border line on the screen
+   console.clear();
+   console.print(redrawScreen_IceStyle.topBorder);
+
+	// Next line
+	// To name
+	var lineNum = 2;
+	console.gotoxy(1, lineNum);
+	// Calculate the width of the user alias field: 28 characters, based
+	// on an 80-column screen width.
+	var fieldWidth = (console.screen_columns * (29/80)).toFixed(0);
+	var screenText = gToName.substr(0, fieldWidth);
+	console.print("n" + randomTwoColorString(VERTICAL_SINGLE,
+	                                            gConfigSettings.iceColors.BorderColor1,
+	                                            gConfigSettings.iceColors.BorderColor2) +
+				  gConfigSettings.iceColors.TopInfoBkgColor + " " +
+				  gConfigSettings.iceColors.TopLabelColor + "TO" +
+				  gConfigSettings.iceColors.TopLabelColonColor + ": " +
+				  gConfigSettings.iceColors.TopToColor + screenText);
+	fieldWidth -= screenText.length;
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(" ");
+
+	// From name
+	fieldWidth = (console.screen_columns * (29/80)).toFixed(0);
+	screenText = gFromName.substr(0, fieldWidth);
+	console.print(" " + gConfigSettings.iceColors.TopLabelColor + "FROM" +
+	              gConfigSettings.iceColors.TopLabelColonColor + ": " +
+	              gConfigSettings.iceColors.TopFromColor + screenText);
+	fieldWidth -= screenText.length;
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(" ");
+	// More spaces until the time location
+	var curpos = console.getxy();
+	var startX = console.screen_columns - 8;
+	while (curpos.x < startX)
+	{
+		console.print(" ");
+		++curpos.x;
+	}
+
+	// Time
+	console.print(" ");
+	displayTime_IceStyle();
+	console.print(" " + randomTwoColorString(VERTICAL_SINGLE, gConfigSettings.iceColors.BorderColor1,
+	                                         gConfigSettings.iceColors.BorderColor2));
+
+	// Next line: Subject, time left, insert/overwrite mode
+	++lineNum;
+	console.gotoxy(1, lineNum);
+	// Subject
+	fieldWidth = (console.screen_columns * (54/80)).toFixed(0);
+	screenText = gMsgSubj.substr(0, fieldWidth);
+	console.print("n" + randomTwoColorString(VERTICAL_SINGLE,
+	                                            gConfigSettings.iceColors.BorderColor1,
+	                                            gConfigSettings.iceColors.BorderColor2) +
+				  gConfigSettings.iceColors.TopInfoBkgColor + " " +
+				  gConfigSettings.iceColors.TopLabelColor + "SUBJECT" +
+				  gConfigSettings.iceColors.TopLabelColonColor + ": " +
+				  gConfigSettings.iceColors.TopSubjectColor + screenText);
+	fieldWidth -= screenText.length;
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(" ");
+
+	// Time left
+	fieldWidth = (console.screen_columns * (4/80)).toFixed(0);
+	screenText = Math.floor(bbs.time_left / 60).toString().substr(0, fieldWidth);
+	startX = console.screen_columns - fieldWidth - 10;
+	// Before outputting the time left, write more spaces until the starting
+	// horizontal location.
+	curpos = console.getxy();
+	while (curpos.x < startX)
+	{
+		console.print(" ");
+		++curpos.x;
+	}
+	console.print(" " + gConfigSettings.iceColors.TopLabelColor + "TL" +
+	              gConfigSettings.iceColors.TopLabelColonColor + ": " +
+	              gConfigSettings.iceColors.TopTimeLeftColor, screenText);
+	fieldWidth -= screenText.length;
+	for (var i = 0; i < fieldWidth; ++i)
+		console.print(" ");
+
+	// Insert/overwrite mode
+	console.print(" " + gConfigSettings.iceColors.EditMode + pInsertMode + " n" +
+	              randomTwoColorString(VERTICAL_SINGLE, gConfigSettings.iceColors.BorderColor1,
+	                                                    gConfigSettings.iceColors.BorderColor2));
+	
+	// Next line: Top border for the message area and also includes the user #,
+	// message area, and node #.
+	// Generate this border line only once, for efficiency.
+	if (typeof(redrawScreen_IceStyle.msgAreaBorder) == "undefined")
+	{
+      redrawScreen_IceStyle.msgAreaBorder = "n"
+                         + randomTwoColorString(LEFT_T_SINGLE + HORIZONTAL_SINGLE,
+                                                gConfigSettings.iceColors.BorderColor1,
+                                                gConfigSettings.iceColors.BorderColor2)
+      // User #, padded with high-black dim block characters, 5 characters for a screen
+      // that's 80 characters wide.
+                       + "h" + THIN_RECTANGLE_LEFT + "#k";
+      fieldWidth = (console.screen_columns * (5/80)).toFixed(0) - user.number.toString().length;
+      for (var i = 0; i < fieldWidth; ++i)
+         redrawScreen_IceStyle.msgAreaBorder += BLOCK1;
+      redrawScreen_IceStyle.msgAreaBorder += "c" + user.number
+                                           + gConfigSettings.iceColors.BorderColor1
+                                           + THIN_RECTANGLE_RIGHT;
+
+      // The message area name should be centered on the line.  So, based on its
+      // length (up to 20 characters), figure out its starting position before
+      // printing it.
+      var msgAreaName = gMsgArea.substr(0, 20);
+      // 2 is subtracted from the starting position to leave room for the
+      // block character and the space.
+      var startPos = (console.screen_columns/2).toFixed(0) - (msgAreaName.length/2).toFixed(0) - 2;
+      // Write border characters up to the message area name start position
+      screenText = "";
+      for (var i = strip_ctrl(redrawScreen_IceStyle.msgAreaBorder).length; i < startPos; ++i)
+         screenText += HORIZONTAL_SINGLE;
+      redrawScreen_IceStyle.msgAreaBorder += randomTwoColorString(screenText,
+                                                             gConfigSettings.iceColors.BorderColor1,
+                                                             gConfigSettings.iceColors.BorderColor2);
+
+      // Write the message area name
+      redrawScreen_IceStyle.msgAreaBorder += "h" + gConfigSettings.iceColors.BorderColor1
+                  + THIN_RECTANGLE_LEFT + " " + iceText(msgAreaName, "w") + " h"
+                  + gConfigSettings.iceColors.BorderColor1 + THIN_RECTANGLE_RIGHT;
+
+      // Calculate the field width for the node number field.
+      // For the node # field, use 3 characters for a screen 80 characters wide.
+      fieldWidth = (console.screen_columns * (3/80)).toFixed(0);
+      // Calculate the horizontal starting position for the node field.
+      var nodeFieldStartPos = console.screen_columns - fieldWidth - 9;
+
+      // Write horizontal border characters up until the point where we'll output
+      // the node number.
+      screenText = "";
+      for (var posX = strip_ctrl(redrawScreen_IceStyle.msgAreaBorder).length; posX < nodeFieldStartPos; ++posX)
+         screenText += HORIZONTAL_SINGLE;
+      redrawScreen_IceStyle.msgAreaBorder += randomTwoColorString(screenText,
+                                                            gConfigSettings.iceColors.BorderColor1,
+                                                            gConfigSettings.iceColors.BorderColor2);
+
+      // Output the node # field
+      redrawScreen_IceStyle.msgAreaBorder += "h" + gConfigSettings.iceColors.BorderColor1
+                         + THIN_RECTANGLE_LEFT + iceText("Node", "w") + "nb:hk";
+      fieldWidth -= bbs.node_num.toString().length;
+      for (var i = 0; i < fieldWidth; ++i)
+         redrawScreen_IceStyle.msgAreaBorder += BLOCK1;
+      redrawScreen_IceStyle.msgAreaBorder += "c" + bbs.node_num
+                         + gConfigSettings.iceColors.BorderColor1 + THIN_RECTANGLE_RIGHT;
+
+      // Write the last 2 characters of top border
+      redrawScreen_IceStyle.msgAreaBorder += randomTwoColorString(HORIZONTAL_SINGLE + RIGHT_T_SINGLE,
+                                                               gConfigSettings.iceColors.BorderColor1,
+                                                               gConfigSettings.iceColors.BorderColor2);
+	}
+	// Draw the border line on the screen
+	++lineNum;
+	console.gotoxy(1, lineNum);
+	console.print(redrawScreen_IceStyle.msgAreaBorder);
+	
+	// Display the bottom message area border and help line
+	DisplayTextAreaBottomBorder_IceStyle(pEditBottom + 1, pUseQuotes);
+	DisplayBottomHelpLine_IceStyle(console.screen_rows, pUseQuotes);
+
+	// If the screen is at least 82 columns wide output vertical lines
+	// to frame the edit area.
+	if (console.screen_columns >= 82)
+	{
+		for (lineNum = pEditTop; lineNum <= pEditBottom; ++lineNum)
+		{
+			console.gotoxy(pEditLeft-1, lineNum);
+			console.print(randomTwoColorString(VERTICAL_SINGLE, gConfigSettings.iceColors.BorderColor1,
+			                                                    gConfigSettings.iceColors.BorderColor2));
+			console.gotoxy(pEditRight+1, lineNum);
+			console.print(randomTwoColorString(VERTICAL_SINGLE, gConfigSettings.iceColors.BorderColor1,
+			                                                    gConfigSettings.iceColors.BorderColor2));
+		}
+	}
+
+	// Go to the start of the edit area
+	console.gotoxy(pEditLeft, pEditTop);
+
+	// Write the message text that has been entered thus far.
+	pDisplayEditLines(pEditTop, pEditLinesIndex);
+	console.print(pEditColor);
+}
+
+// Displays the first help line for the bottom of the screen, in the style
+// of IceEdit.
+//
+// Parameters:
+//  pLineNum: The line number on the screen where the text should be placed
+//  pUseQuotes: Whether or not message quoting is enabled
+//  pCanChgMsgColor: Whether or not changing the text color is allowed
+// The following parameters are not used; this is here to match the function signature of DCTEdit version.
+//  pEditLeft
+//  pEditRight
+//  pInsertMode
+function DisplayTextAreaBottomBorder_IceStyle(pLineNum, pUseQuotes, pEditLeft, pEditRight,
+                                               pInsertMode, pCanChgMsgColor)
+{
+   // The border will use random bright/normal colors.  The colors
+   // should stay the same each time we draw it, so a "static"
+   // variable is used for the border text.  If that variable has
+   // not been defined yet, then build it.
+   if (typeof(DisplayTextAreaBottomBorder_IceStyle.border) == "undefined")
+   {
+      // Build the string of CTRL key combinations that will be displayed
+      var ctrlKeyHelp = gConfigSettings.iceColors.KeyInfoLabelColor
+                      + "CTRL b(nwAhb)n"
+                      + gConfigSettings.iceColors.KeyInfoLabelColor + "Abort";
+      if (pUseQuotes)
+      {
+         ctrlKeyHelp += " b(nwQhb)n"
+                      + gConfigSettings.iceColors.KeyInfoLabelColor + "Quote";
+      }
+      ctrlKeyHelp += " b(nwZhb)n" + gConfigSettings.iceColors.KeyInfoLabelColor
+                   + "Save";
+
+      // Start the border text with the first 2 border characters
+      // The beginning of this line shows that SlyEdit is registered
+      // to the sysop. :)
+      DisplayTextAreaBottomBorder_IceStyle.border =
+               randomTwoColorString(LOWER_LEFT_SINGLE + HORIZONTAL_SINGLE,
+                                    gConfigSettings.iceColors.BorderColor1,
+                                    gConfigSettings.iceColors.BorderColor2)
+             + "h" + gConfigSettings.iceColors.BorderColor1 + THIN_RECTANGLE_LEFT
+             + iceText("Registered To: " + system.operator.substr(0, 20), "w")
+             + "h" + gConfigSettings.iceColors.BorderColor1 + THIN_RECTANGLE_RIGHT;
+      // Append border characters up until the point we'll have to write the CTRL key
+      // help text.
+      var screenText = "";
+      var endPos = console.screen_columns - strip_ctrl(ctrlKeyHelp).length - 3;
+      var textLen = strip_ctrl(DisplayTextAreaBottomBorder_IceStyle.border).length;
+      for (var i = textLen+1; i < endPos; ++i)
+         screenText += HORIZONTAL_SINGLE;
+      DisplayTextAreaBottomBorder_IceStyle.border += randomTwoColorString(screenText,
+                                                                gConfigSettings.iceColors.BorderColor1,
+                                                                gConfigSettings.iceColors.BorderColor2);
+
+      // CTRL key help and the remaining 2 characters in the border.
+      DisplayTextAreaBottomBorder_IceStyle.border += "h" + gConfigSettings.iceColors.BorderColor1
+                  + THIN_RECTANGLE_LEFT + ctrlKeyHelp + gConfigSettings.iceColors.BorderColor1
+                  + THIN_RECTANGLE_RIGHT
+                  + randomTwoColorString(HORIZONTAL_SINGLE + LOWER_RIGHT_SINGLE,
+                                         gConfigSettings.iceColors.BorderColor1,
+                                         gConfigSettings.iceColors.BorderColor2);
+   }
+
+   // Display the border line on the screen
+   // If pLineNum is not specified, then default to the 2nd to the last
+	// line on the screen.
+	var lineNum = console.screen_rows-1;
+	if ((typeof(pLineNum) != "undefined") && (pLineNum != null))
+		lineNum = pLineNum;
+   console.gotoxy(1, lineNum);
+   console.print(DisplayTextAreaBottomBorder_IceStyle.border);
+}
+
+// Displays the second (lower) help line for the bottom of the screen,
+// in the style of IceEdit.
+//
+// Parameters:
+//  pLineNum: The line number on the screen where the text should be placed
+//  The following are not used and are only here to match the DCT-style function:
+//   pUsingQuotes: Boolean - Whether or not message quoting is enabled.
+function DisplayBottomHelpLine_IceStyle(pLineNum, pUsingQuotes)
+{
+   // Construct the help text only once
+   if (typeof(DisplayBottomHelpLine_IceStyle.helpText) == "undefined")
+   {
+      // This line contains the copyright mesage & ESC key help
+      var screenText = iceText(EDITOR_PROGRAM_NAME + " v", "w") + "ch"
+                      + EDITOR_VERSION.toString() + "   "
+                      + iceText("Copyright", "w") + " ch2012 "
+                      + iceText("Eric Oulashin", "w") + " nb" + DOT_CHAR + " "
+                      + iceText("Press ESCape For Help", "w");
+      // Calculate the starting position to center the help text, and front-pad
+      // DisplayBottomHelpLine_IceStyle.helpText with that many spaces.
+      var xPos = (console.screen_columns / 2).toFixed(0)
+                - (strip_ctrl(screenText).length / 2).toFixed(0);
+      DisplayBottomHelpLine_IceStyle.helpText = "";
+      for (var i = 0; i < xPos; ++i)
+         DisplayBottomHelpLine_IceStyle.helpText += " ";
+      DisplayBottomHelpLine_IceStyle.helpText += screenText;
+   }
+
+   // If pLineNum is not specified, then default to the last line
+	// on the screen.
+	var lineNum = console.screen_rows;
+	if ((typeof(pLineNum) != "undefined") && (pLineNum != null))
+		lineNum = pLineNum;
+   // Display the help text on the screen
+	console.gotoxy(1, lineNum);
+	console.print(DisplayBottomHelpLine_IceStyle.helpText);
+}
+
+// Updates the insert mode displayd on the screen, for Ice Edit style.
+//
+// Parameters:
+//  pEditRight: Not used; this is only here to match the signature of the DCTEdit version.
+//  pEditBottom: Not used; this is only here to match the signature of the DCTEdit version.
+//  pInsertMode: The insert mode ("INS" or "OVR")
+function updateInsertModeOnScreen_IceStyle(pEditRight, pEditBottom, pInsertMode)
+{
+	console.gotoxy(console.screen_columns-4, 3);
+	console.print(gConfigSettings.iceColors.TopInfoBkgColor + gConfigSettings.iceColors.EditMode
+	              + pInsertMode);
+}
+
+// Draws the top border of the quote window, IceEdit style.
+//
+// Parameters:
+//  pQuoteWinHeight: The height of the quote window
+//  pEditLeft: The leftmost column of the edit area
+//  pEditRight: The rightmost column of the edit area
+function DrawQuoteWindowTopBorder_IceStyle(pQuoteWinHeight, pEditLeft, pEditRight)
+{
+   // Top border of the quote window
+   // The border will use random bright/normal colors.  The colors
+   // should stay the same each time we draw it, so a "static"
+   // variable is used for the border text.  If that variable has
+   // not been defined yet, then build it.
+   if (typeof(DrawQuoteWindowTopBorder_IceStyle.border) == "undefined")
+   {
+      DrawQuoteWindowTopBorder_IceStyle.border = randomTwoColorString(UPPER_LEFT_VSINGLE_HDOUBLE,
+                                                              gConfigSettings.iceColors.BorderColor1,
+                                                              gConfigSettings.iceColors.BorderColor2)
+                + gConfigSettings.iceColors.BorderColor2 + THIN_RECTANGLE_LEFT
+                + gConfigSettings.iceColors.QuoteWinBorderTextColor + "Quote Window"
+                + gConfigSettings.iceColors.BorderColor2
+                + THIN_RECTANGLE_RIGHT;
+      // The border from here to the end of the line: Random high/low blue
+      var screenText = "";
+      for (var posX = pEditLeft+16; posX <= pEditRight; ++posX)
+         screenText += HORIZONTAL_DOUBLE;
+      screenText += UPPER_RIGHT_VSINGLE_HDOUBLE;
+      DrawQuoteWindowTopBorder_IceStyle.border += randomTwoColorString(screenText,
+                                                           gConfigSettings.iceColors.BorderColor1,
+                                                           gConfigSettings.iceColors.BorderColor2);
+   }
+
+   // Draw the border line on the screen
+   console.gotoxy(pEditLeft, console.screen_rows - pQuoteWinHeight + 1);
+   console.print(DrawQuoteWindowTopBorder_IceStyle.border);
+}
+
+// Draws the bottom border of the quote window, IceEdit style.  Note:
+// The cursor should be placed at the start of the line (leftmost screen
+// column on the screen line where the border should be drawn).
+//
+// Parameters:
+//  pEditLeft: The leftmost column of the edit area
+//  pEditRight: The rightmost column of the edit area
+function DrawQuoteWindowBottomBorder_IceStyle(pEditLeft, pEditRight)
+{
+   // The border will use random bright/normal colors.  The colors
+   // should stay the same each time we draw it, so a "static"
+   // variable is used for the border text.  If that variable has
+   // not been defined yet, then build it.
+   if (typeof(DrawQuoteWindowBottomBorder_IceStyle.border) == "undefined")
+   {
+      DrawQuoteWindowBottomBorder_IceStyle.border = randomTwoColorString(LOWER_LEFT_VSINGLE_HDOUBLE,
+                                                             gConfigSettings.iceColors.BorderColor1,
+                                                             gConfigSettings.iceColors.BorderColor2)
+                + gConfigSettings.iceColors.BorderColor2 + THIN_RECTANGLE_LEFT
+                + gConfigSettings.iceColors.QuoteWinBorderTextColor + "^Q/ESC-End"
+                + gConfigSettings.iceColors.BorderColor2 + THIN_RECTANGLE_RIGHT
+                + gConfigSettings.iceColors.BorderColor1 + HORIZONTAL_DOUBLE
+                + gConfigSettings.iceColors.BorderColor2  + THIN_RECTANGLE_LEFT
+                + gConfigSettings.iceColors.QuoteWinBorderTextColor + "CR-Accept"
+                + gConfigSettings.iceColors.BorderColor2 + THIN_RECTANGLE_RIGHT
+                + gConfigSettings.iceColors.BorderColor1 + HORIZONTAL_DOUBLE
+                + gConfigSettings.iceColors.BorderColor2 + THIN_RECTANGLE_LEFT
+                + gConfigSettings.iceColors.QuoteWinBorderTextColor + "Up/Down-Scroll"
+                + gConfigSettings.iceColors.BorderColor2 + THIN_RECTANGLE_RIGHT;
+      // The border from here to the end of the line: Random high/low blue
+      var screenText = "";
+      for (var posX = pEditLeft + 43; posX <= pEditRight; ++posX)
+         screenText += HORIZONTAL_DOUBLE;
+      screenText += LOWER_RIGHT_VSINGLE_HDOUBLE;
+      DrawQuoteWindowBottomBorder_IceStyle.border += randomTwoColorString(screenText,
+                                                          gConfigSettings.iceColors.BorderColor1,
+                                                          gConfigSettings.iceColors.BorderColor2);
+   }
+
+   // Draw the border line on the screen
+   console.print(DrawQuoteWindowBottomBorder_IceStyle.border);
+}
+
+// Prompts the user for a yes/no question, IceEdit-style.  Note that
+// the cursor should be position where it needs to be before calling
+// this function.
+//
+// Parameters:
+//  pQuestion: The question to ask, without the trailing "?"
+//  pDefaultYes: Whether to default to "yes" (true/false).  If false, this
+//               will default to "no".
+function promptYesNo_IceStyle(pQuestion, pDefaultYes)
+{
+   var userResponse = pDefaultYes;
+
+   // Print the question, and highlight "yes" or "no", depending on
+   // the value of pDefaultYes.
+   console.print(iceText(pQuestion + "? ", "w"));
+   displayIceYesNoText(pDefaultYes);
+
+   // yesNoX contains the horizontal position for the "Yes" & "No" text.
+   const yesNoX = strip_ctrl(pQuestion).length + 3;
+
+   // Input loop
+   var userInput = "";
+   var continueOn = true;
+   while (continueOn)
+   {
+      // Move the cursor to the start of the "Yes" or "No" text (whichever
+      // one is currently selected).
+      console.gotoxy(userResponse ? yesNoX : yesNoX+7, console.screen_rows);
+      // Get a key, (time out after 1 minute), and take appropriate action.
+		userInput = console.inkey(0, 100000).toUpperCase();
+		// If userInput is blank, then the timeout was hit, so exit the loop.
+		// Also exit the loop of the user pressed enter.
+		if ((userInput == "") || (userInput == KEY_ENTER))
+         continueOn = false;
+      else if (userInput == "Y")
+      {
+         userResponse = true;
+         continueOn = false;
+      }
+      else if (userInput == "N")
+      {
+         userResponse = false;
+         continueOn = false;
+      }
+      // Left or right arrow key: Toggle userResponse and update the
+      // yes/no text with the appropriate colors
+      else if ((userInput == KEY_LEFT) || (userInput == KEY_RIGHT))
+      {
+         // Move the cursor to the start of the "Yes" and "No" text, and
+         // update the text depending on the value of userResponse.
+         console.gotoxy(yesNoX, console.screen_rows);
+         userResponse = !userResponse;
+         displayIceYesNoText(userResponse);
+      }
+   }
+
+   return userResponse;
+}
+
+// Displays the time on the screen.
+//
+// Parameters:
+//  pTimeStr: The time to display (optional).  If this is omitted,
+//            then this funtion will get the current time.
+function displayTime_IceStyle(pTimeStr)
+{
+	console.gotoxy(73, 2);
+	if (pTimeStr == null)
+		console.print(gConfigSettings.iceColors.TopInfoBkgColor + gConfigSettings.iceColors.TopTimeColor + getCurrentTimeStr());
+	else
+		console.print(gConfigSettings.iceColors.TopInfoBkgColor + gConfigSettings.iceColors.TopTimeColor + pTimeStr);
+}
+
+// For IceEdit mode: This function takes a string and returns a copy
+// of the string with uppercase letters dim and the rest bright for
+// a given color.
+//
+// Parameters:
+//  pString: The string to convert
+//  pColor: The Synchronet color code to use as the base text color.
+//  pBkgColor: Optional - The background color (a Synchronet color code).
+//             Defaults to normal white.
+function iceText(pString, pColor, pBkgColor)
+{
+	// Return if an invalid string is passed in.
+	if (typeof(pString) == "undefined")
+		return "";
+	if (pString == null)
+		return "";
+
+	// Set the color.  Default to blue.
+	var color = "b";
+	if ((typeof(pColor) != "undefined") && (pColor != null))
+      color = pColor;
+
+   // Set the background color.  Default to normal black.
+   var bkgColor = "nk";
+   if ((typeof(pBkgColor) != "undefined") && (pBkgColor != null))
+      bkgColor = pBkgColor;
+
+	// Create a copy of the string without any control characters,
+	// and then add our coloring to it.
+	pString = strip_ctrl(pString);
+	var returnString = "n" + bkgColor + color;
+	var lastColor = "n" + color;
+	var character = "";
+	for (var i = 0; i < pString.length; ++i)
+	{
+		character = pString.charAt(i);
+
+		// Upper-case letters: Make it dim with the passed-in color.
+		if (character.match(/[A-Z]/) != null)
+		{
+         // If the last color was not the normal color, then append
+         // the normal color to returnString.
+         if (lastColor != "n" + color)
+            returnString += "n" + bkgColor + color;
+			lastColor = "n" + color;
+		}
+		// Lower-case letter: Make it bright with the passed-in color.
+		else if (character.match(/[a-z]/) != null)
+		{
+			// If this is the first character or if the last color was
+			// not the bright color, then append the bright color to
+			// returnString.
+			if ((i == 0) || (lastColor != ("h" + color)))
+            returnString += "h" + color;
+			lastColor = "h" + color;
+		}
+		// Number: Make it bright cyan
+		else if (character.match(/[0-9]/) != null)
+		{
+         // If the last color was not bright cyan, then append
+         // bright cyan to returnString.
+         if (lastColor != "hc")
+            returnString += "hc";
+			lastColor = "hc";
+		}
+		// All else: Make it bright blue
+		else
+		{
+         // If the last color was not bright cyan, then append
+         // bright cyan to returnString.
+         if (lastColor != "hb")
+            returnString += "hb";
+			lastColor = "hb";
+		}
+
+		// Append the character from pString.
+		returnString += character;
+	}
+
+	return returnString;
+}
+
+// Displays & handles the input loop for the DCT Edit menu.
+//
+// Parameters:
+//  pY: The line number on the screen for the menu
+//
+// Return value: One of the ICE_ESC_MENU values, based on the
+//               user's response.
+function doIceESCMenu(pY)
+{
+   var promptText = " Select An Option:  ";
+
+   console.gotoxy(1, pY);
+   console.print(iceText(promptText, "w"));
+   console.cleartoeol("n");
+   // Input loop
+   var userInput;
+   var userChoice = ICE_ESC_MENU_SAVE;
+   var continueOn = true;
+   while (continueOn)
+   {
+      console.gotoxy(promptText.length, pY);
+
+      // Display the options, with the correct one highlighted.
+      switch (userChoice)
+      {
+         case ICE_ESC_MENU_SAVE:
+            console.print(iceStyledPromptText("Save", true) + "n ");
+            console.print(iceStyledPromptText("Abort", false) + "n ");
+            console.print(iceStyledPromptText("Edit", false) + "n ");
+            console.print(iceStyledPromptText("Help", false));
+            break;
+         case ICE_ESC_MENU_ABORT:
+            console.print(iceStyledPromptText("Save", false) + "n ");
+            console.print(iceStyledPromptText("Abort", true) + "n ");
+            console.print(iceStyledPromptText("Edit", false) + "n ");
+            console.print(iceStyledPromptText("Help", false));
+            break;
+         case ICE_ESC_MENU_EDIT:
+            console.print(iceStyledPromptText("Save", false) + "n ");
+            console.print(iceStyledPromptText("Abort", false) + "n ");
+            console.print(iceStyledPromptText("Edit", true) + "n ");
+            console.print(iceStyledPromptText("Help", false));
+            break;
+         case ICE_ESC_MENU_HELP:
+            console.print(iceStyledPromptText("Save", false) + "n ");
+            console.print(iceStyledPromptText("Abort", false) + "n ");
+            console.print(iceStyledPromptText("Edit", false) + "n ");
+            console.print(iceStyledPromptText("Help", true));
+            break;
+      }
+
+      // Get the user's choice
+      userInput = console.getkey(K_UPPER|K_ALPHA|K_NOECHO|K_NOSPIN|K_NOCRLF);
+      switch (userInput)
+      {
+         case KEY_UP:
+         case KEY_LEFT:
+            --userChoice;
+            if (userChoice < ICE_ESC_MENU_SAVE)
+               userChoice = ICE_ESC_MENU_HELP;
+            break;
+         case KEY_DOWN:
+         case KEY_RIGHT:
+            ++userChoice;
+            if (userChoice > ICE_ESC_MENU_HELP)
+               userChoice = ICE_ESC_MENU_SAVE;
+            break;
+         case "S": // Save
+            userChoice = ICE_ESC_MENU_SAVE;
+            continueOn = false;
+            break;
+         case "A": // Abort
+            userChoice = ICE_ESC_MENU_ABORT;
+            continueOn = false;
+            break;
+         case KEY_ESC: // Go back to editing the message
+         case "E":     // Go back to editing the message
+            userChoice = ICE_ESC_MENU_EDIT;
+            continueOn = false;
+            break;
+         case "H": // Help
+            userChoice = ICE_ESC_MENU_HELP;
+            continueOn = false;
+            break;
+         case KEY_ENTER: // Accept the current choice
+            continueOn = false;
+            break;
+      }
+   }
+
+   // Make sure special text attributes are cleared.
+   console.print("n");
+
+   return userChoice;
+}
+
+// Returns text to be used in prompts, such as the ESC menu, etc.
+//
+// Parameters:
+//  pText: The text to display
+//  pHighlight: Whether or not to use highlight colors
+//
+// Return value: The styled text
+function iceStyledPromptText(pText, pHighlight)
+{
+   var styledText;
+   if (pHighlight)
+      styledText = gConfigSettings.iceColors.SelectedOptionBorderColor + THIN_RECTANGLE_LEFT
+                 + gConfigSettings.iceColors.SelectedOptionTextColor + pText
+                 + gConfigSettings.iceColors.SelectedOptionBorderColor + THIN_RECTANGLE_RIGHT;
+   else
+      styledText = gConfigSettings.iceColors.UnselectedOptionBorderColor + THIN_RECTANGLE_LEFT
+                 + iceText(pText, "w") + gConfigSettings.iceColors.UnselectedOptionBorderColor
+                 + THIN_RECTANGLE_RIGHT;
+   return styledText;
+}
+
+// Displays yes/no options in Ice style.
+//
+// Parameters:
+//  pYesSelected: Whether or not the "yes" option is to be selected.
+function displayIceYesNoText(pYesSelected)
+{
+   if (pYesSelected)
+   {
+      console.print(gConfigSettings.iceColors.SelectedOptionBorderColor + THIN_RECTANGLE_LEFT +
+                    gConfigSettings.iceColors.SelectedOptionTextColor + "YES" +
+                    gConfigSettings.iceColors.SelectedOptionBorderColor +
+                    THIN_RECTANGLE_RIGHT + gConfigSettings.iceColors.UnselectedOptionBorderColor +
+                    "  " + THIN_RECTANGLE_LEFT + gConfigSettings.iceColors.UnselectedOptionTextColor +
+                    "NO" + gConfigSettings.iceColors.UnselectedOptionBorderColor +
+                    THIN_RECTANGLE_RIGHT);
+   }
+   else
+   {
+      console.print(gConfigSettings.iceColors.UnselectedOptionBorderColor + THIN_RECTANGLE_LEFT +
+                    gConfigSettings.iceColors.UnselectedOptionTextColor + "YES" +
+                    gConfigSettings.iceColors.UnselectedOptionBorderColor + THIN_RECTANGLE_RIGHT +
+                    "  " + gConfigSettings.iceColors.SelectedOptionBorderColor + THIN_RECTANGLE_LEFT +
+                    gConfigSettings.iceColors.SelectedOptionTextColor + "NO" +
+                    gConfigSettings.iceColors.SelectedOptionBorderColor + THIN_RECTANGLE_RIGHT +
+                    "n");
+   }
+}
+
+// Returns the the script's execution directory.
+// The code in this function is a trick that was created by Deuce, suggested
+// by Rob Swindell as a way to detect which directory the script was executed
+// in.  I've shortened the code a little.
+function getScriptDir()
+{
+   var startup_path = '.';
+   try { throw dig.dist(dist); } catch(e) { startup_path = e.fileName; }
+   return(backslash(startup_path.replace(/[\/\\][^\/\\]*$/,'')));
+}
\ No newline at end of file
diff --git a/exec/SlyEdit_Misc.js b/exec/SlyEdit_Misc.js
new file mode 100644
index 0000000000..0b832c524c
--- /dev/null
+++ b/exec/SlyEdit_Misc.js
@@ -0,0 +1,1636 @@
+/* This file declares functions and variables that are used by Digital
+ * Distortion Editor for both DCTEdit and IceEdit modes.
+ *
+ * Author: Eric Oulashin (AKA Nightfox)
+ * BBS: Digital Distortion
+ * BBS address: digdist.bbsindex.com
+ *
+ * Date       User              Description
+ * 2009-06-06 Eric Oulashin     Started development
+ * 2009-06-11 Eric Oulashin     Taking a break from development
+ * 2009-08-09 Eric Oulashin     Started more development & testing
+ * 2009-08-22 Eric Oulashin     Version 1.00
+ *                              Initial public release
+ * ....Removed some comments...
+ * 2010-01-19 Eric Oulashin     Updated reAdjustTextLines() to return a boolean
+ *                              to signify whether any text had been changed.
+ *                              Moved isQuoteLine() to here from SlyEdit.js.
+ *                              Updated isQuoteLine() to just use the isQuoteLine
+ *                              property of the line and not check to see if the
+ *                              line starts with a >.
+ *                              Updated displayProgramInfo(): Removed my BBS
+ *                              name & URL, as well as my handle.
+ * 2010-02-14 Eric Oulashin     Updated reAdjustTextLines() so that it won't
+ *                              assume it's splitting around a space: If a space
+ *                              is not found in the line, it won't drop a
+ *                              character from the line.
+ * 2010-04-03 Eric Oulashin     Started working on Ctrl-A color support.
+ * 2010-04-06 Eric Oulashin     Updated ReadSlyEditConfigFile() to read the
+ *                              allowColorSelection value from the config file.
+ * 2010-04-10 Eric Oulashin     Added toggleAttr().
+ * 2012-02-17 Eric Oulashin     Added rewrapTextLines(). Changed the configuration
+ *                              setting "splitLongQuoteLines" to "reWrapQuoteLines".
+ * 2012-03-31 Eric Oulashin     Added the following configuration options:
+ *                              add3rdPartyStartupScript
+ *                              addJSOnStart
+ *                              add3rdPartyExitScript
+ *                              addJSOnExit
+ * 2012-04-11 Eric Oulashin     Fixed a bug with quote line wrapping where it
+ *                              was incorrectly dealing with quote lines that
+ *                              were blank after the quote text.
+ * 2012-12-21 Eric Oulashin     Updated to check for the .cfg files in the
+ *                              /sbbs/ctrl directory first, and if they aren't
+ *                              there, assume they're in the same directory as
+ *                              the .js file.
+ */
+
+// Note: These variables are declared with "var" instead of "const" to avoid
+// multiple declaration errors when this file is loaded more than once.
+
+// Values for attribute types (for text attribute substitution)
+var FORE_ATTR = 1; // Foreground color attribute
+var BKG_ATTR = 2;  // Background color attribute
+var SPECIAL_ATTR = 3; // Special attribute
+
+// Box-drawing/border characters: Single-line
+var UPPER_LEFT_SINGLE = "�";
+var HORIZONTAL_SINGLE = "�";
+var UPPER_RIGHT_SINGLE = "�";
+var VERTICAL_SINGLE = "�";
+var LOWER_LEFT_SINGLE = "�";
+var LOWER_RIGHT_SINGLE = "�";
+var T_SINGLE = "�";
+var LEFT_T_SINGLE = "�";
+var RIGHT_T_SINGLE = "�";
+var BOTTOM_T_SINGLE = "�";
+var CROSS_SINGLE = "�";
+// Box-drawing/border characters: Double-line
+var UPPER_LEFT_DOUBLE = "�";
+var HORIZONTAL_DOUBLE = "�";
+var UPPER_RIGHT_DOUBLE = "�";
+var VERTICAL_DOUBLE = "�";
+var LOWER_LEFT_DOUBLE = "�";
+var LOWER_RIGHT_DOUBLE = "�";
+var T_DOUBLE = "�";
+var LEFT_T_DOUBLE = "�";
+var RIGHT_T_DOUBLE = "�";
+var BOTTOM_T_DOUBLE = "�";
+var CROSS_DOUBLE = "�";
+// Box-drawing/border characters: Vertical single-line with horizontal double-line
+var UPPER_LEFT_VSINGLE_HDOUBLE = "�";
+var UPPER_RIGHT_VSINGLE_HDOUBLE = "�";
+var LOWER_LEFT_VSINGLE_HDOUBLE = "�";
+var LOWER_RIGHT_VSINGLE_HDOUBLE = "�";
+// Other special characters
+var DOT_CHAR = "�";
+var THIN_RECTANGLE_LEFT = "�";
+var THIN_RECTANGLE_RIGHT = "�";
+var BLOCK1 = "�"; // Dimmest block
+var BLOCK2 = "�";
+var BLOCK3 = "�";
+var BLOCK4 = "�"; // Brightest block
+
+// Navigational keys
+var UP_ARROW = "";
+var DOWN_ARROW = "";
+// CTRL keys
+var CTRL_A = "\x01";
+var CTRL_B = "\x02";
+//var KEY_HOME = CTRL_B;
+var CTRL_C = "\x03";
+var CTRL_D = "\x04";
+var CTRL_E = "\x05";
+//var KEY_END = CTRL_E;
+var CTRL_F = "\x06";
+//var KEY_RIGHT = CTRL_F;
+var CTRL_G = "\x07";
+var BEEP = CTRL_G;
+var CTRL_H = "\x08";
+var BACKSPACE = CTRL_H;
+var CTRL_I = "\x09";
+var TAB = CTRL_I;
+var CTRL_J = "\x0a";
+//var KEY_DOWN = CTRL_J;
+var CTRL_K = "\x0b";
+var CTRL_L = "\x0c";
+var INSERT_LINE = CTRL_L;
+var CTRL_M = "\x0d";
+var CR = CTRL_M;
+var KEY_ENTER = CTRL_M;
+var CTRL_N = "\x0e";
+var CTRL_O = "\x0f";
+var CTRL_P = "\x10";
+var CTRL_Q = "\x11";
+var XOFF = CTRL_Q;
+var CTRL_R = "\x12";
+var CTRL_S = "\x13";
+var XON = CTRL_S;
+var CTRL_T = "\x14";
+var CTRL_U = "\x15";
+var CTRL_V = "\x16";
+var KEY_INSERT = CTRL_V;
+var CTRL_W = "\x17";
+var CTRL_X = "\x18";
+var CTRL_Y = "\x19";
+var CTRL_Z = "\x1a";
+var KEY_ESC = "\x1b";
+
+// gQuotePrefix contains the text to prepend to quote lines.
+var gQuotePrefix = " > ";
+
+///////////////////////////////////////////////////////////////////////////////////
+// Object/class stuff
+
+// TextLine object constructor: This is used to keep track of a text line,
+// and whether it has a hard newline at the end (i.e., if the user pressed
+// enter to break the line).
+//
+// Parameters (all optional):
+//  pText: The text for the line
+//  pHardNewlineEnd: Whether or not the line has a "hard newline" - What
+//                   this means is that text below it won't be wrapped up
+//                   to this line when re-adjusting the text lines.
+//  pIsQuoteLine: Whether or not the line is a quote line.
+function TextLine(pText, pHardNewlineEnd, pIsQuoteLine)
+{
+	this.text = "";               // The line text
+	this.hardNewlineEnd = false; // Whether or not the line has a hard newline at the end
+	this.isQuoteLine = false;    // Whether or not this is a quote line
+   // Copy the parameters if they are valid.
+   if ((pText != null) && (typeof(pText) == "string"))
+      this.text = pText;
+   if ((pHardNewlineEnd != null) && (typeof(pHardNewlineEnd) == "boolean"))
+      this.hardNewlineEnd = pHardNewlineEnd;
+   if ((pIsQuoteLine != null) && (typeof(pIsQuoteLine) == "boolean"))
+      this.isQuoteLine = pIsQuoteLine;
+
+	// NEW & EXPERIMENTAL:
+   // For color support
+   this.attrs = new Array(); // An array of attributes for the line
+   // Functions
+   this.length = TextLine_Length;
+   this.print = TextLine_Print;
+}
+// For the TextLine class: Returns the length of the text.
+function TextLine_Length()
+{
+   return this.text.length;
+}
+// For  the TextLine class: Prints the text line, using its text attributes.
+//
+// Parameters:
+//  pClearToEOL: Boolean - Whether or not to clear to the end of the line
+function TextLine_Print(pClearToEOL)
+{
+   console.print(this.text);
+
+   if (pClearToEOL)
+      console.cleartoeol();
+}
+
+// AbortConfirmFuncParams constructor: This object contains parameters used by
+// the abort confirmation function (actually, there are separate ones for
+// IceEdit and DCT Edit styles).
+function AbortConfirmFuncParams()
+{
+   this.editTop = gEditTop;
+   this.editBottom = gEditBottom;
+   this.editWidth = gEditWidth;
+   this.editHeight = gEditHeight;
+   this.editLinesIndex = gEditLinesIndex;
+   this.displayMessageRectangle = displayMessageRectangle;
+}
+
+
+///////////////////////////////////////////////////////////////////////////////////
+// Functions
+
+// This function takes a string and returns a copy of the string
+// with a color randomly alternating between dim & bright versions.
+//
+// Parameters:
+//  pString: The string to convert
+//  pColor: The color to use (Synchronet color code)
+function randomDimBrightString(pString, pColor)
+{
+	// Return if an invalid string is passed in.
+	if (pString == null)
+		return "";
+	if (typeof(pString) != "string")
+		return "";
+
+   // Set the color.  Default to green.
+	var color = "g";
+	if ((pColor != null) && (typeof(pColor) != "undefined"))
+      color = pColor;
+
+   return(randomTwoColorString(pString, "n" + color, "nh" + color));
+}
+
+// This function takes a string and returns a copy of the string
+// with colors randomly alternating between two given colors.
+//
+// Parameters:
+//  pString: The string to convert
+//  pColor11: The first color to use (Synchronet color code)
+//  pColor12: The second color to use (Synchronet color code)
+function randomTwoColorString(pString, pColor1, pColor2)
+{
+	// Return if an invalid string is passed in.
+	if (pString == null)
+		return "";
+	if (typeof(pString) != "string")
+		return "";
+
+	// Set the colors.  Default to green.
+	var color1 = "ng";
+	if ((pColor1 != null) && (typeof(pColor1) != "undefined"))
+      color1 = pColor1;
+   var color2 = "ngh";
+	if ((pColor2 != null) && (typeof(pColor2) != "undefined"))
+      color2 = pColor2;
+
+	// Create a copy of the string without any control characters,
+	// and then add our coloring to it.
+	pString = strip_ctrl(pString);
+	var returnString = color1;
+	var useColor1 = false;     // Whether or not to use the useColor1 version of the color1
+	var oldUseColor1 = useColor1; // The value of useColor1 from the last pass
+	for (var i = 0; i < pString.length; ++i)
+	{
+		// Determine if this character should be useColor1
+		useColor1 = (Math.floor(Math.random()*2) == 1);
+		if (useColor1 != oldUseColor1)
+         returnString += (useColor1 ? color1 : color2);
+
+		// Append the character from pString.
+		returnString += pString.charAt(i);
+
+		oldUseColor1 = useColor1;
+	}
+
+	return returnString;
+}
+
+// Returns the current time as a string, to be displayed on the screen.
+function getCurrentTimeStr()
+{
+	var timeStr = strftime("%I:%M%p", time());
+	timeStr = timeStr.replace("AM", "a");
+	timeStr = timeStr.replace("PM", "p");
+	
+	return timeStr;
+}
+
+// Returns whether or not a character is printable.
+function isPrintableChar(pText)
+{
+   // Make sure pText is valid and is a string.
+   if ((pText == null) || (pText == undefined))
+      return false;
+   if (typeof(pText) != "string")
+      return false;
+   if (pText.length == 0)
+      return false;
+
+   // Make sure the character is a printable ASCII character in the range of 32 to 254,
+   // except for 127 (delete).
+   var charCode = pText.charCodeAt(0);
+   return ((charCode > 31) && (charCode < 255) && (charCode != 127));
+}
+
+// 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)
+{
+   // Make sure pString is a string.
+   if (typeof(pString) == "string")
+   {
+      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;
+}
+
+// Displays the text to display above help screens.
+function displayHelpHeader()
+{
+   // Construct the header text lines only once.
+   if (typeof(displayHelpHeader.headerLines) == "undefined")
+   {
+      displayHelpHeader.headerLines = new Array();
+
+      var headerText = EDITOR_PROGRAM_NAME + " Help w(y"
+                      + (EDITOR_STYLE == "DCT" ? "DCT" : "Ice")
+                      + " modew)";
+      var headerTextLen = strip_ctrl(headerText).length;
+
+      // Top border
+      var headerTextStr = "nhc" + UPPER_LEFT_SINGLE;
+      for (var i = 0; i < headerTextLen + 2; ++i)
+         headerTextStr += HORIZONTAL_SINGLE;
+      headerTextStr += UPPER_RIGHT_SINGLE;
+      displayHelpHeader.headerLines.push(headerTextStr);
+
+      // Middle line: Header text string
+      headerTextStr = VERTICAL_SINGLE + "4y " + headerText + " nhc"
+                    + VERTICAL_SINGLE;
+      displayHelpHeader.headerLines.push(headerTextStr);
+
+      // Lower border
+      headerTextStr = LOWER_LEFT_SINGLE;
+      for (var i = 0; i < headerTextLen + 2; ++i)
+         headerTextStr += HORIZONTAL_SINGLE;
+      headerTextStr += LOWER_RIGHT_SINGLE;
+      displayHelpHeader.headerLines.push(headerTextStr);
+   }
+
+   // Print the header strings
+   for (var index in displayHelpHeader.headerLines)
+      console.center(displayHelpHeader.headerLines[index]);
+}
+
+// Displays the command help.
+//
+// Parameters:
+//  pDisplayHeader: Whether or not to display the help header.
+//  pClear: Whether or not to clear the screen first
+//  pPause: Whether or not to pause at the end
+//  pIsSysop: Whether or not the user is the sysop.
+function displayCommandList(pDisplayHeader, pClear, pPause, pIsSysop)
+{
+   if (pClear)
+      console.clear("n");
+   if (pDisplayHeader)
+   {
+      displayHelpHeader();
+      console.crlf();
+   }
+
+   var isSysop = false;
+   if (pIsSysop != null)
+      isSysop = pIsSysop;
+   else
+      isSysop = user.compare_ars("SYSOP");
+
+   // This function displays a key and its description with formatting & colors.
+   //
+   // Parameters:
+   //  pKey: The key description
+   //  pDesc: The description of the key's function
+   //  pCR: Whether or not to display a carriage return (boolean).  Optional;
+   //       if not specified, this function won't display a CR.
+   function displayCmdKeyFormatted(pKey, pDesc, pCR)
+   {
+      printf("ch%-13sg: nc%s", pKey, pDesc);
+      if (pCR)
+         console.crlf();
+   }
+   // This function does the same, but outputs 2 on the same line.
+   function displayCmdKeyFormattedDouble(pKey, pDesc, pKey2, pDesc2, pCR)
+   {
+      printf("ch%-13sg: nc%-28s kh" + VERTICAL_SINGLE +
+             " ch%-7sg: nc%s", pKey, pDesc, pKey2, pDesc2);
+      if (pCR)
+         console.crlf();
+   }
+
+   // Help keys and slash commands
+   printf("ng%-44s  %-33s\r\n", "Help keys", "Slash commands (on blank line)");
+   printf("kh%-44s  %-33s\r\n", "���������", "������������������������������");
+   displayCmdKeyFormattedDouble("Ctrl-G", "General help", "/A", "Abort", true);
+   displayCmdKeyFormattedDouble("Ctrl-P", "Command key help", "/S", "Save", true);
+   displayCmdKeyFormattedDouble("Ctrl-R", "Program information", "/Q", "Quote message", true);
+   printf(" ch%-7sg  nc%s", "", "", "/?", "Show help");
+   console.crlf();
+   // Command/edit keys
+   console.print("ngCommand/edit keys\r\nkh�����������������\r\n");
+   displayCmdKeyFormattedDouble("Ctrl-A", "Abort message", "Ctrl-W", "Page up", true);
+   displayCmdKeyFormattedDouble("Ctrl-Z", "Save message", "Ctrl-S", "Page down", true);
+   displayCmdKeyFormattedDouble("Ctrl-Q", "Quote message", "Ctrl-N", "Find text", true);
+   displayCmdKeyFormattedDouble("Insert/Ctrl-I", "Toggle insert/overwrite mode",
+                                "ESC", "Command menu", true);
+   if (isSysop)
+      displayCmdKeyFormattedDouble("Ctrl-O", "Import a file", "Ctrl-X", "Export to file", true);
+   displayCmdKeyFormatted("Ctrl-D", "Delete line", true);
+
+   if (pPause)
+      console.pause();
+}
+
+// Displays the general help screen.
+//
+// Parameters:
+//  pDisplayHeader: Whether or not to display the help header.
+//  pClear: Whether or not to clear the screen first
+//  pPause: Whether or not to pause at the end
+function displayGeneralHelp(pDisplayHeader, pClear, pPause)
+{
+   if (pClear)
+      console.clear("n");
+   if (pDisplayHeader)
+      displayHelpHeader();
+
+   console.print("ncThis is a full-screen message editor that mimics the look & feel of\r\n");
+   console.print("IceEdit or DCT Edit, two popular editors.  The editor is currently in " +
+                 (EDITOR_STYLE == "DCT" ? "DCT" : "Ice") + "\r\nmode.\r\n");
+   console.print("At the top of the screen, information about the message being written (or\r\n");
+   console.print("file being edited) is displayed.  The middle section is the edit area,\r\n");
+   console.print("where the message/file is edited.  Finally, the bottom section displays\r\n");
+   console.print("some of the most common keys and/or status.");
+   console.crlf();
+   if (pPause)
+      console.pause();
+}
+
+// Displays the text to display above program info screens.
+function displayProgInfoHeader()
+{
+   // Construct the header text lines only once.
+   if (typeof(displayProgInfoHeader.headerLines) == "undefined")
+   {
+      displayProgInfoHeader.headerLines = new Array();
+
+      var progNameLen = strip_ctrl(EDITOR_PROGRAM_NAME).length;
+
+      // Top border
+      var headerTextStr = "nhc" + UPPER_LEFT_SINGLE;
+      for (var i = 0; i < progNameLen + 2; ++i)
+         headerTextStr += HORIZONTAL_SINGLE;
+      headerTextStr += UPPER_RIGHT_SINGLE;
+      displayProgInfoHeader.headerLines.push(headerTextStr);
+
+      // Middle line: Header text string
+      headerTextStr = VERTICAL_SINGLE + "4y " + EDITOR_PROGRAM_NAME + " nhc"
+                    + VERTICAL_SINGLE;
+      displayProgInfoHeader.headerLines.push(headerTextStr);
+
+      // Lower border
+      headerTextStr = LOWER_LEFT_SINGLE;
+      for (var i = 0; i < progNameLen + 2; ++i)
+         headerTextStr += HORIZONTAL_SINGLE;
+      headerTextStr += LOWER_RIGHT_SINGLE;
+      displayProgInfoHeader.headerLines.push(headerTextStr);
+   }
+
+   // Print the header strings
+   for (var index in displayProgInfoHeader.headerLines)
+      console.center(displayProgInfoHeader.headerLines[index]);
+}
+
+// Displays program information.
+//
+// Parameters:
+//  pDisplayHeader: Whether or not to display the help header.
+//  pClear: Whether or not to clear the screen first
+//  pPause: Whether or not to pause at the end
+function displayProgramInfo(pDisplayHeader, pClear, pPause)
+{
+   if (pClear)
+      console.clear("n");
+   if (pDisplayHeader)
+      displayProgInfoHeader();
+
+   // Print the program information
+   console.center("ncVersion g" + EDITOR_VERSION + " wh(b" +
+                  EDITOR_VER_DATE + "w)");
+   console.center("ncby Eric Oulashin");
+   console.crlf();
+   console.print("ncThis is a full-screen message editor written for Synchronet that mimics\r\n");
+   console.print("the look & feel of IceEdit or DCT Edit.");
+   console.crlf();
+   if (pPause)
+      console.pause();
+}
+
+// Displays the informational screen for the program exit.
+//
+// Parameters:
+//  pClearScreen: Whether or not to clear the screen.
+function displayProgramExitInfo(pClearScreen)
+{
+	if (pClearScreen)
+		console.clear("n");
+
+   console.print("ncYou have been using:\r\n");
+   console.print("hk�7����������������������������������0�\r\n");
+   console.print("�7 nb7����� �       �����    � �       hk0�\r\n");
+   console.print("�7 nb7����  � �   � ����   ��� � ����� hk0�\r\n");
+   console.print("�7     nb7� � �   � �     �  � �   �   hk0�\r\n");
+   console.print("�7 nb7����  �  ���  �����  ��� �   ��� hk0�\r\n");
+   console.print("�7         nb7��                       hk0�\r\n");
+   console.print("�7        nb7�                         hk0�\r\n");
+   console.print("������������������������������������\r\n");
+   console.print("ngVersion hy" + EDITOR_VERSION + " nm(" +
+	              EDITOR_VER_DATE + ")");
+   console.crlf();
+	console.print("nbhby Eric Oulashin nwof chDncigital hDncistortion hBncBS");
+	console.crlf();
+	console.crlf();
+	console.print("ncAcknowledgements for look & feel go to the following people:");
+	console.crlf();
+	console.print("Dan Traczynski: Creator of DCT Edit");
+	console.crlf();
+	console.print("Jeremy Landvoigt: Original creator of IceEdit");
+	console.crlf();
+}
+
+// Writes some text on the screen at a given location with a given pause.
+//
+// Parameters:
+//  pX: The column number on the screen at which to write the message
+//  pY: The row number on the screen at which to write the message
+//  pText: The text to write
+//  pPauseMS: The pause time, in milliseconds
+//  pClearLineAttrib: Optional - The color/attribute to clear the line with.
+//                    If not specified, defaults to normal attribute.
+function writeWithPause(pX, pY, pText, pPauseMS, pClearLineAttrib)
+{
+   var clearLineAttrib = "n";
+   if ((pClearLineAttrib != null) && (typeof(pClearLineAttrib) == "string"))
+      clearLineAttrib = pClearLineAttrib;
+   console.gotoxy(pX, pY);
+   console.cleartoeol(clearLineAttrib);
+   console.print(pText);
+   mswait(pPauseMS);
+}
+
+// Prompts the user for a yes/no question.
+//
+// Parameters:
+//  pQuestion: The question to ask the user
+//  pDefaultYes: Boolean - Whether or not the default should be Yes.
+//               For false, the default will be No.
+//  pBoxTitle: For DCT mode, this specifies the title to use for the
+//             prompt box.  This is optional; if this is left out,
+//             the prompt box title will default to "Prompt".
+//
+// Return value: Boolean - true for a "Yes" answer, false for "No"
+function promptYesNo(pQuestion, pDefaultYes, pBoxTitle)
+{
+   var userResponse = pDefaultYes;
+
+   if (EDITOR_STYLE == "DCT")
+   {
+      // We need to create an object of parameters to pass to the DCT-style
+      // Yes/No function.
+      var paramObj = new AbortConfirmFuncParams();
+      paramObj.editLinesIndex = gEditLinesIndex;
+      if (typeof(pBoxTitle) == "string")
+         userResponse = promptYesNo_DCTStyle(pQuestion, pBoxTitle, pDefaultYes, paramObj);
+      else
+         userResponse = promptYesNo_DCTStyle(pQuestion, "Prompt", pDefaultYes, paramObj);
+   }
+   else if (EDITOR_STYLE == "ICE")
+   {
+      const originalCurpos = console.getxy();
+      // Go to the bottom line on the screen and prompt the user
+      console.gotoxy(1, console.screen_rows);
+      console.cleartoeol();
+      console.gotoxy(1, console.screen_rows);
+      userResponse = promptYesNo_IceStyle(pQuestion, pDefaultYes);
+      // If the user chose "No", then re-display the bottom help line and
+      // move the cursor back to its original position.
+      if (!userResponse)
+      {
+         fpDisplayBottomHelpLine(console.screen_rows, gUseQuotes);
+         console.gotoxy(originalCurpos);
+      }
+   }
+
+   return userResponse;
+}
+
+// Reads the SlyEdit configuration settings from SlyEdit.cfg.
+//
+// Return value: An object containing the settings as properties.
+function ReadSlyEditConfigFile()
+{
+   // Create the configuration object
+   var cfgObj = new Object();
+   cfgObj.thirdPartyLoadOnStart = new Array();
+   cfgObj.runJSOnStart = new Array();
+   cfgObj.thirdPartyLoadOnExit = new Array();
+   cfgObj.runJSOnExit = new Array();
+   cfgObj.displayEndInfoScreen = true;
+   cfgObj.userInputTimeout = true;
+   cfgObj.inputTimeoutMS = 300000;
+   cfgObj.reWrapQuoteLines = true;
+   cfgObj.allowColorSelection = true;
+   // Ice-style colors
+   cfgObj.iceColors = new Object();
+   // Ice color theme file
+   cfgObj.iceColors.ThemeFilename = system.ctrl_dir + "SlyIceColors_BlueIce.cfg";
+   if (!file_exists(cfgObj.iceColors.ThemeFilename))
+      cfgObj.iceColors.ThemeFilename = gStartupPath + "SlyIceColors_BlueIce.cfg";
+   // Text edit color
+   cfgObj.iceColors.TextEditColor = "nw";
+   // Quote line color
+   cfgObj.iceColors.QuoteLineColor = "nc";
+   // Ice colors for the quote window
+   cfgObj.iceColors.QuoteWinText = "nhw";            // White
+   cfgObj.iceColors.QuoteLineHighlightColor = "4hc"; // High cyan on blue background
+   cfgObj.iceColors.QuoteWinBorderTextColor = "nch"; // Bright cyan
+   cfgObj.iceColors.BorderColor1 = "nb";              // Blue
+   cfgObj.iceColors.BorderColor2 = "nbh";          // Bright blue
+   // Ice colors for multi-choice prompts
+   cfgObj.iceColors.SelectedOptionBorderColor = "nbh4";
+   cfgObj.iceColors.SelectedOptionTextColor = "nch4"
+   cfgObj.iceColors.UnselectedOptionBorderColor = "nb";
+   cfgObj.iceColors.UnselectedOptionTextColor = "nw";
+   // Ice colors for the top info area
+   cfgObj.iceColors.TopInfoBkgColor = "4";
+   cfgObj.iceColors.TopLabelColor = "ch";
+   cfgObj.iceColors.TopLabelColonColor = "bh";
+   cfgObj.iceColors.TopToColor = "wh";
+   cfgObj.iceColors.TopFromColor = "wh";
+   cfgObj.iceColors.TopSubjectColor = "wh";
+   cfgObj.iceColors.TopTimeColor = "gh";
+   cfgObj.iceColors.TopTimeLeftColor = "gh";
+   cfgObj.iceColors.EditMode = "ch";
+   cfgObj.iceColors.KeyInfoLabelColor = "ch";
+
+   // DCT-style colors
+   cfgObj.DCTColors = new Object();
+   // DCT color theme file
+   cfgObj.DCTColors.ThemeFilename = system.ctrl_dir + "SlyDCTColors_Default.cfg";
+   if (!file_exists(cfgObj.DCTColors.ThemeFilename))
+      cfgObj.DCTColors.ThemeFilename = gStartupPath + "SlyDCTColors_Default.cfg";
+   // Text edit color
+   cfgObj.DCTColors.TextEditColor = "nw";
+   // Quote line color
+   cfgObj.DCTColors.QuoteLineColor = "nc";
+   // DCT colors for the border stuff
+   cfgObj.DCTColors.TopBorderColor1 = "nr";
+   cfgObj.DCTColors.TopBorderColor2 = "nrh";
+   cfgObj.DCTColors.EditAreaBorderColor1 = "ng";
+   cfgObj.DCTColors.EditAreaBorderColor2 = "ngh";
+   cfgObj.DCTColors.EditModeBrackets = "nkh";
+   cfgObj.DCTColors.EditMode = "nw";
+   // DCT colors for the top informational area
+   cfgObj.DCTColors.TopLabelColor = "nbh";
+   cfgObj.DCTColors.TopLabelColonColor = "nb";
+   cfgObj.DCTColors.TopFromColor = "nch";
+   cfgObj.DCTColors.TopFromFillColor = "nc";
+   cfgObj.DCTColors.TopToColor = "nch";
+   cfgObj.DCTColors.TopToFillColor = "nc";
+   cfgObj.DCTColors.TopSubjColor = "nwh";
+   cfgObj.DCTColors.TopSubjFillColor = "nw";
+   cfgObj.DCTColors.TopAreaColor = "ngh";
+   cfgObj.DCTColors.TopAreaFillColor = "ng";
+   cfgObj.DCTColors.TopTimeColor = "nyh";
+   cfgObj.DCTColors.TopTimeFillColor = "nr";
+   cfgObj.DCTColors.TopTimeLeftColor = "nyh";
+   cfgObj.DCTColors.TopTimeLeftFillColor = "nr";
+   cfgObj.DCTColors.TopInfoBracketColor = "nm";
+   // DCT colors for the quote window
+   cfgObj.DCTColors.QuoteWinText = "n7k";
+   cfgObj.DCTColors.QuoteLineHighlightColor = "nw";
+   cfgObj.DCTColors.QuoteWinBorderTextColor = "n7r";
+   cfgObj.DCTColors.QuoteWinBorderColor = "nk7";
+   // DCT colors for the quote window
+   cfgObj.DCTColors.QuoteWinText = "n7b";
+   cfgObj.DCTColors.QuoteLineHighlightColor = "nw";
+   cfgObj.DCTColors.QuoteWinBorderTextColor = "n7r";
+   cfgObj.DCTColors.QuoteWinBorderColor = "nk7";
+   // DCT colors for the bottom row help text
+   cfgObj.DCTColors.BottomHelpBrackets = "nkh";
+   cfgObj.DCTColors.BottomHelpKeys = "nrh";
+   cfgObj.DCTColors.BottomHelpFill = "nr";
+   cfgObj.DCTColors.BottomHelpKeyDesc = "nc";
+   // DCT colors for text boxes
+   cfgObj.DCTColors.TextBoxBorder = "nk7";
+   cfgObj.DCTColors.TextBoxBorderText = "nr7";
+   cfgObj.DCTColors.TextBoxInnerText = "nb7";
+   cfgObj.DCTColors.YesNoBoxBrackets = "nk7";
+   cfgObj.DCTColors.YesNoBoxYesNoText = "nwh7";
+   // DCT colors for the menus
+   cfgObj.DCTColors.SelectedMenuLabelBorders = "nw";
+   cfgObj.DCTColors.SelectedMenuLabelText = "nk7";
+   cfgObj.DCTColors.UnselectedMenuLabelText = "nwh";
+   cfgObj.DCTColors.MenuBorders = "nk7";
+   cfgObj.DCTColors.MenuSelectedItems = "nw";
+   cfgObj.DCTColors.MenuUnselectedItems = "nk7";
+   cfgObj.DCTColors.MenuHotkeys = "nwh7";
+
+   // Open the configuration file
+   var ctrlCfgFileName = system.ctrl_dir + "SlyEdit.cfg";
+   var cfgFile = new File(file_exists(ctrlCfgFileName) ? ctrlCfgFileName : gStartupPath + "SlyEdit.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)
+      var valueUpper = null;   // Upper-cased value
+      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 for some reason it isn't.  If it's not a string,
+         // then continue onto the next line.
+         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() == "[ICE_COLORS]")
+         {
+            settingsMode = "ICEColors";
+            continue;
+         }
+         else if (fileLine.toUpperCase() == "[DCT_COLORS]")
+         {
+            settingsMode = "DCTColors";
+            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);
+            valueUpper = value.toUpperCase();
+
+            if (settingsMode == "behavior")
+            {
+               if (settingUpper == "DISPLAYENDINFOSCREEN")
+                  cfgObj.displayEndInfoScreen = (valueUpper == "TRUE");
+               else if (settingUpper == "USERINPUTTIMEOUT")
+                  cfgObj.userInputTimeout = (valueUpper == "TRUE");
+               else if (settingUpper == "INPUTTIMEOUTMS")
+                  cfgObj.inputTimeoutMS = +value;
+               else if (settingUpper == "REWRAPQUOTELINES")
+                  cfgObj.reWrapQuoteLines = (valueUpper == "TRUE");
+               else if (settingUpper == "ALLOWCOLORSELECTION")
+                  cfgObj.allowColorSelection = (valueUpper == "TRUE");
+               else if (settingUpper == "ADD3RDPARTYSTARTUPSCRIPT")
+                  cfgObj.thirdPartyLoadOnStart.push(value);
+               else if (settingUpper == "ADD3RDPARTYEXITSCRIPT")
+                  cfgObj.thirdPartyLoadOnExit.push(value);
+               else if (settingUpper == "ADDJSONSTART")
+                  cfgObj.runJSOnStart.push(value);
+               else if (settingUpper == "ADDJSONEXIT")
+                  cfgObj.runJSOnExit.push(value);
+            }
+            else if (settingsMode == "ICEColors")
+            {
+               if (settingUpper == "THEMEFILENAME")
+               {
+                  //system.ctrl_dir
+                  //gStartupPath
+                  cfgObj.iceColors.ThemeFilename = system.ctrl_dir + value;
+                  if (!file_exists(cfgObj.iceColors.ThemeFilename))
+                     cfgObj.iceColors.ThemeFilename = gStartupPath + value;
+               }
+            }
+            else if (settingsMode == "DCTColors")
+            {
+               if (settingUpper == "THEMEFILENAME")
+               {
+                  cfgObj.DCTColors.ThemeFilename = system.ctrl_dir + value;
+                  if (!file_exists(cfgObj.DCTColors.ThemeFilename))
+                     cfgObj.DCTColors.ThemeFilename = gStartupPath + value;
+               }
+            }
+         }
+      }
+
+      cfgFile.close();
+
+      // Validate the settings
+      if (cfgObj.inputTimeoutMS < 1000)
+         cfgObj.inputTimeoutMS = 300000;
+   }
+
+   return cfgObj;
+}
+
+// This function reads a configuration file containing
+// setting=value pairs and returns the settings in
+// an Object.
+//
+// Parameters:
+//  pFilename: The name of the configuration file.
+//  pLineReadLen: The maximum number of characters to read from each
+//                line.  This is optional; if not specified, then up
+//                to 512 characters will be read from each line.
+//
+// Return value: An Object containing the value=setting pairs.  If the
+//               file can't be opened or no settings can be read, then
+//               this function will return null.
+function readValueSettingConfigFile(pFilename, pLineReadLen)
+{
+   var retObj = null;
+
+   var cfgFile = new File(pFilename);
+   if (cfgFile.open("r"))
+   {
+      // Set the number of characters to read per line.
+      var numCharsPerLine = 512;
+      if (pLineReadLen != null)
+         numCharsPerLine = pLineReadLen;
+
+      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)
+      var valueUpper = null;   // Upper-cased value
+      while (!cfgFile.eof)
+      {
+         // Read the next line from the config file.
+         fileLine = cfgFile.readln(numCharsPerLine);
+
+         // fileLine should be a string, but I've seen some cases
+         // where it isn't, so check its type.
+         if (typeof(fileLine) != "string")
+            continue;
+
+         // If the line starts with with a semicolon (the comment
+         // character) or is blank, then skip it.
+         if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
+            continue;
+
+         // If the line has a semicolon anywhere in it, then remove
+         // everything from the semicolon onward.
+         commentPos = fileLine.indexOf(";");
+         if (commentPos > -1)
+            fileLine = fileLine.substr(0, commentPos);
+
+         // Look for an equals sign, and if found, separate the line
+         // into the setting name (before the =) and the value (after the
+         // equals sign).
+         equalsPos = fileLine.indexOf("=");
+         if (equalsPos > 0)
+         {
+            // If retObj hasn't been created yet, then create it.
+            if (retObj == null)
+               retObj = new Object();
+
+            // Read the setting & value, and trim leading & trailing spaces.  Then
+            // set the value in retObj.
+            setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true);
+            value = trimSpaces(fileLine.substr(equalsPos+1), true, false, true);
+            retObj[setting] = value;
+         }
+      }
+
+      cfgFile.close();
+   }
+
+   return retObj;
+}
+
+// Splits a string up by a maximum length, preserving whole words.
+//
+// Parameters:
+//  pStr: The string to split
+//  pMaxLen: The maximum length for the strings (strings longer than this
+//           will be split)
+//
+// Return value: An array of strings resulting from the string split
+function splitStrStable(pStr, pMaxLen)
+{
+   var strings = new Array();
+
+   // Error checking
+   if (typeof(pStr) != "string")
+   {
+      console.print("1 - pStr not a string!\r\n");
+      return strings;
+   }
+
+   // If the string's length is less than or equal to pMaxLen, then
+   // just insert it into the strings array.  Otherwise, we'll
+   // need to split it.
+   if (pStr.length <= pMaxLen)
+      strings.push(pStr);
+   else
+   {
+      // Make a copy of pStr so that we don't modify it.
+      var theStr = pStr;
+
+      var tempStr = "";
+      var splitIndex = 0; // Index of a space in a string
+      while (theStr.length > pMaxLen)
+      {
+         // If there isn't a space at the pMaxLen location in theStr,
+         // then assume there's a word there and look for a space
+         // before it.
+         splitIndex = pMaxLen;
+         if (theStr.charAt(splitIndex) != " ")
+         {
+            splitIndex = theStr.lastIndexOf(" ", splitIndex);
+            // If a space was not found, then we should split at
+            // pMaxLen.
+            if (splitIndex == -1)
+               splitIndex = pMaxLen;
+         }
+
+         // Extract the first part of theStr up to splitIndex into
+         // tempStr, and then remove that part from theStr.
+         tempStr = theStr.substr(0, splitIndex);
+         theStr = theStr.substr(splitIndex+1);
+
+         // If tempStr is not blank, then insert it into the strings
+         // array.
+         if (tempStr.length > 0)
+            strings.push(tempStr);
+      }
+      // Edge case: If theStr is not blank, then insert it into the
+      // strings array.
+      if (theStr.length > 0)
+         strings.push(theStr);
+   }
+
+   return strings;
+}
+
+// Inserts a string inside another string.
+//
+// Parameters:
+//  pStr: The string inside which to insert the other string
+//  pIndex: The index of pStr at which to insert the other string
+//  pStr2: The string to insert into the first string
+//
+// Return value: The spliced string
+function spliceIntoStr(pStr, pIndex, pStr2)
+{
+   // Error checking
+   var typeofPStr = typeof(pStr);
+   var typeofPStr2 = typeof(pStr2);
+   if ((typeofPStr != "string") && (typeofPStr2 != "string"))
+      return "";
+   else if ((typeofPStr == "string") && (typeofPStr2 != "string"))
+      return pStr;
+   else if ((typeofPStr != "string") && (typeofPStr2 == "string"))
+      return pStr2;
+   // If pIndex is beyond the last index of pStr, then just return the
+   // two strings concatenated.
+   if (pIndex >= pStr.length)
+      return (pStr + pStr2);
+   // If pIndex is below 0, then just return pStr2 + pStr.
+   else if (pIndex < 0)
+      return (pStr2 + pStr);
+
+   return (pStr.substr(0, pIndex) + pStr2 + pStr.substr(pIndex));
+}
+
+// Fixes the text lines in the gEditLines array so that they all
+// have a maximum width to fit within the edit area.
+//
+// Parameters:
+//  pTextLineArray: An array of TextLine objects to adjust
+//  pStartIndex: The index of the line in the array to start at.
+//  pEndIndex: One past the last index of the line in the array to end at.
+//  pEditWidth: The width of the edit area (AKA the maximum line length + 1)
+//
+// Return value: Boolean - Whether or not any text was changed.
+function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth)
+{
+   // Returns without doing anything if any of the parameters are not
+   // what they should be. (Note: Not checking pTextLineArray for now..)
+   if (typeof(pStartIndex) != "number")
+      return false;
+   if (typeof(pEndIndex) != "number")
+      return false;
+   if (typeof(pEditWidth) != "number")
+      return false;
+   // Range checking
+   if ((pStartIndex < 0) || (pStartIndex >= pTextLineArray.length))
+      return false;
+   if ((pEndIndex <= pStartIndex) || (pEndIndex < 0))
+      return false;
+   if (pEndIndex > pTextLineArray.length)
+      pEndIndex = pTextLineArray.length;
+   if (pEditWidth <= 5)
+      return false;
+
+   var textChanged = false; // We'll return this upon function exit.
+
+   var nextLineIndex = 0;
+   var charsToRemove = 0;
+   var splitIndex = 0;
+   var spaceFound = false;      // Whether or not a space was found in a text line
+   var splitIndexOriginal = 0;
+   var tempText = null;
+   var appendedNewLine = false; // If we appended another line
+   for (var i = pStartIndex; i < pEndIndex; ++i)
+   {
+      // As an extra precaution, check to make sure this array element is defined.
+      if (pTextLineArray[i] == undefined)
+         continue;
+
+      nextLineIndex = i + 1;
+      // If the line's text is longer or equal to the edit width, then if
+      // possible, move the last word to the beginning of the next line.
+      if (pTextLineArray[i].text.length >= pEditWidth)
+      {
+         charsToRemove = pTextLineArray[i].text.length - pEditWidth + 1;
+         splitIndex = pTextLineArray[i].text.length - charsToRemove;
+         splitIndexOriginal = splitIndex;
+         // If the character in the text line at splitIndex is not a space,
+         // then look for a space before splitIndex.
+         spaceFound = (pTextLineArray[i].text.charAt(splitIndex) == " ");
+         if (!spaceFound)
+         {
+            splitIndex = pTextLineArray[i].text.lastIndexOf(" ", splitIndex-1);
+            spaceFound = (splitIndex > -1);
+            if (!spaceFound)
+               splitIndex = splitIndexOriginal;
+         }
+         tempText = pTextLineArray[i].text.substr(spaceFound ? splitIndex+1 : splitIndex);
+         pTextLineArray[i].text = pTextLineArray[i].text.substr(0, splitIndex);
+         textChanged = true;
+         // If we're on the last line, or if the current line has a hard
+         // newline or is a quote line, then append a new line below.
+         appendedNewLine = false;
+         if ((nextLineIndex == pTextLineArray.length) || pTextLineArray[i].hardNewlineEnd ||
+             isQuoteLine(pTextLineArray, i))
+         {
+            pTextLineArray.splice(nextLineIndex, 0, new TextLine());
+            pTextLineArray[nextLineIndex].hardNewlineEnd = pTextLineArray[i].hardNewlineEnd;
+            pTextLineArray[i].hardNewlineEnd = false;
+            pTextLineArray[nextLineIndex].isQuoteLine = pTextLineArray[i].isQuoteLine;
+            appendedNewLine = true;
+         }
+
+         // Move the text around and adjust the line properties.
+         if (appendedNewLine)
+            pTextLineArray[nextLineIndex].text = tempText;
+         else
+         {
+            // If we're in insert mode, then insert the text at the beginning of
+            // the next line.  Otherwise, overwrite the text in the next line.
+            if (inInsertMode())
+               pTextLineArray[nextLineIndex].text = tempText + " " + pTextLineArray[nextLineIndex].text;
+            else
+            {
+               // We're in overwrite mode, so overwite the first part of the next
+               // line with tempText.
+               if (pTextLineArray[nextLineIndex].text.length < tempText.length)
+                  pTextLineArray[nextLineIndex].text = tempText;
+               else
+               {
+                  pTextLineArray[nextLineIndex].text = tempText
+                                           + pTextLineArray[nextLineIndex].text.substr(tempText.length);
+               }
+            }
+         }
+      }
+      else
+      {
+         // pTextLineArray[i].text.length is < pEditWidth, so try to bring up text
+         // from the next line.
+
+         // Only do it if the line doesn't have a hard newline and it's not a
+         // quote line and there is a next line.
+         if (!pTextLineArray[i].hardNewlineEnd && !isQuoteLine(pTextLineArray, i) &&
+             (i < pTextLineArray.length-1))
+         {
+            if (pTextLineArray[nextLineIndex].text.length > 0)
+            {
+               splitIndex = pEditWidth - pTextLineArray[i].text.length - 2;
+               // If splitIndex is negative, that means the entire next line
+               // can fit on the current line.
+               if ((splitIndex < 0) || (splitIndex > pTextLineArray[nextLineIndex].text.length))
+                  splitIndex = pTextLineArray[nextLineIndex].text.length;
+               else
+               {
+                  // If the character in the next line at splitIndex is not a
+                  // space, then look for a space before it.
+                  if (pTextLineArray[nextLineIndex].text.charAt(splitIndex) != " ")
+                     splitIndex = pTextLineArray[nextLineIndex].text.lastIndexOf(" ", splitIndex);
+                  // If no space was found, then skip to the next line (we don't
+                  // want to break up words from the next line).
+                  if (splitIndex == -1)
+                     continue;
+               }
+
+               // Get the text to bring up to the current line.
+               // If the current line does not end with a space and the next line
+               // does not start with a space, then add a space between this line
+               // and the next line's text.  This is done to avoid joining words
+               // accidentally.
+               tempText = "";
+               if ((pTextLineArray[i].text.charAt(pTextLineArray[i].text.length-1) != " ") &&
+                   (pTextLineArray[nextLineIndex].text.substr(0, 1) != " "))
+               {
+                  tempText = " ";
+               }
+               tempText += pTextLineArray[nextLineIndex].text.substr(0, splitIndex);
+               // Move the text from the next line to the current line, if the current
+               // line has room for it.
+               if (pTextLineArray[i].text.length + tempText.length < pEditWidth)
+               {
+                  pTextLineArray[i].text += tempText;
+                  pTextLineArray[nextLineIndex].text = pTextLineArray[nextLineIndex].text.substr(splitIndex+1);
+                  textChanged = true;
+
+                  // If the next line is now blank, then remove it.
+                  if (pTextLineArray[nextLineIndex].text.length == 0)
+                  {
+                     // The current line should take on the next line's
+                     // hardnewlineEnd property before removing the next line.
+                     pTextLineArray[i].hardNewlineEnd = pTextLineArray[nextLineIndex].hardNewlineEnd;
+                     pTextLineArray.splice(nextLineIndex, 1);
+                  }
+               }
+            }
+            else
+            {
+               // The next line's text string is blank.  If its hardNewlineEnd
+               // property is false, then remove the line.
+               if (!pTextLineArray[nextLineIndex].hardNewlineEnd)
+               {
+                  pTextLineArray.splice(nextLineIndex, 1);
+                  textChanged = true;
+               }
+            }
+         }
+      }
+   }
+
+   return textChanged;
+}
+
+// Returns indexes of the first unquoted text line and the next
+// quoted text line in an array of text lines.
+//
+// Parameters:
+//  pTextLineArray: An array of TextLine objects
+//  pStartIndex: The index of where to start looking in the array
+//  pQuotePrefix: The quote line prefix (string)
+//
+// Return value: An object containing the following properties:
+//               noQuoteLineIndex: The index of the next non-quoted line.
+//                                 Will be -1 if none are found.
+//               nextQuoteLineIndex: The index of the next quoted line.
+//                                   Will be -1 if none are found.
+function quotedLineIndexes(pTextLineArray, pStartIndex, pQuotePrefix)
+{
+   var retObj = new Object();
+   retObj.noQuoteLineIndex = -1;
+   retObj.nextQuoteLineIndex = -1;
+
+   if (pTextLineArray.length == 0)
+      return retObj;
+   if (typeof(pStartIndex) != "number")
+      return retObj;
+   if (pStartIndex >= pTextLineArray.length)
+      return retObj;
+
+   var startIndex = (pStartIndex > -1 ? pStartIndex : 0);
+
+   // Look for the first non-quoted line in the array.
+   retObj.noQuoteLineIndex = startIndex;
+   for (; retObj.noQuoteLineIndex < pTextLineArray.length; ++retObj.noQuoteLineIndex)
+   {
+      if (pTextLineArray[retObj.noQuoteLineIndex].text.indexOf(pQuotePrefix) == -1)
+         break;
+   }
+   // If the index is pTextLineArray.length, then what we're looking for wasn't
+   // found, so set the index to -1.
+   if (retObj.noQuoteLineIndex == pTextLineArray.length)
+      retObj.noQuoteLineIndex = -1;
+
+   // Look for the next quoted line in the array.
+   // If we found a non-quoted line, then use that index; otherwise,
+   // start at the first line.
+   if (retObj.noQuoteLineIndex > -1)
+      retObj.nextQuoteLineIndex = retObj.noQuoteLineIndex;
+   else
+      retObj.nextQuoteLineIndex = 0;
+   for (; retObj.nextQuoteLineIndex < pTextLineArray.length; ++retObj.nextQuoteLineIndex)
+   {
+      if (pTextLineArray[retObj.nextQuoteLineIndex].text.indexOf(pQuotePrefix) == 0)
+         break;
+   }
+   // If the index is pTextLineArray.length, then what we're looking for wasn't
+   // found, so set the index to -1.
+   if (retObj.nextQuoteLineIndex == pTextLineArray.length)
+      retObj.nextQuoteLineIndex = -1;
+
+   return retObj;
+}
+
+// Returns whether a line in an array of TextLine objects is a quote line.
+// This is true if the line's isQuoteLine property is true or the line's text
+// starts with > (preceded by any # of spaces).
+//
+// Parameters:
+//  pLineArray: An array of TextLine objects
+//  pLineIndex: The index of the line in gEditLines
+function isQuoteLine(pLineArray, pLineIndex)
+{
+   if (typeof(pLineArray) == "undefined")
+      return false;
+   if (typeof(pLineIndex) != "number")
+      return false;
+
+   var lineIsQuoteLine = false;
+   if (typeof(pLineArray[pLineIndex]) != "undefined")
+   {
+      /*
+      lineIsQuoteLine = ((pLineArray[pLineIndex].isQuoteLine) ||
+                     (/^ *>/.test(pLineArray[pLineIndex].text)));
+      */
+      lineIsQuoteLine = (pLineArray[pLineIndex].isQuoteLine);
+   }
+   return lineIsQuoteLine;
+}
+
+// Replaces an attribute in a text attribute string.
+//
+// Parameters:
+//  pAttrType: Numeric:
+//             FORE_ATTR: Foreground attribute
+//             BKG_ATTR: Background attribute
+//             3: Special attribute
+//  pAttrs: The attribute string to change
+//  pNewAttr: The new attribute to put into the attribute string (without the
+//            control character)
+function toggleAttr(pAttrType, pAttrs, pNewAttr)
+{
+   // Removes an attribute from an attribute string, if it
+   // exists.  Returns the new attribute string.
+   function removeAttrIfExists(pAttrs, pNewAttr)
+   {
+      var index = pAttrs.search(pNewAttr);
+      if (index > -1)
+         pAttrs = pAttrs.replace(pNewAttr, "");
+      return pAttrs;
+   }
+
+   // Convert pAttrs and pNewAttr to all uppercase for ease of searching
+   pAttrs = pAttrs.toUpperCase();
+   pNewAttr = pNewAttr.toUpperCase();
+
+   // If pAttrs starts with the normal attribute, then
+   // remove it (we'll put it back on later).
+   var normalAtStart = false;
+   if (pAttrs.search(/^N/) == 0)
+   {
+      normalAtStart = true;
+      pAttrs = pAttrs.substr(2);
+   }
+
+   // Prepend the attribute control character to the new attribute
+   var newAttr = "" + pNewAttr;
+
+   // Set a regex for searching & replacing
+   var regex = "";
+   switch (pAttrType)
+   {
+      case FORE_ATTR: // Foreground attribute
+         regex = /K|R|G|Y|B|M|C|W/g;
+         break;
+      case BKG_ATTR: // Background attribute
+         regex = /0|1|2|3|4|5|6|7/g;
+         break;
+      case SPECIAL_ATTR: // Special attribute
+         //regex = /H|I|N/g;
+         index = pAttrs.search(newAttr);
+         if (index > -1)
+            pAttrs = pAttrs.replace(newAttr, "");
+         else
+            pAttrs += newAttr;
+         break;
+      default:
+         break;
+   }
+
+   // If regex is not blank, then search & replace on it in
+   // pAttrs.
+   if (regex != "")
+   {
+      pAttrs = removeAttrIfExists(pAttrs, newAttr);
+      // If the regex is found, then replace it.  Otherwise,
+      // add pNewAttr to the attribute string.
+      if (pAttrs.search(regex) > -1)
+         pAttrs = pAttrs.replace(regex, "" + pNewAttr);
+      else
+         pAttrs += "" + pNewAttr;
+   }
+
+   // If pAttrs started with the normal attribute, then
+   // put it back on.
+   if (normalAtStart)
+      pAttrs = "N" + pAttrs;
+
+   return pAttrs;
+}
+
+// This function wraps an array of strings based on a line width.
+//
+// Parameters:
+//  pLineArr: An array of strings
+//  pStartLineIndex: The index of the text line in the array to start at
+//  pStopIndex: The index of where to stop in the array.  This is one past
+//              the last line in the array.  For example, to end at the
+//              last line in the array, use the array's .length property
+//              for this parameter.
+//  pLineWidth: The maximum width of each line
+//
+// Return value: The number of strings in lineArr
+function wrapTextLines(pLineArr, pStartLineIndex, pStopIndex, pLineWidth)
+{
+  // Validate parameters
+  if (pLineArr == null)
+    return 0;
+  if ((pStartLineIndex == null) || (typeof(pStartLineIndex) != "number") || (pStartLineIndex < 0))
+    pStartLineIndex = 0;
+  if (pStartLineIndex >= pLineArr.length)
+    return pLineArr.length;
+  if ((typeof(pStopIndex) != "number") || (pStopIndex == null) || (pStopIndex > pLineArr.length))
+    pStopIndex = pLineArr.length;
+
+  // Now for the actual code:
+  var trimLen = 0;   // The number of characters to trim from the end of a string
+  var trimIndex = 0; // The index of where to start trimming
+  for (var i = pStartLineIndex; i < pStopIndex; ++i)
+  {
+    // If the object in pLineArr is not a string for some reason, then skip it.
+    if (typeof(pLineArr[i]) != "string")
+      continue;
+
+    if (pLineArr[i].length > pLineWidth)
+    {
+      trimLen = pLineArr[i].length - pLineWidth;
+      trimIndex = pLineArr[i].lastIndexOf(" ", pLineArr[i].length - trimLen);
+      if (trimIndex == -1)
+        trimIndex = pLineArr[i].length - trimLen;
+      // Trim the text, and remove leading spaces from it too.
+      trimmedText = pLineArr[i].substr(trimIndex).replace(/^ +/, "");
+      pLineArr[i] = pLineArr[i].substr(0, trimIndex);
+      if (i < pLineArr.length - 1)
+      {
+        // If the next line is blank, then append another blank
+        // line there to preserve the message's formatting.
+        if (pLineArr[i+1].length == 0)
+          pLineArr.splice(i+1, 0, "");
+        else
+        {
+          // Since the next line is not blank, then append a space
+          // to the end of the trimmed text if it doesn't have one.
+          if (trimmedText.charAt(trimmedText.length-1) != " ")
+            trimmedText += " "
+        }
+        // Prepend the trimmed text to the next line.
+        pLineArr[i+1] = trimmedText + pLineArr[i+1];
+      }
+      else
+        pLineArr.push(trimmedText);
+    }
+  }
+  return pLineArr.length;
+}
+
+// Returns the index of a string for the first non-quote character.
+//
+// Parameters:
+//  pStr: A string to check
+//
+// Return value: An object containing the following properties:
+//               startIndex: The index of the first non-quote character in the string.
+//                           If pStr is an invalid string, or if a non-quote character
+//                           is not found, this will be -1.
+//               quoteLevel: The number of > characters at the start of the string
+function firstNonQuoteTxtIndex(pStr)
+{
+  // Create the return object with initial values.
+  var retObj = new Object();
+  retObj.startIndex = -1;
+  retObj.quoteLevel = 0;
+
+  // If pStr is not a valid positive-length string, then just return.
+  if ((pStr == null) || (typeof(pStr) != "string") || (pStr.length == 0))
+    return retObj;
+
+  // Look for quote lines that begin with 1 or 2 initials followed by a > (i.e.,
+  // "EO>" or "E>" at the start of the line.  If found, set an index to look for
+  // & count the > characters from the >.
+  var searchStartIndex = 0;
+  // Regex notes:
+  //  \w: Matches any alphanumerical character (word characters) including underscore (short for [a-zA-Z0-9_])
+  //  ?: Supposed to match 0 or 1 occurance, but seems to match 1 or 2
+  var lineStartsWithQuoteText = /^ *\w?[^ ]>/.test(pStr);
+  if (lineStartsWithQuoteText)
+  {
+    searchStartIndex = pStr.indexOf(">");
+    if (searchStartIndex < 0) searchStartIndex = 0;
+  }
+
+  // Look for the first non-quote text and quote level in the string.
+  var strChar = "";
+  var j = 0;
+  for (var i = searchStartIndex; i < pStr.length; ++i)
+  {
+    strChar = pStr.charAt(i);
+    if ((strChar != " ") && (strChar != ">"))
+    {
+      // We've found the first non-quote character.
+      retObj.startIndex = i;
+      // Count the number of times the > character appears at the start of
+      // the line, and set quoteLevel to that.
+      if (i >= 0)
+      {
+        for (j = 0; j < i; ++j)
+        {
+          if (pStr.charAt(j) == ">")
+            ++retObj.quoteLevel;
+        }
+      }
+      break;
+    }
+  }
+
+  // If we haven't found non-quote text but the line starts with quote text,
+  // then set the starting index & quote level in retObj.
+  //displayDebugText(1, 2, "Search start index: " + searchStartIndex, console.getxy(), true, true);
+  if (lineStartsWithQuoteText && ((retObj.startIndex == -1) || (retObj.quoteLevel == 0)))
+  {
+    retObj.startIndex = pStr.indexOf(">") + 1;
+    retObj.quoteLevel = 1;
+  }
+
+  return retObj;
+}
+
+function wrapQuoteLines()
+{
+  if (gQuoteLines.length == 0)
+    return;
+
+  // This function checks if a string has only > characters separated by
+  // whitespace and returns a version where the > characters are only separated
+  // by one space each, and if the line starts with " >", the leading space
+  // will be removed.
+  function normalizeGTChars(pStr)
+  {
+    if (/^\s*>\s*$/.test(pStr))
+      pStr = ">";
+    else
+    {
+      pStr = pStr.replace(/>\s*>/g, "> >")
+                 .replace(/^\s>/, ">")
+                 .replace(/^\s*$/, "");
+    }
+    return pStr;
+  }
+
+  // Create an array for line information objects, and append the
+  // first line's info to it.  Also, store the first line's quote
+  // level in the lastQuoteLevel variable.
+  var lineInfos = new Array();
+  var retObj = firstNonQuoteTxtIndex(gQuoteLines[0]);
+  lineInfos.push(retObj);
+  var lastQuoteLevel = retObj.quoteLevel;
+
+  // Loop through the array starting at the 2nd line and wrap the lines
+  var startArrIndex = 0;
+  var endArrIndex = 0;
+  var quoteStr = "";
+  var quoteLevel = 0;
+  var retObj = null;
+  var i = 0; // Index variable
+  for (var quoteLineIndex = 1; quoteLineIndex < gQuoteLines.length; ++quoteLineIndex)
+  {
+    retObj = firstNonQuoteTxtIndex(gQuoteLines[quoteLineIndex]);
+    lineInfos.push(retObj);
+    if (retObj.quoteLevel != lastQuoteLevel)
+    {
+      endArrIndex = quoteLineIndex;
+      // Remove the quote strings from the lines we're about to wrap
+      for (i = startArrIndex; i < endArrIndex; ++i)
+      {
+        // TODO
+        // Error on next line: !JavaScript  TypeError: lineInfos[i] is undefined
+        // Fixed by checking that lineInfos[i] is not null..  but why would it be?
+        if (lineInfos[i] != null)
+        {
+          if (lineInfos[i].startIndex > -1)
+            gQuoteLines[i] = gQuoteLines[i].substr(lineInfos[i].startIndex);
+          else
+            gQuoteLines[i] = normalizeGTChars(gQuoteLines[i]);
+          // If the quote line now only consists of spaces after removing the quote
+          // characters, then make it blank.
+          if (/^ +$/.test(gQuoteLines[i])) gQuoteLines[i] = "";
+        }
+      }
+      // Wrap the text lines in the range we've seen
+      // Note: 79 is assumed as the maximum line length because
+      // that seems to be a commonly-accepted message width for
+      // BBSs.  Also, the following length is subtracted from it:
+      // (2*(lastQuoteLevel+1) + gQuotePrefix.length)
+      // That is because we'll be prepending "> " to the quote lines,
+      // and then SlyEdit will prepend gQuotePrefix to them during quoting.
+      var numLinesBefore = gQuoteLines.length;
+      wrapTextLines(gQuoteLines, startArrIndex, endArrIndex, 79 - (2*(lastQuoteLevel+1) + gQuotePrefix.length));
+      // If quote lines were added as a result of wrapping, then
+      // determine the number of lines added, and update endArrIndex
+      // and quoteLineIndex accordingly.
+      if (gQuoteLines.length > numLinesBefore)
+      {
+        var numLinesAdded = gQuoteLines.length - numLinesBefore;
+        endArrIndex += numLinesAdded;
+        quoteLineIndex += (numLinesAdded-1); // - 1 because quoteLineIndex will be incremented by the for loop
+      }
+      // Put quote strings ("> ") back into the lines we just wrapped
+      if ((quoteLineIndex > 0) && (lastQuoteLevel > 0))
+      {
+        quoteStr = "";
+        for (i = 0; i < lastQuoteLevel; ++i)
+          quoteStr += "> ";
+        for (i = startArrIndex; i < endArrIndex; ++i)
+          gQuoteLines[i] = quoteStr + gQuoteLines[i].replace(/^\s*>/, ">");
+      }
+      lastQuoteLevel = retObj.quoteLevel;
+      startArrIndex = quoteLineIndex;
+    }
+  }
+  // Wrap the last block of lines
+  wrapTextLines(gQuoteLines, startArrIndex, gQuoteLines.length, 79 - (2*(lastQuoteLevel+1) + gQuotePrefix.length));
+
+  // Go through the quote lines again, and for ones that start with " >", remove
+  // the leading whitespace.  This is because the quote string is " > ", so it
+  // would insert an extra space before the first > in the quote line.
+  for (i = 0; i < gQuoteLines.length; ++i)
+    gQuoteLines[i] = gQuoteLines[i].replace(/^\s*>/, ">");
+}
+
+// This function displays debug text at a given location on the screen, then
+// moves the cursor back to a given location.
+//
+// Parameters:
+//  pDebugX: The X lcoation of where to write the debug text
+//  pDebugY: The Y lcoation of where to write the debug text
+//  pText: The text to write at the debug location
+//  pOriginalPos: An object with x and y properties containing the original cursor position
+//  pClearDebugLineFirst: Whether or not to clear the debug line before writing the text
+//  pPauseAfter: Whether or not to pause after displaying the text
+function displayDebugText(pDebugX, pDebugY, pText, pOriginalPos, pClearDebugLineFirst, pPauseAfter)
+{
+	console.gotoxy(pDebugX, pDebugY);
+	if (pClearDebugLineFirst)
+		console.clearline();
+	// Output the text
+	console.print(pText);
+	if (pPauseAfter)
+      console.pause();
+	if ((typeof(pOriginalPos) != "undefined") && (pOriginalPos != null))
+		console.gotoxy(pOriginalPos);
+}
\ No newline at end of file
-- 
GitLab