unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#67677: 30.0.50; ERC 5.6: Use templates for formatting chat messages
@ 2023-12-07  7:06 J.P.
  0 siblings, 0 replies; 6+ messages in thread
From: J.P. @ 2023-12-07  7:06 UTC (permalink / raw)
  To: 67677; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 6636 bytes --]

Tags: patch

The most valuable messages in a typical target buffer are those
featuring a leading display name, i.e., chat messages. While ERC's many
modules exert a good deal of influence over the fit and finish of an
inserted "speaker message," the fundamental layout is mostly set in
stone beforehand. This currently happens in a somewhat haphazard
fashion, with ERC cobbling together various ingredients that may or may
not factor into the finished article, which makes predicting and
influencing crucial aspects of the final look a somewhat shaky affair.

Longtime users are likely acquainted with ERC's notion of a "language
catalog." On the surface, these are just collections of variables
sharing a common "erc-message-<catalog>-<key>" naming scheme. Each
variable's value is a `format-spec' template befitting its key. Some
keys are just IRC commands, like `JOIN', while others describe the
context in which they're typically summoned.

The real benefit of these template catalogs is twofold:

  1. They're "declarative" in the sense that their form describes their
     purpose as a model preview of the result, in plain, quasi-literal
     terms (what you see is what you get).

  2. They're also an interface, with each collection of keys and their
     respective format strings and spec parameters forming a schema of
     sorts, making extensibility essentially ingrained.

The catch here is that ERC only currently uses these catalogs for select
informative messages but not actual chat content. The proposal is thus
to extend this catalog system to encompass speaker messages.

The approach I've chosen tries to strike a balance between code reuse
and extensibility and disregards seemingly vital distinctions, like
whether a message is being sent or received. Instead, it prefers to
organize things in terms of envelope semantics. Who is it from? How
should it be delivered? Etc. The answers become parameters that form the
various catalog keys, for example, "ctcp-action-statusmsg" or
"query-notice-input". Most of these combinations come prescribed by the
IRC protocol itself and by compatibility concerns. It's mostly just the
terminology and the ordering of the components that we're free to tinker
with.

So far, the most perceptible gains are maintenance oriented. But moving
toward user-facing extensibility is mainly a matter of nailing down the
core set of keys for the base speaker catalog and exposing custom
catalogs by promoting what's currently an internal variable to a user
option. At that point, the possibilities are many. For example, over the
years, folks have expressed a desire to have both "heading-style"
display names, which occupy their own line above a message body, and
"bracketless" display names. Both are easily doable with such a system
in place. (PoC available.)

I was originally hoping to tackle this feature in 5.7, but I think the
proposed approach is mature enough to refine and ship with this release.
A number of tangential and ancillary changes are included in this set as
soft dependencies, many as follow-ons to recently "closed" bugs. I will
address some of them separately, as needed, in their respective threads.

Thanks.


In GNU Emacs 30.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version
 3.24.38, cairo version 1.17.6) of 2023-12-06 built on localhost
Repository revision: d8a00879309a3bf62f6ffcae103aa3bdba776ee9
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12014000
System Description: Fedora Linux 37 (Workstation Edition)

Configured using:
 'configure --enable-check-lisp-object-type --enable-checking=yes,glyphs
 'CFLAGS=-O0 -g3'
 PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
JSON LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES
NATIVE_COMP NOTIFY INOTIFY PDUMPER PNG RSVG SECCOMP SOUND SQLITE3
THREADS TIFF TOOLKIT_SCROLL_BARS WEBP X11 XDBE XIM XINPUT2 XPM GTK3 ZLIB

Important settings:
  value of $LANG: en_US.UTF-8
  value of $XMODIFIERS: @im=ibus
  locale-coding-system: utf-8-unix

Major mode: Lisp Interaction

Minor modes in effect:
  tooltip-mode: t
  global-eldoc-mode: t
  eldoc-mode: t
  show-paren-mode: t
  electric-indent-mode: t
  mouse-wheel-mode: t
  tool-bar-mode: t
  menu-bar-mode: t
  file-name-shadow-mode: t
  global-font-lock-mode: t
  font-lock-mode: t
  blink-cursor-mode: t
  minibuffer-regexp-mode: t
  line-number-mode: t
  indent-tabs-mode: t
  transient-mark-mode: t
  auto-composition-mode: t
  auto-encryption-mode: t
  auto-compression-mode: t

Load-path shadows:
None found.

Features:
(shadow sort mail-extr emacsbug message mailcap yank-media puny dired
dired-loaddefs rfc822 mml mml-sec epa derived epg rfc6068 epg-config
gnus-util time-date mm-decode mm-bodies mm-encode mail-parse rfc2231
mailabbrev gmm-utils mailheader sendmail rfc2047 rfc2045 ietf-drums
mm-util mail-prsvr mail-utils compile text-property-search comint
ansi-osc ansi-color ring comp-run comp-common rx erc auth-source cl-seq
eieio eieio-core cl-macs password-cache json subr-x map format-spec
cl-loaddefs cl-lib erc-backend erc-networks byte-opt gv bytecomp
byte-compile erc-common erc-compat erc-loaddefs rmc iso-transl tooltip
cconv eldoc paren electric uniquify ediff-hook vc-hooks lisp-float-type
elisp-mode mwheel term/x-win x-win term/common-win x-dnd touch-screen
tool-bar dnd fontset image regexp-opt fringe tabulated-list replace
newcomment text-mode lisp-mode prog-mode register page tab-bar menu-bar
rfn-eshadow isearch easymenu timer select scroll-bar mouse jit-lock
font-lock syntax font-core term/tty-colors frame minibuffer nadvice seq
simple cl-generic indonesian philippine cham georgian utf-8-lang
misc-lang vietnamese tibetan thai tai-viet lao korean japanese eucjp-ms
cp51932 hebrew greek romanian slovak czech european ethiopic indian
cyrillic chinese composite emoji-zwj charscript charprop case-table
epa-hook jka-cmpr-hook help abbrev obarray oclosure cl-preloaded button
loaddefs theme-loaddefs faces cus-face macroexp files window
text-properties overlay sha1 md5 base64 format env code-pages mule
custom widget keymap hashtable-print-readable backquote threads dbusbind
inotify lcms2 dynamic-setting system-font-setting font-render-setting
cairo gtk x-toolkit xinput2 x multi-tty move-toolbar
make-network-process native-compile emacs)

Memory information:
((conses 16 82762 11177) (symbols 48 9671 0) (strings 32 25958 4639)
 (string-bytes 1 784755) (vectors 16 19276)
 (vector-slots 8 328221 11347) (floats 8 24 28) (intervals 56 260 0)
 (buffers 984 12))


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-5.6-Define-ERC-message-formatting-templates-with-def.patch --]
[-- Type: text/x-patch, Size: 22800 bytes --]

From 41f475e993aa2a3f1ca1faea4ed0ef375518f476 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 28 Nov 2023 16:51:36 -0800
Subject: [PATCH 01/11] [5.6] Define ERC message-formatting templates with
 defvar

* etc/ERC-NEWS: Mention convenience macro being preferred means of
defining message templates.
* lisp/erc/erc-common.el (erc--define-catalog,
erc-define-message-format-catalog): New macro and internal variant to
replace `erc-define-catalog-entry'.  Internal variant allows us to
defer reindenting existing definitions until meaningfully edited.
* lisp/erc/erc-dcc.el (erc-message-english-dcc-chat-discarded,
erc-message-english-dcc-chat-ended,
erc-message-english-dcc-chat-no-request,
erc-message-english-dcc-chat-offered,
erc-message-english-dcc-chat-offer,
erc-message-english-dcc-chat-accept,
erc-message-english-dcc-chat-privmsg, erc-message-english-dcc-closed,
erc-message-english-dcc-command-undefined,
erc-message-english-dcc-ctcp-errmsg,
erc-message-english-dcc-ctcp-unknown,
erc-message-english-dcc-get-bytes-received,
erc-message-english-dcc-get-complete,
erc-message-english-dcc-get-failed,
erc-message-english-dcc-get-cmd-aborted,
erc-message-english-dcc-get-file-too-long,
erc-message-english-dcc-get-notfound,
erc-message-english-dcc-list-head, erc-message-english-dcc-list-line,
erc-message-english-dcc-list-item, erc-message-english-dcc-list-end,
erc-message-english-dcc-malformed,
erc-message-english-dcc-privileged-port,
erc-message-english-dcc-request-bogus,
erc-message-english-dcc-send-finished,
erc-message-english-dcc-send-offered,
erc-message-english-dcc-send-offer): Define at top level using
`defvar'.
* lisp/erc/erc-netsplit.el (erc-netsplit-mode, erc-netsplit-enable):
Don't call `erc-netsplit-install-message-catalogs'.
(erc-netsplit-install-message-catalogs): Deprecate function.
(erc-message-english-netsplit, erc-message-english-netjoin,
erc-message-english-netjoin-done, erc-message-english-netsplit-none,
erc-message-english-netsplit-wholeft): Define at top level using
`defvar'.
* lisp/erc/erc-notify.el (erc-notify-install-message-catalogs):
Deprecate.
(erc-message-english-notify_current, erc-message-english-notify_list,
erc-message-english-notify_on, erc-message-english-notify_off): Define
at top level using `defvar'.
* lisp/erc/erc-page.el (erc-message-english-CTCP-PAGE): Define at top
level using `defvar'.
* lisp/erc/erc-sasl.el (erc-message-english-s902,
erc-message-english-s904, erc-message-english-s905,
erc-message-english-s906, erc-message-english-s907,
erc-message-english-s908): Define at top level using `defvar'.
* lisp/erc/erc-sound.el (erc-message-english-CTCP-SOUND): Define using
`defvar'.
* lisp/erc/erc.el (erc--make-message-variable-name): New function to
replace `erc-make-message-variable-name' internally, where most uses
formerly checked whether the returned variable was bound.  This helper
now conditionally performs that common task.
(erc-make-message-variable-name): Defer to internal variant
`erc--make-message-variable-name'.
(erc-define-catalog-entry): Deprecate.
(erc-define-catalog): Deprecate.
(erc-retrieve-catalog-entry): Refactor to favor
`default-toplevel-value' of `erc-current-message-catalog' before
falling back to `english'.  Not doing this was arguably a bug.
(erc-message-english-bad-ping-response,
erc-message-english-bad-syntax, erc-message-english-incorrect-args,
erc-message-english-cannot-find-file,
erc-message-english-cannot-read-file, erc-message-english-connect,
erc-message-english-country, erc-message-english-country-unknown,
erc-message-english-ctcp-empty, erc-message-english-ctcp-request,
erc-message-english-ctcp-request-to,
erc-message-english-ctcp-too-many, erc-message-english-flood-ctcp-off,
erc-message-english-flood-strict-mode,
erc-message-english-disconnected,
erc-message-english-disconnected-noreconnect,
erc-message-english-reconnecting,
erc-message-english-reconnect-canceled, erc-message-english-finished,
erc-message-english-terminated, erc-message-english-login,
erc-message-english-nick-in-use, erc-message-english-nick-too-long,
erc-message-english-no-default-channel,
erc-message-english-no-invitation, erc-message-english-no-target,
erc-message-english-ops, erc-message-english-ops-none,
erc-message-english-undefined-ctcp,
erc-message-english-user-mode-redundant-add,
erc-message-english-user-mode-redundant-drop,
erc-message-english-variable-not-bound, erc-message-english-ACTION,
erc-message-english-CTCP-CLIENTINFO, erc-message-english-CTCP-ECHO,
erc-message-english-CTCP-FINGER, erc-message-english-CTCP-PING,
erc-message-english-CTCP-TIME, erc-message-english-CTCP-UNKNOWN,
erc-message-english-CTCP-VERSION, erc-message-english-ERROR,
erc-message-english-INVITE, erc-message-english-JOIN,
erc-message-english-JOIN-you, erc-message-english-KICK,
erc-message-english-KICK-you, erc-message-english-KICK-by-you,
erc-message-english-MODE, erc-message-english-MODE-nick,
erc-message-english-NICK, erc-message-english-NICK-you,
erc-message-english-PART, erc-message-english-PING,
erc-message-english-PONG, erc-message-english-QUIT,
erc-message-english-TOPIC, erc-message-english-WALLOPS,
erc-message-english-s004, erc-message-english-s221,
erc-message-english-s252, erc-message-english-s253,
erc-message-english-s254, erc-message-english-s275,
erc-message-english-s301, erc-message-english-s303,
erc-message-english-s305, erc-message-english-s306,
erc-message-english-s307, erc-message-english-s311,
erc-message-english-s312, erc-message-english-s313,
erc-message-english-s314, erc-message-english-s317,
erc-message-english-s317-on-since, erc-message-english-s319,
erc-message-english-s320, erc-message-english-s321,
erc-message-english-s322, erc-message-english-s324,
erc-message-english-s328, erc-message-english-s329,
erc-message-english-s330, erc-message-english-s331,
erc-message-english-s332, erc-message-english-s333,
erc-message-english-s341, erc-message-english-s352,
erc-message-english-s353, erc-message-english-s367,
erc-message-english-s367-set-by, erc-message-english-s368,
erc-message-english-s379, erc-message-english-s391,
erc-message-english-s396, erc-message-english-s401,
erc-message-english-s402, erc-message-english-s403,
erc-message-english-s404, erc-message-english-s405,
erc-message-english-s406, erc-message-english-s412,
erc-message-english-s421, erc-message-english-s431,
erc-message-english-s432, erc-message-english-s442,
erc-message-english-s445, erc-message-english-s446,
erc-message-english-s451, erc-message-english-s461,
erc-message-english-s462, erc-message-english-s463,
erc-message-english-s464, erc-message-english-s465,
erc-message-english-s471, erc-message-english-s473,
erc-message-english-s474, erc-message-english-s475,
erc-message-english-s481, erc-message-english-s482,
erc-message-english-s483, erc-message-english-s484,
erc-message-english-s485, erc-message-english-s491,
erc-message-english-s501, erc-message-english-s502,
erc-message-english-s671): Define at top level using `defvar'.
* test/lisp/erc/erc-tests.el (erc--make-message-variable-name,
erc-retrieve-catalog-entry): New tests.
---
 etc/ERC-NEWS               |  5 +++
 lisp/erc/erc-common.el     | 27 ++++++++++++++++
 lisp/erc/erc-dcc.el        |  5 ++-
 lisp/erc/erc-netsplit.el   | 13 ++++++--
 lisp/erc/erc-notify.el     | 10 ++++--
 lisp/erc/erc-page.el       |  3 +-
 lisp/erc/erc-sasl.el       |  5 ++-
 lisp/erc/erc-sound.el      |  3 +-
 lisp/erc/erc.el            | 53 +++++++++++++++++++-----------
 test/lisp/erc/erc-tests.el | 66 ++++++++++++++++++++++++++++++++++++++
 10 files changed, 159 insertions(+), 31 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 7b39af03a88..238c40feefb 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -510,6 +510,11 @@ handling specific "MODE" types and letters in coming releases.  If
 you'd like a say in shaping how this transpires, please share your
 ideas and use cases on the tracker.
 
+*** A better way to define message-formatting templates.
+The functions 'erc-define-catalog-entry' and 'erc-define-catalog' have
+been deprecated in favor of 'erc-define-message-format-catalog', a new
+macro for defining template "catalogs" at the top level of libraries.
+
 *** Miscellaneous changes
 Two helper macros from GNU ELPA's Compat library are now available to
 third-party modules as 'erc-compat-call' and 'erc-compat-function'.
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 8daedf9b019..ea4374da7b6 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -506,6 +506,33 @@ erc--with-dependent-type-match
                              (,(widget-get (widget-convert type) :match) w v))
                     ',(cdr type)))
 
+;; This internal variant only exists as a transition aid to avoid
+;; immediately having to reflow lengthy definition lists, like the one
+;; below.  These sites should switch to using the public macro when
+;; undergoing their next major edit.
+(defmacro erc--define-catalog (name entries)
+  "Define `erc-display-message' formatting templates for NAME, a symbol.
+See `erc-define-message-format-catalog' for the meaning of the
+alist ENTRIES."
+  (declare (indent 1))
+  (let (out)
+    (dolist (e entries (cons 'progn (nreverse out)))
+      (push `(defvar ,(intern (format "erc-message-%s-%s" name (car e)))
+               ,(cdr e)
+               ,(format "Message template for key `%s' in catalog `%s'."
+                        (car e) name))
+            out))))
+
+(defmacro erc-define-message-format-catalog (language &rest entries)
+  "Define message-formatting templates for LANGUAGE, a symbol.
+Expect ENTRIES to be pairs of (KEY . FORMAT), where KEY is a
+symbol, and FORMAT evaluates to a format string compatible with
+`format-spec'.  Expect modules that only define a handful of
+entries to do so manually, instead of using this macro, so that
+the resulting variables will end up with more useful doc strings."
+  (declare (indent 1))
+  `(erc--define-catalog ,language ,entries))
+
 (provide 'erc-common)
 
 ;;; erc-common.el ends here
diff --git a/lisp/erc/erc-dcc.el b/lisp/erc/erc-dcc.el
index f05ae41fc51..3bcdfb96eb8 100644
--- a/lisp/erc/erc-dcc.el
+++ b/lisp/erc/erc-dcc.el
@@ -131,9 +131,8 @@ erc-dcc-open-network-stream
     (open-network-stream procname buffer addr port
                          :type (and (plist-get entry :secure) 'tls))))
 
-(erc-define-catalog
- 'english
- '((dcc-chat-discarded
+(erc--define-catalog english
+  ((dcc-chat-discarded
     . "DCC: previous chat request from %n (%u@%h) discarded")
    (dcc-chat-ended . "DCC: chat with %n ended %t: %e")
    (dcc-chat-no-request . "DCC: chat request from %n not found")
diff --git a/lisp/erc/erc-netsplit.el b/lisp/erc/erc-netsplit.el
index 5dd11ab1869..076e1f0254b 100644
--- a/lisp/erc/erc-netsplit.el
+++ b/lisp/erc/erc-netsplit.el
@@ -41,7 +41,7 @@ erc-netsplit
 ;;;###autoload(autoload 'erc-netsplit-mode "erc-netsplit")
 (define-erc-module netsplit nil
   "This mode hides quit/join messages if a netsplit occurs."
-  ((erc-netsplit-install-message-catalogs)
+  ( ; FIXME delete newline on next edit
    (add-hook 'erc-server-JOIN-functions #'erc-netsplit-JOIN)
    (add-hook 'erc-server-MODE-functions #'erc-netsplit-MODE)
    (add-hook 'erc-server-QUIT-functions #'erc-netsplit-QUIT)
@@ -85,13 +85,22 @@ erc-netsplit-list
 join from that split has been detected or not.")
 
 (defun erc-netsplit-install-message-catalogs ()
+  (declare (obsolete "defined at top level in erc-netsplit.el" "30.1"))
+  (with-suppressed-warnings ((obsolete erc-define-catalog)) ; indentation
   (erc-define-catalog
    'english
    '((netsplit	       . "netsplit: %s")
      (netjoin	       . "netjoin: %s, %N were split")
      (netjoin-done     . "netjoin: All lost souls are back!")
      (netsplit-none    . "No netsplits in progress")
-     (netsplit-wholeft . "split: %s missing: %n %t"))))
+     (netsplit-wholeft . "split: %s missing: %n %t"))))) ; indentation
+
+(erc-define-message-format-catalog english
+  (netsplit . "netsplit: %s")
+  (netjoin . "netjoin: %s, %N were split")
+  (netjoin-done . "netjoin: All lost souls are back!")
+  (netsplit-none . "No netsplits in progress")
+  (netsplit-wholeft . "split: %s missing: %n %t"))
 
 (defun erc-netsplit-JOIN (proc parsed)
   "Show/don't show rejoins."
diff --git a/lisp/erc/erc-notify.el b/lisp/erc/erc-notify.el
index cf7ffbb40d7..2c207d99bb0 100644
--- a/lisp/erc/erc-notify.el
+++ b/lisp/erc/erc-notify.el
@@ -78,12 +78,14 @@ erc-last-ison-time
 ;;;; Setup
 
 (defun erc-notify-install-message-catalogs ()
+  (declare (obsolete "defined at top level in erc-notify.el" "30.1"))
+  (with-suppressed-warnings ((obsolete erc-define-catalog)) ; indentation
   (erc-define-catalog
    'english
    '((notify_current . "Notified people online: %l")
      (notify_list    . "Current notify list: %l")
      (notify_on      . "Detected %n on IRC network %m")
-     (notify_off     . "%n has left IRC network %m"))))
+     (notify_off     . "%n has left IRC network %m"))))) ; indentation
 
 ;;;###autoload(autoload 'erc-notify-mode "erc-notify" nil t)
 (define-erc-module notify nil
@@ -241,7 +243,11 @@ pcomplete/erc-mode/NOTIFY
   (require 'pcomplete)
   (pcomplete-here (pcomplete-erc-all-nicks)))
 
-(erc-notify-install-message-catalogs)
+(erc-define-message-format-catalog english
+  (notify_current . "Notified people online: %l")
+  (notify_list . "Current notify list: %l")
+  (notify_on . "Detected %n on IRC network %m")
+  (notify_off . "%n has left IRC network %m"))
 
 (provide 'erc-notify)
 
diff --git a/lisp/erc/erc-page.el b/lisp/erc/erc-page.el
index a94678e5132..2e5974bd22e 100644
--- a/lisp/erc/erc-page.el
+++ b/lisp/erc/erc-page.el
@@ -42,7 +42,8 @@ page
   "Process CTCP PAGE requests from IRC."
   nil nil)
 
-(erc-define-catalog-entry 'english 'CTCP-PAGE "Page from %n (%u@%h): %m")
+(defvar erc-message-english-CTCP-PAGE "Page from %n (%u@%h): %m"
+  "English template for a CTCP PAGE message.")
 
 (defcustom erc-page-function nil
   "A function to process a \"page\" request.
diff --git a/lisp/erc/erc-sasl.el b/lisp/erc/erc-sasl.el
index c6922b1b26b..8ecce7aef31 100644
--- a/lisp/erc/erc-sasl.el
+++ b/lisp/erc/erc-sasl.el
@@ -305,9 +305,8 @@ erc-sasl--mechanism-offered-p
                        (| eot ",")))
                   (downcase offered)))
 
