diff --git a/ctrl/SlyEdit.cfg b/ctrl/SlyEdit.cfg
index 2ddf3acd10584aa5213d3ae1288c911eb5182206..76249cdb4cea7902833785a27b2f24b1918bd7de 100644
--- a/ctrl/SlyEdit.cfg
+++ b/ctrl/SlyEdit.cfg
@@ -59,6 +59,25 @@ allowSpellCheck=true
 ; the same directory as SlyEdit.
 dictionaryFilenames=en
 
+; Meme settings
+
+; Maximum text length for memes
+memeMaxTextLen=500
+; Default width for memes
+memeDefaultWidth=39
+; Whether or not to choose a random style (border style & color) for the meme.
+; Valid values are true and false. If this is true, the border & color settings
+; will be ignored.
+memeStyleRandom=false
+; Default border for posting a meme (1-based number or none, single, mixed1,
+; mixed2, mixed3, double, ornate1, ornate2, or ornate3)
+memeDefaultBorder=double
+; Default color number for posting a meme (1-based)
+memeDefaultColor=4
+; Meme text justification (center, left, right)
+memeJustify=center
+
+
 ; String settings - Currently, the only setting is the strings configuration
 ; filename
 [STRINGS]
diff --git a/docs/slyedit_readme.txt b/docs/slyedit_readme.txt
index 7a5ad1db79aee0fd028a4921ad11e2d487614386..93ddb5dc81e2d4100a29293ae56b0f50482c376e 100644
--- a/docs/slyedit_readme.txt
+++ b/docs/slyedit_readme.txt
@@ -1,6 +1,6 @@
                          SlyEdit message editor
-                              Version 1.89e
-                        Release date: 2025-02-09
+                              Version 1.90
+                        Release date: 2025-05-07
 
                                   by
 
@@ -137,23 +137,23 @@ ICE parameter for IceEdit emulation:
       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                                �
-    � �Record Terminal Width           Yes                               �
-    � �Word-wrap Quoted Text           Yes, for terminal width           �
-    � �Automatically 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                              �
+    +[�][?]--------------------------------------------------------------+
+    �                      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                                �
+    � �Record Terminal Width           Yes                               �
+    � �Word-wrap Quoted Text           Yes, for terminal width           �
+    � �Automatically 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
@@ -218,24 +218,27 @@ BBS machine):
 
 Help keys                                     Slash commands (on blank line)
 ---------                                     ------------------------------
-Ctrl-G       : Input graphic character      � /A      : Abort
-Ctrl-L       : Command key list (this list) � /S      : Save
-                                            � /Q      : Quote message
-Ctrl-T       : List text replacements       � /T      : List text replacements
-                                            � /U      : Your user settings
-                                            � /C      : Cross-post selection
-                                            � /UPLOAD : Upload a message
+Ctrl-G       : Input graphic character      � /A      : Abort
+Ctrl-L       : Command key list (this list) � /S      : Save
+Ctrl-T       : List text replacements       � /T      : List text replacements
+                                            � /Q      : Quote message
+                                            � /M      : Add a meme
+                                            � /U      : Your user settings
+                                            � /C      : Cross-post selection
+                                            � /UPLOAD : Upload a message
 
 Command/edit keys
 -----------------
-Ctrl-A       : Abort message                � PageUp  : Page up
-Ctrl-Z       : Save message                 � PageDown: Page down
-Ctrl-Q       : Quote message                � Ctrl-W  : Word/text search
-Insert/Ctrl-I: Toggle insert/overwrite mode � Ctrl-D  : Delete line
-Ctrl-S       : Change subject               � ESC     : Command menu
-Ctrl-U       : Your user settings           � Ctrl-C  : Cross-post selection
-Ctrl-K       : Change text color            � Ctrl-R  : Spell checker
-Ctrl-O       : Import a file                � Ctrl-X  : Export to file
+Ctrl-A       : Abort message                � PageUp  : Page up
+Ctrl-Z       : Save message                 � PageDown: Page down
+Ctrl-Q       : Quote message                � Ctrl-W  : Word/text search
+Insert/Ctrl-I: Toggle insert/overwrite mode � Ctrl-D  : Delete line
+Ctrl-S       : Change subject               � ESC     : Command menu
+Ctrl-U       : Your user settings           � Ctrl-C  : Cross-post selection
+Ctrl-K       : Change text color            � Ctrl-R  : Spell checker
+Ctrl-O       : Import a file                � Ctrl-X  : Export to file
+
+
 
 
 5. Configuration file
@@ -393,6 +396,52 @@ dictionaryFilenames               Dictionary filenames (used for spell check).
                                   are located in either sbbs/mods, sbbs/ctrl,
                                   or the same directory as SlyEdit.
 
+memeMaxTextLen                    For appending a 'meme' to a message, this
+                                  specifies the maximum text length for a meme.
+
+memeDefaultWidth                  The default width for memes
+
+memeStyleRandom                   For appending a meme to a message, whether or
+                                  not to choose a random style (border style &
+                                  color) for the meme. The user will still be
+                                  able to change it. Valid values are true and
+                                  false. If this is true, the border & color
+                                  settings will be ignored.
+								  
+memeDefaultBorder                 Default border for posting a meme. This can be
+                                  one of the following:
+                                  none
+                                  single
+                                  mixed1
+                                  mixed2
+                                  mixed3
+                                  double
+                                  ornate1
+                                  ornate2
+                                  ornate3
+								  This can also be a number between 1 and the
+                                  maximum number of meme border styles. You can
+                                  refer to sbbs/exec/load/meme_lib.js - Near the
+                                  top, there are definitions such as
+                                  BORDER_NONE, BORDER_SINGLE, etc., up to
+                                  BORDER_COUNT (which is the number of border
+                                  styles supported).
+
+memeDefaultColor                  For appending a meme to a message, this is a
+                                  number that specifies the coloring for the
+                                  meme. This can be between 1 and the maximum
+                                  number of coloring options supported by
+                                  sbbs/exec/load/meme_chooser.js. You can refer
+                                  to sbbs/exec/load/meme_chooser.js. At the time
+                                  of this writing, under "function main" in that
+                                  file, there is an array of attribute codes,
+                                  declared via "var attr", and there are are 7
+                                  of them as of now.
+
+memeJustify                       For appending a meme to a message, this
+                                  specifies the text justification. Valid
+                                  values are center, left, and right.
+
 String settings
 -----------------
 Setting                           Description
@@ -949,6 +998,37 @@ message to lower-case and comparing them with the words in the dictionary.
 ===================
 Version  Date         Description
 -------  ----         -----------
+1.90     2025-05-07   Better behavior when editing a general file rather than a
+                      message (i.e., if the user is editing an SSH key): Not
+                      adding a space after each (wrapped) line, etc.. Lines will
+                      still be wrapped in the user's terminal during editing but
+                      should be saved properly.
+
+                      Color/attribute codes are no longer removed from quote
+                      lines. Although quote lines still appear in SlyEdit with
+                      one (configurable) color, the quoted text will retain
+                      color/attribute codes when the message is posted.
+
+                      New feature: A 'meme' can be added to the message by
+                      typing /m on an empty line by itself and pressing enter.
+
+                      Bug fix: When closing the User Settings dialog, quote
+                      lines are refreshed with the configured quote line color
+                      (instead of with any color codes included in those lines)
+ 
+                      Bug fix: When closing the User Settings dialog, existing
+                      message lines are refreshed better; sometimes, there were
+                      small parts of the beginnings of some lines that were
+                      blanked out.
+ 
+                      Bug fix (sort of): DCT mode File menu wasn't drawing when
+                      the user presses the ESC key to bring up the menu. I don't
+                      know why that's happening. I found a kludge that seems to
+                      fix it.
+ 
+                      On startup, SlyEdit now sets the 'normal' attribute on the
+                      console to clear away any background color or other
+                      attribute(s) that may have been set.
 1.89e    2025-02-09   User inactivity timeout: Display a warning message
                       (without messing with the screen). New [STRINGS]
                       configuration section with stringsFilename to specify the
diff --git a/exec/SlyEdit.js b/exec/SlyEdit.js
index 4aef4b639b916ba67591d49de61eca3da471b664..0e1a89ebd33b37333d94829b4e3b2b473ff319e6 100644
--- a/exec/SlyEdit.js
+++ b/exec/SlyEdit.js
@@ -89,6 +89,33 @@
  *                              messing with the screen). New [STRINGS] configuration section
  *                              with stringsFilename to specify the name of a strings file,
  *                              which for now just contains an areYouThere setting
+ * 2025-04-23 Eric Oulashin     Version 1.90 Beta
+ *                              Started work on updating the way files are saved when not
+ *                              editing a message (i.e., if the user is editing an SSH key):
+ *                              Not adding a space after each (wrapped) line, etc..
+ *                              Also, color/attribute codes are no longer removed from
+ *                              quote lines. Although quote lines still appear in SlyEdit
+ *                              with one (configurable) color, the quoted text will retain
+ *                              color/attribute codes when the message is posted.
+ * 2025-05-03 Eric Oulashin     New feature: A 'meme' can be added to the message by typing
+ *                              /m on an empty line by itself and pressing enter.
+ * 2025-05-04 Eric Oulashin     Bug fix: When closing the User Settings dialog, quote lines
+ *                              are refreshed with the configured quote line color (instead
+ *                              of with any color codes included in those lines)
+ *
+ *                              Bug fix: When closing the User Settings dialog, existing message
+ *                              lines are refreshed better; sometimes, there were small parts of
+ *                              the beginnings of some lines that were blanked out.
+ *
+ * 2025-05-06 Eric Oulashin     Bug fix (sort of): DCT mode File menu wasn't drawing when the user
+ *                              presses the ESC key to bring up the menu. I don't know why that's
+ *                              happening. I found a kludge that seems to fix it.
+ *
+ *                              On startup, SlyEdit now sets the 'normal' attribute on the
+ *                              console to clear away any background color or other attribute(s)
+ *                              that may have been set.
+ * 2025-05-07 Eric Oulashin     Version 1.90
+ *                              Releasing this version
  */
 
 "use strict";
@@ -179,8 +206,8 @@ if (console.screen_columns < 80)
 }
 
 // Version information
-var EDITOR_VERSION = "1.89e";
-var EDITOR_VER_DATE = "2025-02-09";
+var EDITOR_VERSION = "1.90";
+var EDITOR_VER_DATE = "2025-05-07";
 
 
 // Program variables
@@ -214,6 +241,15 @@ if (console.screen_columns < 80)
 var gEditWidth = gEditRight - gEditLeft + 1;
 var gEditHeight = gEditBottom - gEditTop + 1;
 
+
+// Even though SlyEdit is always editing a file, this
+// stores returns whether or not we're simply editing
+// a file (as opposed to posting a message).
+var gJustEditingAFile = false;
+
+// Whether the user can change the subject
+var gCanChangeSubject = true;
+
 // Colors
 var gQuoteWinTextColor = "\x01n\x01" + "7\x01k";   // Normal text color for the quote window (DCT default)
 var gQuoteLineHighlightColor = "\x01n\x01w"; // Highlighted text color for the quote window (DCT default)
@@ -260,10 +296,13 @@ gCrossPostMsgSubs.subCodeExists = function(pSubCode) {
 	if (pSubCode === "")
 		return false;
 
-	var grpIndex = msg_area.sub[pSubCode].grp_index;
 	var foundIt = false;
-	if (this.hasOwnProperty(grpIndex))
-		foundIt = this[grpIndex].hasOwnProperty(pSubCode);
+	if (typeof(msg_area.sub[pSubCode]) === "object")
+	{
+		var grpIndex = msg_area.sub[pSubCode].grp_index;
+		if (this.hasOwnProperty(grpIndex))
+			foundIt = this[grpIndex].hasOwnProperty(pSubCode);
+	}
 	return foundIt;
 };
 // This function adds a sub-board code to gCrossPostMsgSubs.
@@ -428,6 +467,13 @@ var gValidWordChars = "";
 // do the line re-wrapping at the end before saving the message).
 var gUploadedMessageFile = false;
 
+// Definitions for actions to take after the Enter key is pressed
+const ENTER_ACTION_NONE = 0;
+const ENTER_ACTION_DO_QUOTE_SELECTION = 1;
+const ENTER_ACTION_DO_CROSS_POST_SELECTION = 2;
+const ENTER_ACTION_DO_MEME_INPUT = 3;
+const ENTER_ACTION_SHOW_HELP = 4;
+
 // 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()
