From debbugs-submit-bounces@debbugs.gnu.org Sun Feb 02 18:38:55 2025 Received: (at submit) by debbugs.gnu.org; 2 Feb 2025 23:38:55 +0000 Received: from localhost ([127.0.0.1]:36731 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1tejYF-0008Ps-5j for submit@debbugs.gnu.org; Sun, 02 Feb 2025 18:38:55 -0500 Received: from lists.gnu.org ([2001:470:142::17]:40770) by debbugs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.84_2) (envelope-from ) id 1tejY7-0008PJ-RO for submit@debbugs.gnu.org; Sun, 02 Feb 2025 18:38:52 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tejY1-0001ve-Ra for bug-gnu-emacs@gnu.org; Sun, 02 Feb 2025 18:38:42 -0500 Received: from mail-108-mta77.mxroute.com ([136.175.108.77]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1tejXu-0004ZG-6D for bug-gnu-emacs@gnu.org; Sun, 02 Feb 2025 18:38:40 -0500 Received: from filter006.mxroute.com ([136.175.111.3] filter006.mxroute.com) (Authenticated sender: mN4UYu2MZsgR) by mail-108-mta77.mxroute.com (ZoneMTA) with ESMTPSA id 194c902f131000310e.001 for (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Sun, 02 Feb 2025 23:33:20 +0000 X-Zone-Loop: 2de881aaaea26e7ccc63e6b7c5aec220f208de34663e X-Originating-IP: [136.175.111.3] DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=neverwas.me ; s=x; h=Content-Type:MIME-Version:Message-ID:Date:Subject:To:From:Sender: Reply-To:Cc:Content-Transfer-Encoding:Content-ID:Content-Description: Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID: In-Reply-To:References:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=8qdxJHKSHkUfvr7ArcT0IQ4Dty6j1f6lzeRRE+GN8l8=; b=nl/llbypYIEU/hCX2V12Fv3RkR VfxJKOF13JnNky61ArWdDuD2EhiO4/FcIDt5/ERLCS+ljVjVmrVqANyP8HC+Xbln/T6weS7+4TDV1 sytZQCnrKfBzxMcEiKfxxEoc+jttIpe9CWx2qtyrtHVcjwzbF09xQEINjtiytsJ9NLcXS44ATiZGz /fAYYG+o2ptT1swLJK8dOOybjAnLqZKQucxQY2Cp0taF5fUL9bN8p0ExrJSmHlaU5KWrFIQFI3l5b UtBs2UNMlF4DDUwASfyjk7DAJ3xoOwHEiY8Jx5F739F2bKOSWoW1GLHsIdhVfcj3wKNCNaWsaHNB0 iTqt8xDw==; From: "J.P." To: bug-gnu-emacs@gnu.org Subject: 31.0.50; ERC 5.7: A 'settings' module for locally scoped configuration X-Debbugs-CC: emacs-erc@gnu.org Date: Sun, 02 Feb 2025 15:33:17 -0800 Message-ID: <87y0yn6hxe.fsf@neverwas.me> User-Agent: Gnus/5.13 (Gnus v5.13) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Authenticated-Id: masked@neverwas.me Received-SPF: pass client-ip=136.175.108.77; envelope-from=jp@neverwas.me; helo=mail-108-mta77.mxroute.com X-Spam_score_int: -19 X-Spam_score: -2.0 X-Spam_bar: -- X-Spam_report: (-2.0 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001, URIBL_SBL_A=0.1 autolearn=ham autolearn_force=no X-Spam_action: no action X-Spam-Score: 1.7 (+) X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org", has NOT identified this incoming email as spam. The original message has been attached to this so you can view it or label similar future email. If you have any questions, see the administrator of that system for details. Content preview: Tags: patch Severity: wishlist The name `erc-settings' likely no longer rings a bell, even for longtime ERC users. It represents an idea flirted with decades ago but sadly never brought to fruition. Still, you need only look near t [...] Content analysis details: (1.7 points, 10.0 required) pts rule name description ---- ---------------------- -------------------------------------------------- 0.1 URIBL_SBL_A Contains URL's A record listed in the Spamhaus SBL blocklist [URIs: libera.chat] 0.6 URIBL_SBL Contains an URL's NS IP listed in the Spamhaus SBL blocklist [URIs: libera.chat] -0.0 SPF_HELO_PASS SPF: HELO matches SPF record 0.9 SPF_FAIL SPF: sender does not match SPF record (fail) [SPF failed: Please see http://www.openspf.org/Why?s=mfrom; id=jp%40neverwas.me; ip=2001%3A470%3A142%3A%3A17; r=debbugs.gnu.org] -0.0 RCVD_IN_DNSWL_NONE RBL: Sender listed at https://www.dnswl.org/, no trust [2001:470:142:0:0:0:0:17 listed in] [list.dnswl.org] X-Debbugs-Envelope-To: submit X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" --=-=-= Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable Tags: patch Severity: wishlist The name `erc-settings' likely no longer rings a bell, even for longtime ERC users. It represents an idea flirted with decades ago but sadly never brought to fruition. Still, you need only look near the bottom of lisp/erc/erc-networks.el for traces of its promise. This proposal aims to revive that abandoned pursuit, which remains as relevant as ever. Indeed, among the more common gripes directed at ERC in recent years has been the lack of granular control over its many options [0]. This is often voiced as a desire not only for per-session precision but for per-buffer control as well. This bug presents an attempt to meet these requirements in a somewhat crude but ultimately functional way. What follows is the latest draft of its documentation: File: erc.info, Node: Settings, Up: Modules 4.1 Settings =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D This global module lets you apply ERC options on a per-buffer basis, as local variable bindings (*note (elisp)Buffer-Local Variables::). It consists of a single =E2=80=9Cmeta option=E2=80=9D: -- User Option: erc-settings An alist where each key is a =E2=80=9Cmatch condition=E2=80=9D and e= ach value a list of bindings that loosely resembles a =E2=80=98let=E2=80=99-styl= e VARLIST. The first element of each binding is the variable of a user option and the second an arbitrary sexp assigned to it locally in all matching buffers. You can tell ERC to evaluate a given sexp as a Lisp form before assigning it by including a trailing third element: the =E2=80=9Cmodifier flag=E2=80=9D =E2=80=98:eval=E2=80=99. A match condition specifies when and whether ERC should apply each group of bindings. These resemble =E2=80=98buffer-match-p=E2=80=99 = conditions, with two major differences. The first is that predicate conditions don't take any arguments. Instead, ERC runs them with the candidate buffer current. The second difference is that the assortment of associative cons-cell conditions is completely new. Gone are =E2=80=98major-mode=E2=80=99, =E2=80=98category=E2=80=99, e= tc. In their place, ERC offers the following: =E2=80=A2 =E2=80=98(network . =E2=80=98=E2=80=99)=E2=80=99 =E2=80=A2 =E2=80=98(id . =E2=80=98=E2=80=99)=E2=80=99 =E2=80=A2 =E2=80=98(name . =E2=80=98=E2=80=99)=E2=80=99 =E2=80=A2 =E2=80=98(target . =E2=80=98=E2=80=99)= =E2=80=99 The associated =E2=80=98cdr=E2=80=99 values for the =E2=80=98network= =E2=80=99 and =E2=80=98id=E2=80=99 conditions are symbols identifying the current IRC network and the =E2=80=9Cnet= work context=E2=80=9D, respectively. When first connecting, a =E2=80=98n= etwork=E2=80=99 condition is always =E2=80=98nil=E2=80=99, but an =E2=80=98id=E2=80= =99 condition can be non-=E2=80=98nil=E2=80=99 if you supply a matching ID argument to an entry-point command, like =E2=80=98erc-tls=E2=80=99 (*note Network Identifier::). Additionally, ERC also recognizes =E2=80=98buffer-match-p=E2=80=99's= boolean operations and constants: =E2=80=A2 =E2=80=98(and =E2=80=98=E2=80=99)=E2=80=99 =E2=80=A2 =E2=80=98(or =E2=80=98=E2=80=99)=E2=80=99 =E2=80=A2 =E2=80=98(not =E2=80=98=E2=80=99)=E2=80=99 =E2=80=A2 =E2=80=98t=E2=80=99 =E2=80=A2 =E2=80=98nil=E2=80=99 Also valid are regular expressions and nullary predicates: =E2=80=A2 =E2=80=98""=E2=80=99 =E2=80=A2 =E2=80=98=E2=80=99 Here's a contrived =E2=80=98erc-settings=E2=80=99 value containing six en= tries, the first of which has two assignments: (((and (network . Libera.Chat) ; buffers #emacs, "\\`#emacs") ; #emacs-foo, etc., (my-own-variable 42) ; on Libera.Chat only (erc-foo "")) ((or (target . "#chan") ; buffers #chan, bob, (target . "bob")) ; chan@Libera.Chat, (erc-foo (erc-current-nick) :eval)) ; bob@OFTC, etc. (erc-open-server-buffer-p (erc-bar 1)) ; connected server buffers (erc-query-buffer-p (erc-bar 2)) ; query buffers only (erc-channel-buffer-p (erc-bar 3)) ; channel buffers only (erc-target (erc-bar 4)) ; query or channel buffers (t (erc-bar 5))) ; any `erc-mode' buffer Unfortunately, predicates too dissimilar from those shown above may not work in all matching contexts due to the interaction between this module and ERC's rather convoluted major-mode setup. While the only way to know for sure is to try them, selecting them becomes much easier if you can gain a sense for when exactly =E2=80=98settings=E2=80=99 attempts to = create bindings: =E2=80=A2 In a server buffer, binding happens 1. before ERC enables any modules or sets any local variables 2. after establishing a logical IRC connection and learning what network it's connected to =E2=80=A2 In a target buffer, binding happens 1. before enabling any modules, but only if you're overriding =E2=80=98erc-modules=E2=80=99, and only then, for that option a= lone 2. after ERC has set most of its default local variables and run global-module setup but before running major-mode hooks or local-module setup The main downside of this staggered approach is that =E2=80=98settings= =E2=80=99 skips any bindings for variables that already have a local binding rather than do the intuitive thing and simply clobber them. The reason for this is to retain a consistent state between a global module's minor mode setup and any local-module setup or major-mode hooks. They must all witness the same value for the same variable. Revisiting the quasi-example above with this precedence quirk in mind, notice that the made-up variable =E2=80=98erc-bar=E2=80=99 will be = =E2=80=982=E2=80=99 in query buffers, =E2=80=983=E2=80=99 in channel buffers, and =E2=80=985=E2=80=99 = in server buffers. It's never =E2=80=981=E2=80=99 because the first binding opportunity occurs before c= onnecting, when =E2=80=98erc-open-server-buffer-p=E2=80=99 returns =E2=80=98nil=E2=80=99.= It's likewise never =E2=80=984=E2=80=99 because that binding appears lower in the list than those for =E2=80=982= =E2=80=99 and =E2=80=983=E2=80=99. For more on this behavior, see the very last exampl= e below. When it comes to binding the option =E2=80=98erc-modules=E2=80=99, =E2= =80=98settings=E2=80=99 must make special accommodations. Getting it right may take some trial and error, and it's only guaranteed to work for local modules because most global ones share state among all sessions. However, it's fine to include global modules in an =E2=80=98erc-modules=E2=80=99 binding as lon= g as they also appear in the =E2=80=98default-value=E2=80=99. To set =E2=80=98erc-modul= es=E2=80=99 for a specific server buffer, specify an entry like ("[.]libera[.]chat:" (erc-modules (seq-union '(sasl keep-place-indicator) erc-modules) :eval) (erc-sasl-user :nick)) Here, the match condition is a regexp matching a dialed TCP address because server buffers typically have these as temporary names while connecting. As mentioned, ERC later renames them to reflect a discovered network, after which =E2=80=98settings=E2=80=99 binds any matc= hing unbound options. A session's target buffers normally share the =E2=80=98erc-modules=E2= =80=99 value bound locally in their server buffer. To specify a separate, overriding set of modules for certain target buffers, use a condition that only applies to them: ((and (network . Libera.Chat) erc-channel-buffer-p) (erc-modules (seq-union '(fill-wrap) erc-modules) :eval)) This is often unnecessary because local modules that only operate in one kind of buffer tend to disable themselves elsewhere. And some, like =E2=80=98keep-place-indicator=E2=80=99, offer an option to restrict activ= ation based on buffer type. 4.1.1 Settings Examples ----------------------- A typical setup might include configuration for two networks, one that uses SASL to authenticate and another that uses a server password. (setopt erc-modules `(settings ,@erc-modules) erc-settings ;; Pre-connect and server buffer config for =E2=80=98Libera.= Chat=E2=80=99. '(("libera\\.chat:" (erc-prompt "Libera>") (erc-autojoin-channels-alist ((Libera.Chat "##jrh"))) (erc-header-line-format "%n on %S (%m, %l)") (erc-sasl-user :nick) (erc-modules `(sasl ,@erc-modules) :eval)) ;; Channel buffer config for =E2=80=98Libera.Chat=E2=80=99 ((and (network . Libera.Chat) erc-channel-buffer-p) (erc-modules `(keep-place-indicator ,@erc-modules) :eval) (erc-prompt erc-prompt-format) (erc-prompt-format "%c%b>") (erc-header-line-format "%t: %o")) ;; Query buffer configuration for any network. (erc-query-buffer-p (erc-prompt erc-prompt-format) (erc-prompt-format "%S>") (erc-header-line-format nil)) ;; Pre-connect and server-buffer config for =E2=80=98OFTC= =E2=80=99. ("oftc.net:" (erc-prompt "OFTC!") (erc-modules `(nicks ,@erc-modules) :eval)))) Note that the pre-connection config described in the last entry can't use the =E2=80=9Cglobal module=E2=80=9D =E2=80=98services=E2=80=99 becaus= e it's incompatible with the =E2=80=9Clocal module=E2=80=9D =E2=80=98sasl=E2=80=99. In this scenario,= instead of a server password, the =E2=80=98OFTC=E2=80=99 connection could alternatively use =E2=80=9CCe= rtFP=E2=80=9D via auth-source (see next example). 4.1.1.1 ID-based ................ New users coming from traditional IRC clients often express a desire to store connection details alongside other per-network or per-connection configuration. Although ERC was designed to keep these concerns separate, the =E2=80=98settings=E2=80=99 module can emulate that experien= ce to some degree. (setopt erc-modules `(settings ,@erc-modules) erc-settings ;; Connection details common to both =E2=80=98Libera.Chat=E2= =80=99 IDs. '(((and (or (id . lib-jrh) (id . lib-jrbot)) (not erc-server-process-alive)) (erc-server "irc.libera.chat") (erc-port 6697) (erc-modules `(sasl ,@erc-modules) :eval)) ;; Connection details for account =E2=80=98jrh=E2=80=99. ((and (id . lib-jrh) (not erc-server-process-alive)) (erc-nick "jrh") (erc-user-full-name "J. Random Hacker") (erc-email-userid "jrh") (erc-autojoin-channels-alist ((lib-jrh "##jrh")))) ;; Connection details for account =E2=80=98jrbot=E2=80=99. ((and (id . lib-jrbot) (not erc-server-process-alive)) (erc-nick "jrbot") (erc-email-userid "jrbot") (erc-user-full-name "J.R. Bot (jrh's bot)") (erc-sasl-auth-source-function erc-auth-source-function) (erc-autojoin-channels-alist ((lib-jrbot "##jrh" "##jrbot= ")))) ;; Non-connection opts for both =E2=80=98Libera.Chat=E2=80= =99 sessions. ((and (network . Libera.Chat) erc-open-server-buffer-p) (erc-prompt "libera>") (erc-header-line-format "%n on %S (%m, %l)")) ((and (network . Libera.Chat) erc-channel-buffer-p) (erc-modules `(keep-place-indicator ,@erc-modules) :eval) (erc-prompt erc-prompt-format) (erc-prompt-format "%c%b>") (erc-header-line-format "%t: %o")) ;; Server details for =E2=80=98OFTC=E2=80=99. ((and (id . oftc) (target . nil)) (erc-server "irc.oftc.net") (erc-port 6697) (erc-nick "jrh") (erc-email-userid "jrh") (erc-user-full-name "J. Random Hacker") (erc-client-certificate t) (erc-modules `(services-regain ,@erc-modules) :eval) (erc-prompt "jrh@OFTC!")))) (defun my-erc-up (id) "Connect to preconfigured session identified by ID." (interactive "SNet id: ") (erc-tls :server nil :port nil :nick nil :user nil :password nil :full-name nil :id network-id)) With a setup like the above, you can connect explicitly to a predefined session by supplying an identifying symbol, like =E2=80=98lib-jrh=E2=80= =99, along with the ID keyword parameter to the entry-point command =E2=80=98erc-tls=E2= =80=99. The main gotcha when using these identifiers is that a regexp targeting a dialed TCP address, like =E2=80=98"\\`irc[.]libera[.]chat:6697\\'"=E2=80=99, won= 't work for conditions like the first one above because server buffers receive permanent names immediately upon creation, long before connecting. Instead, you'll want to specify a chosen identifier via the =E2=80=98(id . )=E2=80=99 pattern and likely qualify it with another subconditio= n, like =E2=80=98(not erc-server-process-alive)=E2=80=99, to ensure that the =E2= =80=98erc-modules=E2=80=99 binding only matches the server buffer and not any target buffers. Equivalent qualifying subconditions would be =E2=80=98(not erc-target)=E2= =80=99 and =E2=80=98(target . nil)=E2=80=99. Regarding authentication, notice that the =E2=80=98lib-jrbot=E2=80=99 = connection retrieves its SASL credentials, which might be a client certificate, via auth-source, while session =E2=80=98lib-jrh=E2=80=99 prompts you interact= ively for a password. (*Note SASL in ERC: SASL.) The =E2=80=98oftc=E2=80=99 connecti= on also uses auth-source to supply its credentials, this time for non-SASL client-cert (=E2=80=9CCertFP=E2=80=9D) authentication. Lastly, in the sc= enario above, a user would need to run =E2=80=98M-x my-erc-up =E2=80=99 instead of = =E2=80=98M-x erc =E2=80=99 when connecting to IRC. For convenience, ERC provides a similar command: -- Function: erc-settings-connect-by-id network-id Prompt the user and provide completion for some =E2=80=98(id . )=E2=80=99 appearing in their =E2=80=98erc-settings=E2=80=99, and specify it as= the argument to the ID keyword parameter when invoking =E2=80=98erc-tls=E2=80=99. With both traditional and ID-based setups, it's often more convenient to define less exclusive, more "overlapping" match conditions: (use-package erc-settings :custom (erc-settings '(((not erc-server-process-alive) ; common to all servers (erc-nick "jrh") (erc-port 6697) (erc-email-userid "jrh") (erc-user-full-name "J. Random Hacker")) (erc-channel-buffer-p ; all channels (erc-modules `(keep-place-indicator ,@erc-modules) :eval)) ((id . id-libera) ; Libera.Chat only, including targets (erc-server "irc.libera.chat") (erc-sasl-password "changeme") (erc-modules `(sasl ,@erc-modules) :eval) (erc-prompt erc-prompt-format) (erc-autojoin-channels-alist ((id-foonet "#erc")))) ((id . id-oftc) ; OFTC only, including targets (erc-server "irc.oftc.net") (erc-modules `(fill-wrap ,@erc-modules) :eval) (erc-autojoin-channels-alist ((id-barnet "##jrh"))))))) If you go this route, know that =E2=80=98settings=E2=80=99 will often = create local bindings in buffers where they're not applicable. In the above example, buffer =E2=80=98#erc=E2=80=99 on =E2=80=98Libera.Chat=E2=80=99 will have = =E2=80=98erc-server=E2=80=99 and =E2=80=98erc-sasl-password=E2=80=99 bound locally for no reason. In most= cases, this is perfectly harmless. A final thing to notice about the example above is that the =E2=80=98erc-modules=E2=80=99 binding in the second group, the one with m= atch condition =E2=80=98erc-channel-buffer-p=E2=80=99, effectively =E2=80=9Cshadow=E2=80= =9Ds that same variable in all remaining settings affecting channel buffers. If it instead appeared at the end of the list, ERC would skip its =E2=80=98erc-modules=E2=80=99 bin= ding because the variable would already be locally bound. But, as is, both =E2=80=98#= #jrh=E2=80=99 on =E2=80=98OFTC=E2=80=99 and =E2=80=98#erc=E2=80=99 on =E2=80=98Libera.C= hat=E2=80=99 activate =E2=80=98keep-place-indicator=E2=80=99 but not =E2=80=98fll-wrap=E2=80=99. Server buffer =E2=80=98id-oftc=E2=80= =99 has only =E2=80=98fll-wrap=E2=80=99, and server buffer =E2=80=98id-libera=E2=80=99 has neither. And, if you were = to receive a query from =E2=80=98alice=E2=80=99 on =E2=80=98OFTC=E2=80=99, buffer =E2= =80=98alice=E2=80=99 would only have =E2=80=98fill-wrap=E2=80=99 but not =E2=80=98keep-place-indicator=E2=80=99 because it's a non-channel= on =E2=80=98OFTC=E2=80=99. Mock FAQ =3D=3D=3D=3D=3D=3D=3D=3D These are just some imagined areas of concern. Any actual feedback is of course most welcome. Q: Why the flat alist structure for the `erc-settings' option. Wouldn't a shallow tree resembling dir-locals be better? Something with a top level that matches against a session/connection and a secondary level for individual constituent buffers? A: This sounds great in theory because it's closer to how we organize these assignments mentally. And indeed, my earlier drafts used this exact shape. However, anecdotal dog-fooding has convinced me that this leads quickly to unwieldy configuration patterns that are overly verbose and difficult to predict. I'm open to being proven wrong, of course. Q: Why no deep integration with Customization buffers and `use-package'? A: Based on a cursory investigation, I believe any integration with `use-package' would require a change to that library itself. As for Customize, I think we could, in theory, couch all of ERC's existing user options in a widget that supports declaring a `settings' scope in situ. This would then somehow propagate to modify the global value of `erc-settings' the variable, likely on module init, perhaps with some sort of caching. As for the Customization buffer for `erc-settings' the option: I agree that it'd be ideal to find a way to dynamically generate typed fields for arbitrary binding values instead of the current and rather ugly catch-all `sexp' type. I'm currently looking into this. Q: Buffer-local options are messy in that they complicate the mental model of how various inputs influence the state of the program. Moreover, it would seem to defy the main purpose of local modules, which is to maximize predictability and flexibility. So why not instead "snapshot" the state of all options of interest during local module setup? A: This "snapshotting" approach seems preferable in theory but would also require universal buy-in from all local modules. Basically, local modules would henceforth be required to use some flavor of mandated indirection for accessing and updating persistent state. I fear such a hard pivot to a "managed framework" would be a tough sell to would-be contributors and package authors, many of whom are already wary of being force-fed excess magic. Q: Why not have bindings clobber earlier ones in a "cascading" manner instead of the current "early bird gets the worm" basis? A: The clobbering approach is indeed more intuitive and it would make sense were it not for our need to perform bindings in two stages per buffer type (both server and target). This has its downsides but also affords users multiple opportunities to match against an evolving landscape as more useful input arrives. As implied in the manual, partially at fault for this situation is a bootstrapping conundrum wherein the module must both influence and be influenced by its fellow modules (hence the annoying "meta option" designation). Q. Isn't enabling ERC's major mode before updating modules a breaking change because modules lose access to the caller's buffer? It's a breaking change, but only superficially. As things stand, a module's setup code (its "enable body") can't meaningfully leverage this access for state transfer because it lacks a reasonable means of discovering the reason for its execution. We *could* make the original `erc-open' parameters and the calling buffer available via dynamic variable, but the first patch provides what I believe to be a tidier solution (its ERC-NEWS entry is in the _second_ patch). Thanks for reading. [0] https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-10/msg00776.html https://lists.gnu.org/archive/html/bug-gnu-emacs/2024-06/msg01098.html https://lists.gnu.org/archive/html/bug-gnu-emacs/2022-09/msg02116.html https://lists.gnu.org/archive/html/emacs-erc/2022-02/msg00008.html https://lists.gnu.org/archive/html/emacs-erc/2021-10/msg00003.html In GNU Emacs 31.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.43, cairo version 1.18.2) of 2025-01-26 built on localhost Repository revision: 01d93d56cd469ddb45d142da948caef9f2dc1a3f Repository branch: master Windowing system distributor 'The X.Org Foundation', version 11.0.12401004 System Description: Fedora Linux 41 (Workstation Edition) Configured using: 'configure --enable-check-lisp-object-type --enable-checking=3Dyes,glyphs 'CFLAGS=3D-O0 -g3' PKG_CONFIG_PATH=3D:/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=3Dibus 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 compile comint ansi-osc ansi-color ring comp-run comp-common mail-extr emacsbug message mailcap yank-media puny dired dired-loaddefs rfc822 mml mml-sec epa epg rfc6068 epg-config gnus-util text-property-search 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 erc derived auth-source eieio eieio-core icons password-cache json map format-spec erc-backend erc-networks easy-mmode byte-opt bytecomp byte-compile erc-common inline cl-extra help-mode erc-compat cl-seq cl-macs gv pcase rx compat subr-x cl-loaddefs cl-lib 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 tty-child-frames native-compile emacs) Memory information: ((conses 16 159659 10485) (symbols 48 11748 0) (strings 32 28884 4410) (string-bytes 1 1019143) (vectors 16 17190) (vector-slots 8 187670 5036) (floats 8 34 1) (intervals 56 325 0) (buffers 984 11)) --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-5.7-Set-major-mode-before-updating-modules-in-erc-op.patch >From 7b09e582102ee04951832b294bec471304b1c30e Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sat, 1 Feb 2025 16:59:51 -0800 Subject: [PATCH 1/2] [5.7] Set major-mode before updating modules in erc-open * lisp/erc/erc-button.el (erc-button-mode, erc-button-enable) (erc-button-disable): Use `erc-with-initialized-session' instead of `erc-mode-hook'. * lisp/erc/erc-common.el (erc-with-initialized-session): New macro. * lisp/erc/erc-goodies.el (erc-scrolltobottom-mode) (erc-scrolltobottom-enable, erc-scrolltobottom-disable) (erc-move-to-prompt-mode, erc-move-to-prompt-enable) (erc-move-to-prompt-disable): Use `erc-with-initialized-session' instead of `erc-mode-hook'. * lisp/erc/erc-imenu.el (erc-imenu-mode, erc-imenu-enable) (erc-imenu-disable): Use macro instead of `erc-mode-hook'. * lisp/erc/erc-stamp.el (erc-stamp-mode, erc-stamp-enable) (erc-stamp-disable): Use macro instead of `erc-mode-hook'. * lisp/erc/erc-track.el (erc-track-mode, erc-track-enable) (erc-track-disable): Use macro instead of `erc-mode-hook'. * lisp/erc/erc-truncate.el (erc-truncate-mode, erc-truncate-enable) (erc-truncate-disable): Use macro instead of `erc-mode-hook'. * lisp/erc/erc.el (erc--set-modules-functions): New variable, an internal hook to modify erc-modules membership. (erc-open): Set major mode before updating modules. --- lisp/erc/erc-button.el | 4 +--- lisp/erc/erc-common.el | 16 ++++++++++++++++ lisp/erc/erc-goodies.el | 14 +++++--------- lisp/erc/erc-imenu.el | 8 +++----- lisp/erc/erc-stamp.el | 8 +++----- lisp/erc/erc-track.el | 4 +--- lisp/erc/erc-truncate.el | 4 +--- lisp/erc/erc.el | 10 ++++++++-- 8 files changed, 38 insertions(+), 30 deletions(-) diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el index e6ed0e2f75f..ec5a012b8c9 100644 --- a/lisp/erc/erc-button.el +++ b/lisp/erc/erc-button.el @@ -54,13 +54,11 @@ button "This mode buttonizes all messages according to `erc-button-alist'." ((add-hook 'erc-insert-modify-hook #'erc-button-add-buttons 30) (add-hook 'erc-send-modify-hook #'erc-button-add-buttons 30) - (add-hook 'erc-mode-hook #'erc-button-setup 91) - (unless erc--updating-modules-p (erc-buffer-do #'erc-button-setup)) + (erc-with-initialized-session (erc-button-setup)) (add-hook 'erc--tab-functions #'erc-button-next) (erc--modify-local-map t "" #'erc-button-previous)) ((remove-hook 'erc-insert-modify-hook #'erc-button-add-buttons) (remove-hook 'erc-send-modify-hook #'erc-button-add-buttons) - (remove-hook 'erc-mode-hook #'erc-button-setup) (remove-hook 'erc--tab-functions #'erc-button-next) (erc--modify-local-map nil "" #'erc-button-previous))) diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el index d293e6ba878..776407b0e8d 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el @@ -434,6 +434,22 @@ define-erc-module (put ',enable 'definition-name ',name) (put ',disable 'definition-name ',name)))) +(defmacro erc-with-initialized-session (&rest body) + "Run BODY in all ERC buffers if outside `erc-open' and soon otherwise. +When inside `erc-open', run BODY after session variables have been +initialzied and after all `erc-mode-hook' members but before any +`after-change-major-mode-hook' members. Expect caller to know this is +only useful in global-module setup and that they're still responsible +for teardown, which is often done with `erc-buffer-do' or similar." + (let ((fn (make-symbol "fn")) + (hook-var (make-symbol "hook-var"))) + `(let ((,fn (lambda () ,@body))) + (if erc--updating-modules-p + (let ((,hook-var (gensym "erc--oneoff-major-mode-hook-"))) + (set ,hook-var ,fn) + (push ,hook-var delayed-mode-hooks)) + (erc-buffer-do ,fn))))) + (defmacro erc-with-buffer (spec &rest body) "Execute BODY in the buffer associated with SPEC. diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el index cfd0d3415ea..b1384c5bb08 100644 --- a/lisp/erc/erc-goodies.el +++ b/lisp/erc/erc-goodies.el @@ -79,12 +79,11 @@ erc-scrolltobottom-all ;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t) (define-erc-module scrolltobottom nil "This mode causes the prompt to stay at the end of the window." - ((add-hook 'erc-mode-hook #'erc--scrolltobottom-setup) - (when (and erc-scrolltobottom-all (< emacs-major-version 28)) + ((when (and erc-scrolltobottom-all (< emacs-major-version 28)) (erc-button--display-error-notice-with-keys "Option `erc-scrolltobottom-all' requires Emacs 28+. Disabling.") (setq erc-scrolltobottom-all nil)) - (unless erc--updating-modules-p (erc-buffer-do #'erc--scrolltobottom-setup)) + (erc-with-initialized-session (erc--scrolltobottom-setup)) (if erc-scrolltobottom-all (progn (add-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert 25) @@ -92,8 +91,7 @@ scrolltobottom (add-hook 'erc-insert-done-hook #'erc--scrolltobottom-all) (add-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)) (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom))) - ((remove-hook 'erc-mode-hook #'erc--scrolltobottom-setup) - (erc-buffer-do #'erc--scrolltobottom-setup) + ((erc-buffer-do #'erc--scrolltobottom-setup) (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert) (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all) (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all) @@ -253,10 +251,8 @@ erc-make-read-only ;;;###autoload(autoload 'erc-move-to-prompt-mode "erc-goodies" nil t) (define-erc-module move-to-prompt nil "This mode causes the point to be moved to the prompt when typing text." - ((add-hook 'erc-mode-hook #'erc-move-to-prompt-setup) - (unless erc--updating-modules-p (erc-buffer-do #'erc-move-to-prompt-setup))) - ((remove-hook 'erc-mode-hook #'erc-move-to-prompt-setup) - (dolist (buffer (erc-buffer-list)) + ((erc-with-initialized-session (erc-move-to-prompt-setup))) + ((dolist (buffer (erc-buffer-list)) (with-current-buffer buffer (remove-hook 'pre-command-hook #'erc-move-to-prompt t))))) diff --git a/lisp/erc/erc-imenu.el b/lisp/erc/erc-imenu.el index 2e244fa157e..6919e02bc8b 100644 --- a/lisp/erc/erc-imenu.el +++ b/lisp/erc/erc-imenu.el @@ -135,11 +135,9 @@ erc-imenu--create-index-function ;;;###autoload(autoload 'erc-imenu-mode "erc-imenu" nil t) (define-erc-module imenu nil "Simple Imenu integration for ERC." - ((add-hook 'erc-mode-hook #'erc-imenu-setup) - (add-hook 'which-function-mode-hook #'erc-imenu--disable-which-func) - (unless erc--updating-modules-p (erc-buffer-do #'erc-imenu-setup))) - ((remove-hook 'erc-mode-hook #'erc-imenu-setup) - (remove-hook 'which-function-mode-hook #'erc-imenu--disable-which-func) + ((add-hook 'which-function-mode-hook #'erc-imenu--disable-which-func) + (erc-with-initialized-session (erc-imenu-setup))) + ((remove-hook 'which-function-mode-hook #'erc-imenu--disable-which-func) (erc-buffer-do #'erc-imenu-setup))) (defun erc-imenu-setup () diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el index 5b6550c4a88..79fd4ecaacd 100644 --- a/lisp/erc/erc-stamp.el +++ b/lisp/erc/erc-stamp.el @@ -178,13 +178,11 @@ erc-timestamp-face ;;;###autoload(autoload 'erc-timestamp-mode "erc-stamp" nil t) (define-erc-module stamp timestamp "This mode timestamps messages in the channel buffers." - ((add-hook 'erc-mode-hook #'erc-stamp--setup) - (add-hook 'erc-insert-modify-hook #'erc-add-timestamp 70) + ((add-hook 'erc-insert-modify-hook #'erc-add-timestamp 70) (add-hook 'erc-send-modify-hook #'erc-add-timestamp 70) (add-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect) - (unless erc--updating-modules-p (erc-buffer-do #'erc-stamp--setup))) - ((remove-hook 'erc-mode-hook #'erc-stamp--setup) - (remove-hook 'erc-insert-modify-hook #'erc-add-timestamp) + (erc-with-initialized-session (erc-stamp--setup))) + ((remove-hook 'erc-insert-modify-hook #'erc-add-timestamp) (remove-hook 'erc-send-modify-hook #'erc-add-timestamp) (remove-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect) (erc-buffer-do #'erc-stamp--setup))) diff --git a/lisp/erc/erc-track.el b/lisp/erc/erc-track.el index 2897c3ede93..7c3523670ad 100644 --- a/lisp/erc/erc-track.el +++ b/lisp/erc/erc-track.el @@ -607,8 +607,7 @@ track ;; enable the tracking keybindings (add-hook 'erc-connect-pre-hook #'erc-track-minor-mode-maybe) (erc-track-minor-mode-maybe)) - (add-hook 'erc-mode-hook #'erc-track--setup) - (unless erc--updating-modules-p (erc-buffer-do #'erc-track--setup)) + (erc-with-initialized-session (erc-track--setup)) (add-hook 'erc-networks--copy-server-buffer-functions #'erc-track--replace-killed-buffer)) ;; Disable: @@ -630,7 +629,6 @@ track (remove-hook 'erc-connect-pre-hook #'erc-track-minor-mode-maybe) (when erc-track-minor-mode (erc-track-minor-mode -1))) - (remove-hook 'erc-mode-hook #'erc-track--setup) (erc-buffer-do #'erc-track--setup) (remove-hook 'erc-networks--copy-server-buffer-functions #'erc-track--replace-killed-buffer))) diff --git a/lisp/erc/erc-truncate.el b/lisp/erc/erc-truncate.el index f5404a15b71..7b458b6164a 100644 --- a/lisp/erc/erc-truncate.el +++ b/lisp/erc/erc-truncate.el @@ -67,12 +67,10 @@ truncate ;;enable ((add-hook 'erc-insert-done-hook #'erc-truncate-buffer) (add-hook 'erc-connect-pre-hook #'erc-truncate--warn-about-logging) - (add-hook 'erc-mode-hook #'erc-truncate--setup) - (unless erc--updating-modules-p (erc-buffer-do #'erc-truncate--setup))) + (erc-with-initialized-session (erc-truncate--setup))) ;; disable ((remove-hook 'erc-insert-done-hook #'erc-truncate-buffer) (remove-hook 'erc-connect-pre-hook #'erc-truncate--warn-about-logging) - (remove-hook 'erc-mode-hook #'erc-truncate--setup) (erc-buffer-do #'erc-truncate--setup))) (defvar-local erc-truncate--buffer-size nil diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 72335a444cb..fdf988dcb10 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -2447,6 +2447,12 @@ erc--updating-modules-p confidently call (erc-foo-mode 1) without having to learn anything about the dependency's implementation.") +(defvar erc--set-modules-functions nil + "Abnormal hook that runs before updating modules on major-mode. +Calls members with the TARGET and ID parameters of `erc-open' along with +the buffer from which `erc-open' was invoked, which is always the server +buffer for new targets and reconnecting servers.") + (defvar erc--setup-buffer-hook '(erc--warn-about-aberrant-modules) "Internal hook for module setup involving windows and frames.") @@ -2597,6 +2603,8 @@ erc-open (when connect (run-hook-with-args 'erc-before-connect server port nick)) (set-buffer buffer) (setq old-point (point)) + (delay-mode-hooks (erc-mode)) + (run-hook-with-args 'erc--set-modules-functions channel id old-buffer) (setq delayed-modules (erc--merge-local-modes (let ((erc--updating-modules-p t)) (erc--update-modules @@ -2604,8 +2612,6 @@ erc-open (or erc--server-reconnecting erc--target-priors))) - (delay-mode-hooks (erc-mode)) - (setq erc-server-reconnect-count old-recon-count) (when (setq erc-server-connected (not connect)) -- 2.48.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0002-5.7-Make-erc-settings-a-module.patch >From 0bbb3e4d236eeb5e9175a02216bd972839e44971 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sun, 17 Nov 2024 16:32:25 -0800 Subject: [PATCH 2/2] [5.7] Make erc-settings a module * doc/misc/erc.texi: Add Settings node under Modules. * etc/ERC-NEWS: Mention new module along with option `erc-client-certificate' and function `erc-channel-buffer-p. * lisp/erc/erc-networks.el (erc-settings): Move to new module `erc-settings'. (erc-get): Neuter completely by effectively aliasing to `ignore'. * lisp/erc/erc-settings.el: New file. * lisp/erc/erc.el (erc-client-certificate): New user option. (erc-channel-buffer-p): New function. (erc-modules): Add `settings' available set for custom-type. (erc--set-modules-functions): New variable, an internal hook to modify erc-modules membership. (erc-open): Set major mode before updating modules, and use `erc-client-certificate' as a fallback for the `client-certificate' parameter. * test/lisp/erc/erc-scenarios-settings.el: New file. * test/lisp/erc/erc-settings-tests.el: New file. * test/lisp/erc/erc-tests.el (erc-tests--modules): Add `settings'. * test/lisp/erc/resources/settings/basic-sasl.eld: New file. * test/lisp/erc/resources/settings/basic.eld: New file. --- doc/misc/erc.texi | 377 +++++++++++++++++- etc/ERC-NEWS | 29 ++ lisp/erc/erc-networks.el | 50 +-- lisp/erc/erc-settings.el | 295 ++++++++++++++ lisp/erc/erc.el | 18 +- test/lisp/erc/erc-scenarios-settings.el | 190 +++++++++ test/lisp/erc/erc-settings-tests.el | 120 ++++++ test/lisp/erc/erc-tests.el | 2 +- .../erc/resources/settings/basic-sasl.eld | 47 +++ test/lisp/erc/resources/settings/basic.eld | 38 ++ 10 files changed, 1121 insertions(+), 45 deletions(-) create mode 100644 lisp/erc/erc-settings.el create mode 100644 test/lisp/erc/erc-scenarios-settings.el create mode 100644 test/lisp/erc/erc-settings-tests.el create mode 100644 test/lisp/erc/resources/settings/basic-sasl.eld create mode 100644 test/lisp/erc/resources/settings/basic.eld diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 7c934d8bb3a..4f94fef281a 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -75,6 +75,9 @@ Top * Sample Session:: Example of connecting to the @samp{#emacs} channel * Special Features:: Differences from standalone IRC clients +Modules +* Settings:: Per-buffer configuration. + Advanced Usage * Connecting:: Ways of connecting to an IRC server. @@ -135,7 +138,6 @@ Introduction @node Getting Started @chapter Getting Started -@cindex settings The command @kbd{M-x erc} will start ERC and prompt for the server to connect to. If you're unsure of which server or network to connect @@ -547,6 +549,10 @@ Modules @item services Identify to Nickserv (IRC Services) automatically +@cindex modules, settings +@item settings +Apply options locally per-buffer + @cindex modules, smiley @item smiley Convert smileys to pretty icons @@ -723,6 +729,370 @@ Modules @c PRE5_4: Document every option of every module in its own subnode +@node Settings +@section Settings +@cindex settings + +This global module lets you apply ERC options on a per-buffer basis, as +local variable bindings (@pxref{Buffer-Local Variables,,, elisp,}). It +consists of a single @dfn{meta option}: + +@defopt erc-settings +An alist where each key is a @dfn{match condition} and each value a list +of bindings that loosely resembles a @code{let}-style @var{varlist}. +The first element of each binding is the variable of a user option and +the second an arbitrary sexp assigned to it locally in all matching +buffers. You can tell ERC to evaluate a given sexp as a Lisp form +before assigning it by including a trailing third element: the +@dfn{modifier flag} @code{:eval}. + +A match condition specifies when and whether ERC should apply each group +of bindings. These resemble @code{buffer-match-p} conditions, with two +major differences. The first is that predicate conditions don't take +any arguments. Instead, ERC runs them with the candidate buffer +current. The second difference is that the assortment of associative +cons-cell conditions is completely new. Gone are @code{major-mode}, +@code{category}, etc. In their place, ERC offers the following: + +@itemize +@item @code{(network . @samp{})} +@item @code{(id . @samp{})} +@item @code{(name . @samp{})} +@item @code{(target . @samp{})} +@end itemize + +The associated @code{cdr} values for the @code{network} and @code{id} +conditions are symbols identifying the current IRC network and the +@dfn{network context}, respectively. When first connecting, a +@code{network} condition is always @code{nil}, but an @code{id} +condition can be non-@code{nil} if you supply a matching @var{ID} +argument to an entry-point command, like @code{erc-tls} (@pxref{Network +Identifier}). + +Additionally, ERC also recognizes @code{buffer-match-p}'s boolean +operations and constants: + +@itemize +@item @code{(and @samp{})} +@item @code{(or @samp{})} +@item @code{(not @samp{})} +@item @code{t} +@item @code{nil} +@end itemize + +Also valid are regular expressions and nullary predicates: + +@itemize +@item @code{""} +@item @code{} +@end itemize + +@end defopt + +@noindent +Here's a contrived @code{erc-settings} value containing six entries, +the first of which has two assignments: + +@lisp +(((and (network . Libera.Chat) ; buffers #emacs, + "\\`#emacs") ; #emacs-foo, etc., + (my-own-variable 42) ; on Libera.Chat only + (erc-foo "")) + + ((or (target . "#chan") ; buffers #chan, bob, + (target . "bob")) ; chan@@Libera.Chat, + (erc-foo (erc-current-nick) :eval)) ; bob@@OFTC, etc. + + (erc-open-server-buffer-p (erc-bar 1)) ; connected server buffers + (erc-query-buffer-p (erc-bar 2)) ; query buffers only + (erc-channel-buffer-p (erc-bar 3)) ; channel buffers only + (erc-target (erc-bar 4)) ; query or channel buffers + (t (erc-bar 5))) ; any `erc-mode' buffer +@end lisp + +@noindent +Unfortunately, predicates too dissimilar from those shown above may not +work in all matching contexts due to the interaction between this module +and ERC's rather convoluted major-mode setup. While the only way to +know for sure is to try them, selecting them becomes much easier if you +can gain a sense for when exactly @samp{settings} attempts to create +bindings: + +@itemize +@item +In a server buffer, binding happens +@enumerate +@item +before ERC enables any modules or sets any local variables +@item +after establishing a logical IRC connection and learning what network +it's connected to +@end enumerate + +@item +In a target buffer, binding happens +@enumerate +@item +before enabling any modules, but only if you're overriding +@code{erc-modules}, and only then, for that option alone +@item +after ERC has set most of its default local variables and run +global-module setup but before running major-mode hooks or local-module +setup +@end enumerate +@end itemize + +The main downside of this staggered approach is that @samp{settings} +skips any bindings for variables that already have a local binding +rather than do the intuitive thing and simply clobber them. The reason +for this is to retain a consistent state between a global module's minor +mode setup and any local-module setup or major-mode hooks. They must +all witness the same value for the same variable. + +Revisiting the quasi-example above with this precedence quirk in mind, +notice that the made-up variable @code{erc-bar} will be @code{2} in +query buffers, @code{3} in channel buffers, and @code{5} in server +buffers. It's never @code{1} because the first binding opportunity +occurs before connecting, when @code{erc-open-server-buffer-p} returns +@code{nil}. It's likewise never @code{4} because that binding appears +lower in the list than those for @code{2} and @code{3}. For more on +this behavior, see the very last example below. + +When it comes to binding the option @code{erc-modules}, @samp{settings} +must make special accommodations. Getting it right may take some trial +and error, and it's only guaranteed to work for local modules because +most global ones share state among all sessions. However, it's fine to +include global modules in an @code{erc-modules} binding as long as they +also appear in the @code{default-value}. To set @code{erc-modules} for +a specific server buffer, specify an entry like + +@lisp +("[.]libera[.]chat:" + (erc-modules (seq-union '(sasl keep-place-indicator) erc-modules) + :eval) + (erc-sasl-user :nick)) +@end lisp + +@noindent +Here, the match condition is a regexp matching a dialed @acronym{TCP} +address because server buffers typically have these as temporary names +while connecting. As mentioned, ERC later renames them to reflect a +discovered network, after which @samp{settings} binds any matching +unbound options. + +A session's target buffers normally share the @code{erc-modules} value +bound locally in their server buffer. To specify a separate, overriding +set of modules for certain target buffers, use a condition that only +applies to them: + +@lisp +((and (network . Libera.Chat) erc-channel-buffer-p) + (erc-modules (seq-union '(fill-wrap) erc-modules) :eval)) +@end lisp + +@noindent +This is often unnecessary because local modules that only operate in one +kind of buffer tend to disable themselves elsewhere. And some, like +@samp{keep-place-indicator}, offer an option to restrict activation +based on buffer type. + +@anchor{Settings Examples} +@subsection Settings Examples +@cindex settings, examples + +A typical setup might include configuration for two networks, one that +uses @acronym{SASL} to authenticate and another that uses a server +password. + +@lisp +(setopt erc-modules + `(settings ,@@erc-modules) + + erc-settings + ;; Pre-connect and server buffer config for @samp{Libera.Chat}. + '(("libera\\.chat:" + (erc-prompt "Libera>") + (erc-autojoin-channels-alist ((Libera.Chat "##jrh"))) + (erc-header-line-format "%n on %S (%m, %l)") + (erc-sasl-user :nick) + (erc-modules `(sasl ,@@erc-modules) :eval)) + + ;; Channel buffer config for @samp{Libera.Chat} + ((and (network . Libera.Chat) erc-channel-buffer-p) + (erc-modules `(keep-place-indicator ,@@erc-modules) :eval) + (erc-prompt erc-prompt-format) + (erc-prompt-format "%c%b>") + (erc-header-line-format "%t: %o")) + + ;; Query buffer configuration for any network. + (erc-query-buffer-p + (erc-prompt erc-prompt-format) + (erc-prompt-format "%S>") + (erc-header-line-format nil)) + + ;; Pre-connect and server-buffer config for @samp{OFTC}. + ("oftc.net:" + (erc-prompt "OFTC!") + (erc-modules `(nicks ,@@erc-modules) :eval)))) +@end lisp + +Note that the pre-connection config described in the last entry can't +use the @dfn{global module} @samp{services} because it's incompatible +with the @dfn{local module} @samp{sasl}. In this scenario, instead of a +server password, the @samp{OFTC} connection could alternatively use +@dfn{CertFP} via auth-source (see next example). + +@anchor{ID-based Settings Example} +@subsubsection ID-based +@cindex settings, examples, id-based + +New users coming from traditional IRC clients often express a desire to +store connection details alongside other per-network or per-connection +configuration. Although ERC was designed to keep these concerns +separate, the @samp{settings} module can emulate that experience to some +degree. + +@lisp +(setopt erc-modules + `(settings ,@@erc-modules) + + erc-settings + ;; Connection details common to both @samp{Libera.Chat} IDs. + '(((and (or (id . lib-jrh) (id . lib-jrbot)) + (not erc-server-process-alive)) + (erc-server "irc.libera.chat") + (erc-port 6697) + (erc-modules `(sasl ,@@erc-modules) :eval)) + + ;; Connection details for account @samp{jrh}. + ((and (id . lib-jrh) (not erc-server-process-alive)) + (erc-nick "jrh") + (erc-user-full-name "J. Random Hacker") + (erc-email-userid "jrh") + (erc-autojoin-channels-alist ((lib-jrh "##jrh")))) + + ;; Connection details for account @samp{jrbot}. + ((and (id . lib-jrbot) (not erc-server-process-alive)) + (erc-nick "jrbot") + (erc-email-userid "jrbot") + (erc-user-full-name "J.R. Bot (jrh's bot)") + (erc-sasl-auth-source-function erc-auth-source-function) + (erc-autojoin-channels-alist ((lib-jrbot "##jrh" "##jrbot")))) + + ;; Non-connection opts for both @samp{Libera.Chat} sessions. + ((and (network . Libera.Chat) erc-open-server-buffer-p) + (erc-prompt "libera>") + (erc-header-line-format "%n on %S (%m, %l)")) + ((and (network . Libera.Chat) erc-channel-buffer-p) + (erc-modules `(keep-place-indicator ,@@erc-modules) :eval) + (erc-prompt erc-prompt-format) + (erc-prompt-format "%c%b>") + (erc-header-line-format "%t: %o")) + + ;; Server details for @samp{OFTC}. + ((and (id . oftc) (target . nil)) + (erc-server "irc.oftc.net") + (erc-port 6697) + (erc-nick "jrh") + (erc-email-userid "jrh") + (erc-user-full-name "J. Random Hacker") + (erc-client-certificate t) + (erc-modules `(services-regain ,@@erc-modules) :eval) + (erc-prompt "jrh@@OFTC!")))) + +(defun my-erc-up (id) + "Connect to preconfigured session identified by ID." + (interactive "SNet id: ") + (erc-tls :server nil + :port nil + :nick nil + :user nil + :password nil + :full-name nil + :id network-id)) +@end lisp + +@noindent +With a setup like the above, you can connect explicitly to a predefined +session by supplying an identifying symbol, like @code{lib-jrh}, along +with the @var{ID} keyword parameter to the entry-point command +@code{erc-tls}. The main gotcha when using these identifiers is that a +regexp targeting a dialed @acronym{TCP} address, like +@code{"\\`irc[.]libera[.]chat:6697\\'"}, won't work for conditions like +the first one above because server buffers receive permanent names +immediately upon creation, long before connecting. Instead, you'll want +to specify a chosen identifier via the @samp{(id . )} pattern +and likely qualify it with another subcondition, like @code{(not +erc-server-process-alive)}, to ensure that the @code{erc-modules} +binding only matches the server buffer and not any target buffers. +Equivalent qualifying subconditions would be @code{(not erc-target)} and +@code{(target . nil)}. + +Regarding authentication, notice that the @samp{lib-jrbot} connection +retrieves its @acronym{SASL} credentials, which might be a client +certificate, via auth-source, while session @samp{lib-jrh} prompts you +interactively for a password. (@xref{SASL, SASL in ERC}.) The +@samp{oftc} connection also uses auth-source to supply its credentials, +this time for non-@acronym{SASL} client-cert (@dfn{CertFP}) +authentication. Lastly, in the scenario above, a user would need to run +@kbd{M-x my-erc-up @key{RET}} instead of @kbd{M-x erc @key{RET}} when +connecting to IRC. For convenience, ERC provides a similar command: + +@defun erc-settings-connect-by-id network-id +Prompt the user and provide completion for some @samp{(id . )} +appearing in their @code{erc-settings}, and specify it as the argument +to the @var{ID} keyword parameter when invoking @code{erc-tls}. +@end defun + +With both traditional and ID-based setups, it's often more convenient to +define less exclusive, more ``overlapping'' match conditions: + +@lisp +(use-package erc-settings + :custom + (erc-settings + '(((not erc-server-process-alive) ; common to all servers + (erc-nick "jrh") + (erc-port 6697) + (erc-email-userid "jrh") + (erc-user-full-name "J. Random Hacker")) + + (erc-channel-buffer-p ; all channels + (erc-modules `(keep-place-indicator ,@@erc-modules) :eval)) + + ((id . id-libera) ; Libera.Chat only, including targets + (erc-server "irc.libera.chat") + (erc-sasl-password "changeme") + (erc-modules `(sasl ,@@erc-modules) :eval) + (erc-prompt erc-prompt-format) + (erc-autojoin-channels-alist ((id-foonet "#erc")))) + + ((id . id-oftc) ; OFTC only, including targets + (erc-server "irc.oftc.net") + (erc-modules `(fill-wrap ,@@erc-modules) :eval) + (erc-autojoin-channels-alist ((id-barnet "##jrh"))))))) +@end lisp + +If you go this route, know that @samp{settings} will often create local +bindings in buffers where they're not applicable. In the above example, +buffer @file{#erc} on @samp{Libera.Chat} will have @code{erc-server} and +@code{erc-sasl-password} bound locally for no reason. In most cases, +this is perfectly harmless. + +A final thing to notice about the example above is that the +@code{erc-modules} binding in the second group, the one with match +condition @code{erc-channel-buffer-p}, effectively @dfn{shadow}s that +same variable in all remaining settings affecting channel buffers. If +it instead appeared at the end of the list, ERC would skip its +@code{erc-modules} binding because the variable would already be locally +bound. But, as is, both @file{##jrh} on @samp{OFTC} and @file{#erc} on +@samp{Libera.Chat} activate @samp{keep-place-indicator} but not +@samp{fll-wrap}. Server buffer @file{id-oftc} has only @samp{fll-wrap}, +and server buffer @file{id-libera} has neither. And, if you were to +receive a query from @samp{alice} on @samp{OFTC}, buffer @file{alice} +would only have @samp{fill-wrap} but not @samp{keep-place-indicator} +because it's a non-channel on @samp{OFTC}. + @node Advanced Usage @chapter Advanced Usage @@ -1222,6 +1592,11 @@ SASL @item Multiple networks +If you use the @samp{settings} module, introduced in ERC 5.7, please +refer to that section's guide for a more modern, config-based approach +to specifying buffer-local @acronym{SASL} options (@pxref{Settings +Examples}). + @example # ~/.authinfo.gpg diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index dd960994b4f..4aaa92e5e71 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -11,6 +11,35 @@ This file is about changes in ERC, the powerful, modular, and extensible IRC (Internet Relay Chat) client distributed with GNU Emacs since Emacs version 22.1. + +* Changes in ERC 5.7 + +** Apply options granularly to specific connections or buffers. +Born from the ashes of a PoC drafted nearly a quarter century ago, the +'settings' module offers a simple means of applying arbitrary user +options to various "scopes" of interest based on match conditions +reminiscent of 'buffer-match-p'. + +** Specify a TLS client certificate globally via user option. +Option 'erc-client-certificate' allows you to do just that. It may also +be helpful with some usage patterns involving the new 'settings' module. + +** Changes in the library API. + +*** Major-mode setup runs before global-module setup in 'erc-open'. +The "enable" body of a global module's minor-mode command will now see +'erc-mode' as the 'major-mode'. Although ERC still delays its +major-mode hook until after it has initialized its main session +variables, a module can also now set any of its own variables that don't +depend on such initialization immediately and without fear of their +being killed. For modules still needing to run delayed, session-aware +code, a new convenience macro called 'erc-with-initialized-session' is +now available to automate any 'erc-mode-hook' wrangling for you. + +*** A predicate to detect if the current buffer is a channel. +New function 'erc-channel-buffer-p' is a channel-detecting complement to +'erc-query-buffer-p', and a light wrapper around 'erc-channel-p'. + * Changes in ERC 5.6.1 diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el index 289b6f8bd8f..a783cb7cbdb 100644 --- a/lisp/erc/erc-networks.el +++ b/lisp/erc/erc-networks.el @@ -1609,28 +1609,12 @@ erc-networks--server-select (scheme (if (cdr pspec) "ircs" "irc"))) (if ports (format "%s://%s:%d" scheme host (seq-random-elt ports)) host))) -;;; The following experimental -;; It does not work yet, help me with it if you -;; think it is worth the effort. - -(defvar 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") +;; Longtime occupant of this file `erc-settings' was repurposed in ERC +;; 5.7 and moved to erc-settings.el. +;; Attempting to provide a "working" version of this function would be +;; nonsensical because `erc-settings' now has a completely different +;; layout than the unfinished version from `erc-networks' (circa 2002). (defun erc-get (var &optional net target) "Retrieve configuration values from `erc-settings'. @@ -1639,27 +1623,9 @@ erc-get 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 - (setq elt (car items) - items (cdr items)) - (when (eq (car elt) var) - (cond ((and net target (listp (nth 1 elt)) - (eq net (car (nth 1 elt))) - (string-equal target (nth 1 (nth 1 elt)))) - (setq val (nth 2 elt) - items nil)) - ((and net (eq net (nth 1 elt))) - (setq val (nth 2 elt) - items nil)) - ((and (not net) (not target) (not (nth 1 elt))) - (setq val (nth 2 elt) - items nil))))) - val)) - -;; (erc-get 'pals 'Libera.Chat) + (declare (obsolete "`erc-settings' schema incompatible with PoC from 2002" + "31.1")) + (ignore var net target)) (provide 'erc-networks) diff --git a/lisp/erc/erc-settings.el b/lisp/erc/erc-settings.el new file mode 100644 index 00000000000..c912d805dea --- /dev/null +++ b/lisp/erc/erc-settings.el @@ -0,0 +1,295 @@ +;;; erc-settings.el -- Buffer-local options -*- lexical-binding: t; -*- + +;; Copyright (C) 2025 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published +;; by the Free Software Foundation, either version 3 of the License, +;; or (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; This module is based on an ancient proof-of-concept by Mario Lang, +;; with contributions from Diane Murray and possibly others. It lived +;; in erc-networks.el for over two decades before being repurposed as a +;; module in 2025. + +;; Usage: add `settings' to `erc-modules', and customize the option +;; `erc-settings'. + +;;; Code: +(require 'erc) + +(defgroup erc-settings nil + "Buffer-local values for arbitrary user options." + :group 'erc) + +(define-widget 'erc--buffer-match 'lazy + "Condition for `erc-settings--buffer-match-p'." + :tag "Valid `erc-settings' match condition" + :type `(choice (boolean :tag "Always/never") + (regexp :tag "Buffer-name regexp") + (function :tag "Predicate") + (cons :tag "Network" (const network) symbol) + (cons :tag "ID" (const id) symbol) + (cons :tag "Target" (const target) (choice string + (const nil))) + (cons :tag "Buffer name" (const name) string) + (list :tag "Negation" + (const not) + (erc--buffer-match :tag "Match condition")) + (cons :tag "All/any" + (choice :tag "Operator" (const or) (const and)) + (repeat :tag "Match condition" erc--buffer-match)))) + +(defcustom erc-settings () + "A match-condition alist for setting ERC's user options locally. + +This \"meta option\" provides a means of specifying buffer-local values +for other user options. It does this by setting an option's variable +locally in server and target buffers. If a local binding already exists +for a variable, ERC leaves it alone. Alist members should be of the +form (MATCHCOND . BINDINGS), where BINDINGS is a list of assignments +like (VAR VAL . FLAGS). + +MATCHCOND works like a `buffer-match-p' condition, only tailored +slightly to meet ERC's unique requirements. The boolean operators +`and', `or', and `not' remain unchanged, as do strings, which ERC treats +as regexps to be matched against a buffer's name. For servers, this +name is usually a dialed TCP address while connecting and a network (ID) +once connected. Query and channel buffers normally share their target's +name, possibly suffixed by a disambiguating \"@\" + identifier. If +MATCHCOND is a function, ERC assumes it's a predicate that takes no +arguments and runs in the candidate ERC buffer. As for key-value +cons-cell conditions, ERC ignores all traditional ones defined by +`buffer-match-p', like `derived-mode' and `major-mode', instead +preferring to define its own: (network . NETWORK), where NETWORK is a +symbol returned by the function `erc-network'; (id . ID), where ID is a +user-provided symbol as described by the Info node `(erc) Network +Identifier'; (target . TARGET), where TARGET, if non-nil, is the channel +name or query nick; and (name . NAME), which matches a buffer's NAME, as +a string. + +In BINDINGS, each member's VAR is the symbol of a user option or +variable to which ERC assigns the sexp VAL locally in the current +buffer. When FLAGS contains the keyword :eval, ERC evaluates VAL as a +lisp form and assigns the result to VAR. Each entry in BINDINGS should +appear in order of decreasing match specificity and increasing +generality because even though ERC applies all matching entries, it +skips subsequent ones for which a local binding already exists. + +See Info node `(erc) Settings' for a friendlier description of the DSL +and Info node `(erc) Settings Examples' for example usage." + :package-version '(ERC . "5.7") ; FIXME sync on release + :type (let* ((flags '(set :tag "Flags" + (const :tag "Prefer custom-set (advanced)" :custom) + (const :tag "Evaluate form" :eval))) + (setting `(cons :tag "Binding assignment" + (variable :tag "Option/variable") + (cons :tag "Value" sexp ,flags)))) + `(alist :key-type erc--buffer-match :value-type (repeat ,setting)))) + +;;;###autoload(autoload 'erc-settings-mode "erc-settings" nil t) +(define-erc-module settings nil + "Global module to set ERC options locally per-buffer. +Like many modules, this cannot reasonably be toggled via mode command +mid-session. To see the result of updates to the option `erc-settings' +immediately, destroy and reopen all affected sessions." + ((when (and erc--updating-modules-p + (not (memq #'erc-settings--set-modules + (default-value 'erc--set-modules-functions)))) + (erc--warn-once-before-connect 'erc-settings-mode + "Module `settings' did not load properly. Add it to `erc-modules'" + " and run `erc-update-modules' before connecting, or use a" + " Custom-aware configuration method, like `setopt'.")) + ;; Set non-`erc-module' entries in target buffers after all default + ;; non-connection related local session variables have been set. + (if erc--updating-modules-p + (add-hook 'change-major-mode-after-body-hook + #'erc-settings--setup -90 t) + (erc-buffer-do #'erc-settings--setup)) + ;; Set any remaining entries in server buffers once the network is + ;; known and all local session variables have been populated. + (add-hook 'erc-after-connect #'erc-settings-bind -30) + (add-hook 'erc--set-modules-functions #'erc-settings--set-modules 30)) + ((erc-buffer-do #'erc-settings--setup) + (remove-hook 'erc-after-connect #'erc-settings-bind) + (remove-hook 'erc--set-modules-functions #'erc-settings--set-modules))) + +(defun erc-settings--setup () + (if erc-settings-mode + (when erc-server-connected + (erc-settings--set (erc-settings--gather-bindings (current-buffer)))) + (remove-hook 'change-major-mode-after-body-hook #'erc-settings--setup t) + (dolist (entry erc-settings) + (pcase-dolist (`(,var . ,_) (cdr entry)) + (when (local-variable-p var) + (kill-local-variable var)))))) + +(defun erc-settings--buffer-match-p (condition) + "Return non-nil if CONDITION matches current buffer. +Act almost like `buffer-match-p', except recognize an alternate set of +property-based cons-cell conditions. Additionally, don't pass any +arguments to predicate-type conditions, and interpret CONDITION in the +current buffer, where CONDITION should among the following: + +- the symbol t, which always matches, +- the symbol nil, which never matches, +- a regular expression matched against current buffer's name, +- a predicate taking no arguments and run in the candidate buffer +- a cons-cell with a... + + ...car like and that matches if its cdr is + ----------- ------------------------------ + `id' `eq' to current buffer's network context ID + `network' `eq' to current buffer's `erc-network' + `target' `equal' to current buffer's `erc-target' + `name' `equal' to current buffer's name + `and' a list of matching conditions + `or' a list containing at least one matching condition + `not' a list of a single condition that matches when negated" + (pcase condition + ('t t) + ((pred stringp) (string-match-p condition (buffer-name))) + ((pred functionp) (funcall condition)) + (`(id . ,id) (and erc-networks--id + (eq (erc-networks--id-symbol erc-networks--id) id))) + (`(network . ,network) (and erc-network (eq erc-network network))) + (`(target . ,target) (equal target (erc-target))) + (`(name . ,name) (equal name (buffer-name))) + (`(not . ,cond) (not (erc-settings--buffer-match-p cond))) + (`(or . ,args) (seq-some #'erc-settings--buffer-match-p args)) + (`(and . ,args) (seq-every-p #'erc-settings--buffer-match-p args)))) + +(defun erc-settings-bind (&rest _) + "Set all settings in option `erc-settings' from current network's ID." + (erc-settings--set (erc-settings--gather-bindings (current-buffer)))) + +;;;###autoload +(defun erc-settings--set-modules (target id calling-buffer) + "Set `erc-modules' if `erc-settings' has a binding for current buffer. +In server buffers, bind all matching entries, not just `erc-modules'. +Borrow state from CALLING-BUFFER for matching purposes. Expect TARGET +to be a string or nil and ID to be a symbol." + (when (and erc-settings (or erc-settings-mode (memq 'settings erc-modules))) + ;; Shadow `erc-networks--id' for (id . ), `erc--target' for + ;; (target . ), etc. Although predicates of interest, like + ;; `erc-query-buffer-p', may call on `buffer-local-value', its + ;; `buffer' argument will be the current buffer, which doesn't yet + ;; have any buffer-local bindings. + (cl-assert (not (local-variable-p 'erc-networks--id))) + (cl-assert (not (local-variable-p 'erc-network))) + (cl-assert (not (local-variable-p 'erc--target))) + (let ((erc-networks--id (or (and id (erc-networks--id-create id)) + (buffer-local-value 'erc-networks--id + calling-buffer)))) + (if target + (if-let* + ((erc-network (buffer-local-value 'erc-network calling-buffer)) + (erc--target (erc--target-from-string target)) + (bindings (erc-settings--gather-bindings (current-buffer))) + (entry (assq 'erc-modules bindings))) + (erc-settings--set-value 'erc-modules (nth 1 entry) (cddr entry)) + (when-let* ((old-value (with-current-buffer calling-buffer + (and (local-variable-p 'erc-modules) + erc-modules)))) + (setq-local erc-modules old-value))) + (erc-settings-bind))))) + +(defun erc-settings--gather-bindings (buffer) + "Return a list of matching bindings for server BUFFER's session." + (with-current-buffer buffer + (mapcan (lambda (entry) + (and (erc-settings--buffer-match-p (car entry)) + (copy-sequence (cdr entry)))) + erc-settings))) + +;; Currently, when a binding contains the experimental flag :custom, ERC +;; tries to use VAR's `custom-set' function, if defined, for setting its +;; value. ERC doesn't do so by default because many such functions use +;; `set-default', which defeats the purpose. +(defun erc-settings--set-value (var value flags) + "Set VAR to VALUE locally and interpret FLAGS. +Do nothing if VAR already has a local binding. If VAR has the symbol +property `erc-settings--wrap', assume it's a function that takes the +arguments (SETTER VAR VALUE), and defer to it to perform the actual +setting." + (unless (local-variable-p var) + (let ((setter #'set) + (evalp nil) + (customp nil)) + (dolist (flag flags) + (pcase-exhaustive flag + (:eval (setq evalp t)) + (:custom (setq customp t)))) + (make-local-variable var) + (when evalp + (setq value (eval value t))) + (when customp + (custom-load-symbol var) + (setq setter (or (get var 'custom-set) #'set))) + (if-let* ((xsetter (get var 'erc-settings--wrap))) + (funcall xsetter setter var value) + (funcall setter var value))))) + +(defun erc-settings--set (bindings) + "Set all matching bindings locally in current buffer. +Expect BINDINGS to be a list of (KEY VALUE . FLAGS)." + (pcase-dolist (`(,var ,value . ,flags) bindings) + (erc-settings--set-value var value flags))) + +(defun erc-settings--ensure-prompt-reset (setter var value) + (cl-assert (eq var 'erc-prompt)) + (when (and erc-input-marker (stringp value)) + (let ((erc-prompt (lambda () value))) + (with-silent-modifications + (erc--refresh-prompt)))) + (funcall setter var value)) + +(put 'erc-prompt 'erc-settings--wrap #'erc-settings--ensure-prompt-reset) + +(defun erc-settings--find-condition (predicate settings) + "Return all conditions in SETTINGS for which PREDICATE returns non-nil." + (letrec ((found ()) + (find (lambda (condition) + (pcase condition + (`(and . ,rest) (mapc find rest)) + (`(or . ,rest) (mapc find rest)) + (`(not ,cond) (funcall find cond)) + ((pred (funcall predicate)) (push condition found)))))) + (dolist (setting settings) + (funcall find (car setting))) + (nreverse found))) + +(defun erc-settings--extract-ids () + "Return all arguments to an `id' property condition in `erc-settings'." + (mapcar #'cdr (erc-settings--find-condition + (lambda (condition) (eq (car-safe condition) 'id)) + erc-settings))) + +;;;###autoload +(defun erc-settings-connect-by-id (network-id) + "Connect via some preconfigured network ID specified in `erc-settings'. +See Info node `(erc) ID-based Settings Example'." + (interactive (list (completing-read "Network ID: " + (erc-settings--extract-ids)))) + (erc-tls :server nil :port nil :nick nil :user nil + :password nil :full-name nil :id network-id)) + +(provide 'erc-settings) + +;;; erc-settings.el ends here +;; +;; Local Variables: +;; generated-autoload-file: "erc-loaddefs.el" +;; End: diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index fdf988dcb10..5ab1444e2ba 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1092,6 +1092,16 @@ erc-email-userid :group 'erc :type 'string) +(defcustom erc-client-certificate nil + "Client certificate for TLS connections. +Files must be abolute paths." + :package-version '(ERC . "5.7") ; FIXME sync on release + :group 'erc + :type '(choice (const :tag "Don't supply a client-cert" nil) + (const :tag "Defer to auth-source" t) + (list :tag "Key and cert files" + (file :tag "Key") (file :tag "Cert")))) + (defcustom erc-system-name nil "Use this as the name of your system. If nil, ERC will call function `system-name' to get this information." @@ -1966,6 +1976,10 @@ erc-channel-p (target (buffer-local-value 'erc--target target)) ((erc--target-channel-p target))))))) +(defun erc-channel-buffer-p (&optional buffer) + "Call `erc-channel-p' with BUFFER or the current buffer." + (erc-channel-p (or buffer (current-buffer)))) + ;; For the sake of compatibility, a historical quirk concerning this ;; option, when nil, has been preserved: all buffers are suffixed with ;; the original dialed host name, which is usually something like @@ -2360,6 +2374,7 @@ erc-modules scrolltobottom) (const :tag "services: Identify to Nickserv (IRC Services) automatically" services) + (const :tag "settings: Set ERC user options buffer locally" settings) (const :tag "smiley: Convert smileys to pretty icons" smiley) (const :tag "sound: Play sounds when you receive CTCP SOUND requests" sound) @@ -2652,7 +2667,8 @@ erc-open ;; The local copy of `erc-nick' - the list of nicks to choose (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick))) ;; client certificate (only useful if connecting over TLS) - (setq erc-session-client-certificate client-certificate) + (setq erc-session-client-certificate (or client-certificate + erc-client-certificate)) (setq erc-networks--id (if connect (or (and erc--server-reconnecting diff --git a/test/lisp/erc/erc-scenarios-settings.el b/test/lisp/erc/erc-scenarios-settings.el new file mode 100644 index 00000000000..c1197d3fb99 --- /dev/null +++ b/test/lisp/erc/erc-scenarios-settings.el @@ -0,0 +1,190 @@ +;;; erc-scenarios-settings.el --- erc-settings tests -*- lexical-binding: t -*- + +;; Copyright (C) 2025 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Code: + +(require 'ert-x) +(eval-and-compile + (let ((load-path (cons (ert-resource-directory) load-path))) + (require 'erc-scenarios-common))) + +(require 'erc-settings) + +(ert-deftest erc-scenarios-settings/sasl () + :tags '(:expensive-test) + (erc-scenarios-common-with-cleanup + ((erc-server-flood-penalty 0.1) + (erc-scenarios-common-dialog "settings") + (server-foonet (erc-d-run "localhost" t "server-foonet" 'basic-sasl)) + (server-barnet (erc-d-run "localhost" t "server-barnet" 'basic)) + (port-foonet (process-contact server-foonet :service)) + (port-barnet (process-contact server-barnet :service)) + (erc-modules `(settings ,@erc-modules)) + (erc-settings + `(((and ,(rx-to-string `(: ":" ,(number-to-string port-foonet) eot) t) + (not erc-server-process-alive)) + (erc-modules `(sasl ,@erc-modules) :eval)) + ((and ,(rx-to-string `(: ":" ,(number-to-string port-barnet) eot) t) + (not erc-server-process-alive)) + (erc-modules `(fill-wrap ,@erc-modules) :eval) + (erc-autojoin-channels-alist (("." "#spam")))) + ((and (network . foonet) erc-open-server-buffer-p) + (erc-prompt "server!!!") + (erc-autojoin-channels-alist ((foonet "#chan")))) + ((and (network . foonet) erc-channel-buffer-p) + (erc-modules `(keep-place-indicator ,@erc-modules) :eval)))) + (expect (erc-d-t-make-expecter))) + + ;; Simulate `setopt', `custom-set-variables', etc. + (erc-update-modules) + (with-current-buffer (erc :server "127.0.0.1" + :port port-foonet + :nick "tester" + :user "tester" + :password "changeme" + :full-name "tester") + (funcall expect 10 "This server is in debug mode")) + (with-current-buffer (erc :server "127.0.0.1" + :port port-barnet + :nick "tester" + :user "tester" + :full-name "tester") + (funcall expect 10 "This server is in debug mode")) + + ;; Sentinel variables for modules we're enabling locally. + (defvar erc-fill--wrap-last-msg) + (defvar erc--keep-place-indicator-overlay) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan")) + (funcall expect 10 " bob: Grows, lives, and dies") + (should erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + ;; Targets that don't specify their own local modules inherit any + ;; from their server buffer (if `erc-modules' is locally bound). + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam")) + (funcall expect 10 " alice: Ay, like a") + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + ;; Point in fact: `fill-wrap' is enabled in this query buffer. + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "alice")) + (funcall expect 10 " My hand to thee") + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "foonet")) + (should-not erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "server!!!")) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "barnet")) + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + (erc-settings-mode -1))) + +(ert-deftest erc-scenarios-settings/sasl/id () + :tags '(:expensive-test) + (erc-scenarios-common-with-cleanup + ((erc-server-flood-penalty 0.1) + (erc-scenarios-common-dialog "settings") + (server-foonet (erc-d-run "localhost" t "server-foonet" 'basic-sasl)) + (server-barnet (erc-d-run "localhost" t "server-barnet" 'basic)) + (port-foonet (process-contact server-foonet :service)) + (port-barnet (process-contact server-barnet :service)) + (erc-modules `(settings ,@erc-modules)) + (erc-settings + `(((not erc-server-process-alive) ; common to both servers + (erc-server "127.0.0.1") + (erc-nick "tester") + (erc-email-userid "tester") + (erc-user-full-name "tester")) + ;; If this were moved to the end of the list (try it), ERC + ;; would skip it because (id . x) matches in channels too. + (erc-channel-buffer-p ; all channels + (erc-modules `(keep-place-indicator ,@erc-modules) :eval)) + ((id . id-foonet) ; foonet only (channels too) + (erc-port ,port-foonet) + (erc-sasl-password "changeme") + (erc-modules `(sasl ,@erc-modules) :eval) + (erc-prompt "wee!!!") + (erc-autojoin-channels-alist ((id-foonet "#chan")))) + ((id . id-barnet) ; barnet only (channels too) + (erc-port ,port-barnet) + (erc-modules `(fill-wrap ,@erc-modules) :eval) + (erc-autojoin-channels-alist ((id-barnet "#spam")))))) + (expect (erc-d-t-make-expecter))) + + ;; Simulate `setopt', `custom-set-variables', etc. + (erc-update-modules) + + ;; Mimic `erc-settings-connect-by-id', except don't use TLS. + (with-current-buffer (erc :server nil :port nil :nick nil :user nil + :password nil :full-name nil :id 'id-foonet) + (should (equal (buffer-name) "id-foonet")) + (funcall expect 10 "This server is in debug mode")) + (with-current-buffer (erc :server nil :port nil :nick nil :user nil + :password nil :full-name nil :id 'id-barnet) + (should (equal (buffer-name) "id-barnet")) + (funcall expect 10 "This server is in debug mode")) + + ;; Sentinel variables for modules we're enabling locally. + (defvar erc-fill--wrap-last-msg) + (defvar erc--keep-place-indicator-overlay) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan")) + (funcall expect 10 " bob: Grows, lives, and dies") + (should erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "wee!!!")) + + ;; Channel-only module overrides were applied correctly. + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam")) + (funcall expect 10 " alice: Ay, like a") + (should erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + ;; Module `fill-wrap' is active in this query buffer because it has + ;; no query-specific overrides for `erc-modules'. It instead + ;; inherits the local value from the server buffer. + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "alice")) + (funcall expect 10 " My hand to thee") + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + (with-current-buffer "id-foonet" + (should-not erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "wee!!!")) + + (with-current-buffer "id-barnet" + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + (erc-settings-mode -1))) + +;;; erc-scenarios-settings.el ends here diff --git a/test/lisp/erc/erc-settings-tests.el b/test/lisp/erc/erc-settings-tests.el new file mode 100644 index 00000000000..8563e3acbcb --- /dev/null +++ b/test/lisp/erc/erc-settings-tests.el @@ -0,0 +1,120 @@ +;;; erc-settings-tests.el --- Tests for erc-settings -*- lexical-binding:t -*- + +;; Copyright (C) 2024 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;;; Code: + +(require 'erc-settings) +(require 'ert-x) +(eval-and-compile + (let ((load-path (cons (ert-resource-directory) load-path))) + (require 'erc-tests-common))) + +(ert-deftest erc-settings () + (when (< emacs-major-version 29) (ert-skip "Needs `setopt'")) + + ;; Defers to `setopt' to perform some type checking. + (cl-letf* ((erc-settings ()) + ((symbol-function 'set-default) #'set) + ((symbol-function 'warn) #'error)) + + (setopt erc-settings `(((or (network . foonet) + ,(rx bot "127.0.0.1:")) + (erc-foo 1)) + ((and ,(rx bot "#emacs") + (not ,(rx bot "#emacs-devel"))) + (erc-foo 2) + (erc-bar 3)))) + + + ;; Realistic predicates and bindings. + (setopt erc-settings '(((and (id . foonet) + (not erc-server-process-alive)) + (erc-prompt "ERC! ") + (erc-sasl-user :nick)) + (erc-open-server-buffer-p + (erc-autojoin-channels-alist (("." "#chan")))) + ((or "#chan" "#spam" erc-query-buffer-p) + (erc-modules `(keep-place-indicator ,@erc-modules) + :eval)))) + + ;; Network can also be a regexp. + (setopt erc-settings `(((or ,(rx bot (| "localhost" "127.0.0.1") ":") + (network . foonet)) + (erc-foo 1)) + ((or ,(rx bot "#emacs") "#erc") + (erc-foo 2))))) + + (should (equal erc-settings ()))) + +(ert-deftest erc-settings--gather-bindings () + + ;; Match by network only. + (with-current-buffer (get-buffer-create "foonet") + (setq erc-network 'foonet) + (let ((erc-settings '(((or (network . barnet) ignore nil)) + ((and (network . foonet) always t) + (erc-foo 1) (erc-bar 2)) + ((network . baznet))))) + (should (equal '((erc-foo 1) (erc-bar 2)) + (erc-settings--gather-bindings (current-buffer))))) + (kill-buffer)) + + ;; Multiple regexps. + (with-current-buffer (get-buffer-create "localhost:6697") + (let ((erc-settings '(((network . barnet)) + ("\\`localhost" + (erc-foo 1)) + ("." + (erc-bar 2))))) + (should (equal '((erc-foo 1) (erc-bar 2)) + (erc-settings--gather-bindings (current-buffer)))) + (kill-buffer))) + + ;; One regexp, one network. + (with-current-buffer (get-buffer-create "foonet") + (setq erc-network 'foonet) + (let ((erc-settings `((,(rx (| "foonet" "barnet")) + (erc-foo 1)) + ((and (network . foonet) (name . "foonet")) + (erc-bar 2))))) + (should (equal '((erc-foo 1) (erc-bar 2)) + (erc-settings--gather-bindings (current-buffer))))) + (kill-buffer))) + +(ert-deftest erc-settings--extract-ids () + (should-not (erc-settings--extract-ids)) + + ;; Does not dedupe. + (let ((erc-settings '(((id . a) + (erc-foo 0)) + ("." + (erc-foo 1)) + ((and t nil) + (erc-foo 2)) + ((and ignore (id . b)) + (erc-foo 3)) + ((or (not (id . c)) (and (id . d) ignore)) + (erc-foo 4)) + ((not (id . a)) + (erc-foo 5))))) + (should (equal (erc-settings--extract-ids) '(a b c d a))))) + +;;; erc-settings-tests.el ends here diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index df9e4d52f77..456ababef52 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -3525,7 +3525,7 @@ erc-tests--modules command-indicator completion dcc fill identd imenu irccontrols keep-place list log match menu move-to-prompt netsplit networks nickbar nicks noncommands notifications notify page readonly - replace ring sasl scrolltobottom services smiley sound + replace ring sasl scrolltobottom services settings smiley sound spelling stamp track truncate unmorse xdcc)) ;; Ensure that `:initialize' doesn't change the ordering of the diff --git a/test/lisp/erc/resources/settings/basic-sasl.eld b/test/lisp/erc/resources/settings/basic-sasl.eld new file mode 100644 index 00000000000..33d57018bb7 --- /dev/null +++ b/test/lisp/erc/resources/settings/basic-sasl.eld @@ -0,0 +1,47 @@ +;; -*- mode: lisp-data; -*- +((cap 10 "CAP REQ :sasl")) +((nick 10 "NICK tester")) +((user 10 "USER tester 0 * :tester")) +((authenticate 10 "AUTHENTICATE PLAIN") + (0.00 ":irc.foonet.org CAP * ACK sasl") + (0.04 "AUTHENTICATE +")) +((authenticate 10 "AUTHENTICATE AHRlc3RlcgBjaGFuZ2VtZQ==") + (0.01 ":irc.foonet.org 900 * * tester :You are now logged in as tester") + (0.01 ":irc.foonet.org 903 * :Authentication successful")) +((cap 10 "CAP END") + (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester") + (0.01 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.11.1") + (0.01 ":irc.foonet.org 003 tester :This server was created Sat, 01 Feb 2025 03:19:43 UTC") + (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv") + (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server") + (0.01 ":irc.foonet.org 005 tester KICKLEN=390 MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8ONLY WHOX :are supported by this server") + (0.03 ":irc.foonet.org 005 tester draft/CHATHISTORY=25 :are supported by this server") + (0.00 ":irc.foonet.org 251 tester :There are 0 users and 4 invisible on 1 server(s)") + (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online") + (0.00 ":irc.foonet.org 253 tester 0 :unregistered connections") + (0.00 ":irc.foonet.org 254 tester 2 :channels formed") + (0.00 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers") + (0.00 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4") + (0.03 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4") + (0.00 ":irc.foonet.org 422 tester :MOTD File is missing") + (0.00 ":irc.foonet.org 221 tester +i") + (0.00 ":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.")) + +((mode-user 10 "MODE tester +i") + (0.00 ":irc.foonet.org 221 tester +i")) + +((join 10 "JOIN #chan") + (0.00 ":tester!~u@pzawpqpfiqshn.irc JOIN #chan") + (0.03 ":irc.foonet.org 353 tester = #chan :@fsbot bob alice tester") + (0.03 ":irc.foonet.org 366 tester #chan :End of NAMES list") + (0.00 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :tester, welcome!") + (0.01 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :tester, welcome!") + (0.01 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :alice: Then the liars and swearers are fools, for there are liars and swearers enow to beat the honest men, and hang up them.")) + +((mode-chan 10 "MODE #chan") + (0.00 ":irc.foonet.org 324 tester #chan +Cnt") + (0.01 ":irc.foonet.org 329 tester #chan 1738379988") + (0.07 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.") + (0.05 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :alice: Is't come to this, i' faith ? Hath not the world one man but he will wear his cap with suspicion ? Shall I never see a bachelor of three-score again ? Go to, i' faith; an thou wilt needs thrust thy neck into a yoke, wear the print of it, and sigh away Sundays. Look! Don Pedro is returned to seek you.") + (0.05 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :There's the fool hangs on your back already.") + (0.07 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :bob: Here is a purr of Fortune's, sir, or of Fortune's cat but not a musk-cat that has fallen into the unclean fishpond of her displeasure, and, as he says, is muddied withal. Pray you, sir, use the carp as you may, for he looks like a poor, decayed, ingenious, foolish, rascally knave. I do pity his distress in my similes of comfort, and leave him to your lordship.")) diff --git a/test/lisp/erc/resources/settings/basic.eld b/test/lisp/erc/resources/settings/basic.eld new file mode 100644 index 00000000000..e6a54f1fd38 --- /dev/null +++ b/test/lisp/erc/resources/settings/basic.eld @@ -0,0 +1,38 @@ +;; -*- mode: lisp-data; -*- +((nick 10 "NICK tester")) +((user 10 "USER tester 0 * :tester") + (0.00 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester") + (0.01 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version ergo-v2.11.1") + (0.01 ":irc.barnet.org 003 tester :This server was created Sat, 01 Feb 2025 03:19:43 UTC") + (0.00 ":irc.barnet.org 004 tester irc.barnet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv") + (0.00 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server") + (0.01 ":irc.barnet.org 005 tester KICKLEN=390 MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8ONLY WHOX :are supported by this server") + (0.03 ":irc.barnet.org 005 tester draft/CHATHISTORY=25 :are supported by this server") + (0.00 ":irc.barnet.org 251 tester :There are 0 users and 4 invisible on 1 server(s)") + (0.00 ":irc.barnet.org 252 tester 0 :IRC Operators online") + (0.00 ":irc.barnet.org 253 tester 0 :unregistered connections") + (0.00 ":irc.barnet.org 254 tester 2 :channels formed") + (0.00 ":irc.barnet.org 255 tester :I have 4 clients and 0 servers") + (0.00 ":irc.barnet.org 265 tester 4 4 :Current local users 4, max 4") + (0.03 ":irc.barnet.org 266 tester 4 4 :Current global users 4, max 4") + (0.00 ":irc.barnet.org 422 tester :MOTD File is missing") + (0.00 ":irc.barnet.org 221 tester +i") + (0.00 ":irc.barnet.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.")) + +((mode-user 10 "MODE tester +i") + (0.00 ":irc.barnet.org 221 tester +i")) + +((join 10 "JOIN #spam") + (0.00 ":tester!~u@pzawpqpfiqshn.irc JOIN #spam") + (0.03 ":irc.barnet.org 353 tester = #spam :@fsbot bob alice tester") + (0.03 ":irc.barnet.org 366 tester #spam :End of NAMES list") + (0.00 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :tester, welcome!") + (0.01 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :tester, welcome!") + (0.07 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :Prithee, unpin me,have grace and favour in them.")) + +((mode-chan 10 "MODE #spam") + (0.00 ":irc.barnet.org 324 tester #spam +Cnt") + (0.01 ":irc.barnet.org 329 tester #spam 1738379988") + (0.04 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :alice: Ay, like a black dog, as the saying is.") + (0.07 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG tester :My hand to thee; mine honour on my promise.") + (0.02 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :bob: She is Lavinia, therefore must be lov'd.")) -- 2.48.1 --=-=-=-- From debbugs-submit-bounces@debbugs.gnu.org Tue Feb 18 23:00:13 2025 Received: (at 76019) by debbugs.gnu.org; 19 Feb 2025 04:00:14 +0000 Received: from localhost ([127.0.0.1]:38849 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1tkbFf-00046F-2R for submit@debbugs.gnu.org; Tue, 18 Feb 2025 23:00:13 -0500 Received: from mail-108-mta16.mxroute.com ([136.175.108.16]:32959) by debbugs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.84_2) (envelope-from ) id 1tkbFX-00045N-3D for 76019@debbugs.gnu.org; Tue, 18 Feb 2025 22:59:56 -0500 Received: from filter006.mxroute.com ([136.175.111.3] filter006.mxroute.com) (Authenticated sender: mN4UYu2MZsgR) by mail-108-mta16.mxroute.com (ZoneMTA) with ESMTPSA id 1951c5ca22c000310e.001 for <76019@debbugs.gnu.org> (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Wed, 19 Feb 2025 03:59:47 +0000 X-Zone-Loop: 7886ee869e212cd688ac91e27ca355c8099ba6c1b89e X-Originating-IP: [136.175.111.3] DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=neverwas.me ; s=x; h=Content-Type:MIME-Version:Message-ID:Date:References:In-Reply-To: Subject:Cc:To:From:Sender:Reply-To:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=+lsKTcimLzIi5bo0lAV4L0xOTJWzUqnQvp23du7uGXU=; b=K8Pd6L8b6AfcAIyktRww6aEwXg /eyDQ8XRoBrE6J+IRKUtSKuy3bfBFwgRoDeTp3n8g2F1NHhyCSc4tEMuMuwun+ivPVTQD9CeIZLqD 7S96TnvkpnnZOmCa90DQvS9YwCxWsxESbbHvzJXLV3mgCHCAjYYHBsirbaEWGr6hE587yO5Ja0v51 ZMFlYyVHuAEoZarAeAnyvfepq2a2Rky9WBMu1c4FZ/4zy1PeK9acRnKDWWe3et+HGwJdWoz5XW67A RDlxSYQvK2HuVtmAwFvh/Hk+s8mafU0kGSv9ondvllN+kI9KHcKZP6JfDCMtmGFVF6LIsr/5UZDYp Pzb6wfvA==; From: "J.P." To: 76019@debbugs.gnu.org Subject: Re: bug#76019: 31.0.50; ERC 5.7: A 'settings' module for locally scoped configuration In-Reply-To: <87y0yn6hxe.fsf@neverwas.me> References: <87y0yn6hxe.fsf@neverwas.me> Date: Tue, 18 Feb 2025 19:59:43 -0800 Message-ID: <87bjuyk2j4.fsf@neverwas.me> User-Agent: Gnus/5.13 (Gnus v5.13) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Authenticated-Id: masked@neverwas.me X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 76019 Cc: emacs-erc@gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) --=-=-= Content-Type: text/plain v2. Remove new option `erc-client-certificate'. Change parameters of function `erc-settings--set-modules'. --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0000-v1-v2.diff >From 576012bb779a0cf1571080a7d20256a7c29470d6 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Tue, 18 Feb 2025 19:54:19 -0800 Subject: [PATCH 0/3] *** NOT A PATCH *** *** BLURB HERE *** F. Jason Park (3): [5.7] Split ERC module documentation into subnodes [5.7] Set major-mode before updating modules in erc-open [5.7] Make erc-settings a module doc/misc/erc.texi | 401 +++++++++++++++++- etc/ERC-NEWS | 25 ++ lisp/erc/erc-button.el | 4 +- lisp/erc/erc-common.el | 16 + lisp/erc/erc-goodies.el | 14 +- lisp/erc/erc-imenu.el | 8 +- lisp/erc/erc-networks.el | 50 +-- lisp/erc/erc-settings.el | 298 +++++++++++++ lisp/erc/erc-stamp.el | 8 +- lisp/erc/erc-track.el | 4 +- lisp/erc/erc-truncate.el | 4 +- lisp/erc/erc.el | 15 +- test/lisp/erc/erc-scenarios-settings.el | 190 +++++++++ test/lisp/erc/erc-settings-tests.el | 120 ++++++ test/lisp/erc/erc-tests.el | 2 +- .../erc/resources/settings/basic-sasl.eld | 47 ++ test/lisp/erc/resources/settings/basic.eld | 38 ++ 17 files changed, 1165 insertions(+), 79 deletions(-) create mode 100644 lisp/erc/erc-settings.el create mode 100644 test/lisp/erc/erc-scenarios-settings.el create mode 100644 test/lisp/erc/erc-settings-tests.el create mode 100644 test/lisp/erc/resources/settings/basic-sasl.eld create mode 100644 test/lisp/erc/resources/settings/basic.eld Interdiff: diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 9a35cddc9f3..13694228319 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -75,8 +75,14 @@ Top * Sample Session:: Example of connecting to the @samp{#emacs} channel * Special Features:: Differences from standalone IRC clients -Modules -* Settings:: Per-buffer configuration. +Module Usage + +* Settings: Settings Module. Per-buffer configuration. + +Advanced Module Topics + +* Scope: Module Scope. Differences between module types. +* Loading: Module Loading. How ERC loads modules. Advanced Usage @@ -547,7 +553,7 @@ Modules @cindex modules, settings @item settings -Apply options locally per-buffer +Apply options locally per-buffer (@pxref{Settings Module}) @cindex modules, smiley @item smiley @@ -620,9 +626,13 @@ Modules At present, the only such module is @code{networks}, whose library ERC always loads anyway. +@c Advanced module topics and individual module usage. + @anchor{Local Modules} -@subheading Local Modules +@node Module Scope +@section Scope @cindex local modules +@cindex module scope @c Earlier language in code comments, commit messages, and tracker @c discussions used to describe a local module as being "active" in a @@ -703,10 +713,8 @@ Modules unlike global toggles, none of these ever mutates @code{erc-modules}. -@c FIXME add section to Advanced chapter for creating modules, and -@c move this there. -@anchor{Module Loading} -@subheading Loading +@node Module Loading +@section Loading @cindex module loading ERC loads internal modules in alphabetical order and third-party @@ -761,9 +769,15 @@ Modules to the user-provided free-form area. @c PRE5_4: Document every option of every module in its own subnode +@node Module Usage +@section Usage +@cindex individual modules +@cindex module usage + +Detailed documentation on individual built-in modules. -@node Settings -@section Settings +@node Settings Module +@subsection Settings @cindex settings This global module lets you apply ERC options on a per-buffer basis, as @@ -964,7 +978,7 @@ Settings (erc-header-line-format nil)) ;; Pre-connect and server-buffer config for @samp{OFTC}. - ("oftc.net:" + ("oftc\\.net:" (erc-prompt "OFTC!") (erc-modules `(nicks ,@@erc-modules) :eval)))) @end lisp @@ -1029,7 +1043,7 @@ Settings (erc-nick "jrh") (erc-email-userid "jrh") (erc-user-full-name "J. Random Hacker") - (erc-client-certificate t) + (network-stream-use-client-certificates t) (erc-modules `(services-regain ,@@erc-modules) :eval) (erc-prompt "jrh@@OFTC!")))) @@ -1046,11 +1060,11 @@ Settings @end lisp @noindent -With a setup like the above, you can connect explicitly to a predefined -session by supplying an identifying symbol, like @code{lib-jrh}, along -with the @var{ID} keyword parameter to the entry-point command -@code{erc-tls}. The main gotcha when using these identifiers is that a -regexp targeting a dialed @acronym{TCP} address, like +With a setup like the above, you can connect to a predefined session by +supplying an identifying symbol, like @code{lib-jrh}, along with the +@var{ID} keyword parameter to the entry-point command @code{erc-tls}. +The main gotcha when using these identifiers is that a regexp targeting +a dialed @acronym{TCP} address, like @code{"\\`irc[.]libera[.]chat:6697\\'"}, won't work for conditions like the first one above because server buffers receive permanent names immediately upon creation, long before connecting. Instead, you'll want diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index 4aaa92e5e71..90036b9eb37 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -20,10 +20,6 @@ Born from the ashes of a PoC drafted nearly a quarter century ago, the options to various "scopes" of interest based on match conditions reminiscent of 'buffer-match-p'. -** Specify a TLS client certificate globally via user option. -Option 'erc-client-certificate' allows you to do just that. It may also -be helpful with some usage patterns involving the new 'settings' module. - ** Changes in the library API. *** Major-mode setup runs before global-module setup in 'erc-open'. diff --git a/lisp/erc/erc-settings.el b/lisp/erc/erc-settings.el index c912d805dea..14294801b0c 100644 --- a/lisp/erc/erc-settings.el +++ b/lisp/erc/erc-settings.el @@ -176,11 +176,12 @@ erc-settings-bind (erc-settings--set (erc-settings--gather-bindings (current-buffer)))) ;;;###autoload -(defun erc-settings--set-modules (target id calling-buffer) - "Set `erc-modules' if `erc-settings' has a binding for current buffer. +(defun erc-settings--set-modules (id target target-server-buffer) + "Set `erc-modules' locally if an entry exists for the current buffer. In server buffers, bind all matching entries, not just `erc-modules'. -Borrow state from CALLING-BUFFER for matching purposes. Expect TARGET -to be a string or nil and ID to be a symbol." +Expect TARGET to be a string or nil and ID to be a symbol. When TARGET +is non-nil, borrow local values from TARGET-SERVER-BUFFER to temporarily +bind identifying variables sought by match conditions." (when (and erc-settings (or erc-settings-mode (memq 'settings erc-modules))) ;; Shadow `erc-networks--id' for (id . ), `erc--target' for ;; (target . ), etc. Although predicates of interest, like @@ -190,17 +191,19 @@ erc-settings--set-modules (cl-assert (not (local-variable-p 'erc-networks--id))) (cl-assert (not (local-variable-p 'erc-network))) (cl-assert (not (local-variable-p 'erc--target))) - (let ((erc-networks--id (or (and id (erc-networks--id-create id)) - (buffer-local-value 'erc-networks--id - calling-buffer)))) + (let ((erc-networks--id + (or (and id (erc-networks--id-create id)) + (and target (buffer-local-value 'erc-networks--id + target-server-buffer))))) (if target (if-let* - ((erc-network (buffer-local-value 'erc-network calling-buffer)) + ((erc-network (buffer-local-value 'erc-network + target-server-buffer)) (erc--target (erc--target-from-string target)) (bindings (erc-settings--gather-bindings (current-buffer))) (entry (assq 'erc-modules bindings))) (erc-settings--set-value 'erc-modules (nth 1 entry) (cddr entry)) - (when-let* ((old-value (with-current-buffer calling-buffer + (when-let* ((old-value (with-current-buffer target-server-buffer (and (local-variable-p 'erc-modules) erc-modules)))) (setq-local erc-modules old-value))) diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index d99f52e4a4f..3e3de13114d 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1092,16 +1092,6 @@ erc-email-userid :group 'erc :type 'string) -(defcustom erc-client-certificate nil - "Client certificate for TLS connections. -Files must be abolute paths." - :package-version '(ERC . "5.7") ; FIXME sync on release - :group 'erc - :type '(choice (const :tag "Don't supply a client-cert" nil) - (const :tag "Defer to auth-source" t) - (list :tag "Key and cert files" - (file :tag "Key") (file :tag "Cert")))) - (defcustom erc-system-name nil "Use this as the name of your system. If nil, ERC will call function `system-name' to get this information." @@ -2464,10 +2454,9 @@ erc--updating-modules-p anything about the dependency's implementation.") (defvar erc--set-modules-functions nil - "Abnormal hook that runs before updating modules on major-mode. -Calls members with the TARGET and ID parameters of `erc-open' along with -the buffer from which `erc-open' was invoked, which is always the server -buffer for new targets and reconnecting servers.") + "Abnormal hook run before updating modules on major-mode init. +Calls members with ID and TARGET parameters of `erc-open', both possibly +nil, along with a non-nil TARGET's server buffer when applicable.") (defvar erc--setup-buffer-hook '(erc--warn-about-aberrant-modules) "Internal hook for module setup involving windows and frames.") @@ -2620,7 +2609,8 @@ erc-open (set-buffer buffer) (setq old-point (point)) (delay-mode-hooks (erc-mode)) - (run-hook-with-args 'erc--set-modules-functions channel id old-buffer) + (run-hook-with-args 'erc--set-modules-functions id channel + (and channel old-buffer)) (setq delayed-modules (erc--merge-local-modes (let ((erc--updating-modules-p t)) (erc--update-modules @@ -2668,8 +2658,7 @@ erc-open ;; The local copy of `erc-nick' - the list of nicks to choose (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick))) ;; client certificate (only useful if connecting over TLS) - (setq erc-session-client-certificate (or client-certificate - erc-client-certificate)) + (setq erc-session-client-certificate client-certificate) (setq erc-networks--id (if connect (or (and erc--server-reconnecting -- 2.48.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-5.7-Split-ERC-module-documentation-into-subnodes.patch >From dcfae5be13d048b4166dcbad1ec880e39cd1436c Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 10 Feb 2025 04:01:52 -0800 Subject: [PATCH 1/3] [5.7] Split ERC module documentation into subnodes * doc/misc/erc.texi: Add "Modules" section to the main detailed menu. (Modules): Promote "Local Modules" and "Module Loading" subheadings to sections and proper nodes. Rename "Local Modules" to "Module Scope" but retain anchor for compatibility. --- doc/misc/erc.texi | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 1c0afa3b300..818693db2f8 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -75,6 +75,11 @@ Top * Sample Session:: Example of connecting to the @samp{#emacs} channel * Special Features:: Differences from standalone IRC clients +Advanced Module Topics + +* Scope: Module Scope. Differences between module types. +* Loading: Module Loading. How ERC loads modules. + Advanced Usage * Connecting:: Ways of connecting to an IRC server. @@ -614,9 +619,13 @@ Modules At present, the only such module is @code{networks}, whose library ERC always loads anyway. +@c Advanced module topics and individual module usage. + @anchor{Local Modules} -@subheading Local Modules +@node Module Scope +@section Scope @cindex local modules +@cindex module scope @c Earlier language in code comments, commit messages, and tracker @c discussions used to describe a local module as being "active" in a @@ -697,10 +706,8 @@ Modules unlike global toggles, none of these ever mutates @code{erc-modules}. -@c FIXME add section to Advanced chapter for creating modules, and -@c move this there. -@anchor{Module Loading} -@subheading Loading +@node Module Loading +@section Loading @cindex module loading ERC loads internal modules in alphabetical order and third-party -- 2.48.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0002-5.7-Set-major-mode-before-updating-modules-in-erc-op.patch >From f044a4db684a78e805424f0009a90c536861733a Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sat, 1 Feb 2025 16:59:51 -0800 Subject: [PATCH 2/3] [5.7] Set major-mode before updating modules in erc-open * lisp/erc/erc-button.el (erc-button-mode, erc-button-enable) (erc-button-disable): Use `erc-with-initialized-session' instead of `erc-mode-hook'. * lisp/erc/erc-common.el (erc-with-initialized-session): New macro. * lisp/erc/erc-goodies.el (erc-scrolltobottom-mode) (erc-scrolltobottom-enable, erc-scrolltobottom-disable) (erc-move-to-prompt-mode, erc-move-to-prompt-enable) (erc-move-to-prompt-disable): Use `erc-with-initialized-session' instead of `erc-mode-hook'. * lisp/erc/erc-imenu.el (erc-imenu-mode, erc-imenu-enable) (erc-imenu-disable): Use macro instead of `erc-mode-hook'. * lisp/erc/erc-stamp.el (erc-stamp-mode, erc-stamp-enable) (erc-stamp-disable): Use macro instead of `erc-mode-hook'. * lisp/erc/erc-track.el (erc-track-mode, erc-track-enable) (erc-track-disable): Use macro instead of `erc-mode-hook'. * lisp/erc/erc-truncate.el (erc-truncate-mode, erc-truncate-enable) (erc-truncate-disable): Use macro instead of `erc-mode-hook'. * lisp/erc/erc.el (erc--set-modules-functions): New variable, an internal hook to modify erc-modules membership. (erc-open): Set major mode before updating modules. (Bug#76019) --- lisp/erc/erc-button.el | 4 +--- lisp/erc/erc-common.el | 16 ++++++++++++++++ lisp/erc/erc-goodies.el | 14 +++++--------- lisp/erc/erc-imenu.el | 8 +++----- lisp/erc/erc-stamp.el | 8 +++----- lisp/erc/erc-track.el | 4 +--- lisp/erc/erc-truncate.el | 4 +--- lisp/erc/erc.el | 10 ++++++++-- 8 files changed, 38 insertions(+), 30 deletions(-) diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el index e6ed0e2f75f..ec5a012b8c9 100644 --- a/lisp/erc/erc-button.el +++ b/lisp/erc/erc-button.el @@ -54,13 +54,11 @@ button "This mode buttonizes all messages according to `erc-button-alist'." ((add-hook 'erc-insert-modify-hook #'erc-button-add-buttons 30) (add-hook 'erc-send-modify-hook #'erc-button-add-buttons 30) - (add-hook 'erc-mode-hook #'erc-button-setup 91) - (unless erc--updating-modules-p (erc-buffer-do #'erc-button-setup)) + (erc-with-initialized-session (erc-button-setup)) (add-hook 'erc--tab-functions #'erc-button-next) (erc--modify-local-map t "" #'erc-button-previous)) ((remove-hook 'erc-insert-modify-hook #'erc-button-add-buttons) (remove-hook 'erc-send-modify-hook #'erc-button-add-buttons) - (remove-hook 'erc-mode-hook #'erc-button-setup) (remove-hook 'erc--tab-functions #'erc-button-next) (erc--modify-local-map nil "" #'erc-button-previous))) diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el index d293e6ba878..776407b0e8d 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el @@ -434,6 +434,22 @@ define-erc-module (put ',enable 'definition-name ',name) (put ',disable 'definition-name ',name)))) +(defmacro erc-with-initialized-session (&rest body) + "Run BODY in all ERC buffers if outside `erc-open' and soon otherwise. +When inside `erc-open', run BODY after session variables have been +initialzied and after all `erc-mode-hook' members but before any +`after-change-major-mode-hook' members. Expect caller to know this is +only useful in global-module setup and that they're still responsible +for teardown, which is often done with `erc-buffer-do' or similar." + (let ((fn (make-symbol "fn")) + (hook-var (make-symbol "hook-var"))) + `(let ((,fn (lambda () ,@body))) + (if erc--updating-modules-p + (let ((,hook-var (gensym "erc--oneoff-major-mode-hook-"))) + (set ,hook-var ,fn) + (push ,hook-var delayed-mode-hooks)) + (erc-buffer-do ,fn))))) + (defmacro erc-with-buffer (spec &rest body) "Execute BODY in the buffer associated with SPEC. diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el index 24a3205e0cd..133d2943c84 100644 --- a/lisp/erc/erc-goodies.el +++ b/lisp/erc/erc-goodies.el @@ -79,12 +79,11 @@ erc-scrolltobottom-all ;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t) (define-erc-module scrolltobottom nil "This mode causes the prompt to stay at the end of the window." - ((add-hook 'erc-mode-hook #'erc--scrolltobottom-setup) - (when (and erc-scrolltobottom-all (< emacs-major-version 28)) + ((when (and erc-scrolltobottom-all (< emacs-major-version 28)) (erc-button--display-error-notice-with-keys "Option `erc-scrolltobottom-all' requires Emacs 28+. Disabling.") (setq erc-scrolltobottom-all nil)) - (unless erc--updating-modules-p (erc-buffer-do #'erc--scrolltobottom-setup)) + (erc-with-initialized-session (erc--scrolltobottom-setup)) (if erc-scrolltobottom-all (progn (add-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert 25) @@ -92,8 +91,7 @@ scrolltobottom (add-hook 'erc-insert-done-hook #'erc--scrolltobottom-all) (add-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)) (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom))) - ((remove-hook 'erc-mode-hook #'erc--scrolltobottom-setup) - (erc-buffer-do #'erc--scrolltobottom-setup) + ((erc-buffer-do #'erc--scrolltobottom-setup) (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert) (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all) (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all) @@ -253,10 +251,8 @@ erc-make-read-only ;;;###autoload(autoload 'erc-move-to-prompt-mode "erc-goodies" nil t) (define-erc-module move-to-prompt nil "This mode causes the point to be moved to the prompt when typing text." - ((add-hook 'erc-mode-hook #'erc-move-to-prompt-setup) - (unless erc--updating-modules-p (erc-buffer-do #'erc-move-to-prompt-setup))) - ((remove-hook 'erc-mode-hook #'erc-move-to-prompt-setup) - (dolist (buffer (erc-buffer-list)) + ((erc-with-initialized-session (erc-move-to-prompt-setup))) + ((dolist (buffer (erc-buffer-list)) (with-current-buffer buffer (remove-hook 'pre-command-hook #'erc-move-to-prompt t))))) diff --git a/lisp/erc/erc-imenu.el b/lisp/erc/erc-imenu.el index 2e244fa157e..6919e02bc8b 100644 --- a/lisp/erc/erc-imenu.el +++ b/lisp/erc/erc-imenu.el @@ -135,11 +135,9 @@ erc-imenu--create-index-function ;;;###autoload(autoload 'erc-imenu-mode "erc-imenu" nil t) (define-erc-module imenu nil "Simple Imenu integration for ERC." - ((add-hook 'erc-mode-hook #'erc-imenu-setup) - (add-hook 'which-function-mode-hook #'erc-imenu--disable-which-func) - (unless erc--updating-modules-p (erc-buffer-do #'erc-imenu-setup))) - ((remove-hook 'erc-mode-hook #'erc-imenu-setup) - (remove-hook 'which-function-mode-hook #'erc-imenu--disable-which-func) + ((add-hook 'which-function-mode-hook #'erc-imenu--disable-which-func) + (erc-with-initialized-session (erc-imenu-setup))) + ((remove-hook 'which-function-mode-hook #'erc-imenu--disable-which-func) (erc-buffer-do #'erc-imenu-setup))) (defun erc-imenu-setup () diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el index 5b6550c4a88..79fd4ecaacd 100644 --- a/lisp/erc/erc-stamp.el +++ b/lisp/erc/erc-stamp.el @@ -178,13 +178,11 @@ erc-timestamp-face ;;;###autoload(autoload 'erc-timestamp-mode "erc-stamp" nil t) (define-erc-module stamp timestamp "This mode timestamps messages in the channel buffers." - ((add-hook 'erc-mode-hook #'erc-stamp--setup) - (add-hook 'erc-insert-modify-hook #'erc-add-timestamp 70) + ((add-hook 'erc-insert-modify-hook #'erc-add-timestamp 70) (add-hook 'erc-send-modify-hook #'erc-add-timestamp 70) (add-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect) - (unless erc--updating-modules-p (erc-buffer-do #'erc-stamp--setup))) - ((remove-hook 'erc-mode-hook #'erc-stamp--setup) - (remove-hook 'erc-insert-modify-hook #'erc-add-timestamp) + (erc-with-initialized-session (erc-stamp--setup))) + ((remove-hook 'erc-insert-modify-hook #'erc-add-timestamp) (remove-hook 'erc-send-modify-hook #'erc-add-timestamp) (remove-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect) (erc-buffer-do #'erc-stamp--setup))) diff --git a/lisp/erc/erc-track.el b/lisp/erc/erc-track.el index 2897c3ede93..7c3523670ad 100644 --- a/lisp/erc/erc-track.el +++ b/lisp/erc/erc-track.el @@ -607,8 +607,7 @@ track ;; enable the tracking keybindings (add-hook 'erc-connect-pre-hook #'erc-track-minor-mode-maybe) (erc-track-minor-mode-maybe)) - (add-hook 'erc-mode-hook #'erc-track--setup) - (unless erc--updating-modules-p (erc-buffer-do #'erc-track--setup)) + (erc-with-initialized-session (erc-track--setup)) (add-hook 'erc-networks--copy-server-buffer-functions #'erc-track--replace-killed-buffer)) ;; Disable: @@ -630,7 +629,6 @@ track (remove-hook 'erc-connect-pre-hook #'erc-track-minor-mode-maybe) (when erc-track-minor-mode (erc-track-minor-mode -1))) - (remove-hook 'erc-mode-hook #'erc-track--setup) (erc-buffer-do #'erc-track--setup) (remove-hook 'erc-networks--copy-server-buffer-functions #'erc-track--replace-killed-buffer))) diff --git a/lisp/erc/erc-truncate.el b/lisp/erc/erc-truncate.el index f5404a15b71..7b458b6164a 100644 --- a/lisp/erc/erc-truncate.el +++ b/lisp/erc/erc-truncate.el @@ -67,12 +67,10 @@ truncate ;;enable ((add-hook 'erc-insert-done-hook #'erc-truncate-buffer) (add-hook 'erc-connect-pre-hook #'erc-truncate--warn-about-logging) - (add-hook 'erc-mode-hook #'erc-truncate--setup) - (unless erc--updating-modules-p (erc-buffer-do #'erc-truncate--setup))) + (erc-with-initialized-session (erc-truncate--setup))) ;; disable ((remove-hook 'erc-insert-done-hook #'erc-truncate-buffer) (remove-hook 'erc-connect-pre-hook #'erc-truncate--warn-about-logging) - (remove-hook 'erc-mode-hook #'erc-truncate--setup) (erc-buffer-do #'erc-truncate--setup))) (defvar-local erc-truncate--buffer-size nil diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index e9e643a3df7..4e4ea378cd3 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -2448,6 +2448,11 @@ erc--updating-modules-p confidently call (erc-foo-mode 1) without having to learn anything about the dependency's implementation.") +(defvar erc--set-modules-functions nil + "Abnormal hook run before updating modules on major-mode init. +Calls members with ID and TARGET parameters of `erc-open', both possibly +nil, along with a non-nil TARGET's server buffer when applicable.") + (defvar erc--setup-buffer-hook '(erc--warn-about-aberrant-modules) "Internal hook for module setup involving windows and frames.") @@ -2598,6 +2603,9 @@ erc-open (when connect (run-hook-with-args 'erc-before-connect server port nick)) (set-buffer buffer) (setq old-point (point)) + (delay-mode-hooks (erc-mode)) + (run-hook-with-args 'erc--set-modules-functions id channel + (and channel old-buffer)) (setq delayed-modules (erc--merge-local-modes (let ((erc--updating-modules-p t)) (erc--update-modules @@ -2605,8 +2613,6 @@ erc-open (or erc--server-reconnecting erc--target-priors))) - (delay-mode-hooks (erc-mode)) - (setq erc-server-reconnect-count old-recon-count) (when (setq erc-server-connected (not connect)) -- 2.48.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0003-5.7-Make-erc-settings-a-module.patch >From 576012bb779a0cf1571080a7d20256a7c29470d6 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sun, 17 Nov 2024 16:32:25 -0800 Subject: [PATCH 3/3] [5.7] Make erc-settings a module * doc/misc/erc.texi: Add Settings node under Modules. * etc/ERC-NEWS: Mention new module along with function `erc-channel-buffer-p. * lisp/erc/erc-networks.el (erc-settings): Move to new module `erc-settings'. (erc-get): Neuter completely by effectively aliasing to `ignore'. * lisp/erc/erc-settings.el: New file. * lisp/erc/erc.el (erc-channel-buffer-p): New function. (erc-modules): Add `settings' available set for custom-type. * test/lisp/erc/erc-scenarios-settings.el: New file. * test/lisp/erc/erc-settings-tests.el: New file. * test/lisp/erc/erc-tests.el (erc-tests--modules): Add `settings'. * test/lisp/erc/resources/settings/basic-sasl.eld: New file. * test/lisp/erc/resources/settings/basic.eld: New file. (Bug#76019) --- doc/misc/erc.texi | 384 +++++++++++++++++- etc/ERC-NEWS | 25 ++ lisp/erc/erc-networks.el | 50 +-- lisp/erc/erc-settings.el | 298 ++++++++++++++ lisp/erc/erc.el | 5 + test/lisp/erc/erc-scenarios-settings.el | 190 +++++++++ test/lisp/erc/erc-settings-tests.el | 120 ++++++ test/lisp/erc/erc-tests.el | 2 +- .../erc/resources/settings/basic-sasl.eld | 47 +++ test/lisp/erc/resources/settings/basic.eld | 38 ++ 10 files changed, 1115 insertions(+), 44 deletions(-) create mode 100644 lisp/erc/erc-settings.el create mode 100644 test/lisp/erc/erc-scenarios-settings.el create mode 100644 test/lisp/erc/erc-settings-tests.el create mode 100644 test/lisp/erc/resources/settings/basic-sasl.eld create mode 100644 test/lisp/erc/resources/settings/basic.eld diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 818693db2f8..13694228319 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -75,6 +75,10 @@ Top * Sample Session:: Example of connecting to the @samp{#emacs} channel * Special Features:: Differences from standalone IRC clients +Module Usage + +* Settings: Settings Module. Per-buffer configuration. + Advanced Module Topics * Scope: Module Scope. Differences between module types. @@ -140,7 +144,6 @@ Introduction @node Getting Started @chapter Getting Started -@cindex settings The command @kbd{M-x erc} will start ERC and prompt for the server to connect to. If you're unsure of which server or network to connect @@ -548,6 +551,10 @@ Modules @item services Identify to Nickserv (IRC Services) automatically +@cindex modules, settings +@item settings +Apply options locally per-buffer (@pxref{Settings Module}) + @cindex modules, smiley @item smiley Convert smileys to pretty icons @@ -762,6 +769,376 @@ Module Loading to the user-provided free-form area. @c PRE5_4: Document every option of every module in its own subnode +@node Module Usage +@section Usage +@cindex individual modules +@cindex module usage + +Detailed documentation on individual built-in modules. + +@node Settings Module +@subsection Settings +@cindex settings + +This global module lets you apply ERC options on a per-buffer basis, as +local variable bindings (@pxref{Buffer-Local Variables,,, elisp,}). It +consists of a single @dfn{meta option}: + +@defopt erc-settings +An alist where each key is a @dfn{match condition} and each value a list +of bindings that loosely resembles a @code{let}-style @var{varlist}. +The first element of each binding is the variable of a user option and +the second an arbitrary sexp assigned to it locally in all matching +buffers. You can tell ERC to evaluate a given sexp as a Lisp form +before assigning it by including a trailing third element: the +@dfn{modifier flag} @code{:eval}. + +A match condition specifies when and whether ERC should apply each group +of bindings. These resemble @code{buffer-match-p} conditions, with two +major differences. The first is that predicate conditions don't take +any arguments. Instead, ERC runs them with the candidate buffer +current. The second difference is that the assortment of associative +cons-cell conditions is completely new. Gone are @code{major-mode}, +@code{category}, etc. In their place, ERC offers the following: + +@itemize +@item @code{(network . @samp{})} +@item @code{(id . @samp{})} +@item @code{(name . @samp{})} +@item @code{(target . @samp{})} +@end itemize + +The associated @code{cdr} values for the @code{network} and @code{id} +conditions are symbols identifying the current IRC network and the +@dfn{network context}, respectively. When first connecting, a +@code{network} condition is always @code{nil}, but an @code{id} +condition can be non-@code{nil} if you supply a matching @var{ID} +argument to an entry-point command, like @code{erc-tls} (@pxref{Network +Identifier}). + +Additionally, ERC also recognizes @code{buffer-match-p}'s boolean +operations and constants: + +@itemize +@item @code{(and @samp{})} +@item @code{(or @samp{})} +@item @code{(not @samp{})} +@item @code{t} +@item @code{nil} +@end itemize + +Also valid are regular expressions and nullary predicates: + +@itemize +@item @code{""} +@item @code{} +@end itemize + +@end defopt + +@noindent +Here's a contrived @code{erc-settings} value containing six entries, +the first of which has two assignments: + +@lisp +(((and (network . Libera.Chat) ; buffers #emacs, + "\\`#emacs") ; #emacs-foo, etc., + (my-own-variable 42) ; on Libera.Chat only + (erc-foo "")) + + ((or (target . "#chan") ; buffers #chan, bob, + (target . "bob")) ; chan@@Libera.Chat, + (erc-foo (erc-current-nick) :eval)) ; bob@@OFTC, etc. + + (erc-open-server-buffer-p (erc-bar 1)) ; connected server buffers + (erc-query-buffer-p (erc-bar 2)) ; query buffers only + (erc-channel-buffer-p (erc-bar 3)) ; channel buffers only + (erc-target (erc-bar 4)) ; query or channel buffers + (t (erc-bar 5))) ; any `erc-mode' buffer +@end lisp + +@noindent +Unfortunately, predicates too dissimilar from those shown above may not +work in all matching contexts due to the interaction between this module +and ERC's rather convoluted major-mode setup. While the only way to +know for sure is to try them, selecting them becomes much easier if you +can gain a sense for when exactly @samp{settings} attempts to create +bindings: + +@itemize +@item +In a server buffer, binding happens +@enumerate +@item +before ERC enables any modules or sets any local variables +@item +after establishing a logical IRC connection and learning what network +it's connected to +@end enumerate + +@item +In a target buffer, binding happens +@enumerate +@item +before enabling any modules, but only if you're overriding +@code{erc-modules}, and only then, for that option alone +@item +after ERC has set most of its default local variables and run +global-module setup but before running major-mode hooks or local-module +setup +@end enumerate +@end itemize + +The main downside of this staggered approach is that @samp{settings} +skips any bindings for variables that already have a local binding +rather than do the intuitive thing and simply clobber them. The reason +for this is to retain a consistent state between a global module's minor +mode setup and any local-module setup or major-mode hooks. They must +all witness the same value for the same variable. + +Revisiting the quasi-example above with this precedence quirk in mind, +notice that the made-up variable @code{erc-bar} will be @code{2} in +query buffers, @code{3} in channel buffers, and @code{5} in server +buffers. It's never @code{1} because the first binding opportunity +occurs before connecting, when @code{erc-open-server-buffer-p} returns +@code{nil}. It's likewise never @code{4} because that binding appears +lower in the list than those for @code{2} and @code{3}. For more on +this behavior, see the very last example below. + +When it comes to binding the option @code{erc-modules}, @samp{settings} +must make special accommodations. Getting it right may take some trial +and error, and it's only guaranteed to work for local modules because +most global ones share state among all sessions. However, it's fine to +include global modules in an @code{erc-modules} binding as long as they +also appear in the @code{default-value}. To set @code{erc-modules} for +a specific server buffer, specify an entry like + +@lisp +("[.]libera[.]chat:" + (erc-modules (seq-union '(sasl keep-place-indicator) erc-modules) + :eval) + (erc-sasl-user :nick)) +@end lisp + +@noindent +Here, the match condition is a regexp matching a dialed @acronym{TCP} +address because server buffers typically have these as temporary names +while connecting. As mentioned, ERC later renames them to reflect a +discovered network, after which @samp{settings} binds any matching +unbound options. + +A session's target buffers normally share the @code{erc-modules} value +bound locally in their server buffer. To specify a separate, overriding +set of modules for certain target buffers, use a condition that only +applies to them: + +@lisp +((and (network . Libera.Chat) erc-channel-buffer-p) + (erc-modules (seq-union '(fill-wrap) erc-modules) :eval)) +@end lisp + +@noindent +This is often unnecessary because local modules that only operate in one +kind of buffer tend to disable themselves elsewhere. And some, like +@samp{keep-place-indicator}, offer an option to restrict activation +based on buffer type. + +@anchor{Settings Examples} +@subsection Settings Examples +@cindex settings, examples + +A typical setup might include configuration for two networks, one that +uses @acronym{SASL} to authenticate and another that uses a server +password. + +@lisp +(setopt erc-modules + `(settings ,@@erc-modules) + + erc-settings + ;; Pre-connect and server buffer config for @samp{Libera.Chat}. + '(("libera\\.chat:" + (erc-prompt "Libera>") + (erc-autojoin-channels-alist ((Libera.Chat "##jrh"))) + (erc-header-line-format "%n on %S (%m, %l)") + (erc-sasl-user :nick) + (erc-modules `(sasl ,@@erc-modules) :eval)) + + ;; Channel buffer config for @samp{Libera.Chat} + ((and (network . Libera.Chat) erc-channel-buffer-p) + (erc-modules `(keep-place-indicator ,@@erc-modules) :eval) + (erc-prompt erc-prompt-format) + (erc-prompt-format "%c%b>") + (erc-header-line-format "%t: %o")) + + ;; Query buffer configuration for any network. + (erc-query-buffer-p + (erc-prompt erc-prompt-format) + (erc-prompt-format "%S>") + (erc-header-line-format nil)) + + ;; Pre-connect and server-buffer config for @samp{OFTC}. + ("oftc\\.net:" + (erc-prompt "OFTC!") + (erc-modules `(nicks ,@@erc-modules) :eval)))) +@end lisp + +Note that the pre-connection config described in the last entry can't +use the @dfn{global module} @samp{services} because it's incompatible +with the @dfn{local module} @samp{sasl}. In this scenario, instead of a +server password, the @samp{OFTC} connection could alternatively use +@dfn{CertFP} via auth-source (see next example). + +@anchor{ID-based Settings Example} +@subsubsection ID-based +@cindex settings, examples, id-based + +New users coming from traditional IRC clients often express a desire to +store connection details alongside other per-network or per-connection +configuration. Although ERC was designed to keep these concerns +separate, the @samp{settings} module can emulate that experience to some +degree. + +@lisp +(setopt erc-modules + `(settings ,@@erc-modules) + + erc-settings + ;; Connection details common to both @samp{Libera.Chat} IDs. + '(((and (or (id . lib-jrh) (id . lib-jrbot)) + (not erc-server-process-alive)) + (erc-server "irc.libera.chat") + (erc-port 6697) + (erc-modules `(sasl ,@@erc-modules) :eval)) + + ;; Connection details for account @samp{jrh}. + ((and (id . lib-jrh) (not erc-server-process-alive)) + (erc-nick "jrh") + (erc-user-full-name "J. Random Hacker") + (erc-email-userid "jrh") + (erc-autojoin-channels-alist ((lib-jrh "##jrh")))) + + ;; Connection details for account @samp{jrbot}. + ((and (id . lib-jrbot) (not erc-server-process-alive)) + (erc-nick "jrbot") + (erc-email-userid "jrbot") + (erc-user-full-name "J.R. Bot (jrh's bot)") + (erc-sasl-auth-source-function erc-auth-source-function) + (erc-autojoin-channels-alist ((lib-jrbot "##jrh" "##jrbot")))) + + ;; Non-connection opts for both @samp{Libera.Chat} sessions. + ((and (network . Libera.Chat) erc-open-server-buffer-p) + (erc-prompt "libera>") + (erc-header-line-format "%n on %S (%m, %l)")) + ((and (network . Libera.Chat) erc-channel-buffer-p) + (erc-modules `(keep-place-indicator ,@@erc-modules) :eval) + (erc-prompt erc-prompt-format) + (erc-prompt-format "%c%b>") + (erc-header-line-format "%t: %o")) + + ;; Server details for @samp{OFTC}. + ((and (id . oftc) (target . nil)) + (erc-server "irc.oftc.net") + (erc-port 6697) + (erc-nick "jrh") + (erc-email-userid "jrh") + (erc-user-full-name "J. Random Hacker") + (network-stream-use-client-certificates t) + (erc-modules `(services-regain ,@@erc-modules) :eval) + (erc-prompt "jrh@@OFTC!")))) + +(defun my-erc-up (id) + "Connect to preconfigured session identified by ID." + (interactive "SNet id: ") + (erc-tls :server nil + :port nil + :nick nil + :user nil + :password nil + :full-name nil + :id network-id)) +@end lisp + +@noindent +With a setup like the above, you can connect to a predefined session by +supplying an identifying symbol, like @code{lib-jrh}, along with the +@var{ID} keyword parameter to the entry-point command @code{erc-tls}. +The main gotcha when using these identifiers is that a regexp targeting +a dialed @acronym{TCP} address, like +@code{"\\`irc[.]libera[.]chat:6697\\'"}, won't work for conditions like +the first one above because server buffers receive permanent names +immediately upon creation, long before connecting. Instead, you'll want +to specify a chosen identifier via the @samp{(id . )} pattern +and likely qualify it with another subcondition, like @code{(not +erc-server-process-alive)}, to ensure that the @code{erc-modules} +binding only matches the server buffer and not any target buffers. +Equivalent qualifying subconditions would be @code{(not erc-target)} and +@code{(target . nil)}. + +Regarding authentication, notice that the @samp{lib-jrbot} connection +retrieves its @acronym{SASL} credentials, which might be a client +certificate, via auth-source, while session @samp{lib-jrh} prompts you +interactively for a password. (@xref{SASL, SASL in ERC}.) The +@samp{oftc} connection also uses auth-source to supply its credentials, +this time for non-@acronym{SASL} client-cert (@dfn{CertFP}) +authentication. Lastly, in the scenario above, a user would need to run +@kbd{M-x my-erc-up @key{RET}} instead of @kbd{M-x erc @key{RET}} when +connecting to IRC. For convenience, ERC provides a similar command: + +@defun erc-settings-connect-by-id network-id +Prompt the user and provide completion for some @samp{(id . )} +appearing in their @code{erc-settings}, and specify it as the argument +to the @var{ID} keyword parameter when invoking @code{erc-tls}. +@end defun + +With both traditional and ID-based setups, it's often more convenient to +define less exclusive, more ``overlapping'' match conditions: + +@lisp +(use-package erc-settings + :custom + (erc-settings + '(((not erc-server-process-alive) ; common to all servers + (erc-nick "jrh") + (erc-port 6697) + (erc-email-userid "jrh") + (erc-user-full-name "J. Random Hacker")) + + (erc-channel-buffer-p ; all channels + (erc-modules `(keep-place-indicator ,@@erc-modules) :eval)) + + ((id . id-libera) ; Libera.Chat only, including targets + (erc-server "irc.libera.chat") + (erc-sasl-password "changeme") + (erc-modules `(sasl ,@@erc-modules) :eval) + (erc-prompt erc-prompt-format) + (erc-autojoin-channels-alist ((id-foonet "#erc")))) + + ((id . id-oftc) ; OFTC only, including targets + (erc-server "irc.oftc.net") + (erc-modules `(fill-wrap ,@@erc-modules) :eval) + (erc-autojoin-channels-alist ((id-barnet "##jrh"))))))) +@end lisp + +If you go this route, know that @samp{settings} will often create local +bindings in buffers where they're not applicable. In the above example, +buffer @file{#erc} on @samp{Libera.Chat} will have @code{erc-server} and +@code{erc-sasl-password} bound locally for no reason. In most cases, +this is perfectly harmless. + +A final thing to notice about the example above is that the +@code{erc-modules} binding in the second group, the one with match +condition @code{erc-channel-buffer-p}, effectively @dfn{shadow}s that +same variable in all remaining settings affecting channel buffers. If +it instead appeared at the end of the list, ERC would skip its +@code{erc-modules} binding because the variable would already be locally +bound. But, as is, both @file{##jrh} on @samp{OFTC} and @file{#erc} on +@samp{Libera.Chat} activate @samp{keep-place-indicator} but not +@samp{fll-wrap}. Server buffer @file{id-oftc} has only @samp{fll-wrap}, +and server buffer @file{id-libera} has neither. And, if you were to +receive a query from @samp{alice} on @samp{OFTC}, buffer @file{alice} +would only have @samp{fill-wrap} but not @samp{keep-place-indicator} +because it's a non-channel on @samp{OFTC}. @node Advanced Usage @@ -1262,6 +1639,11 @@ SASL @item Multiple networks +If you use the @samp{settings} module, introduced in ERC 5.7, please +refer to that section's guide for a more modern, config-based approach +to specifying buffer-local @acronym{SASL} options (@pxref{Settings +Examples}). + @example # ~/.authinfo.gpg diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index dd960994b4f..90036b9eb37 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -11,6 +11,31 @@ This file is about changes in ERC, the powerful, modular, and extensible IRC (Internet Relay Chat) client distributed with GNU Emacs since Emacs version 22.1. + +* Changes in ERC 5.7 + +** Apply options granularly to specific connections or buffers. +Born from the ashes of a PoC drafted nearly a quarter century ago, the +'settings' module offers a simple means of applying arbitrary user +options to various "scopes" of interest based on match conditions +reminiscent of 'buffer-match-p'. + +** Changes in the library API. + +*** Major-mode setup runs before global-module setup in 'erc-open'. +The "enable" body of a global module's minor-mode command will now see +'erc-mode' as the 'major-mode'. Although ERC still delays its +major-mode hook until after it has initialized its main session +variables, a module can also now set any of its own variables that don't +depend on such initialization immediately and without fear of their +being killed. For modules still needing to run delayed, session-aware +code, a new convenience macro called 'erc-with-initialized-session' is +now available to automate any 'erc-mode-hook' wrangling for you. + +*** A predicate to detect if the current buffer is a channel. +New function 'erc-channel-buffer-p' is a channel-detecting complement to +'erc-query-buffer-p', and a light wrapper around 'erc-channel-p'. + * Changes in ERC 5.6.1 diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el index 289b6f8bd8f..a783cb7cbdb 100644 --- a/lisp/erc/erc-networks.el +++ b/lisp/erc/erc-networks.el @@ -1609,28 +1609,12 @@ erc-networks--server-select (scheme (if (cdr pspec) "ircs" "irc"))) (if ports (format "%s://%s:%d" scheme host (seq-random-elt ports)) host))) -;;; The following experimental -;; It does not work yet, help me with it if you -;; think it is worth the effort. - -(defvar 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") +;; Longtime occupant of this file `erc-settings' was repurposed in ERC +;; 5.7 and moved to erc-settings.el. +;; Attempting to provide a "working" version of this function would be +;; nonsensical because `erc-settings' now has a completely different +;; layout than the unfinished version from `erc-networks' (circa 2002). (defun erc-get (var &optional net target) "Retrieve configuration values from `erc-settings'. @@ -1639,27 +1623,9 @@ erc-get 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 - (setq elt (car items) - items (cdr items)) - (when (eq (car elt) var) - (cond ((and net target (listp (nth 1 elt)) - (eq net (car (nth 1 elt))) - (string-equal target (nth 1 (nth 1 elt)))) - (setq val (nth 2 elt) - items nil)) - ((and net (eq net (nth 1 elt))) - (setq val (nth 2 elt) - items nil)) - ((and (not net) (not target) (not (nth 1 elt))) - (setq val (nth 2 elt) - items nil))))) - val)) - -;; (erc-get 'pals 'Libera.Chat) + (declare (obsolete "`erc-settings' schema incompatible with PoC from 2002" + "31.1")) + (ignore var net target)) (provide 'erc-networks) diff --git a/lisp/erc/erc-settings.el b/lisp/erc/erc-settings.el new file mode 100644 index 00000000000..14294801b0c --- /dev/null +++ b/lisp/erc/erc-settings.el @@ -0,0 +1,298 @@ +;;; erc-settings.el -- Buffer-local options -*- lexical-binding: t; -*- + +;; Copyright (C) 2025 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published +;; by the Free Software Foundation, either version 3 of the License, +;; or (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; This module is based on an ancient proof-of-concept by Mario Lang, +;; with contributions from Diane Murray and possibly others. It lived +;; in erc-networks.el for over two decades before being repurposed as a +;; module in 2025. + +;; Usage: add `settings' to `erc-modules', and customize the option +;; `erc-settings'. + +;;; Code: +(require 'erc) + +(defgroup erc-settings nil + "Buffer-local values for arbitrary user options." + :group 'erc) + +(define-widget 'erc--buffer-match 'lazy + "Condition for `erc-settings--buffer-match-p'." + :tag "Valid `erc-settings' match condition" + :type `(choice (boolean :tag "Always/never") + (regexp :tag "Buffer-name regexp") + (function :tag "Predicate") + (cons :tag "Network" (const network) symbol) + (cons :tag "ID" (const id) symbol) + (cons :tag "Target" (const target) (choice string + (const nil))) + (cons :tag "Buffer name" (const name) string) + (list :tag "Negation" + (const not) + (erc--buffer-match :tag "Match condition")) + (cons :tag "All/any" + (choice :tag "Operator" (const or) (const and)) + (repeat :tag "Match condition" erc--buffer-match)))) + +(defcustom erc-settings () + "A match-condition alist for setting ERC's user options locally. + +This \"meta option\" provides a means of specifying buffer-local values +for other user options. It does this by setting an option's variable +locally in server and target buffers. If a local binding already exists +for a variable, ERC leaves it alone. Alist members should be of the +form (MATCHCOND . BINDINGS), where BINDINGS is a list of assignments +like (VAR VAL . FLAGS). + +MATCHCOND works like a `buffer-match-p' condition, only tailored +slightly to meet ERC's unique requirements. The boolean operators +`and', `or', and `not' remain unchanged, as do strings, which ERC treats +as regexps to be matched against a buffer's name. For servers, this +name is usually a dialed TCP address while connecting and a network (ID) +once connected. Query and channel buffers normally share their target's +name, possibly suffixed by a disambiguating \"@\" + identifier. If +MATCHCOND is a function, ERC assumes it's a predicate that takes no +arguments and runs in the candidate ERC buffer. As for key-value +cons-cell conditions, ERC ignores all traditional ones defined by +`buffer-match-p', like `derived-mode' and `major-mode', instead +preferring to define its own: (network . NETWORK), where NETWORK is a +symbol returned by the function `erc-network'; (id . ID), where ID is a +user-provided symbol as described by the Info node `(erc) Network +Identifier'; (target . TARGET), where TARGET, if non-nil, is the channel +name or query nick; and (name . NAME), which matches a buffer's NAME, as +a string. + +In BINDINGS, each member's VAR is the symbol of a user option or +variable to which ERC assigns the sexp VAL locally in the current +buffer. When FLAGS contains the keyword :eval, ERC evaluates VAL as a +lisp form and assigns the result to VAR. Each entry in BINDINGS should +appear in order of decreasing match specificity and increasing +generality because even though ERC applies all matching entries, it +skips subsequent ones for which a local binding already exists. + +See Info node `(erc) Settings' for a friendlier description of the DSL +and Info node `(erc) Settings Examples' for example usage." + :package-version '(ERC . "5.7") ; FIXME sync on release + :type (let* ((flags '(set :tag "Flags" + (const :tag "Prefer custom-set (advanced)" :custom) + (const :tag "Evaluate form" :eval))) + (setting `(cons :tag "Binding assignment" + (variable :tag "Option/variable") + (cons :tag "Value" sexp ,flags)))) + `(alist :key-type erc--buffer-match :value-type (repeat ,setting)))) + +;;;###autoload(autoload 'erc-settings-mode "erc-settings" nil t) +(define-erc-module settings nil + "Global module to set ERC options locally per-buffer. +Like many modules, this cannot reasonably be toggled via mode command +mid-session. To see the result of updates to the option `erc-settings' +immediately, destroy and reopen all affected sessions." + ((when (and erc--updating-modules-p + (not (memq #'erc-settings--set-modules + (default-value 'erc--set-modules-functions)))) + (erc--warn-once-before-connect 'erc-settings-mode + "Module `settings' did not load properly. Add it to `erc-modules'" + " and run `erc-update-modules' before connecting, or use a" + " Custom-aware configuration method, like `setopt'.")) + ;; Set non-`erc-module' entries in target buffers after all default + ;; non-connection related local session variables have been set. + (if erc--updating-modules-p + (add-hook 'change-major-mode-after-body-hook + #'erc-settings--setup -90 t) + (erc-buffer-do #'erc-settings--setup)) + ;; Set any remaining entries in server buffers once the network is + ;; known and all local session variables have been populated. + (add-hook 'erc-after-connect #'erc-settings-bind -30) + (add-hook 'erc--set-modules-functions #'erc-settings--set-modules 30)) + ((erc-buffer-do #'erc-settings--setup) + (remove-hook 'erc-after-connect #'erc-settings-bind) + (remove-hook 'erc--set-modules-functions #'erc-settings--set-modules))) + +(defun erc-settings--setup () + (if erc-settings-mode + (when erc-server-connected + (erc-settings--set (erc-settings--gather-bindings (current-buffer)))) + (remove-hook 'change-major-mode-after-body-hook #'erc-settings--setup t) + (dolist (entry erc-settings) + (pcase-dolist (`(,var . ,_) (cdr entry)) + (when (local-variable-p var) + (kill-local-variable var)))))) + +(defun erc-settings--buffer-match-p (condition) + "Return non-nil if CONDITION matches current buffer. +Act almost like `buffer-match-p', except recognize an alternate set of +property-based cons-cell conditions. Additionally, don't pass any +arguments to predicate-type conditions, and interpret CONDITION in the +current buffer, where CONDITION should among the following: + +- the symbol t, which always matches, +- the symbol nil, which never matches, +- a regular expression matched against current buffer's name, +- a predicate taking no arguments and run in the candidate buffer +- a cons-cell with a... + + ...car like and that matches if its cdr is + ----------- ------------------------------ + `id' `eq' to current buffer's network context ID + `network' `eq' to current buffer's `erc-network' + `target' `equal' to current buffer's `erc-target' + `name' `equal' to current buffer's name + `and' a list of matching conditions + `or' a list containing at least one matching condition + `not' a list of a single condition that matches when negated" + (pcase condition + ('t t) + ((pred stringp) (string-match-p condition (buffer-name))) + ((pred functionp) (funcall condition)) + (`(id . ,id) (and erc-networks--id + (eq (erc-networks--id-symbol erc-networks--id) id))) + (`(network . ,network) (and erc-network (eq erc-network network))) + (`(target . ,target) (equal target (erc-target))) + (`(name . ,name) (equal name (buffer-name))) + (`(not . ,cond) (not (erc-settings--buffer-match-p cond))) + (`(or . ,args) (seq-some #'erc-settings--buffer-match-p args)) + (`(and . ,args) (seq-every-p #'erc-settings--buffer-match-p args)))) + +(defun erc-settings-bind (&rest _) + "Set all settings in option `erc-settings' from current network's ID." + (erc-settings--set (erc-settings--gather-bindings (current-buffer)))) + +;;;###autoload +(defun erc-settings--set-modules (id target target-server-buffer) + "Set `erc-modules' locally if an entry exists for the current buffer. +In server buffers, bind all matching entries, not just `erc-modules'. +Expect TARGET to be a string or nil and ID to be a symbol. When TARGET +is non-nil, borrow local values from TARGET-SERVER-BUFFER to temporarily +bind identifying variables sought by match conditions." + (when (and erc-settings (or erc-settings-mode (memq 'settings erc-modules))) + ;; Shadow `erc-networks--id' for (id . ), `erc--target' for + ;; (target . ), etc. Although predicates of interest, like + ;; `erc-query-buffer-p', may call on `buffer-local-value', its + ;; `buffer' argument will be the current buffer, which doesn't yet + ;; have any buffer-local bindings. + (cl-assert (not (local-variable-p 'erc-networks--id))) + (cl-assert (not (local-variable-p 'erc-network))) + (cl-assert (not (local-variable-p 'erc--target))) + (let ((erc-networks--id + (or (and id (erc-networks--id-create id)) + (and target (buffer-local-value 'erc-networks--id + target-server-buffer))))) + (if target + (if-let* + ((erc-network (buffer-local-value 'erc-network + target-server-buffer)) + (erc--target (erc--target-from-string target)) + (bindings (erc-settings--gather-bindings (current-buffer))) + (entry (assq 'erc-modules bindings))) + (erc-settings--set-value 'erc-modules (nth 1 entry) (cddr entry)) + (when-let* ((old-value (with-current-buffer target-server-buffer + (and (local-variable-p 'erc-modules) + erc-modules)))) + (setq-local erc-modules old-value))) + (erc-settings-bind))))) + +(defun erc-settings--gather-bindings (buffer) + "Return a list of matching bindings for server BUFFER's session." + (with-current-buffer buffer + (mapcan (lambda (entry) + (and (erc-settings--buffer-match-p (car entry)) + (copy-sequence (cdr entry)))) + erc-settings))) + +;; Currently, when a binding contains the experimental flag :custom, ERC +;; tries to use VAR's `custom-set' function, if defined, for setting its +;; value. ERC doesn't do so by default because many such functions use +;; `set-default', which defeats the purpose. +(defun erc-settings--set-value (var value flags) + "Set VAR to VALUE locally and interpret FLAGS. +Do nothing if VAR already has a local binding. If VAR has the symbol +property `erc-settings--wrap', assume it's a function that takes the +arguments (SETTER VAR VALUE), and defer to it to perform the actual +setting." + (unless (local-variable-p var) + (let ((setter #'set) + (evalp nil) + (customp nil)) + (dolist (flag flags) + (pcase-exhaustive flag + (:eval (setq evalp t)) + (:custom (setq customp t)))) + (make-local-variable var) + (when evalp + (setq value (eval value t))) + (when customp + (custom-load-symbol var) + (setq setter (or (get var 'custom-set) #'set))) + (if-let* ((xsetter (get var 'erc-settings--wrap))) + (funcall xsetter setter var value) + (funcall setter var value))))) + +(defun erc-settings--set (bindings) + "Set all matching bindings locally in current buffer. +Expect BINDINGS to be a list of (KEY VALUE . FLAGS)." + (pcase-dolist (`(,var ,value . ,flags) bindings) + (erc-settings--set-value var value flags))) + +(defun erc-settings--ensure-prompt-reset (setter var value) + (cl-assert (eq var 'erc-prompt)) + (when (and erc-input-marker (stringp value)) + (let ((erc-prompt (lambda () value))) + (with-silent-modifications + (erc--refresh-prompt)))) + (funcall setter var value)) + +(put 'erc-prompt 'erc-settings--wrap #'erc-settings--ensure-prompt-reset) + +(defun erc-settings--find-condition (predicate settings) + "Return all conditions in SETTINGS for which PREDICATE returns non-nil." + (letrec ((found ()) + (find (lambda (condition) + (pcase condition + (`(and . ,rest) (mapc find rest)) + (`(or . ,rest) (mapc find rest)) + (`(not ,cond) (funcall find cond)) + ((pred (funcall predicate)) (push condition found)))))) + (dolist (setting settings) + (funcall find (car setting))) + (nreverse found))) + +(defun erc-settings--extract-ids () + "Return all arguments to an `id' property condition in `erc-settings'." + (mapcar #'cdr (erc-settings--find-condition + (lambda (condition) (eq (car-safe condition) 'id)) + erc-settings))) + +;;;###autoload +(defun erc-settings-connect-by-id (network-id) + "Connect via some preconfigured network ID specified in `erc-settings'. +See Info node `(erc) ID-based Settings Example'." + (interactive (list (completing-read "Network ID: " + (erc-settings--extract-ids)))) + (erc-tls :server nil :port nil :nick nil :user nil + :password nil :full-name nil :id network-id)) + +(provide 'erc-settings) + +;;; erc-settings.el ends here +;; +;; Local Variables: +;; generated-autoload-file: "erc-loaddefs.el" +;; End: diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 4e4ea378cd3..3e3de13114d 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1967,6 +1967,10 @@ erc-channel-p (target (buffer-local-value 'erc--target target)) ((erc--target-channel-p target))))))) +(defun erc-channel-buffer-p (&optional buffer) + "Call `erc-channel-p' with BUFFER or the current buffer." + (erc-channel-p (or buffer (current-buffer)))) + ;; For the sake of compatibility, a historical quirk concerning this ;; option, when nil, has been preserved: all buffers are suffixed with ;; the original dialed host name, which is usually something like @@ -2361,6 +2365,7 @@ erc-modules scrolltobottom) (const :tag "services: Identify to Nickserv (IRC Services) automatically" services) + (const :tag "settings: Set ERC user options buffer locally" settings) (const :tag "smiley: Convert smileys to pretty icons" smiley) (const :tag "sound: Play sounds when you receive CTCP SOUND requests" sound) diff --git a/test/lisp/erc/erc-scenarios-settings.el b/test/lisp/erc/erc-scenarios-settings.el new file mode 100644 index 00000000000..c1197d3fb99 --- /dev/null +++ b/test/lisp/erc/erc-scenarios-settings.el @@ -0,0 +1,190 @@ +;;; erc-scenarios-settings.el --- erc-settings tests -*- lexical-binding: t -*- + +;; Copyright (C) 2025 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Code: + +(require 'ert-x) +(eval-and-compile + (let ((load-path (cons (ert-resource-directory) load-path))) + (require 'erc-scenarios-common))) + +(require 'erc-settings) + +(ert-deftest erc-scenarios-settings/sasl () + :tags '(:expensive-test) + (erc-scenarios-common-with-cleanup + ((erc-server-flood-penalty 0.1) + (erc-scenarios-common-dialog "settings") + (server-foonet (erc-d-run "localhost" t "server-foonet" 'basic-sasl)) + (server-barnet (erc-d-run "localhost" t "server-barnet" 'basic)) + (port-foonet (process-contact server-foonet :service)) + (port-barnet (process-contact server-barnet :service)) + (erc-modules `(settings ,@erc-modules)) + (erc-settings + `(((and ,(rx-to-string `(: ":" ,(number-to-string port-foonet) eot) t) + (not erc-server-process-alive)) + (erc-modules `(sasl ,@erc-modules) :eval)) + ((and ,(rx-to-string `(: ":" ,(number-to-string port-barnet) eot) t) + (not erc-server-process-alive)) + (erc-modules `(fill-wrap ,@erc-modules) :eval) + (erc-autojoin-channels-alist (("." "#spam")))) + ((and (network . foonet) erc-open-server-buffer-p) + (erc-prompt "server!!!") + (erc-autojoin-channels-alist ((foonet "#chan")))) + ((and (network . foonet) erc-channel-buffer-p) + (erc-modules `(keep-place-indicator ,@erc-modules) :eval)))) + (expect (erc-d-t-make-expecter))) + + ;; Simulate `setopt', `custom-set-variables', etc. + (erc-update-modules) + (with-current-buffer (erc :server "127.0.0.1" + :port port-foonet + :nick "tester" + :user "tester" + :password "changeme" + :full-name "tester") + (funcall expect 10 "This server is in debug mode")) + (with-current-buffer (erc :server "127.0.0.1" + :port port-barnet + :nick "tester" + :user "tester" + :full-name "tester") + (funcall expect 10 "This server is in debug mode")) + + ;; Sentinel variables for modules we're enabling locally. + (defvar erc-fill--wrap-last-msg) + (defvar erc--keep-place-indicator-overlay) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan")) + (funcall expect 10 " bob: Grows, lives, and dies") + (should erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + ;; Targets that don't specify their own local modules inherit any + ;; from their server buffer (if `erc-modules' is locally bound). + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam")) + (funcall expect 10 " alice: Ay, like a") + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + ;; Point in fact: `fill-wrap' is enabled in this query buffer. + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "alice")) + (funcall expect 10 " My hand to thee") + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "foonet")) + (should-not erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "server!!!")) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "barnet")) + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + (erc-settings-mode -1))) + +(ert-deftest erc-scenarios-settings/sasl/id () + :tags '(:expensive-test) + (erc-scenarios-common-with-cleanup + ((erc-server-flood-penalty 0.1) + (erc-scenarios-common-dialog "settings") + (server-foonet (erc-d-run "localhost" t "server-foonet" 'basic-sasl)) + (server-barnet (erc-d-run "localhost" t "server-barnet" 'basic)) + (port-foonet (process-contact server-foonet :service)) + (port-barnet (process-contact server-barnet :service)) + (erc-modules `(settings ,@erc-modules)) + (erc-settings + `(((not erc-server-process-alive) ; common to both servers + (erc-server "127.0.0.1") + (erc-nick "tester") + (erc-email-userid "tester") + (erc-user-full-name "tester")) + ;; If this were moved to the end of the list (try it), ERC + ;; would skip it because (id . x) matches in channels too. + (erc-channel-buffer-p ; all channels + (erc-modules `(keep-place-indicator ,@erc-modules) :eval)) + ((id . id-foonet) ; foonet only (channels too) + (erc-port ,port-foonet) + (erc-sasl-password "changeme") + (erc-modules `(sasl ,@erc-modules) :eval) + (erc-prompt "wee!!!") + (erc-autojoin-channels-alist ((id-foonet "#chan")))) + ((id . id-barnet) ; barnet only (channels too) + (erc-port ,port-barnet) + (erc-modules `(fill-wrap ,@erc-modules) :eval) + (erc-autojoin-channels-alist ((id-barnet "#spam")))))) + (expect (erc-d-t-make-expecter))) + + ;; Simulate `setopt', `custom-set-variables', etc. + (erc-update-modules) + + ;; Mimic `erc-settings-connect-by-id', except don't use TLS. + (with-current-buffer (erc :server nil :port nil :nick nil :user nil + :password nil :full-name nil :id 'id-foonet) + (should (equal (buffer-name) "id-foonet")) + (funcall expect 10 "This server is in debug mode")) + (with-current-buffer (erc :server nil :port nil :nick nil :user nil + :password nil :full-name nil :id 'id-barnet) + (should (equal (buffer-name) "id-barnet")) + (funcall expect 10 "This server is in debug mode")) + + ;; Sentinel variables for modules we're enabling locally. + (defvar erc-fill--wrap-last-msg) + (defvar erc--keep-place-indicator-overlay) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan")) + (funcall expect 10 " bob: Grows, lives, and dies") + (should erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "wee!!!")) + + ;; Channel-only module overrides were applied correctly. + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam")) + (funcall expect 10 " alice: Ay, like a") + (should erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + ;; Module `fill-wrap' is active in this query buffer because it has + ;; no query-specific overrides for `erc-modules'. It instead + ;; inherits the local value from the server buffer. + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "alice")) + (funcall expect 10 " My hand to thee") + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + (with-current-buffer "id-foonet" + (should-not erc--keep-place-indicator-overlay) + (should-not erc-fill--wrap-last-msg) + (funcall expect 10 "wee!!!")) + + (with-current-buffer "id-barnet" + (should-not erc--keep-place-indicator-overlay) + (should erc-fill--wrap-last-msg) + (funcall expect 10 "ERC>")) + + (erc-settings-mode -1))) + +;;; erc-scenarios-settings.el ends here diff --git a/test/lisp/erc/erc-settings-tests.el b/test/lisp/erc/erc-settings-tests.el new file mode 100644 index 00000000000..8563e3acbcb --- /dev/null +++ b/test/lisp/erc/erc-settings-tests.el @@ -0,0 +1,120 @@ +;;; erc-settings-tests.el --- Tests for erc-settings -*- lexical-binding:t -*- + +;; Copyright (C) 2024 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;;; Code: + +(require 'erc-settings) +(require 'ert-x) +(eval-and-compile + (let ((load-path (cons (ert-resource-directory) load-path))) + (require 'erc-tests-common))) + +(ert-deftest erc-settings () + (when (< emacs-major-version 29) (ert-skip "Needs `setopt'")) + + ;; Defers to `setopt' to perform some type checking. + (cl-letf* ((erc-settings ()) + ((symbol-function 'set-default) #'set) + ((symbol-function 'warn) #'error)) + + (setopt erc-settings `(((or (network . foonet) + ,(rx bot "127.0.0.1:")) + (erc-foo 1)) + ((and ,(rx bot "#emacs") + (not ,(rx bot "#emacs-devel"))) + (erc-foo 2) + (erc-bar 3)))) + + + ;; Realistic predicates and bindings. + (setopt erc-settings '(((and (id . foonet) + (not erc-server-process-alive)) + (erc-prompt "ERC! ") + (erc-sasl-user :nick)) + (erc-open-server-buffer-p + (erc-autojoin-channels-alist (("." "#chan")))) + ((or "#chan" "#spam" erc-query-buffer-p) + (erc-modules `(keep-place-indicator ,@erc-modules) + :eval)))) + + ;; Network can also be a regexp. + (setopt erc-settings `(((or ,(rx bot (| "localhost" "127.0.0.1") ":") + (network . foonet)) + (erc-foo 1)) + ((or ,(rx bot "#emacs") "#erc") + (erc-foo 2))))) + + (should (equal erc-settings ()))) + +(ert-deftest erc-settings--gather-bindings () + + ;; Match by network only. + (with-current-buffer (get-buffer-create "foonet") + (setq erc-network 'foonet) + (let ((erc-settings '(((or (network . barnet) ignore nil)) + ((and (network . foonet) always t) + (erc-foo 1) (erc-bar 2)) + ((network . baznet))))) + (should (equal '((erc-foo 1) (erc-bar 2)) + (erc-settings--gather-bindings (current-buffer))))) + (kill-buffer)) + + ;; Multiple regexps. + (with-current-buffer (get-buffer-create "localhost:6697") + (let ((erc-settings '(((network . barnet)) + ("\\`localhost" + (erc-foo 1)) + ("." + (erc-bar 2))))) + (should (equal '((erc-foo 1) (erc-bar 2)) + (erc-settings--gather-bindings (current-buffer)))) + (kill-buffer))) + + ;; One regexp, one network. + (with-current-buffer (get-buffer-create "foonet") + (setq erc-network 'foonet) + (let ((erc-settings `((,(rx (| "foonet" "barnet")) + (erc-foo 1)) + ((and (network . foonet) (name . "foonet")) + (erc-bar 2))))) + (should (equal '((erc-foo 1) (erc-bar 2)) + (erc-settings--gather-bindings (current-buffer))))) + (kill-buffer))) + +(ert-deftest erc-settings--extract-ids () + (should-not (erc-settings--extract-ids)) + + ;; Does not dedupe. + (let ((erc-settings '(((id . a) + (erc-foo 0)) + ("." + (erc-foo 1)) + ((and t nil) + (erc-foo 2)) + ((and ignore (id . b)) + (erc-foo 3)) + ((or (not (id . c)) (and (id . d) ignore)) + (erc-foo 4)) + ((not (id . a)) + (erc-foo 5))))) + (should (equal (erc-settings--extract-ids) '(a b c d a))))) + +;;; erc-settings-tests.el ends here diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index 2279496e600..3d9e2158166 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -3525,7 +3525,7 @@ erc-tests--modules command-indicator completion dcc fill identd imenu irccontrols keep-place list log match menu move-to-prompt netsplit networks nickbar nicks noncommands notifications notify page readonly - replace ring sasl scrolltobottom services smiley sound + replace ring sasl scrolltobottom services settings smiley sound spelling stamp track truncate unmorse xdcc)) ;; Ensure that `:initialize' doesn't change the ordering of the diff --git a/test/lisp/erc/resources/settings/basic-sasl.eld b/test/lisp/erc/resources/settings/basic-sasl.eld new file mode 100644 index 00000000000..33d57018bb7 --- /dev/null +++ b/test/lisp/erc/resources/settings/basic-sasl.eld @@ -0,0 +1,47 @@ +;; -*- mode: lisp-data; -*- +((cap 10 "CAP REQ :sasl")) +((nick 10 "NICK tester")) +((user 10 "USER tester 0 * :tester")) +((authenticate 10 "AUTHENTICATE PLAIN") + (0.00 ":irc.foonet.org CAP * ACK sasl") + (0.04 "AUTHENTICATE +")) +((authenticate 10 "AUTHENTICATE AHRlc3RlcgBjaGFuZ2VtZQ==") + (0.01 ":irc.foonet.org 900 * * tester :You are now logged in as tester") + (0.01 ":irc.foonet.org 903 * :Authentication successful")) +((cap 10 "CAP END") + (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester") + (0.01 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.11.1") + (0.01 ":irc.foonet.org 003 tester :This server was created Sat, 01 Feb 2025 03:19:43 UTC") + (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv") + (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server") + (0.01 ":irc.foonet.org 005 tester KICKLEN=390 MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8ONLY WHOX :are supported by this server") + (0.03 ":irc.foonet.org 005 tester draft/CHATHISTORY=25 :are supported by this server") + (0.00 ":irc.foonet.org 251 tester :There are 0 users and 4 invisible on 1 server(s)") + (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online") + (0.00 ":irc.foonet.org 253 tester 0 :unregistered connections") + (0.00 ":irc.foonet.org 254 tester 2 :channels formed") + (0.00 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers") + (0.00 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4") + (0.03 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4") + (0.00 ":irc.foonet.org 422 tester :MOTD File is missing") + (0.00 ":irc.foonet.org 221 tester +i") + (0.00 ":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.")) + +((mode-user 10 "MODE tester +i") + (0.00 ":irc.foonet.org 221 tester +i")) + +((join 10 "JOIN #chan") + (0.00 ":tester!~u@pzawpqpfiqshn.irc JOIN #chan") + (0.03 ":irc.foonet.org 353 tester = #chan :@fsbot bob alice tester") + (0.03 ":irc.foonet.org 366 tester #chan :End of NAMES list") + (0.00 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :tester, welcome!") + (0.01 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :tester, welcome!") + (0.01 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :alice: Then the liars and swearers are fools, for there are liars and swearers enow to beat the honest men, and hang up them.")) + +((mode-chan 10 "MODE #chan") + (0.00 ":irc.foonet.org 324 tester #chan +Cnt") + (0.01 ":irc.foonet.org 329 tester #chan 1738379988") + (0.07 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.") + (0.05 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :alice: Is't come to this, i' faith ? Hath not the world one man but he will wear his cap with suspicion ? Shall I never see a bachelor of three-score again ? Go to, i' faith; an thou wilt needs thrust thy neck into a yoke, wear the print of it, and sigh away Sundays. Look! Don Pedro is returned to seek you.") + (0.05 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :There's the fool hangs on your back already.") + (0.07 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #chan :bob: Here is a purr of Fortune's, sir, or of Fortune's cat but not a musk-cat that has fallen into the unclean fishpond of her displeasure, and, as he says, is muddied withal. Pray you, sir, use the carp as you may, for he looks like a poor, decayed, ingenious, foolish, rascally knave. I do pity his distress in my similes of comfort, and leave him to your lordship.")) diff --git a/test/lisp/erc/resources/settings/basic.eld b/test/lisp/erc/resources/settings/basic.eld new file mode 100644 index 00000000000..e6a54f1fd38 --- /dev/null +++ b/test/lisp/erc/resources/settings/basic.eld @@ -0,0 +1,38 @@ +;; -*- mode: lisp-data; -*- +((nick 10 "NICK tester")) +((user 10 "USER tester 0 * :tester") + (0.00 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester") + (0.01 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version ergo-v2.11.1") + (0.01 ":irc.barnet.org 003 tester :This server was created Sat, 01 Feb 2025 03:19:43 UTC") + (0.00 ":irc.barnet.org 004 tester irc.barnet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv") + (0.00 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server") + (0.01 ":irc.barnet.org 005 tester KICKLEN=390 MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8ONLY WHOX :are supported by this server") + (0.03 ":irc.barnet.org 005 tester draft/CHATHISTORY=25 :are supported by this server") + (0.00 ":irc.barnet.org 251 tester :There are 0 users and 4 invisible on 1 server(s)") + (0.00 ":irc.barnet.org 252 tester 0 :IRC Operators online") + (0.00 ":irc.barnet.org 253 tester 0 :unregistered connections") + (0.00 ":irc.barnet.org 254 tester 2 :channels formed") + (0.00 ":irc.barnet.org 255 tester :I have 4 clients and 0 servers") + (0.00 ":irc.barnet.org 265 tester 4 4 :Current local users 4, max 4") + (0.03 ":irc.barnet.org 266 tester 4 4 :Current global users 4, max 4") + (0.00 ":irc.barnet.org 422 tester :MOTD File is missing") + (0.00 ":irc.barnet.org 221 tester +i") + (0.00 ":irc.barnet.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.")) + +((mode-user 10 "MODE tester +i") + (0.00 ":irc.barnet.org 221 tester +i")) + +((join 10 "JOIN #spam") + (0.00 ":tester!~u@pzawpqpfiqshn.irc JOIN #spam") + (0.03 ":irc.barnet.org 353 tester = #spam :@fsbot bob alice tester") + (0.03 ":irc.barnet.org 366 tester #spam :End of NAMES list") + (0.00 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :tester, welcome!") + (0.01 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :tester, welcome!") + (0.07 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :Prithee, unpin me,have grace and favour in them.")) + +((mode-chan 10 "MODE #spam") + (0.00 ":irc.barnet.org 324 tester #spam +Cnt") + (0.01 ":irc.barnet.org 329 tester #spam 1738379988") + (0.04 ":bob!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :alice: Ay, like a black dog, as the saying is.") + (0.07 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG tester :My hand to thee; mine honour on my promise.") + (0.02 ":alice!~u@pgq8itg3w7n3a.irc PRIVMSG #spam :bob: She is Lavinia, therefore must be lov'd.")) -- 2.48.1 --=-=-=--