-(erc-define-catalog
- 'english
- '((s902 . "ERR_NICKLOCKED nick %n unavailable: %s")
+(erc--define-catalog english
+  ((s902 . "ERR_NICKLOCKED nick %n unavailable: %s")
    (s904 . "ERR_SASLFAIL (authentication failed) %s")
    (s905 . "ERR SASLTOOLONG (credentials too long) %s")
    (s906 . "ERR_SASLABORTED (authentication aborted) %s")
diff --git a/lisp/erc/erc-sound.el b/lisp/erc/erc-sound.el
index 083d72805df..aaa2e059070 100644
--- a/lisp/erc/erc-sound.el
+++ b/lisp/erc/erc-sound.el
@@ -63,7 +63,8 @@ sound
   ((remove-hook 'erc-ctcp-query-SOUND-hook #'erc-ctcp-query-SOUND)
    (define-key erc-mode-map "\C-c\C-s" #'undefined)))
 
-(erc-define-catalog-entry 'english 'CTCP-SOUND "%n (%u@%h) plays %s:%m")
+(defvar erc-message-english-CTCP-SOUND "%n (%u@%h) plays %s:%m"
+  "English template for a CTCP SOUND message.")
 
 (defcustom erc-play-sound t
   "Play sounds when you receive CTCP SOUND requests."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 616129bf780..030d7787d8a 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -8690,24 +8690,38 @@ erc-popup-input-buffer
 
 ;;; Message catalog
 
+(define-inline erc--make-message-variable-name (catalog key softp)
+  "Return variable name conforming to ERC's message-catalog interface.
+Given a CATALOG symbol `mycat' and format-string KEY `mykey',
+also a symbol, return the symbol `erc-message-mycat-key'.  With
+SOFTP, only do so when defined as a variable."
+  (inline-quote
+   (let* ((catname (symbol-name ,catalog))
+          (prefix (if (eq ?- (aref catname 0)) "erc--message" "erc-message-"))
+          (name (concat prefix catname "-" (symbol-name ,key))))
+     (if ,softp
+         (and-let* ((s (intern-soft name)) ((boundp s))) s)
+       (intern name)))))
+
 (defun erc-make-message-variable-name (catalog entry)
   "Create a variable name corresponding to CATALOG's ENTRY."
-  (intern (concat "erc-message-"
-                  (symbol-name catalog) "-" (symbol-name entry))))
+  (erc--make-message-variable-name catalog entry nil))
 
 (defun erc-define-catalog-entry (catalog entry format-spec)
   "Set CATALOG's ENTRY to FORMAT-SPEC."
+  (declare (obsolete "define manually using `defvar' instead" "30.1"))
   (set (erc-make-message-variable-name catalog entry)
        format-spec))
 
 (defun erc-define-catalog (catalog entries)
   "Define a CATALOG according to ENTRIES."
-  (dolist (entry entries)
-    (erc-define-catalog-entry catalog (car entry) (cdr entry))))
+  (declare (obsolete erc-define-message-format-catalog "30.1"))
+  (with-suppressed-warnings ((obsolete erc-define-catalog-entry))
+    (dolist (entry entries)
+      (erc-define-catalog-entry catalog (car entry) (cdr entry)))))
 
-(erc-define-catalog
- 'english
- '((bad-ping-response . "Unexpected PING response from %n (time %t)")
+(erc--define-catalog english
+  ((bad-ping-response . "Unexpected PING response from %n (time %t)")
    (bad-syntax . "Error occurred - incorrect usage?\n%c %u\n%d")
    (incorrect-args . "Incorrect arguments. Usage:\n%c %u\n%d")
    (cannot-find-file . "Cannot find file %f")
@@ -8764,7 +8778,7 @@ erc-define-catalog
    (MODE-nick . "%n has changed mode for %t to %m")
    (NICK   . "%n (%u@%h) is now known as %N")
    (NICK-you . "Your new nickname is %N")
-   (PART   . erc-message-english-PART)
+   (PART   . #'erc-message-english-PART)
    (PING   . "PING from server (last: %s sec. ago)")
    (PONG   . "PONG from %h (%i second%s)")
    (QUIT   . "%n (%u@%h) has quit: %r")
@@ -8862,18 +8876,19 @@ erc-message-english-PART
 (defvar-local erc-current-message-catalog 'english)
 
 (defun erc-retrieve-catalog-entry (entry &optional catalog)
-  "Retrieve ENTRY from CATALOG.
-
-If CATALOG is nil, `erc-current-message-catalog' is used.
-
-If ENTRY is nil in CATALOG, it is retrieved from the fallback,
-english, catalog."
+  "Retrieve `format-spec' for symbol key ENTRY in CATALOG.
+Without CATALOG, use `erc-current-message-catalog'.  If lookup
+fails, try the latter's `default-toplevel-value' if it's not the
+same as CATALOG. Failing that, try the `english' catalog if yet
+untried."
   (unless catalog (setq catalog erc-current-message-catalog))
-  (let ((var (erc-make-message-variable-name catalog entry)))
-    (if (boundp var)
-        (symbol-value var)
-      (when (boundp (erc-make-message-variable-name 'english entry))
-        (symbol-value (erc-make-message-variable-name 'english entry))))))
+  (symbol-value
+   (or (erc--make-message-variable-name catalog entry 'softp)
+       (let ((default (default-toplevel-value 'erc-current-message-catalog)))
+         (or (and (not (eq default catalog))
+                  (erc--make-message-variable-name default entry 'softp))
+             (and (not (memq 'english (list default catalog)))
+                  (erc--make-message-variable-name 'english entry 'softp)))))))
 
 (defun erc-format-message (msg &rest args)
   "Format MSG according to ARGS.
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 912a85ad5e0..49d500fadea 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -3262,4 +3262,70 @@ define-erc-module--local
                       (put 'erc-mname-enable 'definition-name 'mname)
                       (put 'erc-mname-disable 'definition-name 'mname))))))
 
+(ert-deftest erc--make-message-variable-name ()
+  (should (erc--make-message-variable-name 'english 'QUIT 'softp))
+  (should (erc--make-message-variable-name 'english 'QUIT nil))
+
+  (let ((obarray (obarray-make)))
+    (should-not (erc--make-message-variable-name 'testcat 'testkey 'softp))
+    (should (erc--make-message-variable-name 'testcat 'testkey nil))
+    (should (intern-soft "erc-message-testcat-testkey" obarray))
+    (should-not (erc--make-message-variable-name 'testcat 'testkey 'softp))
+    (set (intern "erc-message-testcat-testkey" obarray) "hello world")
+    (should (equal (symbol-value
+                    (erc--make-message-variable-name 'testcat 'testkey nil))
+                   "hello world")))
+
+  ;; Hyphenated (internal catalog).
+  (let ((obarray (obarray-make)))
+    (should-not (erc--make-message-variable-name '-testcat 'testkey 'softp))
+    (should (erc--make-message-variable-name '-testcat 'testkey nil))
+    (should (intern-soft "erc--message-testcat-testkey" obarray))
+    (should-not (erc--make-message-variable-name '-testcat 'testkey 'softp))
+    (set (intern "erc--message-testcat-testkey" obarray) "hello world")
+    (should (equal (symbol-value
+                    (erc--make-message-variable-name '-testcat 'testkey nil))
+                   "hello world"))))
+
+(ert-deftest erc-retrieve-catalog-entry ()
+  (should (eq 'english erc-current-message-catalog))
+  (should (equal (erc-retrieve-catalog-entry 's221) "User modes for %n: %m"))
+
+  ;; Local binding.
+  (with-temp-buffer
+    (should (equal (erc-retrieve-catalog-entry 's221) "User modes for %n: %m"))
+    (setq erc-current-message-catalog 'test)
+    ;; No catalog named `test'.
+    (should (equal (erc-retrieve-catalog-entry 's221) "User modes for %n: %m"))
+
+    (let ((obarray (obarray-make)))
+      (set (intern "erc-message-test-s221") "test 221 val")
+      (should (equal (erc-retrieve-catalog-entry 's221) "test 221 val"))
+      (set (intern "erc-message-english-s221") "eng 221 val")
+
+      (let ((erc-current-message-catalog 'english))
+        (should (equal (erc-retrieve-catalog-entry 's221) "eng 221 val")))
+
+      (with-temp-buffer
+        (should (equal (erc-retrieve-catalog-entry 's221) "eng 221 val"))
+        (let ((erc-current-message-catalog 'test))
+          (should (equal (erc-retrieve-catalog-entry 's221) "test 221 val"))))
+
+      (should (equal (erc-retrieve-catalog-entry 's221) "test 221 val")))
+
+    (should (equal (erc-retrieve-catalog-entry 's221) "User modes for %n: %m"))
+    (should (equal erc-current-message-catalog 'test)))
+
+  ;; Default top-level value.
+  (set-default-toplevel-value 'erc-current-message-catalog 'test-top)
+  (should (equal (erc-retrieve-catalog-entry 's221) "User modes for %n: %m"))
+  (set (intern "erc-message-test-top-s221") "test-top 221 val")
+  (should (equal (erc-retrieve-catalog-entry 's221) "test-top 221 val"))
+
+  (setq erc-current-message-catalog 'test-local)
+  (should (equal (erc-retrieve-catalog-entry 's221) "test-top 221 val"))
+
+  (makunbound (intern "erc-message-test-top-s221"))
+  (unintern "erc-message-test-top-s221" obarray))
+
 ;;; erc-tests.el ends here
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-5.6-Sequester-some-special-variable-declarations-in-.patch --]
[-- Type: text/x-patch, Size: 19023 bytes --]

From 5395c0085b0b45d5a18a4b584cb1703def55acde Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 26 Nov 2023 18:24:48 -0800
Subject: [PATCH 02/11] [5.6] Sequester some special-variable declarations in
 ERC

* lisp/erc/erc-backend.el (erc-log-p): Remove declaration.
(erc-server-reconnect): Move declaration for `erc-reuse-buffers' here.
(erc-process-sentinel-1): Move `erc-kill-server-buffer-on-quit'
declaration here.
(erc--conceal-prompt): Move `erc-prompt-hidden' declaration here.
(erc-server-INVITE): Move `erc-invitation' declaration here.
(erc-server-PART): Move `erc-kill-buffer-on-part' declaration here.
(erc-server-PRIVMSG): Move declarations for `erc-minibuffer-ignored',
`erc-receive-query-display', `erc-receive-query-display-defer',
`erc--cmem-from-nick-function', `erc-format-nick-function', and
`erc-format-query-as-channel' here.
(erc-server-401): Move `erc-whowas-on-nosuchnick' declaration here.
(erc-server-475): Move `erc--called-as-input-p' and
`erc-prompt-for-channel-key' declarations here.
* lisp/erc/erc-common.el (erc-log-p): Remove declaration for
`erc-log-p' and replace with actual definition.
(erc-log-aux): Move `erc-dbuf' declaration from top level into
function body.
* lisp/erc/erc-fill.el (erc-fill--wrap-continued-message-p): Note in
the doc string that this function produces side effects.
* lisp/erc/erc-networks.el (erc-determine-network,
erc-networks--copy-server-buffer-functions): Move some variable
declarations to function body.
(erc-settings, erc-get): Deprecate for now and explain why in doc
strings.  We could deprecate them unconditionally, but they never
provided usable code, and their names are short and valuable.
* lisp/erc/erc.el (tabbar--local-hlf, motif-version-string,
gtk-version-string): Prefer moving these single-serving declarations
to function bodies, if only to make closures from this library less
cluttered when debugging.
(erc-hooks, erc-timer-hook): Revise doc strings.
(erc-log-p): Move definition to erc-common.el.
(erc-cmd-SV): Mimic `emacs-version' and stick with `featurep', here in
combination with special-variable declarations, instead of `boundp' or
similar.
(erc-header-line-uses-tabbar-p): Explain that this has nothing to do
with `tab-bar'.
(erc-update-mode-line-buffer): Only assign when bound.  This has been
verified to work with version 2.0 on EmacsWiki.
* test/lisp/erc/erc-scenarios-base-reconnect.el: Timeouts.
* test/lisp/erc/erc-scenarios-base-renick.el: Timeouts.
* test/lisp/erc/resources/join/network-id/foonet-again.eld: Timeouts.
---
 lisp/erc/erc-backend.el                       | 29 ++++++++--------
 lisp/erc/erc-common.el                        |  6 ++--
 lisp/erc/erc-fill.el                          |  4 ++-
 lisp/erc/erc-networks.el                      | 23 ++++++++++---
 lisp/erc/erc.el                               | 34 ++++++++++++-------
 test/lisp/erc/erc-scenarios-base-reconnect.el |  2 +-
 test/lisp/erc/erc-scenarios-base-renick.el    |  2 +-
 .../join/network-id/foonet-again.eld          |  2 +-
 8 files changed, 64 insertions(+), 38 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 7ff55de0d0c..500e025e5a1 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -101,36 +101,21 @@
 (eval-when-compile (require 'cl-lib))
 (require 'erc-common)
 
-(defvar erc--called-as-input-p)
 (defvar erc--display-context)
 (defvar erc--target)
-(defvar erc--cmem-from-nick-function)
 (defvar erc-channel-list)
 (defvar erc-channel-users)
 (defvar erc-default-nicks)
 (defvar erc-default-recipients)
 (defvar erc-ensure-target-buffer-on-privmsg)
-(defvar erc-format-nick-function)
-(defvar erc-format-query-as-channel-p)
 (defvar erc-hide-prompt)
 (defvar erc-input-marker)
 (defvar erc-insert-marker)
-(defvar erc-invitation)
 (defvar erc-join-buffer)
-(defvar erc-kill-buffer-on-part)
-(defvar erc-kill-server-buffer-on-quit)
-(defvar erc-log-p)
-(defvar erc-minibuffer-ignored)
 (defvar erc-networks--id)
 (defvar erc-nick)
 (defvar erc-nick-change-attempt-count)
-(defvar erc-prompt-for-channel-key)
-(defvar erc-prompt-hidden)
-(defvar erc-receive-query-display)
-(defvar erc-receive-query-display-defer)
-(defvar erc-reuse-buffers)
 (defvar erc-verbose-server-ping)
-(defvar erc-whowas-on-nosuchnick)
 
 (declare-function erc--init-channel-modes "erc" (channel raw-args))
 (declare-function erc--open-target "erc" (target))
@@ -816,6 +801,7 @@ erc-server-reconnect
                   nil nil nil erc-session-client-certificate
                   erc-session-username
                   (erc-networks--id-given erc-networks--id))
+        (defvar erc-reuse-buffers)
         (unless (with-suppressed-warnings ((obsolete erc-reuse-buffers))
                   erc-reuse-buffers)
           (cl-assert (not (eq buffer (current-buffer)))))))))
@@ -1038,6 +1024,7 @@ erc-process-sentinel-1
           (erc-update-mode-line)
           ;; Kill server buffer if user wants it
           (set-buffer-modified-p nil)
+          (defvar erc-kill-server-buffer-on-quit)
           (when erc-kill-server-buffer-on-quit
             (kill-buffer (current-buffer))))
       ;; unexpected disconnect
@@ -1055,6 +1042,7 @@ erc--conceal-prompt
   (when-let (((null erc--hidden-prompt-overlay))
              (ov (make-overlay erc-insert-marker (1- erc-input-marker)
                                nil 'front-advance)))
+    (defvar erc-prompt-hidden)
     (overlay-put ov 'display erc-prompt-hidden)
     (setq erc--hidden-prompt-overlay ov)))
 
@@ -1716,6 +1704,7 @@ define-erc-response-handler
         (chnl (erc-response.contents parsed)))
     (pcase-let ((`(,nick ,login ,host)
                  (erc-parse-user (erc-response.sender parsed))))
+      (defvar erc-invitation)
       (setq erc-invitation chnl)
       (when (string= target (erc-current-nick))
         (erc-display-message
@@ -1888,6 +1877,7 @@ erc--server-determine-join-display-context
         (with-suppressed-warnings ((obsolete erc-delete-default-channel))
           (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
+        (defvar erc-kill-buffer-on-part)
         (when erc-kill-buffer-on-part
           (kill-buffer buffer))))))
 
@@ -1921,6 +1911,7 @@ erc--server-determine-join-display-context
         (cmd (erc-response.command parsed))
         (tgt (car (erc-response.command-args parsed)))
         (msg (erc-response.contents parsed)))
+    (defvar erc-minibuffer-ignored)
     (if (or (erc-ignored-user-p sender-spec)
             (erc-ignored-reply-p msg tgt proc))
         (when erc-minibuffer-ignored
@@ -1942,6 +1933,8 @@ erc--server-determine-join-display-context
         ;; Even worth checking for empty target here? (invalid anyway)
         (unless (or buffer noticep (string-empty-p tgt) (eq ?$ (aref tgt 0))
                     (erc-is-message-ctcp-and-not-action-p msg))
+          (defvar erc-receive-query-display)
+          (defvar erc-receive-query-display-defer)
           (if privp
               (when-let ((erc-join-buffer
                           (or (and (not erc-receive-query-display-defer)
@@ -1963,6 +1956,8 @@ erc--server-determine-join-display-context
             ;; at this point.
             (erc-update-channel-member (if privp nick tgt) nick nick
                                        privp nil nil nil nil nil host login nil nil t)
+            (defvar erc--cmem-from-nick-function)
+            (defvar erc-format-nick-function)
             (let ((cdata (funcall erc--cmem-from-nick-function
                                   (erc-downcase nick) sndr parsed)))
               (setq fnick (funcall erc-format-nick-function
@@ -1975,6 +1970,7 @@ erc--server-determine-join-display-context
                                             (match-string 1 msg)))))
          (t
           (setq erc-server-last-peers (cons nick (cdr erc-server-last-peers)))
+          (defvar erc-format-query-as-channel-p)
           (setq s (erc-format-privmessage
                    (or fnick nick) msg
                    ;; If buffer is a query buffer,
@@ -2479,6 +2475,7 @@ erc-server-322-message
 (define-erc-response-handler (401)
   "No such nick/channel." nil
   (let ((nick/channel (cadr (erc-response.command-args parsed))))
+    (defvar erc-whowas-on-nosuchnick)
     (when erc-whowas-on-nosuchnick
       (erc-log (format "cmd: WHOWAS: %s" nick/channel))
       (erc-server-send (format "WHOWAS %s 1" nick/channel)))
@@ -2579,6 +2576,8 @@ erc-server-322-message
   "Channel key needed." nil
   (erc-display-message parsed '(notice error) nil 's475
                        ?c (cadr (erc-response.command-args parsed)))
+  (defvar erc-prompt-for-channel-key)
+  (defvar erc--called-as-input-p)
   (when erc-prompt-for-channel-key
     (let ((channel (cadr (erc-response.command-args parsed)))
           (erc--called-as-input-p t)
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index ea4374da7b6..c5e4901c6d2 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -29,9 +29,7 @@
 (defvar erc--casemapping-rfc1459)
 (defvar erc--casemapping-rfc1459-strict)
 (defvar erc-channel-users)
-(defvar erc-dbuf)
 (defvar erc-insert-this)
-(defvar erc-log-p)
 (defvar erc-modules)
 (defvar erc-send-this)
 (defvar erc-server-process)
@@ -458,6 +456,7 @@ erc-log-aux
     (if session-buffer
         (progn
           (set-buffer session-buffer)
+          (defvar erc-dbuf)
           (if (not (and erc-dbuf (bufferp erc-dbuf) (buffer-live-p erc-dbuf)))
               (progn
                 (setq erc-dbuf (get-buffer-create
@@ -473,6 +472,9 @@ erc-log-aux
           (set-buffer cb))
       (message "ERC: ** %s" string))))
 
+(defvar erc-log-p nil
+  "When non-nil, generate debug messages in an \"*ERC-DEBUG*\" buffer.")
+
 (define-inline erc-log (string)
   "Logs STRING if logging is on (see `erc-log-p')."
   (inline-quote
diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index 82e881cb71c..9b0c74b518d 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -553,7 +553,9 @@ erc-fill--wrap-max-lull
 (defun erc-fill--wrap-continued-message-p ()
   "Return non-nil when the current speaker hasn't changed.
 That is, indicate whether the text just inserted is from the same
-sender as that of the previous \"PRIVMSG\"."
+sender as that of the previous \"PRIVMSG\".  As a side effect,
+advance `erc-fill--wrap-last-msg' unless the message has been
+marked as being ephemeral."
   (and
    (not (erc--check-msg-prop 'erc-ephemeral))
    (progn ; preserve blame for now, unprogn on next major change
diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index f168c90df65..694f56ed0d5 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -42,16 +42,12 @@
 
 (defvar erc--target)
 (defvar erc-insert-marker)
-(defvar erc-kill-buffer-hook)
-(defvar erc-kill-server-hook)
 (defvar erc-modules)
 (defvar erc-rename-buffers)
 (defvar erc-reuse-buffers)
 (defvar erc-server-announced-name)
 (defvar erc-server-connected)
-(defvar erc-server-parameters)
 (defvar erc-server-process)
-(defvar erc-session-server)
 
 (declare-function erc--get-isupport-entry "erc-backend" (key &optional single))
 (declare-function erc-buffer-filter "erc" (predicate &optional proc))
@@ -1229,6 +1225,8 @@ erc-determine-network
 server name and search for a match in `erc-networks-alist'."
   ;; The server made it easy for us and told us the name of the NETWORK
   (declare (obsolete "maybe see `erc-networks--determine'" "29.1"))
+  (defvar erc-server-parameters)
+  (defvar erc-session-server)
   (let ((network-name (cdr (assoc "NETWORK" erc-server-parameters))))
     (if network-name
 	(intern network-name)
@@ -1381,6 +1379,8 @@ erc-networks--copy-server-buffer-functions
 (defun erc-networks--copy-over-server-buffer-contents (existing name)
   "Kill off existing server buffer after copying its contents.
 Must be called from the replacement buffer."
+  (defvar erc-kill-buffer-hook)
+  (defvar erc-kill-server-hook)
   ;; ERC expects `erc-open' to be idempotent when setting up local
   ;; vars and other context properties for a new identity.  Thus, it's
   ;; unlikely we'll have to copy anything else over besides text.  And
@@ -1586,14 +1586,29 @@ erc-settings
   '((pals Libera.Chat ("kensanata" "shapr" "anti\\(fuchs\\|gone\\)"))
     (format-nick-function (Libera.Chat "#emacs") erc-format-@nick))
   "Experimental: Alist of configuration options.
+
+WARNING: this variable is a vestige from a long-abandoned
+experiment.  ERC may redefine it using the same name for any
+purpose at any time.
+
 The format is (VARNAME SCOPE VALUE) where
 VARNAME is a symbol identifying the configuration option,
 SCOPE is either a symbol which identifies an entry from
   `erc-networks-alist' or a list (NET TARGET) where NET is a network symbol and
   TARGET is a string identifying the channel/query target.
 VALUE is the options value.")
+(make-obsolete-variable 'erc-settings
+                        "temporarily deprecated for later repurposing" "30.1")
 
 (defun erc-get (var &optional net target)
+  "Retrieve configuration values from `erc-settings'.
+
+WARNING: this function is a non-functioning remnant from a
+long-abandoned experiment.  ERC may redefine it using the same
+name for any purpose at any time.
+
+\(fn &rest UNKNOWN)"
+  (declare (obsolete "temporarily deprecated for later repurposing" "30.1"))
   (let ((items erc-settings)
 	elt val)
     (while items
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 030d7787d8a..a42c50d91ff 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -191,10 +191,6 @@ erc--msg-prop-overrides
 already non-nil.")
 
 ;; Forward declarations
-(defvar tabbar--local-hlf)
-(defvar motif-version-string)
-(defvar gtk-version-string)
-
 (declare-function decoded-time-period "time-date" (time))
 (declare-function iso8601-parse-duration "iso8601" (string))
 (declare-function word-at-point "thingatpt" (&optional no-properties))
@@ -1138,7 +1134,13 @@ erc-user-information
 ;; Hooks
 
 (defgroup erc-hooks nil
-  "Hook variables for fancy customizations of ERC."
+  "Hooks for ERC.
+Users of the interactive client should be aware that many of
+these hooks have names predating the modern convention of
+conveying abnormality via the \"-function\" suffix.  Users should
+likewise be aware that built-in and third-party modules use these
+hooks as well, and some of their variables may be buffer-local in
+particular sessions and/or `let'-bound for spells."
   :group 'erc)
 
 (defcustom erc-mode-hook nil
@@ -1148,9 +1150,8 @@ erc-mode-hook
   :options '(erc-add-scroll-to-bottom))
 
 (defcustom erc-timer-hook nil
-  "Put functions which should get called more or less periodically here.
-The idea is that servers always play ping pong with the client, and so there
-is no need for any idle-timer games with Emacs."
+  "Abnormal hook run after each response handler.
+Called with a float returned from `erc-current-time'."
   :group 'erc-hooks
   :type 'hook)
 
@@ -1450,9 +1451,8 @@ erc-nick-msg-face
 
 ;; Debugging support
 
-(defvar erc-log-p nil
-  "When set to t, generate debug messages in a separate debug buffer.")
-
+;; FIXME if this variable plays some role, indicate that here.
+;; Otherwise, deprecate.
 (defvar erc-debug-log-file (expand-file-name "ERC.debug")
   "Debug log file name.")
 
@@ -4969,9 +4969,11 @@ erc-cmd-SV
                             system-configuration
                             (concat
                              (cond ((featurep 'motif)
+                                    (defvar motif-version-string)
                                     (concat ", " (substring
                                                   motif-version-string 4)))
                                    ((featurep 'gtk)
+                                    (defvar gtk-version-string)
                                     (concat ", GTK+ Version "
                                             gtk-version-string))
                                    ((featurep 'x-toolkit) ", X toolkit")
@@ -8288,8 +8290,13 @@ erc-header-line-format
   :type '(choice (const :tag "Disabled" nil)
                  string))
 
+;; This should optionally support the built-in `tab-bar'.
 (defcustom erc-header-line-uses-tabbar-p nil
-  "Use tabbar mode instead of the header line to display the header."
+  "Use `tabbar-mode' integration instead of the header line.
+This concerns a historical integration with the external library
+`tabbar' <https://www.emacswiki.org/emacs/tabbar.el>, which
+shouldn't be confused with the built-in `tab-bar' described in
+Info node `(emacs) Tab Bars'."
   :group 'erc-mode-line-and-header
   :type 'boolean)
 
@@ -8496,7 +8503,8 @@ erc-update-mode-line-buffer
                         (format-spec erc-header-line-format spec)
                       nil)))
         (cond (erc-header-line-uses-tabbar-p
-               (setq-local tabbar--local-hlf header-line-format)
+               (when (boundp 'tabbar--local-hlf)
+                 (setq-local tabbar--local-hlf header-line-format))
                (kill-local-variable 'header-line-format))
               ((null header)
                (setq header-line-format nil))
diff --git a/test/lisp/erc/erc-scenarios-base-reconnect.el b/test/lisp/erc/erc-scenarios-base-reconnect.el
index 7bd16d1ed14..163521f4a7b 100644
--- a/test/lisp/erc/erc-scenarios-base-reconnect.el
+++ b/test/lisp/erc/erc-scenarios-base-reconnect.el
@@ -171,7 +171,7 @@ erc-scenarios-base-cancel-reconnect
           (funcall expect 2 "Canceled")
           (funcall expect 3 "Opening connection")
           (funcall expect 2 "Password incorrect")
-          (funcall expect 2 "Connection failed!")
+          (funcall expect 10 "Connection failed!")
           (funcall expect 2 "Re-establishing connection"))
         (ert-info ("Explicitly cancel timer")
           (erc-cmd-RECONNECT "cancel")
diff --git a/test/lisp/erc/erc-scenarios-base-renick.el b/test/lisp/erc/erc-scenarios-base-renick.el
index 5a87e5871f7..689f962812a 100644
--- a/test/lisp/erc/erc-scenarios-base-renick.el
+++ b/test/lisp/erc/erc-scenarios-base-renick.el
@@ -267,7 +267,7 @@ erc-scenarios-base-renick-queries-bouncer
 
     (ert-info ("Sync convo for rando@foonet")
       (with-current-buffer "rando@foonet"
-        (funcall expect 1 "u are dumb")
+        (funcall expect 10 "u are dumb")
         (erc-scenarios-common-say "not so")))
 
     (ert-info ("Sync convo for rando@barnet")
diff --git a/test/lisp/erc/resources/join/network-id/foonet-again.eld b/test/lisp/erc/resources/join/network-id/foonet-again.eld
index b230eff27c7..a8b8a52f87a 100644
--- a/test/lisp/erc/resources/join/network-id/foonet-again.eld
+++ b/test/lisp/erc/resources/join/network-id/foonet-again.eld
@@ -43,4 +43,4 @@
  (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: But we are spirits of another sort.")
  (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it."))
 
-((linger 6 LINGER))
+((linger 30 LINGER))
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-5.6-Double-hyphenate-internal-ERC-5.6-text-props.patch --]
[-- Type: text/x-patch, Size: 95055 bytes --]

From 218a4f1f4b405fe5c7d934948bdc12a9ea0f2baf Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 1 Dec 2023 22:30:04 -0800
Subject: [PATCH 03/11] [5.6] Double hyphenate internal ERC 5.6 text props

* lisp/erc/erc-fill.el (erc-fill, erc-fill-static,
erc-fill--wrap-continued-message-p, erc-fill-wrap,
erc-fill--wrap-rejigger-region): Add second hyphen to "msg prop" text
properties.
* lisp/erc/erc-goodies.el (erc--command-indicator-display): Rename
`erc-msg' to `erc--msg'.
* lisp/erc/erc-stamp.el (erc-stamp--current-time, erc-add-timestamp,
erc-stamp-prefix-log-filter, erc-stamp--lr-date-on-pre-modify,
erc-munge-invisibility-spec, erc-stamp--add-csf-on-post-modify,
erc-stamp--on-clear-message, erc-echo-timestamp, erc--echo-ts-csf):
Rename "msg props" with second hyphen.
* lisp/erc/erc-track.el (erc-track--skipped-msgs,
erc-track-modified-channels): Rename "msg prop" text properties with
second hyphen.
* lisp/erc/erc.el (erc--msg-props): Update doc with double-hyphenated
"msg prop" names.
(erc--send-action-display erc--get-inserted-msg-bounds,
erc--traverse-inserted, erc-insert-line, erc-display-line,
erc--ranked-properties, erc-display-message, erc--get-speaker-bounds,
erc-process-ctcp-query, erc-display-msg): Update all "msg prop" names
to have two hyphens.
* test/lisp/erc/erc-scenarios-display-message.el
(erc-scenarios-display-message--multibuf): Double hyphenate "msg prop"
text properties.
* test/lisp/erc/erc-scenarios-match.el
(erc-scenarios-match--hide-fools/stamp-both/fill-wrap,
erc-scenarios-match--hide-fools/stamp-both/fill-wrap/speak,
erc-scenarios-match--stamp-both-invisible-fill-static): Update "msg
prop" names.
* test/lisp/erc/erc-scenarios-stamp.el
(erc-scenarios-stamp--on-post-modify,
erc-scenarios-stamp--left/display-margin-mode,
erc-scenarios-stamp--legacy-date-stamps,
erc-scenarios-stamp--on-insert-modify,
erc-scenarios-stamp--date-mode/left-and-right): Add second hyphen to
all "msg props".
* test/lisp/erc/erc-stamp-tests.el (erc-echo-timestamp): Rename "msg
prop".
* test/lisp/erc/erc-tests.el (erc--get-inserted-msg-bounds,
erc--delete-inserted-message, erc--order-text-properties-from-hash,
erc--route-insertion): Rename "msg props" with second hyphen.
(Bug#60936)
; * test/lisp/erc/resources/fill/snapshots/merge-01-start.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/merge-02-right.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld:
; Add second hyphen to msg props.
; * test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld:
; Add second hyphen to msg props.
---
 lisp/erc/erc-fill.el                          | 22 ++++----
 lisp/erc/erc-goodies.el                       |  2 +-
 lisp/erc/erc-stamp.el                         | 24 ++++-----
 lisp/erc/erc-track.el                         |  4 +-
 lisp/erc/erc.el                               | 50 +++++++++----------
 .../lisp/erc/erc-scenarios-display-message.el |  4 +-
 test/lisp/erc/erc-scenarios-match.el          | 14 +++---
 test/lisp/erc/erc-scenarios-stamp.el          | 18 +++----
 test/lisp/erc/erc-stamp-tests.el              |  2 +-
 test/lisp/erc/erc-tests.el                    | 20 ++++----
 .../fill/snapshots/merge-01-start.eld         |  2 +-
 .../fill/snapshots/merge-02-right.eld         |  2 +-
 .../fill/snapshots/merge-wrap-01.eld          |  2 +-
 .../merge-wrap-indicator-post-01.eld          |  2 +-
 .../snapshots/merge-wrap-indicator-pre-01.eld |  2 +-
 .../fill/snapshots/monospace-01-start.eld     |  2 +-
 .../fill/snapshots/monospace-02-right.eld     |  2 +-
 .../fill/snapshots/monospace-03-left.eld      |  2 +-
 .../fill/snapshots/monospace-04-reset.eld     |  2 +-
 .../fill/snapshots/spacing-01-mono.eld        |  2 +-
 .../fill/snapshots/stamps-left-01.eld         |  2 +-
 21 files changed, 91 insertions(+), 91 deletions(-)

diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index 9b0c74b518d..5434d9af966 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -177,10 +177,10 @@ erc-fill
           (when-let ((erc-fill-line-spacing)
                      (p (point-min)))
             (widen)
-            (when (or (erc--check-msg-prop 'erc-msg 'msg)
+            (when (or (erc--check-msg-prop 'erc--msg 'msg)
                       (and-let* ((m (save-excursion
                                       (forward-line -1)
-                                      (erc--get-inserted-msg-prop 'erc-msg))))
+                                      (erc--get-inserted-msg-prop 'erc--msg))))
                         (eq 'msg m)))
               (put-text-property (1- p) p
                                  'line-spacing erc-fill-line-spacing))))))))
@@ -190,7 +190,7 @@ erc-fill-static
   (save-restriction
     (goto-char (point-min))
     (when-let (((looking-at "^\\(\\S-+\\)"))
-               ((not (erc--check-msg-prop 'erc-msg 'datestamp)))
+               ((not (erc--check-msg-prop 'erc--msg 'datestamp)))
                (nick (match-string 1)))
       (progn
         (let ((fill-column (- erc-fill-column (erc-timestamp-offset)))
@@ -557,7 +557,7 @@ erc-fill--wrap-continued-message-p
 advance `erc-fill--wrap-last-msg' unless the message has been
 marked as being ephemeral."
   (and
-   (not (erc--check-msg-prop 'erc-ephemeral))
+   (not (erc--check-msg-prop 'erc--ephemeral))
    (progn ; preserve blame for now, unprogn on next major change
      (prog1
          (and-let*
@@ -568,12 +568,12 @@ erc-fill--wrap-continued-message-p
               (props (save-restriction
                        (widen)
                        (and-let*
-                           (((eq 'msg (get-text-property m 'erc-msg)))
-                            ((not (eq (get-text-property m 'erc-ctcp)
+                           (((eq 'msg (get-text-property m 'erc--msg)))
+                            ((not (eq (get-text-property m 'erc--ctcp)
                                       'ACTION)))
                             ((not (invisible-p m)))
                             (spr (next-single-property-change m 'erc-speaker)))
-                         (cons (get-text-property m 'erc-ts)
+                         (cons (get-text-property m 'erc--ts)
                                (get-text-property spr 'erc-speaker)))))
               (ts (pop props))
               (props)
@@ -582,7 +582,7 @@ erc-fill--wrap-continued-message-p
                             erc-fill--wrap-max-lull))
               ;; Assume presence of leading angle bracket or hyphen.
               (speaker (next-single-property-change (point-min) 'erc-speaker))
-              ((not (erc--check-msg-prop 'erc-ctcp 'ACTION)))
+              ((not (erc--check-msg-prop 'erc--ctcp 'ACTION)))
               (nick (get-text-property speaker 'erc-speaker))
               ((erc-nick-equal-p props nick))))
        (set-marker erc-fill--wrap-last-msg (point-min))))))
@@ -668,12 +668,12 @@ erc-fill-wrap
     (goto-char (point-min))
     (let ((len (or (and erc-fill--wrap-length-function
                         (funcall erc-fill--wrap-length-function))
-                   (and-let* ((msg-prop (erc--check-msg-prop 'erc-msg))
+                   (and-let* ((msg-prop (erc--check-msg-prop 'erc--msg))
                               ((not (eq msg-prop 'unknown))))
                      (when-let ((e (erc--get-speaker-bounds))
                                 (b (pop e))
                                 ((or erc-fill--wrap-action-dedent-p
-                                     (not (erc--check-msg-prop 'erc-ctcp
+                                     (not (erc--check-msg-prop 'erc--ctcp
                                                                'ACTION)))))
                        (goto-char e))
                      (skip-syntax-forward "^-")
@@ -755,7 +755,7 @@ erc-fill--wrap-rejigger-region
                       (field-beginning beg)
                     beg))
              (erc--msg-props (map-into (text-properties-at pos) 'hash-table))
-             (erc-stamp--current-time (gethash 'erc-ts erc--msg-props)))
+             (erc-stamp--current-time (gethash 'erc--ts erc--msg-props)))
         (save-restriction
           (narrow-to-region beg (1+ end))
           (let ((erc-fill--wrap-last-msg erc-fill--wrap-rejigger-last-message))
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 6c8ec567bd9..e10f047b187 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -578,7 +578,7 @@ erc--command-indicator-display
       (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)
+                                  (map-into `((erc--msg . slash-cmd)
                                               ,@(reverse ovs))
                                             'hash-table)))))
         (when-let ((string (erc-command-indicator))
diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index e6a8f36c332..a6efa3b5151 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -212,7 +212,7 @@ erc-stamp--current-time
 
 (cl-defgeneric erc-stamp--current-time ()
   "Return a lisp time object to associate with an IRC message.
-This becomes the message's `erc-ts' text property."
+This becomes the message's `erc--ts' text property."
   (erc-compat--current-lisp-time))
 
 (cl-defmethod erc-stamp--current-time :around ()
@@ -249,10 +249,10 @@ erc-add-timestamp
             ;; FIXME on major version bump, make this `erc-' prefixed.
             (if invisible `(timestamp ,@(ensure-list invisible)) 'timestamp))
            (skipp (or (and erc-stamp--skip-when-invisible invisible)
-                      (erc--check-msg-prop 'erc-ephemeral)))
+                      (erc--check-msg-prop 'erc--ephemeral)))
            (erc-stamp--current-time ct))
       (when erc--msg-props
-        (puthash 'erc-ts ct erc--msg-props))
+        (puthash 'erc--ts ct erc--msg-props))
       (unless skipp
         (funcall erc-insert-timestamp-function
                  (erc-format-timestamp ct erc-timestamp-format)))
@@ -270,7 +270,7 @@ erc-add-timestamp
 			   ;; be different on different entries (bug#22700).
 			   (list 'cursor-sensor-functions
                                  ;; Regions are no longer contiguous ^
-                                 '(erc--echo-ts-csf) 'erc-ts ct))))))
+                                 '(erc--echo-ts-csf) 'erc--ts ct))))))
 
 (defvar-local erc-timestamp-last-window-width nil
   "The width of the last window that showed the current buffer.
@@ -403,7 +403,7 @@ erc-stamp-prefix-log-filter
                    ;; Skip a line that's just a timestamp.
                    ((> beg (point))))
           (delete-region beg (1+ end)))
-        (when-let (time (erc--get-inserted-msg-prop 'erc-ts))
+        (when-let (time (erc--get-inserted-msg-prop 'erc--ts))
           (insert (format-time-string "[%H:%M:%S] " time)))
         (zerop (forward-line))))
   "")
@@ -711,8 +711,8 @@ erc-stamp--lr-date-on-pre-modify
         (setq erc-timestamp-last-inserted-left nil)
         (let* ((aligned (erc-stamp--time-as-day ct))
                (erc-stamp--current-time aligned)
-               ;; Forget current `erc-cmd', etc.
-               (erc--msg-props (map-into `((erc-msg . datestamp))
+               ;; Forget current `erc--cmd', etc.
+               (erc--msg-props (map-into `((erc--msg . datestamp))
                                          'hash-table))
                (erc-timestamp-last-inserted-left rendered)
                erc-timestamp-format erc-away-timestamp-format)
@@ -867,7 +867,7 @@ erc-munge-invisibility-spec
             erc-stamp--csf-props-updated-p nil)
           (unless erc-stamp--csf-props-updated-p
             (setq erc-stamp--csf-props-updated-p t)
-            (let ((erc--msg-props (map-into '((erc-ts . t)) 'hash-table)))
+            (let ((erc--msg-props (map-into '((erc--ts . t)) 'hash-table)))
               (with-silent-modifications
                 (erc--traverse-inserted
                  (point-min) erc-insert-marker
@@ -889,7 +889,7 @@ erc-munge-invisibility-spec
 
 (defun erc-stamp--add-csf-on-post-modify ()
   "Add `cursor-sensor-functions' to narrowed buffer."
-  (when (erc--check-msg-prop 'erc-ts)
+  (when (erc--check-msg-prop 'erc--ts)
     (put-text-property (point-min) (1- (point-max))
                        'cursor-sensor-functions '(erc--echo-ts-csf))))
 
@@ -940,7 +940,7 @@ erc-stamp--last-stamp
 (defun erc-stamp--on-clear-message (&rest _)
   "Return `dont-clear-message' when operating inside the same stamp."
   (and erc-stamp--last-stamp erc-echo-timestamps
-       (eq (erc--get-inserted-msg-prop 'erc-ts) erc-stamp--last-stamp)
+       (eq (erc--get-inserted-msg-prop 'erc--ts) erc-stamp--last-stamp)
        'dont-clear-message))
 
 (defun erc-echo-timestamp (dir stamp &optional zone)
@@ -950,7 +950,7 @@ erc-echo-timestamp
 interpret a \"raw\" prefix as UTC.  To specify a zone for use
 with the option `erc-echo-timestamps', see the companion option
 `erc-echo-timestamp-zone'."
-  (interactive (list nil (erc--get-inserted-msg-prop 'erc-ts)
+  (interactive (list nil (erc--get-inserted-msg-prop 'erc--ts)
                      (pcase current-prefix-arg
                        ((and (pred numberp) v)
                         (if (<= (abs v) 14) (* v 3600) v))
@@ -964,7 +964,7 @@ erc-echo-timestamp
       (setq erc-stamp--last-stamp nil))))
 
 (defun erc--echo-ts-csf (_window _before dir)
-  (erc-echo-timestamp dir (erc--get-inserted-msg-prop 'erc-ts)))
+  (erc-echo-timestamp dir (erc--get-inserted-msg-prop 'erc--ts)))
 
 (defun erc-stamp--update-saved-position (&rest _)
   (remove-hook 'erc-stamp--insert-date-hook
diff --git a/lisp/erc/erc-track.el b/lisp/erc/erc-track.el
index a36b781e04d..7dc4fe754cd 100644
--- a/lisp/erc/erc-track.el
+++ b/lisp/erc/erc-track.el
@@ -786,7 +786,7 @@ erc-track-select-mode-line-face
         choice))))
 
 (defvar erc-track--skipped-msgs '(datestamp)
-  "Values of `erc-msg' text prop to ignore.")
+  "Values of `erc--msg' text prop to ignore.")
 
 (defun erc-track-modified-channels ()
   "Hook function for `erc-insert-post-hook'.
@@ -806,7 +806,7 @@ erc-track-modified-channels
                                                  erc-track-exclude-types)
                         ;; Skip certain non-server-sent messages.
                         (and (not parsed)
-                             (erc--check-msg-prop 'erc-msg
+                             (erc--check-msg-prop 'erc--msg
                                                   erc-track--skipped-msgs))))))
 	;; If the active buffer is not visible (not shown in a
 	;; window), and not to be excluded, determine the kinds of
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a42c50d91ff..c68c74467b8 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -154,26 +154,26 @@ erc--msg-props
 their markers accordingly.  The following properties have meaning
 as of ERC 5.6:
 
- - `erc-msg': a symbol, guaranteed present; values include:
+ - `erc--msg': a symbol, guaranteed present; values include:
    `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--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)
 
- - `erc-ctcp': a CTCP command, like `ACTION'
+ - `erc--ctcp': a CTCP command, like `ACTION'
 
- - `erc-ts': a timestamp, possibly provided by the server; as of
+ - `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
 
- - `erc-ephemeral': a symbol prefixed by or matching a 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
@@ -3004,7 +3004,7 @@ erc-send-action
 ;; Sending and displaying are provided separately to afford modules
 ;; more flexibility, e.g., to forgo displaying on the way out when
 ;; expecting the server to echo messages back and/or to associate
-;; outgoing messages with IDs generated for `erc-ephemeral'
+;; outgoing messages with IDs generated for `erc--ephemeral'
 ;; placeholders.
 (defun erc--send-action-perform-ctcp (target string force)
   "Send STRING to TARGET, possibly immediately, with FORCE."
@@ -3013,8 +3013,8 @@ erc--send-action-perform-ctcp
 (defun erc--send-action-display (string)
   "Display STRING as an outgoing \"CTCP ACTION\" message."
   ;; Allow hooks acting on inserted PRIVMSG and NOTICES to process us.
-  (let ((erc--msg-prop-overrides `((erc-msg . msg)
-                                   (erc-ctcp . ACTION)
+  (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
@@ -3142,20 +3142,20 @@ erc--get-inserted-msg-bounds
 POINT, search from POINT instead of `point'."
   ;; TODO add edebug spec.
   `(let* ((point ,(or point '(point)))
-          (at-start-p (get-text-property point 'erc-msg)))
+          (at-start-p (get-text-property point 'erc--msg)))
      (and-let*
          (,@(and (member only '(nil beg 'beg))
                  '((b (or (and at-start-p point)
                           (and-let*
                               ((p (previous-single-property-change point
-                                                                   'erc-msg)))
+                                                                   'erc--msg)))
                             (if (= p (1- point))
-                                (if (get-text-property p 'erc-msg) p (1- p))
+                                (if (get-text-property p 'erc--msg) p (1- p))
                               (1- p)))))))
           ,@(and (member only '(nil end 'end))
                  '((e (1- (next-single-property-change
                            (if at-start-p (1+ point) point)
-                           'erc-msg nil erc-insert-marker))))))
+                           'erc--msg nil erc-insert-marker))))))
        ,(pcase only
           ('(quote beg) 'b)
           ('(quote end) 'e)
@@ -3184,12 +3184,12 @@ erc--traverse-inserted
     (set-marker end (min erc-insert-marker end)))
   (save-excursion
     (goto-char beg)
-    (let ((b (if (get-text-property (point) 'erc-msg)
+    (let ((b (if (get-text-property (point) 'erc--msg)
                  (point)
-               (next-single-property-change (point) 'erc-msg nil end))))
+               (next-single-property-change (point) 'erc--msg nil end))))
       (while-let ((b)
                   ((< b end))
-                  (e (next-single-property-change (1+ b) 'erc-msg nil end)))
+                  (e (next-single-property-change (1+ b) 'erc--msg nil end)))
         (save-restriction
           (narrow-to-region b e)
           (funcall fn))
@@ -3267,7 +3267,7 @@ erc-insert-line
                   (let ((props (if erc--msg-props
                                    (erc--order-text-properties-from-hash
                                     erc--msg-props)
-                                 '(erc-msg unknown))))
+                                 '(erc--msg unknown))))
                     (add-text-properties (point-min) (1+ (point-min)) props)))
                 (erc--refresh-prompt)))))
         (run-hooks 'erc-insert-done-hook)
@@ -3340,8 +3340,8 @@ erc-display-line
 being equivalent to a `erc-display-message' TYPE of `notice'."
   (let ((erc--msg-prop-overrides erc--msg-prop-overrides))
     (when (eq 'erc-notice-face (get-text-property 0 'font-lock-face string))
-      (unless (assq 'erc-msg erc--msg-prop-overrides)
-        (push '(erc-msg . notice) erc--msg-prop-overrides)))
+      (unless (assq 'erc--msg erc--msg-prop-overrides)
+        (push '(erc--msg . notice) erc--msg-prop-overrides)))
     (erc-display-message nil nil buffer string)))
 
 (defvar erc--merge-text-properties-p nil
@@ -3458,7 +3458,7 @@ erc--delete-inserted-message
              (substring (delete-and-extract-region (1- (point)) (1+ end))
                         -1))))))))
 
-(defvar erc--ranked-properties '(erc-msg erc-ts erc-cmd))
+(defvar erc--ranked-properties '(erc--msg erc--ts erc--cmd))
 
 (defun erc--order-text-properties-from-hash (table)
   "Return a plist of text props from items in TABLE.
@@ -3732,7 +3732,7 @@ erc-display-message
              (let ((table (make-hash-table :size 5))
                    (cmd (and parsed (erc--get-eq-comparable-cmd
                                      (erc-response.command parsed)))))
-               (puthash 'erc-msg
+               (puthash 'erc--msg
                         (cond ((and msg (symbolp msg)) msg)
                               ((and cmd (memq cmd '(PRIVMSG NOTICE)) 'msg))
                               (type (pcase type
@@ -3744,7 +3744,7 @@ erc-display-message
                               (t 'unknown))
                         table)
                (when cmd
-                 (puthash 'erc-cmd cmd table))
+                 (puthash 'erc--cmd cmd table))
                (and-let* ((ovs erc--msg-prop-overrides))
                  (pcase-dolist (`(,k . ,v) (reverse ovs))
                    (puthash k v table)))
@@ -5744,7 +5744,7 @@ erc-is-message-ctcp-and-not-action-p
 (defun erc--get-speaker-bounds ()
   "Return the bounds of `erc-speaker' text property when present.
 Assume buffer is narrowed to the confines of an inserted message."
-  (and-let* (((erc--check-msg-prop 'erc-msg 'msg))
+  (and-let* (((erc--check-msg-prop 'erc--msg 'msg))
              (beg (text-property-not-all (point-min) (point-max)
                                          'erc-speaker nil)))
     (cons beg (next-single-property-change beg 'erc-speaker))))
@@ -6074,8 +6074,8 @@ erc-process-ctcp-query
         (while queries
           (let* ((type (upcase (car (split-string (car queries)))))
                  (hook (intern-soft (concat "erc-ctcp-query-" type "-hook")))
-                 (erc--msg-prop-overrides `((erc-msg . msg)
-                                            (erc-ctcp . ,(intern type))
+                 (erc--msg-prop-overrides `((erc--msg . msg)
+                                            (erc--ctcp . ,(intern type))
                                             ,@erc--msg-prop-overrides)))
             (if (and hook (boundp hook))
                 (if (string-equal type "ACTION")
@@ -7521,7 +7521,7 @@ erc-display-msg
       (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 . msg) ,@(reverse ovs))
+                                  (map-into `((erc--msg . msg) ,@(reverse ovs))
                                             'hash-table))))
             beg)
         (insert (erc-format-my-nick))
diff --git a/test/lisp/erc/erc-scenarios-display-message.el b/test/lisp/erc/erc-scenarios-display-message.el
index c7e0c2fc17a..91b82889f3e 100644
--- a/test/lisp/erc/erc-scenarios-display-message.el
+++ b/test/lisp/erc/erc-scenarios-display-message.el
@@ -50,12 +50,12 @@ erc-scenarios-display-message--multibuf
       (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "dummy"))
         (funcall expect 10 "<dummy> hi")
         (funcall expect 10 "*** dummy (~u@rdjcgiwfuwqmc.irc) has quit")
-        (should (eq 'QUIT (get-text-property (match-beginning 0) 'erc-msg)))))
+        (should (eq 'QUIT (get-text-property (match-beginning 0) 'erc--msg)))))
 
     (ert-info ("Dummy's QUIT notice in #chan contains metadata props")
       (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
         (funcall expect 10 "*** dummy (~u@rdjcgiwfuwqmc.irc) has quit")
-        (should (eq 'QUIT (get-text-property (match-beginning 0) 'erc-msg)))))
+        (should (eq 'QUIT (get-text-property (match-beginning 0) 'erc--msg)))))
 
     (with-current-buffer "foonet"
       (erc-cmd-QUIT ""))))
diff --git a/test/lisp/erc/erc-scenarios-match.el b/test/lisp/erc/erc-scenarios-match.el
index 17f7649566e..0eed1853879 100644
--- a/test/lisp/erc/erc-scenarios-match.el
+++ b/test/lisp/erc/erc-scenarios-match.el
@@ -304,9 +304,9 @@ erc-scenarios-match--hide-fools/stamp-both/fill-wrap
                (should (= mend (field-end right-stamp)))
                (should (eq (field-at-pos (1- mend)) 'erc-timestamp))))
 
-           ;; The `erc-ts' property is present in prop stack.
-           (should (get-text-property (pos-bol) 'erc-ts))
-           (should-not (next-single-property-change (1+ (pos-bol)) 'erc-ts))
+           ;; The `erc--ts' property is present in prop stack.
+           (should (get-text-property (pos-bol) 'erc--ts))
+           (should-not (next-single-property-change (1+ (pos-bol)) 'erc--ts))
 
            ;; Line ending has the `invisible' property `match-fools'.
            (should (eq (get-text-property mbeg 'invisible) 'match-fools))
@@ -413,7 +413,7 @@ erc-scenarios-match--hide-fools/stamp-both/fill-wrap/speak
         (should-not (equal "" (get-text-property (pos-bol) 'display)))
 
         ;; No remaining meta-data positions, no more timestamps.
-        (should-not (next-single-property-change (1+ (pos-bol)) 'erc-ts))
+        (should-not (next-single-property-change (1+ (pos-bol)) 'erc--ts))
         ;; No remaining invisible messages.
         (should-not (text-property-not-all (pos-bol) erc-insert-marker
                                            'invisible nil))
@@ -456,10 +456,10 @@ erc-scenarios-match--stamp-both-invisible-fill-static
              (should (eq (field-at-pos (field-end mbeg)) 'erc-timestamp))
              (should (eq (field-at-pos (1- mend)) 'erc-timestamp)))
 
-           ;; The `erc-ts' property is present in the message's
+           ;; The `erc--ts' property is present in the message's
            ;; width 1 prop collection at its first char.
-           (should (get-text-property (pos-bol) 'erc-ts))
-           (should-not (next-single-property-change (1+ (pos-bol)) 'erc-ts))
+           (should (get-text-property (pos-bol) 'erc--ts))
+           (should-not (next-single-property-change (1+ (pos-bol)) 'erc--ts))
 
            ;; Line ending has the `invisible' property `match-fools'.
            (should (= (char-after mend) ?\n))
diff --git a/test/lisp/erc/erc-scenarios-stamp.el b/test/lisp/erc/erc-scenarios-stamp.el
index 49307dd228a..68769e203ff 100644
--- a/test/lisp/erc/erc-scenarios-stamp.el
+++ b/test/lisp/erc/erc-scenarios-stamp.el
@@ -29,7 +29,7 @@
 (defvar erc-scenarios-stamp--user-marker nil)
 
 (defun erc-scenarios-stamp--on-post-modify ()
-  (when-let (((erc--check-msg-prop 'erc-cmd 4)))
+  (when-let (((erc--check-msg-prop 'erc--cmd 4)))
     (set-marker erc-scenarios-stamp--user-marker (point-max))
     (ert-info ("User marker correctly placed at `erc-insert-marker'")
       (should (= ?\n (char-before erc-scenarios-stamp--user-marker)))
@@ -68,8 +68,8 @@ erc-scenarios-stamp--left/display-margin-mode
         (ert-info ("Stamps appear in left margin and are invisible")
           (should (eq 'erc-timestamp (field-at-pos (pos-bol))))
           (should (= (pos-bol) (field-beginning (pos-bol))))
-          (should (eq 'msg (get-text-property (pos-bol) 'erc-msg)))
-          (should (eq 'NOTICE (get-text-property (pos-bol) 'erc-cmd)))
+          (should (eq 'msg (get-text-property (pos-bol) 'erc--msg)))
+          (should (eq 'NOTICE (get-text-property (pos-bol) 'erc--cmd)))
           (should (= ?- (char-after (field-end (pos-bol)))))
           (should (equal (get-text-property (1+ (field-end (pos-bol)))
                                             'erc-speaker)
@@ -104,14 +104,14 @@ erc-scenarios-stamp--legacy-date-stamps
           (funcall expect 5 "Opening connection")
           (goto-char (1- (match-beginning 0)))
           (should (eq 'erc-timestamp (field-at-pos (point))))
-          (should (eq 'unknown (erc--get-inserted-msg-prop 'erc-msg)))
+          (should (eq 'unknown (erc--get-inserted-msg-prop 'erc--msg)))
           ;; Force redraw of date stamp.
           (setq erc-timestamp-last-inserted-left nil)
 
           (funcall expect 5 "This server is in debug mode")
           (while (and (zerop (forward-line -1))
                       (not (eq 'erc-timestamp (field-at-pos (point))))))
-          (should (erc--get-inserted-msg-prop 'erc-cmd)))))))
+          (should (erc--get-inserted-msg-prop 'erc--cmd)))))))
 
 ;; This user-owned hook member places a marker on the first message in
 ;; a buffer.  Inserting a date stamp in front of it shouldn't move the
@@ -125,18 +125,18 @@ erc-scenarios-stamp--on-insert-modify
 
   ;; Sometime after the first message ("Opening connection.."), assert
   ;; that the marker we just placed hasn't moved.
-  (when (erc--check-msg-prop 'erc-cmd 2)
+  (when (erc--check-msg-prop 'erc--cmd 2)
     (save-restriction
       (widen)
       (ert-info ("Date stamp preserves opening user marker")
         (goto-char erc-scenarios-stamp--user-marker)
         (should-not (eq 'erc-timestamp (field-at-pos (point))))
         (should (looking-at "Opening"))
-        (should (eq 'unknown (get-text-property (point) 'erc-msg))))))
+        (should (eq 'unknown (get-text-property (point) 'erc--msg))))))
 
   ;; On 003 ("*** This server was created on"), clear state to force a
   ;; new date stamp on the next message.
-  (when (erc--check-msg-prop 'erc-cmd 3)
+  (when (erc--check-msg-prop 'erc--cmd 3)
     (setq erc-timestamp-last-inserted-left nil)
     (set-marker erc-scenarios-stamp--user-marker erc-insert-marker)))
 
@@ -174,7 +174,7 @@ erc-scenarios-stamp--date-mode/left-and-right
           (goto-char erc-scenarios-stamp--user-marker)
           (should-not (eq 'erc-timestamp (field-at-pos (point))))
           (should (looking-at (rx "*** irc.foonet.org oragono")))
-          (should (eq 's004 (get-text-property (point) 'erc-msg))))
+          (should (eq 's004 (get-text-property (point) 'erc--msg))))
 
         (funcall expect 5 "This server is in debug mode")))))
 
diff --git a/test/lisp/erc/erc-stamp-tests.el b/test/lisp/erc/erc-stamp-tests.el
index cc61d599387..fd2e7000c0e 100644
--- a/test/lisp/erc/erc-stamp-tests.el
+++ b/test/lisp/erc/erc-stamp-tests.el
@@ -279,7 +279,7 @@ erc-echo-timestamp
 
   (should-not erc-echo-timestamps)
   (should-not erc-stamp--last-stamp)
-  (insert (propertize "a" 'erc-ts 433483200 'erc-msg 'msg) "bc")
+  (insert (propertize "a" 'erc--ts 433483200 'erc--msg 'msg) "bc")
   (goto-char (point-min))
   (let ((inhibit-message t)
         (erc-echo-timestamp-format "%Y-%m-%d %H:%M:%S %Z")
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 49d500fadea..b8ebc23e686 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1738,7 +1738,7 @@ erc--get-inserted-msg-bounds
                                    :command "PRIVMSG"
                                    :command-args (list "#chan" "hi")
                                    :contents "hi"))
-        (erc--msg-prop-overrides '((erc-ts . 0))))
+        (erc--msg-prop-overrides '((erc--ts . 0))))
     (erc-display-message parsed nil (current-buffer)
                          (erc-format-privmessage "bob" "hi" nil t)))
   (goto-char 3)
@@ -1785,7 +1785,7 @@ erc--delete-inserted-message
   ;; Put unique invisible properties on the line endings.
   (erc-display-message nil 'notice nil "one")
   (put-text-property (1- erc-insert-marker) erc-insert-marker 'invisible 'a)
-  (let ((erc--msg-prop-overrides '((erc-msg . datestamp) (erc-ts . 0))))
+  (let ((erc--msg-prop-overrides '((erc--msg . datestamp) (erc--ts . 0))))
     (erc-display-message nil nil nil
                          (propertize "\n[date]" 'field 'erc-timestamp)))
   (put-text-property (1- erc-insert-marker) erc-insert-marker 'invisible 'b)
@@ -1794,7 +1794,7 @@ erc--delete-inserted-message
   (ert-info ("Date stamp deleted cleanly")
     (goto-char 11)
     (should (looking-at (rx "\n[date]")))
-    (should (eq 'datestamp (get-text-property (point) 'erc-msg)))
+    (should (eq 'datestamp (get-text-property (point) 'erc--msg)))
     (should (eq (point) (field-beginning (1+ (point)))))
 
     (erc--delete-inserted-message (point))
@@ -1855,19 +1855,19 @@ erc--delete-inserted-message
 
 (ert-deftest erc--order-text-properties-from-hash ()
   (let ((table (map-into '((a . 1)
-                           (erc-ts . 0)
-                           (erc-msg . s005)
+                           (erc--ts . 0)
+                           (erc--msg . s005)
                            (b . 2)
-                           (erc-cmd . 5)
+                           (erc--cmd . 5)
                            (c . 3))
                          'hash-table)))
     (with-temp-buffer
       (erc-mode)
       (insert "abc\n")
       (add-text-properties 1 2 (erc--order-text-properties-from-hash table))
-      (should (equal '( erc-msg s005
-                        erc-ts 0
-                        erc-cmd 5
+      (should (equal '( erc--msg s005
+                        erc--ts 0
+                        erc--cmd 5
                         a 1
                         b 2
                         c 3)
@@ -2392,7 +2392,7 @@ erc--route-insertion
 
         (ert-info ("Cons `buffer' routes to live members")
           ;; Copies a let-bound `erc--msg-props' before mutating.
-          (let* ((table (map-into '(erc-msg msg) 'hash-table))
+          (let* ((table (map-into '(erc--msg msg) 'hash-table))
                  (erc--msg-props table))
             (erc--route-insertion "cons" (list server-buffer spam-buffer))
             (should-not (eq table erc--msg-props)))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
index c07eee3517f..f4a43a9384f 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 27 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 27 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
index cf5cdb4f825..78450ec08e2 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 29 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 29 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 29 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 29 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 29 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 29 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 29 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 29 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 29 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 29 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 29 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 29 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 29 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 29 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 29 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 29 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index ad4e6483f01..8e5535093e1 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
index 893588c028f..a0c03244afe 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
index 2b67cbbf90e..c4a51e06354 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
index 84a1e34670c..5eea73b4f16 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
index 83394f2f639..bc59c0bef22 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
index 1605628b29f..bfb75c0838e 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 25) line-prefix (space :width (- 25 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 25 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 25 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 25 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 25) line-prefix (space :width (- 25 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 25 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 25 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 25 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
index 84a1e34670c..5eea73b4f16 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
index 7a7e01de49d..1362c57ef10 100644
--- a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
+++ b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n<bob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> Somebody stop me\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 190 191 (line-spacing 0.5) 191 192 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 348 349 (line-spacing 0.5) 349 350 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 435 436 (line-spacing 0.5) 436 437 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #5=(space :width (- 27 0)) display #6="") 437 440 (wrap-prefix #1# line-prefix #5# display #6#) 440 442 (wrap-prefix #1# line-prefix #5# display #6#) 442 466 (wrap-prefix #1# line-prefix #5#) 466 467 (line-spacing 0.5) 467 468 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #7=(space :width (- 27 (4)))) 468 484 (wrap-prefix #1# line-prefix #7#) 485 486 (erc-msg notice erc-ts 0 wrap-prefix #1# line-prefix #8=(space :width (- 27 (4)))) 486 502 (wrap-prefix #1# line-prefix #8#) 502 503 (line-spacing 0.5) 503 504 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 504 507 (wrap-prefix #1# line-prefix #9#) 507 525 (wrap-prefix #1# line-prefix #9#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n<bob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> Somebody stop me\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 190 191 (line-spacing 0.5) 191 192 (erc--msg msg erc--cmd PRIVMSG erc--ts 0 wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 348 349 (line-spacing 0.5) 349 350 (erc--msg msg erc--cmd PRIVMSG erc--ts 0 wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 435 436 (line-spacing 0.5) 436 437 (erc--msg msg erc--cmd PRIVMSG erc--ts 0 wrap-prefix #1# line-prefix #5=(space :width (- 27 0)) display #6="") 437 440 (wrap-prefix #1# line-prefix #5# display #6#) 440 442 (wrap-prefix #1# line-prefix #5# display #6#) 442 466 (wrap-prefix #1# line-prefix #5#) 466 467 (line-spacing 0.5) 467 468 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #7=(space :width (- 27 (4)))) 468 484 (wrap-prefix #1# line-prefix #7#) 485 486 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #8=(space :width (- 27 (4)))) 486 502 (wrap-prefix #1# line-prefix #8#) 502 503 (line-spacing 0.5) 503 504 (erc--msg msg erc--cmd PRIVMSG erc--ts 0 wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 504 507 (wrap-prefix #1# line-prefix #9#) 507 525 (wrap-prefix #1# line-prefix #9#))
diff --git a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
index bb248ffb28e..4f87c7d2547 100644
--- a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
@@ -1 +1 @@
-#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n[00:00]<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg notice erc-ts 0 display #3=(#5=(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix #2=(space :width (- 27 (4)))) 3 9 (display #3# field erc-timestamp wrap-prefix #1# line-prefix #2#) 9 171 (wrap-prefix #1# line-prefix #2#) 172 173 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG display #6=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #4=(space :width (- 27 (8)))) 173 179 (display #6# field erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-prefix #1# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187 (wrap-prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#) 190 303 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-prefix #4#) 337 338 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG display #8=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #7=(space :width (- 27 (6)))) 338 344 (display #8# field erc-timestamp wrap-prefix #1# line-prefix #7#) 344 345 (wrap-prefix #1# line-prefix #7#) 345 348 (wrap-prefix #1# line-prefix #7#) 348 350 (wrap-prefix #1# line-prefix #7#) 350 355 (wrap-prefix #1# line-prefix #7#) 355 430 (wrap-prefix #1# line-prefix #7#))
+#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n[00:00]<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg notice erc--ts 0 display #3=(#5=(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix #2=(space :width (- 27 (4)))) 3 9 (display #3# field erc-timestamp wrap-prefix #1# line-prefix #2#) 9 171 (wrap-prefix #1# line-prefix #2#) 172 173 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG display #6=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #4=(space :width (- 27 (8)))) 173 179 (display #6# field erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-prefix #1# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187 (wrap-prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#) 190 303 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-prefix #4#) 337 338 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG display #8=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #7=(space :width (- 27 (6)))) 338 344 (display #8# field erc-timestamp wrap-prefix #1# line-prefix #7#) 344 345 (wrap-prefix #1# line-prefix #7#) 345 348 (wrap-prefix #1# line-prefix #7#) 348 350 (wrap-prefix #1# line-prefix #7#) 350 355 (wrap-prefix #1# line-prefix #7#) 355 430 (wrap-prefix #1# line-prefix #7#))
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-5.6-Add-erc-spkr-text-property-to-chat-messages.patch --]
[-- Type: text/x-patch, Size: 77716 bytes --]

From 1dd470f193d1a7bb0baa34798317d5eac83a93ce Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 4 Dec 2023 22:13:02 -0800
Subject: [PATCH 04/11] [5.6] Add erc--spkr text property to chat messages

* etc/ERC-NEWS: Mention combined face ordering for "/me" messages.
* lisp/erc/erc-backend.el: Bind `erc--msg-prop-overrides'.
* lisp/erc/erc-fill.el (erc-fill): Switch to `erc--spkr' as sentinel
property.
(erc-fill--wrap-continued-message-p): Look for `erc--spkr' property
instead of `erc-speaker'.
* lisp/erc/erc.el (erc--msg-props): Mention `erc--spkr' in doc.
(erc--msg-props): Mention `erc--spkr'.
(erc--send-action-perform-ctcp): Add `erc--spkr' property and ensure
`erc-my-nick-face' appears above `erc-input-face' in the speaker
portion.
(erc--insure-spkr-prop): New function.
(erc--ranked-properties): Add `erc--spkr', `erc--ctcp', and
`erc--ephemeral'.
(erc-display-message): Use default hash table size when initializing.
Remove unnecessary assignment of `msg' to `erc--msg' for PRIVMSG and
NOTICE commands.
(erc--own-property-names): Add all `erc--msg-props' props.
(erc--get-speaker-bounds): Use `erc--spkr' instead of `erc--msg'.
(erc-format-privmessage, erc-format-my-nick, erc-ctcp-query-ACTION):
Add `erc--spkr' to `erc--msg-prop-overrides' when available.
* test/lisp/erc/erc-fill-tests.el:
(erc--order-text-properties-from-hash): Include `erc--spkr'.
(erc-fill-tests--insert-privmsg): bind `erc--msg-prop-overrides'.
(erc-fill-tests--compare): Require environment variable value to match
current test name for saving to work.  Add `erc--msg-props'
individually to white list.
(Bug#60936)
; * test/lisp/erc/resources/fill/snapshots/merge-01-start.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/merge-02-right.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld:
; Update.
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld:
; Update.
; * test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld: Update.
---
 etc/ERC-NEWS                                  |  5 ++-
 lisp/erc/erc-backend.el                       |  3 ++
 lisp/erc/erc-fill.el                          | 20 ++++-----
 lisp/erc/erc.el                               | 43 +++++++++++++------
 test/lisp/erc/erc-fill-tests.el               | 11 +++--
 test/lisp/erc/erc-tests.el                    |  2 +
 .../fill/snapshots/merge-01-start.eld         |  2 +-
 .../fill/snapshots/merge-02-right.eld         |  2 +-
 .../fill/snapshots/merge-wrap-01.eld          |  2 +-
 .../merge-wrap-indicator-post-01.eld          |  2 +-
 .../snapshots/merge-wrap-indicator-pre-01.eld |  2 +-
 .../fill/snapshots/monospace-01-start.eld     |  2 +-
 .../fill/snapshots/monospace-02-right.eld     |  2 +-
 .../fill/snapshots/monospace-03-left.eld      |  2 +-
 .../fill/snapshots/monospace-04-reset.eld     |  2 +-
 .../fill/snapshots/spacing-01-mono.eld        |  2 +-
 .../fill/snapshots/stamps-left-01.eld         |  2 +-
 17 files changed, 66 insertions(+), 40 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 238c40feefb..f6a9d934e80 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -214,7 +214,10 @@ Users of the default theme may notice that 'erc-action-face' and
 'erc-notice-face' now appear slightly less bold on systems supporting
 a weight of 'semi-bold'.  This was done to make buttons detectable and
 to spare users from resorting to tweaking these faces, or options like
-'erc-notice-highlight-type', just to achieve this effect.
+'erc-notice-highlight-type', just to achieve this effect.  It's
+currently most prominent in "/ME" messages, where 'erc-action-face'
+sits beneath 'erc-input-face', as well as 'erc-my-nick-face' in the
+speaker portion.
 
 ** Improved interplay between buffer truncation and message logging.
 While most of these improvements are subtle, some affect everyday use.
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 500e025e5a1..b1ceeea4f44 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1916,6 +1916,7 @@ erc--server-determine-join-display-context
             (erc-ignored-reply-p msg tgt proc))
         (when erc-minibuffer-ignored
           (message "Ignored %s from %s to %s" cmd sender-spec tgt))
+      (defvar erc--msg-prop-overrides)
       (let* ((sndr (erc-parse-user sender-spec))
              (nick (nth 0 sndr))
              (login (nth 1 sndr))
@@ -1926,6 +1927,8 @@ erc--server-determine-join-display-context
              (privp (erc-current-nick-p tgt))
              (erc--display-context `((erc-buffer-display . ,(intern cmd))
                                      ,@erc--display-context))
+             (erc--msg-prop-overrides `((erc--msg . msg)
+                                        ,@erc--msg-prop-overrides))
              s buffer
              fnick)
         (setf (erc-response.contents parsed) msg)
diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index 5434d9af966..de6cd581fec 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -177,11 +177,10 @@ erc-fill
           (when-let ((erc-fill-line-spacing)
                      (p (point-min)))
             (widen)
-            (when (or (erc--check-msg-prop 'erc--msg 'msg)
-                      (and-let* ((m (save-excursion
-                                      (forward-line -1)
-                                      (erc--get-inserted-msg-prop 'erc--msg))))
-                        (eq 'msg m)))
+            (when (or (erc--check-msg-prop 'erc--spkr)
+                      (save-excursion
+                        (forward-line -1)
+                        (erc--get-inserted-msg-prop 'erc--spkr)))
               (put-text-property (1- p) p
                                  'line-spacing erc-fill-line-spacing))))))))
 
@@ -568,22 +567,19 @@ erc-fill--wrap-continued-message-p
               (props (save-restriction
                        (widen)
                        (and-let*
-                           (((eq 'msg (get-text-property m 'erc--msg)))
+                           ((speaker (get-text-property m 'erc--spkr))
                             ((not (eq (get-text-property m 'erc--ctcp)
                                       'ACTION)))
-                            ((not (invisible-p m)))
-                            (spr (next-single-property-change m 'erc-speaker)))
-                         (cons (get-text-property m 'erc--ts)
-                               (get-text-property spr 'erc-speaker)))))
+                            ((not (invisible-p m))))
+                         (cons (get-text-property m 'erc--ts) speaker))))
               (ts (pop props))
               (props)
               ((not (time-less-p (erc-stamp--current-time) ts)))
               ((time-less-p (time-subtract (erc-stamp--current-time) ts)
                             erc-fill--wrap-max-lull))
               ;; Assume presence of leading angle bracket or hyphen.
-              (speaker (next-single-property-change (point-min) 'erc-speaker))
+              (nick (erc--check-msg-prop 'erc--spkr))
               ((not (erc--check-msg-prop 'erc--ctcp 'ACTION)))
-              (nick (get-text-property speaker 'erc-speaker))
               ((erc-nick-equal-p props nick))))
        (set-marker erc-fill--wrap-last-msg (point-min))))))
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index c68c74467b8..7397add1e98 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -167,6 +167,8 @@ erc--msg-props
     and help text, and on outgoing messages unless echoed back by
     the server (assuming future support)
 
+ - `erc--spkr': a string, the nick of the person speaking
+
  - `erc--ctcp': a CTCP command, like `ACTION'
 
  - `erc--ts': a timestamp, possibly provided by the server; as of
@@ -3013,13 +3015,16 @@ erc--send-action-perform-ctcp
 (defun erc--send-action-display (string)
   "Display STRING as an outgoing \"CTCP ACTION\" message."
   ;; Allow hooks acting 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)))
+  (defvar erc--merge-prop-behind-p)
+  (let* ((nick (erc-current-nick))
+         (erc--msg-prop-overrides `((erc--msg . msg)
+                                    (erc--ctcp . ACTION)
+                                    (erc--spkr . ,nick)
+                                    ,@erc--msg-prop-overrides))
+         (erc--merge-prop-behind-p t))
     (setq nick (propertize nick 'erc-speaker nick
                            'font-lock-face 'erc-my-nick-face))
-    (erc-display-message nil '(t action input) (current-buffer)
+    (erc-display-message nil '(t input action) (current-buffer)
                          'ACTION ?n nick ?a string ?u "" ?h "")))
 
 (defun erc--send-action (target string force)
@@ -3029,6 +3034,12 @@ erc--send-action
 
 ;; Display interface
 
+(defun erc--ensure-spkr-prop (nick)
+  "Maybe add NICK to `erc--msg-props' or `erc--msg-prop-overrides'."
+  (cond (erc--msg-props (puthash 'erc--spkr nick erc--msg-props))
+        (erc--msg-prop-overrides
+         (push (cons 'erc--spkr nick) erc--msg-prop-overrides))))
+
 (defun erc-string-invisible-p (string)
   "Check whether STRING is invisible or not.
 I.e. any char in it has the `invisible' property set."
@@ -3458,7 +3469,8 @@ erc--delete-inserted-message
              (substring (delete-and-extract-region (1- (point)) (1+ end))
                         -1))))))))
 
-(defvar erc--ranked-properties '(erc--msg erc--ts erc--cmd))
+(defvar erc--ranked-properties
+  '(erc--msg erc--spkr erc--ts erc--cmd erc--ctcp erc--ephemeral))
 
 (defun erc--order-text-properties-from-hash (table)
   "Return a plist of text props from items in TABLE.
@@ -3729,12 +3741,11 @@ erc-display-message
                   msg))
         (erc--msg-props
          (or erc--msg-props
-             (let ((table (make-hash-table :size 5))
+             (let ((table (make-hash-table))
                    (cmd (and parsed (erc--get-eq-comparable-cmd
                                      (erc-response.command parsed)))))
                (puthash 'erc--msg
                         (cond ((and msg (symbolp msg)) msg)
-                              ((and cmd (memq cmd '(PRIVMSG NOTICE)) 'msg))
                               (type (pcase type
                                       ((pred symbolp) type)
                                       ((pred listp)
@@ -3745,8 +3756,8 @@ erc-display-message
                         table)
                (when cmd
                  (puthash 'erc--cmd cmd table))
-               (and-let* ((ovs erc--msg-prop-overrides))
-                 (pcase-dolist (`(,k . ,v) (reverse ovs))
+               (when erc--msg-prop-overrides
+                 (pcase-dolist (`(,k . ,v) (reverse erc--msg-prop-overrides))
                    (puthash k v table)))
                table)))
         (erc-message-parsed parsed))
@@ -4645,6 +4656,9 @@ erc-send-message
       (funcall erc--send-message-nested-function line force)
     (erc--send-message-external line force)))
 
+;; FIXME fully simulate `erc-display-msg'.  This doesn't currently add
+;; the correct text properties.  For example, the LINE should have
+;; `erc-default-face'.
 (defun erc--send-message-external (line force)
   (erc-message "PRIVMSG" (concat (erc-default-target) " " line) force)
   (erc-display-line
@@ -5258,7 +5272,9 @@ erc-ensure-channel-name
     (concat "#" channel)))
 
 (defvar erc--own-property-names
-  '( tags erc-speaker erc-parsed display ; core
+  `( tags erc-speaker erc-parsed display ; core
+     ;; `erc--msg-props'
+     ,@erc--ranked-properties
      ;; `erc-display-prompt'
      rear-nonsticky erc-prompt field front-sticky read-only
      ;; stamp
@@ -5744,7 +5760,7 @@ erc-is-message-ctcp-and-not-action-p
 (defun erc--get-speaker-bounds ()
   "Return the bounds of `erc-speaker' text property when present.
 Assume buffer is narrowed to the confines of an inserted message."
-  (and-let* (((erc--check-msg-prop 'erc--msg 'msg))
+  (and-let* (((erc--check-msg-prop 'erc--spkr))
              (beg (text-property-not-all (point-min) (point-max)
                                          'erc-speaker nil)))
     (cons beg (next-single-property-change beg 'erc-speaker))))
@@ -5772,6 +5788,7 @@ erc-format-privmessage
                                                 nick-prefix-face nick))
                          0))
          (msg-face (if privp 'erc-direct-msg-face 'erc-default-face)))
+    (erc--ensure-spkr-prop nick)
     ;; add text properties to text before the nick, the nick and after the nick
     (erc-put-text-property 0 (length mark-s) 'font-lock-face msg-face str)
     (erc-put-text-properties (+ (length mark-s) prefix-len)
@@ -5827,6 +5844,7 @@ erc-format-my-nick
              (close "> ")
              (nick (erc-current-nick))
              (mode (erc-get-user-mode-prefix nick)))
+        (erc--ensure-spkr-prop nick)
         (concat
          (propertize open 'font-lock-face 'erc-default-face)
          (propertize mode 'font-lock-face 'erc-my-nick-prefix-face)
@@ -6111,6 +6129,7 @@ erc-ctcp-query-ACTION
           (buf (or (erc-get-buffer to proc)
                    (erc-get-buffer nick proc)
                    (process-buffer proc))))
+      (erc--ensure-spkr-prop nick)
       (setq nick (propertize nick 'erc-speaker nick))
       (erc-display-message
        parsed 'action buf
diff --git a/test/lisp/erc/erc-fill-tests.el b/test/lisp/erc/erc-fill-tests.el
index bfdf8cd7320..8560d421cc2 100644
--- a/test/lisp/erc/erc-fill-tests.el
+++ b/test/lisp/erc/erc-fill-tests.el
@@ -35,7 +35,8 @@ erc-stamp--current-time
 
 (defun erc-fill-tests--insert-privmsg (speaker &rest msg-parts)
   (declare (indent 1))
-  (let* ((msg (erc-format-privmessage speaker
+  (let* ((erc--msg-prop-overrides `((erc--msg . msg)))
+         (msg (erc-format-privmessage speaker
                                       (apply #'concat msg-parts) nil t))
          (parsed (make-erc-response :unparsed (format ":%s PRIVMSG #chan :%s"
                                                       speaker msg)
@@ -150,7 +151,9 @@ erc-fill-tests--compare
                                                 "eld"))
          (erc--own-property-names
           (seq-difference `(font-lock-face ,@erc--own-property-names)
-                          '(field display wrap-prefix line-prefix)
+                          `(field display wrap-prefix line-prefix
+                                  erc--msg erc--cmd erc--spkr erc--ts erc--ctcp
+                                  erc--ephemeral)
                           #'eq))
          (print-circle t)
          (print-escape-newlines t)
@@ -165,12 +168,12 @@ erc-fill-tests--compare
       (with-silent-modifications
         (insert (setq got (read repr))))
       (erc-mode))
-    (if erc-fill-tests--save-p
+    ;; LHS is a string, RHS is a symbol.
+    (if (string= erc-fill-tests--save-p (ert-test-name (ert-running-test)))
         (let (inhibit-message)
           (with-temp-file expect-file
             (insert repr))
           ;; Limit writing snapshots to one test at a time.
-          (setq erc-fill-tests--save-p nil)
           (message "erc-fill-tests--compare: wrote %S" expect-file))
       (if (file-exists-p expect-file)
           ;; Ensure string-valued properties, like timestamps, aren't
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index b8ebc23e686..ed1dcccd59c 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1859,6 +1859,7 @@ erc--order-text-properties-from-hash
                            (erc--msg . s005)
                            (b . 2)
                            (erc--cmd . 5)
+                           (erc--spkr . "X")
                            (c . 3))
                          'hash-table)))
     (with-temp-buffer
@@ -1866,6 +1867,7 @@ erc--order-text-properties-from-hash
       (insert "abc\n")
       (add-text-properties 1 2 (erc--order-text-properties-from-hash table))
       (should (equal '( erc--msg s005
+                        erc--spkr "X"
                         erc--ts 0
                         erc--cmd 5
                         a 1
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
index f4a43a9384f..3c32719a052 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 27 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc--msg msg erc--ts 1680332400 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 27 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc--msg msg erc--ts 1680332400 erc--spkr "Dummy" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc--msg msg erc--ts 1680332400 erc--spkr "Dummy" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
index 78450ec08e2..e2064b914c4 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 29 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 29 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 29 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 29 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 29 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 29 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 29 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 29 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18))) field erc-timestamp) 21 22 (wrap-prefix #1# line-prefix #2=(space :width (- 29 (4))) erc--msg notice erc--ts 0) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (wrap-prefix #1# line-prefix #2# field erc-timestamp display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (wrap-prefix #1# line-prefix #3=(space :width (- 29 (8))) erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix (space :width (- 29 (8)))) 349 350 (wrap-prefix #1# line-prefix #4=(space :width (- 29 (6))) erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (wrap-prefix #1# line-prefix (space :width (- 29 (18))) field erc-timestamp) 455 456 (wrap-prefix #1# line-prefix #5=(space :width (- 29 (6))) erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (wrap-prefix #1# line-prefix #5# field erc-timestamp display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (wrap-prefix #1# line-prefix #7=(space :width (- 29 (8))) erc--msg msg erc--ts 1680332400 erc--spkr "alice" erc--cmd PRIVMSG) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (wrap-prefix #1# line-prefix #8=(space :width (- 29 0)) erc--msg msg erc--ts 1680332400 erc--spkr "alice" erc--cmd PRIVMSG display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (wrap-prefix #1# line-prefix #10=(space :width (- 29 (6))) erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (wrap-prefix #1# line-prefix #11=(space :width (- 29 0)) erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (wrap-prefix #1# line-prefix #12=(space :width (- 29 (8))) erc--msg msg erc--ts 1680332400 erc--spkr "Dummy" erc--cmd PRIVMSG) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (wrap-prefix #1# line-prefix #13=(space :width (- 29 0)) erc--msg msg erc--ts 1680332400 erc--spkr "Dummy" erc--cmd PRIVMSG display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index 8e5535093e1..9f648915d5c 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
index a0c03244afe..a63fcad3d38 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
index c4a51e06354..7cbabfd0581 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
index 5eea73b4f16..c94629cf357 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
index bc59c0bef22..127c0b29bc9 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
index bfb75c0838e..a9f3f1d1904 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 25) line-prefix (space :width (- 25 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 25 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 25 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 25 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 25) line-prefix (space :width (- 25 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 25 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 25 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 25 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
index 5eea73b4f16..c94629cf357 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
index 1362c57ef10..754d7989cea 100644
--- a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
+++ b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n<bob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> Somebody stop me\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 190 191 (line-spacing 0.5) 191 192 (erc--msg msg erc--cmd PRIVMSG erc--ts 0 wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 348 349 (line-spacing 0.5) 349 350 (erc--msg msg erc--cmd PRIVMSG erc--ts 0 wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 435 436 (line-spacing 0.5) 436 437 (erc--msg msg erc--cmd PRIVMSG erc--ts 0 wrap-prefix #1# line-prefix #5=(space :width (- 27 0)) display #6="") 437 440 (wrap-prefix #1# line-prefix #5# display #6#) 440 442 (wrap-prefix #1# line-prefix #5# display #6#) 442 466 (wrap-prefix #1# line-prefix #5#) 466 467 (line-spacing 0.5) 467 468 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #7=(space :width (- 27 (4)))) 468 484 (wrap-prefix #1# line-prefix #7#) 485 486 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #8=(space :width (- 27 (4)))) 486 502 (wrap-prefix #1# line-prefix #8#) 502 503 (line-spacing 0.5) 503 504 (erc--msg msg erc--cmd PRIVMSG erc--ts 0 wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 504 507 (wrap-prefix #1# line-prefix #9#) 507 525 (wrap-prefix #1# line-prefix #9#))
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n<bob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> Somebody stop me\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 190 191 (line-spacing 0.5) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 348 349 (line-spacing 0.5) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 435 436 (line-spacing 0.5) 436 437 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 0)) display #6="") 437 440 (wrap-prefix #1# line-prefix #5# display #6#) 440 442 (wrap-prefix #1# line-prefix #5# display #6#) 442 466 (wrap-prefix #1# line-prefix #5#) 466 467 (line-spacing 0.5) 467 468 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #7=(space :width (- 27 (4)))) 468 484 (wrap-prefix #1# line-prefix #7#) 485 486 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #8=(space :width (- 27 (4)))) 486 502 (wrap-prefix #1# line-prefix #8#) 502 503 (line-spacing 0.5) 503 504 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 504 507 (wrap-prefix #1# line-prefix #9#) 507 525 (wrap-prefix #1# line-prefix #9#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
index 4f87c7d2547..1b22b6c5cfd 100644
--- a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
@@ -1 +1 @@
-#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n[00:00]<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg notice erc--ts 0 display #3=(#5=(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix #2=(space :width (- 27 (4)))) 3 9 (display #3# field erc-timestamp wrap-prefix #1# line-prefix #2#) 9 171 (wrap-prefix #1# line-prefix #2#) 172 173 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG display #6=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #4=(space :width (- 27 (8)))) 173 179 (display #6# field erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-prefix #1# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187 (wrap-prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#) 190 303 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-prefix #4#) 337 338 (erc--msg msg erc--ts 0 erc--cmd PRIVMSG display #8=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #7=(space :width (- 27 (6)))) 338 344 (display #8# field erc-timestamp wrap-prefix #1# line-prefix #7#) 344 345 (wrap-prefix #1# line-prefix #7#) 345 348 (wrap-prefix #1# line-prefix #7#) 348 350 (wrap-prefix #1# line-prefix #7#) 350 355 (wrap-prefix #1# line-prefix #7#) 355 430 (wrap-prefix #1# line-prefix #7#))
+#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n[00:00]<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc--msg notice erc--ts 0 display #3=(#5=(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix #2=(space :width (- 27 (4)))) 3 9 (display #3# field erc-timestamp wrap-prefix #1# line-prefix #2#) 9 171 (wrap-prefix #1# line-prefix #2#) 172 173 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG display #6=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #4=(space :width (- 27 (8)))) 173 179 (display #6# field erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-prefix #1# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187 (wrap-prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#) 190 303 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-prefix #4#) 337 338 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG display #8=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #7=(space :width (- 27 (6)))) 338 344 (display #8# field erc-timestamp wrap-prefix #1# line-prefix #7#) 344 345 (wrap-prefix #1# line-prefix #7#) 345 348 (wrap-prefix #1# line-prefix #7#) 348 350 (wrap-prefix #1# line-prefix #7#) 350 355 (wrap-prefix #1# line-prefix #7#) 355 430 (wrap-prefix #1# line-prefix #7#))
\ No newline at end of file
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-5.6-Add-utility-for-iterating-over-arrays-in-ERC.patch --]
[-- Type: text/x-patch, Size: 6783 bytes --]

From 6ded8441328db8019f546182ec1f7943bead9160 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 3 Dec 2023 19:19:37 -0800
Subject: [PATCH 05/11] [5.6] Add utility for iterating over arrays in ERC

* lisp/erc/erc-common.el (erc--doarray): Add macro for mapping over
arrays.  ERC has the unique requirement of having to repeatedly
traverse strings that contain flags for advertised server features.
It doesn't make sense to "encode" these meanings into enums or
dynamically generate variables for each flag.  Hash tables and plain
lists require additional setup and aren't as compact to print for
inspection.
* lisp/erc/erc-dcc.el (erc-dcc-handle-ctcp-send): Use `string-search'
instead of `seq-contains-p' even though performance doesn't matter
here.
* lisp/erc/erc.el (erc--channel-mode-types): Use `erc--doarray'
instead of `dolist'.
(erc--process-channel-modes): Use `erc--doarray' instead of `dolist',
and don't create a string from current char until needed.
(erc--parse-user-modes): Use `erc--doarray' instead of `dolist'.
* test/lisp/erc/erc-tests.el (erc--doarray): New test.
---
 lisp/erc/erc-common.el     | 19 +++++++++++++++++++
 lisp/erc/erc-dcc.el        |  4 ++--
 lisp/erc/erc.el            | 35 ++++++++++++++++-------------------
 test/lisp/erc/erc-tests.el | 13 +++++++++++++
 4 files changed, 50 insertions(+), 21 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index c5e4901c6d2..e9e494720e5 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -535,6 +535,25 @@ erc-define-message-format-catalog
   (declare (indent 1))
   `(erc--define-catalog ,language ,entries))
 
+(defmacro erc--doarray (spec &rest body)
+  "Map over ARRAY, running BODY with VAR bound to iteration element.
+Behave more or less like `seq-doseq', but tailor operations for
+arrays.
+
+\(fn (VAR ARRAY [RESULT]) BODY...)"
+  (declare (indent 1) (debug ((symbolp form &optional form) body)))
+  (let ((array (make-symbol "array"))
+        (len (make-symbol "len"))
+        (i (make-symbol "i")))
+    `(let* ((,array ,(nth 1 spec))
+            (,len (length ,array))
+            (,i 0))
+       (while-let (((< ,i ,len))
+                   (,(car spec) (aref ,array ,i)))
+         ,@body
+         (cl-incf ,i))
+       ,(nth 2 spec))))
+
 (provide 'erc-common)
 
 ;;; erc-common.el ends here
diff --git a/lisp/erc/erc-dcc.el b/lisp/erc/erc-dcc.el
index 3bcdfb96eb8..ac7fc817cb9 100644
--- a/lisp/erc/erc-dcc.el
+++ b/lisp/erc/erc-dcc.el
@@ -713,8 +713,8 @@ erc-dcc-handle-ctcp-send
              (port (match-string 4 query))
              (size (match-string 5 query))
              (sub (substring (match-string 6 query) 0 -4))
-             (secure (seq-contains-p sub ?S #'eq))
-             (turbo (seq-contains-p sub ?T #'eq)))
+             (secure (string-search "S" sub))
+             (turbo (string-search "T" sub)))
         ;; FIXME: a warning really should also be sent
         ;; if the ip address != the host the dcc sender is on.
         (erc-display-message
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 7397add1e98..9084b7ee042 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -6800,7 +6800,7 @@ erc--channel-mode-types
           (ct (make-char-table 'erc--channel-mode-types))
           (type ?a))
       (dolist (cs types)
-        (dolist (c (append cs nil))
+        (erc--doarray (c cs)
           (aset ct c type))
         (cl-incf type))
       (make-erc--channel-mode-types :key key
@@ -6819,21 +6819,20 @@ erc--process-channel-modes
          (table (erc--channel-mode-types-table obj))
          (fallbackp (erc--channel-mode-types-fallbackp obj))
          (+p t))
-    (dolist (c (append string nil))
-      (let ((letter (char-to-string c)))
-        (cond ((= ?+ c) (setq +p t))
-              ((= ?- c) (setq +p nil))
-              ((and status-letters (string-search letter status-letters))
-               (erc--update-membership-prefix (pop args) c (if +p 'on 'off)))
-              ((and-let* ((group (or (aref table c) (and fallbackp ?d))))
-                 (erc--handle-channel-mode group c +p
-                                           (and (/= group ?d)
-                                                (or (/= group ?c) +p)
-                                                (pop args)))
-                 t))
-              ((not fallbackp)
-               (erc-display-message nil '(notice error) (erc-server-buffer)
-                                    (format "Unknown channel mode: %S" c))))))
+    (erc--doarray (c string)
+      (cond ((= ?+ c) (setq +p t))
+            ((= ?- c) (setq +p nil))
+            ((and status-letters (string-search (string c) status-letters))
+             (erc--update-membership-prefix (pop args) c (if +p 'on 'off)))
+            ((and-let* ((group (or (aref table c) (and fallbackp ?d))))
+               (erc--handle-channel-mode group c +p
+                                         (and (/= group ?d)
+                                              (or (/= group ?c) +p)
+                                              (pop args)))
+               t))
+            ((not fallbackp)
+             (erc-display-message nil '(notice error) (erc-server-buffer)
+                                  (format "Unknown channel mode: %S" c)))))
     (setq erc-channel-modes (sort erc-channel-modes #'string<))
     (setq erc--mode-line-mode-string
           (concat "+" (erc--channel-modes erc--mode-line-chanmodes-arg-len)))
@@ -6913,9 +6912,7 @@ erc--parse-user-modes
   (let ((addp t)
         ;;
         redundant-add redundant-drop adding dropping)
-    ;; For short strings, `append' appears to be no slower than
-    ;; iteration var + `aref' or `mapc' + closure.
-    (dolist (c (append string nil))
+    (erc--doarray (c string)
       (pcase c
         (?+ (setq addp t))
         (?- (setq addp nil))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index ed1dcccd59c..2c70f100c3f 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -142,6 +142,19 @@ erc--with-dependent-type-match
                                     (widget-editable-list-match w v))
                    '(face)))))
 
+(ert-deftest erc--doarray ()
+  (let ((array "abcdefg")
+        out)
+    ;; No return form.
+    (should-not (erc--doarray (c array) (push c out)))
+    (should (equal out '(?g ?f ?e ?d ?c ?b ?a)))
+
+    ;; Return form evaluated upon completion.
+    (setq out nil)
+    (should (= 42 (erc--doarray (c array (+ 39 (length out)))
+                    (when (cl-evenp c) (push c out)))))
+    (should (equal out '(?f ?d ?b)))))
+
 (defun erc-tests--send-prep ()
   ;; Caller should probably shadow `erc-insert-modify-hook' or
   ;; populate user tables for erc-button.
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-5.6-Make-erc-input-s-refoldp-slot-conditionally-avai.patch --]
[-- Type: text/x-patch, Size: 7272 bytes --]

From 6d6bfee8180c9ba37545c18aed46ddc8dc43732f Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 1 Dec 2023 13:54:12 -0800
Subject: [PATCH 06/11] [5.6] Make erc-input's refoldp slot conditionally
 available

* etc/ERC-NEWS: Fix entry regarding `erc-input-refoldp'.
* lisp/erc/erc-common.el (erc-input): Remove `refoldp' slot to reduce
churn in the unlikely event that third-party code subclasses
`erc-input' for use outside of `erc-pre-send-functions'.
(erc--input-split) Add `refoldp' slot here instead.
* lisp/erc/erc.el (erc-pre-send-functions): Amend doc string to stress
that `refoldp' is not a real slot.
(erc--input-ensure-hook-context, erc-input-refoldp): New function, an
impostor accessor for the nonexistent `refoldp' slot of `erc-input',
and a helper function for asserting a valid context at runtime.
(erc--run-send-hooks): Don't copy over `refoldp' from the
`erc--input-lines' object to the working `erc-insert' object.  Check
the insertion context's `erc--input-split' object instead of the
hook's `erc-insert' object when deciding whether to resplit.
* test/lisp/erc/erc-tests.el: Adjust test environment to satisfy
assertion.  (Bug#62947)
---
 etc/ERC-NEWS               |  5 +++--
 lisp/erc/erc-common.el     |  3 ++-
 lisp/erc/erc.el            | 31 +++++++++++++++++++++++++------
 test/lisp/erc/erc-tests.el |  7 ++++---
 4 files changed, 34 insertions(+), 12 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index f6a9d934e80..540d9e98751 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -460,8 +460,9 @@ ERC now adjusts input lines to fall within allowed length limits
 before showing hook members the result.  For compatibility,
 third-party code can request that the final input be adjusted again
 prior to being sent.  To facilitate this, the 'erc-input' object
-shared among hook members has gained a new 'refoldp' slot, making this
-a breaking change, if only in theory.  See doc string for details.
+shared among hook members has gained a "phony" 'refoldp' slot that's
+only accessible from 'erc-pre-send-functions'.  See doc string for
+details.
 
 *** ERC's prompt survives the insertion of user input and messages.
 Previously, ERC's prompt and its input marker disappeared while
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index e9e494720e5..cb820c812b3 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -49,7 +49,7 @@ erc-session-server
 (declare-function widget-type "wid-edit" (widget))
 
 (cl-defstruct erc-input
-  string insertp sendp refoldp)
+  string insertp sendp)
 
 (cl-defstruct (erc--input-split (:include erc-input
                                           (string :read-only)
@@ -57,6 +57,7 @@ erc-input
                                           (sendp (with-suppressed-warnings
                                                      ((obsolete erc-send-this))
                                                    erc-send-this))))
+  (refoldp nil :type boolean)
   (lines nil :type (list-of string))
   (abortp nil :type (list-of symbol))
   (cmdp nil :type boolean))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 9084b7ee042..bb05f17bee6 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1197,13 +1197,18 @@ erc-pre-send-functions
   `string': The current input string.
   `insertp': Whether the string should be inserted into the erc buffer.
   `sendp': Whether the string should be sent to the irc server.
+
+And one \"phony\" slot only accessible by hook members at runtime:
+
   `refoldp': Whether the string should be re-split per protocol limits.
 
 This hook runs after protocol line splitting has taken place, so
 the value of `string' is originally \"pre-filled\".  If you need
-ERC to refill the entire payload before sending it, set the
-`refoldp' slot to a non-nil value.  Preformatted text and encoded
-subprotocols should probably be handled manually."
+ERC to refill the entire payload before sending it, set the phony
+`refoldp' slot to a non-nil value.  Note that this refilling is
+only a convenience, and modules with special needs, such as
+preserving \"preformatted\" text or encoding for subprotocol
+\"tunneling\", should handle splitting manually."
   :group 'erc
   :type 'hook
   :version "27.1")
@@ -7424,6 +7429,22 @@ erc--split-lines
     (setf (erc--input-split-lines state)
           (mapcan #'erc--split-line (erc--input-split-lines state)))))
 
+(defun erc--input-ensure-hook-context ()
+  (unless (erc--input-split-p erc--current-line-input-split)
+    (error "Invoked outside of `erc-pre-send-functions'")))
+
+(defun erc-input-refoldp (_)
+  "Impersonate accessor for phony `erc-input' `refoldp' slot.
+This function only works inside `erc-pre-send-functions' members."
+  (declare (gv-setter (lambda (v)
+                        `(progn
+                           (erc--input-ensure-hook-context)
+                           (setf (erc--input-split-refoldp
+                                  erc--current-line-input-split)
+                                 ,v)))))
+  (erc--input-ensure-hook-context)
+  (erc--input-split-refoldp erc--current-line-input-split))
+
 (defun erc--run-send-hooks (lines-obj)
   "Run send-related hooks that operate on the entire prompt input.
 Sequester some of the back and forth involved in honoring old
@@ -7443,8 +7464,6 @@ 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)
@@ -7456,7 +7475,7 @@ erc--run-send-hooks
                 (if erc--allow-empty-outgoing-lines-p
                     lines
                   (cl-nsubst " " "" lines :test #'equal))))
-        (when (erc-input-refoldp state)
+        (when (erc--input-split-refoldp lines-obj)
           (erc--split-lines lines-obj)))))
   (when (and (erc--input-split-cmdp lines-obj)
              (cdr (erc--input-split-lines lines-obj)))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 2c70f100c3f..28c1e403e41 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2256,10 +2256,11 @@ erc--run-send-hooks
               erc-pre-send-functions
               (lambda (o) (setf (erc-input-string o) "foo bar baz"
                                 (erc-input-refoldp o) t)))
-        (let ((erc-split-line-length 8))
+        (let* ((split (make-erc--input-split :string "foo" :lines '("foo")))
+               (erc--current-line-input-split split)
+               (erc-split-line-length 8))
           (should
-           (pcase (erc--run-send-hooks (make-erc--input-split
-                                        :string "foo" :lines '("foo")))
+           (pcase (erc--run-send-hooks split)
              ((cl-struct erc--input-split
                          (string "foo") (sendp 't) (insertp 't)
                          (lines '("foo bar " "baz")) (cmdp 'nil))
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0007-5.6-Add-erc-server-396-response-handler.patch --]
[-- Type: text/x-patch, Size: 13652 bytes --]

From 6eda82926f0d2c2e3eef1ce19a019c18fe5c5542 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 18 Aug 2023 18:04:22 -0700
Subject: [PATCH 07/11] [5.6] Add erc-server-396 response handler

* lisp/erc/erc-backend.el (erc-server-396, erc-server-396-functions):
Define response handler for 396 numeric.  For now, always display the
message in the active buffer rather than bother with something like
`erc-once-with-server-event' to try and suss out when this is a
response to something client-initiated, like a /VHOST.  Do this
despite most users probably wanting the message to appear in the
server buffer alone when the response is server-initiated.  The
`labeled-response' extension will hopefully make dealing with such
matters trivial.
* lisp/erc/erc.el (erc--parse-user-regexp-pedantic): Tweak slightly to
allow null groups and favor host.
(erc--parse-user-regexp-legacy, erc--parse-user-regexp): Remove the
first variable but preserve its value as that of the second.
(erc--parse-nuh): New function.
(erc-message-english-396): Define format template for 396 response.
* test/lisp/erc/erc-scenarios-misc-commands.el (ert-deftest
erc-scenarios-misc-commands--VHOST): New test.
* test/lisp/erc/erc-tests.el (erc-parse-user): Move "pedantic" section
to new test.
(erc--parse-nuh): New test.
* test/lisp/erc/resources/commands/vhost.eld: New test data file.
---
 lisp/erc/erc-backend.el                      | 20 ++++++++++
 lisp/erc/erc.el                              | 17 ++++++--
 test/lisp/erc/erc-scenarios-misc-commands.el | 32 +++++++++++++++
 test/lisp/erc/erc-tests.el                   | 41 +++++++++++++-------
 test/lisp/erc/resources/commands/vhost.eld   | 40 +++++++++++++++++++
 5 files changed, 132 insertions(+), 18 deletions(-)
 create mode 100644 test/lisp/erc/resources/commands/vhost.eld

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index b1ceeea4f44..d548a788bc0 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -119,6 +119,7 @@ erc-verbose-server-ping
 
 (declare-function erc--init-channel-modes "erc" (channel raw-args))
 (declare-function erc--open-target "erc" (target))
+(declare-function erc--parse-nuh "erc" (string))
 (declare-function erc--target-from-string "erc" (string))
 (declare-function erc--update-modes "erc" (raw-args))
 (declare-function erc-active-buffer "erc" nil)
@@ -2410,6 +2411,7 @@ erc-server-322-message
     (erc-display-message parsed 'notice (erc-get-buffer channel proc)
                          's341 ?n nick ?c channel)))
 
+;; FIXME update or add server user instead when channel is "*".
 (define-erc-response-handler (352)
   "WHO notice." nil
   (pcase-let ((`(,channel ,user ,host ,_server ,nick ,away-flag)
@@ -2475,6 +2477,24 @@ erc-server-322-message
    's391 ?s (cadr (erc-response.command-args parsed))
    ?t (nth 2 (erc-response.command-args parsed))))
 
+;; https://defs.ircdocs.horse/defs/numerics.html#rpl-visiblehost-396
+;; As of ERC 5.6, if the client hasn't yet joined any channels,
+;; there's a good chance a server user for the current nick simply
+;; doesn't exist (and there's not enough info in this reply to create
+;; one).  To fix this, ERC could WHO itself on 372 or similar if it
+;; hasn't yet received a 900.
+(define-erc-response-handler (396)
+  "RPL_VISIBLEHOST or RPL_YOURDISPLAYHOST or RPL_HOSTHIDDEN." nil
+  (pcase-let* ((userhost (cadr (erc-response.command-args parsed)))
+               ;; Behavior blindly copied from event_hosthidden in irssi 1.4.
+               (rejectrx (rx (| (: bot (in ?@ ?: ?-)) (in ?* ?? ?! ?# ?& ?\s)
+                                (: ?- eot))))
+               (`(,_ ,user ,host) (and (not (string-match rejectrx userhost))
+                                       (erc--parse-nuh userhost))))
+    (when host
+      (erc-update-user-nick (erc-current-nick) nil host user)
+      (erc-display-message parsed 'notice 'active 's396 ?s userhost))))
+
 (define-erc-response-handler (401)
   "No such nick/channel." nil
   (let ((nick/channel (cadr (erc-response.command-args parsed))))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index bb05f17bee6..3e45949688d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -7110,9 +7110,9 @@ erc-put-text-property
 (defalias 'erc-list 'ensure-list)
 
 (defconst erc--parse-user-regexp-pedantic
-  (rx bot (group (* (not (any "!\r\n"))))
-      "!" (group (* nonl))
-      "@" (group (* nonl)) eot))
+  (rx bot (? (? (group (+ (not (any "!@\r\n"))))) "!")
+      (? (? (group (+ nonl))) "@")
+      (? (group (+ nonl))) eot))
 
 (defconst erc--parse-user-regexp-legacy
   "^\\([^!\n]*\\)!\\([^@\n]*\\)@\\(.*\\)$")
@@ -7136,6 +7136,16 @@ erc-parse-user
    (t
     (list string "" ""))))
 
+(defun erc--parse-nuh (string)
+  "Match STRING against `erc--parse-user-regexp-pedantic'.
+Return matching groups or nil.  Interpret a lone token or one
+with only a leading \"!\" as a host.  See associated unit test
+for precise behavior."
+  (when (string-match erc--parse-user-regexp-pedantic string)
+    (list (match-string 1 string)
+          (match-string 2 string)
+          (match-string 3 string))))
+
 (defun erc-extract-nick (string)
   "Return the nick corresponding to a user specification STRING.
 
@@ -8863,6 +8873,7 @@ english
    (s368   . "Banlist of %c ends.")
    (s379   . "%c: Forwarded to %f")
    (s391   . "The time at %s is %t")
+   (s396   . "Your visible host has changed to %s")
    (s401   . "%n: No such nick/channel")
    (s402   . "%c: No such server")
    (s403   . "%c: No such channel")
diff --git a/test/lisp/erc/erc-scenarios-misc-commands.el b/test/lisp/erc/erc-scenarios-misc-commands.el
index 2a36d52b835..b96782cf29c 100644
--- a/test/lisp/erc/erc-scenarios-misc-commands.el
+++ b/test/lisp/erc/erc-scenarios-misc-commands.el
@@ -91,4 +91,36 @@ erc-scenarios-misc-commands--SQUERY
         (funcall expect -0.1 "Incorrect arguments")
         (funcall expect 10 "See also: HELP EXAMPLES")))))
 
+;; Note that as of ERC 5.6, there is no actual slash-command function
+;; named `erc-cmd-vhost'.  At the moment, this test merely exists to
+;; assert that the `erc-server-396' response handler updates the rolls
+;; correctly.
+(ert-deftest erc-scenarios-misc-commands--VHOST ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "commands")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'vhost))
+       ;; As of ERC 5.6, we must join a channel before ERC adds itself
+       ;; to `erc-server-users'.  Without such an entry, there's
+       ;; nothing to update when the 396 arrives.
+       (erc-autojoin-channels-alist '((foonet "#chan")))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to server")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (funcall expect 10 "debug mode")))
+
+    (ert-info ("Send VHOST")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (erc-scenarios-common-say "/VHOST tester changeme")
+        (funcall expect 10 "visible host")
+        (should (string= (erc-server-user-host (erc-get-server-user "tester"))
+                         "some.host.test.cc"))))))
+
 ;;; erc-scenarios-misc-commands.el ends here
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 28c1e403e41..ac6eb6b5e3c 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -632,8 +632,6 @@ erc-lurker-maybe-trim
     (should (string= "nick" (erc-lurker-maybe-trim "nick-_`")))))
 
 (ert-deftest erc-parse-user ()
-  (should (equal erc--parse-user-regexp erc--parse-user-regexp-legacy))
-
   (should (equal '("" "" "") (erc-parse-user "!@")))
   (should (equal '("" "!" "") (erc-parse-user "!!@")))
   (should (equal '("" "" "@") (erc-parse-user "!@@")))
@@ -646,23 +644,36 @@ erc-parse-user
   (should (equal '("abc" "123" "fake") (erc-parse-user "abc!123@fake")))
   (should (equal '("abc" "!123" "@xy") (erc-parse-user "abc!!123@@xy")))
 
-  (should (equal '("de" "fg" "xy") (erc-parse-user "abc\nde!fg@xy")))
+  (should (equal '("de" "fg" "xy") (erc-parse-user "abc\nde!fg@xy"))))
+
+(ert-deftest erc--parse-nuh ()
+  (should (equal '(nil nil nil) (erc--parse-nuh "!@")))
+  (should (equal '(nil nil nil) (erc--parse-nuh "@")))
+  (should (equal '(nil nil nil) (erc--parse-nuh "!")))
+  (should (equal '(nil "!" nil) (erc--parse-nuh "!!@")))
+  (should (equal '(nil "@" nil) (erc--parse-nuh "!@@")))
+  (should (equal '(nil "!@" nil) (erc--parse-nuh "!!@@")))
+
+  (should (equal '("abc" nil nil) (erc--parse-nuh "abc!")))
+  (should (equal '(nil "abc" nil) (erc--parse-nuh "abc@")))
+  (should (equal '(nil "abc" nil) (erc--parse-nuh "!abc@")))
 
-  (ert-info ("`erc--parse-user-regexp-pedantic'")
-    (let ((erc--parse-user-regexp erc--parse-user-regexp-pedantic))
-      (should (equal '("" "" "") (erc-parse-user "!@")))
-      (should (equal '("" "!" "") (erc-parse-user "!!@")))
-      (should (equal '("" "@" "") (erc-parse-user "!@@")))
-      (should (equal '("" "!@" "") (erc-parse-user "!!@@")))
+  (should (equal '("abc" "123" "fake") (erc--parse-nuh "abc!123@fake")))
+  (should (equal '("abc" "!123@" "xy") (erc--parse-nuh "abc!!123@@xy")))
 
-      (should (equal '("abc" "" "") (erc-parse-user "abc")))
-      (should (equal '("" "123" "fake") (erc-parse-user "!123@fake")))
-      (should (equal '("abc" "" "123") (erc-parse-user "abc!123")))
+  ;; Missing leading components.
+  (should (equal '(nil "abc" "123") (erc--parse-nuh "abc@123")))
+  (should (equal '(nil "123" "fake") (erc--parse-nuh "!123@fake")))
+  (should (equal '(nil nil "gnu.org") (erc--parse-nuh "@gnu.org")))
 
-      (should (equal '("abc" "123" "fake") (erc-parse-user "abc!123@fake")))
-      (should (equal '("abc" "!123@" "xy") (erc-parse-user "abc!!123@@xy")))
+  ;; Host "wins" over nick and user (sans "@").
+  (should (equal '(nil nil "abc") (erc--parse-nuh "abc")))
+  (should (equal '(nil nil "gnu.org") (erc--parse-nuh "gnu.org")))
+  (should (equal '(nil nil "gnu.org") (erc--parse-nuh "!gnu.org")))
+  (should (equal '("abc" nil "123") (erc--parse-nuh "abc!123")))
 
-      (should (equal '("de" "" "fg@xy") (erc-parse-user "abc\nde!fg@xy"))))))
+  ;; No fallback behavior.
+  (should-not (erc--parse-nuh "abc\nde!fg@xy")))
 
 (ert-deftest erc--parsed-prefix ()
   (erc-mode)
diff --git a/test/lisp/erc/resources/commands/vhost.eld b/test/lisp/erc/resources/commands/vhost.eld
new file mode 100644
index 00000000000..42013198fbc
--- /dev/null
+++ b/test/lisp/erc/resources/commands/vhost.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 10 "PASS :changeme"))
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
+
+((join 10 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode-chan 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!"))
+
+((vhost 10 "VHOST tester changeme")
+ (0 ":irc.foonet.org NOTICE tester :Setting your VHost: some.host.test.cc")
+ (0 ":irc.foonet.org 396 tester some.host.test.cc :is now your displayed host")
+ (0 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden."))
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0008-5.6-Clarify-warning-for-process-dependent-input-in-E.patch --]
[-- Type: text/x-patch, Size: 3117 bytes --]

From 62797d2b97fd8aa00dd45f778555f3c78ea1a6c1 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 27 Nov 2023 19:41:09 -0800
Subject: [PATCH 08/11] [5.6] Clarify warning for process-dependent input in
 ERC

* lisp/erc/erc.el (erc--check-prompt-input-for-running-process):
Resolve dissonance between content of ancient `user-error' message and
condition that triggered it by favoring the former because it's
supported by the underlying mechanism, which revolves around the
`process-not-needed' symbol property.
* test/lisp/erc/erc-tests.el (erc--check-prompt-input-functions):
Revise expected output for error assertion.  (Bug#66073, originally
from bug#54536)
---
 lisp/erc/erc.el            | 17 +++++++++++++----
 test/lisp/erc/erc-tests.el |  4 ++--
 2 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 3e45949688d..98621302abd 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -7318,11 +7318,20 @@ erc--check-prompt-input-for-point-in-bounds
   (when (< (point) (erc-beg-of-input-line))
     "Point is not in the input area"))
 
+;; Originally, `erc-send-current-line' inhibited sends whenever a
+;; server buffer was missing.  In 2007, this was narrowed to
+;; occurrences involving process-dependent commands.  However, the
+;; accompanying error message, which was identical to that emitted by
+;; `erc-server-send', "ERC: No process running", was always inaccurate
+;; because a server buffer can be alive and its process dead.
 (defun erc--check-prompt-input-for-running-process (string _)
-  "Return non-nil unless in an active ERC server buffer."
-  (unless (or (erc-server-buffer-live-p)
-              (erc-command-no-process-p string))
-    "ERC: No process running"))
+  "Return non-nil if STRING is a slash command missing a process.
+Also do so when the server buffer has been killed."
+  ;; Even if the server buffer has been killed, the user should still
+  ;; be able /reconnect and recall previous commands.
+  (and (not (erc-command-no-process-p string))
+       (or (and (not (erc-server-buffer-live-p)) "Server buffer missing")
+           (and (not (erc-server-process-alive)) "Process not running"))))
 
 (defun erc--check-prompt-input-for-multiline-command (line lines)
   "Return non-nil when non-blank lines follow a command line."
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index ac6eb6b5e3c..eb954112ce8 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1414,9 +1414,9 @@ erc--check-prompt-input-functions
        (ert-info ("Input remains untouched")
          (should (save-excursion (erc-bol) (looking-at "/msg #chan hi")))))
 
-     (ert-info ("Errors when no process running")
+     (ert-info ("Errors when server buffer absent")
        (let ((e (should-error (erc-send-current-line))))
-         (should (equal "ERC: No process running" (cadr e))))
+         (should (equal "Server buffer missing" (cadr e))))
        (ert-info ("Input remains untouched")
          (should (save-excursion (erc-bol) (looking-at "/msg #chan hi")))))
 
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0009-5.6-Make-erc-get-user-mode-prefix-more-flexible.patch --]
[-- Type: text/x-patch, Size: 5964 bytes --]

From 401bc01ace91aac6f3423fc8a04fe7a95b67987c Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 27 Nov 2023 22:53:00 -0800
Subject: [PATCH 09/11] [5.6] Make erc-get-user-mode-prefix more flexible

* lisp/erc/erc-speedbar.el (erc-speedbar-insert-user): Use
`erc-get-channel-membership-prefix' so that nicks in the nickbar can
have prefixes beyond just voice and op.
* lisp/erc/erc.el (erc-get-user-mode-prefix,
erc-get-channel-membership-prefix): Rename former to latter because
"user-mode" suggests the function somehow involves user modes, but it
exclusively concerns channel modes.  Also, overload the only parameter
in order to avoid redundantly looking up `erc-channel-user' object
with every predicate call.  In the near future, ERC will likely need
to offer an alternate version of this function that returns multiple
prefixes instead of just one.
(erc-format-@nick): Use `channel-data' parameter.
(erc-format-my-nick, erc--format-channel-status-prefix): Use new name
for function `erc-get-user-mode-prefix'.  (Bug#63595)
---
 etc/ERC-NEWS             |  7 +++++++
 lisp/erc/erc-speedbar.el |  4 +---
 lisp/erc/erc.el          | 30 ++++++++++++++++++++----------
 3 files changed, 28 insertions(+), 13 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 540d9e98751..4fdfe7a9dcb 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -433,6 +433,13 @@ The 'fill' module is now defined by 'define-erc-module'.  The same
 goes for ERC's imenu integration, which has 'imenu' now appearing in
 the default value of 'erc-modules'.
 
+*** Function 'erc-get-user-mode-prefix' renamed.
+This utility has been renamed to 'erc-get-channel-membership-prefix'
+to better reflect its role of delivering a formatted "status prefix",
+like "+" (for "voice"), and to avoid confusion with user modes, like
+"+i" (for "invisible").  Additionally, its lone parameter is now
+overloaded to accept an 'erc-channel-user' object as well as a string.
+
 *** Hidden messages contain a preceding rather than trailing newline.
 ERC has traditionally only offered to hide messages involving fools,
 but plans are to make hiding more powerful.  Anyone depending on the
diff --git a/lisp/erc/erc-speedbar.el b/lisp/erc/erc-speedbar.el
index 93be7b9f074..90d7376fc0c 100644
--- a/lisp/erc/erc-speedbar.el
+++ b/lisp/erc/erc-speedbar.el
@@ -319,9 +319,7 @@ erc-speedbar-insert-user
 	 (info (erc-server-user-info user))
 	 (login (erc-server-user-login user))
 	 (name (erc-server-user-full-name user))
-	 (voice (and cuser (erc-channel-user-voice cuser)))
-	 (op (and cuser (erc-channel-user-op cuser)))
-	 (nick-str (concat (if op "@" "") (if voice "+" "") nick))
+         (nick-str (concat (erc-get-channel-membership-prefix cuser) nick))
 	 (finger (concat login (when (or login host) "@") host))
          (sbtoken (list finger name info (buffer-name buffer))))
     (if (or login host name info) ; we want to be expandable
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 98621302abd..8e7162fec89 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -5815,21 +5815,31 @@ erc-format-nick
 See also `erc-format-nick-function'."
   (when user (erc-server-user-nickname user)))
 
-(defun erc-get-user-mode-prefix (user)
+(define-obsolete-function-alias 'erc-get-user-mode-prefix
+  #'erc-get-channel-membership-prefix "30.1")
+(defun erc-get-channel-membership-prefix (user)
+  "Return channel membership prefix for USER as a string.
+Ensure returned string has a `help-echo' text property with the
+corresponding verbose membership type, like \"voice\", as its
+value.  Expect USER to be an `erc-channel-user' object or a
+string nickname, not necessarily downcased."
   (when user
-    (cond ((erc-channel-user-owner-p user)
+    (when (stringp user)
+      (setq user (and erc-channel-users (cdr (erc-get-channel-user user)))))
+    (cond ((null user) "")
+          ((erc-channel-user-owner user)
            (propertize "~" 'help-echo "owner"))
-          ((erc-channel-user-admin-p user)
+          ((erc-channel-user-admin user)
            (propertize "&" 'help-echo "admin"))
-          ((erc-channel-user-op-p user)
+          ((erc-channel-user-op user)
            (propertize "@" 'help-echo "operator"))
-          ((erc-channel-user-halfop-p user)
+          ((erc-channel-user-halfop user)
            (propertize "%" 'help-echo "half-op"))
-          ((erc-channel-user-voice-p user)
+          ((erc-channel-user-voice user)
            (propertize "+" 'help-echo "voice"))
           (t ""))))
 
-(defun erc-format-@nick (&optional user _channel-data)
+(defun erc-format-@nick (&optional user channel-data)
   "Format the nickname of USER showing if USER has a voice, is an
 operator, half-op, admin or owner.  Owners have \"~\", admins have
 \"&\", operators have \"@\" and users with voice have \"+\" as a
@@ -5838,7 +5848,7 @@ erc-format-@nick
   (when user
     (let ((nick (erc-server-user-nickname user)))
       (concat (propertize
-               (erc-get-user-mode-prefix nick)
+               (erc-get-channel-membership-prefix channel-data)
                'font-lock-face 'erc-nick-prefix-face)
 	      nick))))
 
@@ -5848,7 +5858,7 @@ erc-format-my-nick
       (let* ((open "<")
              (close "> ")
              (nick (erc-current-nick))
-             (mode (erc-get-user-mode-prefix nick)))
+             (mode (erc-get-channel-membership-prefix nick)))
         (erc--ensure-spkr-prop nick)
         (concat
          (propertize open 'font-lock-face 'erc-default-face)
@@ -8486,7 +8496,7 @@ erc--format-user-modes
 (defun erc--format-channel-status-prefix ()
   "Return the current channel membership prefix."
   (and (erc--target-channel-p erc--target)
-       (erc-get-user-mode-prefix (erc-current-nick))))
+       (erc-get-channel-membership-prefix (erc-current-nick))))
 
 (defun erc--format-modes (&optional no-query-p)
   "Return a string of channel modes in channels and user modes elsewhere.
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #11: 0010-5.6-Skip-erc-ignored-user-p-when-erc-ignore-list-is-.patch --]
[-- Type: text/x-patch, Size: 3545 bytes --]

From 545e0fbd648fb7129d557c8c43d87ce0fea891f5 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 11 Sep 2023 21:21:42 -0700
Subject: [PATCH 10/11] [5.6] Skip erc-ignored-user-p when erc-ignore-list is
 empty

* lisp/erc/erc-backend.el (erc-server-PRIVMSG): Don't bother running
`erc-ignored-user-p' and `erc-ignored-reply-p' when their associated
options are null.  The option `erc-ignore-list' is buffer-local when
set, and `erc-ignored-user-p' looks for it in the server buffer.  But
response handlers run in server buffers anyway, so this shouldn't
break anything.  Also remove unneeded call to reassign trailing
response contents.
* lisp/erc/erc-common.el (erc-get-server-user): Rearrange so
`erc-with-server-buffer' doesn't have to switch to the server buffer
because `erc-downcase' can run in channels as well.
* lisp/erc/erc.el (erc-ignored-user-p): Add comment.
---
 lisp/erc/erc-backend.el | 7 ++++---
 lisp/erc/erc-common.el  | 5 +++--
 lisp/erc/erc.el         | 2 ++
 3 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index d548a788bc0..0f6f7e2d4c3 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1913,8 +1913,10 @@ erc--server-determine-join-display-context
         (tgt (car (erc-response.command-args parsed)))
         (msg (erc-response.contents parsed)))
     (defvar erc-minibuffer-ignored)
-    (if (or (erc-ignored-user-p sender-spec)
-            (erc-ignored-reply-p msg tgt proc))
+    (defvar erc-ignore-list)
+    (defvar erc-ignore-reply-list)
+    (if (or (and erc-ignore-list (erc-ignored-user-p sender-spec))
+            (and erc-ignore-reply-list (erc-ignored-reply-p msg tgt proc)))
         (when erc-minibuffer-ignored
           (message "Ignored %s from %s to %s" cmd sender-spec tgt))
       (defvar erc--msg-prop-overrides)
@@ -1932,7 +1934,6 @@ erc--server-determine-join-display-context
                                         ,@erc--msg-prop-overrides))
              s buffer
              fnick)
-        (setf (erc-response.contents parsed) msg)
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
         ;; Even worth checking for empty target here? (invalid anyway)
         (unless (or buffer noticep (string-empty-p tgt) (eq ?$ (aref tgt 0))
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index cb820c812b3..d343a148afc 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -498,8 +498,9 @@ erc-get-channel-user
 (define-inline erc-get-server-user (nick)
   "Find NICK in the current server's `erc-server-users' hash table."
   (inline-letevals (nick)
-    (inline-quote (erc-with-server-buffer
-                    (gethash (erc-downcase ,nick) erc-server-users)))))
+    (inline-quote
+     (gethash (erc-downcase ,nick)
+              (erc-with-server-buffer erc-server-users)))))
 
 (defmacro erc--with-dependent-type-match (type &rest features)
   "Massage Custom :type TYPE with :match function that pre-loads FEATURES."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 8e7162fec89..4c56bc8e058 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -7737,6 +7737,8 @@ erc-delete-query
         (setq erc-default-recipients d2)
       (error "Current target is not a QUERY"))))
 
+;; FIXME move all ignore-related functionality to its own module,
+;; required and enabled by default (until some major version change).
 (defun erc-ignored-user-p (spec)
   "Return non-nil if SPEC matches something in `erc-ignore-list'.
 
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #12: 0011-5.6-Use-templates-for-formatting-chat-messages-in-ER.patch --]
[-- Type: text/x-patch, Size: 86889 bytes --]

From cd4222ba2636bed57c4fe27b25b0e840f376c304 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 26 Nov 2023 18:24:48 -0800
Subject: [PATCH 11/11] [5.6] Use templates for formatting chat messages in ERC

* doc/misc/erc.texi: Replace option `erc-format-nick-function' with
`erc-show-speaker-membership-status'.
* etc/ERC-NEWS: Mention shift to template-based speaker formatting.
* lisp/erc/erc-backend.el
(erc--determine-speaker-message-format-args): Add forward-declaration.
(erc--statusmsg-target): New utility function for detecting whether
the current target is status-prefixed.
(erc-current-message-catalog): Move here from lisp/erc/erc.el.
(erc--message-speaker-catalog): New variable.
(erc--speaker-status-prefix-wanted-p): New variable specifically for
the function `erc-format-@nick' to signal it wants status-prefixes
prepended to the displayed nick.
(erc-server-PRIVMSG): Bind `erc--msg-prop-overrides' and
`erc-current-message-catalog'.  Detect status messages and inform
formatting function of verdict.  Call
`erc--determine-speaker-message-format-args' instead of
`erc-format-privmessage' when possible, and pass a "catalog key"
instead of a pre-formatted message to `erc-display-message'.
* lisp/erc/erc-common.el (erc--ctcp-response): New "subsclass" of
`erc-response' for smuggling extra information to CTCP query handlers
in a mostly backwards-compatible way.
* lisp/erc/erc-dcc.el (erc-dcc-chat-filter): Add `erc--spkr' property
and `erc-speaker' property even though these chat buffers are not
`erc-mode' buffers.
* lisp/erc/erc-fill.el (erc-fill--wrap-continued-message-p):
Rework to look for `erc--spkr' `erc--msg' combinations as indicators
of message "mergeability".
(erc-fill--wrap-rejigger-region): Remove reference to
`erc-stamp-type'.  Instead use the `erc--msg' property combined with
the `erc-timestamp' field to detect date stamps because, at the
moment, all are left-sided.
* lisp/erc/erc-stamp.el (erc-stamp--propertize-left-date-stamp): Don't
add `erc-stamp-type' property.
* lisp/erc/erc.el (erc--msg-props): Revise definition of `erc--msg',
removing ambiguous `msg' value and replacing with catalog keys,
`erc-display-message' "type" parameters, and `unknown'.
(erc--use-language-catalog-for-ctcp-action-p): New
variable, a compatibility switch to help transition from the `ACTION'
entry of the language catalog to the corresponding `ctcp-action'
entries of the new `speaker' catalog.
(erc--send-action-display): Restore pre-5.6 behavior when
compatibility flag is enabled.  Otherwise, use new `speaker' template
for formatting inserted message.
(erc-display-message): Don't add null-valued overrides.
(erc--send-message-external): Overhaul to behave more faithfully in
mimicking a line submitted at the prompt of the current target buffer.
(erc--own-property-names): Remove `erc-stamp-type'.
(erc-ensure-target-buffer-on-privmsg):  Add new
choice variant and slightly change meaning of default.
(erc-format-nick-function,
erc-speaker-from-channel-member-function): Alias to latter, and
redefine purpose slightly.
(erc--message-speaker-statusmsg, erc--message-speaker-statusmsg-input,
erc--message-speaker-input, erc--message-speaker-input-chan-privmsg,
erc--message-speaker-input-chan-notice,
erc--message-speaker-input-query-privmsg,
erc--message-speaker-input-query-notice,
erc--message-speaker-chan-privmsg, erc--message-speaker-query-privmsg,
erc--message-speaker-chan-notice, erc--message-speaker-query-notice,
erc--message-speaker-ctcp-action,
erc--message-speaker-ctcp-action-input,
erc--message-speaker-ctcp-action-statusmsg,
erc--message-speaker-ctcp-action-input-statusmsg): New variables for
new `speaker' format-template catalog.
(erc--speakerize-nick): New helper function.
(erc--determine-speaker-message-format-args): New function to find the
appropriate format key from various contextual parameters.  Could
become the default of a function-valued variable for internal use.
(erc-show-speaker-membership-status): New option.
(erc-format-nick-function, erc-determine-speaker-from-user): Rename
former to latter and obsolete the old name.
(erc-format-nick, erc-determine-speaker-from-user): Rename former to
latter and obsolete old name.
(erc-format-@nick): Deprecate and adapt for use with new
template-based paradigm.
(erc-format-my-nick): Move `erc-speaker' text prop toward head of
list.
(erc--format-speaker-input-message): New function to replace
`erc-format-my-nick' in-tree.
(erc-process-ctcp-query): Don't bind `erc--msg' to `msg'.  Instead,
rely on `erc-display-message' to set it to the current template key.
(erc-ctcp-query-ACTION): Prefer using formatting template, but attempt
to supply pre-5.6 behavior when compatibility flag enabled.
(erc-display-msg): Use `erc--format-speaker-input-message' instead of
`erc-format-my-nick'.
(erc-current-message-catalog): Move to erc-backend.el.
* test/lisp/erc/erc-scenarios-stamp.el
(erc-scenarios-stamp--left/display-margin-mode): Expect format catalog
key instead of unhelpful `msg' as value of `erc--msg' prop.
* test/lisp/erc/erc-tests.el (erc-message): Render format template in
mock function and expect string in assertions.
(erc-tests--format-privmessage): New function, a helper for the
following test.
(erc-format-privmessage, erc--determine-message-format-args): Rename
former to latter and suppress deprecation warning.
(erc--determine-speaker-message-format-args/queries,
erc--determine-speaker-message-format-args/queries-as-channel): New
tests.
(erc-tests--format-my-nick): New helper function for the following
test.
(erc--format-speaker-input-message): New test.
* test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld: Update.
* test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld:
Update.
* test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld:
Update.
---
 doc/misc/erc.texi                             |  11 +-
 etc/ERC-NEWS                                  |  53 +++
 lisp/erc/erc-backend.el                       |  87 +++--
 lisp/erc/erc-common.el                        |  17 +
 lisp/erc/erc-dcc.el                           |   6 +-
 lisp/erc/erc-fill.el                          |  69 ++--
 lisp/erc/erc-stamp.el                         |   3 +-
 lisp/erc/erc.el                               | 366 +++++++++++++++---
 test/lisp/erc/erc-scenarios-stamp.el          |   2 +-
 test/lisp/erc/erc-tests.el                    | 231 +++++++++--
 .../fill/snapshots/merge-wrap-01.eld          |   2 +-
 .../merge-wrap-indicator-post-01.eld          |   2 +-
 .../snapshots/merge-wrap-indicator-pre-01.eld |   2 +-
 13 files changed, 687 insertions(+), 164 deletions(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index d7260ffa329..0eb2ae69aef 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -918,16 +918,11 @@ Connecting
 other nicks are tried in the list order.
 @end defopt
 
-@defopt erc-format-nick-function
-A function to format a nickname for message display
-
-You can set this to @code{erc-format-@@nick} to display user mode prefix
+@defopt erc-show-speaker-membership-status
+A boolean for including a channel member's @dfn{status prefix} in
+their display name when they speak.
 @end defopt
 
-@example
-(setq erc-format-nick-function 'erc-format-@@nick)
-@end example
-
 @defopt erc-nick-uniquifier
 The string to append to the nick if it is already in use.
 @end defopt
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 4fdfe7a9dcb..56d62cd63bd 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -275,6 +275,26 @@ buffers.  In channels, it's grown to include all letters and their
 possibly truncated arguments, with the exception of stateful list
 modes, like "b".
 
+** In-buffer "status messages" are now a thing.
+The ancient option 'erc-ensure-target-buffer-on-privmsg' has been
+repurposed slightly to express a third state denoted by the symbol
+'status', which tells ERC to revert to the old default behavior in
+which separate, "pseudo" target buffers for status-prefixed conversing
+co-exist alongside actual target buffers.  Instead of this awkward
+arrangement, ERC now acts like other clients by default and inserts
+so-called "status messages" in situ, right next to other messages.
+Similar insertion-routing behavior now also applies to CTCP ACTIONs
+directed at status-prefixed channels.  Unfortunately, outgoing "/msg
+@#chan hi" messages aren't yet shown in the same fashion, but the
+groundwork has been laid, making such an addition almost trivial.
+
+** An easier way get channel-membership prefixes on speakers.
+The option 'erc-format-@nick' has been deprecated in favor of the new
+boolean option 'erc-show-speaker-membership-status', a simple switch
+to enable the displaying of status prefixes on the speaker nicks of
+incoming chat messages.  Prefixes on your speaker nick for outgoing
+chat messages continue to always be present.
+
 ** Miscellaneous UX changes.
 Some minor quality-of-life niceties have finally made their way to
 ERC.  For example, fool visibility has become togglable with the new
@@ -526,6 +546,39 @@ The functions 'erc-define-catalog-entry' and 'erc-define-catalog' have
 been deprecated in favor of 'erc-define-message-format-catalog', a new
 macro for defining template "catalogs" at the top level of libraries.
 
+*** Interface for determining display names renamed.
+The option 'erc-format-nick-function' has been renamed to
+'erc-speaker-from-channel-member-function' to better reflect its
+actual role.  So too has the related function 'erc-format-nick', which
+is now 'erc-determine-speaker-from user'.
+
+*** A template-based approach to formatting inserted chat messages.
+Predicting and influencing how ERC formats messages containing a
+leading "<speaker>" has never been straightforward.  The characters
+bracketing the speaker and the faces used for each component have
+always been hard-coded, with 'erc-format-query-as-channel-p' being the
+only knob of any consequence.  With this release, ERC begins its
+transition to a unified formatting paradigm that builds upon the
+already familiar "language catalog" templating system.  Using a
+separate "speaker catalog" keyed by contextual symbols, like
+'query-privmsg', ERC (and eventually everyone) will more easily be
+able to influence how inserted messages take shape in buffers.
+
+*** New formatting templates for inserted CTCP ACTION messages.
+In 5.5 and earlier, ERC displayed outgoing CTCP ACTION messages in
+'erc-input-face' alone (before buttonizing).  Incoming ACTION messages
+mirrored this, except with 'erc-action-face' throughout.  Going
+forward, inserted outgoing "/ME" messages will also incorporate
+'erc-action-face', only underneath 'erc-input-face', with
+'erc-my-nick-face' sitting atop both in the bracketed "speaker"
+(nickname) portion (again, pre-buttonizing).  This new behavior
+sidesteps the traditional format template 'erc-message-english-ACTION'
+from the default "language catalog" in favor of an entry from the new
+internal "speaker catalog".  Users needing to access the old behavior
+can do so by tweaking the latter two variables or by toggling a
+provided compatibility switch.  See source code around the function
+'erc-send-action' for details.
+
 *** Miscellaneous changes
 Two helper macros from GNU ELPA's Compat library are now available to
 third-party modules as 'erc-compat-call' and 'erc-compat-function'.
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 0f6f7e2d4c3..438d7e0c00a 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -142,7 +142,6 @@ erc-verbose-server-ping
 (declare-function erc-display-server-message "erc" (_proc parsed))
 (declare-function erc-emacs-time-to-erc-time "erc" (&optional specified-time))
 (declare-function erc-format-message "erc" (msg &rest args))
-(declare-function erc-format-privmessage "erc" (nick msg privp msgp))
 (declare-function erc-get-buffer "erc" (target &optional proc))
 (declare-function erc-handle-login "erc" nil)
 (declare-function erc-handle-user-status-change "erc" (type nlh &optional l))
@@ -173,6 +172,9 @@ erc-verbose-server-ping
 (declare-function erc-update-mode-line-buffer "erc" (buffer))
 (declare-function erc-wash-quit-reason "erc" (reason nick login host))
 
+(declare-function erc--determine-speaker-message-format-args "erc"
+                  (nick target message queryp privmsgp statusmsgp inputp
+                        &optional prefix disp-nick))
 (declare-function erc-display-message "erc"
                   (parsed type buffer msg &rest args))
 (declare-function erc-get-buffer-create "erc"
@@ -1906,6 +1908,29 @@ erc--server-determine-join-display-context
          ?s (if (/= erc-server-lag 1) "s" "")))
       (erc-update-mode-line))))
 
+(defun erc--statusmsg-target (target)
+  (and-let* ((erc-ensure-target-buffer-on-privmsg)
+             ((not (eq erc-ensure-target-buffer-on-privmsg 'status)))
+             ((not (erc-channel-p target)))
+             (chars (erc--get-isupport-entry 'STATUSMSG 'single))
+             ((string-search (string (aref target 0)) chars))
+             (trimmed (substring target 1))
+             ((erc-channel-p trimmed)))
+    trimmed))
+
+;; Moved to this file from erc.el in ERC 5.6.
+(defvar-local erc-current-message-catalog 'english
+  "Current language or context catalog for formatting inserted messages.
+See `erc-format-message'.")
+
+;; This variable can be converted into a user option if the current
+;; design proves hospitable enough to user expectations.
+(defvar erc--message-speaker-catalog '-speaker
+  "The \"speaker\" catalog used to format PRIVMSGs and NOTICEs.")
+
+(defvar erc--speaker-status-prefix-wanted-p (gensym "erc-")
+  "Sentinel to detect whether `erc-format-@nick' has just run.")
+
 (define-erc-response-handler (PRIVMSG NOTICE)
   "Handle private messages, including messages in channels." nil
   (let ((sender-spec (erc-response.sender parsed))
@@ -1927,12 +1952,14 @@ erc--server-determine-join-display-context
              (msgp (string= cmd "PRIVMSG"))
              (noticep (string= cmd "NOTICE"))
              ;; S.B. downcase *both* tgt and current nick
-             (privp (erc-current-nick-p tgt))
+             (medown (erc-downcase (erc-current-nick)))
+             (privp (string= (erc-downcase tgt) medown))
              (erc--display-context `((erc-buffer-display . ,(intern cmd))
                                      ,@erc--display-context))
-             (erc--msg-prop-overrides `((erc--msg . msg)
-                                        ,@erc--msg-prop-overrides))
-             s buffer
+             (erc--msg-prop-overrides `((erc--tmp) ,@erc--msg-prop-overrides))
+             (erc--speaker-status-prefix-wanted-p nil)
+             (erc-current-message-catalog erc--message-speaker-catalog)
+             s buffer statusmsgp status
              fnick)
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
         ;; Even worth checking for empty target here? (invalid anyway)
@@ -1950,9 +1977,14 @@ erc--server-determine-join-display-context
                 (push `(erc-receive-query-display . ,(intern cmd))
                       erc--display-context)
                 (setq buffer (erc--open-target nick)))
-            ;; A channel buffer has been killed but is still joined.
-            (when erc-ensure-target-buffer-on-privmsg
-              (setq buffer (erc--open-target tgt)))))
+            (cond
+             ;; Target is a channel and contains leading @+ chars.
+             ((and-let* ((trimmed (erc--statusmsg-target tgt)))
+                (setq buffer (erc-get-buffer trimmed proc)
+                      statusmsgp (and buffer t))))
+             ;; A channel buffer has been killed but is still joined.
+             (erc-ensure-target-buffer-on-privmsg
+              (setq buffer (erc--open-target tgt))))))
         (when buffer
           (with-current-buffer buffer
             (when privp (erc--unhide-prompt))
@@ -1963,36 +1995,47 @@ erc--server-determine-join-display-context
                                        privp nil nil nil nil nil host login nil nil t)
             (defvar erc--cmem-from-nick-function)
             (defvar erc-format-nick-function)
+            (defvar erc-show-speaker-membership-status)
+            (defvar erc-speaker-from-channel-member-function)
             (let ((cdata (funcall erc--cmem-from-nick-function
                                   (erc-downcase nick) sndr parsed)))
-              (setq fnick (funcall erc-format-nick-function
-                                   (car cdata) (cdr cdata))))))
+              (setq fnick (funcall erc-speaker-from-channel-member-function
+                                   (car cdata) (cdr cdata))
+                    status (and (or erc--speaker-status-prefix-wanted-p
+                                    erc-show-speaker-membership-status)
+                                (cdr cdata))))))
         (cond
          ((erc-is-message-ctcp-p msg)
-          (setq s (if msgp
+          ;; FIXME explain undefined return values being assigned to `s'.
+          (setq s (if-let ((parsed
+                            (erc--ctcp-response-from-parsed
+                             :parsed parsed :target tgt :buffer buffer
+                             :dispname fnick :statusmsgp statusmsgp))
+                           (msgp))
                       (erc-process-ctcp-query proc parsed nick login host)
                     (erc-process-ctcp-reply proc parsed nick login host
                                             (match-string 1 msg)))))
          (t
           (setq erc-server-last-peers (cons nick (cdr erc-server-last-peers)))
-          (defvar erc-format-query-as-channel-p)
-          (setq s (erc-format-privmessage
-                   (or fnick nick) msg
-                   ;; If buffer is a query buffer,
-                   ;; format the nick as for a channel.
-                   (and (not (and buffer
-                                  (erc-query-buffer-p buffer)
-                                  erc-format-query-as-channel-p))
-                        privp)
-                   msgp))))
+          (with-current-buffer (or buffer (current-buffer))
+            ;; Re-bind in case either buffer has a local value.
+            (let ((erc-current-message-catalog erc--message-speaker-catalog))
+              (setq s (erc--determine-speaker-message-format-args
+                       nick tgt msg privp msgp statusmsgp
+                       (string= medown (erc-downcase nick)) status fnick))))))
         (when s
           (if (and noticep privp)
               (progn
+                (push (cons 'erc--msg (car s)) erc--msg-prop-overrides)
+                (setq s (apply #'erc-format-message s))
                 (run-hook-with-args 'erc-echo-notice-always-hook
                                     s parsed buffer nick)
                 (run-hook-with-args-until-success
                  'erc-echo-notice-hook s parsed buffer nick))
-            (erc-display-message parsed nil buffer s)))))))
+            ;; Redo text props for "echoed messages" (could instead
+            ;; add new method for `erc--format-privmsg')
+            (apply #'erc-display-message parsed nil buffer
+                   (ensure-list s))))))))
 
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index d343a148afc..c1fb522fb5d 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -100,6 +100,23 @@ erc--target
   (contents "" :type string)
   (tags '() :type list))
 
+(cl-defstruct (erc--ctcp-response
+               (:include erc-response)
+               (:constructor
+                erc--ctcp-response-from-parsed
+                (&key parsed target buffer dispname statusmsgp
+                      &aux (unparsed (erc-response.unparsed parsed))
+                      (sender (erc-response.sender parsed))
+                      (command (erc-response.command parsed))
+                      (command-args (erc-response.command-args parsed))
+                      (contents (erc-response.contents parsed))
+                      (tags (erc-response.tags parsed)))))
+  "Data for a processed CTCP query or reply."
+  (target nil :type string)
+  (buffer nil :type (or buffer null))
+  (dispname nil :type (or string null))
+  (statusmsgp nil :type boolean))
+
 (cl-defstruct erc--isupport-data
   "Abstract \"class\" for parsed ISUPPORT data.
 For use with the macro `erc--with-isupport-data'."
diff --git a/lisp/erc/erc-dcc.el b/lisp/erc/erc-dcc.el
index ac7fc817cb9..d12ebd33a86 100644
--- a/lisp/erc/erc-dcc.el
+++ b/lisp/erc/erc-dcc.el
@@ -1251,14 +1251,16 @@ erc-dcc-chat-filter
 (defun erc-dcc-chat-parse-output (proc str)
   (save-match-data
     (let ((posn 0)
+          (erc--msg-prop-overrides `((erc--spkr . ,erc-dcc-from)))
+          (nick (propertize (erc--speakerize-nick erc-dcc-from)
+                            'font-lock-face 'erc-nick-default-face))
           line)
       (while (string-match "\n" str posn)
         (setq line (substring str posn (match-beginning 0)))
         (setq posn (match-end 0))
         (erc-display-message
          nil nil proc
-         'dcc-chat-privmsg ?n (propertize erc-dcc-from 'font-lock-face
-                                          'erc-nick-default-face) ?m line))
+         'dcc-chat-privmsg ?n nick ?m line))
       (setq erc-dcc-unprocessed-output (substring str posn)))))
 
 (defun erc-dcc-chat-buffer-killed ()
diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index de6cd581fec..32a0db29cf5 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -546,42 +546,38 @@ erc-fill--wrap-length-function
 variable can be converted to a public one if needed by third
 parties.")
 
-(defvar-local erc-fill--wrap-last-msg nil)
-(defvar erc-fill--wrap-max-lull (* 24 60 60))
+(defvar-local erc-fill--wrap-last-msg nil "Marker for merging speakers.")
+(defvar erc-fill--wrap-max-lull (* 24 60 60) "Max secs for merging speakers.")
 
 (defun erc-fill--wrap-continued-message-p ()
   "Return non-nil when the current speaker hasn't changed.
-That is, indicate whether the text just inserted is from the same
-sender as that of the previous \"PRIVMSG\".  As a side effect,
-advance `erc-fill--wrap-last-msg' unless the message has been
-marked as being ephemeral."
-  (and
-   (not (erc--check-msg-prop 'erc--ephemeral))
-   (progn ; preserve blame for now, unprogn on next major change
-     (prog1
-         (and-let*
-             ((m (or erc-fill--wrap-last-msg
-                     (setq erc-fill--wrap-last-msg (point-min-marker))
-                     nil))
-              ((< (1+ (point-min)) (- (point) 2)))
-              (props (save-restriction
-                       (widen)
-                       (and-let*
-                           ((speaker (get-text-property m 'erc--spkr))
-                            ((not (eq (get-text-property m 'erc--ctcp)
-                                      'ACTION)))
-                            ((not (invisible-p m))))
-                         (cons (get-text-property m 'erc--ts) speaker))))
-              (ts (pop props))
-              (props)
-              ((not (time-less-p (erc-stamp--current-time) ts)))
-              ((time-less-p (time-subtract (erc-stamp--current-time) ts)
-                            erc-fill--wrap-max-lull))
-              ;; Assume presence of leading angle bracket or hyphen.
-              (nick (erc--check-msg-prop 'erc--spkr))
-              ((not (erc--check-msg-prop 'erc--ctcp 'ACTION)))
-              ((erc-nick-equal-p props nick))))
-       (set-marker erc-fill--wrap-last-msg (point-min))))))
+But only if the `erc--msg' text property also hasn't.  That is,
+indicate whether the chat message just inserted is from the same
+person as the prior one and is formatted in the same manner.  As
+a side effect, advance `erc-fill--wrap-last-msg' unless the
+message has been marked `erc--ephemeral'."
+  (and-let*
+      (((not (erc--check-msg-prop 'erc--ephemeral)))
+       ;; Always set/move `erc-fill--wrap-last-msg' from here on down.
+       (m (or (and erc-fill--wrap-last-msg
+                   (prog1 (marker-position erc-fill--wrap-last-msg)
+                     (set-marker erc-fill--wrap-last-msg (point-min))))
+              (ignore (setq erc-fill--wrap-last-msg (point-min-marker)))))
+       ((>= (point) 4)) ; skip the first message
+       (props (save-restriction
+                (widen)
+                (and-let* ((speaker (get-text-property m 'erc--spkr))
+                           (type (get-text-property m 'erc--msg))
+                           ((not (invisible-p m))))
+                  (list (get-text-property m 'erc--ts) type speaker))))
+       (ts (nth 0 props))
+       (type (nth 1 props))
+       (speaker (nth 2 props))
+       ((not (time-less-p (erc-stamp--current-time) ts)))
+       ((time-less-p (time-subtract (erc-stamp--current-time) ts)
+                     erc-fill--wrap-max-lull))
+       ((erc--check-msg-prop 'erc--msg type))
+       ((erc-nick-equal-p speaker (erc--check-msg-prop 'erc--spkr))))))
 
 (defun erc-fill--wrap-measure (beg end)
   "Return display spec width for inserted region between BEG and END.
@@ -747,8 +743,11 @@ erc-fill--wrap-rejigger-region
                  ((equal "" dval)))
         (remove-text-properties
          dbeg (text-property-not-all dbeg end 'display dval) '(display)))
-      (let* ((pos (if (eq 'date-left (get-text-property beg 'erc-stamp-type))
-                      (field-beginning beg)
+      ;; This "should" work w/o `front-sticky' and `rear-nonsticky'.
+      (let* ((pos (if-let (((eq 'erc-timestamp (field-at-pos beg)))
+                           (b (field-beginning beg))
+                           ((eq 'datestamp (get-text-property b 'erc--msg))))
+                      b
                     beg))
              (erc--msg-props (map-into (text-properties-at pos) 'hash-table))
              (erc-stamp--current-time (gethash 'erc--ts erc--msg-props)))
diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index a6efa3b5151..9ca3ea320a0 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -660,8 +660,7 @@ erc-stamp--date-format-end
 value of t means the option's value doesn't require trimming.")
 
 (defun erc-stamp--propertize-left-date-stamp ()
-  (add-text-properties (point-min) (1- (point-max))
-                       '(field erc-timestamp erc-stamp-type date-left))
+  (add-text-properties (point-min) (1- (point-max)) '(field erc-timestamp))
   (erc--hide-message 'timestamp)
   (run-hooks 'erc-stamp--insert-date-hook))
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 4c56bc8e058..0c7a2835c7a 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -154,11 +154,10 @@ erc--msg-props
 their markers accordingly.  The following properties have meaning
 as of ERC 5.6:
 
- - `erc--msg': a symbol, guaranteed present; values include:
-   `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--msg': a symbol, guaranteed present; possible values
+    include `unknown', a fallback used by `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
@@ -3017,20 +3016,32 @@ erc--send-action-perform-ctcp
   "Send STRING to TARGET, possibly immediately, with FORCE."
   (erc-send-ctcp-message target (format "ACTION %s" string) force))
 
+(defvar erc--use-language-catalog-for-ctcp-action-p nil
+  "When non-nil, use `ACTION' entry from language catalog for /ME's.
+Otherwise, use `ctcp-action' or `ctcp-action-input' from the
+`speaker' catalog.  This is an escape hatch to restore pre-5.6
+behavior for the `font-lock-face' property of incoming and
+outgoing \"CTCP ACTION\" messages, which displayed entire
+pre-buttonized messages in either `erc-input-face' or
+`erc-action-face'.  If you use this, please ask ERC to expose it
+as a public variable via \\[erc-bug] or similar.")
+
 (defun erc--send-action-display (string)
-  "Display STRING as an outgoing \"CTCP ACTION\" message."
+  "Display STRING as an outgoing \"CTCP ACTION\" message.
+Propertize the message according to the compatibility flag
+`erc--use-language-catalog-for-ctcp-action-p'."
   ;; Allow hooks acting on inserted PRIVMSG and NOTICES to process us.
-  (defvar erc--merge-prop-behind-p)
-  (let* ((nick (erc-current-nick))
-         (erc--msg-prop-overrides `((erc--msg . msg)
-                                    (erc--ctcp . ACTION)
-                                    (erc--spkr . ,nick)
-                                    ,@erc--msg-prop-overrides))
-         (erc--merge-prop-behind-p t))
-    (setq nick (propertize nick 'erc-speaker nick
-                           'font-lock-face 'erc-my-nick-face))
-    (erc-display-message nil '(t input action) (current-buffer)
-                         'ACTION ?n nick ?a string ?u "" ?h "")))
+  (let ((erc--msg-prop-overrides `((erc--ctcp . ACTION)
+                                   ,@erc--msg-prop-overrides))
+        (nick (erc-current-nick)))
+    (if erc--use-language-catalog-for-ctcp-action-p
+        (progn (push (cons 'erc--spkr nick) erc--msg-prop-overrides)
+               (erc-display-message nil 'input (current-buffer) 'ACTION
+                                    ?n (propertize nick 'erc-speaker nick)
+                                    ?a string ?u "" ?h ""))
+      (let ((erc-current-message-catalog erc--message-speaker-catalog))
+        (erc-display-message nil nil (current-buffer) 'ctcp-action-input
+                             ?n (erc--speakerize-nick nick) ?m string)))))
 
 (defun erc--send-action (target string force)
   "Display STRING, then send to TARGET as a \"CTCP ACTION\" message."
@@ -3763,7 +3774,7 @@ erc-display-message
                  (puthash 'erc--cmd cmd table))
                (when erc--msg-prop-overrides
                  (pcase-dolist (`(,k . ,v) (reverse erc--msg-prop-overrides))
-                   (puthash k v table)))
+                   (when v (puthash k v table))))
                table)))
         (erc-message-parsed parsed))
     (setq string
@@ -4661,15 +4672,25 @@ erc-send-message
       (funcall erc--send-message-nested-function line force)
     (erc--send-message-external line force)))
 
-;; FIXME fully simulate `erc-display-msg'.  This doesn't currently add
-;; the correct text properties.  For example, the LINE should have
-;; `erc-default-face'.
 (defun erc--send-message-external (line force)
-  (erc-message "PRIVMSG" (concat (erc-default-target) " " line) force)
-  (erc-display-line
-   (concat (erc-format-my-nick) line)
-   (current-buffer))
+  "Send a \"PRIVMSG\" to the default target with optional FORCE.
+Expect caller to bind `erc-default-recipients' if needing to
+specify a status-prefixed target."
+  ;; Almost like an echoed message, but without the `erc--cmd'.
+  (let* ((erc-current-message-catalog erc--message-speaker-catalog)
+         (target (erc-default-target))
+         (erc--msg-prop-overrides `((erc--tmp) ,@erc--msg-prop-overrides))
+         ;; This util sets the `erc--spkr' property in ^.
+         (statusmsgp (erc--statusmsg-target target))
+         (args (erc--determine-speaker-message-format-args
+                (erc-current-nick) target line
+                (and erc--target (not (erc--target-channel-p erc--target)))
+                'privmsgp statusmsgp 'inputp t)))
+    (erc-message "PRIVMSG" (concat target " " line) force)
+    (push (cons 'erc--msg (car args)) erc--msg-prop-overrides)
+    (apply #'erc-display-message nil nil (current-buffer) args))
   ;; FIXME - treat multiline, run hooks, or remove me?
+  ;; FIXME explain this ^ in more detail or remove.
   t)
 
 (defun erc--send-message-nested (input-line force)
@@ -5284,7 +5305,6 @@ erc--own-property-names
      rear-nonsticky erc-prompt field front-sticky read-only
      ;; stamp
      cursor-intangible cursor-sensor-functions isearch-open-invisible
-     erc-stamp-type
      ;; match
      invisible intangible
      ;; button
@@ -5628,7 +5648,9 @@ erc-ensure-target-buffer-on-privmsg
   :package-version '(ERC . "5.6") ; revived
   :group 'erc-buffers
   :group 'erc-query
-  :type 'boolean)
+  :type '(choice boolean
+                 (choice :tag "Create pseudo queries for STATUSMSGs"
+                         status)))
 
 (defcustom erc-format-query-as-channel-p t
   "If non-nil, format text from others in a query buffer like in a channel.
@@ -5805,14 +5827,207 @@ erc-format-privmessage
                            'font-lock-face msg-face str)
     str))
 
-(defcustom erc-format-nick-function 'erc-format-nick
-  "Function to format a nickname for message display."
+;; Format strings in the following `-speaker' catalog shouldn't
+;; contain any non-protocol words so they make sense in any language.
+
+(defvar erc--message-speaker-statusmsg
+  #("(%p%n) %t: %m"
+    0 1 (font-lock-face erc-default-face)
+    1 3 (font-lock-face erc-nick-prefix-face)
+    3 5 (font-lock-face erc-nick-default-face)
+    5 7 (font-lock-face erc-default-face)
+    7 9 (font-lock-face erc-notice-face)
+    9 13 (font-lock-face erc-default-face))
+  "Message template for in-channel status messages.")
+
+(defvar erc--message-speaker-statusmsg-input
+  #("(%p%n) %t: %m"
+    0 1 (font-lock-face erc-default-face)
+    1 3 (font-lock-face erc-my-nick-prefix-face)
+    3 5 (font-lock-face erc-my-nick-face)
+    5 7 (font-lock-face erc-default-face)
+    7 9 (font-lock-face erc-notice-face)
+    9 13 (font-lock-face erc-input-face))
+  "Message template for echoed status messages.")
+
+(defvaralias 'erc--message-speaker-input
+  'erc--message-speaker-input-chan-privmsg)
+(defvar erc--message-speaker-input-chan-privmsg
+  #("<%p%n> %m"
+    0 1 (font-lock-face erc-default-face)
+    1 3 (font-lock-face erc-my-nick-prefix-face)
+    3 5 (font-lock-face erc-my-nick-face)
+    5 7 (font-lock-face erc-default-face)
+    7 9 (font-lock-face erc-input-face))
+  "Message template for prompt input or echoed PRIVMSG from own nick.")
+
+(defvar erc--message-speaker-input-query-privmsg
+  #("*%n* %m"
+    0 1 (font-lock-face erc-direct-msg-face)
+    1 3 (font-lock-face erc-my-nick-face)
+    3 5 (font-lock-face erc-direct-msg-face)
+    5 7 (font-lock-face erc-input-face))
+  "Message template for prompt input or echoed PRIVMSG query from own nick.")
+
+(defvar erc--message-speaker-input-query-notice
+  #("-%n- %m"
+    0 1 (font-lock-face erc-direct-msg-face)
+    1 3 (font-lock-face erc-my-nick-face)
+    3 5 (font-lock-face erc-direct-msg-face)
+    5 7 (font-lock-face erc-input-face))
+  "Message template for echoed or spoofed query NOTICE from own nick.")
+
+(defvar erc--message-speaker-input-chan-notice
+  #("-%p%n- %m"
+    0 1 (font-lock-face erc-default-face)
+    1 3 (font-lock-face erc-my-nick-prefix-face)
+    3 5 (font-lock-face erc-my-nick-face)
+    5 7 (font-lock-face erc-default-face)
+    7 9 (font-lock-face erc-input-face))
+  "Message template for prompt input or echoed NOTICE from own nick.")
+
+(defvar erc--message-speaker-chan-privmsg
+  #("<%p%n> %m"
+    0 1 (font-lock-face erc-default-face)
+    1 3 (font-lock-face erc-nick-prefix-face)
+    3 5 (font-lock-face erc-nick-default-face)
+    5 9 (font-lock-face erc-default-face))
+  "Message template for a PRIVMSG in a channel.")
+
+(defvar erc--message-speaker-query-privmsg
+  #("*%n* %m"
+    0 1 (font-lock-face erc-direct-msg-face)
+    1 3 (font-lock-face erc-nick-msg-face)
+    3 7 (font-lock-face erc-direct-msg-face))
+  "Message template for a PRIVMSG in query buffer.")
+
+(defvar erc--message-speaker-chan-notice
+  #("-%p%n- %m"
+    0 1 (font-lock-face erc-default-face)
+    1 3 (font-lock-face erc-nick-prefix-face)
+    3 5 (font-lock-face erc-nick-default-face)
+    5 9 (font-lock-face erc-default-face))
+  "Message template for a NOTICE in a channel.")
+
+(defvar erc--message-speaker-query-notice
+  #("-%n- %m"
+    0 1 (font-lock-face erc-direct-msg-face)
+    1 3 (font-lock-face erc-nick-msg-face)
+    3 7 (font-lock-face erc-direct-msg-face))
+  "Message template for a NOTICE in a query buffer.")
+
+(defvar erc--message-speaker-ctcp-action
+  #("* %n %m"
+    0 7 (font-lock-face erc-action-face))
+  "Message template for a CTCP ACTION from another user.")
+
+(defvar erc--message-speaker-ctcp-action-input
+  #("* %n %m"
+    0 2 (font-lock-face #1=(erc-input-face erc-action-face))
+    2 4 (font-lock-face (erc-my-nick-face . #1#))
+    4 7 (font-lock-face #1#))
+  "Message template for a CTCP ACTION from current client.")
+
+(defvar erc--message-speaker-ctcp-action-statusmsg
+  #("* (%n) %t: %m"
+    0 7 (font-lock-face erc-action-face)
+    7 9 (font-lock-face (erc-notice-face erc-action-face))
+    9 13 (font-lock-face erc-action-face))
+  "Template for a CTCP ACTION status message from another chan op.")
+
+(defvar erc--message-speaker-ctcp-action-input-statusmsg
+  #("* (%n) %t: %m"
+    0 3 (font-lock-face #1=(erc-input-face erc-action-face))
+    3 5 (font-lock-face (erc-my-nick-face . #1#))
+    5 7 (font-lock-face #1#)
+    7 9 (font-lock-face (erc-notice-face . #1#))
+    9 13 (font-lock-face #1#))
+  "Template for a CTCP ACTION status message from current client.")
+
+(define-inline erc--speakerize-nick (nick &optional disp)
+  "If NICK is without a`erc-speaker' property, return a copy with it present.
+Propertize DISP instead, if given, but still assign NICK as the
+text property's value."
+  (inline-letevals (nick disp)
+    (inline-quote
+     (let ((plain-nick (substring-no-properties ,nick)))
+       (cond (erc--msg-props
+              (puthash 'erc--spkr plain-nick erc--msg-props)
+              (dolist (entry (get erc--message-speaker-catalog
+                                  'erc--msg-prop-overrides))
+                (puthash (car entry) (cdr entry) erc--msg-props)))
+             (erc--msg-prop-overrides
+              (setq erc--msg-prop-overrides
+                    (append (cons (cons 'erc--spkr plain-nick)
+                                  (get erc--message-speaker-catalog
+                                       'erc--msg-prop-overrides))
+                            erc--msg-prop-overrides))))
+       (if (text-property-not-all 0 (length (or ,disp ,nick))
+                                  'erc-speaker nil (or ,disp ,nick))
+           (or ,disp ,nick)
+         (propertize (or ,disp ,nick) 'erc-speaker plain-nick))))))
+
+(defun erc--determine-speaker-message-format-args
+    (nick target message queryp privmsgp statusmsgp inputp
+          &optional prefix disp-nick)
+  "Return a key for a `speaker' format template along with spec args.
+Consider the flags QUERYP, PRIVMSGP, STATUSMSGP, and INPUTP,
+which describe a newly arrived \"PRIVMSG\"'s message's context,
+and return a key for the appropriate `speaker' catalog, along
+with spec args from the other arguments.  Interpret QUERYP to
+mean the original target (not necessarily TARGET) matched the
+client's own nick after case-folding.  Expect NICK to be the
+nickname of the speaker, TARGET to be the channel or query
+target, and MESSAGE the body of a chat message.  When non-nil,
+expect DISP-NICK to be a possibly phony display name.  When
+PREFIX is non-nil, look up NICK's channel-membership status,
+possibly using PREFIX itself if it's an `erc-channel-user'
+object, which is mandatory outside of channel buffers."
+  ;; At the moment, TARGET and STATUSMSG are only used together in
+  ;; practice.
+  (when prefix
+    (setq prefix (erc-get-channel-membership-prefix
+                  (if (erc-channel-user-p prefix) prefix nick))))
+  (when (and queryp erc--target erc-format-query-as-channel-p
+             (not (erc--target-channel-p erc--target)))
+    (setq queryp nil))
+  (list (cond (statusmsgp (if inputp 'statusmsg-input 'statusmsg))
+              (privmsgp (if queryp
+                            (if inputp 'input-query-privmsg 'query-privmsg)
+                          (if inputp 'input-chan-privmsg 'chan-privmsg)))
+              (t (if queryp
+                     (if inputp 'input-query-notice 'query-notice)
+                   (if inputp 'input-chan-notice 'chan-notice))))
+        ?p (or prefix "") ?n (erc--speakerize-nick nick disp-nick)
+        ?t (or target "") ?m message))
+
+(defcustom erc-show-speaker-membership-status nil
+  "Whether to prefix speakers with their channel status.
+For example, when this option is non-nil and some nick \"Alice\"
+has operator status in the current channel, ERC displays their
+leading \"speaker\" label as <@Alice> instead of <Alice>."
+  :package-version '(ERC . "5.6")
+  :group 'erc-display
+  :type 'boolean)
+
+(define-obsolete-variable-alias 'erc-format-nick-function
+  'erc-speaker-from-channel-member-function "30.1")
+(defcustom erc-speaker-from-channel-member-function
+  #'erc-determine-speaker-from-user
+  "Function to determine a message's displayed \"speaker\" label.
+Called with an `erc-server-user' object and an `erc-channel-user'
+object, both possibly nil.  Use this option to do things like
+provide localized display names.  To ask ERC to prepend
+channel-membership \"status\" prefixes, like \"@\", to the
+returned name, see `erc-show-speaker-membership-status'."
+  :package-version '(ERC . "5.6")
   :group 'erc-display
-  :type 'function)
+  :type '(choice (function-item erc-determine-speaker-from-user) function))
 
-(defun erc-format-nick (&optional user _channel-data)
-  "Return the nickname of USER.
-See also `erc-format-nick-function'."
+(define-obsolete-function-alias 'erc-format-nick
+  #'erc-determine-speaker-from-user "30.1")
+(defun erc-determine-speaker-from-user (&optional user _channel-data)
+  "Return nickname slot of `erc-server-user' USER, when non-nil."
   (when user (erc-server-user-nickname user)))
 
 (define-obsolete-function-alias 'erc-get-user-mode-prefix
@@ -5843,14 +6058,17 @@ erc-format-@nick
   "Format the nickname of USER showing if USER has a voice, is an
 operator, half-op, admin or owner.  Owners have \"~\", admins have
 \"&\", operators have \"@\" and users with voice have \"+\" as a
-prefix.  Use CHANNEL-DATA to determine op and voice status.  See
-also `erc-format-nick-function'."
+prefix.  Use CHANNEL-DATA to determine op and voice status."
+  (declare (obsolete "see option `erc-show-speaker-membership-status'" "30.1"))
   (when user
     (let ((nick (erc-server-user-nickname user)))
-      (concat (propertize
-               (erc-get-channel-membership-prefix channel-data)
-               'font-lock-face 'erc-nick-prefix-face)
-	      nick))))
+      (if (not erc--speaker-status-prefix-wanted-p)
+          (prog1 nick
+            (setq erc--speaker-status-prefix-wanted-p 'erc-format-@nick))
+        (concat (propertize
+                 (erc-get-channel-membership-prefix channel-data)
+                 'font-lock-face 'erc-nick-prefix-face)
+                nick)))))
 
 (defun erc-format-my-nick ()
   "Return the beginning of this user's message, correctly propertized."
@@ -5863,11 +6081,34 @@ erc-format-my-nick
         (concat
          (propertize open 'font-lock-face 'erc-default-face)
          (propertize mode 'font-lock-face 'erc-my-nick-prefix-face)
-         (propertize nick 'font-lock-face 'erc-my-nick-face 'erc-speaker nick)
+         (propertize nick 'erc-speaker nick 'font-lock-face 'erc-my-nick-face)
          (propertize close 'font-lock-face 'erc-default-face)))
     (let ((prefix "> "))
       (propertize prefix 'font-lock-face 'erc-default-face))))
 
+(defun erc--format-speaker-input-message (message)
+  "Assemble outgoing message entered at the prompt for insertion.
+Here \"input\" refers to prompt input and the name of the
+`speaker' catalog key.  Format the speaker portion like
+`erc-format-my-nick', but use `erc--message-speaker-input' as a
+template.  Use MESSAGE for the actual message body.  Return a
+copy with possibly shared text-property values."
+  (if-let ((erc-show-my-nick)
+           (nick (erc-current-nick))
+           (pfx (erc-get-channel-membership-prefix nick))
+           (erc-current-message-catalog erc--message-speaker-catalog)
+           (key (if (or erc-format-query-as-channel-p
+                        (erc--target-channel-p erc--target))
+                    'input-chan-privmsg
+                  'input-query-privmsg)))
+      (progn
+        (cond (erc--msg-props (puthash 'erc--msg key erc--msg-props))
+              (erc--msg-prop-overrides (push (cons 'erc--msg key)
+                                             erc--msg-prop-overrides)))
+        (erc-format-message key ?p pfx ?n (erc--speakerize-nick nick)
+                            ?m message))
+    (propertize (concat "> " message) 'font-lock-face 'erc-input-face)))
+
 (defun erc-echo-notice-in-default-buffer (s parsed buffer _sender)
   "Echo a private notice in the default buffer, namely the
 target buffer specified by BUFFER, or there is no target buffer,
@@ -6107,8 +6348,7 @@ erc-process-ctcp-query
         (while queries
           (let* ((type (upcase (car (split-string (car queries)))))
                  (hook (intern-soft (concat "erc-ctcp-query-" type "-hook")))
-                 (erc--msg-prop-overrides `((erc--msg . msg)
-                                            (erc--ctcp . ,(intern type))
+                 (erc--msg-prop-overrides `((erc--ctcp . ,(intern type))
                                             ,@erc--msg-prop-overrides)))
             (if (and hook (boundp hook))
                 (if (string-equal type "ACTION")
@@ -6143,12 +6383,27 @@ erc-ctcp-query-ACTION
     (let ((s (match-string 1 msg))
           (buf (or (erc-get-buffer to proc)
                    (erc-get-buffer nick proc)
-                   (process-buffer proc))))
-      (erc--ensure-spkr-prop nick)
-      (setq nick (propertize nick 'erc-speaker nick))
-      (erc-display-message
-       parsed 'action buf
-       'ACTION ?n nick ?u login ?h host ?a s))))
+                   (process-buffer proc)))
+          (selfp (erc-current-nick-p nick)))
+      (if erc--use-language-catalog-for-ctcp-action-p
+          (progn
+            (setq nick (propertize nick 'erc-speaker nick))
+            (erc-display-message parsed (if selfp 'input 'action) buf
+                                 'ACTION ?n nick ?u login ?h host ?a s))
+        (let* ((obj (and (erc--ctcp-response-p parsed) parsed))
+               (buffer (and obj (erc--ctcp-response-buffer obj)))
+               (target (and obj (erc--ctcp-response-target obj)))
+               (dispnm (and obj (erc--ctcp-response-dispname obj)))
+               (stmsgp (and obj (erc--ctcp-response-statusmsgp obj)))
+               (erc-current-message-catalog erc--message-speaker-catalog))
+          (erc-display-message
+           parsed nil (or buffer buf)
+           (if selfp
+               (if stmsgp 'ctcp-action-input-statusmsg 'ctcp-action-input)
+             (if stmsgp 'ctcp-action-statusmsg 'ctcp-action))
+           ?t (or target to)
+           ?n (erc--speakerize-nick nick dispnm)
+           ?m s))))))
 
 (defvar erc-ctcp-query-CLIENTINFO-hook '(erc-ctcp-query-CLIENTINFO))
 
@@ -7584,15 +7839,11 @@ erc-display-msg
       (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))
+                                (let ((ovs (seq-filter
+                                            #'cdr erc--msg-prop-overrides)))
                                   (map-into `((erc--msg . msg) ,@(reverse ovs))
-                                            'hash-table))))
-            beg)
-        (insert (erc-format-my-nick))
-        (setq beg (point))
-        (insert line)
-        (erc-put-text-property beg (point) 'font-lock-face 'erc-input-face)
-        (insert "\n")
+                                            'hash-table)))))
+        (insert (erc--format-speaker-input-message line) "\n")
         (save-restriction
           (narrow-to-region insert-position (point))
           (run-hooks 'erc-send-modify-hook)
@@ -8947,9 +9198,6 @@ erc-message-english-PART
                           (string-replace "%" "%%" reason))
                 "")))))
 
-
-(defvar-local erc-current-message-catalog 'english)
-
 (defun erc-retrieve-catalog-entry (entry &optional catalog)
   "Retrieve `format-spec' for symbol key ENTRY in CATALOG.
 Without CATALOG, use `erc-current-message-catalog'.  If lookup
diff --git a/test/lisp/erc/erc-scenarios-stamp.el b/test/lisp/erc/erc-scenarios-stamp.el
index 68769e203ff..d8309984d5d 100644
--- a/test/lisp/erc/erc-scenarios-stamp.el
+++ b/test/lisp/erc/erc-scenarios-stamp.el
@@ -68,7 +68,7 @@ erc-scenarios-stamp--left/display-margin-mode
         (ert-info ("Stamps appear in left margin and are invisible")
           (should (eq 'erc-timestamp (field-at-pos (pos-bol))))
           (should (= (pos-bol) (field-beginning (pos-bol))))
-          (should (eq 'msg (get-text-property (pos-bol) 'erc--msg)))
+          (should (eq 'query-notice (get-text-property (pos-bol) 'erc--msg)))
           (should (eq 'NOTICE (get-text-property (pos-bol) 'erc--cmd)))
           (should (= ?- (char-after (field-end (pos-bol)))))
           (should (equal (get-text-property (1+ (field-end (pos-bol)))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index eb954112ce8..ac05a63deee 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2285,7 +2285,8 @@ erc-message
         calls
         erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
     (cl-letf (((symbol-function 'erc-display-message)
-               (lambda (_ _ _ line) (push line calls)))
+               (lambda (_ _ _ msg &rest args)
+                 (push (apply #'erc-format-message msg args) calls)))
               ((symbol-function 'erc-server-send)
                (lambda (line _) (push line calls)))
               ((symbol-function 'erc-server-buffer)
@@ -2327,7 +2328,7 @@ erc-message
           (should-not erc-server-last-peers)
           (erc-message "PRIVMSG" ". hi")
           (should-not erc-server-last-peers)
-          (should (eq 'no-target (pop calls)))
+          (should (equal "No target" (pop calls)))
           (erc-message "PRIVMSG" ", hi")
           (should-not erc-server-last-peers)
           (should (string-match "alice :hi" (pop calls)))))
@@ -2360,42 +2361,208 @@ erc-message
     (kill-buffer "ExampleNet")
     (kill-buffer "#chan")))
 
-(ert-deftest erc-format-privmessage ()
-  ;; Basic PRIVMSG
-  (should (erc-tests--equal-including-properties
-           (erc-format-privmessage (copy-sequence "bob")
-                                   (copy-sequence "oh my")
-                                   nil 'msgp)
-           #("<bob> oh my"
-             0 1 (font-lock-face erc-default-face)
-             1 4 (erc-speaker "bob" font-lock-face erc-nick-default-face)
-             4 11 (font-lock-face erc-default-face))))
-
-  ;; Basic NOTICE
-  (should (erc-tests--equal-including-properties
-           (erc-format-privmessage (copy-sequence "bob")
-                                   (copy-sequence "oh my")
-                                   nil nil)
-           #("-bob- oh my"
-             0 1 (font-lock-face erc-default-face)
-             1 4 (erc-speaker "bob" font-lock-face erc-nick-default-face)
-             4 11 (font-lock-face erc-default-face))))
-
-  ;; Prefixed PRIVMSG
-  (let* ((user (make-erc-server-user :nickname (copy-sequence "Bob")))
+;; This is an adapter that uses formatting templates from the
+;; `-speaker' catalog to mimic `erc-format-privmessage', for testing
+;; purposes.
+(defun erc-tests--format-privmessage (nick msg privp msgp &optional inputp pfx)
+  (let ((erc-current-message-catalog erc--message-speaker-catalog))
+    (apply #'erc-format-message
+           (erc--determine-speaker-message-format-args nick nil msg privp msgp
+                                                       nil inputp pfx))))
+
+;; This asserts that `erc--determine-speaker-message-format-args'
+;; behaves identically to `erc-format-privmessage', the function whose
+;; role it basically replaced.
+(ert-deftest erc--determine-speaker-message-format-args ()
+  ;; Basic PRIVMSG.
+  (let ((expect #("<bob> oh my"
+                  0 1 (font-lock-face erc-default-face)
+                  1 4 (erc-speaker "bob" font-lock-face erc-nick-default-face)
+                  4 11 (font-lock-face erc-default-face)))
+        (args (list (concat "bob") (concat "oh my") nil 'msgp)))
+    (should (erc-tests--equal-including-properties
+             (apply #'erc-format-privmessage args)
+             expect))
+    (should (erc-tests--equal-including-properties
+             (apply #'erc-tests--format-privmessage args)
+             expect)))
+
+  ;; Basic NOTICE.
+  (let ((expect #("-bob- oh my"
+                  0 1 (font-lock-face erc-default-face)
+                  1 4 (erc-speaker "bob" font-lock-face erc-nick-default-face)
+                  4 11 (font-lock-face erc-default-face)))
+        (args (list (copy-sequence "bob") (copy-sequence "oh my") nil nil)))
+    (should (erc-tests--equal-including-properties
+             (apply #'erc-format-privmessage args)
+             expect))
+    (should (erc-tests--equal-including-properties
+             (apply #'erc-tests--format-privmessage args)
+             expect)))
+
+  ;; Status-prefixed PRIVMSG.
+  (let* ((expect
+          #("<@Bob> oh my"
+            0 1 (font-lock-face erc-default-face)
+            1 2 (font-lock-face erc-nick-prefix-face help-echo "operator")
+            2 5 (erc-speaker "Bob" font-lock-face erc-nick-default-face)
+            5 12 (font-lock-face erc-default-face)))
+         (user (make-erc-server-user :nickname (copy-sequence "Bob")))
          (cuser (make-erc-channel-user :op t))
          (erc-channel-users (make-hash-table :test #'equal)))
     (puthash "bob" (cons user cuser) erc-channel-users)
 
+    (with-suppressed-warnings ((obsolete erc-format-@nick))
+      (should (erc-tests--equal-including-properties
+               (erc-format-privmessage (erc-format-@nick user cuser)
+                                       (copy-sequence "oh my")
+                                       nil 'msgp)
+               expect)))
+    (let ((nick "Bob")
+          (msg "oh my"))
+      (should (erc-tests--equal-including-properties
+               (erc-tests--format-privmessage nick msg nil 'msgp nil cuser)
+               expect)) ; overloaded on PREFIX arg
+      (should (erc-tests--equal-including-properties
+               (erc-tests--format-privmessage nick msg nil 'msgp nil t)
+               expect))
+      ;; The new version makes a copy instead of adding properties to
+      ;; the input.
+      (should-not
+       (text-property-not-all 0 (length nick) 'font-lock-face nil nick))
+      (should-not
+       (text-property-not-all 0 (length msg) 'font-lock-face nil msg)))))
+
+(ert-deftest erc--determine-speaker-message-format-args/queries-as-channel ()
+  (should erc-format-query-as-channel-p)
+
+  (with-current-buffer (get-buffer-create "bob")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "alice"))
+
+    (insert "PRIVMSG\n"
+            (erc-tests--format-privmessage "bob" "oh my" 'queryp 'msgp))
+    (should (erc-tests--equal-including-properties
+             #("<bob> oh my"
+               0 1 (font-lock-face erc-default-face)
+               1 4 (erc-speaker "bob" font-lock-face erc-nick-default-face)
+               4 11 (font-lock-face erc-default-face))
+             (buffer-substring (pos-bol) (pos-eol))))
+
+    (insert "\nNOTICE\n"
+            (erc-tests--format-privmessage "bob" "oh my" 'queryp nil))
+    (should (erc-tests--equal-including-properties
+             #("-bob- oh my"
+               0 1 (font-lock-face erc-default-face)
+               1 4 (erc-speaker "bob" font-lock-face erc-nick-default-face)
+               4 11 (font-lock-face erc-default-face))
+             (buffer-substring (pos-bol) (pos-eol))))
+
+    (insert "\nInput PRIVMSG\n"
+            (erc-tests--format-privmessage "bob" "oh my"
+                                           'queryp 'privmsgp 'inputp))
+    (should (erc-tests--equal-including-properties
+             #("<bob> oh my"
+               0 1 (font-lock-face erc-default-face)
+               1 4 (erc-speaker "bob" font-lock-face erc-my-nick-face)
+               4 6 (font-lock-face erc-default-face)
+               6 11 (font-lock-face erc-input-face))
+             (buffer-substring (pos-bol) (pos-eol))))
+
+    (insert "\nInput NOTICE\n"
+            (erc-tests--format-privmessage "bob" "oh my" 'queryp nil 'inputp))
     (should (erc-tests--equal-including-properties
-             (erc-format-privmessage (erc-format-@nick user cuser)
-                                     (copy-sequence "oh my")
-                                     nil 'msgp)
-             #("<@Bob> oh my"
+             #("-bob- oh my"
                0 1 (font-lock-face erc-default-face)
-               1 2 (font-lock-face erc-nick-prefix-face help-echo "operator")
-               2 5 (erc-speaker "Bob" font-lock-face erc-nick-default-face)
-               5 12 (font-lock-face erc-default-face))))))
+               1 4 (erc-speaker "bob" font-lock-face erc-my-nick-face)
+               4 6 (font-lock-face erc-default-face)
+               6 11 (font-lock-face erc-input-face))
+             (buffer-substring (pos-bol) (pos-eol))))
+
+    (when noninteractive (kill-buffer))))
+
+(ert-deftest erc--determine-speaker-message-format-args/queries ()
+  (should erc-format-query-as-channel-p)
+
+  (with-current-buffer (get-buffer-create "bob")
+    (erc-mode)
+    (setq-local erc-format-query-as-channel-p nil)
+    (setq erc--target (erc--target-from-string "alice"))
+
+    (insert "PRIVMSG\n"
+            (erc-tests--format-privmessage "bob" "oh my" 'queryp 'msgp))
+    (should (erc-tests--equal-including-properties
+             #("*bob* oh my"
+               0 1 (font-lock-face erc-direct-msg-face)
+               1 4 (erc-speaker "bob" font-lock-face erc-nick-msg-face)
+               4 11 (font-lock-face erc-direct-msg-face))
+             (buffer-substring (pos-bol) (pos-eol))))
+
+    (insert "\nNOTICE\n"
+            (erc-tests--format-privmessage "bob" "oh my" 'queryp nil))
+    (should (erc-tests--equal-including-properties
+             #("-bob- oh my"
+               0 1 (font-lock-face erc-direct-msg-face)
+               1 4 (erc-speaker "bob" font-lock-face erc-nick-msg-face)
+               4 11 (font-lock-face erc-direct-msg-face))
+             (buffer-substring (pos-bol) (pos-eol))))
+
+    (insert "\nInput PRIVMSG\n"
+            (erc-tests--format-privmessage "bob" "oh my"
+                                           'queryp 'privmsgp 'inputp))
+    (should (erc-tests--equal-including-properties
+             #("*bob* oh my"
+               0 1 (font-lock-face erc-direct-msg-face)
+               1 4 (erc-speaker "bob" font-lock-face erc-my-nick-face)
+               4 6 (font-lock-face erc-direct-msg-face)
+               6 11 (font-lock-face erc-input-face))
+             (buffer-substring (pos-bol) (pos-eol))))
+
+    (insert "\nInput NOTICE\n"
+            (erc-tests--format-privmessage "bob" "oh my" 'queryp nil 'inputp))
+    (should (erc-tests--equal-including-properties
+             #("-bob- oh my"
+               0 1 (font-lock-face erc-direct-msg-face)
+               1 4 (erc-speaker "bob" font-lock-face erc-my-nick-face)
+               4 6 (font-lock-face erc-direct-msg-face)
+               6 11 (font-lock-face erc-input-face))
+             (buffer-substring (pos-bol) (pos-eol))))
+
+    (when noninteractive (kill-buffer))))
+
+(defun erc-tests--format-my-nick (message)
+  (concat (erc-format-my-nick)
+          (propertize message 'font-lock-face 'erc-input-face)))
+
+;; This tests that the default behavior of the replacement formatting
+;; function for prompt input, `erc--format-speaker-input-message'
+;; matches that of the original being replaced, `erc-format-my-nick',
+;; though it only handled the speaker portion.
+(ert-deftest erc--format-speaker-input-message ()
+  ;; No status prefix.
+  (let ((erc-server-current-nick "tester")
+        (expect #("<tester> oh my"
+                  0 1 (font-lock-face erc-default-face)
+                  1 7 (font-lock-face erc-my-nick-face erc-speaker "tester")
+                  7 9 (font-lock-face erc-default-face)
+                  9 14 (font-lock-face erc-input-face))))
+    (should (equal (erc-tests--format-my-nick "oh my") expect))
+    (should (equal (erc--format-speaker-input-message "oh my") expect)))
+
+  ;; With channel-operator status prefix.
+  (let* ((erc-server-current-nick "tester")
+         (cmem (cons (make-erc-server-user :nickname "tester")
+                     (make-erc-channel-user :op t)))
+         (erc-channel-users (map-into (list "tester" cmem)
+                                      '(hash-table :test equal)))
+         (expect #("<@tester> oh my"
+                   0 1 (font-lock-face erc-default-face)
+                   1 2 (font-lock-face erc-my-nick-prefix-face)
+                   2 5 (font-lock-face erc-my-nick-face erc-speaker "bob")
+                   5 7 (font-lock-face erc-default-face)
+                   7 12 (font-lock-face erc-input-face))))
+    (should (equal (erc-tests--format-my-nick "oh my") expect))
+    (should (equal (erc--format-speaker-input-message "oh my") expect))))
 
 (ert-deftest erc--route-insertion ()
   (erc-tests--send-prep)
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index 9f648915d5c..feaba85ec90 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
index a63fcad3d38..ed1488c8595 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
index 7cbabfd0581..a3530a6c44d 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--ts 1680332400 erc--spkr "bob" erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
-- 
2.42.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* bug#67677: 30.0.50; ERC 5.6: Use templates for formatting chat messages
       [not found] <87jzpq7apw.fsf@neverwas.me>
@ 2023-12-18 14:50 ` J.P.
       [not found] ` <87v88vftu6.fsf@neverwas.me>
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 6+ messages in thread
From: J.P. @ 2023-12-18 14:50 UTC (permalink / raw)
  To: 67677; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 248 bytes --]

A version of these changes has been installed as

  49bfea4386f * Use templates for formatting chat messages in ERC

Unfortunately, they included a somewhat glaring omission involving CTCP
replies that's hopefully addressed by the attached patch.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-5.6-Populate-erc-msg-prop-overrides-for-CTCP-replies.patch --]
[-- Type: text/x-patch, Size: 6037 bytes --]

From ad93b795ae3ed3f0b91acdf5806703e32b3ef074 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 17 Dec 2023 21:49:13 -0800
Subject: [PATCH] [5.6] Populate erc--msg-prop-overrides for CTCP replies

* lisp/erc/erc-backend.el (erc-server-PRIVMSG): Don't set string
intended for insertion to the undefined return value of
`erc-process-ctcp-reply' and `erc-process-ctcp-query'.  Rework control
flow slightly for clarity.
* lisp/erc/erc.el (erc-process-ctcp-reply): Bind
`erc--msg-prop-overrides' and populate with `erc--ctcp' and `erc--cmd'
"msg props" for the benefit of `erc-display-message' calls made by
CTCP reply handlers.  (Bug#67677)
---
 lisp/erc/erc-backend.el | 53 +++++++++++++++++++----------------------
 lisp/erc/erc.el         | 10 ++++++--
 2 files changed, 32 insertions(+), 31 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 0c336540483..6d30409e156 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1996,8 +1996,8 @@ erc--speaker-status-prefix-wanted-p
              (erc--msg-prop-overrides `((erc--tmp) ,@erc--msg-prop-overrides))
              (erc--speaker-status-prefix-wanted-p nil)
              (erc-current-message-catalog erc--message-speaker-catalog)
-             s buffer statusmsg cmem-prefix
-             fnick)
+             ;;
+             buffer statusmsg cmem-prefix fnick)
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
         ;; Even worth checking for empty target here? (invalid anyway)
         (unless (or buffer noticep (string-empty-p tgt) (eq ?$ (aref tgt 0))
@@ -2042,36 +2042,31 @@ erc--speaker-status-prefix-wanted-p
                                          erc-show-speaker-membership-status
                                          inputp)
                                      (cdr cdata))))))
-        (cond
-         ((erc-is-message-ctcp-p msg)
-          ;; FIXME explain undefined return values being assigned to `s'.
-          (setq s (if-let ((parsed
-                            (erc--ctcp-response-from-parsed
-                             :parsed parsed :buffer buffer :statusmsg statusmsg
-                             :prefix cmem-prefix :dispname fnick))
-                           (msgp))
-                      (erc-process-ctcp-query proc parsed nick login host)
-                    (erc-process-ctcp-reply proc parsed nick login host
-                                            (match-string 1 msg)))))
-         (t
+        (if (erc-is-message-ctcp-p msg)
+            (if noticep
+                (erc-process-ctcp-reply proc parsed nick login host
+                                        (match-string 1 msg))
+              (setq parsed (erc--ctcp-response-from-parsed
+                            :parsed parsed :buffer buffer :statusmsg statusmsg
+                            :prefix cmem-prefix :dispname fnick))
+              (erc-process-ctcp-query proc parsed nick login host))
           (setq erc-server-last-peers (cons nick (cdr erc-server-last-peers)))
           (with-current-buffer (or buffer (current-buffer))
             ;; Re-bind in case either buffer has a local value.
-            (let ((erc-current-message-catalog erc--message-speaker-catalog))
-              (setq s (erc--determine-speaker-message-format-args
-                       nick msg privp msgp inputp statusmsg
-                       cmem-prefix fnick))))))
-        (when s
-          (if (and noticep privp)
-              (progn
-                (push (cons 'erc--msg (car s)) erc--msg-prop-overrides)
-                (setq s (apply #'erc-format-message s))
-                (run-hook-with-args 'erc-echo-notice-always-hook
-                                    s parsed buffer nick)
-                (run-hook-with-args-until-success
-                 'erc-echo-notice-hook s parsed buffer nick))
-            (apply #'erc-display-message parsed nil buffer
-                   (ensure-list s))))))))
+            (let ((erc-current-message-catalog erc--message-speaker-catalog)
+                  (msg-args (erc--determine-speaker-message-format-args
+                             nick msg privp msgp inputp statusmsg
+                             cmem-prefix fnick)))
+              (if (or msgp (not privp))
+                  ;; This is a PRIVMSG or a NOTICE to a channel.
+                  (apply #'erc-display-message parsed nil buffer msg-args)
+                ;; This is a NOTICE directed at the client's current nick.
+                (push (cons 'erc--msg (car msg-args)) erc--msg-prop-overrides)
+                (let ((fmtmsg (apply #'erc-format-message msg-args)))
+                  (run-hook-with-args 'erc-echo-notice-always-hook
+                                      fmtmsg parsed buffer nick)
+                  (run-hook-with-args-until-success
+                   'erc-echo-notice-hook fmtmsg parsed buffer nick))))))))))
 
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index faa2cbefd1b..37e47591d73 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -6525,8 +6525,14 @@ erc-ctcp-query-VERSION
 (defun erc-process-ctcp-reply (proc parsed nick login host msg)
   "Process MSG as a CTCP reply."
   (let* ((type (car (split-string msg)))
-         (hook (intern (concat "erc-ctcp-reply-" type "-hook"))))
-    (if (boundp hook)
+         (hook (intern-soft (concat "erc-ctcp-reply-" type "-hook")))
+         ;; Help `erc-display-message' by ensuring subsequent
+         ;; insertions retain the necessary props.
+         (cmd (erc--get-eq-comparable-cmd (erc-response.command parsed)))
+         (erc--msg-prop-overrides `((erc--ctcp . ,(and hook (intern type)))
+                                    (erc--cmd . ,cmd)
+                                    ,@erc--msg-prop-overrides)))
+    (if (and hook (boundp hook))
         (run-hook-with-args-until-success
          hook proc nick login host
          (car (erc-response.command-args parsed)) msg)
-- 
2.42.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* bug#67677: 30.0.50; ERC 5.6: Use templates for formatting chat messages
       [not found] ` <87v88vftu6.fsf@neverwas.me>
@ 2024-01-08  5:46   ` J.P.
  0 siblings, 0 replies; 6+ messages in thread
From: J.P. @ 2024-01-08  5:46 UTC (permalink / raw)
  To: 67677-done; +Cc: emacs-erc

"J.P." <jp@neverwas.me> writes:

> A version of these changes has been installed as
>
>   49bfea4386f * Use templates for formatting chat messages in ERC
>
> Unfortunately, they included a somewhat glaring omission involving CTCP
> replies that's hopefully addressed by the attached patch.

The fix has been installed as

  23d692ed014 * Populate erc--msg-prop-overrides for CTCP replies

Closing.





^ permalink raw reply	[flat|nested] 6+ messages in thread

* bug#67677: 30.0.50; ERC 5.6: Use templates for formatting chat messages
       [not found] <87jzpq7apw.fsf@neverwas.me>
  2023-12-18 14:50 ` bug#67677: 30.0.50; ERC 5.6: Use templates for formatting chat messages J.P.
       [not found] ` <87v88vftu6.fsf@neverwas.me>
@ 2024-01-12 16:19 ` J.P.
       [not found] ` <87a5paa5j0.fsf@neverwas.me>
  3 siblings, 0 replies; 6+ messages in thread
From: J.P. @ 2024-01-12 16:19 UTC (permalink / raw)
  To: 67677; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 4092 bytes --]

Earlier work for this bug extended ERC's existing template catalog
system with an internal framework for dictating (hopefully more
explicitly and resolutely) how messages materialize in chat buffers.
Modules can make use of this framework to shape most aspects of message
formatting, which opens the door to radically contrasting styles, such
as "multi-part" messages with header/body/footer sections and messages
with integrated time stamps (meaning you can finally ditch that blessed
`stamp' module for good).

However, this approach remains awkward when it comes to

  1. minor modifications

  2. modularity itself (keeping modules loosely coupled)

At present, making use of the framework involves defining an entire
format catalog (although inheritance helps a bit in this regard). But
the boilerplate issue really begins to compound when trying to integrate
with other modules because the process is somewhat dependent on defining
yet more catalogs to serve the various combinations, and more still if
trying to keep things abstract.

To alleviate some of this awkwardness and cut down on the verbosity, I'm
proposing we introduce a more practical extension to this framework.
It'll be reserved for internal use at first, but with an eye toward
eventual export (likely in 5.7). The basic idea is that we define a
single abnormal hook per speaker catalog that runs just prior to
insertion, and we allow its members to influence the parameters passed
to `format-spec'. And, we do so in a convenient and structured (and
reusable) way, so members don't have to twiddle plists in search of a
single ingredient to manually splice into format strings based on the
verdict of some flimsy heuristic.

I'm tentatively calling this hook system "msgfspc" (one word), short for
"message format-spec." It works by defining a struct to accompany each
hook, with slots based on the catalog's common set of template
specifiers. Subscribing code then has the freedom to modify the template
itself and add or subtract specifiers as needed by mutating the struct
instance for that particular formatting pass. The client API looks like
this:

  ;; Module activation body

  (add-hook 'erc-msgfspec-speaker-hook
            #my-maybe-transform-on-msgfspec-speaker nil t)
  (setq my-state "foo")
  
  ;; Top level of package

  (defun my-maybe-transform-on-msgfspec-speaker (spec)
    (pcase spec

      ;; Modify an outgoing message template.

      ((cl-struct erc-msgfspec-speaker
                  (key (or 'input-chan-privmsg 'input-query-privmsg)))
       (erc-msgfspec-insert-spec-after
        spec ?n ?i (propertize "%i" 'font-lock-face 'my-face))
       (push `(?i . ,my-state) (erc-msgfspec-alist spec)))
  
      ;; Modify an incoming message body.

      ((cl-struct erc-msgfspec-speaker
                  (key (or 'chan-privmsg 'query-privmsg))
                  (\?m msg))
       (setf (erc-msgfspec-speaker-?m spec)
             (decode-coding-string (my-transform-message msg)
                                   'utf-8)))))

  ;; Note that at present, all the "erc-foo" symbols above are actually
  ;; "erc--foo" (internal)

There's at least one unfortunate aspect to the API scheme above: the
buffer where the working version of a template resides isn't current
when hooks run. This happens because members still need access to local
state in the ERC buffer where the actual insertion takes place. I've
experimented a bit with using the virtual buffer facility (via
`buffer-swap-text') to get around this, and it appears to work great
(even seemingly shaving a second or two off runs of ERC's extended test
suite). However, I'm quite reticent to introduce something I've never
used before and almost never see in the wild. Thus, this approach will
have to wait pending further investigation.

The current version of the proposed implementation can be found in the
second of the attached patches. The first is from bug#68265 but included
here because the associated demo addressing a real-world use case
requires both. Please see that bug's recent posts for links and
instructions.

Thanks.

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v1-v2.diff --]
[-- Type: text/x-patch, Size: 53311 bytes --]

From 9bb8693156326cf3ef33f9ccf1e6b4bbbcf9ed61 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 12 Jan 2024 06:48:18 -0800
Subject: [PATCH 0/2] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (2):
  [5.6] Add replacement-text field to erc-input struct
  [5.6] Expose catalog-specific message formatter in ERC

 etc/ERC-NEWS                                  |  14 +-
 lisp/erc/erc-common.el                        | 126 +++++++-
 lisp/erc/erc-goodies.el                       |   7 +-
 lisp/erc/erc.el                               | 289 ++++++++++++------
 test/lisp/erc/erc-tests.el                    | 174 +++++++++++
 .../fill/snapshots/merge-wrap-01.eld          |   2 +-
 .../merge-wrap-indicator-post-01.eld          |   2 +-
 .../snapshots/merge-wrap-indicator-pre-01.eld |   2 +-
 8 files changed, 513 insertions(+), 103 deletions(-)

Interdiff:
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 30ce0a0bb1a..eee06a3fde9 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -165,6 +165,113 @@ erc--isupport-data
   (table (make-char-table 'erc--channel-mode-types) :type char-table)
   (shortargs (make-hash-table :test #'equal)))
 
+(cl-defstruct erc--msgfspec
+  "Abstract struct for object shared among message-format hook members."
+  ( key nil :type symbol
+    :documentation "Catalog entry key, a symbol.")
+  ( buffer nil :type (or buffer null)
+    :documentation "Buffer with catalog entry value, a template string.")
+  ( alist nil :type list
+    :documentation "Extra items for `format-spec' SPECIFICATION."))
+
+(defmacro erc--define-msgfspec (catalog &rest slots)
+  "Define items that satisfy the \"erc-msgfspec\" interface.
+Expect symbol CATALOG to be the name of a message-format catalog
+and SLOTS to be an alist of (CHAR . DOC), where CHAR is a
+`format-spec' character and DOC is a string explaining its role.
+Using these, define a \"msgfspec\" data struct named
+\"erc-msgfspec-CATALOG\", with massaged SLOTS becoming its
+string-typed fields.  Arrange for each field to be DOC'umented
+and accessed by a function named \"erc-msgfspec-CATALOG-?CHAR\"
+\(note the question mark).  Additionally, create a constructor
+for the struct, named \"erc-msgfspec-CATALOG-from-args\", along
+with a hook variable named \"erc-msgfspec-CATALOG-hook\".  Also
+define a wrapper named \"erc-msgfspec-CATALOG-apply-spec\" to
+allow the `erc-display-message' machinery to call `format-spec'
+with a specification alist derived from the struct instance
+passed around and potentially influenced by each hook member."
+  (declare (indent 1)
+           (debug (symbolp (&rest &or symbolp (symbolp &rest sexp)))))
+  (let* ((chars)
+         (specs (let (out)
+                  (while-let (((characterp (car-safe (car-safe slots))))
+                              (spec (pop slots))
+                              (char (car spec)))
+                    (cl-assert (stringp (cdr spec)))
+                    (push `(,(intern (format "?%c" char)) ""
+                            :type 'string :documentation ,(cdr spec))
+                          out)
+                    (push char chars))
+                  (setq chars (nreverse chars))
+                  (nreverse out)))
+         (internalp (eq ?- (aref (symbol-name catalog) 0)))
+         (name (if internalp
+                   (substring (symbol-name catalog) 1)
+                 (symbol-name catalog)))
+         (full-name (concat "erc-" (if internalp "-" "") "msgfspec-" name))
+         (hook (intern (concat full-name "-hook")))
+         (makr (intern (concat full-name "-from-args")))
+         (fmtr (intern (concat full-name "-apply-spec")))
+         (setters (mapcar (lambda (letr)
+                            (list letr '\,
+                                  `(,(intern (format "%s-?%c" full-name letr))
+                                    msgfspec-obj)))
+                          chars))
+         (choices (mapcar (lambda (letr)
+                            (list letr (intern (format ":?%c" letr))))
+                          chars)))
+    `(progn
+
+       (cl-defstruct (,(intern full-name) (:include erc--msgfspec))
+         ,(concat "Shared object for `" name
+                  "' catalog message-format hook.")
+         ,@specs
+         ,@slots)
+
+       (defvar ,hook nil
+         ,(concat "Hook run before formatting a `" name "' catalog entry."
+                  "\nCalled by `erc-format-message' with an `"
+                  full-name "'\nobject."))
+
+       (defun ,makr (key format &rest spec-plist)
+         ,(concat "Create a `" full-name "' object from catalog entry."
+                  "\nExpect KEY to be the entry's key, FORMAT its value, and"
+                  "\nSPEC-PLIST the plist of `format-spec' args originally"
+                  " given" ; continued
+                  "\nto `erc-display-message'.")
+         (let ((buffer (get-buffer-create
+                        (format ,(concat " *" full-name "-%s*") key)
+                        ,@(and (>= emacs-major-version 28) '(t))))
+               args)
+           (with-current-buffer buffer (insert format))
+           (while-let ((spec-plist)
+                       (key (pop spec-plist))
+                       (val (pop spec-plist)))
+             (setq args (nconc (list (pcase key ,@choices) val) args)))
+           (apply #',(intern (concat "make-" full-name))
+                  :key key :buffer buffer args)))
+       (defun ,fmtr (msgfspec-obj)
+         ,(concat "Massage MSGFSPEC-OBJ into args for `format-spec'."
+                  "\nApply the latter after incorporating the `alist' slot"
+                  "\nfor the current `" full-name "' object.")
+         (format-spec (with-current-buffer (erc--msgfspec-buffer msgfspec-obj)
+                        (remove-text-properties (point-min) (point-max)
+                                                '(erc--% nil))
+                        (prog1 (buffer-string) (kill-buffer)))
+                      (,'\`(,@setters
+                            (,'\,@ (,(intern (concat full-name "-alist"))
+                                    msgfspec-obj))))
+                      'ignore))
+       (put ',catalog 'erc-msgfspec-makr #',makr)
+       (put ',catalog 'erc-msgfspec-fmtr ',fmtr)
+       (put ',catalog 'erc-msgfspec-hook ',hook))))
+
+(erc--define-msgfspec -speaker
+  (?n . "Nickname.")
+  (?m . "Message body.")
+  (?p . "Channel-membership prefix.")
+  (?s . "STATUSMSG prefix."))
+
 ;; After dropping 28, we can use prefixed "erc-autoload" cookies.
 (defun erc--normalize-module-symbol (symbol)
   "Return preferred SYMBOL for `erc--modules'."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 00963d24a32..f80bafa42b9 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -5924,121 +5924,134 @@ erc-format-privmessage
 
 ;; The format strings in the following `-speaker' catalog shouldn't
 ;; contain any non-protocol words, so they make sense in any language.
+;; Intervals with a format-spec specifier % ... c must have the text
+;; property (erc--% . ?c) so that msgfspec-related code can find it.
 
 (defvar erc--message-speaker-statusmsg
   #("(%p%n%s) %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-nick-prefix-face)
-    3 5 (font-lock-face erc-nick-default-face)
-    5 7 (font-lock-face erc-notice-face)
-    7 11 (font-lock-face erc-default-face))
+    1 3 (erc--% ?p font-lock-face erc-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-nick-default-face)
+    5 7 (erc--% ?s font-lock-face erc-notice-face)
+    7 9 (font-lock-face erc-default-face)
+    9 11 (erc--% ?m font-lock-face erc-default-face))
   "Message template for in-channel status messages.")
 
 (defvar erc--message-speaker-statusmsg-input
   #("(%p%n%s) %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-my-nick-prefix-face)
-    3 5 (font-lock-face erc-my-nick-face)
-    5 7 (font-lock-face erc-notice-face)
+    1 3 (erc--% ?p font-lock-face erc-my-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-my-nick-face)
+    5 7 (erc--% ?s font-lock-face erc-notice-face)
     7 8 (font-lock-face erc-default-face)
-    8 11 (font-lock-face erc-input-face))
+    8 9 (font-lock-face erc-input-face)
+    9 11 (erc--% ?m font-lock-face erc-input-face))
   "Message template for echoed status messages.")
 
 (defvar erc--message-speaker-input-chan-privmsg
   #("<%p%n> %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-my-nick-prefix-face)
-    3 5 (font-lock-face erc-my-nick-face)
+    1 3 (erc--% ?p font-lock-face erc-my-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-my-nick-face)
     5 7 (font-lock-face erc-default-face)
-    7 9 (font-lock-face erc-input-face))
+    7 9 (erc--% ?m font-lock-face erc-input-face))
   "Message template for prompt input or echoed PRIVMSG from own nick.")
 
 (defvar erc--message-speaker-input-query-privmsg
   #("*%n* %m"
     0 1 (font-lock-face erc-direct-msg-face)
-    1 3 (font-lock-face erc-my-nick-face)
+    1 3 (erc--% ?n font-lock-face erc-my-nick-face)
     3 5 (font-lock-face erc-direct-msg-face)
-    5 7 (font-lock-face erc-input-face))
+    5 7 (erc--% ?m font-lock-face erc-input-face))
   "Message template for prompt input or echoed PRIVMSG query from own nick.")
 
 (defvar erc--message-speaker-input-query-notice
   #("-%n- %m"
     0 1 (font-lock-face erc-direct-msg-face)
-    1 3 (font-lock-face erc-my-nick-face)
+    1 3 (erc--% ?n font-lock-face erc-my-nick-face)
     3 5 (font-lock-face erc-direct-msg-face)
-    5 7 (font-lock-face erc-input-face))
+    5 7 (erc--% ?m font-lock-face erc-input-face))
   "Message template for echoed or spoofed query NOTICE from own nick.")
 
 (defvar erc--message-speaker-input-chan-notice
   #("-%p%n- %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-my-nick-prefix-face)
-    3 5 (font-lock-face erc-my-nick-face)
+    1 3 (erc--% ?p font-lock-face erc-my-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-my-nick-face)
     5 7 (font-lock-face erc-default-face)
-    7 9 (font-lock-face erc-input-face))
+    7 9 (erc--% ?m font-lock-face erc-input-face))
   "Message template for prompt input or echoed NOTICE from own nick.")
 
 (defvar erc--message-speaker-chan-privmsg
   #("<%p%n> %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-nick-prefix-face)
-    3 5 (font-lock-face erc-nick-default-face)
-    5 9 (font-lock-face erc-default-face))
+    1 3 (erc--% ?p font-lock-face erc-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-nick-default-face)
+    5 7 (font-lock-face erc-default-face)
+    7 9 (erc--% ?m font-lock-face erc-default-face))
   "Message template for a PRIVMSG in a channel.")
 
 (defvar erc--message-speaker-query-privmsg
   #("*%n* %m"
     0 1 (font-lock-face erc-direct-msg-face)
-    1 3 (font-lock-face erc-nick-msg-face)
-    3 7 (font-lock-face erc-direct-msg-face))
+    1 3 (erc--% ?n font-lock-face erc-nick-msg-face)
+    3 5 (font-lock-face erc-direct-msg-face)
+    5 7 (erc--% ?m font-lock-face erc-direct-msg-face))
   "Message template for a PRIVMSG in query buffer.")
 
 (defvar erc--message-speaker-chan-notice
   #("-%p%n- %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-nick-prefix-face)
-    3 5 (font-lock-face erc-nick-default-face)
-    5 9 (font-lock-face erc-default-face))
+    1 3 (erc--% ?p font-lock-face erc-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-nick-default-face)
+    5 7 (font-lock-face erc-default-face)
+    7 9 (erc--% ?m font-lock-face erc-default-face))
   "Message template for a NOTICE in a channel.")
 
 (defvar erc--message-speaker-query-notice
   #("-%n- %m"
     0 1 (font-lock-face erc-direct-msg-face)
-    1 3 (font-lock-face erc-nick-msg-face)
-    3 7 (font-lock-face erc-direct-msg-face))
+    1 3 (erc--% 110 font-lock-face erc-nick-msg-face)
+    3 5 (font-lock-face erc-direct-msg-face)
+    5 7 (erc--% 109 font-lock-face erc-direct-msg-face))
   "Message template for a NOTICE in a query buffer.")
 
 (defvar erc--message-speaker-ctcp-action
   #("* %p%n %m"
     0 2 (font-lock-face erc-action-face)
-    2 4 (font-lock-face (erc-nick-prefix-face erc-action-face))
-    4 9 (font-lock-face erc-action-face))
+    2 4 (erc--% ?p font-lock-face (erc-nick-prefix-face erc-action-face))
+    4 6 (erc--% ?n font-lock-face erc-action-face)
+    6 7 (font-lock-face erc-action-face)
+    7 9 (erc--% ?m font-lock-face erc-action-face))
   "Message template for a CTCP ACTION from another user.")
 
 (defvar erc--message-speaker-ctcp-action-input
   #("* %p%n %m"
     0 2 (font-lock-face #1=(erc-input-face erc-action-face))
-    2 4 (font-lock-face (erc-my-nick-prefix-face . #1#))
-    4 6 (font-lock-face (erc-my-nick-face . #1#))
-    6 9 (font-lock-face #1#))
+    2 4 (erc--% ?p font-lock-face (erc-my-nick-prefix-face . #1#))
+    4 6 (erc--% ?n font-lock-face (erc-my-nick-face . #1#))
+    6 7 (font-lock-face #1#)
+    7 9 (erc--% ?m font-lock-face #1#))
   "Message template for a CTCP ACTION from current client.")
 
 (defvar erc--message-speaker-ctcp-action-statusmsg
   #("* (%p%n%s) %m"
     0 3 (font-lock-face erc-action-face)
-    3 5 (font-lock-face (erc-nick-prefix-face erc-action-face))
-    5 7 (font-lock-face erc-action-face)
-    7 9 (font-lock-face (erc-notice-face erc-action-face))
-    9 13 (font-lock-face erc-action-face))
+    3 5 (erc--% ?p font-lock-face (erc-nick-prefix-face erc-action-face))
+    5 7 (erc--% ?n font-lock-face erc-action-face)
+    7 9 (erc--% ?s font-lock-face (erc-notice-face erc-action-face))
+    9 11 (font-lock-face erc-action-face)
+    11 13 (erc--% ?m font-lock-face erc-action-face))
   "Template for a CTCP ACTION status message from another chan op.")
 
 (defvar erc--message-speaker-ctcp-action-statusmsg-input
   #("* (%p%n%s) %m"
     0 3 (font-lock-face #1=(erc-input-face erc-action-face))
-    3 5 (font-lock-face (erc-my-nick-prefix-face . #1#))
-    5 7 (font-lock-face (erc-my-nick-face . #1#))
-    7 9 (font-lock-face (erc-notice-face . #1#))
-    9 13 (font-lock-face #1#))
+    3 5 (erc--% ?p font-lock-face (erc-my-nick-prefix-face . #1#))
+    5 7 (erc--% ?n font-lock-face (erc-my-nick-face . #1#))
+    7 9 (erc--% ?s font-lock-face (erc-notice-face . #1#))
+    9 11 (font-lock-face #1#)
+    11 13 (erc--% ?m font-lock-face #1#))
   "Template for a CTCP ACTION status message from current client.")
 
 (defun erc--speakerize-nick (nick &optional disp)
@@ -6099,6 +6112,87 @@ erc--determine-speaker-message-format-args
         ?p (or prefix "") ?n (erc--speakerize-nick nick disp-nick)
         ?s (or statusmsg "") ?m message))
 
+(defun erc--mfs-get-bounds (char &optional from-pos)
+  "Return a cons cell with the bounds of CHAR's format specifier if found.
+Begin searching at FROM-POS if given.  On success, ensure the
+returned pair can be used to obtain CHAR's associated specifier
+via `buffer-substring', meaning the pair's CDR is one position
+beyond the end of the substring itself."
+  (and-let* ((beg (text-property-any (or from-pos (point-min)) (point-max)
+                                     'erc--% char))
+             (end (next-single-property-change beg 'erc--% nil (point-max))))
+    (cons beg end)))
+
+(defun erc--mfs-get-nth-bounds (char &optional n)
+  "Return bounds of CHAR's Nth occurrence, N=1 being the first/default."
+  (unless n (setq n 1))
+  (let (bounds)
+    (while (and (natnump (cl-decf n))
+                (setq bounds (erc--mfs-get-bounds char (cdr bounds)))))
+    bounds))
+
+(defun erc--mfs-insert-before (target string &optional afterp)
+  "Insert STRING before TARGET's format specifier.
+Expect TARGET to be the character associated with the format
+specifier to insert in front of.  Or, if multiple specifiers for
+the same character exist, and the first among them isn't desired,
+expect a cons of (CHAR . N) instead.  Assume STRING is either a
+plain string lacking any format specifiers or a cons of (CHAR
+. STRING-SPEC) designating exactly one character-specifier
+association for ERC to remember while formatting the current
+message, an example being (?a . \"%a\").  Move point as needed
+before inserting STRING, and return its updated value on success."
+  (let ((nth (cdr-safe target))
+        (char (car-safe string)))
+    (when nth
+      (setq target (car target)))
+    (when char
+      (setq string (cdr string)))
+    (when-let ((bounds (erc--mfs-get-nth-bounds target nth)))
+      (goto-char (if afterp (cdr bounds) (car bounds)))
+      (insert (if char (propertize string 'erc--% char) string))
+      (point))))
+
+(defun erc--msgfspec-insert-plain-before
+    (msgfspec target-char string &optional nth)
+  "Insert STRING before the first (or NTH) of TARGET-CHAR's format specifiers.
+Assume STRING does not contain a format specifier, and expect
+MSGFSPEC to be an `erc--msgfspec' object.  Return point on
+success."
+  (with-current-buffer (erc--msgfspec-buffer msgfspec)
+    (erc--mfs-insert-before (if nth (cons target-char nth) target-char)
+                            string)))
+
+(defun erc--msgfspec-insert-spec-before
+    (msgfspec target-char spec-char spec-string &optional nth)
+  "Insert SPEC-STRING before TARGET-CHAR's first (or NTH) format specifier.
+Assume SPEC-STRING contains a format specifier for SPEC-CHAR, and
+expect MSGFSPEC to be an `erc--msgfspec' object.  Return point on
+success."
+  (with-current-buffer (erc--msgfspec-buffer msgfspec)
+    (erc--mfs-insert-before (if nth (cons target-char nth) target-char)
+                            (cons spec-char spec-string))))
+
+(defun erc--msgfspec-insert-plain-after
+    (msgfspec target-char string &optional nth)
+  "Insert STRING after the first (or NTH) of TARGET-CHAR's format specifiers.
+Assume STRING does not contain a format specifier, and expect
+MSGFSPEC to be an `erc--msgfspec' object.  Return point on
+success."
+  (with-current-buffer (erc--msgfspec-buffer msgfspec)
+    (erc--mfs-insert-before (if nth (cons target-char nth) target-char)
+                            string 'afterp)))
+
+(defun erc--msgfspec-insert-spec-after
+    (msgfspec target-char spec-char spec-string &optional nth)
+  "Insert SPEC-STRING after TARGET-CHAR's first (or NTH) format specifier.
+Assume SPEC-STRING contains a format specifier for SPEC-CHAR, and
+expect MSGFSPEC to be an `erc--msgfspec' object.  Return point on
+success."
+  (with-current-buffer (erc--msgfspec-buffer msgfspec)
+    (erc--mfs-insert-before (if nth (cons target-char nth) target-char)
+                            (cons spec-char spec-string) 'afterp)))
+
 (defcustom erc-show-speaker-membership-status nil
   "Whether to prefix speakers with their channel status.
 For example, when this option is non-nil and some nick \"Alice\"
@@ -9121,17 +9215,25 @@ erc-popup-input-buffer
 
 ;;; Message catalog
 
+(defvar erc--matched-message-catalog (gensym "erc-"))
+
 (define-inline erc--make-message-variable-name (catalog key softp)
   "Return variable name conforming to ERC's message-catalog interface.
 Given a CATALOG symbol `mycat' and format-string KEY `mykey',
 also a symbol, return the symbol `erc-message-mycat-mykey'.  With
-SOFTP, only do so when defined as a variable."
+SOFTP, only do so when defined as a variable.  As a side effect
+of SOFTP, when `erc--matched-message-catalog' is nil, set it to
+CATALOG if KEY is found."
   (inline-quote
    (let* ((catname (symbol-name ,catalog))
           (prefix (if (eq ?- (aref catname 0)) "erc--message" "erc-message-"))
           (name (concat prefix catname "-" (symbol-name ,key))))
      (if ,softp
-         (and-let* ((s (intern-soft name)) ((boundp s))) s)
+         (and-let* ((s (intern-soft name))
+                    ((boundp s)))
+           (unless erc--matched-message-catalog
+             (setq erc--matched-message-catalog ,catalog))
+           s)
        (intern name)))))
 
 (defun erc-make-message-variable-name (catalog entry)
@@ -9331,12 +9433,20 @@ erc-format-message
 See also `format-spec'."
   (when (eq (logand (length args) 1) 1) ; oddp
     (error "Obscure usage of this function appeared"))
-  (let ((entry (erc-retrieve-catalog-entry msg)))
+  (let* ((erc--matched-message-catalog nil)
+         (entry (erc-retrieve-catalog-entry msg)))
     (when (not entry)
       (error "No format spec for message %s" msg))
     (when (functionp entry)
       (setq entry (apply entry args)))
-    (format-spec entry (apply #'format-spec-make args) 'ignore)))
+    (if-let ((catalog erc--matched-message-catalog)
+             (spec-makr (get catalog 'erc-msgfspec-makr))
+             (spec-hook (get catalog 'erc-msgfspec-hook))
+             (spec-fmtr (get catalog 'erc-msgfspec-fmtr))
+             (obj (apply spec-makr msg entry args)))
+        (progn (run-hook-with-args spec-hook obj)
+               (funcall spec-fmtr obj))
+      (format-spec entry (apply #'format-spec-make args) 'ignore))))
 
 ;;; Various hook functions
 
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index b3912cab33d..b6bc4962a2e 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2560,6 +2560,180 @@ erc--format-speaker-input-message
     (should (equal (erc-tests--format-my-nick "oh my") expect))
     (should (equal (erc--format-speaker-input-message "oh my") expect))))
 
+(ert-deftest erc--define-msgfspec ()
+  (when (< emacs-major-version 28)
+    (ert-skip "`get-buffer-create' lacks `inhibit-buffer-hooks'"))
+  (should
+   (equal
+    (macroexpand-1 '(erc--define-msgfspec foo
+                      (?a . "Ay.")
+                      (?b . "Bee.")
+                      (?c . "See.")
+                      (my-slot nil :type list :documentation "OK.")))
+
+    '(progn
+
+       (cl-defstruct (erc-msgfspec-foo (:include erc--msgfspec))
+         "Shared object for `foo' catalog message-format hook."
+         (\?a "" :type 'string :documentation "Ay.")
+         (\?b "" :type 'string :documentation "Bee.")
+         (\?c "" :type 'string :documentation "See.")
+         (my-slot nil :type list :documentation "OK."))
+
+       (defvar erc-msgfspec-foo-hook nil
+         "Hook run before formatting a `foo' catalog entry.
+Called by `erc-format-message' with an `erc-msgfspec-foo'
+object.")
+
+       (defun erc-msgfspec-foo-from-args (key format &rest spec-plist)
+         "Create a `erc-msgfspec-foo' object from catalog entry.
+Expect KEY to be the entry's key, FORMAT its value, and
+SPEC-PLIST the plist of `format-spec' args originally given
+to `erc-display-message'."
+         (let ((buffer (get-buffer-create
+                        (format " *erc-msgfspec-foo-%s*" key) t))
+               args)
+           (with-current-buffer buffer (insert format))
+           (while-let ((spec-plist)
+                       (key (pop spec-plist))
+                       (val (pop spec-plist)))
+             (setq args (nconc (list (pcase key (?a :?a) (?b :?b) (?c :?c))
+                                     val)
+                               args)))
+           (apply #'make-erc-msgfspec-foo :key key :buffer buffer args)))
+
+       (defun erc-msgfspec-foo-apply-spec (msgfspec-obj)
+         "Massage MSGFSPEC-OBJ into args for `format-spec'.
+Apply the latter after incorporating the `alist' slot
+for the current `erc-msgfspec-foo' object."
+         (format-spec (with-current-buffer (erc--msgfspec-buffer msgfspec-obj)
+                        (remove-text-properties (point-min) (point-max)
+                                                '(erc--% nil))
+                        (prog1 (buffer-string) (kill-buffer)))
+                      `((?a . ,(erc-msgfspec-foo-?a msgfspec-obj))
+                        (?b . ,(erc-msgfspec-foo-?b msgfspec-obj))
+                        (?c . ,(erc-msgfspec-foo-?c msgfspec-obj))
+                        ,@(erc-msgfspec-foo-alist msgfspec-obj))
+                      'ignore))
+
+       (put 'foo 'erc-msgfspec-makr #'erc-msgfspec-foo-from-args)
+       (put 'foo 'erc-msgfspec-fmtr 'erc-msgfspec-foo-apply-spec)
+       (put 'foo 'erc-msgfspec-hook 'erc-msgfspec-foo-hook)))))
+
+(ert-deftest erc--mfs-get-bounds ()
+  (erc-mode)
+  (should-not (erc--mfs-get-bounds ?a))
+
+  (insert (propertize "%a" 'erc--% ?a))
+  (should (equal (erc--mfs-get-bounds ?a) '(1 . 3)))
+  (should (equal (buffer-substring 1 3) "%a"))
+  (should (= (point-max) 3))
+
+  (insert (propertize "%<010b" 'erc--% ?b))
+  (should (equal (erc--mfs-get-bounds ?b) '(3 . 9)))
+  (should (equal (buffer-substring 3 9) "%<010b"))
+  (should (= (point-max) 9))
+
+  (insert (propertize "%c" 'erc--% ?c))
+  (should (equal (erc--mfs-get-bounds ?c) '(9 . 11)))
+  (should (equal (buffer-substring 9 11) "%c"))
+  (should (= (point-max) 11))
+
+  ;; With start pos.
+  (insert (propertize "%^a" 'erc--% ?a))
+  (should (equal (erc--mfs-get-bounds ?a 3) '(11 . 14)))
+  (should (equal (buffer-substring 11 14) "%^a"))
+  (should (= (point-max) 14)))
+
+(ert-deftest erc--mfs-get-nth-bounds ()
+  (erc-mode)
+
+  (should-not (erc--mfs-get-nth-bounds ?a 0))
+  (should-not (erc--mfs-get-nth-bounds ?a 1))
+
+  (insert #("%a %a" 0 2 (erc--% 97) 3 5 (erc--% 97)))
+  (should (equal (erc--mfs-get-nth-bounds ?a 1) '(1 . 3)))
+  (should (equal (erc--mfs-get-nth-bounds ?a 2) '(4 . 6))))
+
+(ert-deftest erc--mfs-insert-before () ; and *-after
+  (erc-mode)
+  (should-not (erc--mfs-insert-before ?a ""))
+
+  (insert (propertize "%a" 'erc--% ?a))
+  (should (= 2 (erc--mfs-insert-before ?a "[")))
+  (should (= 5 (erc--mfs-insert-before ?a "]" 'afterp)))
+  (should (equal (buffer-string) "[%a]"))
+
+  (should (= 10 (erc--mfs-insert-before ?a '(?b . "%<010b") 'afterp)))
+  (should (equal (buffer-string) "[%a%<010b]"))
+
+  (should (= 13 (erc--mfs-insert-before ?b '(?a . "%^a") 'afterp)))
+  (should (equal (buffer-string) "[%a%<010b%^a]"))
+
+  ;; With start pos.
+  (should (= 11 (erc--mfs-insert-before '(?a . 2) "@")))
+  (should (equal (buffer-string) "[%a%<010b@%^a]")))
+
+(ert-deftest erc--msgfspec-speaker-from-args ()
+  (erc-mode)
+  (let ((obj (erc--msgfspec-speaker-from-args
+              'input-chan-privmsg erc--message-speaker-input-chan-privmsg
+              ?p "@" ?n "bob" ?s "" ?m "Hi.")))
+
+    (ert-info ("Plain")
+      (with-current-buffer (erc--msgfspec-buffer obj)
+        (should (equal "<%p%n> %m" (buffer-string)))
+        (should (eql 3 (erc--msgfspec-insert-plain-before obj ?p "_")))
+        (should (equal "<_%p%n> %m" (buffer-string)))
+
+        ;; Does not inherit.
+        (should (= ?_ (char-after 2)))
+        (should-not (text-properties-at 2))
+        (should (eql 6 (erc--msgfspec-insert-plain-after obj ?p "_")))
+        (should (= ?_ (char-after 5)))
+        (should-not (text-properties-at 5))
+        (should (equal "<_%p_%n> %m" (buffer-string)))))
+
+    (ert-info ("Spec")
+      (with-current-buffer (erc--msgfspec-buffer obj)
+        ;; Before.
+        (should (equal "<_%p_%n> %m" (buffer-string)))
+        (should (eql 8 (erc--msgfspec-insert-spec-before
+                        obj ?n ?i (propertize "%i" 'font-lock-face 'my-face))))
+        (should (equal "<_%p_%i%n> %m" (buffer-string)))
+        (should (looking-at (rx "%n> %m")))
+        (should (erc-tests-common-equal-with-props
+                 (buffer-substring 6 8)
+                 #("%i" 0 2 (erc--% ?i font-lock-face my-face))))
+
+        ;; After.
+        (should (eql 12 (erc--msgfspec-insert-spec-after obj ?n ?i "%i")))
+        (should (looking-at (rx "> %m")))
+        (should (equal "<_%p_%i%n%i> %m" (buffer-string)))
+        (should (erc-tests-common-equal-with-props
+                 (buffer-substring 10 12) #("%i" 0 2 (erc--% ?i))))
+
+        ;; Seek.
+        (should (eql 13 (erc--msgfspec-insert-plain-after obj ?i "_" 2)))
+        (should (looking-at (rx "> %m")))
+        (should (equal "<_%p_%i%n%i_> %m" (buffer-string)))))
+
+    (ert-info ("Render")
+      (push '(?i . "~") (erc--msgfspec-alist obj))
+      (should (erc-tests-common-equal-with-props
+               (erc--msgfspec-speaker-apply-spec obj)
+               #("<_@_~bob~_> Hi."
+                 0 1 (font-lock-face erc-default-face)
+                 ;; 1 2 _
+                 2 3 (font-lock-face erc-my-nick-prefix-face)
+                 ;; 3 4 _
+                 4 5 (font-lock-face my-face)
+                 5 8 (font-lock-face erc-my-nick-face)
+                 ;; 8 10 ~_
+                 10 12 (font-lock-face erc-default-face)
+                 12 15 (font-lock-face erc-input-face))))
+      (should-not (buffer-live-p (erc--msgfspec-buffer obj))))))
+
 (ert-deftest erc--route-insertion ()
   (erc-tests-common-prep-for-insertion)
   (erc-tests-common-init-server-proc "sleep" "1")
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index feaba85ec90..2da225223ca 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 490 (wrap-prefix #1# line-prefix #9#) 490 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 522 (wrap-prefix #1# line-prefix #12#) 522 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
index ed1488c8595..d3704aa7ed9 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 490 (wrap-prefix #1# line-prefix #9#) 490 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 522 (wrap-prefix #1# line-prefix #12#) 522 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
index a3530a6c44d..e280e654f11 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 490 (wrap-prefix #1# line-prefix #9#) 490 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 522 (wrap-prefix #1# line-prefix #13#) 522 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Add-replacement-text-field-to-erc-input-struct.patch --]
[-- Type: text/x-patch, Size: 11521 bytes --]

From 673a6d7f99d5e2cae263cc919b03ea29f7163f2e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 1 Jan 2024 06:37:25 -0800
Subject: [PATCH 1/2] [5.6] Add replacement-text field to erc-input struct

* etc/ERC-NEWS: Promote `refoldp' slot from simulated to real.
* lisp/erc/erc-common.el (erc-input): Add `substxt' and `refoldp'
slots.
(erc--input-split): Move `refoldp' to "superclass".
* lisp/erc/erc-goodies.el (erc--command-indicator-permit-insertion):
Use `substxt' field instead of `insertp'.
(erc--command-indicator-display): Accept extra lines.
* lisp/erc/erc.el (erc-pre-send-functions): Revise doc.
(erc--input-ensure-hook-context, erc-input-refoldp): Remove unused
functions.
(erc--run-send-hooks): Copy data from additional fields of `erc-input'
object to `erc--input-split' object.
(erc--send-input-lines): Handle `substxt' field of `erc-input' object
when it's non-nil.  (Bug#68265)
---
 etc/ERC-NEWS            | 14 +++++--
 lisp/erc/erc-common.el  | 19 ++++++++-
 lisp/erc/erc-goodies.el |  7 +++-
 lisp/erc/erc.el         | 89 +++++++++++++++++++----------------------
 4 files changed, 74 insertions(+), 55 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 6cfa704d995..2adcc9ab9f4 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -570,9 +570,17 @@ ERC now adjusts input lines to fall within allowed length limits
 before showing hook members the result.  For compatibility,
 third-party code can request that the final input be adjusted again
 prior to being sent.  To facilitate this, the 'erc-input' object
-shared among hook members has gained a "phony" 'refoldp' slot that's
-only accessible from 'erc-pre-send-functions'.  See doc string for
-details.
+shared among hook members has gained a 'refoldp' slot.  See doc string
+for details.
+
+*** More flexibility in sending and displaying prompt input.
+The abnormal hook 'erc-pre-send-functions' previously married outgoing
+message text to its inserted representation in an ERC target buffer.
+Going forward, users can populate the new slot 'substxt' with
+alternate text to insert in place of the 'string' slot's contents,
+which ERC still sends to the server.  This dichotomy lets users
+completely avoid the often fiddly 'erc-send-modify-hook' and friends
+for use cases like language translation and subprotocol encoding.
 
 *** ERC's prompt survives the insertion of user input and messages.
 Previously, ERC's prompt and its input marker disappeared while
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index e7e70fffd3a..30ce0a0bb1a 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -49,7 +49,23 @@ erc-session-server
 (declare-function widget-type "wid-edit" (widget))
 
 (cl-defstruct erc-input
-  string insertp sendp)
+  "Object shared among members of `erc-pre-send-functions'.
+Any use outside of the hook is not supported."
+  ( string "" :type string
+    :documentation "String to send and, without `substxt', insert.
+ERC treats separate lines as separate messages.")
+  ( insertp nil :type boolean
+    :documentation "Whether to insert outgoing message.
+When nil, ERC still sends `string'.")
+  ( sendp nil :type boolean
+    :documentation "Whether to send and (for compat reasons) insert.
+To insert without sending, define a (slash) command.")
+  ( substxt nil :type (or function string null)
+    :documentation "Alternate string to insert without splitting.
+The function form is for internal use.")
+  ( refoldp nil :type boolean
+    :documentation "Whether to resplit a possibly overlong `string'.
+ERC only refolds `string', never `substxt'."))
 
 (cl-defstruct (erc--input-split (:include erc-input
                                           (string "" :read-only t)
@@ -57,7 +73,6 @@ erc-input
                                           (sendp (with-suppressed-warnings
                                                      ((obsolete erc-send-this))
                                                    erc-send-this))))
-  (refoldp nil :type boolean)
   (lines nil :type (list-of string))
   (abortp nil :type (list-of symbol))
   (cmdp nil :type boolean))
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 23589657b2d..3434280bbb4 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -583,15 +583,18 @@ erc--command-indicator-permit-insertion
   "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)
+    (setf (erc--input-split-insertp state) t
+          (erc--input-split-substxt state) #'erc--command-indicator-display)
     (erc-send-distinguish-noncommands state)))
 
 ;; This 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)
+(defun erc--command-indicator-display (line &rest rest)
   "Insert command LINE as echoed input resembling that of REPLs and shells."
   (when erc-insert-this
+    (when rest
+      (setq line (string-join (cons line rest) "\n")))
     (save-excursion
       (erc--assert-input-bounds)
       (let ((insert-position (marker-position (goto-char erc-insert-marker)))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 478683a77f5..00963d24a32 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1211,30 +1211,30 @@ erc-send-pre-hook
 (make-obsolete-variable 'erc-send-pre-hook 'erc-pre-send-functions "27.1")
 
 (defcustom erc-pre-send-functions nil
-  "Special hook run to possibly alter the string that is sent.
-The functions are called with one argument, an `erc-input' struct,
-and should alter that struct.
+  "Special hook to possibly alter the string to send and insert.
+ERC calls the member functions with one argument, an `erc-input'
+struct instance to modify as needed.
 
-The struct has three slots:
-
-  `string': The current input string.
-  `insertp': Whether the string should be inserted into the erc buffer.
-  `sendp': Whether the string should be sent to the irc server.
-
-And one \"phony\" slot only accessible by hook members at runtime:
+The struct has five slots:
 
-  `refoldp': Whether the string should be re-split per protocol limits.
+  `string': String to send, originally from prompt input.
+  `insertp': Whether a string should be inserted in the buffer.
+  `sendp': Whether `string' should be sent to the IRC server.
+  `substxt': String to display (but not send) instead of `string'.
+  `refoldp': Whether to re-split `string' per protocol limits.
 
 This hook runs after protocol line splitting has taken place, so
-the value of `string' is originally \"pre-filled\".  If you need
-ERC to refill the entire payload before sending it, set the phony
-`refoldp' slot to a non-nil value.  Note that this refilling is
-only a convenience, and modules with special needs, such as
-preserving \"preformatted\" text or encoding for subprotocol
-\"tunneling\", should handle splitting manually."
-  :group 'erc
-  :type 'hook
-  :version "27.1")
+the value of `string' comes \"pre-split\" according to the option
+`erc-split-line-length'.  If you need ERC to refill the entire
+payload before sending it, set the `refoldp' slot to a non-nil
+value.  Note that this refilling is only a convenience, and
+modules with special needs, such as preserving \"preformatted\"
+text or encoding for subprotocol \"tunneling\", should handle
+splitting manually and possibly also specify replacement text to
+display via the `substxt' slot."
+  :package-version '(ERC . "5.3")
+  :group 'erc-hooks
+  :type 'hook)
 
 (define-obsolete-variable-alias 'erc--pre-send-split-functions
   'erc--input-review-functions "30.1")
@@ -7825,22 +7825,6 @@ erc--split-lines
     (setf (erc--input-split-lines state)
           (mapcan #'erc--split-line (erc--input-split-lines state)))))
 
-(defun erc--input-ensure-hook-context ()
-  (unless (erc--input-split-p erc--current-line-input-split)
-    (error "Invoked outside of `erc-pre-send-functions'")))
-
-(defun erc-input-refoldp (_)
-  "Impersonate accessor for phony `erc-input' `refoldp' slot.
-This function only works inside `erc-pre-send-functions' members."
-  (declare (gv-setter (lambda (v)
-                        `(progn
-                           (erc--input-ensure-hook-context)
-                           (setf (erc--input-split-refoldp
-                                  erc--current-line-input-split)
-                                 ,v)))))
-  (erc--input-ensure-hook-context)
-  (erc--input-split-refoldp erc--current-line-input-split))
-
 (defun erc--run-send-hooks (lines-obj)
   "Run send-related hooks that operate on the entire prompt input.
 Sequester some of the back and forth involved in honoring old
@@ -7858,12 +7842,17 @@ erc--run-send-hooks
              (state (progn
                       ;; This may change `str' and `erc-*-this'.
                       (run-hook-with-args 'erc-send-pre-hook str)
-                      (make-erc-input :string str
-                                      :insertp erc-insert-this
-                                      :sendp erc-send-this))))
+                      (make-erc-input
+                       :string str
+                       :insertp erc-insert-this
+                       :sendp erc-send-this
+                       :substxt (erc--input-split-substxt lines-obj)
+                       :refoldp (erc--input-split-refoldp lines-obj)))))
         (run-hook-with-args 'erc-pre-send-functions state)
         (setf (erc--input-split-sendp lines-obj) (erc-input-sendp state)
               (erc--input-split-insertp lines-obj) (erc-input-insertp state)
+              (erc--input-split-substxt lines-obj) (erc-input-substxt state)
+              (erc--input-split-refoldp lines-obj) (erc-input-refoldp state)
               ;; See note in test of same name re trailing newlines.
               (erc--input-split-lines lines-obj)
               (let ((lines (split-string (erc-input-string state)
@@ -7881,15 +7870,19 @@ erc--run-send-hooks
 (defun 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))
-      (when (erc--input-split-insertp lines-obj)
-        (if (eq (erc--input-split-insertp lines-obj)
-                'erc--command-indicator-display)
-            (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))))))
+    (let ((insertp (erc--input-split-insertp lines-obj))
+          (substxt (erc--input-split-substxt lines-obj)))
+      (when (and insertp substxt)
+        (setq insertp nil)
+        (if (functionp substxt)
+            (apply substxt (erc--input-split-lines lines-obj))
+          (erc-display-msg substxt)))
+      (dolist (line (erc--input-split-lines lines-obj))
+        (when insertp
+          (erc-display-msg line))
+        (erc-process-input-line (concat line "\n")
+                                (null erc-flood-protect)
+                                (not (erc--input-split-cmdp lines-obj)))))))
 
 (defun erc-send-input (input &optional skip-ws-chk)
   "Treat INPUT as typed in by the user.
-- 
2.42.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Expose-catalog-specific-message-formatter-in-ERC.patch --]
[-- Type: text/x-patch, Size: 55568 bytes --]

From 9bb8693156326cf3ef33f9ccf1e6b4bbbcf9ed61 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 9 Jan 2024 06:54:18 -0800
Subject: [PATCH 2/2] [5.6] Expose catalog-specific message formatter in ERC

* lisp/erc/erc-common.el (erc--msgfspec): New struct to support
"msgfspec" interface.
(erc--define-msgfspec): New macro for defining items needed to support
the "msgfspec" interface.  These can of course be defined manually.
(erc--msgfspec-speaker): New struct for `speaker' catalog
implementation of "msgfspec" interface.
(erc--msgfspec-speaker-hook): New variable.
(erc--msgfspec-speaker-from-args): New function, an alternate
constructor for `make-erc--msgfspec-speaker.
(erc--msgfspec-speaker-apply-spec): New function, a "formatter"
for the "msgfspec" interface.
* lisp/erc/erc.el
(erc--message-speaker-statusmsg, erc--message-speaker-statusmsg-input,
erc--message-speaker-input-chan-privmsg,
erc--message-speaker-input-query-privmsg,
erc--message-speaker-input-query-notice,
erc--message-speaker-input-chan-notice,
erc--message-speaker-chan-privmsg, erc--message-speaker-query-privmsg,
erc--message-speaker-chan-notice, erc--message-speaker-query-notice,
erc--message-speaker-ctcp-action,
erc--message-speaker-ctcp-action-input,
erc--message-speaker-ctcp-action-statusmsg,
erc--message-speaker-ctcp-action-statusmsg-input): Update variable
values to include tracer sentinels in the form of `erc--%' text props
that correspond to each template's format specifiers.  They are
removed during formatting and serve to help hook members splice,
excise, and interrogate a shared copy of the template.
(erc--mfs-get-bounds, erc--mfs-get-nth-bounds, erc--mfs-insert-before,
erc--msgfspec-insert-plain-before, erc--msgfspec-insert-spec-before,
erc--msgfspec-insert-plain-after, erc--msgfspec-insert-spec-after):
New utility functions and internal helpers to assist hook members in
manipulating the working template prior to formatting.
(erc--matched-message-catalog): New variable.
(erc--make-message-variable-name): Assign matched catalog to
`erc--matched-message-catalog' when it's nil, which indicates it's
been let-bound somewhere back in the call stack.
(erc-format-message): Run catalog-specific message-formatting handler
when defined.
* test/lisp/erc/erc-tests.el (erc--define-msgfspec,
erc--mfs-get-bounds,
erc--mfs-insert-before,
erc--msgfspec-speaker-from-args): New tests.  (Bug#67677)  (Bug#68265)
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld: Update.
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld:
; Update.
; * test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld:
; Update.
---
 lisp/erc/erc-common.el                        | 107 ++++++++++
 lisp/erc/erc.el                               | 200 ++++++++++++++----
 test/lisp/erc/erc-tests.el                    | 174 +++++++++++++++
 .../fill/snapshots/merge-wrap-01.eld          |   2 +-
 .../merge-wrap-indicator-post-01.eld          |   2 +-
 .../snapshots/merge-wrap-indicator-pre-01.eld |   2 +-
 6 files changed, 439 insertions(+), 48 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 30ce0a0bb1a..eee06a3fde9 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -165,6 +165,113 @@ erc--isupport-data
   (table (make-char-table 'erc--channel-mode-types) :type char-table)
   (shortargs (make-hash-table :test #'equal)))
 
+(cl-defstruct erc--msgfspec
+  "Abstract struct for object shared among message-format hook members."
+  ( key nil :type symbol
+    :documentation "Catalog entry key, a symbol.")
+  ( buffer nil :type (or buffer null)
+    :documentation "Buffer with catalog entry value, a template string.")
+  ( alist nil :type list
+    :documentation "Extra items for `format-spec' SPECIFICATION."))
+
+(defmacro erc--define-msgfspec (catalog &rest slots)
+  "Define items that satisfy the \"erc-msgfspec\" interface.
+Expect symbol CATALOG to be the name of a message-format catalog
+and SLOTS to be an alist of (CHAR . DOC), where CHAR is a
+`format-spec' character and DOC is a string explaining its role.
+Using these, define a \"msgfspec\" data struct named
+\"erc-msgfspec-CATALOG\", with massaged SLOTS becoming its
+string-typed fields.  Arrange for each field to be DOC'umented
+and accessed by a function named \"erc-msgfspec-CATALOG-?CHAR\"
+\(note the question mark).  Additionally, create a constructor
+for the struct, named \"erc-msgfspec-CATALOG-from-args\", along
+with a hook variable named \"erc-msgfspec-CATALOG-hook\".  Also
+define a wrapper named \"erc-msgfspec-CATALOG-apply-spec\" to
+allow the `erc-display-message' machinery to call `format-spec'
+with a specification alist derived from the struct instance
+passed around and potentially influenced by each hook member."
+  (declare (indent 1)
+           (debug (symbolp (&rest &or symbolp (symbolp &rest sexp)))))
+  (let* ((chars)
+         (specs (let (out)
+                  (while-let (((characterp (car-safe (car-safe slots))))
+                              (spec (pop slots))
+                              (char (car spec)))
+                    (cl-assert (stringp (cdr spec)))
+                    (push `(,(intern (format "?%c" char)) ""
+                            :type 'string :documentation ,(cdr spec))
+                          out)
+                    (push char chars))
+                  (setq chars (nreverse chars))
+                  (nreverse out)))
+         (internalp (eq ?- (aref (symbol-name catalog) 0)))
+         (name (if internalp
+                   (substring (symbol-name catalog) 1)
+                 (symbol-name catalog)))
+         (full-name (concat "erc-" (if internalp "-" "") "msgfspec-" name))
+         (hook (intern (concat full-name "-hook")))
+         (makr (intern (concat full-name "-from-args")))
+         (fmtr (intern (concat full-name "-apply-spec")))
+         (setters (mapcar (lambda (letr)
+                            (list letr '\,
+                                  `(,(intern (format "%s-?%c" full-name letr))
+                                    msgfspec-obj)))
+                          chars))
+         (choices (mapcar (lambda (letr)
+                            (list letr (intern (format ":?%c" letr))))
+                          chars)))
+    `(progn
+
+       (cl-defstruct (,(intern full-name) (:include erc--msgfspec))
+         ,(concat "Shared object for `" name
+                  "' catalog message-format hook.")
+         ,@specs
+         ,@slots)
+
+       (defvar ,hook nil
+         ,(concat "Hook run before formatting a `" name "' catalog entry."
+                  "\nCalled by `erc-format-message' with an `"
+                  full-name "'\nobject."))
+
+       (defun ,makr (key format &rest spec-plist)
+         ,(concat "Create a `" full-name "' object from catalog entry."
+                  "\nExpect KEY to be the entry's key, FORMAT its value, and"
+                  "\nSPEC-PLIST the plist of `format-spec' args originally"
+                  " given" ; continued
+                  "\nto `erc-display-message'.")
+         (let ((buffer (get-buffer-create
+                        (format ,(concat " *" full-name "-%s*") key)
+                        ,@(and (>= emacs-major-version 28) '(t))))
+               args)
+           (with-current-buffer buffer (insert format))
+           (while-let ((spec-plist)
+                       (key (pop spec-plist))
+                       (val (pop spec-plist)))
+             (setq args (nconc (list (pcase key ,@choices) val) args)))
+           (apply #',(intern (concat "make-" full-name))
+                  :key key :buffer buffer args)))
+       (defun ,fmtr (msgfspec-obj)
+         ,(concat "Massage MSGFSPEC-OBJ into args for `format-spec'."
+                  "\nApply the latter after incorporating the `alist' slot"
+                  "\nfor the current `" full-name "' object.")
+         (format-spec (with-current-buffer (erc--msgfspec-buffer msgfspec-obj)
+                        (remove-text-properties (point-min) (point-max)
+                                                '(erc--% nil))
+                        (prog1 (buffer-string) (kill-buffer)))
+                      (,'\`(,@setters
+                            (,'\,@ (,(intern (concat full-name "-alist"))
+                                    msgfspec-obj))))
+                      'ignore))
+       (put ',catalog 'erc-msgfspec-makr #',makr)
+       (put ',catalog 'erc-msgfspec-fmtr ',fmtr)
+       (put ',catalog 'erc-msgfspec-hook ',hook))))
+
+(erc--define-msgfspec -speaker
+  (?n . "Nickname.")
+  (?m . "Message body.")
+  (?p . "Channel-membership prefix.")
+  (?s . "STATUSMSG prefix."))
+
 ;; After dropping 28, we can use prefixed "erc-autoload" cookies.
 (defun erc--normalize-module-symbol (symbol)
   "Return preferred SYMBOL for `erc--modules'."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 00963d24a32..f80bafa42b9 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -5924,121 +5924,134 @@ erc-format-privmessage
 
 ;; The format strings in the following `-speaker' catalog shouldn't
 ;; contain any non-protocol words, so they make sense in any language.
+;; Intervals with a format-spec specifier % ... c must have the text
+;; property (erc--% . ?c) so that msgfspec-related code can find it.
 
 (defvar erc--message-speaker-statusmsg
   #("(%p%n%s) %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-nick-prefix-face)
-    3 5 (font-lock-face erc-nick-default-face)
-    5 7 (font-lock-face erc-notice-face)
-    7 11 (font-lock-face erc-default-face))
+    1 3 (erc--% ?p font-lock-face erc-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-nick-default-face)
+    5 7 (erc--% ?s font-lock-face erc-notice-face)
+    7 9 (font-lock-face erc-default-face)
+    9 11 (erc--% ?m font-lock-face erc-default-face))
   "Message template for in-channel status messages.")
 
 (defvar erc--message-speaker-statusmsg-input
   #("(%p%n%s) %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-my-nick-prefix-face)
-    3 5 (font-lock-face erc-my-nick-face)
-    5 7 (font-lock-face erc-notice-face)
+    1 3 (erc--% ?p font-lock-face erc-my-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-my-nick-face)
+    5 7 (erc--% ?s font-lock-face erc-notice-face)
     7 8 (font-lock-face erc-default-face)
-    8 11 (font-lock-face erc-input-face))
+    8 9 (font-lock-face erc-input-face)
+    9 11 (erc--% ?m font-lock-face erc-input-face))
   "Message template for echoed status messages.")
 
 (defvar erc--message-speaker-input-chan-privmsg
   #("<%p%n> %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-my-nick-prefix-face)
-    3 5 (font-lock-face erc-my-nick-face)
+    1 3 (erc--% ?p font-lock-face erc-my-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-my-nick-face)
     5 7 (font-lock-face erc-default-face)
-    7 9 (font-lock-face erc-input-face))
+    7 9 (erc--% ?m font-lock-face erc-input-face))
   "Message template for prompt input or echoed PRIVMSG from own nick.")
 
 (defvar erc--message-speaker-input-query-privmsg
   #("*%n* %m"
     0 1 (font-lock-face erc-direct-msg-face)
-    1 3 (font-lock-face erc-my-nick-face)
+    1 3 (erc--% ?n font-lock-face erc-my-nick-face)
     3 5 (font-lock-face erc-direct-msg-face)
-    5 7 (font-lock-face erc-input-face))
+    5 7 (erc--% ?m font-lock-face erc-input-face))
   "Message template for prompt input or echoed PRIVMSG query from own nick.")
 
 (defvar erc--message-speaker-input-query-notice
   #("-%n- %m"
     0 1 (font-lock-face erc-direct-msg-face)
-    1 3 (font-lock-face erc-my-nick-face)
+    1 3 (erc--% ?n font-lock-face erc-my-nick-face)
     3 5 (font-lock-face erc-direct-msg-face)
-    5 7 (font-lock-face erc-input-face))
+    5 7 (erc--% ?m font-lock-face erc-input-face))
   "Message template for echoed or spoofed query NOTICE from own nick.")
 
 (defvar erc--message-speaker-input-chan-notice
   #("-%p%n- %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-my-nick-prefix-face)
-    3 5 (font-lock-face erc-my-nick-face)
+    1 3 (erc--% ?p font-lock-face erc-my-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-my-nick-face)
     5 7 (font-lock-face erc-default-face)
-    7 9 (font-lock-face erc-input-face))
+    7 9 (erc--% ?m font-lock-face erc-input-face))
   "Message template for prompt input or echoed NOTICE from own nick.")
 
 (defvar erc--message-speaker-chan-privmsg
   #("<%p%n> %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-nick-prefix-face)
-    3 5 (font-lock-face erc-nick-default-face)
-    5 9 (font-lock-face erc-default-face))
+    1 3 (erc--% ?p font-lock-face erc-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-nick-default-face)
+    5 7 (font-lock-face erc-default-face)
+    7 9 (erc--% ?m font-lock-face erc-default-face))
   "Message template for a PRIVMSG in a channel.")
 
 (defvar erc--message-speaker-query-privmsg
   #("*%n* %m"
     0 1 (font-lock-face erc-direct-msg-face)
-    1 3 (font-lock-face erc-nick-msg-face)
-    3 7 (font-lock-face erc-direct-msg-face))
+    1 3 (erc--% ?n font-lock-face erc-nick-msg-face)
+    3 5 (font-lock-face erc-direct-msg-face)
+    5 7 (erc--% ?m font-lock-face erc-direct-msg-face))
   "Message template for a PRIVMSG in query buffer.")
 
 (defvar erc--message-speaker-chan-notice
   #("-%p%n- %m"
     0 1 (font-lock-face erc-default-face)
-    1 3 (font-lock-face erc-nick-prefix-face)
-    3 5 (font-lock-face erc-nick-default-face)
-    5 9 (font-lock-face erc-default-face))
+    1 3 (erc--% ?p font-lock-face erc-nick-prefix-face)
+    3 5 (erc--% ?n font-lock-face erc-nick-default-face)
+    5 7 (font-lock-face erc-default-face)
+    7 9 (erc--% ?m font-lock-face erc-default-face))
   "Message template for a NOTICE in a channel.")
 
 (defvar erc--message-speaker-query-notice
   #("-%n- %m"
     0 1 (font-lock-face erc-direct-msg-face)
-    1 3 (font-lock-face erc-nick-msg-face)
-    3 7 (font-lock-face erc-direct-msg-face))
+    1 3 (erc--% 110 font-lock-face erc-nick-msg-face)
+    3 5 (font-lock-face erc-direct-msg-face)
+    5 7 (erc--% 109 font-lock-face erc-direct-msg-face))
   "Message template for a NOTICE in a query buffer.")
 
 (defvar erc--message-speaker-ctcp-action
   #("* %p%n %m"
     0 2 (font-lock-face erc-action-face)
-    2 4 (font-lock-face (erc-nick-prefix-face erc-action-face))
-    4 9 (font-lock-face erc-action-face))
+    2 4 (erc--% ?p font-lock-face (erc-nick-prefix-face erc-action-face))
+    4 6 (erc--% ?n font-lock-face erc-action-face)
+    6 7 (font-lock-face erc-action-face)
+    7 9 (erc--% ?m font-lock-face erc-action-face))
   "Message template for a CTCP ACTION from another user.")
 
 (defvar erc--message-speaker-ctcp-action-input
   #("* %p%n %m"
     0 2 (font-lock-face #1=(erc-input-face erc-action-face))
-    2 4 (font-lock-face (erc-my-nick-prefix-face . #1#))
-    4 6 (font-lock-face (erc-my-nick-face . #1#))
-    6 9 (font-lock-face #1#))
+    2 4 (erc--% ?p font-lock-face (erc-my-nick-prefix-face . #1#))
+    4 6 (erc--% ?n font-lock-face (erc-my-nick-face . #1#))
+    6 7 (font-lock-face #1#)
+    7 9 (erc--% ?m font-lock-face #1#))
   "Message template for a CTCP ACTION from current client.")
 
 (defvar erc--message-speaker-ctcp-action-statusmsg
   #("* (%p%n%s) %m"
     0 3 (font-lock-face erc-action-face)
-    3 5 (font-lock-face (erc-nick-prefix-face erc-action-face))
-    5 7 (font-lock-face erc-action-face)
-    7 9 (font-lock-face (erc-notice-face erc-action-face))
-    9 13 (font-lock-face erc-action-face))
+    3 5 (erc--% ?p font-lock-face (erc-nick-prefix-face erc-action-face))
+    5 7 (erc--% ?n font-lock-face erc-action-face)
+    7 9 (erc--% ?s font-lock-face (erc-notice-face erc-action-face))
+    9 11 (font-lock-face erc-action-face)
+    11 13 (erc--% ?m font-lock-face erc-action-face))
   "Template for a CTCP ACTION status message from another chan op.")
 
 (defvar erc--message-speaker-ctcp-action-statusmsg-input
   #("* (%p%n%s) %m"
     0 3 (font-lock-face #1=(erc-input-face erc-action-face))
-    3 5 (font-lock-face (erc-my-nick-prefix-face . #1#))
-    5 7 (font-lock-face (erc-my-nick-face . #1#))
-    7 9 (font-lock-face (erc-notice-face . #1#))
-    9 13 (font-lock-face #1#))
+    3 5 (erc--% ?p font-lock-face (erc-my-nick-prefix-face . #1#))
+    5 7 (erc--% ?n font-lock-face (erc-my-nick-face . #1#))
+    7 9 (erc--% ?s font-lock-face (erc-notice-face . #1#))
+    9 11 (font-lock-face #1#)
+    11 13 (erc--% ?m font-lock-face #1#))
   "Template for a CTCP ACTION status message from current client.")
 
 (defun erc--speakerize-nick (nick &optional disp)
@@ -6099,6 +6112,87 @@ erc--determine-speaker-message-format-args
         ?p (or prefix "") ?n (erc--speakerize-nick nick disp-nick)
         ?s (or statusmsg "") ?m message))
 
+(defun erc--mfs-get-bounds (char &optional from-pos)
+  "Return a cons cell with the bounds of CHAR's format specifier if found.
+Begin searching at FROM-POS if given.  On success, ensure the
+returned pair can be used to obtain CHAR's associated specifier
+via `buffer-substring', meaning the pair's CDR is one position
+beyond the end of the substring itself."
+  (and-let* ((beg (text-property-any (or from-pos (point-min)) (point-max)
+                                     'erc--% char))
+             (end (next-single-property-change beg 'erc--% nil (point-max))))
+    (cons beg end)))
+
+(defun erc--mfs-get-nth-bounds (char &optional n)
+  "Return bounds of CHAR's Nth occurrence, N=1 being the first/default."
+  (unless n (setq n 1))
+  (let (bounds)
+    (while (and (natnump (cl-decf n))
+                (setq bounds (erc--mfs-get-bounds char (cdr bounds)))))
+    bounds))
+
+(defun erc--mfs-insert-before (target string &optional afterp)
+  "Insert STRING before TARGET's format specifier.
+Expect TARGET to be the character associated with the format
+specifier to insert in front of.  Or, if multiple specifiers for
+the same character exist, and the first among them isn't desired,
+expect a cons of (CHAR . N) instead.  Assume STRING is either a
+plain string lacking any format specifiers or a cons of (CHAR
+. STRING-SPEC) designating exactly one character-specifier
+association for ERC to remember while formatting the current
+message, an example being (?a . \"%a\").  Move point as needed
+before inserting STRING, and return its updated value on success."
+  (let ((nth (cdr-safe target))
+        (char (car-safe string)))
+    (when nth
+      (setq target (car target)))
+    (when char
+      (setq string (cdr string)))
+    (when-let ((bounds (erc--mfs-get-nth-bounds target nth)))
+      (goto-char (if afterp (cdr bounds) (car bounds)))
+      (insert (if char (propertize string 'erc--% char) string))
+      (point))))
+
+(defun erc--msgfspec-insert-plain-before
+    (msgfspec target-char string &optional nth)
+  "Insert STRING before the first (or NTH) of TARGET-CHAR's format specifiers.
+Assume STRING does not contain a format specifier, and expect
+MSGFSPEC to be an `erc--msgfspec' object.  Return point on
+success."
+  (with-current-buffer (erc--msgfspec-buffer msgfspec)
+    (erc--mfs-insert-before (if nth (cons target-char nth) target-char)
+                            string)))
+
+(defun erc--msgfspec-insert-spec-before
+    (msgfspec target-char spec-char spec-string &optional nth)
+  "Insert SPEC-STRING before TARGET-CHAR's first (or NTH) format specifier.
+Assume SPEC-STRING contains a format specifier for SPEC-CHAR, and
+expect MSGFSPEC to be an `erc--msgfspec' object.  Return point on
+success."
+  (with-current-buffer (erc--msgfspec-buffer msgfspec)
+    (erc--mfs-insert-before (if nth (cons target-char nth) target-char)
+                            (cons spec-char spec-string))))
+
+(defun erc--msgfspec-insert-plain-after
+    (msgfspec target-char string &optional nth)
+  "Insert STRING after the first (or NTH) of TARGET-CHAR's format specifiers.
+Assume STRING does not contain a format specifier, and expect
+MSGFSPEC to be an `erc--msgfspec' object.  Return point on
+success."
+  (with-current-buffer (erc--msgfspec-buffer msgfspec)
+    (erc--mfs-insert-before (if nth (cons target-char nth) target-char)
+                            string 'afterp)))
+
+(defun erc--msgfspec-insert-spec-after
+    (msgfspec target-char spec-char spec-string &optional nth)
+  "Insert SPEC-STRING after TARGET-CHAR's first (or NTH) format specifier.
+Assume SPEC-STRING contains a format specifier for SPEC-CHAR, and
+expect MSGFSPEC to be an `erc--msgfspec' object.  Return point on
+success."
+  (with-current-buffer (erc--msgfspec-buffer msgfspec)
+    (erc--mfs-insert-before (if nth (cons target-char nth) target-char)
+                            (cons spec-char spec-string) 'afterp)))
+
 (defcustom erc-show-speaker-membership-status nil
   "Whether to prefix speakers with their channel status.
 For example, when this option is non-nil and some nick \"Alice\"
@@ -9121,17 +9215,25 @@ erc-popup-input-buffer
 
 ;;; Message catalog
 
+(defvar erc--matched-message-catalog (gensym "erc-"))
+
 (define-inline erc--make-message-variable-name (catalog key softp)
   "Return variable name conforming to ERC's message-catalog interface.
 Given a CATALOG symbol `mycat' and format-string KEY `mykey',
 also a symbol, return the symbol `erc-message-mycat-mykey'.  With
-SOFTP, only do so when defined as a variable."
+SOFTP, only do so when defined as a variable.  As a side effect
+of SOFTP, when `erc--matched-message-catalog' is nil, set it to
+CATALOG if KEY is found."
   (inline-quote
    (let* ((catname (symbol-name ,catalog))
           (prefix (if (eq ?- (aref catname 0)) "erc--message" "erc-message-"))
           (name (concat prefix catname "-" (symbol-name ,key))))
      (if ,softp
-         (and-let* ((s (intern-soft name)) ((boundp s))) s)
+         (and-let* ((s (intern-soft name))
+                    ((boundp s)))
+           (unless erc--matched-message-catalog
+             (setq erc--matched-message-catalog ,catalog))
+           s)
        (intern name)))))
 
 (defun erc-make-message-variable-name (catalog entry)
@@ -9331,12 +9433,20 @@ erc-format-message
 See also `format-spec'."
   (when (eq (logand (length args) 1) 1) ; oddp
     (error "Obscure usage of this function appeared"))
-  (let ((entry (erc-retrieve-catalog-entry msg)))
+  (let* ((erc--matched-message-catalog nil)
+         (entry (erc-retrieve-catalog-entry msg)))
     (when (not entry)
       (error "No format spec for message %s" msg))
     (when (functionp entry)
       (setq entry (apply entry args)))
-    (format-spec entry (apply #'format-spec-make args) 'ignore)))
+    (if-let ((catalog erc--matched-message-catalog)
+             (spec-makr (get catalog 'erc-msgfspec-makr))
+             (spec-hook (get catalog 'erc-msgfspec-hook))
+             (spec-fmtr (get catalog 'erc-msgfspec-fmtr))
+             (obj (apply spec-makr msg entry args)))
+        (progn (run-hook-with-args spec-hook obj)
+               (funcall spec-fmtr obj))
+      (format-spec entry (apply #'format-spec-make args) 'ignore))))
 
 ;;; Various hook functions
 
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index b3912cab33d..b6bc4962a2e 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2560,6 +2560,180 @@ erc--format-speaker-input-message
     (should (equal (erc-tests--format-my-nick "oh my") expect))
     (should (equal (erc--format-speaker-input-message "oh my") expect))))
 
+(ert-deftest erc--define-msgfspec ()
+  (when (< emacs-major-version 28)
+    (ert-skip "`get-buffer-create' lacks `inhibit-buffer-hooks'"))
+  (should
+   (equal
+    (macroexpand-1 '(erc--define-msgfspec foo
+                      (?a . "Ay.")
+                      (?b . "Bee.")
+                      (?c . "See.")
+                      (my-slot nil :type list :documentation "OK.")))
+
+    '(progn
+
+       (cl-defstruct (erc-msgfspec-foo (:include erc--msgfspec))
+         "Shared object for `foo' catalog message-format hook."
+         (\?a "" :type 'string :documentation "Ay.")
+         (\?b "" :type 'string :documentation "Bee.")
+         (\?c "" :type 'string :documentation "See.")
+         (my-slot nil :type list :documentation "OK."))
+
+       (defvar erc-msgfspec-foo-hook nil
+         "Hook run before formatting a `foo' catalog entry.
+Called by `erc-format-message' with an `erc-msgfspec-foo'
+object.")
+
+       (defun erc-msgfspec-foo-from-args (key format &rest spec-plist)
+         "Create a `erc-msgfspec-foo' object from catalog entry.
+Expect KEY to be the entry's key, FORMAT its value, and
+SPEC-PLIST the plist of `format-spec' args originally given
+to `erc-display-message'."
+         (let ((buffer (get-buffer-create
+                        (format " *erc-msgfspec-foo-%s*" key) t))
+               args)
+           (with-current-buffer buffer (insert format))
+           (while-let ((spec-plist)
+                       (key (pop spec-plist))
+                       (val (pop spec-plist)))
+             (setq args (nconc (list (pcase key (?a :?a) (?b :?b) (?c :?c))
+                                     val)
+                               args)))
+           (apply #'make-erc-msgfspec-foo :key key :buffer buffer args)))
+
+       (defun erc-msgfspec-foo-apply-spec (msgfspec-obj)
+         "Massage MSGFSPEC-OBJ into args for `format-spec'.
+Apply the latter after incorporating the `alist' slot
+for the current `erc-msgfspec-foo' object."
+         (format-spec (with-current-buffer (erc--msgfspec-buffer msgfspec-obj)
+                        (remove-text-properties (point-min) (point-max)
+                                                '(erc--% nil))
+                        (prog1 (buffer-string) (kill-buffer)))
+                      `((?a . ,(erc-msgfspec-foo-?a msgfspec-obj))
+                        (?b . ,(erc-msgfspec-foo-?b msgfspec-obj))
+                        (?c . ,(erc-msgfspec-foo-?c msgfspec-obj))
+                        ,@(erc-msgfspec-foo-alist msgfspec-obj))
+                      'ignore))
+
+       (put 'foo 'erc-msgfspec-makr #'erc-msgfspec-foo-from-args)
+       (put 'foo 'erc-msgfspec-fmtr 'erc-msgfspec-foo-apply-spec)
+       (put 'foo 'erc-msgfspec-hook 'erc-msgfspec-foo-hook)))))
+
+(ert-deftest erc--mfs-get-bounds ()
+  (erc-mode)
+  (should-not (erc--mfs-get-bounds ?a))
+
+  (insert (propertize "%a" 'erc--% ?a))
+  (should (equal (erc--mfs-get-bounds ?a) '(1 . 3)))
+  (should (equal (buffer-substring 1 3) "%a"))
+  (should (= (point-max) 3))
+
+  (insert (propertize "%<010b" 'erc--% ?b))
+  (should (equal (erc--mfs-get-bounds ?b) '(3 . 9)))
+  (should (equal (buffer-substring 3 9) "%<010b"))
+  (should (= (point-max) 9))
+
+  (insert (propertize "%c" 'erc--% ?c))
+  (should (equal (erc--mfs-get-bounds ?c) '(9 . 11)))
+  (should (equal (buffer-substring 9 11) "%c"))
+  (should (= (point-max) 11))
+
+  ;; With start pos.
+  (insert (propertize "%^a" 'erc--% ?a))
+  (should (equal (erc--mfs-get-bounds ?a 3) '(11 . 14)))
+  (should (equal (buffer-substring 11 14) "%^a"))
+  (should (= (point-max) 14)))
+
+(ert-deftest erc--mfs-get-nth-bounds ()
+  (erc-mode)
+
+  (should-not (erc--mfs-get-nth-bounds ?a 0))
+  (should-not (erc--mfs-get-nth-bounds ?a 1))
+
+  (insert #("%a %a" 0 2 (erc--% 97) 3 5 (erc--% 97)))
+  (should (equal (erc--mfs-get-nth-bounds ?a 1) '(1 . 3)))
+  (should (equal (erc--mfs-get-nth-bounds ?a 2) '(4 . 6))))
+
+(ert-deftest erc--mfs-insert-before () ; and *-after
+  (erc-mode)
+  (should-not (erc--mfs-insert-before ?a ""))
+
+  (insert (propertize "%a" 'erc--% ?a))
+  (should (= 2 (erc--mfs-insert-before ?a "[")))
+  (should (= 5 (erc--mfs-insert-before ?a "]" 'afterp)))
+  (should (equal (buffer-string) "[%a]"))
+
+  (should (= 10 (erc--mfs-insert-before ?a '(?b . "%<010b") 'afterp)))
+  (should (equal (buffer-string) "[%a%<010b]"))
+
+  (should (= 13 (erc--mfs-insert-before ?b '(?a . "%^a") 'afterp)))
+  (should (equal (buffer-string) "[%a%<010b%^a]"))
+
+  ;; With start pos.
+  (should (= 11 (erc--mfs-insert-before '(?a . 2) "@")))
+  (should (equal (buffer-string) "[%a%<010b@%^a]")))
+
+(ert-deftest erc--msgfspec-speaker-from-args ()
+  (erc-mode)
+  (let ((obj (erc--msgfspec-speaker-from-args
+              'input-chan-privmsg erc--message-speaker-input-chan-privmsg
+              ?p "@" ?n "bob" ?s "" ?m "Hi.")))
+
+    (ert-info ("Plain")
+      (with-current-buffer (erc--msgfspec-buffer obj)
+        (should (equal "<%p%n> %m" (buffer-string)))
+        (should (eql 3 (erc--msgfspec-insert-plain-before obj ?p "_")))
+        (should (equal "<_%p%n> %m" (buffer-string)))
+
+        ;; Does not inherit.
+        (should (= ?_ (char-after 2)))
+        (should-not (text-properties-at 2))
+        (should (eql 6 (erc--msgfspec-insert-plain-after obj ?p "_")))
+        (should (= ?_ (char-after 5)))
+        (should-not (text-properties-at 5))
+        (should (equal "<_%p_%n> %m" (buffer-string)))))
+
+    (ert-info ("Spec")
+      (with-current-buffer (erc--msgfspec-buffer obj)
+        ;; Before.
+        (should (equal "<_%p_%n> %m" (buffer-string)))
+        (should (eql 8 (erc--msgfspec-insert-spec-before
+                        obj ?n ?i (propertize "%i" 'font-lock-face 'my-face))))
+        (should (equal "<_%p_%i%n> %m" (buffer-string)))
+        (should (looking-at (rx "%n> %m")))
+        (should (erc-tests-common-equal-with-props
+                 (buffer-substring 6 8)
+                 #("%i" 0 2 (erc--% ?i font-lock-face my-face))))
+
+        ;; After.
+        (should (eql 12 (erc--msgfspec-insert-spec-after obj ?n ?i "%i")))
+        (should (looking-at (rx "> %m")))
+        (should (equal "<_%p_%i%n%i> %m" (buffer-string)))
+        (should (erc-tests-common-equal-with-props
+                 (buffer-substring 10 12) #("%i" 0 2 (erc--% ?i))))
+
+        ;; Seek.
+        (should (eql 13 (erc--msgfspec-insert-plain-after obj ?i "_" 2)))
+        (should (looking-at (rx "> %m")))
+        (should (equal "<_%p_%i%n%i_> %m" (buffer-string)))))
+
+    (ert-info ("Render")
+      (push '(?i . "~") (erc--msgfspec-alist obj))
+      (should (erc-tests-common-equal-with-props
+               (erc--msgfspec-speaker-apply-spec obj)
+               #("<_@_~bob~_> Hi."
+                 0 1 (font-lock-face erc-default-face)
+                 ;; 1 2 _
+                 2 3 (font-lock-face erc-my-nick-prefix-face)
+                 ;; 3 4 _
+                 4 5 (font-lock-face my-face)
+                 5 8 (font-lock-face erc-my-nick-face)
+                 ;; 8 10 ~_
+                 10 12 (font-lock-face erc-default-face)
+                 12 15 (font-lock-face erc-input-face))))
+      (should-not (buffer-live-p (erc--msgfspec-buffer obj))))))
+
 (ert-deftest erc--route-insertion ()
   (erc-tests-common-prep-for-insertion)
   (erc-tests-common-init-server-proc "sleep" "1")
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index feaba85ec90..2da225223ca 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 490 (wrap-prefix #1# line-prefix #9#) 490 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 522 (wrap-prefix #1# line-prefix #12#) 522 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
index ed1488c8595..d3704aa7ed9 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-post-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 0)) display #8="") 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 490 (wrap-prefix #1# line-prefix #9#) 490 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #10#) 499 505 (wrap-prefix #1# line-prefix #10#) 505 506 (display #("~\n" 0 2 (font-lock-face shadow))) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #8#) 507 510 (wrap-prefix #1# line-prefix #11# display #8#) 510 512 (wrap-prefix #1# line-prefix #11# display #8#) 512 515 (wrap-prefix #1# line-prefix #11#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #12=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #12#) 518 521 (wrap-prefix #1# line-prefix #12#) 521 522 (wrap-prefix #1# line-prefix #12#) 522 527 (wrap-prefix #1# line-prefix #12#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #13#) 532 539 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
index a3530a6c44d..e280e654f11 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#5=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #6#) 459 466 (wrap-prefix #1# line-prefix #6#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 #10=(2))) display #8=#("> " 0 1 (font-lock-face shadow))) 475 478 (wrap-prefix #1# line-prefix #7# display #8#) 478 480 (wrap-prefix #1# line-prefix #7# display #8#) 480 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 485 486 (wrap-prefix #1# line-prefix #9#) 486 489 (wrap-prefix #1# line-prefix #9#) 489 490 (wrap-prefix #1# line-prefix #9#) 490 494 (wrap-prefix #1# line-prefix #9#) 495 496 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 (6)))) 496 499 (wrap-prefix #1# line-prefix #11#) 499 505 (wrap-prefix #1# line-prefix #11#) 506 507 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 #10#)) display #8#) 507 510 (wrap-prefix #1# line-prefix #12# display #8#) 510 512 (wrap-prefix #1# line-prefix #12# display #8#) 512 515 (wrap-prefix #1# line-prefix #12#) 516 517 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #13=(space :width (- 27 (2)))) 517 518 (wrap-prefix #1# line-prefix #13#) 518 521 (wrap-prefix #1# line-prefix #13#) 521 522 (wrap-prefix #1# line-prefix #13#) 522 527 (wrap-prefix #1# line-prefix #13#) 528 529 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #14=(space :width (- 27 (6)))) 529 532 (wrap-prefix #1# line-prefix #14#) 532 539 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
-- 
2.42.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* bug#67677: 30.0.50; ERC 5.6: Use templates for formatting chat messages
       [not found] ` <87a5paa5j0.fsf@neverwas.me>
@ 2024-01-19  2:16   ` J.P.
       [not found]   ` <87fryurrst.fsf@neverwas.me>
  1 sibling, 0 replies; 6+ messages in thread
From: J.P. @ 2024-01-19  2:16 UTC (permalink / raw)
  To: 67677; +Cc: emacs-erc

"J.P." <jp@neverwas.me> writes:

> Earlier work for this bug extended ERC's existing template catalog
> system with an internal framework for dictating (hopefully more
> explicitly and resolutely) how messages materialize in chat buffers.
> Modules can make use of this framework to shape most aspects of message
> formatting, which opens the door to radically contrasting styles, such
> as "multi-part" messages with header/body/footer sections and messages
> with integrated time stamps (meaning you can finally ditch that blessed
> `stamp' module for good).
>
> However, this approach remains awkward when it comes to
>
>   1. minor modifications
>
>   2. modularity itself (keeping modules loosely coupled)
>
> At present, making use of the framework involves defining an entire
> format catalog (although inheritance helps a bit in this regard). But
> the boilerplate issue really begins to compound when trying to integrate
> with other modules because the process is somewhat dependent on defining
> yet more catalogs to serve the various combinations, and more still if
> trying to keep things abstract.

Perhaps this bears some clarifying because a traditional reason for
defining "behavior protocols" is to promote modularity and code reuse.
That'd still be the case here, but not so much in the short term, where
catalog proliferation would be unavoidable. For example, let's say a new
module for spoofing bridge nicknames (call it `masquerade') provides an
option for showing select envelope info on its own line in a
"headline-style" message format:

  <{DisplayName}> {Handle{Affiliation}}\n{Body}

Now suppose another module, `away-indicator', normally displays
"<speaker>" tags in italics for nicks currently away. And suppose this
module wants to integrate with templates supplying headline-style
formatting because there's more room to show a dedicated "away" lighter.
It would first need to detect when such formatting was in play (or
arrange for doing so later) during module init, perhaps by checking
whether the `masquerade' option's variable is bound and true. It would
then replace the current `erc-message-speaker-catalog', possibly set to
something like `masqueraade-speaker', with its own `away-masq-speaker'
(or whatever). It could not, though, elect to detect such behavior on
the fly in a hook member (likely by searching for behavioral clues, like
a newline following a "<speaker>" label) because by that time, the
message has already been formatted.

A superior approach to such an explicit, cross-contaminating integration
(and in-hook heuristics-based detection, for that matter) would be for
ERC to provide a base protocol for headline-style catalogs. This would
likely mean a higher order "meta template" sporting text properties for
identifying intervals relevant to insertion-time formatting. Such
properties need not be exposed directly to users and would likely be
removed after running insertion hooks. Implementers of compatible
catalogs would then declare support by invoking a constructor, perhaps
at top level, that "pre-renders" their actual user-facing templates
(although a runtime version may also work via the function variant of
the symbol-name-based `erc-message-{catalog}-{key}' interface).

For example, a headline-style meta template might look something like

  {Pre}{Headline}{Sep}{Body} ; in reality #("%P%H%S%B" 0 2 (...) ...)

with predefined text properties turning each specifier's argument into a
quasi field of sorts. This would allow the rendered version to do things
like have "{Pre}" and "{Sep}" contain (even multiple) newlines, possibly
as `display' property replacement text. But the main point would be to
make it trivial for "consumers," like `fill', to isolate and operate on
various components and implement consistent provider-agnostic behavior
in hook members without being thrown off by specifics, like the length
or visibility of "{Sep}". Of course, the cost of all this is complexity
and verbosity in the form of additional catalogs, even if some are just
stubs consisting mostly of glue. (That is unless we add some kind of
hook-based solution as proposed by this bug.)

> To alleviate some of this awkwardness and cut down on the verbosity, I'm
> proposing we introduce a more practical extension to this framework.
> It'll be reserved for internal use at first, but with an eye toward
> eventual export (likely in 5.7). The basic idea is that we define a
> single abnormal hook per speaker catalog that runs just prior to
> insertion, and we allow its members to influence the parameters passed
> to `format-spec'. And, we do so in a convenient and structured (and
> reusable) way, so members don't have to twiddle plists in search of a
> single ingredient to manually splice into format strings based on the
> verdict of some flimsy heuristic.
>
> I'm tentatively calling this hook system "msgfspc" (one word), short for
> "message format-spec."

This name is pretty ugly, but we can't really drop the leading "msg" for
just "fspec" or "fmtspec" because ERC already uses `format-spec' in
differing contexts, such as for creating its prompt and its mode line.
IOW, it must be qualified as message-related somehow. Secondly, the name
probably shouldn't contain hyphens because that hampers readability once
a reader has been conditioned to expect it. For example, I'm guessing
most would agree that `erc--msg-fmt-spec-foo-bar' is initially
preferable to `erc--msgfspec-foo-bar'. However, after repeated exposure,
the latter starts to appear less cluttered (IMO), with the elision from
`msgfmtspec' to `msgfspec' perhaps helping in this regard (hand wave).

> It works by defining a struct to accompany each
> hook, with slots based on the catalog's common set of template
> specifiers. Subscribing code then has the freedom to modify the template
> itself and add or subtract specifiers as needed by mutating the struct
> instance for that particular formatting pass. The client API looks like
> this:
>
>   ;; Module activation body
>
>   (add-hook 'erc-msgfspec-speaker-hook
>             #my-maybe-transform-on-msgfspec-speaker nil t)
>   (setq my-state "foo")
>   
>   ;; Top level of package
>
>   (defun my-maybe-transform-on-msgfspec-speaker (spec)
>     (pcase spec
>
>       ;; Modify an outgoing message template.
>
>       ((cl-struct erc-msgfspec-speaker
>                   (key (or 'input-chan-privmsg 'input-query-privmsg)))
>        (erc-msgfspec-insert-spec-after
>         spec ?n ?i (propertize "%i" 'font-lock-face 'my-face))
>        (push `(?i . ,my-state) (erc-msgfspec-alist spec)))
>   
>       ;; Modify an incoming message body.
>
>       ((cl-struct erc-msgfspec-speaker
>                   (key (or 'chan-privmsg 'query-privmsg))
>                   (\?m msg))
>        (setf (erc-msgfspec-speaker-?m spec)
>              (decode-coding-string (my-transform-message msg)
>                                    'utf-8)))))
>
>   ;; Note that at present, all the "erc-foo" symbols above are actually
>   ;; "erc--foo" (internal)
>
> There's at least one unfortunate aspect to the API scheme above: the
> buffer where the working version of a template resides isn't current
> when hooks run. This happens because members still need access to local
> state in the ERC buffer where the actual insertion takes place. I've
> experimented a bit with using the virtual buffer facility (via
> `buffer-swap-text') to get around this, and it appears to work great
> (even seemingly shaving a second or two off runs of ERC's extended test
> suite). However, I'm quite reticent to introduce something I've never
> used before and almost never see in the wild. Thus, this approach will
> have to wait pending further investigation.

An alternative approach would be to use positional arguments of the
hook's member functions to provide access to the various parameters,
instead of passing around a struct instance. This trades the burden of
having to learn the struct's composition with learning the hook's
calling convention. Such an implementation somewhat resembles args- or
return-filtering advice, except it'd almost certainly be implemented via
`run-hook-wrapped'.

Such an approach might be friendlier to more casual users who aren't
comfortable manipulating cl-struct objects. However, one possible
downside is a slightly elevated maintenance cost. IMO, evolving the
schema of a struct is pretty painless and transparent to the user as
long as they're able to take sessions offline to recompile dependencies.
However, updating function signatures, as would be required by the
wrapped-hook method, likely involves adding a `condition-case' to the
wrapper, wherein we'd trap v1 calls and issue a warning before adapting
them to handle v2's format.

I may implement a demo version with a simulated version migration so we
have something concrete to compare.

> The current version of the proposed implementation can be found in the
> second of the attached patches. The first is from bug#68265 but included
> here because the associated demo addressing a real-world use case
> requires both. Please see that bug's recent posts for links and
> instructions.
>
> Thanks.

In the second patch,

> +                              (char (car spec)))
> +                    (cl-assert (stringp (cdr spec)))
> +                    (push `(,(intern (format "?%c" char)) ""
> +                            :type 'string :documentation ,(cdr spec))

                                     ^ this should not be quoted
> +                          out)
> +                    (push char chars))

> +
> +       (cl-defstruct (erc-msgfspec-foo (:include erc--msgfspec))
> +         "Shared object for `foo' catalog message-format hook."
> +         (\?a "" :type 'string :documentation "Ay.")
> +         (\?b "" :type 'string :documentation "Bee.")
> +         (\?c "" :type 'string :documentation "See.")
> +         (my-slot nil :type list :documentation "OK."))

And likewise in the test's assertion for the macro expansion.





^ permalink raw reply	[flat|nested] 6+ messages in thread

* bug#67677: 30.0.50; ERC 5.6: Use templates for formatting chat messages
       [not found]   ` <87fryurrst.fsf@neverwas.me>
@ 2024-02-01  3:14     ` J.P.
  0 siblings, 0 replies; 6+ messages in thread
From: J.P. @ 2024-02-01  3:14 UTC (permalink / raw)
  To: 67677-done; +Cc: emacs-erc

Further work on this will be handled by bug#68861:

  30.0.50; ERC 5.x: Introduce a modern message-insertion API

Closing (again).





^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2024-02-01  3:14 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
     [not found] <87jzpq7apw.fsf@neverwas.me>
2023-12-18 14:50 ` bug#67677: 30.0.50; ERC 5.6: Use templates for formatting chat messages J.P.
     [not found] ` <87v88vftu6.fsf@neverwas.me>
2024-01-08  5:46   ` J.P.
2024-01-12 16:19 ` J.P.
     [not found] ` <87a5paa5j0.fsf@neverwas.me>
2024-01-19  2:16   ` J.P.
     [not found]   ` <87fryurrst.fsf@neverwas.me>
2024-02-01  3:14     ` J.P.
2023-12-07  7:06 J.P.

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).