GNU bug report logs - #57955
29.0.50; Allow session-local ERC modules

Previous Next

Package: emacs;

Reported by: "J.P." <jp <at> neverwas.me>

Date: Tue, 20 Sep 2022 13:06:02 UTC

Severity: wishlist

Tags: patch

Found in version 29.0.50

Full log


View this message in rfc822 format

From: "J.P." <jp <at> neverwas.me>
To: 57955 <at> debbugs.gnu.org
Cc: emacs-erc <at> gnu.org
Subject: bug#57955: 29.0.50; Allow session-local ERC modules
Date: Tue, 18 Feb 2025 20:17:58 -0800
[Message part 1 (text/plain, inline)]
"J.P." <jp <at> neverwas.me> 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:
  <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 <emacs-erc <at> 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:

     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 \\`<backspace>' 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."
         "<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)

     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 \\`<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))))

     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::).

[0001-5.7-Split-ERC-module-documentation-into-subnodes.patch (text/x-patch, attachment)]
[0002-5.7-Add-module-example-to-ERC-s-documentation.patch (text/x-patch, attachment)]

This bug report was last modified 120 days ago.

Previous Next


GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997,2003 nCipher Corporation Ltd, 1994-97 Ian Jackson.