@@ -575,7 +621,6 @@ if (dropFileName != undefined)
 			gFromName = info[0];
 			gToName = info[1];
 			gMsgSubj = info[2];
-			gMsgSubj = info[2];
 			gMsgArea = info[4];
 
 			// Now that we know the name of the message area
@@ -583,6 +628,54 @@ if (dropFileName != undefined)
 			// getCurMsgInfo() to set gMsgAreaInfo.
 			gMsgAreaInfo = getCurMsgInfo(gMsgArea);
 			setMsgAreaInfoObj = true;
+			// If there is no sub-board code, that means we're editing a
+			// regular file:
+			// - Disable quote selection
+			// - Make the 'to' name just the filename without the full
+			// path
+			// - Don't allow changing the subject
+			// - Enable color selection regardless of the configuration
+			//   (we could be editing an .asc file for the BBS, for instance)
+			if (gMsgAreaInfo.subBoardCode.length == 0)
+			{
+				gUseQuotes = false;
+				// It's possible that a script could call console.editfile()
+				// with a 'to', 'from', subject, and message area to provide
+				// that metadata when editing a file (i.e., maybe the caller
+				// is a news reader that wants to post a message), but if
+				// not, the 'to' name will be the filename (and the 'from'
+				// name and subject will be blank). The file could be a
+				// new file, so it might not exist yet.
+				// There's also a case where Synchronet will use an editor to
+				// edit things like an extended file description. In that case,
+				// just the filename will be in the subject. The way this is
+				// coded now, it won't set gJustEditingAFile to true, and
+				// that might actually be desirable when editing file descriptions
+				// & such, due to handling of spaces at the ends of lines.
+				if (gToName.length > 0 && gFromName.length == 0 && gMsgSubj.length == 0)
+				{
+					gJustEditingAFile = true;
+					//gToName = file_getname(gToName);
+					//gMsgSubj = "Editing a file";
+					gMsgSubj = file_getname(gToName);
+					gToName = "";
+					gCanChangeSubject = false;
+					gConfigSettings.allowColorSelection = true;
+					js.global.slyEditData.useQuotes = false;
+					// Should we also disable tag lines here?
+				}
+				else if (gToName.length == 0 && gFromName.length == 0 && gMsgSubj.length > 0)
+				{
+					// This is the case where we might be editing a file description
+					// (not sure what other cases might result in just the subject being
+					// populated). In this case, we probably shouldn't allow changing
+					// the subject.
+					gCanChangeSubject = false;
+					gConfigSettings.allowColorSelection = true;
+					js.global.slyEditData.useQuotes = false;
+					// Should we also disable tag lines here?
+				}
+			}
 
 			// If the msginf file has at least 7 lines, then the 7th line is the full
 			// path & filename of the tagline file, where we can write the
@@ -645,10 +738,10 @@ if (!setMsgAreaInfoObj)
 }
 
 // Set a variable to store whether or not cross-posting can be done.
-var gCanCrossPost = (gConfigSettings.allowCrossPosting && postingInMsgSubBoard(gMsgArea));
+var gCanCrossPost = (gMsgAreaInfo.subBoardCode.length > 0 && gConfigSettings.allowCrossPosting && postingInMsgSubBoard(gMsgArea));
 // If the user is posting in a message sub-board, then add its information
 // to gCrossPostMsgSubs.
-if (postingInMsgSubBoard(gMsgArea))
+if (gMsgAreaInfo.subBoardCode.length > 0 && postingInMsgSubBoard(gMsgArea))
 	gCrossPostMsgSubs.add(gMsgAreaInfo.subBoardCode);
 
 // Open the quote file / message file
@@ -658,6 +751,11 @@ readQuoteOrMessageFile();
 // change the subject in the future)
 var gOldSubj = gMsgSubj;
 
+// Make sure the console has the normal attribute
+// before starting the editor, in case any background
+// color etc. was set
+console.attributes = "N";
+
 // Now it's edit time.
 var exitCode = doEditLoop();
 
@@ -753,10 +851,48 @@ if ((exitCode == 0) && (gEditLines.length > 0))
 					while (continueOn)
 					{
 						addedSpace = false;
-						if (textLine.length > 0)
+						// New (2025-04-23): If the sub-board code is not empty, then
+						// we're posting in the messagebase, so we should add a space
+						// after the line. Otherwise, the user is editing a file, and
+						// we don't want to do that.
+						if (gMsgAreaInfo.subBoardCode.length > 0)
 						{
-							textLine += " ";
-							addedSpace = true;
+							if (textLine.length > 0)
+							{
+								textLine += " ";
+								addedSpace = true;
+							}
+						}
+						else
+						{
+							// Editing a file (not posting a message for a sub-board or email)
+							if (i > blockIdxes.allBlocks[blockIdx].start)
+							{
+								var prevLineIdx = i - 1;
+								// TODO: This isn't working properly
+								// Temporary
+								//console.print("\x01n" + prevLineIdx + ", " + typeof(gEditLines[prevLineIdx]) + "\r\n"); // Temporary
+								//console.print(i + ", " + typeof(gEditLines[i]) + "\r\n"); // Temporary
+								// End Temporary
+								// TODO: This doesn't seem to be adding the space as expected
+								/*
+								if (!gEditLines[prevLineIdx].hardNewlineEnd)
+								{
+									var maxLineLenForScreen = console.screen_columns - 1;
+									var prevLineLen = console.strlen(gEditLines[prevLineIdx].text);
+									// Temporary
+									printf("\x01n%d - Max len: %d, prev line len: %d\r\n", i, maxLineLenForScreen, prevLineLen);
+									printf("%s:\r\n\r\n", gEditLines[prevLineIdx].text); // Temporary
+									// End Temporary
+									if (prevLineLen < maxLineLenForScreen)
+									{
+										var numSpaces = maxLineLenForScreen - prevLineLen;
+										//printf("\x01n# spaces: %d\r\n\r\n", numSpaces); // Temporary
+										//textLine += format("%*s", numSpaces, "");
+									}
+								}
+								*/
+							}
 						}
 						if (addedSpace)
 							++attrIdxOffset;
@@ -770,7 +906,14 @@ if ((exitCode == 0) && (gEditLines.length > 0))
 						// When we reach a line with a hard newline end, or the end of the text block or edit lines,
 						// stop accumulating the text into textLine.
 						continueOn = (!gEditLines[i].hardNewlineEnd) && (i < blockIdxes.allBlocks[blockIdx].end) && (i+1 < gEditLines.length);
-						delete gEditLines[i]; // Save some memory
+						// Save some memory
+						if (gMsgAreaInfo.subBoardCode.length > 0) // If editing a message for a sub-board or mail
+							delete gEditLines[i];
+						else // Editing a file
+						{
+							if (typeof(gEditLines[i-1]) === "object")
+								delete gEditLines[i-1];
+						}
 						++i;
 					}
 					var newTextLine = new TextLine(textLine, true, false);
@@ -833,9 +976,9 @@ if ((exitCode == 0) && (gEditLines.length > 0))
 				}
 			}
 
