diff --git a/exec/SlyEdit.js b/exec/SlyEdit.js
index 98254de6e5ba1a25c7f0846be0dbd9b6b4fd950a..16515fe8d7b779cd6de3a65770d3f269eef0a75e 100644
--- a/exec/SlyEdit.js
+++ b/exec/SlyEdit.js
@@ -47,6 +47,13 @@
  *                              Bug fix: Updated ReadSlyEditConfigFile() to
  *                              default cfgObj.genColors.txtReplacementList to
  *                              ensure that it gets defined.
+ *                              Bug fix: Made use of K_NOSPIN wherever user input
+ *                              is done so that the spinning cursor doesn't overwrite
+ *                              anything on the screen.
+ *                              Code refactor: Moved doMacroTxtReplacementInEditLine()
+ *                              and getWordFromEditLine() to TextLine member
+ *                              methods TextLine_doMacroTxtReplacement() and
+ *                              TextLine_getWord() in SlyEdit_Misc.js.
  */
 
 /* Command-line arguments:
@@ -124,7 +131,6 @@ const EDITOR_VER_DATE = "2013-09-07";
 
 
 // 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
@@ -814,15 +820,7 @@ function doEditLoop()
    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);
+      userInput = getUserKey(K_NOCRLF|K_NOSPIN, gConfigSettings);
       // If userInput is blank, then the input timeout was probably
       // reached, so abort.
       if (userInput == "")
@@ -864,7 +862,7 @@ function doEditLoop()
             continueOn = false;
             break;
          case CMDLIST_HELP_KEY:
-            displayCommandList(true, true, true, gCanCrossPost, gIsSysop, gConfigSettings.enableTextReplacements);
+            displayCommandList(true, true, true, gCanCrossPost, gConfigSettings.userIsSysop, gConfigSettings.enableTextReplacements);
             clearEditAreaBuffer();
             fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
                            gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
@@ -904,9 +902,8 @@ function doEditLoop()
             }
             break;
          case CHANGE_COLOR_KEY:
-                /*
             // Let the user change the text color.
-            if (gConfigSettings.allowColorSelection)
+            /*if (gConfigSettings.allowColorSelection)
             {
                var retObject = doColorSelection(curpos, currentWordLength);
                curpos.x = retObject.x;
@@ -921,8 +918,7 @@ function doEditLoop()
                   console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
                   continue;
                }
-            }
-            */
+            }*/
             break;
          case KEY_UP:
             // Move the cursor up one line.
