bug#68861: 30.0.50; ERC 5.x: Introduce a modern message-insertion API
"J.P." <[email protected]> Mon, 23 Feb 2026 23:46:34 -0800
| Newsgroups | gmane.emacs.bugs,gmane.emacs.erc.general |
|---|---|
| Message-ID | <[email protected]> |
"J.P." <[email protected]> writes: > Severity: wishlist > > Some recent ramblings on this topic (by me) were crudely tacked on to > the end of the now closed bug#67677. This bug is also closely related to what I've been calling "part two" of bug#49860, which aims to add IRCv3 features that basically require not only insertions away from prompt but also random splicing, deleting, and refreshing of those inserted messages. These are all currently foreign concepts to ERC. I'd say the surest route to attaining them is by revamping ERC's insertion handling so it's based on message objects. By this I mean organizing a message's makeup into a structured and persistent constellation of individual ingredients to be rendered in an elegant and straightforward manner. This should make influencing everything from portions of messages to spans of multiple messages more predictable, extensible, and maintainable. > In summary, the solution originally delivered by that bug of having > all chat messages formatted by literal `format-spec' templates was > left wanting in terms of an easily exposable and extendable public > API. Unfortunately, what I'm proposing here will only prolong that deficit in terms of moving first to a solely internal API to be trialed and improved upon indefinitely or until complaints arise. To that end, I think we ought to introduce this as two separate but related areas in the code base: - An optional library and module providing the underlying "refreshable" insertion framework, which new code can depend on directly - An interface for existing code to alter its behavior when refreshable insertion is in effect This two-pronged approach at least superficially favors caution in the name of preserving compatibility with existing interfaces, like `erc-insert-modify-hook', and all the behavioral nuances baked into them [1]. In the end, though, this change is quite radical in that it upends fundamental assumptions underpinning the client's design: no longer will ERC be presenting an append-only log to users but instead a dynamic "palimpsest" of sorts that can, in a very general sense, even be likened to a graphical canvas. > Instead, I think it'd be preferable to take > a more traditional tack and preserve the various constituent parts of a > template as separate nodes in a tree-like structure for deferred > assembly just prior to formatting, perhaps in conjunction with some > caching mechanism. The literal, propertized base template would still > feature prominently as part of a template class's definition, but its > role would be reduced to serving as input for a validation scheme. I've taken a stab at something loosely resembling the approach described, although most existing template-based messages won't be decomposed into meaningful hierarchies beyond simplistic partitioning into {head} and {body} components. However, those with a speaker-based {head} could conceivably be broken down further into: __________________{head}__________________ / \ {left-bracket}{speaker}{right-bracket}{space} The reason they're not expanded to that level of granularity at the moment is because I want to resist introducing contiguous but discrete intervals of common text properties, which can slow down scanning. Such entropy can be mitigated somewhat by consolidating them into a `category' text property, although there are compatibility trade offs with that as well. > In terms of proposals, I think a successful candidate for adoption > should provide at least two working demos, preferably among the > following: > > - Bidirectional input and display > - Headline style messages w. speaker on a separate line > - Text substitution (language translation, encryption, etc.) > - Display names (e.g., for bridges, perhaps reversible) (To this list, I'd add a top-down layout, where the prompt sits atop the buffer and new messages spawn just below it.) Anyway, here is a simplified sketch of the proposed design (full PoC available soon): Message objects =============== Each message is represented by an instance of a struct-based type that marries a hierarchical model of arbitrary message subdivisions (called "divs") with attributes derived from common `erc--msg-props'. Traditional modules that don't want to `require' the library can interact with message objects before, during, and after insertion via a "refreshable" compatibility API defined in erc-common. A div tree's root is represented by a function assigned to the message object's "div" slot. Its job is to insert the entire contents, including any bounding newlines. It does this either by deferring to a list of similar "child" div functions, implemented as a familiar Lisp hook, or by inserting text directly, or both. A special wrapper that runs the hook ensures its members (children) are inserted depth-first, with smaller hook depths corresponding to earlier buffer positions. So, a div is in fact more than a function: it depends on supporting properties as well. A div function's own behavior can be modified by normal Lisp advice, which, like its hook members, is added via the help of an associative labeling scheme and a dedicated helper utility. Advice can also be applied to its children in this same manner as well as their children, and so on. Once again, for the sake of clarity: each div consists of an insertion function, a label, a hook, and an advice stack, with the latter two only defined as needed. For example, a div tree for a foldable "erc-multiline--code" message displaying a block of syntax-highlighted source code might look like: - :label "root" :inserter #'erc-div--insert-children :advice [:after #'erc-read-only] :hook - :label "above" :inserter #'erc-div--insert-hard-newline - :label "head" :inserter #'erc-div--insert-speaker :advice - :override #'erc-gravitar--iconify - :after #'erc-fill--wrap-add-indent-prefix - :label "body" :inserter #'erc-div--insert-children :advice [:after #'erc-fill--wrap-add-wrap-prefix] :hook - :label "multiline-bookend-beg" :inserter #'erc-multiline--insert-bookend-beg - :label "body/inner" :inserter #'erc-div--insert-body-from-backing-store :advice - :after #'erc-multiline--code-fontify-by-major-mode - :after #'erc-multiline--add-folding-overlay - :label "multiline-bookend-end" :inserter #'erc-multiline--insert-bookend-end - :label "tail" :inserter #'ignore - :label "below" :inserter #'ignore While a div's shape is arbitrary, the three "well known" message types that serve as adapters for traditional template-based messages all subdivide their "root" div into top-level divs named "above", "head", "body", "tail", and "below". Thus, a module that has no interest in defining its own message types or integrating with the framework at all, can still piggyback on other div-based messages using more abstract, albeit less convenient, hooks and advice. For example: (add-hook 'erc--refreshable-modify-div-functions (erc--refreshable-modify-div-functions #'erc--make-invisible :after 40 'head 'tail)) The above adds the function `erc--make-invisible' as advice that runs after the "head" or "tail" div has inserted its contents in any buffer. (add-hook 'erc--refreshable-modify-div-functions (erc--refreshable-modify-div-functions #'erc-foo--insert-right 'foo-right 10 'head) 0 'localp) This example instead adds a *new* div called "foo-right" as a child of "head" that runs after the latter's own insertion code because the depth is positive; a -10 would have it run before. Each generated root div is cached with a composite key composed of relevant factors available just before insertion, such as the default and buffer-local values of `erc--refreshable-modify-div-functions'. "Mutable" buffers ================= A buffer's contents are modeled as an ERC-flavored Ewoc with a few minor modifications. Modules that manage state between messages, such as those providing unique speaker tags or timestamps, or those needing to adapt to splices and deletions for other reasons can do so by providing handler functions conforming to local-advice-based interfaces defined as function-valued variables. A handler can react to changes by requesting that arbitrary nodes be deleted or refreshed. These actions can cascade, so care must be taken to avoid cycles: (defun erc-foo--handle-splice (i n inst prev next) "Consider INST, the Ith message object currently being inserted. Weigh it in the context of neighbors PREV and NEXT, the I-1th and I+1th (of N total) messages in the sequence, respectively. If INST is the last of its sequence and has an `erc-foo' property, schedule NEXT for refreshing if it too has one." (when (and prev next (= i n) (erc--refreshable-prop-get inst 'erc-foo) (erc--refreshable-prop-get next 'erc-foo)) (list next))) The function above would ostensibly belong to an older `erc-foo' module that doesn't directly depend on the library providing the framework. Instead, it treats params INST and co as opaque object handles, and it uses the abstract "refreshable" interface to interact with them. By contrast, a "native" module built directly atop the framework would interact with such objects directly, through their slot accessors and various library utilities. The primary reason for maintaining this designation and the consequent indirection is to avoid foisting a rather massive library on users who only ever use traditional modules. For sequential insertions, such as history playback, modules may need to maintain state apart from what's normally reserved for "live" arrivals inserted at prompt. Otherwise, important data, like that contained in `erc-server-users', will be munged. For this shadowing to work, a module provides a piece of local :around advice invoking a special macro: (defun erc-foo--wrap-state (inner) (erc--refreshable-with-locals ((erc-foo-var1 (erc-foo-init 1)) ; value form runs once (erc-foo-var2 (erc-foo-init 2))) (funcall inner) ; defer to insertion code (erc-foo-teardown))) ; clean up resources The macro captures and restores the values of the `let'-style VARLIST before and after insertion for every message in the sequence. The WIP framework currently addresses both concerns outlined above, that is, both message objects and mutable buffers, as a single module in a single library. It may be prudent to split these up in into separate files, although I'd rather preserve the library-module correspondence if at all possible. That said, the library is quite large, at around 1,700 lines, with around half being compatibility oriented. Additional details to follow at some point. Thanks for reading. [1] Some compatibility related limitations of the current version: - When redrawing an inserted message, it currently only restores the `erc-parsed' text property for commands backed by a data store, since these sustain enough info to reconstruct an `erc-response' object. Members of traditional insertion hooks that rely on `erc-get-parsed-vector' for other message types, like a 353, must adapt to the property's absence. - It discards the trailing newline normally retained in the narrowed buffer after `erc-insert-modify-hook' and friends have visited. Thus, any markers left at `point-max' by a hook member no longer sit between a newline and the start of a message but between the end of a message and a newline. - The value of the quasi-internal `erc--msg' text property is now a message object instance, and it may appear at a later position relative to the start of a message because it now demarcates the boundary between any preceding newlines and the logical contents. Some utilities, like `erc--get-inserted-msg-prop', have been made aware of this difference.