-			// If the user has not chosen to auto-sign messages, then also append their
-			// signature to the message now.
-			if (!gUserSettings.autoSignMessages)
+			// If the user has not chosen to auto-sign messages and we're posting a message in a
+			// sub-board or personal email, then also append their signature to the message now.
+			if (!gUserSettings.autoSignMessages && gMsgAreaInfo.subBoardCode.length > 0)
 			{
 				// Append a blank line to separate the message & signature.
 				// Note: msgContents already has a newline at the end, so we don't have
@@ -903,10 +1046,12 @@ if ((exitCode == 0) && (gEditLines.length > 0))
 					printf("\x01n  " + gConfigSettings.genColors.msgPostedSubBoardName + "%-73s", msg_area.sub[subCode].description.substr(0, 73));
 					if (msg_area.sub[subCode].can_post)
 					{
-						// If the user's auto-sign setting is enabled, then auto-sign
-						// the message and append their signature afterward.  Otherwise,
-						// don't auto-sign, and their signature has already been appended.
-						if (gUserSettings.autoSignMessages)
+						// If the user's auto-sign setting is enabled and we're posting in a
+						// sub-board/personal email (not editing a file), then auto-sign the
+						// message and append their signature afterward.  Otherwise, don't
+						// auto-sign, and their signature has already been appended (if posting
+						// a message).
+						if (gUserSettings.autoSignMessages && gMsgAreaInfo.subBoardCode.length > 0)
 						{
 							var msgContents2 = msgContents + "\r\n";
 							var userSignName = getSignName(subCode, gUserSettings.autoSignRealNameOnlyFirst, gUserSettings.autoSignEmailsRealName);
@@ -922,7 +1067,9 @@ if ((exitCode == 0) && (gEditLines.length > 0))
 							}
 							msgContents2 += userSignName;
 							msgContents2 += "\r\n\r\n";
-							if (msgSigInfo.sigContents.length > 0)
+							// If the user has a signature and we're posting a message (not editing a file), then
+							// append the user's signature
+							if (msgSigInfo.sigContents.length > 0 && gMsgAreaInfo.subBoardCode.length > 0)
 							{
 								if (messageLinesHaveAttrs())
 									msgContents2 += "\x01n";
@@ -1003,7 +1150,8 @@ function saveMessageToFile()
 	// use the first command-line argument.
 	var outputFilename = (argc == 0 ? system.temp_dir + "INPUT.MSG" : argv[0]);
 	var msgFile = new File(outputFilename);
-	if (msgFile.open("w"))
+	//if (msgFile.open("w"))
+	if (msgFile.open("wb")) // Open in binary mode to suppress end-of-line translations
 	{
 		// If the message has any attribute codes in it, then append a normal attribute code
 		// to the end of the last line of the message. This is a workaround to ensure colors
@@ -1011,9 +1159,12 @@ function saveMessageToFile()
 		var msgHasAttrCodes = messageLinesHaveAttrs();
 		if (msgHasAttrCodes)
 			gEditLines[gEditLines.length-1].text += "\x01n";
-		// 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.
+		// Write each line of the message to the file.
+		// 2025-04-28: It used to be that "Expand Line Feeds to CRLF" option should
+		// be turned on in SCFG for this to work properly for all platforms.
+		// However, that probably isn't a problem now, and that option should now be
+		// disabled so that line endings don't change in case we're editing a
+		// file (rather than posting a messge).
 		var lastLineIdx = gEditLines.length - 1;
 		for (var i = 0; i < gEditLines.length; ++i)
 		{
@@ -1021,8 +1172,8 @@ function saveMessageToFile()
 			var msgTxtLine = gEditLines[i].getText(gConfigSettings.allowColorSelection);
 			if (msgHasAttrCodes && gConfigSettings.saveColorsAsANSI)
 				msgTxtLine = syncAttrCodesToANSI(msgTxtLine);
-			// If UTF-8 is enabled, then write each character propertly.  Otherwise, write the entire
-			// line to the file with writeln()
+			// If UTF-8 is enabled, then write each character properly.  Otherwise, write the entire
+			// line to the file as we'd normally do, with write() or writeln()
 			if (gPrintMode & P_UTF8)
 			{
 				for (var txtLineI = 0; txtLineI < msgTxtLine.length; ++txtLineI)
@@ -1032,13 +1183,21 @@ function saveMessageToFile()
 					for (var encodedI = 0; encodedI < encoded.length; ++encodedI)
 						msgFile.writeBin(ascii(encoded[encodedI]), 1);
 				}
-				msgFile.writeln(""); // Write an end-line
+				// Write an end-line
+				// Note: The editor setting "Expand Line Feeds to CRLF" will cause
+				// line endings to be CRLF (they'd normally be just LF in *nix)
+				msgFile.writeln("");
 			}
 			else
+			{
+				// Note: The editor setting "Expand Line Feeds to CRLF" will cause
+				// line endings to be CRLF (they'd normally be just LF in *nix)
 				msgFile.writeln(msgTxtLine);
+			}
 		}
-		// Auto-sign the message if the user's setting to do so is enabled
-		if (gUserSettings.autoSignMessages)
+		// Auto-sign the message if the user's setting to do so is enabled and
+		// we're posting a message (not editing a file)
+		if (gUserSettings.autoSignMessages && gMsgAreaInfo.subBoardCode.length > 0)
 		{
 			msgFile.writeln("");
 			var subCode = (postingInMsgSubBoard(gMsgArea) ? gMsgAreaInfo.subBoardCode : "mail");
@@ -1053,20 +1212,22 @@ function saveMessageToFile()
 
 
 // Set the end-of-program status message.
+var editObjName = (gMsgAreaInfo.subBoardCode.length > 0 ? "message" : "file");
+var operationName = (gMsgAreaInfo.subBoardCode.length > 0 ? "Message" : "Edit");
 var endStatusMessage = "";
 if (exitCode == 1)
-	endStatusMessage = gConfigSettings.genColors.msgAbortedText + "Message aborted.";
+	endStatusMessage = format("%s%s aborted.", gConfigSettings.genColors.msgAbortedText, operationName);
 else if (exitCode == 0)
 {
 	if (gEditLines.length > 0)
 	{
 		if (savedTheMessage)
-			endStatusMessage = gConfigSettings.genColors.msgHasBeenSavedText + "The message has been saved.";
+			endStatusMessage = format("%sThe %s has been saved.", gConfigSettings.genColors.msgHasBeenSavedText, editObjName);
 		else
-			endStatusMessage = gConfigSettings.genColors.msgAbortedText + "Message aborted.";
+			endStatusMessage = format("%s%s aborted.", gConfigSettings.genColors.msgAbortedText, operationName);
 	}
 	else
-		endStatusMessage = gConfigSettings.genColors.emptyMsgNotSentText + "Empty message not sent.";
+		endStatusMessage = format("%sEmpty %s not saved.", gConfigSettings.genColors.emptyMsgNotSentText, editObjName);
 }
 // We shouldn't hit this else case, but it's here just to be safe.
 else
@@ -1122,7 +1283,7 @@ function readQuoteOrMessageFile()
 					// TODO: I'd like to comment this out & leave attribute codes in the quote lines,
 					// but that's currently causing wrapTextLinesForQuoting() to not find prefixes
 					// properly & not wrap properly
-					textLine = strip_ctrl(textLine);
+					//textLine = strip_ctrl(textLine);
 					// If the line has only whitespace and/or > characters,
 					// then make the line blank before putting it into
 					// gQuoteLines.
@@ -1154,10 +1315,41 @@ function readQuoteOrMessageFile()
 		}
 		else
 		{
-			var textLine = null;
 			while (!inputFile.eof)
 			{
-				textLine = new TextLine();
+				//gMsgAreaInfo.subBoardCode
+				// If the line is too long to fit on the screen, then
+				// split it (console.screen_columns-1)
+				var textLineFromFile = inputFile.readln(8192);
+				if (typeof(textLineFromFile) !== "string") // Shouldn't be true, but I've seen it before
+					continue;
+				var maxLineLen = console.screen_columns - 1;
+				if (textLineFromFile.length > maxLineLen)
+				{
+					do
+					{
+						// Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js
+						var textLine = new TextLine();
+						textLine.setText(substrWithAttrCodes(textLineFromFile, 0, maxLineLen));
+						var textLineLen = console.strlen(textLineFromFile);
+						textLine.hardNewlineEnd = (textLineLen <= maxLineLen);
+						gEditLines.push(textLine);
+						if (textLineLen > maxLineLen)
+							textLineFromFile = substrWithAttrCodes(textLineFromFile, maxLineLen);
+						else
+							textLineFromFile = "";
+					} while (console.strlen(textLineFromFile) > 0);
+				}
+				else
+				{
+					var textLine = new TextLine();
+					textLine.setText(textLineFromFile);
+					textLine.hardNewlineEnd = true;
+					gEditLines.push(textLine);
+				}
+				// Old code - Does not split lines depending on terminal width:
+				/*
+				var textLine = new TextLine();
 				var textLineFromFile = inputFile.readln(2048);
 				if (typeof(textLineFromFile) == "string")
 					textLine.setText(strip_ctrl(textLineFromFile));
@@ -1170,6 +1362,7 @@ function readQuoteOrMessageFile()
 				if (textLine.screenLength() < console.screen_columns-1)
 					textLine.text += " ";
 				gEditLines.push(textLine);
+				*/
 			}
 
 			// If the last edit line is undefined (which is possible after reading the end
@@ -1188,6 +1381,7 @@ function readQuoteOrMessageFile()
 	}
 }
 
+console.print("\x01n\r\ngJustEditingAFile: " + gJustEditingAFile + "\r\n\x01p"); // Temporary
 // Edit mode & input loop
 function doEditLoop()
 {
@@ -1224,6 +1418,8 @@ function doEditLoop()
 	const SEARCH_TEXT_KEY           = CTRL_W;
 	const EXPORT_TO_FILE_KEY        = CTRL_X;
 	const SAVE_KEY                  = CTRL_Z;
+	const INSERT_MEME_KEY_COMBO_1    = "/m";
+	const INSERT_MEME_KEY_COMBO_2    = "/M";
 
 	// Draw the screen.
 	// Note: This is purposefully drawing the top of the message.  We
@@ -1305,7 +1501,8 @@ function doEditLoop()
 			case ABORT_KEY:
 				// Before aborting, ask they user if they really want to abort.
 				console.attributes = "N"; // To avoid problems with background colors
-				if (promptYesNo("Abort message", false, "Abort", false, false))
+				var editObjName = (gMsgAreaInfo.subBoardCode.length > 0 ? "message" : "edit");
+				if (promptYesNo("Abort " + editObjName, false, "Abort", false, false))
 				{
 					returnCode = 1; // Aborted
 					continueOn = false;
@@ -1325,7 +1522,7 @@ function doEditLoop()
 			case CMDLIST_HELP_KEY_2:
 				displayCommandList(true, true, true, gCanCrossPost,
 				                   gConfigSettings.enableTextReplacements, gConfigSettings.allowUserSettings,
-				                   gConfigSettings.allowSpellCheck, gConfigSettings.allowColorSelection);
+				                   gConfigSettings.allowSpellCheck, gConfigSettings.allowColorSelection, gCanChangeSubject);
 				clearEditAreaBuffer();
 				fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
 				               gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
@@ -1664,45 +1861,69 @@ function doEditLoop()
 					currentWordLength = enterRetObj.currentWordLength;
 					returnCode = enterRetObj.returnCode;
 					continueOn = enterRetObj.continueOn;
-					// Check for whether we should do quote selection or
-					// show the help screen (if the user entered /Q or /?)
+					// Do what the next action should be after the enter key was pressed
 					if (continueOn)
 					{
-						if (enterRetObj.doQuoteSelection)
+						switch (enterRetObj.nextAction)
 						{
-							if (gUseQuotes)
-							{
-								enterRetObj = doQuoteSelection(curpos, currentWordLength);
-								curpos.x = enterRetObj.x;
-								curpos.y = enterRetObj.y;
-								currentWordLength = enterRetObj.currentWordLength;
-								// If user input timed out, then abort.
-								if (enterRetObj.timedOut)
+							case ENTER_ACTION_DO_QUOTE_SELECTION:
+								if (gUseQuotes)
 								{
-									returnCode = 1; // Aborted
-									continueOn = false;
-									console.crlf();
-									console.print("\x01n\x01h\x01r" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
-									continue;
+									enterRetObj = doQuoteSelection(curpos, currentWordLength);
+									curpos.x = enterRetObj.x;
+									curpos.y = enterRetObj.y;
+									currentWordLength = enterRetObj.currentWordLength;
+									// If user input timed out, then abort.
+									if (enterRetObj.timedOut)
+									{
+										returnCode = 1; // Aborted
+										continueOn = false;
+										console.crlf();
+										console.print("\x01n\x01h\x01r" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
+										continue;
+									}
 								}
-							}
-						}
-						else if (enterRetObj.showHelp)
-						{
-							displayProgramInfo(true, false);
-							displayCommandList(false, false, true, gCanCrossPost,
-							                   gConfigSettings.enableTextReplacements, gConfigSettings.allowUserSettings,
-							                   gConfigSettings.allowSpellCheck, gConfigSettings.allowColorSelection);
-							clearEditAreaBuffer();
-							fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-							               gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
-							               displayEditLines);
-							console.gotoxy(curpos);
-						}
-						else if (enterRetObj.doCrossPostSelection)
-						{
-							if (gCanCrossPost)
-								doCrossPosting();
+								break;
+							case ENTER_ACTION_DO_CROSS_POST_SELECTION:
+								if (gCanCrossPost)
+									doCrossPosting();
+								break;
+							case ENTER_ACTION_DO_MEME_INPUT:
+								// Input a meme from the user
+								var memeInputRetObj = doMemeInput();
+								// If a meme was added and we're able, move the
+								// cursor below the meme that was addded
+								if (memeInputRetObj.numMemeLines > 0)
+								{
+									var newY = curpos.y + memeInputRetObj.numMemeLines;
+									if (newY <= gEditBottom)
+										curpos.y = newY;
+									else
+										curpos.y = gEditBottom;
+								}
+								if (memeInputRetObj.refreshScreen)
+								{
+									// Refresh the screen
+									clearEditAreaBuffer();
+									fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+									               gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+									               displayEditLines);
+								}
+								console.gotoxy(curpos);
+								break;
+							case ENTER_ACTION_SHOW_HELP:
+								displayProgramInfo(true, false);
+								displayCommandList(false, false, true, gCanCrossPost,
+								                   gConfigSettings.enableTextReplacements, gConfigSettings.allowUserSettings,
+								                   gConfigSettings.allowSpellCheck, gConfigSettings.allowColorSelection, gCanChangeSubject);
+								clearEditAreaBuffer();
+								fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+								               gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+								               displayEditLines);
+								console.gotoxy(curpos);
+								break;
+							default:
+								break;
 						}
 					}
 					// For attribute code right-shifting, start at index+1 if in the middle of the line or index at the beginning
@@ -1911,17 +2132,20 @@ function doEditLoop()
 				doUserSettings(curpos, true);
 				break;
 			case CHANGE_SUBJECT_KEY:
-				console.attributes = "N";
-				console.gotoxy(gSubjPos.x, gSubjPos.y);
-				var subj = console.getstr(gSubjScreenLen, K_LINE|K_NOCRLF|K_NOSPIN|K_TRIM);
-				if (subj.length > 0)
-					gMsgSubj = subj;
-				// Refresh the subject line on the screen with the proper colors etc.
-				fpRefreshSubjectOnScreen(gSubjPos.x, gSubjPos.y, gSubjScreenLen, gMsgSubj);
-
-				// Restore the edit color and cursor position
-				console.print(chooseEditColor());
-				console.gotoxy(curpos);
+				if (gCanChangeSubject)
+				{
+					console.attributes = "N";
+					console.gotoxy(gSubjPos.x, gSubjPos.y);
+					var subj = console.getstr(gSubjScreenLen, K_LINE|K_NOCRLF|K_NOSPIN|K_TRIM);
+					if (subj.length > 0)
+						gMsgSubj = subj;
+					// Refresh the subject line on the screen with the proper colors etc.
+					fpRefreshSubjectOnScreen(gSubjPos.x, gSubjPos.y, gSubjScreenLen, gMsgSubj);
+
+					// Restore the edit color and cursor position
+					console.print(chooseEditColor());
+					console.gotoxy(curpos);
+				}
 				break;
 			default:
 				// For the tab character, insert 3 spaces.  Otherwise,
@@ -2011,7 +2235,7 @@ function doEditLoop()
 						gEditLines.push(new TextLine());
 						var newLine = new TextLine(taglineRetObj.tagline);
 						gEditLines.push(newLine);
-						reAdjustTextLines(gEditLines, gEditLines.length-1, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection);
+						reAdjustTextLines(gEditLines, gEditLines.length-1, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection, gJustEditingAFile); // gMsgAreaInfo.subBoardCode.length == 0
 					}
 				}
 			}
@@ -2203,7 +2427,7 @@ function doBackspace(pCurpos, pCurrentWordLength)
 			prevTextline = gEditLines[gEditLinesIndex-1].text;
 
 		// Re-adjust the text lines
-		reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection);
+		reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection, gJustEditingAFile); // gMsgAreaInfo.subBoardCode.length == 0
 
 		// 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
