master 500afb626cb 3/9: Add module example to ERC's documentation

"F. Jason Park" <[email protected]> Tue, 30 Jun 2026 23:37:21 -0400 (EDT)
Newsgroups gmane.emacs.diffs
Message-ID <[email protected]>
branch: master
commit 500afb626cbc9d3ef589295bb72e2d3759530cc9
Author: F. Jason Park <[email protected]>
Commit: F. Jason Park <[email protected]>

    Add module example to ERC's documentation
    
    * 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.
    (Module Example): New section under the Modules chapter.
---
 doc/misc/erc.texi | 264 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 259 insertions(+), 5 deletions(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index 03767a2e68c..9b75ac6a83c 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -76,6 +76,13 @@ Getting Started
 * 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.
+* Example: Module Example.      An example module.
+* Usage: Module Usage.          How to use specific modules.
+
 Advanced Usage
 
 * Connecting::                  Ways of connecting to an IRC server.
@@ -615,9 +622,19 @@ so demands special precautions to avoid degrading the user experience.
 At present, the only such module is @code{networks}, whose library ERC
 always loads anyway.
 
+@c Advanced module topics and individual module usage.
+@menu
+* Scope: Module Scope.          Differences between module types.
+* Loading: Module Loading.      How ERC loads modules.
+* Example: Module Example.      An example module.
+* Usage: Module Usage.          How to use specific modules.
+@end menu
+
 @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
@@ -698,10 +715,8 @@ buffers belonging to their connection (when called interactively).  And
 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
@@ -755,6 +770,245 @@ Customize into displaying the widget for @code{erc-modules}
 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 walkthrough of a working module, presented in sections.
+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 automatically enables
+;; `view-mode' when leaving the prompt area and disables it when
+;; reentering.  It also ensures `view-mode' stays enabled or disabled
+;; when reconnecting.
+
+;;; Code:
+@end lisp
+
+@noindent
+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
+
+@noindent
+Avoid headaches by aligning the name of your module with its containing
+library and Custom group: one group and 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)
+
+(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
+reinitialization of ERC's @code{major-mode} performed in each buffer
+upon reconnecting.  Do this by leveraging the @code{permanent-local}
+symbol property.  (@pxref{Creating Buffer-Local,,,elisp,}.)  Also see
+the mini-section after this example for a few caveats regarding the
+durability of permanent values like this.
+
+@lisp
+(put 'erc-view--enabled-p 'permanent-local t)
+@end lisp
+
+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 ; Vi backwards line
+  "j" #'View-scroll-line-forward ; Vi 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, do 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
+  "Use `view-mode' for reading and navigating \"scrollback\".
+If enabling, restore `view-mode' activation state.
+
+Note that if paired with the `scrolltobottom' module, the option
+`erc-scrolltobottom-all' should probably be enabled.  This module is
+buffer-local."
+  ((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."
+  (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,}).
+
+@subsection Nuances Regarding Local Module State
+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 if given a prefix argument.
+
+There are also occasions in which the persistence of a buffer-local
+variable is undefined, most notably during a @dfn{transplant}, when
+copying an old buffer's contents into its replacement buffer.  This
+crude merge operation can occur, for example, in server buffers upon
+@dfn{logical connection} (at @samp{MOTD}'s end) if 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.)
+
+Transplants can also happen in target buffers, most often after a user
+reconnects under a new nick and conducts business in the same channel
+and query buffers, 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 ones
+assigned during module initialization.  Package authors needing access
+to these internal transplant junctures should make a case on the mailing
+list or the bug tracker.
+
+
 @c PRE5_4: Document every option of every module in its own subnode