Subject: Re: bug#67032: 30.0.50; ERC 5.6-git: Treat erc-send-message more responsibly

From: "J.P." <[email-masked]>
Date: Fri, 10 Nov 2023 17:15:14 -0800

v2. Revive `erc-command-indicator' functionality as new module.

"J.P." <[email-masked]> writes:

> In tackling this, I've opted to sidestep a few related concerns that
> will have to be dealt with eventually. The first has to do with
> discovering and possibly reintegrating whatever role the `noncommands'
> module is designed to play in processing specialty slash commands, like
> those reliant on `erc-send-message'. These days (reaching as far back as
> Emacs 26.1), the module appears to effectively be a no-op, at least with
> the default client configuration. Until we know more, I'm inclined to
> let it rot away in erc-goodies, since its code only runs on interactive
> input anyway.

Actually, I've gone ahead and done some cursory spelunking in hopes of
shedding some light on this `noncommands' quandary. First off, there's
no relevant mention of `erc-display-command' or `erc-command-indicator'
in the old erc-discuss archives. But it's my current understanding that
the module seems to have been effectively sidelined when the lone call
site for `erc-display-command' was deep sixed in

  d1036d288de backport: erc bugfixes

(This was back in November of 2013, under 5.3.x and what became Emacs
24.5.) Strangely, that commit did not include any change log or ERC-NEWS
mentions, which is unfortunate because it's wide-ranging and largely
nontrivial. For example, it left behind related faces and options, like
`erc-command-indicator', thereupon effectively neutering them along with
`noncommands'. More recently, `erc-display-command' was commented out in

  0c599ee2e2c * lisp/erc/erc.el: Use `run-hook-with-args` for
              `erc-pre-send-functions`

and later removed by

  a63ed6f78a6 Remove duplicate ERC prompt on reconnect

but the damage had long since been done. My point in drudging this up is
merely to highlight where the behavior concerned diverged and that we'll
likely never know why. I really have no interest in relitigating ancient
motivations and methods. (It's quite possible that discussions did take
place elsewhere, like on Freenode or the old sourceforge list.)

Anyway, since both the interface (options, fonts) and infrastructure are
still in place, I think it behooves us to revive this functionality, not
least as a signal to others that ERC picks up after itself (eventually).
I've thus added some changes to this effect (see last patch).

Screenshot:

https://debbugs.gnu.org/cgi/bugreport.cgi?msg=6;filename=erc-command-indicator.png;att=1;bug=67032

>From 96b62219e43df0585ddfbabaa904342ca0385d22 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Fri, 10 Nov 2023 16:28:33 -0800
Subject: [PATCH 0/6] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (6):
  [5.6] Make ERC error-notice formatting more consistent
  [5.6] Make ERC's message-sending functions more flexible
  [5.6] Simplify logic for inserting ERC prompt input
  [5.6] Slightly simplify text props on echoed ERC input
  [5.6] Don't output non-modules in erc-modes
  [5.6] Revive erc-display-command as module command-indicator

 etc/ERC-NEWS               |  15 ++++
 lisp/erc/erc-backend.el    |  16 ++--
 lisp/erc/erc-goodies.el    | 113 +++++++++++++++++++++--
 lisp/erc/erc-match.el      |  14 +--
 lisp/erc/erc.el            | 177 +++++++++++++++++++++----------------
 test/lisp/erc/erc-tests.el |   5 +-
 6 files changed, 238 insertions(+), 102 deletions(-)

Interdiff:
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index f59023eae62..49b623aa7f0 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -157,6 +157,21 @@ asking users who've customized this option to switch to
 that some other solution, like automatic migration, is justified,
 please make that known on the bug list.
 
+** Module 'noncommands' deprecated, replaced by 'command-indicator'.
+Command-line echoing has returned to ERC after a near-decade-long
+hiatus.  This means you can elect to have ERC leave a trail of most
+slash-command input submitted at the prompt, in a manner resembling a
+shell or a REPL.  The particulars are likely of little interest to
+most users, but the gist is that this functionality was removed in
+5.3.x (Emacs 24.5) under mysterious circumstances, without mention in
+this document or any change log.  Everything's mostly been restored,
+except that the feature is now opt-in.  (Add 'command-indicator' to
+'erc-modules' to get started.)  The only real gotcha is that related
+faces and options, like 'erc-command-indicator', have moved to the
+'erc-goodies' library, although their Custom groups remain the same.
+Hint: once enabled, use 'erc-command-indicator-toggle-hidden' to
+dynamically toggle echoed command-line visibility.
+
 ** 'erc-button-alist' and 'erc-nick-popup-alist' have evolved slightly.
 It's no secret that the 'buttons' module treats potential nicknames
 specially.  This is perhaps most evident in its treatment of the
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 4cc81dd9378..93b76c544aa 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -471,21 +471,26 @@ erc-noncommands-list
                                erc-cmd-COUNTRY
                                erc-cmd-SV
                                erc-cmd-SM
-                               erc-cmd-SMV
+                               erc-cmd-SAY
                                erc-cmd-LASTLOG)
-  "List of commands that are aliases for CTCP ACTION or for ERC messages.
-
-If a command's function symbol is in this list, the typed command
-does not appear in the ERC buffer after the user presses ENTER.")
+  "List of commands that perform their own buffer I/O.
+The `command-indicator' module forgoes echoing commands in this
+list.")
 
 ;;;###autoload(autoload 'erc-noncommands-mode "erc-goodies" nil t)
 (define-erc-module noncommands nil
-  "This mode distinguishes non-commands.
-Commands listed in `erc-insert-this' know how to display
-themselves."
+  "Treat commands that display themselves specially.
+This module has been a no-op since ERC 5.3 and has likely only
+ever made sense in the context of `erc-command-indicator'.  It
+was deprecated in ERC 5.6."
   ((add-hook 'erc--input-review-functions #'erc-send-distinguish-noncommands))
   ((remove-hook 'erc--input-review-functions
                 #'erc-send-distinguish-noncommands)))
+(make-obsolete-variable 'erc-noncommand-mode
+                        'erc-command-indicator-mode "30.1")
+(make-obsolete 'erc-noncommand-mode 'erc-command-indicator-mode "30.1")
+(make-obsolete 'erc-noncommand-enable 'erc-command-indicator-enable "30.1")
+(make-obsolete 'erc-noncommand-disable 'erc-command-indicator-disable "30.1")
 
 (defun erc-send-distinguish-noncommands (state)
   "If STR is an ERC non-command, set `insertp' in STATE to nil."
@@ -499,6 +504,98 @@ erc-send-distinguish-noncommands
       ;; Inhibit sending this string.
       (setf (erc-input-insertp state) nil))))
 