@@ -2305,7 +2529,8 @@ function doDeleteKey(pCurpos, pCurrentWordLength)
 		}
 
 		// Re-adjust the line lengths and refresh the edit area.
-		var textChanged = reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection);
+		var reAdjustRetObj = reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection, gJustEditingAFile); // gMsgAreaInfo.subBoardCode.length == 0
+		var textChanged = reAdjustRetObj.textChanged;
 
 		// If the line text changed, then update the message area from the
 		// current line on down.
@@ -2350,7 +2575,8 @@ function doDeleteKey(pCurpos, pCurrentWordLength)
 		}
 
 		// Re-adjust the text lines, update textChanged & set a few other things
-		textChanged = textChanged || reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection);
+		var reAdjustRetObj = reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection, gJustEditingAFile); // gMsgAreaInfo.subBoardCode.length == 0
+		textChanged = textChanged || reAdjustRetObj.textChanged;
 		retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
 		var startRow = retObj.y;
 		var startEditLinesIndex = gEditLinesIndex;
@@ -2401,6 +2627,7 @@ function doPrintableChar(pUserInput, pCurpos, 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.
+	var idxIsAtEndOfTextLine = false;
 	if (gTextLineIndex > gEditLines[gEditLinesIndex].screenLength())
 	{
 		var numSpaces = gTextLineIndex - gEditLines[gEditLinesIndex].screenLength();
@@ -2410,7 +2637,10 @@ function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
 	}
 	// If gTextLineIndex is at the end of the line, then just append the char.
 	else if (gTextLineIndex == gEditLines[gEditLinesIndex].screenLength())
+	{
 		gEditLines[gEditLinesIndex].text += pUserInput;
+		idxIsAtEndOfTextLine = true;
+	}
 	else
 	{
 		// gTextLineIndex is at the beginning or in the middle of the line.
@@ -2448,8 +2678,13 @@ function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
 	// 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;
+	var addedSpaceAtSplitPointDuringReAdjust = false;
 	if (gEditLines[gEditLinesIndex].screenLength() >= gEditWidth)
-		reAdjusted = reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection);
+	{
+		var reAdjustRetObj = reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth, gConfigSettings.allowColorSelection, gJustEditingAFile); // gMsgAreaInfo.subBoardCode.length == 0
+		reAdjusted = reAdjustRetObj.textChanged;
+		addedSpaceAtSplitPointDuringReAdjust = reAdjustRetObj.addedSpaceAtSplitPoint;
+	}
 
 	// placeCursorAtEnd specifies whether or not to place the cursor at its
 	// spot using console.gotoxy() at the end.  This is an optimization.
@@ -2480,7 +2715,14 @@ function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
 			if (gEditLines[gEditLinesIndex].screenLength() == gEditWidth-1)
 				numChars = ((pUserInput == " ") ? 0 : 1);
 			else
+			{
 				numChars = gTextLineIndex - gEditLines[gEditLinesIndex].screenLength();
+				// New (2025-05-04) - If editing a regular file (not a message)
+				// and a space was added where the line was split, increment
+				// numChars
+				if (gJustEditingAFile && addedSpaceAtSplitPointDuringReAdjust) // gMsgAreaInfo.subBoardCode.length == 0
+					++numChars;
+			}
 			retObj.x = gEditLeft + numChars;
 			var originalEditLinesIndex = gEditLinesIndex++;
 			gTextLineIndex = numChars;
@@ -2583,9 +2825,10 @@ function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
 //               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
+//               nextAction: Will have one of the ENTER_ACTION_* values to specify
+//                           what action to take based on the user's input on the
+//                           current line. Defaults to ENTER_ACTION_NONE, for no
+//                           special action.
 function doEnterKey(pCurpos, pCurrentWordLength)
 {
 	// Create the return object
@@ -2595,9 +2838,7 @@ function doEnterKey(pCurpos, pCurrentWordLength)
 		currentWordLength: pCurrentWordLength,
 		returnCode: 0,
 		continueOn: true,
-		doQuoteSelection: false,
-		doCrossPostSelection: false,
-		showHelp: false
+		nextAction: ENTER_ACTION_NONE
 	};
 
 	// Store the current screen row position and gEditLines index.
@@ -2627,7 +2868,8 @@ function doEnterKey(pCurpos, pCurrentWordLength)
 		else if (lineUpper == "/A")
 		{
 			// Confirm with the user
-			if (promptYesNo("Abort message", false, "Abort", false, false))
+			var editObjName = (gMsgAreaInfo.subBoardCode.length > 0 ? "message" : "edit");
+			if (promptYesNo("Abort " + editObjName, false, "Abort", false, false))
 			{
 				retObj.returnCode = 1; // 1: Abort
 				retObj.continueOn = false;
@@ -2654,11 +2896,13 @@ function doEnterKey(pCurpos, pCurrentWordLength)
 				return(retObj);
 			}
 		}
-		// /Q: Do quote selection, and /?: Show help
+		// /Q: Do quote selection or /?: Show help
 		else if ((lineUpper == "/Q") || (lineUpper == "/?"))
 		{
-			retObj.doQuoteSelection = (lineUpper == "/Q");
-			retObj.showHelp = (lineUpper == "/?");
+			if (lineUpper == "/Q")
+				retObj.nextAction = ENTER_ACTION_DO_QUOTE_SELECTION;
+			else if (lineUpper == "/?")
+				retObj.nextAction = ENTER_ACTION_SHOW_HELP;
 			retObj.currentWordLength = 0;
 			gTextLineIndex = 0;
 			gEditLines[gEditLinesIndex].text = "";
@@ -2671,10 +2915,26 @@ function doEnterKey(pCurpos, pCurrentWordLength)
 			console.gotoxy(retObj.x, retObj.y);
 			return(retObj);
 		}
+		// /M: Input & insert a meme
+		else if (lineUpper == "/M")
+		{
+			retObj.nextAction = ENTER_ACTION_DO_MEME_INPUT;
+			retObj.currentWordLength = 0;
+			gTextLineIndex = 0;
+			gEditLines[gEditLinesIndex].text = "";
+			// Blank out the /M on the screen
+			console.print(chooseEditColor());
+			retObj.x = gEditLeft;
+			console.gotoxy(retObj.x, retObj.y);
+			console.print("  ");
+			// Put the cursor where it should be
+			console.gotoxy(retObj.x, retObj.y);
+			return(retObj);
+		}
 		// /C: Cross-post
 		else if (lineUpper == "/C")
 		{
-			retObj.doCrossPostSelection = true;
+			retObj.nextAction = ENTER_ACTION_DO_CROSS_POST_SELECTION;
 
 			// Blank out the data in the text line, set the data in
 			// retObj, and return it.
@@ -2692,7 +2952,7 @@ function doEnterKey(pCurpos, pCurrentWordLength)
 			console.gotoxy(retObj.x, retObj.y);
 			return(retObj);
 		}
-		// /T: List text replacements
+		// /T: List text replacements (do that here)
 		else if (lineUpper == "/T")
 		{
 			if (gConfigSettings.enableTextReplacements)
@@ -2713,7 +2973,7 @@ function doEnterKey(pCurpos, pCurrentWordLength)
 			console.gotoxy(retObj.x, retObj.y);
 			return(retObj);
 		}
-		// /U: User settings
+		// /U: User settings (do that here)
 		else if (lineUpper == "/U")
 		{
 			var currentCursorPos = {
@@ -3690,10 +3950,7 @@ function displayMessageRectangle(pX, pY, pWidth, pHeight, pEditLinesIndex, pClea
 			if (pClearExtraWidth)
 			{
 				if (pWidth > actualLenWritten)
-				{
 					printf("%*s", pWidth-actualLenWritten, "");
-					//console.print(editColor);
-				}
 			}
 		}
 		else
@@ -3760,7 +4017,8 @@ function doESCMenu(pCurpos, pCurrentWordLength)
 			break;
 		case ESC_MENU_ABORT:
 			// Before aborting, ask they user if they really want to abort.
-			if (promptYesNo("Abort message", false, "Abort", false, false))
+			var editObjName = (gMsgAreaInfo.subBoardCode.length > 0 ? "message" : "edit");
+			if (promptYesNo("Abort " + editObjName, false, "Abort", false, false))
 			{
 				returnObj.returnCode = 1; // Aborted
 				returnObj.continueOn = false;
@@ -3797,7 +4055,7 @@ function doESCMenu(pCurpos, pCurrentWordLength)
 		case ESC_MENU_HELP_COMMAND_LIST:
 			displayCommandList(true, true, true, gCanCrossPost,
 			                   gConfigSettings.enableTextReplacements, gConfigSettings.allowUserSettings,
-			                   gConfigSettings.allowSpellCheck, gConfigSettings.allowColorSelection);
+			                   gConfigSettings.allowSpellCheck, gConfigSettings.allowColorSelection, gCanChangeSubject);
 			clearEditAreaBuffer();
 			fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs, gInsertMode,
 			               gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop), displayEditLines);
@@ -3835,6 +4093,28 @@ function doESCMenu(pCurpos, pCurrentWordLength)
 			returnObj.y = spellCheckRetObj.y;
 			returnObj.currentWordLength = spellCheckRetObj.currentWordLength;
 			break;
+		case ESC_MENU_INSERT_MEME:
+			// Input a meme from the user
+			var memeInputRetObj = doMemeInput();
+			// If a meme was added and we're able, move the
+			// cursor below the meme that was addded
+			if (memeInputRetObj.numMemeLines > 0)
+			{
+				var newY = returnObj.y + memeInputRetObj.numMemeLines;
+				if (newY <= gEditBottom)
+					returnObj.y = newY;
+				else
+					returnObj.y = gEditBottom;
+			}
+			if (memeInputRetObj.refreshScreen)
+			{
+				// Refresh the screen
+				clearEditAreaBuffer();
+				fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+				               gInsertMode, gUseQuotes, gEditLinesIndex-(returnObj.y-gEditTop),
+				               displayEditLines);
+			}
+			break;
 	}
 
 	// Make sure the edit color attribute is set back.
@@ -5658,12 +5938,22 @@ function printEditLine(pIndex, pUseColors, pStart, pLength)
 	if (useColors)
 	{
 		var lineLengthToGet = (length > -1 ? length : gEditLines[pIndex].screenLength()-start);
-		// Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js
-		//var lineText = substrWithAttrCodes(gEditLines[pIndex].getText(true), start, lineLengthToGet);
-		// The line's substr() will include the necessary attribute codes
-		//var lineText = gEditLines[pIndex].substr(true, start, lineLengthToGet);
-		var lineText = substrWithAttrCodes(gEditLines[pIndex].getText(true), start, lineLengthToGet);
-		lengthWritten = console.strlen(lineText, str_is_ascii(lineText) ? P_NONE : P_UTF8);
+		var lineText = "";
+		if (gEditLines[pIndex].isAQuoteLine())
+		{
+			// The line is a quote line. Print the line with the configured quote color.
+			lineText = "\x01n" + gQuoteLineColor + gEditLines[pIndex].text.substr(start, lineLengthToGet);
+		}
+		else
+		{
+			// The line isn't a quote line
+			// Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js
+			//var lineText = substrWithAttrCodes(gEditLines[pIndex].getText(true), start, lineLengthToGet);
+			// The line's substr() will include the necessary attribute codes
+			//lineText = gEditLines[pIndex].substr(true, start, lineLengthToGet);
+			lineText = substrWithAttrCodes(gEditLines[pIndex].getText(true), start, lineLengthToGet);
+		}
+		lengthWritten = console.strlen(lineText, str_is_utf8(strip_ctrl(lineText)) ? P_UTF8 : P_NONE);
 		printStrConsideringUTF8(lineText, gPrintMode);
 	}
 	else
@@ -6416,7 +6706,7 @@ function letUserUploadMessageFile(pCurpos)
 		originalCurpos = console.getxy();
 
 	var uploadedMessage = false;
-	if (promptYesNo("Upload a mesage", true, "Upload message", true, true))
+	if (promptYesNo("Upload a message", true, "Upload message", true, true))
 	{
 		console.attributes = "N";
 		console.gotoxy(1, console.screen_rows);
@@ -6695,3 +6985,101 @@ function promptForGraphicsChar(pCurPos)
 
 	console.attributes = "N";
 }