@@ -1162,7 +1158,7 @@ function doEditLoop()
                else if (retObject.showHelp)
                {
                   displayProgramInfo(true, false);
-                  displayCommandList(false, false, true, gCanCrossPost, gIsSysop, gConfigSettings.enableTextReplacements);
+                  displayCommandList(false, false, true, gCanCrossPost, gConfigSettings.userIsSysop, gConfigSettings.enableTextReplacements);
                   clearEditAreaBuffer();
                   fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
                                 gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
@@ -1211,9 +1207,9 @@ function doEditLoop()
             break;
          case IMPORT_FILE_KEY:
             // Only let sysops import files.
-            if (gIsSysop)
+            if (gConfigSettings.userIsSysop)
             {
-               var retObj = importFile(gIsSysop, curpos);
+               var retObj = importFile(gConfigSettings.userIsSysop, curpos);
                curpos.x = retObj.x;
                curpos.y = retObj.y;
                currentWordLength = retObj.currentWordLength;
@@ -1222,9 +1218,9 @@ function doEditLoop()
             break;
          case EXPORT_FILE_KEY:
             // Only let sysops export files.
-            if (gIsSysop)
+            if (gConfigSettings.userIsSysop)
             {
-               exportToFile(gIsSysop);
+               exportToFile(gConfigSettings.userIsSysop);
                console.gotoxy(curpos);
             }
             break;
@@ -1780,9 +1776,8 @@ function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
    var madeTxtReplacement = false; // For screen refresh purposes
    if (gConfigSettings.enableTextReplacements && (pUserInput == " "))
    {
-      var txtReplaceObj = doMacroTxtReplacementInEditLine(gTxtReplacements, gEditLinesIndex,
-                                                     gTextLineIndex,
-                                                     gConfigSettings.textReplacementsUseRegex);
+      var txtReplaceObj = gEditLines[gEditLinesIndex].doMacroTxtReplacement(gTxtReplacements, gTextLineIndex,
+                                                            gConfigSettings.textReplacementsUseRegex);
       madeTxtReplacement = txtReplaceObj.madeTxtReplacement;
       if (madeTxtReplacement)
       {
@@ -2068,9 +2063,8 @@ function doEnterKey(pCurpos, pCurrentWordLength)
    var cursorHorizDiff = 0;
    if (gConfigSettings.enableTextReplacements)
    {
-      var txtReplaceObj = doMacroTxtReplacementInEditLine(gTxtReplacements, gEditLinesIndex,
-                                                     gTextLineIndex-1,
-                                                     gConfigSettings.textReplacementsUseRegex);
+      var txtReplaceObj = gEditLines[gEditLinesIndex].doMacroTxtReplacement(gTxtReplacements, gTextLineIndex-1,
+                                                             gConfigSettings.textReplacementsUseRegex);
       if (txtReplaceObj.madeTxtReplacement)
       {
          gTextLineIndex += txtReplaceObj.wordLenDiff;
@@ -2346,8 +2340,8 @@ function doQuoteSelection(pCurpos, pCurrentWordLength)
    var continueOn = true;
    while (continueOn)
    {
-      // Get a key, and time out after 1 minute.
-      userInput = console.inkey(0, 100000);
+      // Get a keypress from the user
+      userInput = getUserKey(K_UPPER|K_NOCRLF|K_NOSPIN, gConfigSettings);
       if (userInput == "")
       {
          // The input timeout was reached.  Abort.
@@ -2965,7 +2959,7 @@ function handleDCTESCMenu(pCurpos, pCurrentWordLength)
    var editLineDiff = pCurpos.y - gEditTop;
    var menuChoice = doDCTMenu(gEditLeft, gEditRight, gEditTop,
                               displayMessageRectangle, gEditLinesIndex,
-                              editLineDiff, gIsSysop, gCanCrossPost);
+                              editLineDiff, gConfigSettings.userIsSysop, gCanCrossPost);
    // Take action according to the user's choice.
    // Save
    if ((menuChoice == "S") || (menuChoice == CTRL_Z) ||
@@ -2997,7 +2991,7 @@ function handleDCTESCMenu(pCurpos, pCurrentWordLength)
    // Import file (sysop only)
    else if (menuChoice == DCTMENU_SYSOP_IMPORT_FILE)
    {
-      var retval = importFile(gIsSysop, pCurpos);
+      var retval = importFile(gConfigSettings.userIsSysop, pCurpos);
       returnObj.x = retval.x;
       returnObj.y = retval.y;
       returnObj.currentWordLength = retval.currentWordLength;
@@ -3005,9 +2999,9 @@ function handleDCTESCMenu(pCurpos, pCurrentWordLength)
    // Import file for sysop, or Insert/Overwrite toggle for non-sysop
    else if (menuChoice == "I")
    {
-      if (gIsSysop)
+      if (gConfigSettings.userIsSysop)
       {
-         var retval = importFile(gIsSysop, pCurpos);
+         var retval = importFile(gConfigSettings.userIsSysop, pCurpos);
          returnObj.x = retval.x;
          returnObj.y = retval.y;
          returnObj.currentWordLength = retval.currentWordLength;
@@ -3026,7 +3020,7 @@ function handleDCTESCMenu(pCurpos, pCurrentWordLength)
    // Command List
    else if ((menuChoice == "O") || (menuChoice == DCTMENU_HELP_COMMAND_LIST))
    {
-      displayCommandList(true, true, true, gCanCrossPost, gIsSysop, gConfigSettings.enableTextReplacements);
+      displayCommandList(true, true, true, gCanCrossPost, gConfigSettings.userIsSysop, gConfigSettings.enableTextReplacements);
       clearEditAreaBuffer();
       fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
                      gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
@@ -3053,9 +3047,9 @@ function handleDCTESCMenu(pCurpos, pCurrentWordLength)
    // Export the message
    else if ((menuChoice == "X") || (menuChoice == DCTMENU_SYSOP_EXPORT_FILE))
    {
-      if (gIsSysop)
+      if (gConfigSettings.userIsSysop)
       {
-         exportToFile(gIsSysop);
+         exportToFile(gConfigSettings.userIsSysop);
          console.gotoxy(returnObj.x, returnObj.y);
       }
    }
@@ -3065,12 +3059,17 @@ function handleDCTESCMenu(pCurpos, pCurrentWordLength)
       // We don't need to do do anything in here.
    }
    // Cross-post
-   else if ((menuChoice == CTRL_C) || (menuChoice == "C") ||
-             (menuChoice == DCTMENU_CROSS_POST))
+   else if ((menuChoice == CTRL_C) || (menuChoice == "C") || (menuChoice == DCTMENU_CROSS_POST))
    {
       if (gCanCrossPost)
          doCrossPosting(pCurpos);
    }
+   // List text replacements
+   else if ((menuChoice == CTRL_T) || (menuChoice == "T") || (menuChoice == DCTMENU_LIST_TXT_REPLACEMENTS))
+   {
+      if (gConfigSettings.enableTextReplacements)
+         listTextReplacements();
+   }
 
    // Make sure the edit color attribute is set back.
    //console.print("n" + gTextAttrs);
@@ -3126,7 +3125,7 @@ function handleIceESCMenu(pCurpos, pCurrentWordLength)
          break;
       case ICE_ESC_MENU_HELP:
          displayProgramInfo(true, false);
-         displayCommandList(false, false, true, gCanCrossPost, gIsSysop, gConfigSettings.enableTextReplacements);
+         displayCommandList(false, false, true, gCanCrossPost, gConfigSettings.userIsSysop, gConfigSettings.enableTextReplacements);
          clearEditAreaBuffer();
          fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
                         gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
@@ -3693,11 +3692,12 @@ function doColorSelection(pCurpos, pCurrentWordLength)
    console.cleartoeol("n");
    console.crlf();
    console.clearline("n");
+   //Special: H:High Intensity I:Blinking N:Normal � Choose Color: 
    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);
+   var key = getUserKey(K_UPPER|K_NOCRLF|K_NOSPIN, gConfigSettings);
    switch (key)
    {
       // Foreground colors:
@@ -3791,8 +3791,8 @@ function doColorSelection(pCurpos, pCurrentWordLength)
    var continueOn = true;
    while (continueOn)
    {
-      // Get a key, and time out after 1 minute.
-      userInput = console.inkey(0, 100000);
+      // Get a keypress from the user
+      userInput = getUserKey(K_UPPER|K_NOCRLF|K_NOSPIN, gConfigSettings);
       if (userInput == "")
       {
          // The input timeout was reached.  Abort.
@@ -4199,7 +4199,7 @@ function doCrossPosting(pOriginalCurpos)
     pageNum = calcPageNum(topMsgGrpIndex, selBoxInnerHeight);
 
     // Get a key from the user (upper-case) and take action based upon it.
-    userInput = console.getkey(K_UPPER | K_NOCRLF);
+    userInput = getUserKey(K_UPPER|K_NOCRLF|K_NOSPIN, gConfigSettings);
     switch (userInput)
     {
       case KEY_UP: // Move up one message group in the list
@@ -4774,7 +4774,7 @@ function crossPosting_selectSubBoardInGrp(pGrpIndex, pSelBoxUpperLeft, pSelBoxLo
     pageNum = calcPageNum(topMsgSubIndex, pSelBoxInnerHeight);
 
     // Get a key from the user (upper-case) and take action based upon it.
-    userInput = console.getkey(K_UPPER | K_NOCRLF);
+    userInput = getUserKey(K_UPPER|K_NOCRLF|K_NOSPIN, gConfigSettings);
     switch (userInput)
     {
       case KEY_UP: // Move up one message sub-board in the list
@@ -5467,7 +5467,7 @@ function listTextReplacements()
       }
 
       // Get a key from the user (upper-case) and take action based upon it.
-      userInput = console.getkey(K_UPPER | K_NOCRLF);
+      userInput = getUserKey(K_UPPER|K_NOCRLF|K_NOSPIN, gConfigSettings);
       switch (userInput)
       {
          case KEY_UP:
diff --git a/exec/SlyEdit_DCTStuff.js b/exec/SlyEdit_DCTStuff.js
index aed6ffb05af5689cd85029e9ca9cc60e602ea072..ce02c11551d8588f976763d8995cabb08a6dbe8c 100644
--- a/exec/SlyEdit_DCTStuff.js
+++ b/exec/SlyEdit_DCTStuff.js
@@ -65,6 +65,7 @@ var DCTMENU_HELP_COMMAND_LIST = 8;
 var DCTMENU_HELP_GENERAL = 9;
 var DCTMENU_HELP_PROGRAM_INFO = 10;
 var DCTMENU_CROSS_POST = 12;
+var DCTMENU_LIST_TXT_REPLACEMENTS = 13;
 
 // Read the color configuration file
 readColorConfig(gConfigSettings.DCTColors.ThemeFilename);
@@ -582,8 +583,8 @@ function promptYesNo_DCTStyle(pQuestion, pBoxTitle, pDefaultYes, pParamObj)
       // 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();
+      // Get a key and take appropriate action.
+		userInput = getUserKey(K_UPPER|K_NOECHO|K_NOCRLF|K_NOSPIN, gConfigSettings);
       if (userInput == KEY_ENTER)
          continueOn = false;
       else if (userInput == "Y")
@@ -818,6 +819,15 @@ function doDCTMenu(pEditLeft, pEditRight, pEditTop, pDisplayMessageRectangle,
       doDCTMenu.allMenus[helpMenuNum].addItem("C&ommand 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);
+      if (gConfigSettings.enableTextReplacements)
+      {
+         // For some reason, Ctrl-T isn't working properly in this context - It
+         // exits the help menu but doesn't exit the menu overall.  So for now,
+         // I'm not showing or allowing Ctrl-T in this context.
+         //doDCTMenu.allMenus[helpMenuNum].addItem("&Text replcmts  Ctrl-T", DCTMENU_LIST_TXT_REPLACEMENTS);
+         //doDCTMenu.allMenus[helpMenuNum].addExitLoopKey(CTRL_T, DCTMENU_LIST_TXT_REPLACEMENTS);
+         doDCTMenu.allMenus[helpMenuNum].addItem("&Text replcmts        ", DCTMENU_LIST_TXT_REPLACEMENTS);
+      }
       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);
@@ -865,7 +875,7 @@ function doDCTMenu(pEditLeft, pEditRight, pEditTop, pDisplayMessageRectangle,
          userInput = menuRetObj.userInput;
       // If nothing from the menu was matched, then get a key from the user.
       else
-         userInput = console.inkey(K_UPPER, 60000);
+         userInput = getUserKey(K_UPPER|K_NOECHO|K_NOCRLF|K_NOSPIN, gConfigSettings);
       menuRetObj.userInput = "";
 
 		// If a menu return code was matched or userInput is blank (the
@@ -928,6 +938,11 @@ function doDCTMenu(pEditLeft, pEditRight, pEditTop, pDisplayMessageRectangle,
             if (pCanCrossPost)
                continueOn = false;
             break;
+         case "T": // List text replacements
+         //case CTRL_T: // List text replacements
+            if (gConfigSettings.enableTextReplacements)
+               continueOn = false;
+            break;
          default:
             break;
       }
@@ -967,6 +982,8 @@ function valMatchesMenuCode(pVal, pIsSysop)
                    (pVal == DCTMENU_EDIT_FIND_TEXT) || (pVal == DCTMENU_HELP_COMMAND_LIST) ||
                    (pVal == DCTMENU_HELP_GENERAL) || (pVal == DCTMENU_HELP_PROGRAM_INFO));
    }
+   if (gConfigSettings.enableTextReplacements)
+      valMatch = (valMatches || DCTMENU_LIST_TXT_REPLACEMENTS);
    return valMatches;
 }
 
@@ -983,7 +1000,7 @@ function inputMatchesMenuSelection(pInput, pIsSysop)
            (pInput == "S") || (pInput == "A") || (pInput == "E") ||
            (pInput == "I") || (pIsSysop && (pInput == "X")) ||
            (pInput == "F") || (pInput == "C") || (pInput == "G") ||
-           (pInput == "P"));
+           (pInput == "P") || (pInput == "T"));
 }
 
 //////////////////////////////////////////////////////////////////////////////////////////
@@ -1341,8 +1358,7 @@ function DCTMenu_DoInputLoop()
    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);
+		returnObj.userInput = getUserKey(K_UPPER|K_NOECHO|K_NOCRLF|K_NOSPIN, gConfigSettings);
 		// If the user input is blank, then the input timed out, and we should quit.
 		if (returnObj.userInput == "")
 		{
diff --git a/exec/SlyEdit_IceStuff.js b/exec/SlyEdit_IceStuff.js
index 0210190e18d58737196b91a284558005532a0cf9..d3c9d014ab8c34b70c7701004272b8ddf5259c4b 100644
--- a/exec/SlyEdit_IceStuff.js
+++ b/exec/SlyEdit_IceStuff.js
@@ -515,8 +515,8 @@ function promptYesNo_IceStyle(pQuestion, pDefaultYes)
       // 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();
+      // Get a keypress from the user and take appropriate action.
+		userInput = getUserKey(K_UPPER|K_NOECHO|K_NOCRLF|K_NOSPIN, gConfigSettings);
 		// 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))
@@ -711,7 +711,7 @@ function doIceESCMenu(pY, pCanCrossPost)
       }
 
       // Get the user's choice
-      userInput = console.getkey(K_UPPER|K_ALPHA|K_NOECHO|K_NOSPIN|K_NOCRLF);
+      userInput = getUserKey(K_UPPER|K_NOECHO|K_NOCRLF|K_NOSPIN, gConfigSettings);
       switch (userInput)
       {
          case KEY_UP:
diff --git a/exec/SlyEdit_Misc.js b/exec/SlyEdit_Misc.js
index 1450b7cd75fb9c588a4d1e38f2179ae3adefed54..8e536bc47aeed50569038fa4f7d761e64773ab4b 100644
--- a/exec/SlyEdit_Misc.js
+++ b/exec/SlyEdit_Misc.js
@@ -47,6 +47,10 @@
  * 2013-09-07 Eric Oulashin     Bug fix: Updated ReadSlyEditConfigFile() to
  *                              default cfgObj.genColors.txtReplacementList to
  *                              ensure that it gets defined.
+ *                              Code refactor: Moved doMacroTxtReplacementInEditLine()
+ *                              and getWordFromEditLine() to TextLine member
+ *                              methods TextLine_doMacroTxtReplacement() and
+ *                              TextLine_getWord().
  */
 
 // Note: These variables are declared with "var" instead of "const" to avoid
@@ -177,6 +181,8 @@ function TextLine(pText, pHardNewlineEnd, pIsQuoteLine)
    // Functions
    this.length = TextLine_Length;
    this.print = TextLine_Print;
+   this.doMacroTxtReplacement = TextLine_doMacroTxtReplacement;
+   this.getWord = TextLine_getWord;
 }
 // For the TextLine class: Returns the length of the text.
 function TextLine_Length()
@@ -194,6 +200,171 @@ function TextLine_Print(pClearToEOL)
    if (pClearToEOL)
       console.cleartoeol();
 }
+// Performs text replacement (AKA macro replacement) in the text line.
+//
+// Parameters:
+//  pTxtReplacements: An associative array of text to be replaced (i.e.,
+//                    gTxtReplacements)
+//  pCharIndex: The current character index in the text line
+//  pUseRegex: Whether or not to treat the text replacement search string as a
+//             regular expression.
+//
+// Return value: An object containing the following properties:
+//               textLineIndex: The updated text line index (integer)
+//               wordLenDiff: The change in length of the word that
+//                            was replaced (integer)
+//               wordStartIdx: The index of the first character in the word.
+//                             Only valid if a word was found.  Otherwise, this
+//                             will be 0.
+//               newTextEndIdx: The index of the last character in the new
+//                              text.  Only valid if a word was replaced.
+//                              Otherwise, this will be 0.
+//               newTextLen: The length of the new text in the string.  Will be
+//                           the length of the existing word if the word wasn't
+//                           replaced or 0 if no word was found.
+//               madeTxtReplacement: Whether or not a text replacement was made
+//                                   (boolean)
+function TextLine_doMacroTxtReplacement(pTxtReplacements, pCharIndex, pUseRegex)
+{
+   var retObj = new Object();
+   retObj.textLineIndex = pCharIndex;
+   retObj.wordLenDiff = 0;
+   retObj.wordStartIdx = 0;
+   retObj.newTextEndIdx = 0;
+   retObj.newTextLen = 0;
+   retObj.madeTxtReplacement = false;
+
+   var wordObj = this.getWord(retObj.textLineIndex);
+   if (wordObj.foundWord)
+   {
+      retObj.wordStartIdx = wordObj.startIdx;
+      retObj.newTextLen = wordObj.word.length;
+
+      // See if the word starts with a capital letter; if so, we'll capitalize
+      // the replacement word.
+      var firstCharUpper = false;
+      var txtReplacement = "";
+      if (pUseRegex)
+      {
+         // Since a regular expression might have more characters in addition
+         // to the actual word, we need to go through all the replacement strings
+         // in pTxtReplacements and use the first one that changes the text.
+         for (var prop in pTxtReplacements)
+         {
+            if (pTxtReplacements.hasOwnProperty(prop))
+            {
+               var regex = new RegExp(prop);
+               txtReplacement = wordObj.word.replace(regex, pTxtReplacements[prop]);
+               retObj.madeTxtReplacement = (txtReplacement != wordObj.word);
+               // If a text replacement was made, then check and see if the first
+               // letter in the original text was uppercase, and if so, make the
+               // first letter in the new text (txtReplacement) uppercase.
+               if (retObj.madeTxtReplacement)
+               {
+                  if (firstLetterIsUppercase(wordObj.word))
+                  {
+                     var letterInfo = getFirstLetterFromStr(txtReplacement);
+                     if (letterInfo.idx > -1)
+                     {
+                        txtReplacement = txtReplacement.substr(0, letterInfo.idx)
+                                       + letterInfo.letter.toUpperCase()
+                                       + txtReplacement.substr(letterInfo.idx+1);
+                     }
+                  }
+                  // Now that we've made a text replacement, stop going through
+                  // pTxtReplacements looking for a matching regex.
+                  break;
+               }
+            }
+         }
+      }
+      else
+      {
+         // Not using a regular expression.
+         firstCharUpper = (wordObj.word.charAt(0) == wordObj.word.charAt(0).toUpperCase());
+         // Convert the word to all uppercase to do the case-insensitive lookup
+         // in pTxtReplacements.
+         wordObj.word = wordObj.word.toUpperCase();
+         if (pTxtReplacements.hasOwnProperty(wordObj.word))
+         {
+            txtReplacement = pTxtReplacements[wordObj.word];
+            retObj.madeTxtReplacement = true;
+         }
+      }
+      if (retObj.madeTxtReplacement)
+      {
+         if (firstCharUpper)
+            txtReplacement = txtReplacement.charAt(0).toUpperCase() + txtReplacement.substr(1);
+         this.text = this.text.substr(0, wordObj.startIdx) + txtReplacement
+                   + this.text.substr(wordObj.endIndex+1);
+         // Based on the difference in word length, update the data that
+         // matters (retObj.textLineIndex, which keeps track of the index of the current line).
+         // Note: The horizontal cursor position variable should be replaced after calling this
+         // function.
+         retObj.wordLenDiff = txtReplacement.length - wordObj.word.length;
+         retObj.textLineIndex += retObj.wordLenDiff;
+         retObj.newTextEndIdx = wordObj.endIndex + retObj.wordLenDiff;
+         retObj.newTextLen = txtReplacement.length;
+      }
+   }
+
+   return retObj;
+}
+// Returns the word in a text line at a given index.  If the index
+// is at a space, then this function will return the word before
+// (to the left of) the space.
+//
+// Parameters:
+//  pEditLinesIndex: The index of the line to look at (0-based)
+//  pCharIndex: The character index in the text line (0-based)
+//
+// Return value: An object containing the following properties:
+//               foundWord: Whether or not a word was found (boolean)
+//               word: The word in the edit line at the given indexes (text).
+//                     This might include control/color codes, etc..
+//               plainWord: The word in the edit line without any control
+//                          or color codes, etc.  This may or may not be
+//                          the same as word.
+//               startIdx: The index of the first character of the word (integer)
+//               endIndex: The index of the last character of the word (integer)
+//                         This includes any control/color codes, etc.
+function TextLine_getWord(pCharIndex)
+{
+   var retObj = new Object();
+   retObj.foundWord = false;
+   retObj.word = "";
+   retObj.plainWord = "";
+   retObj.startIdx = 0;
+   retObj.endIndex = 0;
+
+   // Parameter checking
+   if ((pCharIndex < 0) || (pCharIndex >= this.text.length))
+      return retObj;
+
+   // If pCharIndex specifies the index of a space, then look for a non-space
+   // character before it.
+   var charIndex = pCharIndex;
+   while (this.text.charAt(charIndex) == " ")
+      --charIndex;
+   // Look for the start & end of the word based on the indexes of a space
+   // before and at/after the given character index.
+   var wordStartIdx = charIndex;
+   var wordEndIdx = charIndex;
+   while ((this.text.charAt(wordStartIdx) != " ") && (wordStartIdx >= 0))
+      --wordStartIdx;
+   ++wordStartIdx;
+   while ((this.text.charAt(wordEndIdx) != " ") && (wordEndIdx < this.text.length))
+      ++wordEndIdx;
+   --wordEndIdx;
+
+   retObj.foundWord = true;
+   retObj.startIdx = wordStartIdx;
+   retObj.endIndex = wordEndIdx;
+   retObj.word = this.text.substring(wordStartIdx, wordEndIdx+1);
+   retObj.plainWord = strip_ctrl(retObj.word);
+   return retObj;
+}
+
 
 // AbortConfirmFuncParams constructor: This object contains parameters used by
 // the abort confirmation function (actually, there are separate ones for
@@ -634,6 +805,7 @@ function ReadSlyEditConfigFile()
    cfgObj.allowCrossPosting = true;
    cfgObj.enableTextReplacements = false;
    cfgObj.textReplacementsUseRegex = false;
+   cfgObj.userIsSysop = user.compare_ars("SYSOP"); // Whether or not the user is a sysop
 
    // General SlyEdit color settings
    cfgObj.genColors = new Object();
@@ -2365,7 +2537,7 @@ function populateTxtReplacements(pArray, pRegex)
 
 function moveGenColorsToGenSettings(pColorsArray, pCfgObj)
 {
-   // Set up an array of color setting names 
+   // Set up an array of color setting names
    var colorSettingStrings = new Array();
    colorSettingStrings.push("crossPostBorder"); // Deprecated
    colorSettingStrings.push("crossPostBorderText"); // Deprecated
@@ -2426,179 +2598,6 @@ function charIsLetter(pChar)
    return /^[ABCDEFGHIJKLMNOPQRSTUVWXYZ�������������������������������������������������������������]$/.test(pChar.toUpperCase());
 }
 
-// Returns the word in a text line at a given index.  If the index
-// is at a space, then this function will return the word before
-// (to the left of) the space.
-//
-// Parameters:
-//  pEditLinesIndex: The index of the line to look at (0-based)
-//  pCharIndex: The character index in the text line (0-based)
-//
-// Return value: An object containing the following properties:
-//               foundWord: Whether or not a word was found (boolean)
-//               word: The word in the edit line at the given indexes (text)
-//               editLineIndex: The index of the edit line (integer)
-//               startIdx: The index of the first character of the word (integer)
-//               endIndex: The index of the last character of the word (integer)
-function getWordFromEditLine(pEditLinesIndex, pCharIndex)
-{
-   var retObj = new Object();
-   retObj.foundWord = false;
-   retObj.word = "";
-   retObj.editLineIndex = pEditLinesIndex;
-   retObj.startIdx = 0;
-   retObj.endIndex = 0;
-
-   // Parameter checking
-   if ((pEditLinesIndex < 0) || (pEditLinesIndex >= gEditLines.length))
-   {
-      retObj.editLineIndex = 0;
-      return retObj;
-   }
-   if ((pCharIndex < 0) || (pCharIndex >= gEditLines[pEditLinesIndex].text.length))
-   {
-      //displayDebugText(1, 1, "pCharIndex: " + pCharIndex, null, true, false); // Temporary
-      //displayDebugText(1, 2, "Line len: " + gEditLines[pEditLinesIndex].text.length, console.getxy(), true, false); // Temporary
-      return retObj;
-   }
-
-   // If pCharIndex specifies the index of a space, then look for a non-space
-   // character before it.
-   var charIndex = pCharIndex;
-   while (gEditLines[pEditLinesIndex].text.charAt(charIndex) == " ")
-      --charIndex;
-   // Look for the start & end of the word based on the indexes of a space
-   // before and at/after the given character index.
-   var wordStartIdx = charIndex;
-   var wordEndIdx = charIndex;
-   while ((gEditLines[pEditLinesIndex].text.charAt(wordStartIdx) != " ") && (wordStartIdx >= 0))
-      --wordStartIdx;
-   ++wordStartIdx;
-   while ((gEditLines[pEditLinesIndex].text.charAt(wordEndIdx) != " ") && (wordEndIdx < gEditLines[pEditLinesIndex].text.length))
-      ++wordEndIdx;
-   --wordEndIdx;
-
-   retObj.foundWord = true;
-   retObj.startIdx = wordStartIdx;
-   retObj.endIndex = wordEndIdx;
-   retObj.word = gEditLines[pEditLinesIndex].text.substring(wordStartIdx, wordEndIdx+1);
-   return retObj;
-}
-
-// Performs text replacement (AKA macro replacement) in an edit line.
-//
-// Parameters:
-//  pTxtReplacements: An associative array of text to be replaced (i.e.,
-//                    gTxtReplacements)
-//  pEditLinesIndex: The index of the line in gEditLines
-//  pCharIndex: The current character index in the text line
-//  pUseRegex: Whether or not to treat the text replacement search string as a
-//             regular expression.
-//
-// Return value: An object containing the following properties:
-//               textLineIndex: The updated text line index (integer)
-//               wordLenDiff: The change in length of the word that
-//                            was replaced (integer)
-//               wordStartIdx: The index of the first character in the word.
-//                             Only valid if a word was found.  Otherwise, this
-//                             will be 0.
-//               newTextEndIdx: The index of the last character in the new
-//                              text.  Only valid if a word was replaced.
-//                              Otherwise, this will be 0.
-//               newTextLen: The length of the new text in the string.  Will be
-//                           the length of the existing word if the word wasn't
-//                           replaced or 0 if no word was found.
-//               madeTxtReplacement: Whether or not a text replacement was made
-//                                   (boolean)
-function doMacroTxtReplacementInEditLine(pTxtReplacements, pEditLinesIndex, pCharIndex, pUseRegex)
-{
-   var retObj = new Object();
-   retObj.textLineIndex = pCharIndex;
-   retObj.wordLenDiff = 0;
-   retObj.wordStartIdx = 0;
-   retObj.newTextEndIdx = 0;
-   retObj.newTextLen = 0;
-   retObj.madeTxtReplacement = false;
-
-   var wordObj = getWordFromEditLine(pEditLinesIndex, retObj.textLineIndex);
-   if (wordObj.foundWord)
-   {
-      retObj.wordStartIdx = wordObj.startIdx;
-      retObj.newTextLen = wordObj.word.length;
-
-      // See if the word starts with a capital letter; if so, we'll capitalize
-      // the replacement word.
-      //var firstCharUpper = (wordObj.word.charAt(0) == wordObj.word.charAt(0).toUpperCase());
-      var firstCharUpper = false;
-      var txtReplacement = "";
-      if (pUseRegex)
-      {
-         // Since a regular expression might have more characters in addition
-         // to the actual word, we need to go through all the replacement strings
-         // in pTxtReplacements and use the first one that changes the text.
-         for (var prop in pTxtReplacements)
-         {
-            if (pTxtReplacements.hasOwnProperty(prop))
-            {
-               var regex = new RegExp(prop);
-               txtReplacement = wordObj.word.replace(regex, pTxtReplacements[prop]);
-               retObj.madeTxtReplacement = (txtReplacement != wordObj.word);
-               // If a text replacement was made, then check and see if the first
-               // letter in the original text was uppercase, and if so, make the
-               // first letter in the new text (txtReplacement) uppercase.
-               if (retObj.madeTxtReplacement)
-               {
-                  if (firstLetterIsUppercase(wordObj.word))
-                  {
-                     var letterInfo = getFirstLetterFromStr(txtReplacement);
-                     if (letterInfo.idx > -1)
-                     {
-                        txtReplacement = txtReplacement.substr(0, letterInfo.idx)
-                                       + letterInfo.letter.toUpperCase()
-                                       + txtReplacement.substr(letterInfo.idx+1);
-                     }
-                  }
-                  // Now that we've made a text replacement, stop going through
-                  // pTxtReplacements looking for a matching regex.
-                  break;
-               }
-            }
-         }
-      }
-      else
-      {
-         // Not using a regular expression.
-         firstCharUpper = (wordObj.word.charAt(0) == wordObj.word.charAt(0).toUpperCase());
-         // Convert the word to all uppercase to do the case-insensitive lookup
-         // in pTxtReplacements.
-         wordObj.word = wordObj.word.toUpperCase();
-         if (pTxtReplacements.hasOwnProperty(wordObj.word))
-         {
-            txtReplacement = pTxtReplacements[wordObj.word];
-            retObj.madeTxtReplacement = true;
-         }
-      }
-      if (retObj.madeTxtReplacement)
-      {
-         if (firstCharUpper)
-            txtReplacement = txtReplacement.charAt(0).toUpperCase() + txtReplacement.substr(1);
-         gEditLines[pEditLinesIndex].text = gEditLines[pEditLinesIndex].text.substr(0, wordObj.startIdx)
-                                          + txtReplacement
-                                          + gEditLines[pEditLinesIndex].text.substr(wordObj.endIndex+1);
-         // Based on the difference in word length, update the data that
-         // matters (retObj.textLineIndex, which keeps track of the index of the current line).
-         // Note: The horizontal cursor position variable should be replaced after calling this
-         // function.
-         retObj.wordLenDiff = txtReplacement.length - wordObj.word.length;
-         retObj.textLineIndex += retObj.wordLenDiff;
-         retObj.newTextEndIdx = wordObj.endIndex + retObj.wordLenDiff;
-         retObj.newTextLen = txtReplacement.length;
-      }
-   }
-
-   return retObj;
-}
-
 // For configuration files, this function returns a fully-pathed filename.
 // This function first checks to see if the file exists in the sbbs/mods
 // directory, then the sbbs/ctrl directory, and if the file is not found there,
@@ -2674,6 +2673,26 @@ function firstLetterIsUppercase(pString)
    return firstIsUpper;
 }
 
+// Gets a keypress from the user.  Uses the configured timeout if configured to
+// do so and the user is not a sysop; otherwise (no timeout configured or the
+// user is a sysop), the configured input timeout will be used.
+//
+// Parameters:
+//  pMode: The input mode flag(s)
+//  pCfgObj: The configuration object
+//
+// Return value: The user's keypress (the return value of console.getkey()
+//               or console.inkey()).
+function getUserKey(pMode, pCfgObj)
+{
+   var userKey = "";
+   if (!pCfgObj.userInputTimeout || pCfgObj.userIsSysop)
+      userKey = console.getkey(pMode);
+   else
+      userKey = console.inkey(pMode, pCfgObj.inputTimeoutMS);
+   return userKey;
+}
+
 // This function displays debug text at a given location on the screen, then
 // moves the cursor back to a given location.
 //