Package: emacs;
Reported by: "J.P." <jp <at> neverwas.me>
Date: Mon, 14 Oct 2024 02:25:02 UTC
Severity: wishlist
Tags: patch
Found in version 31.0.50
To reply to this bug, email your comments to 73798 AT debbugs.gnu.org.
Toggle the display of automated, internal messages from the tracker.
View this report as an mbox folder, status mbox, maintainer mbox
emacs-erc <at> gnu.org, bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Mon, 14 Oct 2024 02:25:02 GMT) Full text and rfc822 format available."J.P." <jp <at> neverwas.me>
:emacs-erc <at> gnu.org, bug-gnu-emacs <at> gnu.org
.
(Mon, 14 Oct 2024 02:25:02 GMT) Full text and rfc822 format available.Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: bug-gnu-emacs <at> gnu.org Subject: 31.0.50; ERC 5.7: New extensibility focused match API Date: Sun, 13 Oct 2024 19:21:37 -0700
[Message part 1 (text/plain, inline)]
Tags: patch This follows loosely from bug#73580 and bug#68265. It's well known that a favorite pastime of ERC users is griping about its lack of an easily exploitable API for analyzing and manipulating message content. The current landscape is indeed regrettably sparse, with avenues either too rough and tumble, like at the protocol handling level, or too zoomed out and refined, like at the user options level. The traditional go-to solution (one certainly not known for its stellar UX) lies somewhere in between. Often called the "message insertion" phase (by me), this somewhat nebulous and often feared limbo period between message receipt and display is home to the infamous `erc-insert-modify-hook' and friends, where a good many modules, including `match', do their dirty work. Anyone familiar with the aforementioned bug#68265 will know that it's very much about offering access before initial formatting takes place, meaning just prior to insertion. Another TBD bug to be opened eventually will explore the flip side: an easier way to examine and influence a message's fate during insertion (e.g., determining which insertion hooks run and how their arguments and return values take shape). As mentioned, this current bug is meant to address modification immediately after insertion, while a message's content and format are less violently in flux but still being finalized in the narrowed buffer. Here's the proposed documentation: File: erc.info, Node: Match API, Next: Options, Prev: Module Loading, Up: Advanced Usage 5.6 Match API ============= This section describes the low-level ‘match’ API introduced in ERC 5.7. For basic, options-oriented usage, please see the doc strings for option ‘erc-pal-highlight-type’ and friends in the ‘erc-match’ group. Unfortunately, those options often prove insufficient for more granular filtering and highlighting needs, and advanced users eventually outgrow them. However, under the hood, those options all use the same foundational ‘erc-match’ API, which centers around a ‘cl-defstruct’ “type” of the same name: -- Struct: erc-match predicate spkr-beg spkr-end body-beg sender nick command handler This is a ‘cl-struct’ type that contains some handy facts about the message being processed. That message's formatted body occupies the narrowed buffer when ERC creates and provides access to each ‘erc-match’ instance. To use this interface, you add a “constructor”-like function to the list ‘erc-match-types’: -- User Option: erc-match-types A hook-like list of functions, where each accepts the parameters named above as an ‘&rest’-style plist and returns a new ‘erc-match’ instance. A function can also be a traditional ‘cl-defstruct’-provided constructor belonging to a “subtype” you've defined. The only slot you definitely need to specify is ‘predicate’. Both it and ‘handler’ are functions that take a single argument: the instance itself. As its name implies, ‘predicate’ must return non-‘nil’ if ‘handler’, whose return value ERC ignores, should run. A few slots, like ‘spkr-beg’, ‘spkr-end’, and ‘nick’, may surprise you. The first two are ‘nil’ for non-chat messages, like those displayed for ‘JOIN’ events. The ‘nick’ slot can likewise be ‘nil’ if the sender of the message is a domain-style host name, such as ‘irc.example.org’, which it often is for informational messages, like ‘*** #chan was created on 2023-12-26 00:36:42’. To locate the start of the just-inserted message, use ‘body-beg’, a marker indicating the beginning of the message proper. Don't forget: all inserted messages include a trailing newline. If you want to extract just the message body's text, use the function ‘erc-match-get-message-body’: -- Function: erc-match-get-message-body match Takes an ‘erc-match’ instance and returns a string containing the message body, sans trailing newline and any leading speaker or decorative component, such as ‘erc-notice-prefix’. Although module authors may want to subclass this struct, everyday users can just instantiate it directly (it's “concrete”). This is especially handy for one-off tasks or simple customizations in your ‘init.el’. To do this, define a function that invokes its constructor: (require 'erc-match) (defvar my-mentions 0) (defun my-match (&rest plist) (apply #'erc-match :predicate (lambda (_) (search-forward "my-project" nil t)) :handler (lambda (_) (cl-incf my-mentions)) plist)) (setopt erc-match-types (add-to-list 'erc-match-types #'my-match) erc-prompt (lambda () (format "%d!" my-mentions))) Here, the user could just as well shove the incrementer into the ‘predicate’ body, since ‘handler’ is set to ‘ignore’ by default (however, some frown at the notion of a predicate exhibiting side effects). Likewise, the user could also choose to concentrate only on chat content by filtering out non-‘PRIVMSG’ messages via the slot ‘command’. For a detailed example showing how to use this API for more involved matching that doesn't involve highlighting, see the ‘notifications’ module, which lives in ‘erc-desktop-notifications.el’. Ignore the parts that involve adapting the global setup (and teardown) business to a buffer-local context. Since your module is declared ‘local’, as per the modern convention, you won't be needing such code, so feel free to use utility functions like ‘erc-match-add-local-type’ directly in your module's definition. 5.6.1 Highlighting ------------------ Third-party modules likely want to manage and apply faces themselves. However, in a pinch you can just piggyback atop the highlighting functionality already provided by ‘match’ to support its many high-level options. (require 'erc-match) (defvar my-keywords `((foonet ("#chan" ,(rx bow (or "foo" "bar" "baz") eow))))) (defface my-face '((t (:inherit font-lock-constant-face :weight bold))) "My face.") (defun my-match (&rest plist) (apply #'erc-match-opt-keyword :data (and-let* ((chans (alist-get (erc-network) my-keywords)) ((cdr (assoc (erc-target) chans))))) :face 'my-face plist)) (setopt erc-match-types (add-to-list 'erc-match-types #'my-match)) Here, the user leverages a handy subtype of ‘erc-match’, called ‘erc-match-opt-keyword’, which actually descends directly from another, intermediate ‘erc-match’ type: -- Struct: erc-match-traditional category face data part Use this type or one of its descendants (see below) if you want ‘erc-text-matched-hook’ to run alongside (after) the ‘handler’ slot's default highlighter, ‘erc-match-highlight’, on every match for which the ‘category’ slot's value is non-‘nil’ (it becomes the argument provided for the hook's MATCH-TYPE parameter). Much more important, however, is ‘part’. This slot determines what portion of the message is being highlighted or otherwise operated on. It can be any symbol, but the ones with predefined methods are ‘nick’, ‘message’, ‘all’, ‘keyword’, ‘nick-or-keyword’, and ‘nick-or-mention’. The default handler, ‘erc-match-highlight’, does its work by deferring to a purpose-built “method” meant to handle ‘part’-based highlighting: -- Method on erc-match-traditional: erc-match-highlight-by-part instance part You can override this method by “specializing” on any subclassed ‘erc-match-traditional’ type and/or non-reserved PART, such as one known only to your ‘init.el’ or (informally) associated with your package by its library “namespace”. You likely won't be needing these, but for the sake of completeness, other options-based types similar to ‘erc-match-opt-keyword’ include ‘erc-match-opt-current-nick’, ‘erc-match-opt-fool’, ‘erc-match-opt-pal’, and ‘erc-match-opt-dangerous-host’. (If you're familiar with this module's user options, you'll notice some parallels here.) And, finally, here's a more elaborate, module-like example demoing highlighting based on the ‘erc-match-traditional’ type: ;; -*- lexical-binding: t; -*- (require 'erc-match) (require 'erc-button) (defvar my-keywords `((foonet ("#chan" ,(rx bow (or "foo" "bar" "baz") eow))))) (defface my-keyword '((t (:underline (:color "tomato" :style wave)))) "My face.") (defun my-get-keyword () (and-let* ((chans (alist-get (erc-network) my-keywords)) ((cdr (assoc (erc-target) chans)))))) (cl-defstruct (my-match (:include erc-match-opt-keyword (part 'keyword) (data (my-get-keyword)) (face 'my-keyword)) (:constructor my-match))) (setopt erc-match-types (add-to-list 'erc-match-types #'my-match)) (cl-defmethod erc-match-highlight-by-part ((instance my-match) (_ (eql keyword))) "Highlight keywords by merging instead of clobbering." (dolist (pat (my-match-data instance)) (goto-char (my-match-body-beg instance)) (while (re-search-forward pat nil t) (erc-button-add-face (match-beginning 0) (match-end 0) (my-match-face instance))))) (Note that in the method body, you _could_ technically skip to the beginning of the last match for the first go around because the match data from the ‘predicate’ is still fresh.) Some canned Q&As: 1. ERC is already famously bogged down by ill-conceived contracts, why add another? This isn't just some new feature. It's a revamping and refactoring of the `match' library that exposes much needed foundational seams to users while encapsulating some compat-related business and other unruly minutiae, such as text-property twiddling. It also includes a partial refactoring of the `notifications' module (housed in erc-desktop-notifications.el) that makes heavy use of this new API and thus serves as a reference implementation for certain flavors of non-highlight-centric matching. But TBF, the "extensibility focused" bit from this bug's subject line is somewhat of a stretch. In reality, such extensibility is merely a knock-on effect of bending over backward to prioritize compatibility. But that doesn't mean it can't also help tamp down on contracts proliferation in ERC. For those unfamiliar, users often rightly call for some existing option to be expanded or spun off into a variant more attuned to a specific context: e.g., option erc-foo needs to be network/channel/nickname aware. One way to push back on such demands is to point to a not-too-painful existing workaround. 2. Why not combine the predicate and handler slots of match objects? We certainly could (and very well still might), in which case this would end up looking more like a traditional hook arrangement. The original idea was to promote code reuse and separation of concerns by enforcing a bit of structure. However, it's certainly true that hook members could just as easily perform the predicate and handler logic themselves. 3. Why the separate message-body getter instead of a dedicated slot? To retain that state in match objects would mean having to update it between handler runs because users are invited to modify the text. (This will be mentioned in a code comment.) 4. Why not EIEIO? While such a move might help "consumers" of the API to some degree, the expanded feature set won't really have the same impact on implementation and upkeep of the library. And though ERC does currently pull in dependencies that in turn require EIEIO, that may not always be so. (Note that while the attached patches target ERC 5.7, they don't include changes to bump the version, etc.) For anyone interested in this bug, feedback of any form is always welcome. Thanks! In GNU Emacs 31.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.43, cairo version 1.18.0) of 2024-10-07 built on localhost Repository revision: ff4de9eff30ade164655354f71e7cbca48135858 Repository branch: master Windowing system distributor 'The X.Org Foundation', version 11.0.12401002 System Description: Fedora Linux 40 (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 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 compile comint ansi-osc ansi-color ring comp-run bytecomp byte-compile comp-common rx emacsbug message mailcap yank-media puny dired dired-loaddefs rfc822 mml mml-sec password-cache epa derived epg rfc6068 epg-config gnus-util text-property-search time-date subr-x mm-decode mm-bodies mm-encode mail-parse rfc2231 mailabbrev gmm-utils mailheader cl-loaddefs cl-lib sendmail rfc2047 rfc2045 ietf-drums mm-util mail-prsvr mail-utils 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 59412 9167) (symbols 48 6747 0) (strings 32 16822 4165) (string-bytes 1 492565) (vectors 16 11411) (vector-slots 8 139223 13079) (floats 8 21 4) (intervals 56 248 0) (buffers 984 11))
[0001-5.7-Use-speaker-prefix-end-marker-in-ERC-insertion-h.patch (text/x-patch, attachment)]
[0002-5.7-Introduce-lower-level-erc-match-API.patch (text/x-patch, attachment)]
[0003-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch (text/x-patch, attachment)]
bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Fri, 25 Oct 2024 23:49:01 GMT) Full text and rfc822 format available.Message #8 received at 73798 <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: 73798 <at> debbugs.gnu.org Cc: emacs-erc <at> gnu.org Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API Date: Fri, 25 Oct 2024 16:48:13 -0700
v2. Resolve merge conflict after 8903106b. Expose current match object to legacy hook via dynamic variable. "J.P." <jp <at> neverwas.me> writes: > (Note that while the attached patches target ERC 5.7, they don't include > changes to bump the version, etc.) This may cause some confusion until the version on HEAD is updated to 5.7-git.
bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Fri, 25 Oct 2024 23:52:02 GMT) Full text and rfc822 format available.Message #11 received at 73798 <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: 73798 <at> debbugs.gnu.org Cc: emacs-erc <at> gnu.org Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API Date: Fri, 25 Oct 2024 16:50:44 -0700
[Message part 1 (text/plain, inline)]
"J.P." <jp <at> neverwas.me> writes: > v2. Resolve merge conflict after 8903106b. Expose current match object > to legacy hook via dynamic variable. Oof, ENOPATCH.
[0000-v1-v2.diff (text/x-patch, attachment)]
[0001-5.7-Use-speaker-end-marker-in-ERC-insertion-hooks.patch (text/x-patch, attachment)]
[0002-5.7-Introduce-lower-level-erc-match-API.patch (text/x-patch, attachment)]
[0003-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch (text/x-patch, attachment)]
bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Fri, 01 Nov 2024 05:23:02 GMT) Full text and rfc822 format available.Message #14 received at 73798 <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: 73798 <at> debbugs.gnu.org Cc: emacs-erc <at> gnu.org Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API Date: Thu, 31 Oct 2024 22:22:41 -0700
"J.P." <jp <at> neverwas.me> writes: > For a detailed example showing how to use this API for more involved > matching that doesn't involve highlighting, see the ‘notifications’ > module, which lives in ‘erc-desktop-notifications.el’. Ignore the parts > that involve adapting the global setup (and teardown) business to a > buffer-local context. Since your module is declared ‘local’, as per the > modern convention, you won't be needing such code, so feel free to use > utility functions like ‘erc-match-add-local-type’ directly in your > module's definition. Actually, I'm not so sure it's wise to hold up the `notifications' module as a shining example of how to use this API. That's because it currently suffers from some potentially confusing quirks, some of which I'd like to address using internal functions that aren't really meant for third parties. > Here, the user leverages a handy subtype of ‘erc-match’, called > ‘erc-match-opt-keyword’, which actually descends directly from another, > intermediate ‘erc-match’ type: > > -- Struct: erc-match-traditional category face data part > > Use this type or one of its descendants (see below) if you want > ‘erc-text-matched-hook’ to run alongside (after) the ‘handler’ > slot's default highlighter, ‘erc-match-highlight’, on every match > for which the ‘category’ slot's value is non-‘nil’ (it becomes the > argument provided for the hook's MATCH-TYPE parameter). > > Much more important, however, is ‘part’. This slot determines what > portion of the message is being highlighted or otherwise operated > on. It can be any symbol, but the ones with predefined methods are > ‘nick’, ‘message’, ‘all’, ‘keyword’, ‘nick-or-keyword’, and > ‘nick-or-mention’. This should probably also mention what the `data' slot does because it features in both examples. > And, finally, here's a more elaborate, module-like example demoing > highlighting based on the ‘erc-match-traditional’ type: > > ;; -*- lexical-binding: t; -*- > > (require 'erc-match) > (require 'erc-button) > > (defvar my-keywords > `((foonet ("#chan" ,(rx bow (or "foo" "bar" "baz") eow))))) > > (defface my-keyword '((t (:underline (:color "tomato" :style wave)))) > "My face.") > > (defun my-get-keyword () > (and-let* ((chans (alist-get (erc-network) my-keywords)) > ((cdr (assoc (erc-target) chans)))))) > > (cl-defstruct (my-match (:include erc-match-opt-keyword > (part 'keyword) There's no need to override `part' here because `keyword' is already the default. > (data (my-get-keyword)) > (face 'my-keyword)) > (:constructor my-match))) > > (setopt erc-match-types (add-to-list 'erc-match-types #'my-match)) > > (cl-defmethod erc-match-highlight-by-part ((instance my-match) > (_ (eql keyword))) > "Highlight keywords by merging instead of clobbering." > (dolist (pat (my-match-data instance)) > (goto-char (my-match-body-beg instance)) > (while (re-search-forward pat nil t) > (erc-button-add-face (match-beginning 0) (match-end 0) > (my-match-face instance))))) > One thing that's possibly unclear about this example is why the `my-match' definition overrides `face' and `data' only to use their accessors in the body of the method. IOW, readers may wonder why it doesn't just use these init forms directly inline. Moreover, while `data' holds an opaque object that users are technically invited to repurpose as needed, doing so only really makes sense if they're subclassing a traditional options-based type. Indeed, for novel purposes, it's much saner for users to just define their own slot and use it for the usual reasons, e.g., to share processed data in various stages of refinement. This example would do well to incorporate such usage. In any case, it's become clear to me that a practical demonstration of this API might be necessary to fully grasp its facets and trade offs. To that end, I offer the following full-featured demo module: https://emacs-erc.gitlab.io/bugs/archive/jabbycat.html To try it out, you can start Emacs like $ HOME=$(mktemp -d) ./src/emacs and then eval (require 'package) (push '("erc-bugs" . "https://emacs-erc.gitlab.io/bugs/archive/") package-archives) (package-install 'erc-73798) (sit-for 1) ;; see [1] below (package-install 'jabbycat) (setopt jabbycat-server "xmpp.myvps.example.org:5222" jabbycat-recipient "me <at> xmpp.myvps.example.org" jabbycat-username "jabbycat <at> xmpp.myvps.example.org" jabbycat-password "changeme" erc-jabbycat-match-patterns '((Libera.Chat ("#test" . "."))) erc-modules (add-to-list 'erc-modules 'jabbycat)) (erc-tls) If not already obvious, you either need to apply the latest patch set from this bug or (as shown above) run the bug-specific version of ERC with those changes pre-applied. The source code is currently hosted at https://gitlab.com/emacs-erc/jabbycat (though a possibly less objectionable mirror may be provided eventually). Thanks. [1] For some reason, without the pause, `url-insert-file-contents' gives signal(file-error ("https://fake.example.org/foo.tar" "No Data")) package--with-response-buffer-1("https://fake.example.org/" #f(compiled-function () ...>) :file "foo.tar" :async nil :error-function #<subr ...> :noerror nil) package-install-from-archive(#s(package-desc :name foo ... :signed nil)) package-download-transaction((#s(package-desc ... :signed nil))) package-install(foo)
bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Fri, 01 Nov 2024 13:40:02 GMT) Full text and rfc822 format available.Message #17 received at 73798 <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: 73798 <at> debbugs.gnu.org Cc: emacs-erc <at> gnu.org Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API Date: Fri, 01 Nov 2024 06:39:07 -0700
[Message part 1 (text/plain, inline)]
v3. Replace `erc-match-types' with `erc-match-functions', an actual (abnormal) hook. Update docs as mentioned in previous post.
[0000-v2-v3.diff (text/x-patch, attachment)]
[0001-5.7-Use-speaker-end-marker-in-ERC-insertion-hooks.patch (text/x-patch, attachment)]
[0002-5.7-Introduce-lower-level-erc-match-API.patch (text/x-patch, attachment)]
[0003-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch (text/x-patch, attachment)]
Stefan Kangas <stefankangas <at> gmail.com>
to control <at> debbugs.gnu.org
.
(Sun, 03 Nov 2024 05:55:02 GMT) Full text and rfc822 format available.bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Wed, 13 Nov 2024 21:07:01 GMT) Full text and rfc822 format available.Message #22 received at 73798 <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: 73798 <at> debbugs.gnu.org Cc: emacs-erc <at> gnu.org Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API Date: Wed, 13 Nov 2024 13:06:49 -0800
"J.P." <jp <at> neverwas.me> writes: > v3. Replace `erc-match-types' with `erc-match-functions', an actual > (abnormal) hook. Update docs as mentioned in previous post. In the imagined use cases described up thread, it likely won't be uncommon for a module to manage multiple match types, with multiple members in `erc-match-functions' and `erc-text-matched-hook'. Related predicates, handlers, and methods may therefore need to communicate across matches, for example, to know whether a match has already occurred for the current message. So it may well make sense to add another slot to the base type whose value is shared among all objects during matching and filtering. An alist probably makes the most sense for this. If knowledge of prior matches turns out to be desirable and commonplace enough, we can keep and expose a record of all successful matches. (Doing this might further justify the current "split" design with its distinct predicate and handler phases.) One slight challenge here would be the necessity for some form of indirection to access such a record (because tacking on a list of prior match objects as a visible "has a" property of later objects would make printing them somewhat nasty). So, instead of another slot, we could maybe offer an object-retrieval utility keyed by constructor function.
bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Fri, 06 Dec 2024 06:55:02 GMT) Full text and rfc822 format available.Message #25 received at 73798 <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: 73798 <at> debbugs.gnu.org Cc: emacs-erc <at> gnu.org Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API Date: Thu, 05 Dec 2024 22:54:02 -0800
[Message part 1 (text/plain, inline)]
v4. Improve examples in manual. Add utility for match predicates and handlers to retrieve prior (successful) match objects. "J.P." <jp <at> neverwas.me> writes: > In the imagined use cases described up thread, it likely won't be > uncommon for a module to manage multiple match types, with multiple > members in `erc-match-functions' and `erc-text-matched-hook'. > > Related predicates, handlers, and methods may therefore need to > communicate across matches, for example, to know whether a match has > already occurred for the current message. So it may well make sense to > add another slot to the base type whose value is shared among all > objects during matching and filtering. An alist probably makes the most > sense for this. I didn't end up adding such a slot, although I still think one would likely be useful. > If knowledge of prior matches turns out to be desirable and commonplace > enough, we can keep and expose a record of all successful matches. > (Doing this might further justify the current "split" design with its > distinct predicate and handler phases.) One slight challenge here would > be the necessity for some form of indirection to access such a record > (because tacking on a list of prior match objects as a visible "has a" > property of later objects would make printing them somewhat nasty). So, > instead of another slot, we could maybe offer an object-retrieval > utility keyed by constructor function. This I did add, provisionally naming it `erc-match-get-match'. The revised documentation follows. See attached diff for changes. File: erc.info, Node: Match API, Next: Options, Prev: Module Loading, Up: Advanced Usage 5.6 Match API ============= This section describes the low-level ‘match’ API introduced in ERC 5.7. For basic, options-oriented usage, please see the doc strings for option ‘erc-pal-highlight-type’ and friends in the ‘erc-match’ group. Unfortunately, those options often prove insufficient for more granular filtering and highlighting needs, and advanced users eventually outgrow them. However, under the hood, those options all use the same foundational ‘erc-match’ API, which centers around a ‘cl-defstruct’ “type” of the same name: -- Struct: erc-match predicate spkr-beg spkr-end body-beg sender nick command handler This is a ‘cl-struct’ type that contains some handy facts about the message being processed. That message's formatted body occupies the narrowed buffer when ERC creates and provides access to each ‘erc-match’ instance. To use this interface, you add a “constructor”-like function to the hook ‘erc-match-functions’: -- User Option: erc-match-functions An abnormal hook for which each member accepts the parameters named above as an ‘&rest’-style plist and returns a new ‘erc-match’ instance. A function can also be a traditional ‘cl-defstruct’-provided constructor belonging to a “subtype” you've defined. The only slot you definitely need to specify is ‘predicate’. Both it and ‘handler’ are functions that take a single argument: the instance itself. As its name implies, ‘predicate’ must return non-‘nil’ if ‘handler’, whose return value ERC ignores, should run. A few slots, like ‘spkr-beg’, ‘spkr-end’, and ‘nick’, may surprise you. The first two are ‘nil’ for non-chat messages, like those displayed for ‘JOIN’ events. The ‘nick’ slot can likewise be ‘nil’ if the sender of the message is a domain-style host name, such as ‘irc.example.org’, which it often is for informational messages, like ‘*** #chan was created on 2023-12-26 00:36:42’. To locate the start of the just-inserted message, use ‘body-beg’, a marker indicating the beginning of the message proper. Don't forget: all inserted messages include a trailing newline. If you want to extract just the message body's text, use the function ‘erc-match-get-message-body’: -- Function: erc-match-get-message-body match Takes an ‘erc-match’ instance and returns a string containing the message body, sans trailing newline and any leading speaker or decorative component, such as ‘erc-notice-prefix’. Although module authors may want to subclass this struct, everyday users can just instantiate it directly (it's “concrete”). This is especially handy for one-off tasks or simple customizations in your ‘init.el’. To do this, define a function that invokes its constructor: (require 'erc-match) (defvar my-mentions 0) (defun my-match (&rest plist) (apply #'erc-match :predicate (lambda (_) (search-forward "my-project" nil t)) :handler (lambda (_) (cl-incf my-mentions)) plist)) (add-hook 'erc-match-functions #'my-match) (setopt erc-prompt (lambda () (format "%d!" my-mentions))) Here, the user could just as well shove the incrementer into the ‘predicate’ body, since ‘handler’ is set to ‘ignore’ by default (however, some frown at the notion of a predicate exhibiting side effects). The user could also choose to concentrate only on chat content by filtering out non-‘PRIVMSG’ messages via the slot ‘command’. In cases where you need a handler to only run when some other match type appearing earlier in ‘erc-match-functions’ has _not_ yielded a match, use: -- Function: erc-match-get-match constructor When called from a ‘handler’ or a ‘predicate’ body, this utility returns instances of prior ‘erc-match-functions’ that have already successfully matched the current message. Use this for deduplication and to share data between match instances. For a detailed example of matching for non-highlighting purposes, see the ‘jabbycat’ demo module, available on ERC's dev-oriented package archive: <https://emacs-erc.gitlab.io/bugs/archive/jabbycat.html>. If you're in a hurry, check out ‘erc-desktop-notifications.el’, which ships with ERC, but please ignore the parts that involve adapting the global setup (and teardown) business to a buffer-local context. Since your module is declared ‘local’, as per the modern convention, you won't be needing such code, so feel free to do things like add local members to ‘erc-match-functions’ in your module's definition. 5.6.1 Highlighting ------------------ End users and third-party modules likely want to manage and apply faces themselves. If that's you, feel free to skip to the more extensive examples further below. However, for the sake of completeness, it's worth mentioning that in a pinch, you can likely piggyback atop the highlighting functionality already provided by ‘match’ to support its many high-level options. (require 'erc-match) (defvar my-keywords `((foonet ("#chan" ,(rx bow (or "foo" "bar" "baz") eow))))) (defface my-face '((t (:inherit font-lock-constant-face :weight bold))) "My face.") (defun my-match (&rest plist) (apply #'erc-match-opt-keyword :data (and-let* ((chans (alist-get (erc-network) my-keywords)) ((cdr (assoc (erc-target) chans))))) :face 'my-face plist)) (add-hook 'erc-match-functions #'my-match) Here, the user leverages a handy subtype of ‘erc-match’, called ‘erc-match-opt-keyword’, which actually descends directly from another, intermediate ‘erc-match’ type: -- Struct: erc-match-traditional category face data part Use this type or one of its descendants (see below) if you want ‘erc-text-matched-hook’ to run alongside (after) the ‘handler’ slot's default highlighter, ‘erc-match-highlight’, on every match for which the ‘category’ slot's value is non-‘nil’ (it becomes the argument provided for the hook's MATCH-TYPE parameter). Much more important, however, is ‘part’. This slot determines what portion of the message is being highlighted or otherwise operated on. It can be any symbol, but the ones with predefined methods are ‘nick’, ‘message’, ‘all’, ‘keyword’, ‘nick-or-keyword’, and ‘nick-or-mention’. The complement to the ‘part’ slot is ‘data’, which holds the value of the module's option corresponding to the specific type. For example, ERC initializes the ‘data’ slot for the ‘erc-match-opt-pal’ type with the value of ‘erc-pals’. The default handler, ‘erc-match-highlight’, does its work by deferring to a purpose-built “method” meant to handle ‘part’-based highlighting: -- Method on erc-match-traditional: erc-match-highlight-by-part instance part You can override this method by “specializing” on any subclassed ‘erc-match-traditional’ type and/or non-reserved PART, such as one known only to your ‘init.el’ or (informally) associated with your package by its library “namespace”. You likely won't be needing these, but just for the record, other options-based types similar to ‘erc-match-opt-keyword’ include ‘erc-match-opt-current-nick’, ‘erc-match-opt-fool’, ‘erc-match-opt-pal’, and ‘erc-match-opt-dangerous-host’. (If you're familiar with this module's user options, you'll notice some parallels here.) 5.6.1.1 Complete Highlighting Examples ...................................... As mentioned, most users needn't bother with the piggybacking approach detailed above, which can oftentimes be more complicated than starting afresh. Here's a more elaborate, module-like example demoing some highlighting with a bespoke ‘erc-match’-derived type: ;;; erc-org-markup.el --- Org Markup for ERC -*- lexical-binding: t; -*- (require 'erc-match) (require 'org) (defgroup erc-org-markup nil "Highlight messages written in Org markup." :group 'erc) (defcustom erc-org-markup-targets '("#org") "List of buffers in which to highlight messages." :type '(repeat string)) (define-erc-module org-markup nil "Local module that treats messages as having Org markup." ((erc-org-markup-ensure-buffer) (if (member (erc-target) erc-org-markup-targets) (progn (add-hook 'erc-match-functions #'erc-org-markup 0 t) (add-to-invisibility-spec '(org-link))) (erc-org-markup-mode -1))) ((remove-hook 'erc-match-functions #'erc-org-markup t) (remove-from-invisibility-spec '(org-link))) 'local) (cl-defstruct (erc-org-markup (:include erc-match (predicate #'erc-org-markup--should-p) (handler #'erc-org-markup--fontify)) (:constructor erc-org-markup)) "Match type to highlight messages written in Org markup.") (defun erc-org-markup--should-p (match) "Return non-nil if MATCH describes an Org-markup worthy message." (and erc-org-markup-mode (erc-match-nick match))) (defun erc-org-markup-ensure-buffer () "Return existing global work buffer or create it anew." (or (get-buffer "*erc-org-markup*") (with-current-buffer (get-buffer-create "*erc-org-markup*") (org-mode) (make-local-variable 'org-link-parameters) (setf (plist-get (cdr (assoc "https" org-link-parameters)) :activate-func) #'erc-org-markup-activate-link) (setq-local org-hide-emphasis-markers t) (current-buffer)))) (defun erc-org-markup--fontify (match) "Overwrite text properties in MATCH'd message with Org's." (save-restriction (narrow-to-region (erc-match-body-beg match) (1- (point-max))) (let ((buffer (current-buffer))) (with-current-buffer (erc-org-markup-ensure-buffer) (save-window-excursion (buffer-swap-text buffer) (font-lock-ensure) (buffer-swap-text buffer)))))) (defun erc-org-markup-activate-link (beg end path _) "Ensure Org https link between BEG and END has `erc-button' props." (erc-button-add-button beg end #'browse-url-button-open-url nil (list (concat "https:" path)) "")) (provide 'erc-org-markup) ;;; erc-org-markup.el ends here Finally, here's a slightly more complete demo module: a superficial rewrite of ‘erc-colorize.el’ by Sylvain Rousseau <https://github.com/thisirs/erc-colorize.git>. ;;; erc-colorize.el --- Per-user message faces -*- lexical-binding: t; -*- (require 'ring) (require 'erc-match) (require 'erc-button) ; for `erc-button-add-face' (defgroup erc-colorize nil "Highlight messages with per-user faces from a limited pool." :group 'erc) (defface erc-colorize-1 '((t :inherit font-lock-keyword-face)) "Auto-assigned face for distinguishing between messages.") (defface erc-colorize-2 '((t :inherit font-lock-type-face)) "Auto-assigned face for distinguishing between messages.") (defface erc-colorize-3 '((t :inherit font-lock-string-face)) "Auto-assigned face for distinguishing between messages.") (defface erc-colorize-4 '((t :inherit font-lock-constant-face)) "Auto-assigned face for distinguishing between messages.") (defface erc-colorize-5 '((t :inherit font-lock-preprocessor-face)) "Auto-assigned face for distinguishing between messages.") (defface erc-colorize-6 '((t :inherit font-lock-variable-name-face)) "Auto-assigned face for distinguishing between messages.") (defface erc-colorize-7 '((t :inherit font-lock-warning-face)) "Auto-assigned face for distinguishing between messages.") (defvar erc-colorize-faces '(erc-colorize-1 erc-colorize-2 erc-colorize-3 erc-colorize-4 erc-colorize-5 erc-colorize-6 erc-colorize-7) "List of faces to apply to chat messages.") (defvar-local erc-colorize-ring nil "Ring of cons cells of the form (NICK . FACE).") (define-erc-module colorize nil "Highlight messages from a speaker with the same face in target buffers." ((when (erc-target) (add-hook 'erc-match-functions 'erc-colorize 0 t) (setq erc-colorize-ring (make-ring (length erc-colorize-faces))))) ((remove-hook 'erc-match-functions 'erc-colorize t)) 'local) (defun erc-colorize-color (ring nick) "Return a face to use for string NICK. Prefer an existing entry in RING. If there isn't one, pick the first unused face in `erc-colorize-faces'. Otherwise, pick the least used face." (cond ((and-let* ((i (catch 'found (dotimes (i (ring-length ring)) (when (equal (car (ring-ref ring i)) nick) (throw 'found i)))))) (ring-insert ring (ring-remove ring i)) (cdr (ring-ref ring 0)))) ((let ((used (mapcar #'cdr (ring-elements ring)))) (and-let* ((face (catch 'found (dolist (face erc-colorize-faces) (unless (member face used) (throw 'found face)))))) (prog1 face (ring-insert ring (cons nick face)))))) ((let ((older (ring-remove ring))) (ring-insert ring (cons nick (cdr older))) (cdr older))))) (cl-defstruct (erc-colorize ( :include erc-match (predicate #'erc-colorize-nick) (handler #'erc-colorize-message)) (:constructor erc-colorize)) "An `erc-match' type for the `erc-colorize' module.") (defun erc-colorize-message (match) "Highlight MATCH's full message with a face from `erc-colorize-faces'." (erc-button-add-face (point-min) (1- (point-max)) (erc-colorize-color erc-colorize-ring (erc-colorize-nick match)))) (provide 'erc-colorize) ;;; erc-colorize.el ends here
[0000-v3-v4.diff (text/x-patch, attachment)]
[0001-5.7-Use-speaker-end-marker-in-ERC-insertion-hooks.patch (text/x-patch, attachment)]
[0002-5.7-Introduce-lower-level-erc-match-API.patch (text/x-patch, attachment)]
[0003-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch (text/x-patch, attachment)]
bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Wed, 19 Feb 2025 04:02:02 GMT) Full text and rfc822 format available.Message #28 received at 73798 <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: 73798 <at> debbugs.gnu.org Cc: emacs-erc <at> gnu.org Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API Date: Tue, 18 Feb 2025 20:00:24 -0800
[Message part 1 (text/plain, inline)]
v5. Populate slots `spkr-beg' and `spkr-end' of match object regardless of sender.
[0000-v4-v5.diff (text/x-patch, attachment)]
[0001-5.7-Use-speaker-end-marker-in-ERC-insertion-hooks.patch (text/x-patch, attachment)]
[0002-5.7-Introduce-lower-level-erc-match-API.patch (text/x-patch, attachment)]
[0003-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch (text/x-patch, attachment)]
bug-gnu-emacs <at> gnu.org
:bug#73798
; Package emacs
.
(Tue, 04 Mar 2025 13:33:02 GMT) Full text and rfc822 format available.Message #31 received at 73798 <at> debbugs.gnu.org (full text, mbox):
From: "J.P." <jp <at> neverwas.me> To: 73798 <at> debbugs.gnu.org Cc: emacs-erc <at> gnu.org Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API Date: Tue, 04 Mar 2025 05:32:16 -0800
[Message part 1 (text/plain, inline)]
v6. Add hook option to desktop-notifications for suppressing messages based on contents, categories, etc. User k4r4b3y on Libera.Chat mentioned wanting to skip notifications from members of `erc-fools'. This makes that possible and also demos the use of the `erc-match-get-match' API function.
[0000-v5-v6.diff (text/x-patch, attachment)]
[0001-5.7-Use-speaker-end-marker-in-ERC-insertion-hooks.patch (text/x-patch, attachment)]
[0002-5.7-Introduce-lower-level-erc-match-API.patch (text/x-patch, attachment)]
[0003-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch (text/x-patch, attachment)]
GNU bug tracking system
Copyright (C) 1999 Darren O. Benham,
1997,2003 nCipher Corporation Ltd,
1994-97 Ian Jackson.