+
+// Inputs a meme from the user and inserts it into the message
+//
+// Return value: An object with the following properties:
+//               refreshScreen: Whether or not the whole screen should be refreshed (boolean)
+//               numMemeLines: The number of lines in the meme
+function doMemeInput()
+{
+	var retObj = {
+		refreshScreen: true,
+		numMemeLines: 0
+	};
+
+
+	console.attributes = "N";
+	console.gotoxy(1, console.screen_rows);
+	console.crlf();
+	console.print("\x01g\x01h- \x01n\x01cMeme input \x01g-\x01n\r\n");
+	// Allow the user to change the meme width if they want to
+	//console.print("\x01cMeme width\x01g\x01h: \x01n");
+	//var editWidth = console.screen_columns - 13;
+	var promptText = format("\x01cMeme width (up to \x01h%d\x01n\x01c)\x01g\x01h: \x01n", console.screen_columns-1);
+	console.print(promptText);
+	//var editWidth = console.screen_columns - console.strlen(promptText) - 1;
+	var editWidth = (console.screen_columns-1).toString().length + 1;
+	var inputtedMemeWidth = console.getstr(gConfigSettings.memeSettings.memeDefaultWidth.toString(), editWidth, K_EDIT|K_LINE|K_NOSPIN|K_NUMBER);
+	var memeWidthNum = parseInt(inputtedMemeWidth, 10);
+	if (!(!isNaN(memeWidthNum) && memeWidthNum > 0 && memeWidthNum < console.screen_columns))
+	{
+		memeWidthNum = gConfigSettings.memeSettings.memeDefaultWidth;
+		var errorMsg = format("\x01y\x01hInvalid width (\x01wmust be between 1 and %d (1 less than terminal width)\x01y). Defaulting to %d.\x01n",
+		                      console.screen_columns-1, gConfigSettings.memeSettings.memeDefaultWidth);
+		console.print(lfexpand(word_wrap(errorMsg, console.screen_columns-1)));
+		console.crlf();
+	}
+	// Input the meme text from the user
+	console.print("\x01cWhat do you want to say? \x01g\x01h(\x01cENTER\x01g=\x01n\x01cAbort\x01g\x01h)\x01n\r\n");
+	var text = console.getstr(gConfigSettings.memeSettings.memeMaxTextLen, K_LINEWRAP);
+	if (typeof(text) !== "string" || text.length == 0)
+		return retObj;
+	var memeOptions = {
+		random: gConfigSettings.memeSettings.random,
+		border: gConfigSettings.memeSettings.memeDefaultBorderIdx,
+		color: gConfigSettings.memeSettings.memeDefaultColorIdx,
+		justify: gConfigSettings.memeSettings.justify,
+		width: memeWidthNum
+	};
+	var msg = load("meme_chooser.js", text, memeOptions);
+	var memeContent = (typeof(msg) === "string" ? msg : "");
+
+	// Split the meme content into lines
+	var memeLines = memeContent.split("\r\n");
+	// The last meme line will probably be an empty line due to
+	// the trailing \r\n, so remove it
+	if (memeLines.length > 0 && memeLines[memeLines.length-1].length == 0)
+		memeLines.pop();
+	retObj.numMemeLines = memeLines.length;
+	// If the user entered a meme, then add it to the message
+	if (memeLines.length > 0)
+	{
+		// The previous edit line should have a hard newline ending
+		if (gEditLinesIndex > 0)
+			gEditLines[gEditLinesIndex-1].hardNewlineEnd = true;
+
+		// Remove the current edit line; the meme will start on this line
+		gEditLines.splice(gEditLinesIndex, 1);
+
+		// Ensure the meme lines are inserted at the correct place
+		// in gEditLines, depending on the edit lines array index
+		if (gEditLinesIndex == gEditLines.length-1)
+		{
+			// Append the meme to the edit lines
+			for (var i = 0; i < memeLines.length; ++i)
+				gEditLines.push(new TextLine(memeLines[i], true, false));
+			gEditLines.push(new TextLine("", true, false));
+			gEditLinesIndex = gEditLines.length - 1;
+			gTextLineIndex = 0;
+			retObj.currentWordLength = 0;
+		}
+		else
+		{
+			// Currently in the middle of the message
+			for (var i = 0; i < memeLines.length; ++i)
+				gEditLines.splice(gEditLinesIndex++, 0, new TextLine(memeLines[i], true, false));
+		}
+		/*
+		retObj.currentWordLength = 0;
+		gTextLineIndex = 0;
+		gEditLines[gEditLinesIndex].text = "";
+		// Blank out the /M on the screen
+		console.print(chooseEditColor());
+		retObj.x = gEditLeft;
+		console.gotoxy(retObj.x, retObj.y);
+		*/
+	}
+
+	return retObj;
+}
diff --git a/exec/SlyEdit_DCTStuff.js b/exec/SlyEdit_DCTStuff.js
index d054cd29ef7f2633979d8490e20a5d5de7d81ef2..dc78f9d5a9f656ffaafea32457a31bcd56a51871 100644
--- a/exec/SlyEdit_DCTStuff.js
+++ b/exec/SlyEdit_DCTStuff.js
@@ -36,14 +36,15 @@ 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_SETTINGS = 6;
-var DCTMENU_SYSOP_IMPORT_FILE = 7;
-var DCTMENU_SYSOP_EXPORT_FILE = 11;
-var DCTMENU_HELP_COMMAND_LIST = 8;
-var DCTMENU_GRAPHIC_CHAR = 9;
-var DCTMENU_HELP_PROGRAM_INFO = 10;
-var DCTMENU_CROSS_POST = 12;
-var DCTMENU_LIST_TXT_REPLACEMENTS = 13;
+var DCTMENU_EDIT_INSERT_MEME = 6;
+var DCTMENU_EDIT_SETTINGS = 7;
+var DCTMENU_SYSOP_IMPORT_FILE = 8;
+var DCTMENU_SYSOP_EXPORT_FILE = 9;
+var DCTMENU_HELP_COMMAND_LIST = 10;
+var DCTMENU_GRAPHIC_CHAR = 11;
+var DCTMENU_HELP_PROGRAM_INFO = 12;
+var DCTMENU_CROSS_POST = 13;
+var DCTMENU_LIST_TXT_REPLACEMENTS = 14;
 
 // Read the color configuration file
 readColorConfig(gConfigSettings.DCTColors.ThemeFilename);
@@ -846,6 +847,7 @@ function doDCTESCMenu(pEditLeft, pEditRight, pEditTop, pDisplayMessageRectangle,
 		doDCTESCMenu.allMenus[editMenuNum].addItem("&Graphic char   Ctrl-G", DCTMENU_GRAPHIC_CHAR);
 		doDCTESCMenu.allMenus[editMenuNum].addItem("&Find Text      Ctrl-N", DCTMENU_EDIT_FIND_TEXT);
 		doDCTESCMenu.allMenus[editMenuNum].addItem("Spe&ll Checker  Ctrl-W", DCTMENU_EDIT_SPELL_CHECKER);
+		doDCTESCMenu.allMenus[editMenuNum].addItem("Insert &Meme          ", DCTMENU_EDIT_INSERT_MEME);
 		doDCTESCMenu.allMenus[editMenuNum].addItem("Setti&ngs       Ctrl-U", DCTMENU_EDIT_SETTINGS);
 		doDCTESCMenu.allMenus[editMenuNum].addExitLoopKey(CTRL_I, DCTMENU_EDIT_INSERT_TOGGLE);
 		doDCTESCMenu.allMenus[editMenuNum].addExitLoopKey(CTRL_N, DCTMENU_EDIT_FIND_TEXT);
@@ -968,6 +970,7 @@ function doDCTESCMenu(pEditLeft, pEditRight, pEditTop, pDisplayMessageRectangle,
 			case CTRL_V:    // Insert/overwrite toggle
 			case "F":       // Find text
 			case CTRL_F:    // Find text
+			case "M":       // Insert meme
 			case "N":       // User settings
 			case CTRL_U:    // User settings
 			case "O":       // Command List
@@ -1063,6 +1066,9 @@ function doDCTESCMenu(pEditLeft, pEditRight, pEditTop, pDisplayMessageRectangle,
 	// Spell checker
 	else if ((userInput == CTRL_W) || (userInput == "L") || (userInput == DCTMENU_EDIT_SPELL_CHECKER))
 		chosenAction = ESC_MENU_SPELL_CHECK;
+	// Insert meme
+	else if ((userInput == "M") || (userInput == DCTMENU_EDIT_INSERT_MEME))
+		chosenAction = ESC_MENU_INSERT_MEME;
 
 	return chosenAction;
 }
@@ -1099,7 +1105,7 @@ function inputMatchesMenuSelection(pInput)
    return((pInput == KEY_ESC) || (pInput == KEY_LEFT) ||
            (pInput == KEY_RIGHT) || (pInput == KEY_ENTER) ||
            (pInput == "S") || (pInput == "A") || (pInput == "E") ||
-           (pInput == "I") || (user.is_sysop && (pInput == "X")) ||
+           (pInput == "I") || (pInput == "M") || (user.is_sysop && (pInput == "X")) ||
            (pInput == "F") || (pInput == "C") || (pInput == "G") ||
            (pInput == "P") || (pInput == "T"));
 }
@@ -1358,6 +1364,14 @@ function DCTMenu_DoInputLoop()
 	}
 	else
 		console.gotoxy(this.topLeftX, this.topLeftY);
+	// TODO:
+	// 2025-05-06 - Kludge: For some reason, the menu isn't
+	// being drawn now until a key is pressed. Clearing the
+	// key buffer seems to help let the menu be drawn:
+	console.clearkeybuffer();
+	// This also helped get the menu to draw:
+	//console.ungetstr(KEY_ENTER);
+	//console.getkey(K_NOCRLF|K_NOECHO|K_NOSPIN);
 	// Draw the top border
 	var innerWidth = this.width - 2;
 	if (this.borderStyle == "single")
diff --git a/exec/SlyEdit_IceStuff.js b/exec/SlyEdit_IceStuff.js
index 96035ca748c941996a06c3143e9228db91a8858a..e3f53d7a18f9611b30b3986a8d44eee2df6c690f 100644
--- a/exec/SlyEdit_IceStuff.js
+++ b/exec/SlyEdit_IceStuff.js
@@ -705,13 +705,16 @@ function doIceESCMenu(pY, pCanCrossPost)
 	var chosenAction = ESC_MENU_EDIT_MESSAGE;
 
 	// IceEdit ESC menu item return values
+	// Assuming an 80-column terminal, there's only so much room
+	// for the menu items.  For the last item, if cross-posting
+	// is enabled, use cross-posting; otherwise, use "Meme".
 	var ICE_ESC_MENU_SAVE = 0;
 	var ICE_ESC_MENU_ABORT = 1;
 	var ICE_ESC_MENU_EDIT = 2;
 	var ICE_ESC_MENU_SETTINGS = 3;
 	var ICE_ESC_MENU_HELP = 4;
 	var ICE_ESC_MENU_SPELL_CHECK = 5;
-	var ICE_ESC_MENU_CROSS_POST = 6;
+	var ICE_ESC_MENU_CROSS_POST_OR_MEME = 6; // Seems a little janky, but it works
 
 	var promptText = "Select An Option:  ";
 
@@ -719,7 +722,9 @@ function doIceESCMenu(pY, pCanCrossPost)
 	console.print(iceText(promptText, "\x01w"));
 	console.cleartoeol("\x01n");
 	// Input loop
-	var lastMenuItem = (pCanCrossPost ? ICE_ESC_MENU_CROSS_POST : ICE_ESC_MENU_SPELL_CHECK);
+	//var lastMenuItem = (pCanCrossPost ? ICE_ESC_MENU_CROSS_POST : ICE_ESC_MENU_SPELL_CHECK);
+	var lastMenuItem = ICE_ESC_MENU_CROSS_POST_OR_MEME;
+	var lastMenuItemText = (pCanCrossPost ? "Cross-post" : "Meme");
 	var userChoice = ICE_ESC_MENU_SAVE;
 	var userInput;
 	var continueOn = true;
