"J.P." 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 > > 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 =========== 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: . ;;; erc-view.el -- Automatic view-mode for ERC -*- lexical-binding: t; -*- ;; Maintainer: The ERC Maintainers ;; 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: You need to import ERC's main library somehow. The easiest way is directly, via a simple ‘(require 'erc)’, although this module does so indirectly because it also uses definitions from ‘erc-goodies’: (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 \\`' at the prompt scrolls down to enter `view-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 ‘major-mode’ performed in each reassociated buffer upon reconnecting. Do this by leveraging the ‘permanent-local’ 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 ‘erc-view-mode’, do so in the current buffer only, while unidirectional ones, like ‘erc-view-mode-disable’, do so connection-wide. There are also occasions in which persistence is undefined, most notably when “grafting” an old buffer's contents onto a current buffer. This occurs in server buffers upon “logical connection” (at ‘MOTD’'s end), when a user reconnects with a new invocation of an entry-point command, like ‘erc-tls’, instead of via the auto-reconnect facility or by issuing a ‘/reconnect’ at the prompt. Unaffected are entry-point invocations that include an ‘: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 _back_ to the previous nick via a ‘/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 _before_ the module itself, and use the standard minor-mode naming convention of ‘erc-my-module-mode-map’. (defvar-keymap erc-view-mode-map :doc "Keymap for `view-mode' in ERC." " " #'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 \\`' 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-" #'View-scroll-line-backward) You'll almost always want to define your module as buffer-local. Do this by including a ‘localp’ flag as the final parameter to ‘define-erc-module’, after the “disable body”. If your module only operates in one kind of buffer, disable it elsewhere in the “enable body”. For example, if it should only run in server buffers, disable it in target buffers by doing something like ‘(if (erc-target) (erc-my-module-mode -1) (erc-my-module--setup))’. And in all cases, please remember to mention the module's intended “scope” in the doc string. Some informal adjectives that may help with that are: • query-local • channel-local • target-local (query or channel) • server-local • session-local (server and target) • 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-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) 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 \\`'." (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)))) Don't forget to ‘provide’ your module so that ‘erc-update-modules’ 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 ‘erc-modules’, try placing a line like the following above 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::).