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")) {