@@ -737,8 +742,9 @@ function doIceESCMenu(pY, pCanCrossPost)
 				console.print(iceStyledPromptText("Settings", false) + "\x01n ");
 				console.print(iceStyledPromptText("Help", false) + "\x01n ");
 				console.print(iceStyledPromptText("sPlchk", false));
-				if (pCanCrossPost)
-				console.print("\x01n " + iceStyledPromptText("Cross-post", false));
+				console.print("\x01n " + iceStyledPromptText(lastMenuItemText, false));
+				//if (pCanCrossPost)
+				//	console.print("\x01n " + iceStyledPromptText("Cross-post", false));
 				break;
 			case ICE_ESC_MENU_ABORT:
 				console.print(iceStyledPromptText("Save", false) + "\x01n ");
@@ -747,8 +753,9 @@ function doIceESCMenu(pY, pCanCrossPost)
 				console.print(iceStyledPromptText("Settings", false) + "\x01n ");
 				console.print(iceStyledPromptText("Help", false) + "\x01n ");
 				console.print(iceStyledPromptText("sPlchk", false));
-				if (pCanCrossPost)
-					console.print("\x01n " + iceStyledPromptText("Cross-post", false));
+				console.print("\x01n " + iceStyledPromptText(lastMenuItemText, false));
+				//if (pCanCrossPost)
+				//	console.print("\x01n " + iceStyledPromptText("Cross-post", false));
 				break;
 			case ICE_ESC_MENU_EDIT:
 				console.print(iceStyledPromptText("Save", false) + "\x01n ");
@@ -757,8 +764,9 @@ function doIceESCMenu(pY, pCanCrossPost)
 				console.print(iceStyledPromptText("Settings", false) + "\x01n ");
 				console.print(iceStyledPromptText("Help", false) + "\x01n ");
 				console.print(iceStyledPromptText("sPlchk", false));
-				if (pCanCrossPost)
-					console.print("\x01n " + iceStyledPromptText("Cross-post", false));
+				console.print("\x01n " + iceStyledPromptText(lastMenuItemText, false));
+				//if (pCanCrossPost)
+				//	console.print("\x01n " + iceStyledPromptText("Cross-post", false));
 				break;
 			case ICE_ESC_MENU_SETTINGS:
 				console.print(iceStyledPromptText("Save", false) + "\x01n ");
@@ -767,8 +775,9 @@ function doIceESCMenu(pY, pCanCrossPost)
 				console.print(iceStyledPromptText("Settings", true) + "\x01n ");
 				console.print(iceStyledPromptText("Help", false) + "\x01n ");
 				console.print(iceStyledPromptText("sPlchk", false));
-				if (pCanCrossPost)
-					console.print("\x01n " + iceStyledPromptText("Cross-post", false));
+				console.print("\x01n " + iceStyledPromptText(lastMenuItemText, false));
+				//if (pCanCrossPost)
+				//	console.print("\x01n " + iceStyledPromptText("Cross-post", false));
 				break;
 			case ICE_ESC_MENU_HELP:
 				console.print(iceStyledPromptText("Save", false) + "\x01n ");
@@ -777,8 +786,9 @@ function doIceESCMenu(pY, pCanCrossPost)
 				console.print(iceStyledPromptText("Settings", false) + "\x01n ");
 				console.print(iceStyledPromptText("Help", true) + "\x01n ");
 				console.print(iceStyledPromptText("sPlchk", false));
-				if (pCanCrossPost)
-					console.print("\x01n " + iceStyledPromptText("Cross-post", false));
+				console.print("\x01n " + iceStyledPromptText(lastMenuItemText, false));
+				//if (pCanCrossPost)
+				//	console.print("\x01n " + iceStyledPromptText("Cross-post", false));
 				break;
 			case ICE_ESC_MENU_SPELL_CHECK:
 				console.print(iceStyledPromptText("Save", false) + "\x01n ");
@@ -787,18 +797,19 @@ function doIceESCMenu(pY, pCanCrossPost)
 				console.print(iceStyledPromptText("Settings", false) + "\x01n ");
 				console.print(iceStyledPromptText("Help", false) + "\x01n ");
 				console.print(iceStyledPromptText("sPlchk", true));
-				if (pCanCrossPost)
-					console.print("\x01n " + iceStyledPromptText("Cross-post", false));
+				console.print("\x01n " + iceStyledPromptText(lastMenuItemText, false));
+				//if (pCanCrossPost)
+				//	console.print("\x01n " + iceStyledPromptText("Cross-post", false));
 				break;
-				break;
-			case ICE_ESC_MENU_CROSS_POST:
+			case ICE_ESC_MENU_CROSS_POST_OR_MEME:
 				console.print(iceStyledPromptText("Save", false) + "\x01n ");
 				console.print(iceStyledPromptText("Abort", false) + "\x01n ");
 				console.print(iceStyledPromptText("Edit", false) + "\x01n ");
 				console.print(iceStyledPromptText("Settings", false) + "\x01n ");
 				console.print(iceStyledPromptText("Help", false) + "\x01n ");
 				console.print(iceStyledPromptText("sPlchk", false) + "\x01n ");
-				console.print(iceStyledPromptText("Cross-post", true));
+				//console.print(iceStyledPromptText("Cross-post", true));
+				console.print(iceStyledPromptText(lastMenuItemText, true));
 				break;
 		}
 
@@ -838,7 +849,14 @@ function doIceESCMenu(pY, pCanCrossPost)
 			case "C": // Cross-post
 				if (pCanCrossPost)
 				{
-					userChoice = ICE_ESC_MENU_CROSS_POST;
+					userChoice = ICE_ESC_MENU_CROSS_POST_OR_MEME;
+					continueOn = false;
+				}
+				break;
+			case "M": // Meme
+				if (!pCanCrossPost)
+				{
+					userChoice = ICE_ESC_MENU_CROSS_POST_OR_MEME;
 					continueOn = false;
 				}
 				break;
@@ -878,8 +896,11 @@ function doIceESCMenu(pY, pCanCrossPost)
 		case ICE_ESC_MENU_SPELL_CHECK:
 			chosenAction = ESC_MENU_SPELL_CHECK;
 			break;
-		case ICE_ESC_MENU_CROSS_POST:
-			chosenAction = ESC_MENU_CROSS_POST_MESSAGE;
+		case ICE_ESC_MENU_CROSS_POST_OR_MEME:
+			if (pCanCrossPost)
+				chosenAction = ESC_MENU_CROSS_POST_MESSAGE;
+			else
+				chosenAction = ESC_MENU_INSERT_MEME;
 			break;
 	}
 
diff --git a/exec/SlyEdit_Misc.js b/exec/SlyEdit_Misc.js
index e9a1246795cb7def8f6220270759cc4559e0dbe1..a90dadd16746d57f0cbbadf2e300adff98902363 100644
--- a/exec/SlyEdit_Misc.js
+++ b/exec/SlyEdit_Misc.js
@@ -127,6 +127,7 @@ var ESC_MENU_CROSS_POST_MESSAGE = 10;
 var ESC_MENU_LIST_TEXT_REPLACEMENTS = 11;
 var ESC_MENU_USER_SETTINGS = 12;
 var ESC_MENU_SPELL_CHECK = 13;
+var ESC_MENU_INSERT_MEME = 14;
 
 
 var COPYRIGHT_YEAR = 2025;
@@ -1863,8 +1864,9 @@ function displayHelpHeader()
 //  pUserSettings: Whether or not the user settings feature is enabled
 //  pSpellCheck: Whether or not spell check is allowed
 //  pCanChangeColor: Whether or not changing text color is allowed