+
+;;; Command-indicator
+
+(defface erc-command-indicator-face
+  '((t :inherit (shadow fixed-pitch-serif)))
+  "Face for echoed command lines, including the prompt.
+See option `erc-command-indicator'."
+  :package-version '(ERC . "5.6")
+  :group 'erc-faces)
+
+(defcustom erc-command-indicator "$>"
+  "Pseudo prompt for echoed command lines.
+An analog of the option `erc-prompt' that replaces the \"speaker
+label\" for echoed \"slash\" commands submitted at the prompt.  A
+value of nil means ERC only inserts the command line portion,
+sans pseudo prompt, which may trick certain modules into treating
+the slash command itself as the message's \"speaker\"."
+  :package-version '(ERC . "5.6")
+  :group 'erc-display
+  :type '(choice (const "$>") (const nil) string function))
+
+;;;###autoload(autoload 'erc-command-indicator-mode "erc-goodies" nil t)
+(define-erc-module command-indicator nil
+  "Echo command lines for \"slash commands,\" like /JOIN, /HELP, etc.
+Skip those appearing in `erc-noncommands-list'.
+
+Users can run \\[erc-command-indicator-toggle-hidden] to hide and
+reveal echoed command lines after they've been inserted."
+  ((add-hook 'erc--input-review-functions
+             #'erc--command-indicator-permit-insertion 90 t)
+   (erc-command-indicator-toggle-hidden -1))
+  ((remove-hook 'erc--input-review-functions
+                #'erc--command-indicator-permit-insertion t)
+   (erc-command-indicator-toggle-hidden +1))
+  'local)
+
+(defun erc-command-indicator ()
+  "Return the command-indicator prompt as a string.
+Do nothing if the variable `erc-command-indicator' is nil."
+  (and erc-command-indicator
+       (let ((prompt (if (functionp erc-command-indicator)
+                         (funcall erc-command-indicator)
+                       erc-command-indicator)))
+         (concat prompt (and (not (string-empty-p prompt)) " ")))))
+
+(defun erc-command-indicator-toggle-hidden (arg)
+  "Toggle whether echoed \"slash commands\" are visible."
+  (interactive "P")
+  (erc--toggle-hidden 'command-indicator arg))
+
+(defun erc--command-indicator-permit-insertion (state)
+  "Insert `erc-input' STATE's message if it's an echoed command."
+  (cl-assert erc-command-indicator-mode)
+  (when (erc--input-split-cmdp state)
+    (setf (erc--input-split-insertp state) #'erc--command-indicator-display)
+    (erc-send-distinguish-noncommands state)))
+
+;; That function used to be called `erc-display-command'.  It was
+;; neutered in ERC 5.3.x (Emacs 24.5), commented out in 5.4, removed
+;; in 5.5, and restored in 5.6.
+(defun erc--command-indicator-display (line)
+  "Insert command LINE as echoed input resembling that of REPLs and shells."
+  (when erc-insert-this
+    (save-excursion
+      (erc--assert-input-bounds)
+      (let ((insert-position (marker-position (goto-char erc-insert-marker)))
+            (erc--msg-props (or erc--msg-props
+                                (let ((ovs erc--msg-prop-overrides))
+                                  (map-into `((erc-msg . slash-cmd)
+                                              ,@(reverse ovs))
+                                            'hash-table)))))
+        (when-let ((string (erc-command-indicator))
+                   (erc-input-marker (copy-marker erc-input-marker)))
+          (erc-display-prompt nil nil string 'erc-command-indicator-face)
+          (set-marker erc-input-marker nil))
+        (let ((beg (point)))
+          (insert line)
+          (erc-put-text-property beg (point)
+                                 'font-lock-face 'erc-command-indicator-face)
+          (insert "\n"))
+        (save-restriction
+          (narrow-to-region insert-position (point))
+          (run-hooks 'erc-send-modify-hook)
+          (run-hooks 'erc-send-post-hook)
+          (cl-assert (> (- (point-max) (point-min)) 1))
+          (erc--hide-message 'command-indicator)
+          (add-text-properties (point-min) (1+ (point-min))
+                               (erc--order-text-properties-from-hash
+                                erc--msg-props))))
+      (erc--refresh-prompt))))
+
+
 ;;; IRC control character processing.
 (defgroup erc-control-characters nil
   "Dealing with control characters."
diff --git a/lisp/erc/erc-match.el b/lisp/erc/erc-match.el
index 8644e61106f..6fff54d3cf4 100644
--- a/lisp/erc/erc-match.el
+++ b/lisp/erc/erc-match.el
@@ -695,19 +695,7 @@ erc-match-toggle-hidden-fools
 Expect the function `erc-hide-fools' or similar to be present in
 `erc-text-matched-hook'."
   (interactive "P")
-  (erc-match--toggle-hidden 'match-fools arg))
-
-(defun erc-match--toggle-hidden (prop arg)
-  "Toggle invisibility for spec member PROP.
-Treat ARG in a manner similar to mode toggles defined by
-`define-minor-mode'."
-  (when arg
-    (setq arg (prefix-numeric-value arg)))
-  (if (memq prop (ensure-list buffer-invisibility-spec))
-      (unless (natnump arg)
-        (remove-from-invisibility-spec prop))
-    (when (or (not arg) (natnump arg))
-      (add-to-invisibility-spec prop))))
+  (erc--toggle-hidden 'match-fools arg))
 
 (provide 'erc-match)
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 00ee97b7e14..89b3397d8f1 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -346,8 +346,13 @@ erc-hide-prompt
   "If non-nil, hide input prompt upon disconnecting.
 To unhide, type something in the input area.  Once revealed, a
 prompt remains unhidden until the next disconnection.  Channel
-prompts are unhidden upon rejoining.  See
-`erc-unhide-query-prompt' for behavior concerning query prompts."
+prompts are unhidden upon rejoining.  For behavior concerning
+query prompts, see `erc-unhide-query-prompt'.  Longtime ERC users
+should note that this option was repurposed in ERC 5.5 because it
+had lain dormant for years after being sidelined in 5.3 when its
+only use in the interactive client was removed.  Before then, its
+role was controlling whether `erc-command-indicator' would appear
+alongside echoed slash-command lines."
   :package-version '(ERC . "5.5")
   :group 'erc-display
   :type '(choice (const :tag "Always hide prompt" t)
@@ -759,28 +764,6 @@ erc-prompt
         (concat prompt " ")
       prompt)))
 
-(defcustom erc-command-indicator nil
-  "Indicator used by ERC for showing commands.
-
-If non-nil, this will be used in the ERC buffer to indicate
-commands (i.e., input starting with a `/').
-
-If nil, the prompt will be constructed from the variable `erc-prompt'."
-  :group 'erc-display
-  :type '(choice (const nil) string function))
-
-(defun erc-command-indicator ()
-  "Return the command indicator prompt as a string.
-
-This only has any meaning if the variable `erc-command-indicator' is non-nil."
-  (and erc-command-indicator
-       (let ((prompt (if (functionp erc-command-indicator)
-                         (funcall erc-command-indicator)
-                       erc-command-indicator)))
-         (if (> (length prompt) 0)
-             (concat prompt " ")
-           prompt))))
-
 (defcustom erc-notice-prefix "*** "
   "Prefix for all notices."
   :group 'erc-display
@@ -1364,12 +1347,6 @@ erc-prompt-face
   "ERC face for the prompt."
   :group 'erc-faces)
 
-(defface erc-command-indicator-face
-  '((t :weight bold))
-  "ERC face for the command indicator.
-See the variable `erc-command-indicator'."
-  :group 'erc-faces)
-
 (defface erc-notice-face
   '((default :weight bold)
     (((class color) (min-colors 88) (supports :weight semi-bold))
@@ -2077,7 +2054,7 @@ erc--sort-modules
 
 (defcustom erc-modules '( autojoin button completion fill imenu irccontrols
                           list match menu move-to-prompt netsplit
-                          networks noncommands readonly ring stamp track)
+                          networks readonly ring stamp track)
   "A list of modules which ERC should enable.
 If you set the value of this without using `customize' remember to call
 \(erc-update-modules) after you change it.  When using `customize', modules
@@ -2127,6 +2104,7 @@ erc-modules
     (const :tag "button: Buttonize URLs, nicknames, and other text" button)
     (const :tag "capab: Mark unidentified users on servers supporting CAPAB"
            capab-identify)
+    (const :tag "command-indicator: Echo command lines." command-indicator)
     (const :tag "completion: Complete nicknames and commands (programmable)"
            completion)
     (const :tag "dcc: Provide Direct Client-to-Client support" dcc)
@@ -2146,7 +2124,7 @@ erc-modules
     (const :tag "networks: Provide data about IRC networks" networks)
     (const :tag "nickbar: Show nicknames in a dyamic side window" nickbar)
     (const :tag "nicks: Uniquely colorize nicknames in target buffers" nicks)
-    (const :tag "noncommands: Don't display non-IRC commands after evaluation"
+    (const :tag "noncommands: Deprecated. See module `command-indicator'."
            noncommands)
     (const :tag "notifications: Desktop alerts on PRIVMSG or mentions"
            notifications)
@@ -3309,6 +3287,18 @@ erc--hide-message
           (cl-incf beg))
         (erc--merge-prop (1- beg) (1- end) 'invisible value)))))
 
+(defun erc--toggle-hidden (prop arg)
+  "Toggle invisibility for spec member PROP.
+Treat ARG in a manner similar to mode toggles defined by
+`define-minor-mode'."
+  (when arg
+    (setq arg (prefix-numeric-value arg)))
+  (if (memq prop (ensure-list buffer-invisibility-spec))
+      (unless (natnump arg)
+        (remove-from-invisibility-spec prop))
+    (when (or (not arg) (natnump arg))
+      (add-to-invisibility-spec prop))))
+
 (defun erc--delete-inserted-message (beg-or-point &optional end)
   "Remove message between BEG and END.
 Expect BEG and END to match bounds as returned by the macro
@@ -7016,7 +7006,9 @@ erc--send-input-lines
   (when (erc--input-split-sendp lines-obj)
     (dolist (line (erc--input-split-lines lines-obj))
       (when (erc--input-split-insertp lines-obj)
-        (erc-display-msg line))
+        (if (functionp (erc--input-split-insertp lines-obj))
+            (funcall (erc--input-split-insertp lines-obj) line)
+          (erc-display-msg line)))
       (erc-process-input-line (concat line "\n")
                               (null erc-flood-protect)
                               (not (erc--input-split-cmdp lines-obj))))))
@@ -7522,7 +7514,10 @@ erc-load-irc-script-lines
 user input."
   (let* ((cb (current-buffer))
          (s "")
-         (sp (or (erc-command-indicator) (erc-prompt)))
+         (sp (or (and (bound-and-true-p erc-command-indicator-mode)
+                      (fboundp 'erc-command-indicator)
+                      (erc-command-indicator))
+                 (erc-prompt)))
          (args (and (boundp 'erc-script-args) erc-script-args)))
     (if (and args (string-match "^ " args))
         (setq args (substring args 1)))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 2898ca7be75..e7422d330c0 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2543,7 +2543,8 @@ erc-handle-irc-url
     (kill-buffer "#chan")))
 
 (defconst erc-tests--modules
-  '( autoaway autojoin bufbar button capab-identify completion dcc fill identd
+  '( autoaway autojoin bufbar button capab-identify
+     command-indicator completion dcc fill identd
      imenu irccontrols keep-place list log match menu move-to-prompt netsplit
      networks nickbar nicks noncommands notifications notify page readonly
      replace ring sasl scrolltobottom services smiley sound
-- 
2.41.0

>From 7e3b7c789f0c8b8d658b2571d09b124edc94edbd Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Wed, 8 Nov 2023 19:14:55 -0800
Subject: [PATCH 1/6] [5.6] Make ERC error-notice formatting more consistent

* lisp/erc/erc-backend.el (erc--server-propagate-failed-connection):
Print leading notice prefix when reporting process exit status, and
set `erc-msg' type to `notice-error'.
(erc-schedule-reconnect): Print leading notice prefix when printing
`reconnecting' message.
* lisp/erc/erc.el (erc-process-input-line, erc-cmd-PART,
erc-cmd-TOPIC): Print `no-target' messages as error notices.
(erc-message-english-disconnected,
erc-message-english-disconnected-noreconnect): Hard-code standard
"***" notice prefix into message text, for consistency.
* test/lisp/erc/erc-tests.el (erc--refresh-prompt): Expect notice
prefix before `no-target' message.
---
 lisp/erc/erc-backend.el    |  4 ++--
 lisp/erc/erc.el            | 11 ++++++-----
 test/lisp/erc/erc-tests.el |  2 +-
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 4b5edaa77d2..32d891cd1c6 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -728,7 +728,7 @@ erc--server-propagate-failed-connection
 the server buffer in any case."
   (when (eq (process-status process) 'failed)
     (erc-display-message
-     nil 'error (process-buffer process)
+     nil '(notice error) (process-buffer process)
      (format "Process exit status: %S" (process-exit-status process)))
     (pcase (process-exit-status process)
       (111
@@ -995,7 +995,7 @@ erc-schedule-reconnect
                     (- erc-server-reconnect-attempts
                        (cl-incf erc-server-reconnect-count (or incr 1)))))
         (proc (buffer-local-value 'erc-server-process buffer)))
-    (erc-display-message nil 'error buffer 'reconnecting
+    (erc-display-message nil '(notice error) buffer 'reconnecting
                          ?m erc-server-reconnect-timeout
                          ?i (if count erc-server-reconnect-count "N")
                          ?n (if count erc-server-reconnect-attempts "A"))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index fd57cb9d6a0..2d8f388328d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3729,7 +3729,7 @@ erc-process-input-line
       (let ((r (erc-default-target)))
         (if r
             (funcall erc-send-input-line-function r line force)
-          (erc-display-message nil 'error (current-buffer) 'no-target)
+          (erc-display-message nil '(notice error) (current-buffer) 'no-target)
           nil)))))
 
 (defconst erc--shell-parse-regexp
@@ -4581,7 +4581,7 @@ erc-cmd-PART
                                  (format "PART %s" ch)
                                (format "PART %s :%s" ch reason))
                              nil ch))
-        (erc-display-message nil 'error (current-buffer) 'no-target)))
+        (erc-display-message nil '(notice error) (current-buffer) 'no-target)))
     t)
    (t nil)))
 (put 'erc-cmd-PART 'do-not-parse-args t)
@@ -4921,7 +4921,7 @@ erc-cmd-TOPIC
           (progn
             (erc-log (format "cmd: TOPIC [%s]: %s" ch topic))
             (erc-server-send (format "TOPIC %s :%s" ch topic) nil ch))
-        (erc-display-message nil 'error (current-buffer) 'no-target)))
+        (erc-display-message nil '(notice error) (current-buffer) 'no-target)))
     t)
    (t nil)))
 (defalias 'erc-cmd-T #'erc-cmd-TOPIC)
@@ -8194,9 +8194,10 @@ erc-define-catalog
    (flood-ctcp-off . "FLOOD PROTECTION: Automatic CTCP responses turned off.")
    (flood-strict-mode
     . "FLOOD PROTECTION: Switched to Strict Flood Control mode.")
-   (disconnected . "\n\nConnection failed!  Re-establishing connection...\n")
+   (disconnected
+    . "\n\n*** Connection failed!  Re-establishing connection...\n")
    (disconnected-noreconnect
-    . "\n\nConnection failed!  Not re-establishing connection.\n")
+    . "\n\n*** Connection failed!  Not re-establishing connection.\n")
    (reconnecting . "Reconnecting in %ms: attempt %i/%n ...")
    (reconnect-canceled . "Canceled %u reconnect timer with %cs to go...")
    (finished . "\n\n*** ERC finished ***\n")
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 916b394c8ff..2898ca7be75 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -317,7 +317,7 @@ erc--refresh-prompt
         (insert "Howdy")
         (erc-send-current-line)
         (save-excursion (forward-line -1)
-                        (should (looking-at "No target"))
+                        (should (looking-at (rx "*** No target")))
                         (forward-line -1)
                         (should (looking-at "<tester> Howdy")))
         (should (looking-back "ServNet 6> "))
-- 
2.41.0

>From a3bd16a010472ca6b4ce54cb5230f246e84c685d Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Fri, 15 Jul 2022 05:02:35 -0700
Subject: [PATCH 2/6] [5.6] Make ERC's message-sending functions more flexible

* lisp/erc/erc-backend.el (erc--server-send, erc-server-send): Convert
the latter to a wrapper that calls the former, a "new" internal
generic function.  Extend `no-penalty' parameter to mean ERC will
schedule an imminent send via a timer.  Always run
`erc-server-send-queue' on a timer.
* lisp/erc/erc.el (erc-send-action, erc--send-action): Make former a
wrapper for latter, a new generic function.  The rationale is that
built-in modules should be able to elect to handle insertion
themselves.  Also fix bug involving /ME ACTIONs not having
`erc-my-nick-face' applied to the speaker.
(erc-send-message, erc--send-message): Make the former a wrapper for
the latter, a new generic function.  Since commands, like /SV, use
this function to handle their own insertion, modules can't leverage
the normal API to suppress insertion.
(erc--send-input-lines): Convert to generic function.
---
 lisp/erc/erc-backend.el | 12 +++++++++---
 lisp/erc/erc.el         | 11 +++++++++--
 2 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 32d891cd1c6..b16824b1f73 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1171,7 +1171,7 @@ erc-server-send
 When FORCE is non-nil, bypass flood protection so that STRING is
 sent directly without modifying the queue.  When FORCE is the
 symbol `no-penalty', exempt this round from accumulating a
-timeout penalty.
+timeout penalty and schedule it to run ASAP instead of blocking.
 
 If TARGET is specified, look up encoding information for that
 channel in `erc-encoding-coding-alist' or
@@ -1179,6 +1179,9 @@ erc-server-send
 
 See `erc-server-flood-margin' for an explanation of the flood
 protection algorithm."
+  (erc--server-send string force target))
+
+(cl-defmethod erc--server-send (string force target)
   (erc-log (concat "erc-server-send: " string "(" (buffer-name) ")"))
   (setq erc-server-last-sent-time (erc-current-time))
   (let ((encoding (erc-coding-system-for-target target)))
@@ -1199,14 +1202,17 @@ erc-server-send
                         (when (fboundp 'set-process-coding-system)
                           (set-process-coding-system erc-server-process
                                                      'raw-text encoding))
-                        (process-send-string erc-server-process str))
+                        (if (and (eq force 'no-penalty))
+                            (run-at-time nil nil #'process-send-string
+                                         erc-server-process str)
+                          (process-send-string erc-server-process str)))
                     ;; See `erc-server-send-queue' for full
                     ;; explanation of why we need this condition-case
                     (error nil)))
               (setq erc-server-flood-queue
                     (append erc-server-flood-queue
                             (list (cons str encoding))))
-              (erc-server-send-queue (current-buffer))))
+              (run-at-time nil nil #'erc-server-send-queue (current-buffer))))
           t)
       (message "ERC: No process running")
       nil)))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 2d8f388328d..a885038411d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2950,13 +2950,17 @@ erc-toggle-debug-irc-protocol
 
 (defun erc-send-action (tgt str &optional force)
   "Send CTCP ACTION information described by STR to TGT."
+  (erc--send-action tgt str force))
+
+(cl-defmethod erc--send-action (tgt str &optional force)
   (erc-send-ctcp-message tgt (format "ACTION %s" str) force)
   ;; Allow hooks that act on inserted PRIVMSG and NOTICES to process us.
   (let ((erc--msg-prop-overrides `((erc-msg . msg)
                                    (erc-ctcp . ACTION)
                                    ,@erc--msg-prop-overrides))
         (nick (erc-current-nick)))
-    (setq nick (propertize nick 'erc-speaker nick))
+    (setq nick (propertize nick 'erc-speaker nick
+                           'font-lock-face 'erc-my-nick-face))
     (erc-display-message nil '(t action input) (current-buffer)
                          'ACTION ?n nick ?a str ?u "" ?h "")))
 
@@ -4493,6 +4497,9 @@ erc-send-message
   "Send LINE to the current channel or user and display it.
 
 See also `erc-message' and `erc-display-line'."
+  (erc--send-message line force))
+
+(cl-defgeneric erc--send-message (line force)
   (erc-message "PRIVMSG" (concat (erc-default-target) " " line) force)
   (erc-display-line
    (concat (erc-format-my-nick) line)
@@ -6978,7 +6985,7 @@ erc--run-send-hooks
     (user-error "Multiline command detected" ))
   lines-obj)
 
-(defun erc--send-input-lines (lines-obj)
+(cl-defmethod erc--send-input-lines (lines-obj)
   "Send lines in `erc--input-split-lines' object LINES-OBJ."
   (when (erc--input-split-sendp lines-obj)
     (dolist (line (erc--input-split-lines lines-obj))
-- 
2.41.0

>From 65bc05e03a022ed58774226bf3d3dc9f35525820 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Fri, 9 Jun 2023 21:00:03 -0700
Subject: [PATCH 3/6] [5.6] Simplify logic for inserting ERC prompt input

* lisp/erc/erc.el (erc--current-line-input-split): New variable bound
to the current `erc--input-split' object, post review, for the extent
of display processing, so that "slash" commands can access it.
(erc-cmd-SAY): Defer to `erc--send-message'.
(erc--send-message): New implementation.  When called internally and
the variable `erc--current-line-input-split' is non-nil, treat input
as an outgoing message to ensure "send" hooks run and invariants for
prompt markers are preserved.  For example, this means noncommands,
like /SV will have `erc-input-face' applied, which is correct because
this message is sent to an actual target on the server.
(erc--make-input-split): New function.
(erc-send-current-line): Bind `erc--current-line-input-split' when
dispatching prompt-input handlers.  Use helper
`erc--make-input-split'.
(erc--run-send-hooks): Honor existing `refoldp' slot from
`erc--input-split' object.
---
 lisp/erc/erc.el | 50 +++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 40 insertions(+), 10 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a885038411d..4828319727e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3659,6 +3659,12 @@ erc--called-as-input-p
   "Non-nil when a user types a \"/slash\" command.
 Remains bound until `erc-cmd-SLASH' returns.")
 
+(defvar erc--current-line-input-split nil
+  "Current `erc--input-split' instance when processing user input.
+This is for special cases in which a \"slash\" command needs
+details about the input it's handling or needs to detect whether
+it's been dispatched by `erc-send-current-line'.")
+
 (defvar-local erc-send-input-line-function #'erc-send-input-line
   "Function for sending lines lacking a leading \"slash\" command.
 When prompt input starts with a \"slash\" command, like \"/MSG\",
@@ -3795,9 +3801,7 @@ erc-cmd-SAY
   (if (string-match "^\\s-*$" line)
       nil
     (string-match "^ ?\\(.*\\)" line)
-    (let ((msg (match-string 1 line)))
-      (erc-display-msg msg)
-      (erc-process-input-line msg nil t))))
+    (erc--send-message (match-string 1 line) nil)))
 (put 'erc-cmd-SAY 'do-not-parse-args t)
 
 (defun erc-cmd-SET (line)
@@ -4507,6 +4511,27 @@ erc--send-message
   ;; FIXME - treat multiline, run hooks, or remove me?
   t)
 
+;; When running inside insert and send hooks, take more care in
+;; dealing with the prompt.
+(cl-defmethod erc--send-message
+  (input-line force &context (erc--current-line-input-split erc--input-split))
+  "Process non-command INPUT-LINE almost as if it's normal chat input.
+Expect INPUT-LINE to differ from the `string' slot of the calling
+context's `erc--current-line-input-split' object because the
+latter is likely a slash command invocation whose handler
+generated INPUT-LINE.  Before inserting INPUT-LINE, split it and
+run `erc-send-modify-hook' and `erc-send-post-hook' on each
+actual outgoing line.  Forgo input validation because this isn't
+interactive input, and skip `erc-send-completed-hook' because it
+will run just before the outer `erc-send-current-line' call
+returns."
+  (let* ((erc-flood-protect (not force))
+         (lines-obj (erc--make-input-split input-line)))
+    (setf (erc--input-split-refoldp lines-obj) t
+          (erc--input-split-cmdp lines-obj) nil)
+    (erc--send-input-lines (erc--run-send-hooks lines-obj)))
+  t)
+
 (defun erc-cmd-MODE (line)
   "Change or display the mode value of a channel or user.
 The first word specifies the target.  The rest is the mode string
@@ -6880,6 +6905,14 @@ erc--inhibit-slash-cmd-insertion
   (when (erc--input-split-cmdp state)
     (setf (erc--input-split-insertp state) nil)))
 
+(defun erc--make-input-split (string)
+  (make-erc--input-split
+   :string string
+   :insertp erc-insert-this
+   :sendp erc-send-this
+   :lines (split-string string erc--input-line-delim-regexp)
+   :cmdp (string-match erc-command-regexp string)))
+
 (defun erc-send-current-line ()
   "Parse current line and send it to IRC."
   (interactive)
@@ -6894,16 +6927,11 @@ erc-send-current-line
             (expand-abbrev))
           (widen)
           (let* ((str (erc-user-input))
-                 (state (make-erc--input-split
-                         :string str
-                         :insertp erc-insert-this
-                         :sendp erc-send-this
-                         :lines (split-string
-                                 str erc--input-line-delim-regexp)
-                         :cmdp (string-match erc-command-regexp str))))
+                 (state (erc--make-input-split str)))
             (run-hook-with-args 'erc--input-review-functions state)
             (when-let (((not (erc--input-split-abortp state)))
                        (inhibit-read-only t)
+                       (erc--current-line-input-split state)
                        (old-buf (current-buffer)))
               (let ((erc--msg-prop-overrides `((erc-msg . msg)
                                                ,@erc--msg-prop-overrides)))
@@ -6969,6 +6997,8 @@ erc--run-send-hooks
                       (run-hook-with-args 'erc-send-pre-hook str)
                       (make-erc-input :string str
                                       :insertp erc-insert-this
+                                      :refoldp (erc--input-split-refoldp
+                                                lines-obj)
                                       :sendp erc-send-this))))
         (run-hook-with-args 'erc-pre-send-functions state)
         (setf (erc--input-split-sendp lines-obj) (erc-input-sendp state)
-- 
2.41.0

>From 2aaad91082113724ccd8ad6f9b5731815ad621f5 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Wed, 8 Nov 2023 21:20:54 -0800
Subject: [PATCH 4/6] [5.6] Slightly simplify text props on echoed ERC input

* lisp/erc/erc.el (erc--msg-props): Reformat doc to improve
readability.
(erc-send-current-line): Don't bind `erc--msg-prop-overrides' because
it affects all calls to `erc-display-message' made by slash commands,
etc.
(erc-display-msg): Set `erc-msg' property to `msg' instead of `self'
because the only legitimate use of this function is for inserting
"echoed" prompt input.  (Bug#60936)
---
 lisp/erc/erc.el | 39 +++++++++++++++++----------------------
 1 file changed, 17 insertions(+), 22 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 4828319727e..be8e2d19b7f 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -155,31 +155,28 @@ erc--msg-props
 as of ERC 5.6:
 
  - `erc-msg': a symbol, guaranteed present; values include:
-
-   - `msg', signifying a `PRIVMSG' or an incoming `NOTICE'
-   - `self', a fallback used by `erc-display-msg' for callers
-     that don't specify an `erc-msg'
-   - `unknown', a similar fallback for `erc-display-message'
-   - a catalog key, such as `s401' or `finished'
-   - an `erc-display-message' TYPE parameter, like `notice'
+   `msg', signifying a `PRIVMSG' or an incoming `NOTICE';
+   `unknown', a fallback for `erc-display-message'; a catalog
+    key, such as `s401' or `finished'; an `erc-display-message'
+    TYPE parameter, like `notice'
 
  - `erc-cmd': a message's associated IRC command, as read by
    `erc--get-eq-comparable-cmd'; currently either a symbol, like
    `PRIVMSG', or a number, like 5, which represents the numeric
-   \"005\"; absent on \"local\" messages, such as simple warnings
-   and help text, and on outgoing messages unless echoed back by
-   the server (assuming future support)
+    \"005\"; absent on \"local\" messages, such as simple warnings
+    and help text, and on outgoing messages unless echoed back by
+    the server (assuming future support)
 
  - `erc-ctcp': a CTCP command, like `ACTION'
 
  - `erc-ts': a timestamp, possibly provided by the server; as of
-   5.6, a ticks/hertz pair on Emacs 29 and above, and a \"list\"
-   type otherwise; managed by the `stamp' module
+    5.6, a ticks/hertz pair on Emacs 29 and above, and a \"list\"
+    type otherwise; managed by the `stamp' module
 
  - `erc-ephemeral': a symbol prefixed by or matching a module
-   name; indicates to other modules and members of modification
-   hooks that the current message should not affect stateful
-   operations, such as recording a channel's most recent speaker
+    name; indicates to other modules and members of modification
+    hooks that the current message should not affect stateful
+    operations, such as recording a channel's most recent speaker
 
 This is an internal API, and the selection of related helper
 utilities is fluid and provisional.  As of ERC 5.6, see the
@@ -6933,8 +6930,7 @@ erc-send-current-line
                        (inhibit-read-only t)
                        (erc--current-line-input-split state)
                        (old-buf (current-buffer)))
-              (let ((erc--msg-prop-overrides `((erc-msg . msg)
-                                               ,@erc--msg-prop-overrides)))
+              (progn ; unprogn this during next major surgery
                 (erc-set-active-buffer (current-buffer))
                 ;; Kill the input and the prompt
                 (delete-region erc-input-marker (erc-end-of-input-line))
@@ -7079,16 +7075,15 @@ erc-send-input
 
 (defun erc-display-msg (line)
   "Insert LINE into current buffer and run \"send\" hooks.
-Expect LINE to originate from input submitted interactively at
-the prompt, such as outgoing chat messages or echoed slash
-commands."
+Treat LINE as input submitted interactively at the prompt, such
+as outgoing chat messages and echoed slash commands."
   (when erc-insert-this
     (save-excursion
       (erc--assert-input-bounds)
       (let ((insert-position (marker-position (goto-char erc-insert-marker)))
-            (erc--msg-props (or erc--msg-props ; prefer `self' to `unknown'
+            (erc--msg-props (or erc--msg-props
                                 (let ((ovs erc--msg-prop-overrides))
-                                  (map-into `((erc-msg . self) ,@(reverse ovs))
+                                  (map-into `((erc-msg . msg) ,@(reverse ovs))
                                             'hash-table))))
             beg)
         (insert (erc-format-my-nick))
-- 
2.41.0

>From e7f166024092087721269f7bdab9153fceec08ec Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 9 Nov 2023 10:37:49 -0800
Subject: [PATCH 5/6] [5.6] Don't output non-modules in erc-modes

* lisp/erc/erc.el (erc-modes): Only list actual modules created via
`define-erc-module.  Also `quote' members of the resulting list.
---
 lisp/erc/erc.el | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index be8e2d19b7f..00ee97b7e14 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -8135,8 +8135,9 @@ erc-modes
                     (let (modes (case-fold-search nil))
                       (dolist (var (apropos-internal "^erc-.*mode$"))
                         (when (and (boundp var)
+                                   (get var 'erc-module)
                                    (symbol-value var))
-                          (setq modes (cons (symbol-name var)
+                          (setq modes (cons (concat "`" (symbol-name var) "'")
                                             modes))))
                       modes)
                     ", ")))
-- 
2.41.0

>From 96b62219e43df0585ddfbabaa904342ca0385d22 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Fri, 10 Nov 2023 13:34:31 -0800
Subject: [PATCH 6/6] [5.6] Revive erc-display-command as module
 command-indicator

* lisp/erc/erc-goodies.el (erc-noncommands-list): Replace the
nonexistent `erc-cmd-SMV' with `erc-cmd-SAY'.
(erc-noncommands-mode, erc-noncommands-enable,
erc-noncommands-disable): Deprecate.
(erc-command-indicator-face, erc-command-indicator): Migrate from main
library.
(erc-command-indicator-mode, erc-command-indicator-enable,
erc-command-indicator-disable): New module to take the place of
`noncommands'.
(erc-command-indicator-toggle-hidden): New command to toggle echoed
command-line visibility.
(erc--command-indicator-permit-insertion): New function.
(erc--command-indicator-display): New function, a slightly revised
version of the old `erc-display-command' from the main library.  Its
only call site was removed back in d1036d288de "backport: erc
bugfixes".  However, references were left behind to associated assets,
like `erc-command-indicator', etc.  The function was later commented
out in 0c599ee2e2c "* lisp/erc/erc.el: Use `run-hook-with-args` for
`erc-pre-send-functions`", and then removed by a63ed6f78a6 "Remove
duplicate ERC prompt on reconnect".
* lisp/erc/erc-match.el (erc-match-toggle-hidden-fools): Use new
non-module-specific name for `erc-match--toggle-hidden'.
(erc-match--toggle-hidden): Move to main library for shared use by
other modules.
* lisp/erc/erc.el (erc-hide-prompt): Leave note explaining updated
role.
(erc-command-indicator): Move to erc-goodies.
(erc-command-indicator-face): Move to erc-goodies.
(erc-modules): Remove module `noncommands' from standard value.
(erc--toggle-hidden): "New" function, a rebranded version of
`erc-match--toggle-hidden'.
(erc--send-input-lines): Accommodate modules wanting alternate
insertion functions.
(erc-load-irc-script-lines): Account for `erc-command-indicator' no
longer being defined in this library.
* test/lisp/erc/erc-tests.el (erc-tests--modules): Remove
deprecated module `noncommands' from manifest.  (Bug#67031)
---
 etc/ERC-NEWS               |  15 +++++
 lisp/erc/erc-goodies.el    | 113 ++++++++++++++++++++++++++++++++++---
 lisp/erc/erc-match.el      |  14 +----
 lisp/erc/erc.el            |  63 ++++++++++-----------
 test/lisp/erc/erc-tests.el |   3 +-
 5 files changed, 152 insertions(+), 56 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index f59023eae62..49b623aa7f0 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -157,6 +157,21 @@ asking users who've customized this option to switch to
 that some other solution, like automatic migration, is justified,
 please make that known on the bug list.
 
+** Module 'noncommands' deprecated, replaced by 'command-indicator'.
+Command-line echoing has returned to ERC after a near-decade-long
+hiatus.  This means you can elect to have ERC leave a trail of most
+slash-command input submitted at the prompt, in a manner resembling a
+shell or a REPL.  The particulars are likely of little interest to
+most users, but the gist is that this functionality was removed in
+5.3.x (Emacs 24.5) under mysterious circumstances, without mention in
+this document or any change log.  Everything's mostly been restored,
+except that the feature is now opt-in.  (Add 'command-indicator' to
+'erc-modules' to get started.)  The only real gotcha is that related
+faces and options, like 'erc-command-indicator', have moved to the
+'erc-goodies' library, although their Custom groups remain the same.
+Hint: once enabled, use 'erc-command-indicator-toggle-hidden' to
+dynamically toggle echoed command-line visibility.
+
 ** 'erc-button-alist' and 'erc-nick-popup-alist' have evolved slightly.
 It's no secret that the 'buttons' module treats potential nicknames
 specially.  This is perhaps most evident in its treatment of the
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 4cc81dd9378..93b76c544aa 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -471,21 +471,26 @@ erc-noncommands-list
                                erc-cmd-COUNTRY
                                erc-cmd-SV
                                erc-cmd-SM
-                               erc-cmd-SMV
+                               erc-cmd-SAY
                                erc-cmd-LASTLOG)
-  "List of commands that are aliases for CTCP ACTION or for ERC messages.
-
-If a command's function symbol is in this list, the typed command
-does not appear in the ERC buffer after the user presses ENTER.")
+  "List of commands that perform their own buffer I/O.
+The `command-indicator' module forgoes echoing commands in this
+list.")
 
 ;;;###autoload(autoload 'erc-noncommands-mode "erc-goodies" nil t)
 (define-erc-module noncommands nil
-  "This mode distinguishes non-commands.
-Commands listed in `erc-insert-this' know how to display
-themselves."
+  "Treat commands that display themselves specially.
+This module has been a no-op since ERC 5.3 and has likely only
+ever made sense in the context of `erc-command-indicator'.  It
+was deprecated in ERC 5.6."
   ((add-hook 'erc--input-review-functions #'erc-send-distinguish-noncommands))
   ((remove-hook 'erc--input-review-functions
                 #'erc-send-distinguish-noncommands)))
+(make-obsolete-variable 'erc-noncommand-mode
+                        'erc-command-indicator-mode "30.1")
+(make-obsolete 'erc-noncommand-mode 'erc-command-indicator-mode "30.1")
+(make-obsolete 'erc-noncommand-enable 'erc-command-indicator-enable "30.1")
+(make-obsolete 'erc-noncommand-disable 'erc-command-indicator-disable "30.1")
 
 (defun erc-send-distinguish-noncommands (state)
   "If STR is an ERC non-command, set `insertp' in STATE to nil."
@@ -499,6 +504,98 @@ erc-send-distinguish-noncommands
       ;; Inhibit sending this string.
       (setf (erc-input-insertp state) nil))))
 
+
+;;; Command-indicator
+
+(defface erc-command-indicator-face
+  '((t :inherit (shadow fixed-pitch-serif)))
+  "Face for echoed command lines, including the prompt.
+See option `erc-command-indicator'."
+  :package-version '(ERC . "5.6")
+  :group 'erc-faces)
+
+(defcustom erc-command-indicator "$>"
+  "Pseudo prompt for echoed command lines.
+An analog of the option `erc-prompt' that replaces the \"speaker
+label\" for echoed \"slash\" commands submitted at the prompt.  A
+value of nil means ERC only inserts the command line portion,
+sans pseudo prompt, which may trick certain modules into treating
+the slash command itself as the message's \"speaker\"."
+  :package-version '(ERC . "5.6")
+  :group 'erc-display
+  :type '(choice (const "$>") (const nil) string function))
+
+;;;###autoload(autoload 'erc-command-indicator-mode "erc-goodies" nil t)
+(define-erc-module command-indicator nil
+  "Echo command lines for \"slash commands,\" like /JOIN, /HELP, etc.
+Skip those appearing in `erc-noncommands-list'.
+
+Users can run \\[erc-command-indicator-toggle-hidden] to hide and
+reveal echoed command lines after they've been inserted."
+  ((add-hook 'erc--input-review-functions
+             #'erc--command-indicator-permit-insertion 90 t)
+   (erc-command-indicator-toggle-hidden -1))
+  ((remove-hook 'erc--input-review-functions
+                #'erc--command-indicator-permit-insertion t)
+   (erc-command-indicator-toggle-hidden +1))
+  'local)
+
+(defun erc-command-indicator ()
+  "Return the command-indicator prompt as a string.
+Do nothing if the variable `erc-command-indicator' is nil."
+  (and erc-command-indicator
+       (let ((prompt (if (functionp erc-command-indicator)
+                         (funcall erc-command-indicator)
+                       erc-command-indicator)))
+         (concat prompt (and (not (string-empty-p prompt)) " ")))))
+
+(defun erc-command-indicator-toggle-hidden (arg)
+  "Toggle whether echoed \"slash commands\" are visible."
+  (interactive "P")
+  (erc--toggle-hidden 'command-indicator arg))
+
+(defun erc--command-indicator-permit-insertion (state)
+  "Insert `erc-input' STATE's message if it's an echoed command."
+  (cl-assert erc-command-indicator-mode)
+  (when (erc--input-split-cmdp state)
+    (setf (erc--input-split-insertp state) #'erc--command-indicator-display)
+    (erc-send-distinguish-noncommands state)))
+
+;; That function used to be called `erc-display-command'.  It was
+;; neutered in ERC 5.3.x (Emacs 24.5), commented out in 5.4, removed
+;; in 5.5, and restored in 5.6.
+(defun erc--command-indicator-display (line)
+  "Insert command LINE as echoed input resembling that of REPLs and shells."
+  (when erc-insert-this
+    (save-excursion
+      (erc--assert-input-bounds)
+      (let ((insert-position (marker-position (goto-char erc-insert-marker)))
+            (erc--msg-props (or erc--msg-props
+                                (let ((ovs erc--msg-prop-overrides))
+                                  (map-into `((erc-msg . slash-cmd)
+                                              ,@(reverse ovs))
+                                            'hash-table)))))
+        (when-let ((string (erc-command-indicator))
+                   (erc-input-marker (copy-marker erc-input-marker)))
+          (erc-display-prompt nil nil string 'erc-command-indicator-face)
+          (set-marker erc-input-marker nil))
+        (let ((beg (point)))
+          (insert line)
+          (erc-put-text-property beg (point)
+                                 'font-lock-face 'erc-command-indicator-face)
+          (insert "\n"))
+        (save-restriction
+          (narrow-to-region insert-position (point))
+          (run-hooks 'erc-send-modify-hook)
+          (run-hooks 'erc-send-post-hook)
+          (cl-assert (> (- (point-max) (point-min)) 1))
+          (erc--hide-message 'command-indicator)
+          (add-text-properties (point-min) (1+ (point-min))
+                               (erc--order-text-properties-from-hash
+                                erc--msg-props))))
+      (erc--refresh-prompt))))
+
+
 ;;; IRC control character processing.
 (defgroup erc-control-characters nil
   "Dealing with control characters."
diff --git a/lisp/erc/erc-match.el b/lisp/erc/erc-match.el
index 8644e61106f..6fff54d3cf4 100644
--- a/lisp/erc/erc-match.el
+++ b/lisp/erc/erc-match.el
@@ -695,19 +695,7 @@ erc-match-toggle-hidden-fools
 Expect the function `erc-hide-fools' or similar to be present in
 `erc-text-matched-hook'."
   (interactive "P")
-  (erc-match--toggle-hidden 'match-fools arg))
-
-(defun erc-match--toggle-hidden (prop arg)
-  "Toggle invisibility for spec member PROP.
-Treat ARG in a manner similar to mode toggles defined by
-`define-minor-mode'."
-  (when arg
-    (setq arg (prefix-numeric-value arg)))
-  (if (memq prop (ensure-list buffer-invisibility-spec))
-      (unless (natnump arg)
-        (remove-from-invisibility-spec prop))
-    (when (or (not arg) (natnump arg))
-      (add-to-invisibility-spec prop))))
+  (erc--toggle-hidden 'match-fools arg))
 
 (provide 'erc-match)
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 00ee97b7e14..89b3397d8f1 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -346,8 +346,13 @@ erc-hide-prompt
   "If non-nil, hide input prompt upon disconnecting.
 To unhide, type something in the input area.  Once revealed, a
 prompt remains unhidden until the next disconnection.  Channel
-prompts are unhidden upon rejoining.  See
-`erc-unhide-query-prompt' for behavior concerning query prompts."
+prompts are unhidden upon rejoining.  For behavior concerning
+query prompts, see `erc-unhide-query-prompt'.  Longtime ERC users
+should note that this option was repurposed in ERC 5.5 because it
+had lain dormant for years after being sidelined in 5.3 when its
+only use in the interactive client was removed.  Before then, its
+role was controlling whether `erc-command-indicator' would appear
+alongside echoed slash-command lines."
   :package-version '(ERC . "5.5")
   :group 'erc-display
   :type '(choice (const :tag "Always hide prompt" t)
@@ -759,28 +764,6 @@ erc-prompt
         (concat prompt " ")
       prompt)))
 
-(defcustom erc-command-indicator nil
-  "Indicator used by ERC for showing commands.
-
-If non-nil, this will be used in the ERC buffer to indicate
-commands (i.e., input starting with a `/').
-
-If nil, the prompt will be constructed from the variable `erc-prompt'."
-  :group 'erc-display
-  :type '(choice (const nil) string function))
-
-(defun erc-command-indicator ()
-  "Return the command indicator prompt as a string.
-
-This only has any meaning if the variable `erc-command-indicator' is non-nil."
-  (and erc-command-indicator
-       (let ((prompt (if (functionp erc-command-indicator)
-                         (funcall erc-command-indicator)
-                       erc-command-indicator)))
-         (if (> (length prompt) 0)
-             (concat prompt " ")
-           prompt))))
-
 (defcustom erc-notice-prefix "*** "
   "Prefix for all notices."
   :group 'erc-display
@@ -1364,12 +1347,6 @@ erc-prompt-face
   "ERC face for the prompt."
   :group 'erc-faces)
 
-(defface erc-command-indicator-face
-  '((t :weight bold))
-  "ERC face for the command indicator.
-See the variable `erc-command-indicator'."
-  :group 'erc-faces)
-
 (defface erc-notice-face
   '((default :weight bold)
     (((class color) (min-colors 88) (supports :weight semi-bold))
@@ -2077,7 +2054,7 @@ erc--sort-modules
 
 (defcustom erc-modules '( autojoin button completion fill imenu irccontrols
                           list match menu move-to-prompt netsplit
-                          networks noncommands readonly ring stamp track)
+                          networks readonly ring stamp track)
   "A list of modules which ERC should enable.
 If you set the value of this without using `customize' remember to call
 \(erc-update-modules) after you change it.  When using `customize', modules
@@ -2127,6 +2104,7 @@ erc-modules
     (const :tag "button: Buttonize URLs, nicknames, and other text" button)
     (const :tag "capab: Mark unidentified users on servers supporting CAPAB"
            capab-identify)
+    (const :tag "command-indicator: Echo command lines." command-indicator)
     (const :tag "completion: Complete nicknames and commands (programmable)"
            completion)
     (const :tag "dcc: Provide Direct Client-to-Client support" dcc)
@@ -2146,7 +2124,7 @@ erc-modules
     (const :tag "networks: Provide data about IRC networks" networks)
     (const :tag "nickbar: Show nicknames in a dyamic side window" nickbar)
     (const :tag "nicks: Uniquely colorize nicknames in target buffers" nicks)
-    (const :tag "noncommands: Don't display non-IRC commands after evaluation"
+    (const :tag "noncommands: Deprecated. See module `command-indicator'."
            noncommands)
     (const :tag "notifications: Desktop alerts on PRIVMSG or mentions"
            notifications)
@@ -3309,6 +3287,18 @@ erc--hide-message
           (cl-incf beg))
         (erc--merge-prop (1- beg) (1- end) 'invisible value)))))
 
+(defun erc--toggle-hidden (prop arg)
+  "Toggle invisibility for spec member PROP.
+Treat ARG in a manner similar to mode toggles defined by
+`define-minor-mode'."
+  (when arg
+    (setq arg (prefix-numeric-value arg)))
+  (if (memq prop (ensure-list buffer-invisibility-spec))
+      (unless (natnump arg)
+        (remove-from-invisibility-spec prop))
+    (when (or (not arg) (natnump arg))
+      (add-to-invisibility-spec prop))))
+
 (defun erc--delete-inserted-message (beg-or-point &optional end)
   "Remove message between BEG and END.
 Expect BEG and END to match bounds as returned by the macro
@@ -7016,7 +7006,9 @@ erc--send-input-lines
   (when (erc--input-split-sendp lines-obj)
     (dolist (line (erc--input-split-lines lines-obj))
       (when (erc--input-split-insertp lines-obj)
-        (erc-display-msg line))
+        (if (functionp (erc--input-split-insertp lines-obj))
+            (funcall (erc--input-split-insertp lines-obj) line)
+          (erc-display-msg line)))
       (erc-process-input-line (concat line "\n")
                               (null erc-flood-protect)
                               (not (erc--input-split-cmdp lines-obj))))))
@@ -7522,7 +7514,10 @@ erc-load-irc-script-lines
 user input."
   (let* ((cb (current-buffer))
          (s "")
-         (sp (or (erc-command-indicator) (erc-prompt)))
+         (sp (or (and (bound-and-true-p erc-command-indicator-mode)
+                      (fboundp 'erc-command-indicator)
+                      (erc-command-indicator))
+                 (erc-prompt)))
          (args (and (boundp 'erc-script-args) erc-script-args)))
     (if (and args (string-match "^ " args))
         (setq args (substring args 1)))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 2898ca7be75..e7422d330c0 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2543,7 +2543,8 @@ erc-handle-irc-url
     (kill-buffer "#chan")))
 
 (defconst erc-tests--modules
-  '( autoaway autojoin bufbar button capab-identify completion dcc fill identd
+  '( autoaway autojoin bufbar button capab-identify
+     command-indicator completion dcc fill identd
      imenu irccontrols keep-place list log match menu move-to-prompt netsplit
      networks nickbar nicks noncommands notifications notify page readonly
      replace ring sasl scrolltobottom services smiley sound
-- 
2.41.0