Re: bug#57955: 29.0.50; Allow session-local ERC modules
"J.P." <[email protected]> Tue, 18 Feb 2025 20:17:58 -0800
| Newsgroups | gmane.emacs.erc.general |
|---|---|
| Message-ID | <[email protected]> |
--=-=-= Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable "J.P." <[email protected]> writes: > So, in light of the new proposal for "scoped" configuration now > officially on the table [4], it might behoove us to just pretend the > granularity objective is henceforth solely the domain of that proposal's > bug (bug#76019). That'll allow us, here, in this bug, to focus entirely > on the second objective about persistence and to hopefully arrive at > something worthy of some finality for 5.7. To that end, here's some > related territory possibly worth exploring: > > 1. A public utility function to access the prior buffer's local > variables during reconnection > > 2. A managed facility for declaring arbitrary persisted data with > supporting CRUD operations >=20=20=20 > 3. Optional helpers for an option's :set function that update > persisted values in affected buffers or inform users to cycle the > mode or restart the session > > 4. Documenting differences in how a local module's mode command > variants behave with the various flavors of local modules, like > session-wide, target-only, etc. > > 5. An advanced tutorial on how to write a local module using only the > public API via a fully functional demo > > To get started, I've attached a PoC of a possible approach for point 2 > (the CRUD thing). It turns out my having explored the idea some has led > me to the opinion that it's probably better to stick to points 4 and 5 > only and to let module authors deal with the rest. Basically, I'm not > sure asking anyone to adopt yet another magical abstraction layer just > to persist state is any less mentally taxing than asking them to wrangle > it all themselves using lower level Emacs facilities, so long as we > provide clear guidelines and examples with any necessary boilerplate. Of > course, this observation disregards maintainability concerns, so we'd > need to be pretty certain all related infrastructure is mostly here to > stay (famous last words). More to come on this shortly. Here is an initial draft attempting to address points 4 and 5 in the list above (patch also attached): File: erc.info, Node: Module Example, Next: Module Usage, Prev: Module Loading, Up: Modules 4.3 Example =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D This is a walk-through of a working module presented in separate chunks. If you'd prefer to view it as a whole, you can install it as a third-party package through ERC's devel archive: <https://emacs-erc.gitlab.io/bugs/archive/erc-view.html>. ;;; erc-view.el -- Automatic view-mode for ERC -*- lexical-binding: = t; -*- ;; Maintainer: The ERC Maintainers <[email protected]> ;; Keywords: convenience ;; Version: 0.1 ;; Package-Requires: ((emacs "30.1")) ;; URL: https://gitlab.com/emacs-erc/erc-view ;;; Commentary: ;; This is a demo local module for ERC. It arranges for automatical= ly ;; enabling `view-mode' when leaving the prompt area and automatical= ly ;; disabling it when reentering. It also ensures `view-mode' stays ;; enabled or disabled when reconnecting. ;;; Code: You need to import ERC's main library somehow. The easiest way is directly, via a simple =E2=80=98(require 'erc)=E2=80=99, although this mo= dule does so indirectly because it also uses definitions from =E2=80=98erc-goodies=E2= =80=99: (require 'erc-goodies) (require 'view) Avoid headaches by aligning the name of your module with its containing library and Custom group. It's best to have one group and one module per library. (defgroup erc-view nil "Automatically enter and exit `view-mode' in ERC." :version "0.1" :group 'erc) (defcustom erc-view-enable-when-exiting-prompt t "Whether to enable `view-mode' when exiting the prompt area." :type 'boolean) (defcustom erc-view-disable-when-entering-prompt t "Whether to disable `view-mode' when entering the prompt area." :type 'boolean) (defcustom erc-view-backspace-at-prompt-scrolls-down t "Whether a \\`<backspace>' at the prompt scrolls down to enter `vi= ew-mode'." :type 'boolean) You'll almost always want to define internal variables as buffer-local. (defvar-local erc-view--enabled-p nil "Current reconnect-aware activation state of `view-mode'.") In some cases, you may need a variable's value to survive the reapplication of ERC's =E2=80=98major-mode=E2=80=99 performed in each rea= ssociated buffer upon reconnecting. Do this by leveraging the =E2=80=98permanent-l= ocal=E2=80=99 symbol property. (*note (elisp)Creating Buffer-Local::.) (put 'erc-view--enabled-p 'permanent-local t) There are a few caveats regarding the durability of permanent values. By convention, disabling a module's minor mode kills local bindings. Mode commands, like =E2=80=98erc-view-mode=E2=80=99, do so in the current= buffer only, while unidirectional ones, like =E2=80=98erc-view-mode-disable=E2=80=99, = do so connection-wide. There are also occasions in which persistence is undefined, most notably when =E2=80=9Cgrafting=E2=80=9D an old buffer's contents onto a c= urrent buffer. This occurs in server buffers upon =E2=80=9Clogical connection=E2=80=9D (= at =E2=80=98MOTD=E2=80=99's end), when a user reconnects with a new invocation of an entry-point command, like =E2=80=98erc-tls=E2=80=99, instead of via the auto-reconnec= t facility or by issuing a =E2=80=98/reconnect=E2=80=99 at the prompt. Unaffected are = entry-point invocations that include an =E2=80=98:id=E2=80=99 keyword because reassoc= iation happens immediately in such cases, before ERC even initializes any modules. Grafting can also happen in target buffers, most often after a user reconnects under a new nick and conducts business in the same channels and queries as before, only to renick _back_ to the previous nick via a =E2=80=98/nick oldme=E2=80=99 or similar. As of version 5.7, ERC retains= the current buffer's permanent value in all such situations, meaning ERC ignores permanent values from previous buffers and retains default values assigned during module initialization. Moving on, if your module needs to bind keys, define its keymap _before_ the module itself, and use the standard minor-mode naming convention of =E2=80=98erc-my-module-mode-map=E2=80=99. (defvar-keymap erc-view-mode-map :doc "Keymap for `view-mode' in ERC." "<remap> <delete-backward-char>" #'erc-view--enable-on-backspace) (defvar-keymap erc-view-mode-overriding-map :parent view-mode-map :doc "Overriding keymap for `view-mode' when `erc-view-mode' is ac= tive. Hitting \\`<RET>' atop a button prompts for an action by default. U= se \\`C-j' or \\`j' for scrolling up by a line." "C" nil ; View-kill-and-leave "E" #'erc-view--exit-to-bottom ; View-exit-and-edit "Q" nil ; View-quit-all "k" #'View-scroll-line-backward ; Vim backwards line "j" #'View-scroll-line-forward ; Vim forwards line "S-<return>" #'View-scroll-line-backward) You'll almost always want to define your module as buffer-local. Do this by including a =E2=80=98localp=E2=80=99 flag as the final parameter = to =E2=80=98define-erc-module=E2=80=99, after the =E2=80=9Cdisable body=E2= =80=9D. If your module only operates in one kind of buffer, disable it elsewhere in the =E2=80=9Cenab= le body=E2=80=9D. For example, if it should only run in server buffers, dis= able it in target buffers by doing something like =E2=80=98(if (erc-target) (erc-my-module-mode -1) (erc-my-module--setup))=E2=80=99. And in all cas= es, please remember to mention the module's intended =E2=80=9Cscope=E2=80=9D = in the doc string. Some informal adjectives that may help with that are: =E2=80=A2 query-local =E2=80=A2 channel-local =E2=80=A2 target-local (query or channel) =E2=80=A2 server-local =E2=80=A2 session-local (server and target) =E2=80=A2 buffer-local (server or target) You may also wish to mention this in the Custom group's doc string. (define-erc-module view nil "Enable `view-mode' if it was on previously. This module is buffer-local. If you also use the `scrolltobottom' module, you probably want to enable the option `erc-scrolltobottom-a= ll'." ((add-hook 'view-mode-hook #'erc-view--remember 0 t) (add-hook 'post-command-hook #'erc-view--enforce-prompt-boundary = 0 t) (setf (alist-get 'view-mode minor-mode-overriding-map-alist) erc-view-mode-overriding-map) (unless (local-variable-p 'erc-view--enabled-p) (setq-local erc-view--enabled-p nil)) (view-mode (if erc-view--enabled-p +1 -1))) ((kill-local-variable 'erc-view--enabled-p) (remove-hook 'post-command-hook #'erc-view--enforce-prompt-bounda= ry t) (remove-hook 'view-mode-hook #'erc-view--remember t) (setf (alist-get 'view-mode minor-mode-overriding-map-alist nil '= remove) nil)) localp) Always define your module early, before any code that refers to its mode command or minor-mode variable. (defun erc-view--enable-on-backspace (lines) "Enable `view-mode' at the prompt by hitting \\`<backspace>'." (interactive "P") (if (and erc-view-backspace-at-prompt-scrolls-down (not view-mode) (=3D (point) erc-input-marker)) (progn (view-mode +1) (View-scroll-page-backward lines)) (call-interactively #'delete-backward-char))) (defun erc-view--enforce-prompt-boundary () "Enable or disable `view-mode' when crossing prompt boundary." (when-let* ((new (if (>=3D (point) erc-input-marker) (and view-mode erc-view-disable-when-entering-prompt= -1) (and (not view-mode) erc-view-enable-when-exiting-prom= pt +1)))) (run-at-time 0 nil (lambda (buffer new) (with-current-buffer buffer (view-mode new)= )) (current-buffer) new))) (defun erc-view--exit-to-bottom () "Scroll to prompt, exit `view-mode', and move to EOB." (interactive) (let (view-no-disable-on-exit) (View-scroll-to-buffer-end) (View-exit) (goto-char (point-max)))) (defun erc-view--remember () "Remember the value of `view-mode'. Disable `erc-move-to-prompt-setup' locally when `view-mode' is enabl= ed." (cl-assert (local-variable-p 'erc-view--enabled-p)) (setq erc-view--enabled-p view-mode) (when erc-move-to-prompt-mode (if view-mode (remove-hook 'pre-command-hook #'erc-move-to-prompt t) (erc-move-to-prompt-setup)))) Don't forget to =E2=80=98provide=E2=80=99 your module so that =E2=80= =98erc-update-modules=E2=80=99 can find it. (provide 'erc-view) ;;; erc-view.el ends here Mimicking the above should just about cover most use cases. If your module isn't loading correctly, it's likely a naming, layout, or packaging issue. If you _must_ defy the convention recommended earlier regarding a library-group-module correspondence or if you've designed your module mainly to be toggled interactively rather than added to =E2=80=98erc-modules=E2=80=99, try placing a line like the following abov= e the module's definition. ;;;###autoload(autoload 'erc-my-module-mode "erc-my-module" nil t) Just remember, doing so means you'll need to (re)generate the autoload file when hacking locally (*note (emacs)Fetching Package Sources::). --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-5.7-Split-ERC-module-documentation-into-subnodes.patch From bdb72a74a30f23ba4181d054639c75ee89e54095 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" <[email protected]> Date: Wed, 12 Feb 2025 21:11:51 -0800 Subject: [PATCH 1/2] [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-Add-module-example-to-ERC-s-documentation.patch From 074bd8ea6ed4f57fde2e48c21c2df5b9813f5fbf Mon Sep 17 00:00:00 2001 From: "F. Jason Park" <[email protected]> Date: Wed, 12 Feb 2025 21:11:51 -0800 Subject: [PATCH 2/2] [5.7] Add module example to ERC's documentation * doc/misc/erc.texi (Module Example): New section under the Modules chapter. (Bug#57955) --- doc/misc/erc.texi | 237 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 818693db2f8..3f63b530f83 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -79,6 +79,7 @@ Top * Scope: Module Scope. Differences between module types. * Loading: Module Loading. How ERC loads modules. +* Example: Module Example. An example module. Advanced Usage @@ -761,6 +762,242 @@ Module Loading incorrectly, with built-in modules moved from the predefined checklist to the user-provided free-form area. +@node Module Example +@section Example +@cindex module example + +This is a walk-through of a working module presented in separate chunks. +If you'd prefer to view it as a whole, you can install it as a +third-party package through ERC's devel archive: +@uref{https://emacs-erc.gitlab.io/bugs/archive/erc-view.html}. + +@lisp +;;; erc-view.el -- Automatic view-mode for ERC -*- lexical-binding: t; -*- + +;; Maintainer: The ERC Maintainers <emacs-erc@@gnu.org> +;; Keywords: convenience +;; Version: 0.1 +;; Package-Requires: ((emacs "30.1")) +;; URL: https://gitlab.com/emacs-erc/erc-view + +;;; Commentary: + +;; This is a demo local module for ERC. It arranges for automatically +;; enabling `view-mode' when leaving the prompt area and automatically +;; disabling it when reentering. It also ensures `view-mode' stays +;; enabled or disabled when reconnecting. + +;;; Code: +@end lisp + +You need to import ERC's main library somehow. The easiest way is +directly, via a simple @code{(require 'erc)}, although this module does +so indirectly because it also uses definitions from @file{erc-goodies}: + +@lisp +(require 'erc-goodies) +(require 'view) +@end lisp + +Avoid headaches by aligning the name of your module with its containing +library and Custom group. It's best to have one group and one module +per library. + +@lisp +(defgroup erc-view nil + "Automatically enter and exit `view-mode' in ERC." + :version "0.1" + :group 'erc) + +(defcustom erc-view-enable-when-exiting-prompt t + "Whether to enable `view-mode' when exiting the prompt area." + :type 'boolean) + +(defcustom erc-view-disable-when-entering-prompt t + "Whether to disable `view-mode' when entering the prompt area." + :type 'boolean) + +(defcustom erc-view-backspace-at-prompt-scrolls-down t + "Whether a \\`<backspace>' at the prompt scrolls down to enter `view-mode'." + :type 'boolean) +@end lisp + +You'll almost always want to define internal variables as buffer-local. + +@lisp +(defvar-local erc-view--enabled-p nil + "Current reconnect-aware activation state of `view-mode'.") +@end lisp + +@noindent +In some cases, you may need a variable's value to survive the +reapplication of ERC's @code{major-mode} performed in each reassociated +buffer upon reconnecting. Do this by leveraging the +@code{permanent-local} symbol property. (@pxref{Creating +Buffer-Local,,,elisp,}.) + +@lisp +(put 'erc-view--enabled-p 'permanent-local t) +@end lisp + +@noindent +There are a few caveats regarding the durability of permanent values. +By convention, disabling a module's minor mode kills local bindings. +Mode commands, like @code{erc-view-mode}, do so in the current buffer +only, while unidirectional ones, like @code{erc-view-mode-disable}, do +so connection-wide. + +There are also occasions in which persistence is undefined, most notably +when @dfn{grafting} an old buffer's contents onto a current buffer. +This occurs in server buffers upon @dfn{logical connection} (at +@samp{MOTD}'s end), when a user reconnects with a new invocation of an +entry-point command, like @code{erc-tls}, instead of via the +auto-reconnect facility or by issuing a @samp{/reconnect} at the prompt. +Unaffected are entry-point invocations that include an @code{:id} +keyword because reassociation happens immediately in such cases, before +ERC even initializes any modules. Grafting can also happen in target +buffers, most often after a user reconnects under a new nick and +conducts business in the same channels and queries as before, only to +renick @emph{back} to the previous nick via a @samp{/nick oldme} or +similar. As of version 5.7, ERC retains the current buffer's permanent +value in all such situations, meaning ERC ignores permanent values from +previous buffers and retains default values assigned during module +initialization. + +Moving on, if your module needs to bind keys, define its keymap +@emph{before} the module itself, and use the standard minor-mode naming +convention of @code{erc-my-module-mode-map}. + +@lisp +(defvar-keymap erc-view-mode-map + :doc "Keymap for `view-mode' in ERC." + "<remap> <delete-backward-char>" #'erc-view--enable-on-backspace) + +(defvar-keymap erc-view-mode-overriding-map + :parent view-mode-map + :doc "Overriding keymap for `view-mode' when `erc-view-mode' is active. +Hitting \\`<RET>' atop a button prompts for an action by default. Use +\\`C-j' or \\`j' for scrolling up by a line." + "C" nil ; View-kill-and-leave + "E" #'erc-view--exit-to-bottom ; View-exit-and-edit + "Q" nil ; View-quit-all + "k" #'View-scroll-line-backward ; Vim backwards line + "j" #'View-scroll-line-forward ; Vim forwards line + "S-<return>" #'View-scroll-line-backward) +@end lisp + +You'll almost always want to define your module as buffer-local. Do +this by including a @code{localp} flag as the final parameter to +@code{define-erc-module}, after the @dfn{disable body}. If your module +only operates in one kind of buffer, disable it elsewhere in the +@dfn{enable body}. For example, if it should only run in server +buffers, disable it in target buffers by doing something like @code{(if +(erc-target) (erc-my-module-mode -1) (erc-my-module--setup))}. And in +all cases, please remember to mention the module's intended @dfn{scope} +in the doc string. Some informal adjectives that may help with that +are: + +@itemize +@item query-local +@item channel-local +@item target-local (query or channel) +@item server-local +@item session-local (server and target) +@item buffer-local (server or target) +@end itemize + +@noindent +You may also wish to mention this in the Custom group's doc string. + +@lisp +(define-erc-module view nil + "Enable `view-mode' if it was on previously. +This module is buffer-local. If you also use the `scrolltobottom' +module, you probably want to enable the option `erc-scrolltobottom-all'." + ((add-hook 'view-mode-hook #'erc-view--remember 0 t) + (add-hook 'post-command-hook #'erc-view--enforce-prompt-boundary 0 t) + (setf (alist-get 'view-mode minor-mode-overriding-map-alist) + erc-view-mode-overriding-map) + (unless (local-variable-p 'erc-view--enabled-p) + (setq-local erc-view--enabled-p nil)) + (view-mode (if erc-view--enabled-p +1 -1))) + ((kill-local-variable 'erc-view--enabled-p) + (remove-hook 'post-command-hook #'erc-view--enforce-prompt-boundary t) + (remove-hook 'view-mode-hook #'erc-view--remember t) + (setf (alist-get 'view-mode minor-mode-overriding-map-alist nil 'remove) + nil)) + localp) +@end lisp + +Always define your module early, before any code that refers to its mode +command or minor-mode variable. + +@lisp +(defun erc-view--enable-on-backspace (lines) + "Enable `view-mode' at the prompt by hitting \\`<backspace>'." + (interactive "P") + (if (and erc-view-backspace-at-prompt-scrolls-down (not view-mode) + (= (point) erc-input-marker)) + (progn + (view-mode +1) + (View-scroll-page-backward lines)) + (call-interactively #'delete-backward-char))) + +(defun erc-view--enforce-prompt-boundary () + "Enable or disable `view-mode' when crossing prompt boundary." + (when-let* + ((new (if (>= (point) erc-input-marker) + (and view-mode erc-view-disable-when-entering-prompt -1) + (and (not view-mode) erc-view-enable-when-exiting-prompt +1)))) + (run-at-time 0 nil (lambda (buffer new) + (with-current-buffer buffer (view-mode new))) + (current-buffer) new))) + +(defun erc-view--exit-to-bottom () + "Scroll to prompt, exit `view-mode', and move to EOB." + (interactive) + (let (view-no-disable-on-exit) + (View-scroll-to-buffer-end) + (View-exit) + (goto-char (point-max)))) + +(defun erc-view--remember () + "Remember the value of `view-mode'. +Disable `erc-move-to-prompt-setup' locally when `view-mode' is enabled." + (cl-assert (local-variable-p 'erc-view--enabled-p)) + (setq erc-view--enabled-p view-mode) + (when erc-move-to-prompt-mode + (if view-mode + (remove-hook 'pre-command-hook #'erc-move-to-prompt t) + (erc-move-to-prompt-setup)))) +@end lisp + +Don't forget to @code{provide} your module so that +@code{erc-update-modules} can find it. + +@lisp +(provide 'erc-view) + +;;; erc-view.el ends here + +@end lisp +Mimicking the above should just about cover most use cases. If your +module isn't loading correctly, it's likely a naming, layout, or +packaging issue. If you @emph{must} defy the convention recommended +earlier regarding a library-group-module correspondence or if you've +designed your module mainly to be toggled interactively rather than +added to @code{erc-modules}, try placing a line like the following above +the module's definition. + +@lisp +;;;###autoload(autoload 'erc-my-module-mode "erc-my-module" nil t) +@end lisp + +@noindent +Just remember, doing so means you'll need to (re)generate the autoload +file when hacking locally (@pxref{Fetching Package Sources,,, emacs,}). + + @c PRE5_4: Document every option of every module in its own subnode -- 2.48.1 --=-=-=--