+//  pCanChangeSubject: Whether or not changing the subject is allowed
 function displayCommandList(pDisplayHeader, pClear, pPause, pCanCrossPost, pTxtReplacments,
-                            pUserSettings, pSpellCheck, pCanChangeColor)
+                            pUserSettings, pSpellCheck, pCanChangeColor, pCanChangeSubject)
 {
 	if (pClear)
 		console.clear("\x01n");
@@ -1908,9 +1910,10 @@ function displayCommandList(pDisplayHeader, pClear, pPause, pCanCrossPost, pTxtR
 	//displayCmdKeyFormattedDouble("Ctrl-G", "General help", "/A", "Abort", true);
 	displayCmdKeyFormattedDouble("Ctrl-G", "Input graphic character", "/A", "Abort", true);
 	displayCmdKeyFormattedDouble("Ctrl-L", "Command key list (this list)", "/S", "Save", true);
-	displayCmdKeyFormattedDouble("", "", "/Q", "Quote message", true);
 	if (pTxtReplacments)
 		displayCmdKeyFormattedDouble("Ctrl-T", "List text replacements", "/T", "List text replacements", true);
+	displayCmdKeyFormattedDouble("", "", "/Q", "Quote message", true);
+	displayCmdKeyFormattedDouble("", "", "/M", "Add a meme", true);
 	if (pUserSettings)
 		displayCmdKeyFormattedDouble("", "", "/U", "Your user settings", true);
 	if (pCanCrossPost)
@@ -1925,7 +1928,10 @@ function displayCommandList(pDisplayHeader, pClear, pPause, pCanCrossPost, pTxtR
 	displayCmdKeyFormattedDouble("Ctrl-Q", "Quote message", "Ctrl-W", "Word/text search", true);
 	displayCmdKeyFormattedDouble("Insert/Ctrl-I", "Toggle insert/overwrite mode",
 	                             "Ctrl-D", "Delete line", true);
-	displayCmdKeyFormattedDouble("Ctrl-S", "Change subject", "ESC", "Command menu", true);
+	if (pCanChangeSubject)
+		displayCmdKeyFormattedDouble("Ctrl-S", "Change subject", "ESC", "Command menu", true);
+	else
+		displayCmdKeyFormattedDouble("", "", "ESC", "Command menu", true);
 	// For the remaining hotkeys, build an array of them based on whether they're allowed or not.
 	// Then with the array, output each pair of hotkeys on the same line, and if there's only one
 	// left, display it by itself.
@@ -2032,7 +2038,7 @@ function displayProgramInfo(pClear, pPause)
 	console.center("\x01n\x01h\x01c" + EDITOR_PROGRAM_NAME + "\x01n \x01cVersion \x01g" +
 	               EDITOR_VERSION + " \x01w\x01h(\x01b" + EDITOR_VER_DATE + "\x01w)");
 	console.center("\x01n\x01cby Eric Oulashin");
-	console.crlf();
+	//console.crlf();
 	console.print("\x01n\x01cSlyEdit is a full-screen message editor for Synchronet that mimics the look &\r\n");
 	console.print("feel of IceEdit or DCT Edit.");
 	console.crlf();
@@ -2139,6 +2145,9 @@ function promptYesNo(pQuestion, pDefaultYes, pBoxTitle, pIceRefreshForBothAnswer
 // Return value: An object containing the settings as properties.
 function ReadSlyEditConfigFile()
 {
+	// Meme library for meme definitions
+	var memeLib = load({}, "meme_lib.js");
+	// Configuration settings
 	var cfgObj = {
 		// Default settings
 		thirdPartyLoadOnStart: [],
@@ -2163,6 +2172,14 @@ function ReadSlyEditConfigFile()
 		allowEditQuoteLines: true,
 		allowSpellCheck: true,
 		dictionaryFilenames: [],
+		memeSettings: {
+			memeMaxTextLen: 500,
+			memeDefaultWidth: 39,
+			random: false,
+			memeDefaultBorderIdx: 0,
+			memeDefaultColorIdx: 0,
+			justify: memeLib.JUSTIFY_CENTER
+		},
 
 		// General SlyEdit color settings
 		genColors: {
@@ -2337,6 +2354,77 @@ function ReadSlyEditConfigFile()
 				cfgObj.taglinePrefix = behaviorSettings.taglinePrefix;
 			if (behaviorSettings.hasOwnProperty("dictionaryFilenames") && typeof(behaviorSettings.dictionaryFilenames) === "string")
 				cfgObj.dictionaryFilenames = parseDictionaryConfig(behaviorSettings.dictionaryFilenames, js.exec_dir);
+			if (behaviorSettings.hasOwnProperty("memeMaxTextLen") && typeof(behaviorSettings.memeMaxTextLen) === "number")
+			{
+				if (behaviorSettings.memeMaxTextLen >= 1)
+					cfgObj.memeSettings.memeMaxTextLen = behaviorSettings.memeMaxTextLen;
+			}
+			if (behaviorSettings.hasOwnProperty("memeDefaultWidth") && typeof(behaviorSettings.memeDefaultWidth) === "number")
+				{
+					if (behaviorSettings.memeDefaultWidth >= 1)
+						cfgObj.memeSettings.memeDefaultWidth = behaviorSettings.memeDefaultWidth;
+				}
+			if (behaviorSettings.hasOwnProperty("memeStyleRandom") && typeof(behaviorSettings.memeStyleRandom) === "boolean")
+				cfgObj.memeSettings.random = behaviorSettings.memeStyleRandom;
+			if (behaviorSettings.hasOwnProperty("memeDefaultBorder"))
+			{
+				var borderSettingType = typeof(behaviorSettings.memeDefaultBorder);
+				if (borderSettingType === "string")
+				{
+					var borderUpper = behaviorSettings.memeDefaultBorder.toUpperCase();
+					if (borderUpper == "NONE" || borderUpper == "BORDER_NONE")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_NONE;
+					else if (borderUpper == "SINGLE" || borderUpper == "BORDER_SINGLE")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_SINGLE;
+					else if (borderUpper == "MIXED1" || borderUpper == "BORDER_MIXED1")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_MIXED1;
+					else if (borderUpper == "MIXED2" || borderUpper == "BORDER_MIXED2")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_MIXED2;
+					else if (borderUpper == "MIXED3" || borderUpper == "BORDER_MIXED3")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_MIXED3;
+					else if (borderUpper == "DOUBLE" || borderUpper == "BORDER_DOUBLE")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_DOUBLE;
+					else if (borderUpper == "ORNATE1" || borderUpper == "BORDER_ORNATE1")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_ORNATE1;
+					else if (borderUpper == "ORNATE2" || borderUpper == "BORDER_ORNATE2")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_ORNATE2;
+					else if (borderUpper == "ORNATE3" || borderUpper == "BORDER_ORNATE3")
+						cfgObj.memeSettings.memeDefaultBorderIdx = memeLib.BORDER_ORNATE3;
+				}
+				else if (borderSettingType === "number")
+				{
+					if (behaviorSettings.memeDefaultBorder >= 1 && behaviorSettings.memeDefaultBorder < memeLib.BORDER_COUNT)
+						cfgObj.memeSettings.memeDefaultBorderIdx = behaviorSettings.memeDefaultBorder - 1;
+				}
+			}
+			if (behaviorSettings.hasOwnProperty("memeDefaultColor"))
+			{
+				var memeColorSettingType = typeof(behaviorSettings.memeDefaultColor);
+				if (memeColorSettingType  === "number")
+				{
+					if (behaviorSettings.memeMaxTextLen >= 1)
+						cfgObj.memeSettings.memeDefaultColorIdx = behaviorSettings.memeDefaultColor - 1;
+				}
+			}
+			if (behaviorSettings.hasOwnProperty("memeJustify"))
+			{
+				var justifySettingType = typeof(behaviorSettings.memeJustify);
+				if (justifySettingType === "string")
+				{
+					var justifyUpper = behaviorSettings.memeJustify.toUpperCase();
+					if (justifyUpper == "CENTER" || justifyUpper == "JUSTIFY_CENTER")
+						cfgObj.memeSettings.justify = memeLib.JUSTIFY_CENTER;
+					else if (justifyUpper == "LEFT" || justifyUpper == "JUSTIFY_LEFT")
+						cfgObj.memeSettings.justify = memeLib.JUSTIFY_LEFT;
+					else if (justifyUpper == "RIGHT" || justifyUpper == "JUSTIFY_RIGHT")
+						cfgObj.memeSettings.justify = memeLib.JUSTIFY_RIGHT;
+				}
+				else if (justifySettingType === "number")
+				{
+					if (behaviorSettings.memeJustify >= 0 && behaviorSettings.memeJustify < memeLib.JUSTIFY_COUNT)
+						cfgObj.memeSettings.justify = behaviorSettings.memeJustify;
+				}
+			}
 		}
 
 		// Color settings
@@ -2463,29 +2551,38 @@ function splitStrStable(pStr, pMaxLen)
 //  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)
 //  pUsingColors: Boolean - Whether or not text color/attribute codes are being used
-//
-// Return value: Boolean - Whether or not any text was changed.
-function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth, pUsingColors)
+//  pEditingAFile: Boolean - Whether or not we're editing a file (rather than posting
+//                 a message in email or a sub-board)
+//
+// Return value: An object with the following parameters:
+//               textChanged: Boolean - Whether or not any text was changed.
+//               addedSpaceAtSplitPoint: Boolean - Whether or not a space was added at the split point
+//                                       (possible when editing a regular text file rather than a message)
+function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth, pUsingColors, pEditingAFile)
 {
+	var retObj = {
+		textChanged: false,
+		addedSpaceAtSplitPoint: false
+	};
+
+
 	// 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;
+		return retObj;
 	if (typeof(pEndIndex) != "number")
-		return false;
+		return retObj;
 	if (typeof(pEditWidth) != "number")
-		return false;
+		return retObj;
 	// Range checking
 	if ((pStartIndex < 0) || (pStartIndex >= pTextLineArray.length))
-		return false;
+		return retObj;
 	if ((pEndIndex <= pStartIndex) || (pEndIndex < 0))
-		return false;
+		return retObj;
 	if (pEndIndex > pTextLineArray.length)
 		pEndIndex = pTextLineArray.length;
 	if (pEditWidth <= 5)
-		return false;
-
-	var textChanged = false; // We'll return this upon function exit.
+		return retObj;
 
 	var usingColors = (typeof(pUsingColors) === "boolean" ? pUsingColors : true);
 
@@ -2525,10 +2622,12 @@ function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth, p
 			tempText = pTextLineArray[i].text.substr(spaceFoundAtSplitIdx ? splitIndex+1 : splitIndex);
 			// Remove the attributes from the end of the line that was cut short, to be moved to the beginning of
 			// the next line. Note: This must be done before shortening the text.
+			// New (2025-04-23): Note: It seems using splitIndex+1 wouldn't be a good idea
 			var lastAttrs = pTextLineArray[i].popAttrsFromEnd(splitIndex);
 			// Remove the text from the line up to splitIndex
+			//var charAfter = pTextLineArray[i].text.substr(splitIndex, 1); // Temporary (for debugging)
 			pTextLineArray[i].text = pTextLineArray[i].text.substr(0, splitIndex);
-			textChanged = true;
+			retObj.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;
@@ -2552,6 +2651,24 @@ function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth, p
 				// from the split index but we removed the space there.
 				if (spaceFoundAtSplitIdx)
 				{
+					// A space was found at the split index.
+					// If we're editing a file, then add the space back to the end of
+					// that text line. When we're editing a file, spaces aren't added
+					// in between the text lines, so we need to ensure the space is
+					// still there if there was originally a space.
+
+					// 2025-05-04: The following check seems needed when editing a file, but
+					// was causeing an issue where if a line is wrapped, the cursor is placed
+					// directly under the last character rather than at the end of the
+					// text line. I've fixed this issue but left this comment in for future
+					// reference.
+					// This is done below in the 'else' block too.
+					if (pEditingAFile) // New (2025-04-24)
+					{
+						pTextLineArray[i].text += " "; // New (2025-04-24)
+						retObj.addedSpaceAtSplitPoint = true;
+					}
+
 					var lastAttrKeys = Object.keys(lastAttrs);
 					if (lastAttrKeys.length > 0)
 					{
@@ -2575,11 +2692,29 @@ function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth, p
 			else
 			{
 				// Did not append a new line.
+				// New (2025-04-23)
+				// If we're editing a file and a space was found where the line
+				// was split, then add the space back to the end of that text
+				// line. When we're editing a file, spaces aren't added in between
+				// the text lines, so we need to ensure the space is still there if
+				// there was originally a space.
+				if (pEditingAFile)
+				{
+					if (spaceFoundAtSplitIdx)
+					{
+						pTextLineArray[i].text += " ";
+						retObj.addedSpaceAtSplitPoint = true;
+					}
+				}
+				// End new (2025-04-23)
 				// 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;
+					if (pEditingAFile)
+						pTextLineArray[nextLineIndex].text = tempText + pTextLineArray[nextLineIndex].text;
+					else // Editing a message for email/sub-board
+						pTextLineArray[nextLineIndex].text = tempText + " " + pTextLineArray[nextLineIndex].text;
 					// Move the next line's current attributes to the right
 					pTextLineArray[nextLineIndex].moveAttrIdxes(0, tempText.length + 1);
 					// Add the attributes from the last of the line to the next line, adjusting the
@@ -2659,7 +2794,9 @@ function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth, p
 					var prependedTextWithSpace = false;
 					if ((pTextLineArray[i].text.charAt(pTextLineArray[i].text.length-1) != " ") && (pTextLineArray[nextLineIndex].text.substr(0, 1) != " "))
 					{
-						tempText = " ";
+						// TODO: Need to check pEditingAFile here?
+						if (!pEditingAFile) // Editing a message for email or a sub-board
+							tempText = " ";
 						prependedTextWithSpace = true;
 					}
 					tempText += pTextLineArray[nextLineIndex].text.substr(0, splitIndex);
@@ -2673,7 +2810,7 @@ function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth, p
 						// Set the next line's text: Trim off the front up to splitIndex+1.  Also, capture any attribute
 						// codes removed from the front of the next line (to be moved up).
 						var frontAttrs = pTextLineArray[nextLineIndex].trimFront(splitIndex+1);
-						textChanged = true;
+						retObj.textChanged = true;
 						if (prependedTextWithSpace)
 							++currentLineOriginalLen; // To fix off-by-1 issue with color/attribute codes
 						for (var textLineIdx in frontAttrs)
@@ -2699,14 +2836,14 @@ function reAdjustTextLines(pTextLineArray, pStartIndex, pEndIndex, pEditWidth, p
 					if (!pTextLineArray[nextLineIndex].hardNewlineEnd)
 					{
 						pTextLineArray.splice(nextLineIndex, 1);
-						textChanged = true;
+						retObj.textChanged = true;
 					}
 				}
 			}
 		}
 	}
 
-	return textChanged;
+	return retObj;
 }
 
 // Returns indexes of the first unquoted text line and the next
@@ -3187,8 +3324,9 @@ function stringIsEmptyOrOnlyWhitespace(pString)
 //  pMsgAreaName: The name of the message area being posted to
 //
 // Return value: An object containing the following properties:
-//  lastMsg: The last message in the sub-board (i.e., bbs.smb_last_msg)
-//  totalNumMsgs: The total number of messages in the sub-board (i.e., bbs.smb_total_msgs)
+//  lastMsg: The last message in the sub-board (i.e., bbs.smb_last_msg), or -1 if editing a file
+//  totalNumMsgs: The total number of messages in the sub-board (i.e., bbs.smb_total_msgs),
+//                or 0 if editing a file
 //  curMsgNum: The number/index of the current message being read.  Starting
 //             with Synchronet 3.16 on May 12, 2013, this is the absolute
 //             message number (bbs.msg_number).  For Synchronet builds before
@@ -3196,17 +3334,40 @@ function stringIsEmptyOrOnlyWhitespace(pString)
 //             bbs.msg_number is preferred because it works properly in all
 //             situations, whereas in earlier builds, bbs.msg_number was
 //             always given to JavaScript scripts as 0.
+//             If editing a file, this will be -1.
 //  msgNumIsOffset: Boolean - Whether or not the message number is an offset.
 //                  If not, then it is the absolute message number (i.e.,
 //                  bbs.msg_number).
-//  subBoardCode: The current sub-board code (i.e., bbs.smb_sub_code)
-//  grpIndex: The message group index for the sub-board
+//  subBoardCode: The current sub-board code (i.e., bbs.smb_sub_code, "mail", or "" if editing a file)
+//  grpIndex: The message group index for the sub-board (-1 if personal mail or editing a file)
 function getCurMsgInfo(pMsgAreaName)
 {
 	var retObj = {
-		msgNumIsOffset: false
+		lastMsg: -1,
+		totalNumMsgs: 0,
+		curMsgNum: -1,
+		msgNumIsOffset: false,
+		subBoardCode: "",
+		grpIndex: -1
 	};
-	if (bbs.smb_sub_code.length > 0)
+	if (pMsgAreaName.length == 0)
+	{
+		// No message area name. In this case, the user must be editing a file.
+		// We can leave the return values as defaults. SlyEdit can see if the
+		// user is editing a file by checking whether subBoardCode is an empty
+		// string.
+	}
+	else if (pMsgAreaName.toUpperCase() == "ELECTRONIC MAIL")
+	{
+		retObj.subBoardCode = "mail";
+		retObj.grpIndex = -1;
+		var mailInfoForUser = getPersonalMailInfoForUser();
+		retObj.lastMsg = mailInfoForUser.lastMsg;
+		retObj.totalNumMsgs = mailInfoForUser.totalNumMsgs;
+		retObj.curMsgNum = mailInfoForUser.curMsgNum;
+		retObj.msgNumIsOffset = mailInfoForUser.msgNumIsOffset;
+	}
+	else if (bbs.smb_sub_code.length > 0)
 	{
 		retObj.lastMsg = bbs.smb_last_msg;
 		retObj.totalNumMsgs = bbs.smb_total_msgs;
@@ -5684,6 +5845,72 @@ function replaceAtCodesInStr(pStr)
 	});
 }
 
+// Gets message information for the user's personal email
+//
+// Return value: An object with the following properties:
+//  succeeded: Boolean - Whether or not this function successfully opened the messagebase
+//             and got the information
+//  lastMsg: The last message in the sub-board (i.e., bbs.smb_last_msg)
+//  totalNumMsgs: The total number of messages in the sub-board (i.e., bbs.smb_total_msgs)
+//  curMsgNum: The number/index of the current message being read.  Starting
+//             with Synchronet 3.16 on May 12, 2013, this is the absolute
+//             message number (bbs.msg_number).  For Synchronet builds before
+//             May 12, 2013, this is bbs.smb_curmsg.  Starting on May 12, 2013,
+//             bbs.msg_number is preferred because it works properly in all
+//             situations, whereas in earlier builds, bbs.msg_number was
+//             always given to JavaScript scripts as 0.
+//  msgNumIsOffset: Boolean - Whether or not the message number is an offset.
+//                  If not, then it is the absolute message number (i.e.,
+//                  bbs.msg_number).
+function getPersonalMailInfoForUser()
+{
+	var retObj = {
+		succeeded: false,
+		lastMsg: -1,
+		totalNumMsgs: 0,
+		curMsgNum: -1,
+		msgNumIsOffset: false
+	};
+
+	var msgbase = new MsgBase("mail");
+	if (msgbase.open())
+	{
+		var msgIdxArray = msgbase.get_index();
+		msgbase.close();
+		if (msgIdxArray != null)
+		{
+			for (var i = 0; i < msgIdxArray.length; ++i)
+			{
+				var msgIsToUser = false;
+				if (msgIdxArray[i].hasOwnProperty("to"))
+				{
+					if (msgIdxArray[i].to == user.number)
+						msgIsToUser = true;
+					else
+					{
+						msgIsToUser = (msgIdxArray[i].to == crc16_calc(user.handle.toLowerCase()) ||
+						               msgIdxArray[i].to == crc16_calc(user.alias.toLowerCase()) ||
+						               msgIdxArray[i].to == crc16_calc(user.name.toLowerCase()));
+					}
+				}
+				if (msgIsToUser)
+				{
+					retObj.lastMsg = msgIdxArray[i].number;
+					++retObj.totalNumMsgs;
+					if (retObj.curMsgNum == -1 && !Boolean(msgIdxArray[i].attr & MSG_READ))
+					{
+						retObj.curMsgNum = msgIdxArray[i].number;
+						retObj.msgNumIsOffset = false;
+					}
+				}
+			}
+			retObj.succeeded = true;
+		}
+	}
+
+	return retObj;
+}
+
 // This function displays debug text at a given location on the screen, then
 // moves the cursor back to a given location.
 //
diff --git a/exec/slyedcfg.js b/exec/slyedcfg.js
index 7929861751ee3e817a0183aa82172268c63f8ca3..0be01c694e2b323577e1cd44692285a973edcb6b 100644
--- a/exec/slyedcfg.js
+++ b/exec/slyedcfg.js
@@ -1,7 +1,7 @@
 // SlyEdit configurator: This is a menu-driven configuration program/script for SlyEdit.
 // Any changes are saved to SlyEdit.cfg in sbbs/mods, so that custom changes don't get
 // overridden with SlyEdit.cfg in sbbs/ctrl due to an update.
-// Currently for SlyEdit 1.89e.
+// Currently for SlyEdit 1.90.
 
 "use strict";
 
@@ -10,7 +10,7 @@ require("sbbsdefs.js", "P_NONE");
 require("uifcdefs.js", "UIFC_INMSG");
 
 
-if (!uifc.init("SlyEdit 1.89e Configurator"))
+if (!uifc.init("SlyEdit 1.90 Configurator"))
 {
 	print("Failed to initialize uifc");
 	exit(1);
@@ -162,7 +162,14 @@ function doBehaviorMenu()
 		"enableTextReplacements",
 		"tagLineFilename",
 		"taglinePrefix",
-		"dictionaryFilenames"
+		"dictionaryFilenames",
+		// Meme settings
+		"memeMaxTextLen",    // Number
+		"memeDefaultWidth",  // Number
+		"memeStyleRandom",   // Boolean
+		"memeDefaultBorder", // String
+		"memeDefaultColor",  // Number
+		"memeJustify"        // String
 	];
 	// Menu item text for the options:
 	var optionStrs = [
@@ -185,7 +192,13 @@ function doBehaviorMenu()
 		"Enable text replacements",
 		"Tagline filename",
 		"Tagline prefix",
-		"Dictionary filenames"
+		"Dictionary filenames",
+		"Maximum meme text length",
+		"Default meme width",
+		"Random meme style",
+		"Meme default border style",
+		"Meme default color number",
+		"Meme justification"
 	];
 	// Build the array of items to be displayed on the menu
 	var menuItems = [];
@@ -199,6 +212,12 @@ function doBehaviorMenu()
 	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.taglinePrefix));
 	//menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.dictionaryFilenames.substr(0,30)));
 	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.dictionaryFilenames));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.memeMaxTextLen));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.memeDefaultWidth));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.memeStyleRandom));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.memeDefaultBorder));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.memeDefaultColor));
+	menuItems.push(formatCfgMenuText(itemTextMaxLen, optionStrs[optionIdx++], gCfgInfo.cfgSections.BEHAVIOR.memeJustify));
 
 	// A dictionary of help text for each option, indexed by the option name from the configuration file
 	if (doBehaviorMenu.optHelp == undefined)
@@ -257,6 +276,44 @@ function doBehaviorMenu()
 					menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, "Enable text replacements", getTxtReplacementsVal());
 				}
 			}
+			else if (optName == "memeDefaultBorder")
+			{
+				// Default border style for memes
+				var valBackup = gCfgInfo.cfgSections.BEHAVIOR.memeDefaultBorder;
+				// Prompt the user
+				var possibleOptions = ["none", "single", "mixed1", "mixed2", "mixed3", "double", "ornate1", "ornate2", "ornate3"];
+				var ctx = uifc.list.CTX();
+				var currentSelectedOptIdx = possibleOptions.indexOf(gCfgInfo.cfgSections.BEHAVIOR.memeDefaultBorder);
+				if (currentSelectedOptIdx > -1)
+					ctx.cur = currentSelectedOptIdx;
+				var borderStyleSelection = uifc.list(winMode, optionStrs[optionMenuSelection], possibleOptions, ctx);
+				if (borderStyleSelection >= 0 && borderStyleSelection < possibleOptions.length)
+					gCfgInfo.cfgSections.BEHAVIOR.memeDefaultBorder = possibleOptions[borderStyleSelection];
+				if (gCfgInfo.cfgSections.BEHAVIOR.memeDefaultBorder != valBackup)
+				{
+					anyOptionChanged = true;
+					menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, "Border style", possibleOptions[borderStyleSelection]);
+				}
+			}
+			else if (optName == "memeJustify")
+			{
+				// Text justification for memes
+				var valBackup = gCfgInfo.cfgSections.BEHAVIOR.memeJustify;
+				// Prompt the user
+				var possibleOptions = ["left", "center", "right"];
+				var ctx = uifc.list.CTX();
+				var currentSelectedOptIdx = possibleOptions.indexOf(gCfgInfo.cfgSections.BEHAVIOR.memeJustify);
+				if (currentSelectedOptIdx > -1)
+					ctx.cur = currentSelectedOptIdx;
+				var memeTextJustifySelection = uifc.list(winMode, optionStrs[optionMenuSelection], possibleOptions, ctx);
+				if (memeTextJustifySelection >= 0 && memeTextJustifySelection < possibleOptions.length)
+					gCfgInfo.cfgSections.BEHAVIOR.memeJustify = possibleOptions[memeTextJustifySelection];
+				if (gCfgInfo.cfgSections.BEHAVIOR.memeJustify != valBackup)
+				{
+					anyOptionChanged = true;
+					menuItems[optionMenuSelection] = formatCfgMenuText(itemTextMaxLen, "Meme text justification", possibleOptions[memeTextJustifySelection]);
+				}
+			}
 			else if (itemType === "boolean")
 			{
 				gCfgInfo.cfgSections.BEHAVIOR[optName] = !gCfgInfo.cfgSections.BEHAVIOR[optName];
@@ -631,6 +688,19 @@ function getOptionHelpText()
 	optionHelpText["dictionaryFilenames"] += "sbbs/mods, sbbs/ctrl, or the same directory as SlyEdit. Users can change ";
 	optionHelpText["dictionaryFilenames"] += "this for themselves too.";
 
+	optionHelpText["memeMaxTextLen"] = "Maximum meme text length: The maximum text length allowed for memes. A 'meme' for messages ";
+	optionHelpText["memeMaxTextLen"] += "is a paragraph of text in a stylized box with an optional border and background color.";
+
+	optionHelpText["memeDefaultWidth"] = "Default meme width: The default width of a meme box";
+
+	optionHelpText["memeStyleRandom"] = "Random meme style: For meme input, whether to use an initially random meme style (color & border style).";
+
+	optionHelpText["memeDefaultBorder"] = "Meme default border style: The default border style for meme input.";
+
+	optionHelpText["memeDefaultColor"] = "Meme default color number: The default color (number) for meme input.";
+
+	optionHelpText["memeJustify"] = "Meme justification: The text justification for meme input (left, center, right).";
+
 	// Word-wrap the help text items
 	for (var prop in optionHelpText)
 		optionHelpText[prop] = word_wrap(optionHelpText[prop], gHelpWrapWidth);
@@ -766,6 +836,18 @@ function readSlyEditCfgFile()
 		retObj.cfgSections.BEHAVIOR.allowSpellCheck = true;
 	if (!retObj.cfgSections.BEHAVIOR.hasOwnProperty("dictionaryFilenames"))
 		retObj.cfgSections.BEHAVIOR.dictionaryFilenames = "en,en-US-supplemental";
+	if (!retObj.cfgSections.BEHAVIOR.hasOwnProperty("memeMaxTextLen"))
+		retObj.cfgSections.BEHAVIOR.memeMaxTextLen = 500;
+	if (!retObj.cfgSections.BEHAVIOR.hasOwnProperty("memeDefaultWidth"))
+		retObj.cfgSections.BEHAVIOR.memeDefaultWidth = 39;
+	if (!retObj.cfgSections.BEHAVIOR.hasOwnProperty("memeStyleRandom"))
+		retObj.cfgSections.BEHAVIOR.memeStyleRandom = false;
+	if (!retObj.cfgSections.BEHAVIOR.hasOwnProperty("memeDefaultBorder"))
+		retObj.cfgSections.BEHAVIOR.memeDefaultBorder = "double";
+	if (!retObj.cfgSections.BEHAVIOR.hasOwnProperty("memeDefaultColor"))
+		retObj.cfgSections.BEHAVIOR.memeDefaultColor = 4;
+	if (!retObj.cfgSections.BEHAVIOR.hasOwnProperty("memeJustify"))
+		retObj.cfgSections.BEHAVIOR.memeJustify = "center";
 
 	if (!retObj.cfgSections.hasOwnProperty("STRINGS"))
 	{