Subject: 30.0.50; ERC 5.6: Add buffer-list and nick-list modules
From: "J.P." <[email-masked]>
To: [email-masked]
Date: Fri, 19 May 2023 12:25:29 -0700
List-Id: <debbugs-submit.debbugs.gnu.org>

Tags: patch
Severity: wishlist

(This bug could just as well be titled "Make ERC look more like
standalone clients," although that's perhaps a bit clickbaity.)

Hi,

Newcomers to ERC are occasionally dumbfounded by the absence of a
certain pair of IRC essentials, namely, a "window list" and a "nicknames
list." But what even existing users may not know is that ERC already has
both; they just don't quite work the way folks coming from other clients
might expect. This patch aims to address that as if it were a problem.

What's being proposed here is the addition of two new modules built
entirely on functionality already offered by their host libraries,
erc-status-sidebar.el and erc-speedbar.el. What's different is these
additions flirt with something somewhat verboten in ERC, and that's
changing existing defaults for non-security reasons (in this case,
chiefly for unifying UX [1]). Beyond that, these changes also hard-wire
potentially offensive choices into some of these defaults while trying
in earnest to ensure old behavior remains accessible. For example, one
module assumes no one with a non-nil `erc-header-line-format' has any
use for modes and topics in a side window. Other baked in preferences
include the nixing of all throwback icons [2] and the showing of
disconnected buffers in a muted face. The list goes on but is open for
discussion, as always.

Another consideration here is of course naming [3]. I've gone ahead and
dubbed the speedbar module 'nickbar' as a bit of an homage to its roots.
`nicklist' wasn't available because a popular module of that name was
once part of ERC, and people still use it today. For status-sidebar, I
went with `bufbar', in part for consistency but also because the
would-be canonical "status-sidebar" is already taken by the side
window's buffer for its major mode. We might also consider going with
the slightly pedestrian `buflist', which is already used by at least one
other IRC client for referring to this feature.

Lastly, I'd like to stress that these modules (thankfully) won't be
loaded by default, which means you'll need to add `bufbar' and `nickbar'
to `erc-modules' when trying them out. Note that this patch set
currently depends on bug#63569 [3], whose changes need applying
beforehand.

Thanks!

[1] My justifications for taking such liberties are at best anecdotal,
    based mostly on personal impressions of prevailing usage patterns
    and the perceived motivations behind them. A few examples:

    a. The relative newness of erc-status-sidebar.el, its lack of an
       associated module, and its absence from ERC's Custom menu all
       point to its defaults being somewhat more free for the changing.
       The speedbar integration likewise lacks a module, and its
       graphical-only nature means its set of users currently excludes
       anyone using ERC from a terminal. (Less expected fallout from the
       proposed churn, IOW.)

    b. Both libraries provide an always-open side window (or, in
       speedbar's case, frame), which imposes a specific layout on the
       user, something that's relatively rare in Emacs outside of
       IDE-style modes. Compare this to something like ibuffer for
       listing buffers or a hypothetical /NAMES buffer, which, if based
       on `tabulated-list-mode', would make for a more familiar means of
       reviewing channel members. The point here is that devoted users
       likely aren't messing with erc-status-sidebar or erc-speedbar in
       droves, preferring more Emacsy alternatives instead.

    c. Newcomers can't be bothered with an overly involved setup if it
       requires carefully exploring options before trying. But they're
       perfectly willing to copy/paste gobs of configuration or even
       mindlessly follow a minutes long Customize itinerary, so long as
       the promise of emerging with something resembling their present
       idea of what makes an IRC client remains within reach. By
       introducing these modules "preconfigured," I'm positing that
       these preconceived notions typically include a reactive window
       list and/or nick list. That said, my hope is that these users
       will eventually gravitate toward the more Emacs-native way of
       doing things, at which point these modules will have served their
       primary purpose (of acting as bait).

[2] My reason for 86'ing the icons outright in the speedbar's new
    "window mode" is consistency. I figure anyone who wants the vanilla
    experience wants it in full and thus also wants a separate frame.
    That said, I suppose we could add a knob for tweaking this (or any
    of the other hard-coded choices mentioned), but IMO doing that
    unprovoked just contributes to "options sprawl."

[3] FWIW, all module names in contention flout our supposed policy of
    preferring those that align with host libraries and the features
    they `provide'.

In GNU Emacs 30.0.50 (build 2, x86_64-pc-linux-gnu, GTK+ Version
 3.24.37, cairo version 1.17.6) of 2023-05-13 built on localhost
Repository revision: 867b104010760c4b7cd700078884cc774a01860a
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12014000
System Description: Fedora Linux 37 (Workstation Edition)

Configured using:
 'configure --enable-check-lisp-object-type --enable-checking=yes,glyphs
 'CFLAGS=-O0 -g3'
 PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
JSON LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES 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=ibus
  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
  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 mail-extr emacsbug message mailcap yank-media puny dired
dired-loaddefs rfc822 mml mml-sec epa derived 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 auth-source cl-seq
eieio eieio-core cl-macs password-cache json subr-x map format-spec
cl-loaddefs cl-lib erc-backend erc-networks byte-opt gv bytecomp
byte-compile erc-common erc-compat 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 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
move-toolbar gtk x-toolkit xinput2 x multi-tty make-network-process
emacs)

Memory information:
((conses 16 64236 9476)
 (symbols 48 8573 0)
 (strings 32 23246 1709)
 (string-bytes 1 674076)
 (vectors 16 15015)
 (vector-slots 8 207266 8159)
 (floats 8 24 33)
 (intervals 56 229 0)
 (buffers 976 10))
From 4a1bd9e173d91164d5abe6f2e349a447eaf019d7 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Wed, 17 May 2023 19:48:02 -0700
Subject: [PATCH 1/5] [5.6] Fix buffer-mismatch bug in erc-scroll-to-bottom

* lisp/erc/erc-goodies.el (erc-scroll-to-bottom): Only `recenter' when
the selected window's buffer is current.  Previously, the module
`scrolltobottom' signaled an "Error in `post-command-hook'" when a
user clicked a channel indicator in the mode line from a window
showing another ERC buffer.
---
 lisp/erc/erc-goodies.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 01eae4b63c5..87c95778523 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -92,6 +92,7 @@ erc-scroll-to-bottom
     (save-restriction
       (widen)
       (when (and erc-insert-marker
+                 (eq (current-buffer) (window-buffer))
 		 ;; we're editing a line. Scroll.
 		 (> (point) erc-insert-marker))
 	(save-excursion
-- 
2.40.0

From 5ae05b1ad34ce1baea149c7af1e9e9282f101725 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Mon, 15 May 2023 00:16:00 -0700
Subject: [PATCH 2/5] [5.6] Allow ERC's module toggles access to the prefix arg

* lisp/erc/erc-common.el (erc--module-toggle-prefix-arg): Add internal
variable for preserving the `arg' passed to a module's minor-mode
toggle, which was previously discarded.  Doing this lets modules that
are more interactive in nature overload their mode toggles with
alternate behaviors.
(define-erc-module): Bind `erc--module-toggle-prefix-arg' to the `arg'
parameter, which is normally defined inside a `define-minor-mode' body
form.
* test/lisp/erc/erc-tests.el (define-erc-module--global,
define-erc-module--local): Expect activation body to be wrapped by a
let form binding `erc--module-toggle-prefix-arg'.
---
 lisp/erc/erc-common.el     | 14 +++++++++++---
 test/lisp/erc/erc-tests.el | 14 ++++++++------
 2 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index f152a1a32d9..dd39b30c4db 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -289,6 +289,15 @@ erc--find-feature
              (intern (file-name-base file))))
      (v v)))
 
+(defvar erc--module-toggle-prefix-arg nil
+  "The interpreted prefix arg of the minor-mode toggle.
+Non-nil inside an ERC module's activation (or deactivation)
+command, such as `erc-spelling-enable', when it's been called
+indirectly via the module's minor-mode toggle, i.e.,
+`erc-spelling-mode'.  Nil otherwise.  Its value is either the
+symbol `toggle' or an integer produced by `prefix-numeric-value'.
+See Info node `(elisp) Defining Minor Modes' for more.")
+
 (defmacro define-erc-module (name alias doc enable-body disable-body
                                   &optional local-p)
   "Define a new minor mode using ERC conventions.
@@ -337,9 +346,8 @@ define-erc-module
          :group (erc--find-group ',name ,(and alias (list 'quote alias)))
          ,@(unless local-p `(:require ',(erc--find-feature name alias)))
          ,@(unless local-p `(:type ,(erc--prepare-custom-module-type name)))
-         (if ,mode
-             (,enable)
-           (,disable)))
+         (let ((erc--module-toggle-prefix-arg arg))
+           (if ,mode (,enable) (,disable))))
        ,(erc--assemble-toggle local-p name enable mode t enable-body)
        ,(erc--assemble-toggle local-p name disable mode nil disable-body)
        ,@(and-let* ((alias)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 991bfa3b082..de472527bde 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2175,9 +2175,10 @@ define-erc-module--global
                         :group (erc--find-group 'mname 'malias)
                         :require 'nil
                         :type "mname"
-                        (if erc-mname-mode
-                            (erc-mname-enable)
-                          (erc-mname-disable)))
+                        (let ((erc--module-toggle-prefix-arg arg))
+                          (if erc-mname-mode
+                              (erc-mname-enable)
+                            (erc-mname-disable))))
 
                       (defun erc-mname-enable ()
                         "Enable ERC mname mode."
@@ -2230,9 +2231,10 @@ define-erc-module--local
 Some docstring."
                         :global nil
                         :group (erc--find-group 'mname nil)
-                        (if erc-mname-mode
-                            (erc-mname-enable)
-                          (erc-mname-disable)))
+                        (let ((erc--module-toggle-prefix-arg arg))
+                          (if erc-mname-mode
+                              (erc-mname-enable)
+                            (erc-mname-disable))))
 
                       (defun erc-mname-enable (&optional ,arg-en)
                         "Enable ERC mname mode.
-- 
2.40.0

From c4746e4bb3fde7670bd655ed7f17ec90435018ac Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 4 May 2023 00:01:11 -0700
Subject: [PATCH 3/5] [5.6] Add preset styles to erc-status-sidebar

* lisp/erc/erc-networks.el (erc-networks--rename-server-buffer): Store
`erc-networks--id' in process object's plist.
* lisp/erc/erc-status-sidebar.el (erc-status-sidebar): Change parent
from `convenience' to `erc'.
(erc-status-sidebar-channel-format):
Mention in doc string that it depends on new option
`erc-status-sidebar-style'.
(erc-status-sidebar-highlight-active-buffer): New option to control
whether the current window's target is highlighted in the status bar.
(erc-status-sidebar-style): New option to determine whether servers
and queries also appear in the sidebar.
(erc-status-sidebar-click-display-action,
erc-status-sidebar-singular): New options.
(erc-status-sidebar-get-window): Consider
`erc-status-sidebar-singular'.
(erc-status-sidebar-open): Fix toggle functionality that somehow fell
through the cracks after the adoption of the package into ERC proper.
(erc-buflist-mode, erc-buflist-enable, erc-buflist-disable): New
module named `buflist' instead of `sidebar', which is more easily
confusable with `speedbar'.  The preferred name, `status-sidebar' was
unavailable because its minor-mode would have been
`erc-status-sidebar-mode', which is already taken by the major mode
used for status-bar buffers themselves.
(erc-status-sidebar-toggle): Ignore `erc-status-sidebar-singular'.
(erc-status-sidebar--trimpat, erc-status-sidebar--prechan): Add helper
vars for new sorting function, allowing it to honor the existing
interface, which only expects one argument.
(erc-status-sidebar-prefer-target-as-name): New function for
determining buffer name, preferring targets for target buffers.
(erc-status-sidebar-get-channame): Use internal API to help determine
name of buffer in sidebar.
(erc-status-sidebar-prefer-target-as-name,
erc-status-sidebar--show-disconnected,
erc-status-sidebar-all-target-buffers,
erc-status-sidebar-default-allsort): Add new naming and sorting
functions and associated helper functions and variables.
(erc-status-sidebar--active-marker,
erc-status-sidebar--set-active-line): New variable and function for
highlighting the active target in the status bar.
(erc-status-sidebar-default-insert,
erc-status-sidebar-pad-hierarchy): New functions for visiting various
stages of buffer modification when rendering sidebar.
(erc-status-sidebar-click): Appeal to option
`erc-status-sidebar-display-action' for `pop-to-buffer' action.
(erc-status-sidebar-scroll-up, erc-status-sidebar-scroll-down,
erc-status-sidebar-recenter): Add commands to scroll and
recenter sidebar from a target buffer's window.
(erc-status-sidebar-refresh): Consider presets and new options when
rendering sidebar.  Disable `erc-buflist-mode' when active.
(erc-status-sidebar-set-window-preserve-size): Ignore
`erc-status-sidebar-singular'.
(erc-status-sidebar-mode): Make non-interactive to avoid confusion
when folks run "M-x erc-status-sidebar-mode" expecting a module
toggle.
* test/lisp/erc/erc-scenarios-status-sidebar.el: New file.
* test/lisp/erc/resources/base/gapless-connect/foonet.eld: Fix wrong
manifest for channel and extend PASS timeout.
---
 lisp/erc/erc-networks.el                      |   1 +
 lisp/erc/erc-status-sidebar.el                | 328 ++++++++++++++++--
 test/lisp/erc/erc-scenarios-status-sidebar.el |  93 +++++
 .../resources/base/gapless-connect/foonet.eld |   8 +-
 4 files changed, 400 insertions(+), 30 deletions(-)
 create mode 100644 test/lisp/erc/erc-scenarios-status-sidebar.el

diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index dd481032e7e..60d76d059da 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -1459,6 +1459,7 @@ erc-networks--rename-server-buffer
          ;; When this ends up being the current buffer, either we have
          ;; a "given" ID or the buffer was reused on reconnecting.
          (existing (get-buffer name)))
+    (process-put new-proc 'erc-networks--id erc-networks--id)
     (cond ((or (not existing)
                (erc-networks--id-given erc-networks--id)
                (eq existing (current-buffer)))
diff --git a/lisp/erc/erc-status-sidebar.el b/lisp/erc/erc-status-sidebar.el
index f11faa3db10..c06ba53f3ae 100644
--- a/lisp/erc/erc-status-sidebar.el
+++ b/lisp/erc/erc-status-sidebar.el
@@ -45,6 +45,13 @@
 ;; Use M-x erc-status-sidebar-kill RET to kill the sidebar buffer and
 ;; close the sidebar on all frames.
 
+;; In addition to the commands above, you can also try the all-in-one,
+;; "DWIM" command, `erc-bufbar-mode'.  See its doc string for usage.
+
+;; If you want the status sidebar enabled whenever you use ERC, add
+;; `bufbar' to `erc-modules'.  Note that this library also has a major
+;; mode, `erc-status-sidebar-mode', which is for internal use.
+
 ;;; Code:
 
 (require 'erc)
@@ -53,8 +60,15 @@
 (require 'seq)
 
 (defgroup erc-status-sidebar nil
-  "A sidebar for ERC channel status."
-  :group 'convenience)
+  "A responsive side window listing all connected ERC buffers.
+More commonly known as a window list or \"buflist\", this side
+panel displays clickable buffer names for switching to with the
+mouse.  By default, ERC highlights the name corresponding to the
+selected window's buffer, if any.  Here, \"connected buffer\"
+means one belonging to a session context for a server that ERC is
+currently communicating with.  For information on how the window
+itself works, see Info node `(elisp) Side Windows'."
+  :group 'erc)
 
 (defcustom erc-status-sidebar-buffer-name "*ERC Status*"
   "Name of the sidebar buffer."
@@ -80,9 +94,78 @@ erc-status-sidebar-channel-sort
 
 (defcustom erc-status-sidebar-channel-format
   'erc-status-sidebar-default-chan-format
-  "Function used to format channel names for display in the sidebar."
+  "Function used to format channel names for display in the sidebar.
+Only consulted for certain values of `erc-status-sidebar-style'."
   :type 'function)
 
+(defcustom erc-status-sidebar-highlight-active-buffer t
+  "Whether to highlight the selected window's buffer in the sidebar.
+ERC uses the same instance across all frames.  May not be
+compatible with all values of `erc-status-sidebar-style'."
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type 'boolean)
+
+(defcustom erc-status-sidebar-style 'all-queries-first
+  "Preset style for rendering the sidebar.
+
+When set to `channels-only', ERC limits the items in the
+status bar to uniquified channels.  It uses the options
+and functions
+
+  `erc-channel-list',
+  `erc-status-sidebar-channel-sort',
+  `erc-status-sidebar-get-channame',
+  `erc-status-sidebar-channel-format'
+  `erc-status-sidebar-default-insert'
+
+for selecting, formatting, naming, and inserting entries.  When
+set to one of the various \\=`all-*' values, such as `all-mixed',
+ERC shows channels and queries under their respective server
+buffers, using the functions
+
+  `erc-status-sidebar-all-target-buffers',
+  `erc-status-sidebar-default-allsort',
+  `erc-status-sidebar-prefer-target-as-name',
+  `erc-status-sidebar-default-chan-format',
+  `erc-status-sidebar-pad-hierarchy'
+
+for the above-mentioned purposes.  ERC also accepts a list of
+functions to preform these roles a la carte.  See doc strings for
+a description of their expected arguments and return values."
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type '(choice (const channels-only)
+                 (const all-mixed)
+                 (const all-queries-first)
+                 (const all-channels-first)
+                 (list (function :tag "Buffer lister")
+                       (function :tag "Buffer sorter")
+                       (function :tag "Name extractor")
+                       (function :tag "Name formatter")
+                       (function :tag "Name inserter"))))
+
+(defcustom erc-status-sidebar-click-display-action t
+  "How to display a buffer when clicked.
+Values can be anything recognized by `display-buffer' for its
+ACTION parameter."
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type '(choice (const :tag "Always use/create other window" t)
+                 (const :tag "Let `display-buffer' decide" nil)
+                 (const :tag "Same window" (display-buffer-same-window
+                                            (inhibit-same-window . nil)))
+                 (cons :tag "Action"
+                       (choice function (repeat function))
+                       (alist :tag "Action arguments"
+                              :key-type symbol
+                              :value-type (sexp :tag "Value")))))
+
+(defcustom erc-status-sidebar-singular t
+  "Whether to show the sidebar on all frames or just one (default)."
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type 'boolean)
+
+(defvar hl-line-mode)
+(declare-function hl-line-highlight "hl-line" nil)
+
 (defun erc-status-sidebar-display-window ()
   "Display the status buffer in a side window.  Return the new window."
   (display-buffer
@@ -94,7 +177,8 @@ erc-status-sidebar-get-window
   "Return the created/existing window displaying the status buffer.
 
 If NO-CREATION is non-nil, the window is not created."
-  (let ((sidebar-window (get-buffer-window erc-status-sidebar-buffer-name)))
+  (let ((sidebar-window (get-buffer-window erc-status-sidebar-buffer-name
+                                           erc-status-sidebar-singular)))
     (unless (or sidebar-window no-creation)
       (with-current-buffer (erc-status-sidebar-get-buffer)
         (setq-local vertical-scroll-bar nil))
@@ -144,22 +228,50 @@ erc-status-sidebar-open
   "Open or create a sidebar."
   (interactive)
   (save-excursion
-    (let ((sidebar-exists (erc-status-sidebar-buffer-exists-p))
-          (sidebar-buffer (erc-status-sidebar-get-buffer))
-          ;; (sidebar-window (erc-status-sidebar-get-window))
-          )
-      (unless sidebar-exists
-        (with-current-buffer sidebar-buffer
-          (erc-status-sidebar-mode)
-          (erc-status-sidebar-refresh))))))
+    (if (erc-status-sidebar-buffer-exists-p)
+        (erc-status-sidebar-get-window)
+      (with-current-buffer (erc-status-sidebar-get-buffer)
+        (erc-status-sidebar-mode)
+        (erc-status-sidebar-refresh)))))
+
+;;;###autoload(autoload 'erc-bufbar-mode "erc-status-sidebar" nil t)
+(define-erc-module bufbar nil
+  "Show `erc-track'-like activity in a side window.
+When enabling, show the sidebar immediately if called from a
+connected ERC buffer.  Otherwise, arrange for doing so on connect
+or whenever next displaying a new ERC buffer.  When disabling,
+hide the status window if it's showing.  With a negative prefix
+arg, also shutdown the session."
+  ((unless erc-track-mode
+     (unless (memq 'track erc-modules)
+       (erc--warn-once-before-connect 'erc-bufbar-mode
+         "Module `bufbar' needs global module `track'. Enabling now."
+         " This will affect \C-]all\C-] ERC sessions."
+         " Add `track' to `erc-modules' to silence this message."))
+     (erc-track-mode +1))
+   (add-hook 'erc--setup-buffer-hook #'erc-status-sidebar-open)
+   (unless erc--updating-modules-p
+     (if (erc-with-server-buffer erc-server-connected)
+         (erc-status-sidebar-open)
+       (setq erc-bufbar-mode nil)
+       (user-error "`%s' not called from a connected ERC buffer."
+                   'erc-bufbar-mode))))
+  ((remove-hook 'erc--setup-buffer-hook #'erc-status-sidebar-open)
+   (erc-status-sidebar-close erc-status-sidebar-singular)
+   (when-let* ((arg erc--module-toggle-prefix-arg)
+               ((numberp arg))
+               ((< arg 0)))
+     (erc-status-sidebar-kill))))
 
 ;;;###autoload
 (defun erc-status-sidebar-toggle ()
-  "Toggle the sidebar open/closed on the current frame."
+  "Toggle the sidebar open/closed on the current frame.
+Do this regardless of `erc-status-sidebar-singular'."
   (interactive)
   (if (get-buffer-window erc-status-sidebar-buffer-name nil)
       (erc-status-sidebar-close)
-    (erc-status-sidebar-open)))
+    (let (erc-status-sidebar-singular)
+      (erc-status-sidebar-open))))
 
 (defun erc-status-sidebar-get-channame (buffer)
   "Return name of BUFFER with all leading \"#\" characters removed."
@@ -174,6 +286,98 @@ erc-status-sidebar-default-chansort
                    (string< (erc-status-sidebar-get-channame x)
                             (erc-status-sidebar-get-channame y)))))
 
+(defvar erc-status-sidebar--trimpat nil)
+(defvar erc-status-sidebar--prechan nil)
+
+(defun erc-status-sidebar-prefer-target-as-name (buffer)
+  "Return some name to represent buffer in the sidebar."
+  (if-let ((target (buffer-local-value 'erc--target buffer)))
+      (cond ((and erc-status-sidebar--trimpat (erc--target-channel-p target))
+             (string-trim-left (erc--target-string target)
+                               erc-status-sidebar--trimpat))
+            ((and erc-status-sidebar--prechan (erc--target-channel-p target))
+             (concat erc-status-sidebar--prechan
+                     (erc--target-string target)))
+            (t (erc--target-string target)))
+    (buffer-name buffer)))
+
+;; This could be converted into an option if people want.
+(defvar erc-status-sidebar--show-disconnected t)
+
+(defun erc-status-sidebar-all-target-buffers (process)
+  (erc-buffer-filter (lambda ()
+                       (and erc--target
+                            (or erc-status-sidebar--show-disconnected
+                                (erc-server-process-alive))))
+                     process))
+
+;; FIXME profile this.  Rebuilding the graph every time track updates
+;; seems wasteful for occasions where server messages are processed
+;; unthrottled, such as during history playback.  If it's a problem,
+;; we should look into rewriting this using `ewoc' or some other
+;; solution that maintains a persistent model.
+(defun erc-status-sidebar-default-allsort (target-buffers)
+  "Return a list of servers interspersed with their targets."
+  (mapcan (pcase-lambda (`(,proc . ,chans))
+            (cons (process-buffer proc)
+                  (let ((erc-status-sidebar--trimpat
+                         (and (eq erc-status-sidebar-style 'all-mixed)
+                              (with-current-buffer (process-buffer proc)
+                                (when-let ((ch-pfxs (erc--get-isupport-entry
+                                                     'CHANTYPES 'single)))
+                                  (regexp-quote ch-pfxs)))))
+                        (erc-status-sidebar--prechan
+                         (and (eq erc-status-sidebar-style
+                                  'all-queries-first)
+                              "\C-?")))
+                    (sort chans
+                          (lambda (x y)
+                            (string<
+                             (erc-status-sidebar-prefer-target-as-name x)
+                             (erc-status-sidebar-prefer-target-as-name y)))))))
+          (sort (seq-group-by (lambda (b)
+                                (buffer-local-value 'erc-server-process b))
+                              target-buffers)
+                (lambda (a b)
+                  (string< (buffer-name (process-buffer (car a)))
+                           (buffer-name (process-buffer (car b))))))))
+
+(defvar-local erc-status-sidebar--active-marker nil
+  "Marker indicating currently active buffer.")
+
+(defun erc-status-sidebar--set-active-line (erc-buffer)
+  (when (and erc-status-sidebar-highlight-active-buffer
+             (eq (window-buffer (and (minibuffer-window-active-p
+                                      (selected-window))
+                                     (minibuffer-selected-window)))
+                 erc-buffer))
+    (set-marker erc-status-sidebar--active-marker (point))))
+
+(defun erc-status-sidebar-default-insert (channame chanbuf _chanlist)
+  "Insert CHANNAME followed by a newline.
+Maybe arrange to highlight line if CHANBUF is showing in the
+focused window."
+  (erc-status-sidebar--set-active-line chanbuf)
+  (insert channame "\n"))
+
+(defun erc-status-sidebar-pad-hierarchy (bufname buffer buflist)
+  "Prefix BUFNAME to emphasize BUFFER's role in BUFLIST."
+  (if (and (buffer-live-p buffer) (buffer-local-value 'erc--target buffer))
+      (insert " ")
+    (unless (eq buffer (car buflist))
+      (insert "\n"))) ;  ^L
+  (when bufname
+    (erc-status-sidebar--set-active-line buffer))
+  (insert (or bufname
+              (and-let* (((not (buffer-live-p buffer)))
+                         (next (cadr (member buffer buflist)))
+                         ((buffer-live-p next))
+                         (proc (buffer-local-value 'erc-server-process next))
+                         (id (process-get proc 'erc-networks--id)))
+                (symbol-name (erc-networks--id-symbol id)))
+              "???")
+          "\n"))
+
 (defun erc-status-sidebar-default-chan-format (channame
                                                &optional num-messages erc-face)
   "Format CHANNAME for display in the sidebar.
@@ -193,43 +397,111 @@ erc-status-sidebar-default-chan-format
 (defun erc-status-sidebar-refresh ()
   "Update the content of the sidebar."
   (interactive)
-  (let ((chanlist (apply erc-status-sidebar-channel-sort
-                         (erc-channel-list nil) nil)))
+  (pcase-let* ((`(,list-fn ,sort-fn ,name-fn ,fmt-fn ,insert-fn)
+                (pcase erc-status-sidebar-style
+                  ('channels-only (list #'erc-channel-list
+                                        erc-status-sidebar-channel-sort
+                                        #'erc-status-sidebar-get-channame
+                                        erc-status-sidebar-channel-format
+                                        #'erc-status-sidebar-default-insert))
+                  ((or 'all-mixed 'all-queries-first 'all-channels-first)
+                   '(erc-status-sidebar-all-target-buffers
+                     erc-status-sidebar-default-allsort
+                     erc-status-sidebar-prefer-target-as-name
+                     erc-status-sidebar-default-chan-format
+                     erc-status-sidebar-pad-hierarchy))
+                  (v v)))
+               (chanlist (apply sort-fn (funcall list-fn nil) nil))
+               (window nil)
+               (winstart nil))
     (with-current-buffer (erc-status-sidebar-get-buffer)
+      (setq window (get-buffer-window nil erc-status-sidebar-singular)
+            winstart (and window (window-start window)))
       (erc-status-sidebar-writable
        (delete-region (point-min) (point-max))
        (goto-char (point-min))
+       (if erc-status-sidebar--active-marker
+           (set-marker erc-status-sidebar--active-marker nil)
+         (setq erc-status-sidebar--active-marker (make-marker)))
        (dolist (chanbuf chanlist)
          (let* ((tup (seq-find (lambda (tup) (eq (car tup) chanbuf))
                                erc-modified-channels-alist))
                 (count (if tup (cadr tup)))
                 (face (if tup (cddr tup)))
-                (channame (apply erc-status-sidebar-channel-format
-                                 (buffer-name chanbuf) count face nil))
+                (face (if (or (not (buffer-live-p chanbuf))
+                              (not (erc-server-process-alive chanbuf)))
+                          `(shadow ,face)
+                        face))
+                (channame (apply fmt-fn
+                                 (copy-sequence (funcall name-fn chanbuf))
+                                 count face nil))
                 (cnlen (length channame)))
            (put-text-property 0 cnlen 'erc-buf chanbuf channame)
            (put-text-property 0 cnlen 'mouse-face 'highlight channame)
            (put-text-property
             0 cnlen 'help-echo
             "mouse-1: switch to buffer in other window" channame)
-           (insert channame "\n")))))))
+           (funcall insert-fn channame chanbuf chanlist)))
+       (when winstart
+         (set-window-point window winstart)
+         (with-selected-window window (recenter 0)))
+       (when (and erc-status-sidebar-highlight-active-buffer
+                  (marker-buffer erc-status-sidebar--active-marker))
+         (goto-char erc-status-sidebar--active-marker)
+         (require 'hl-line)
+         (unless hl-line-mode (hl-line-mode +1))
+         (hl-line-highlight))))))
 
 (defun erc-status-sidebar-kill ()
   "Close the ERC status sidebar and its buffer."
   (interactive)
+  (when (and erc-bufbar-mode (not erc--module-toggle-prefix-arg))
+    (erc-bufbar-mode -1))
   (ignore-errors (kill-buffer erc-status-sidebar-buffer-name)))
 
 (defun erc-status-sidebar-click (event)
   "Handle click EVENT in `erc-status-sidebar-mode-map'."
   (interactive "e")
   (save-excursion
-    (let ((window (posn-window (event-end event)))
+    (let ((window (posn-window (event-start event)))
           (pos (posn-point (event-end event))))
-      (set-buffer (window-buffer window))
-      (let ((buf (get-text-property pos 'erc-buf)))
-        (when buf
-          (select-window window)
-          (switch-to-buffer-other-window buf))))))
+      ;; Current buffer is "ERC Status" and its window is selected
+      (cl-assert (eq major-mode 'erc-status-sidebar-mode))
+      (cl-assert (eq (selected-window) window))
+      (cl-assert (eq (window-buffer window) (current-buffer)))
+      (when-let ((buf (get-text-property pos 'erc-buf)))
+        ;; Option operates relative to last selected window
+        (select-window (get-mru-window nil nil 'not-selected))
+        (pop-to-buffer buf erc-status-sidebar-click-display-action)))))
+
+(defun erc-status-sidebar-scroll-up (lines)
+  "Scroll sidebar buffer's content LINES linse upward.
+If LINES is nil, scroll up a full screen's worth."
+  (interactive "P")
+  (let ((other-window-scroll-buffer (erc-status-sidebar-get-buffer)))
+    (scroll-other-window lines)))
+
+(defun erc-status-sidebar-scroll-down (lines)
+  "Scroll sidebar buffer's content LINES lines downward.
+If LINES is nil, scroll down a full screen's worth."
+  (interactive "P")
+  (let ((other-window-scroll-buffer (erc-status-sidebar-get-buffer)))
+    (scroll-other-window-down lines)))
+
+(defun erc-status-sidebar-recenter (arg)
+  "Recenter the status sidebar.
+Expect `erc-status-sidebar-highlight-active-buffer' to be non-nil
+and to be invoked in a buffer matching the line currently
+highlighted."
+  (interactive "P")
+  (let* ((buf (erc-status-sidebar-get-buffer))
+         (win (get-buffer-window buf)))
+    (with-current-buffer buf
+      (when (and erc-status-sidebar--active-marker
+                 (marker-position erc-status-sidebar--active-marker))
+        (with-selected-window win
+          (goto-char erc-status-sidebar--active-marker)
+          (recenter arg t))))))
 
 (defvar erc-status-sidebar-mode-map
   (let ((map (make-sparse-keymap)))
@@ -268,13 +540,17 @@ erc-status-sidebar-set-window-preserve-size
 Note that preserve status needs to be reset when the window is
 manually resized, so `erc-status-sidebar-mode' adds this function
 to the `window-configuration-change-hook'."
-  (when (and (eq (selected-window) (erc-status-sidebar-get-window))
+  (when (and (eq (selected-window) (let (erc-status-sidebar-singular)
+                                     (erc-status-sidebar-get-window)))
              (fboundp 'window-preserve-size))
     (unless (eq (window-total-width) (window-min-size nil t))
       (apply #'window-preserve-size (selected-window) t t nil))))
 
 (define-derived-mode erc-status-sidebar-mode special-mode "ERC Sidebar"
   "Major mode for ERC status sidebar."
+  ;; Users invoking M-x erc-status-sidebar-mode most likely expect to
+  ;; summon the module's minor-mode, `erc-bufbar-mode'.
+  :interactive nil
   ;; Don't scroll the buffer horizontally, if a channel name is
   ;; obscured then the window can be resized.
   (setq-local auto-hscroll-mode nil)
diff --git a/test/lisp/erc/erc-scenarios-status-sidebar.el b/test/lisp/erc/erc-scenarios-status-sidebar.el
new file mode 100644
index 00000000000..aec61333e0b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-status-sidebar.el
@@ -0,0 +1,93 @@
+;;; erc-scenarios-status-sidebar.el --- sidebar/speedbar -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022-2023 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 <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-status-sidebar)
+
+
+(ert-deftest erc-scenarios-status-sidebar--bufbar ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/gapless-connect")
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-penalty erc-server-flood-penalty)
+       (erc-modules `(bufbar ,@erc-modules))
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to two different endpoints")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "MOTD File is missing"))
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "barnet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "marked as being away")))
+
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#bar"))
+      (funcall expect 10 "was created on")
+      (funcall expect 2 "his second fit"))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#foo"))
+      (funcall expect 10 "was created on")
+      (funcall expect 2 "no use of him")
+      (ert-info ("Activity marker is in the right spot")
+        (let ((obuf (window-buffer))) ; *scratch*
+          (set-window-buffer (selected-window) "#foo")
+          (erc-d-t-wait-for 5
+              (when noninteractive
+                (erc-status-sidebar-refresh))
+            (with-current-buffer "*ERC Status*"
+              (and (marker-position erc-status-sidebar--active-marker)
+                   (goto-char erc-status-sidebar--active-marker)
+                   ;; The " [N]" suffix disappears because it's selected
+                   (search-forward "#foo" (pos-eol) t))))
+          (set-window-buffer (selected-window) obuf))))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "*ERC Status*"))
+      (ert-info ("Hierarchy printed correctly")
+        (funcall expect 10 "barnet [")
+        (funcall expect 10 "#bar [")
+        (funcall expect 10 "foonet [")
+        (funcall expect 10 "#foo")))
+
+    (with-current-buffer "#foo"
+      (ert-info ("Core toggle and kill commands work")
+        ;; Avoid using API, e.g., `erc-status-sidebar-buffer-exists-p',
+        ;; etc. for testing commands that call those same functions.
+        (should (get-buffer-window "*ERC Status*"))
+        (erc-bufbar-mode -1)
+        (should-not (get-buffer-window "*ERC Status*"))
+        (erc-status-sidebar-kill)
+        (should-not (get-buffer "*ERC Status*"))))))
+
+;;; erc-scenarios-status-sidebar.el ends here
diff --git a/test/lisp/erc/resources/base/gapless-connect/foonet.eld b/test/lisp/erc/resources/base/gapless-connect/foonet.eld
index 4ac4a3e5968..10b742fdb34 100644
--- a/test/lisp/erc/resources/base/gapless-connect/foonet.eld
+++ b/test/lisp/erc/resources/base/gapless-connect/foonet.eld
@@ -1,7 +1,7 @@
 ;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :foonet:changeme"))
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((pass 10 "PASS :foonet:changeme"))
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
  (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
  (0 ":irc.foonet.org 003 tester :This server was created Sun, 25 Apr 2021 11:28:28 UTC")
@@ -21,7 +21,7 @@
  ;; No mode answer
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":[email-masked] JOIN #foo")
- (0 ":irc.foonet.org 353 tester = #foo :joe @mike tester")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob tester")
  (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
  (0 ":[email-masked] PRIVMSG #foo :Buffer Playback...")
  (0 ":[email-masked] PRIVMSG #foo :[07:02:41] bob: To-morrow is the joyful day, Audrey; to-morrow will we be married.")
-- 
2.40.0

From fd2ec89c40dd52322c8196c3653e06c937176bc7 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 4 May 2023 00:01:11 -0700
Subject: [PATCH 4/5] [5.6] Add erc-status-sidebar integration to erc-speedbar

* lisp/erc/erc-speedbar.el: Require `erc-button' atop file and don't
bother loading `dframe', which `speedbar' handles for us.
(erc-speedbar-hide-mode-topic): New option determining whether to hide
the mode and topic.
(erc-speedbar-my-nick-face): New option for determining face to use
when displaying user's current nick.
(erc-speedbar-browser): Call `erc-install-speedbar-variables'
explicitly and remove top-level `with-eval-after-load'.
(erc-speedbar-insert-target): Add parenthesized channel count after
channel name in server and channel views.
(erc-speedbar-expand-channel): Hide mode and topic depending on option
`erc-speedbar-hide-mode-topic' and pass buffer to
`erc-speedbar-insert-user'.
(erc-speedbar--nick-face-function): New internal function-interface
variable.
(erc-speedbar--highlight-self-and-ops): New function to serve as
default value for `erc-speedbar--nick-face-function'.
(erc-speedbar--on-click): Dispatch `erc-nick-popup' after trimming
status chars.
(erc-speedbar-insert-user): Revise doc string.  Call
`erc-speedbar--nick-face-function' to determine face.  Change
token for both expansion and on-click text props.  Assign
`erc-speedbar--on-click' as the mouse handler for nick items.
(erc-speedbar-emulated-sidebar-width): New option to control width
of nicknames list window.
(erc-speedbar): Mention `erc-nickbar-mode' in group doc string.
(erc-speedbar--buffer-options): Variable to override options locally
in speedbar buffer.
(erc-speedbar--hidden-speedbar-frame): Add variable to hold original
`speedbar-frame' before spoofing by setting to selected frame
containing window showing ERC buffer.
(erc-speedbar--dframe-controlled) Add function to overwrite
`speedbar-frame-mode' as `dframe-controlled' in speedbar buffer.
(erc-speedbar--emulate-sidebar-set-window-preserve-size,
erc-speedbar--status-sidebar-mode--unhook): Add function
to ensure status sidebar is showing correctly and helper to
unregister from hook on teardown.
(erc-speedbar--emulate-sidebar): Add function to
control sidebar nicknames setup.
(erc-speedbar--toggle-nicknames-sidebar): Add toggle function
for sidebar or speedbar.
(erc-speedbar--ensure): Add helper function to show speedbar if its
hidden or create one if none exists.
(erc-nickbar-mode, erc-nickbar-enable, erc-nickbar-disable):
Add new module.
(erc-speedbar-toggle-nicknames-window-lock,
erc-speedbar-close-nicknames-window): Add commands to close speedbar
window and toggle its cyclability.
(erc-speedbar--compose-nicks-face): Add helper for nicks integration.
* test/lisp/erc/erc-scenarios-status-sidebar.el
(erc-scenarios-status-sidebar--nickbar): New test, unfortunately only
runs in terminals.
---
 lisp/erc/erc-speedbar.el                      | 284 ++++++++++++++++--
 test/lisp/erc/erc-scenarios-status-sidebar.el |  76 +++++
 2 files changed, 342 insertions(+), 18 deletions(-)

diff --git a/lisp/erc/erc-speedbar.el b/lisp/erc/erc-speedbar.el
index a9443e0ea17..21ad4c6f97a 100644
--- a/lisp/erc/erc-speedbar.el
+++ b/lisp/erc/erc-speedbar.el
@@ -32,20 +32,31 @@
 ;;   update-channel, update-nick, remove-nick-from-channel, ...
 ;; * Use indicator-strings for op/voice
 ;; * Extract/convert face notes field from bbdb if available
+;; * Write tests that run in a term-mode subprocess
 ;;
 ;;; Code:
 
 (require 'erc)
 (require 'erc-goodies)
+(require 'erc-button)
 (require 'speedbar)
-(condition-case nil (require 'dframe) (error nil))
 
 ;;; Customization:
 
 (defgroup erc-speedbar nil
-  "Integration of ERC in the Speedbar."
+  "Speedbar integration for ERC.
+To open an ERC-flavored speedbar in a separate frame, run the
+command `erc-speedbar-browser'.  To use a window-based proxy
+instead, run \\[erc-nickbar-mode] in a connected ERC buffer or
+put `nickbar' in `erc-modules' before connecting.  See Info
+node `(speedbar) Top' for more about the underlying integration."
   :group 'erc)
 
+(defcustom erc-speedbar-nicknames-window-width 18
+  "Default width of the nicknames sidebar (in columns)."
+  :package-version '(ERC . "5.7")
+  :type 'integer)
+
 (defcustom erc-speedbar-sort-users-type 'activity
   "How channel nicknames are sorted.
 
@@ -56,6 +67,23 @@ erc-speedbar-sort-users-type
 		 (const :tag "Sort users alphabetically" alphabetical)
 		 (const :tag "Do not sort users" nil)))
 
+(defcustom erc-speedbar-hide-mode-topic 'headerline
+  "Hide mode and topic lines."
+  :package-version '(ERC . "5.7") ; FIXME sync on release
+  :type '(choice (const :tag "Always show" nil)
+                 (const :tag "Always hide" t)
+                 (const :tag "Omit when headerline visible" headerline)))
+
+(defcustom erc-speedbar-my-nick-face t
+  "A face to use for your nickname.
+When the value is t, ERC uses `erc-current-nick-face' if
+`erc-match' has been loaded and `erc-my-nick-face' otherwise.
+When using the `nicks' module, you can see your nick as it
+appears to others by coordinating with the option
+`erc-nicks-skip-faces'."
+  :package-version '(ERC . "5.7")
+  :type '(choice face (const :tag "Match or own input face" t)))
+
 (defvar erc-speedbar-key-map nil
   "Keymap used when in erc display mode.")
 
@@ -88,10 +116,6 @@ erc-speedbar-menu-items
 		     (looking-at "[0-9]+: *.-. "))])
   "Additional menu-items to add to speedbar frame.")
 
-;; Make sure our special speedbar major mode is loaded
-(with-eval-after-load 'speedbar
-  (erc-install-speedbar-variables))
-
 ;;; ERC hierarchy display method
 ;;;###autoload
 (defun erc-speedbar-browser ()
@@ -99,6 +123,7 @@ erc-speedbar-browser
 This will add a speedbar major display mode."
   (interactive)
   (require 'speedbar)
+  (erc-install-speedbar-variables)
   ;; Make sure that speedbar is active
   (speedbar-frame-mode 1)
   ;; Now, throw us into Info mode on speedbar.
@@ -169,12 +194,18 @@ erc-speedbar-channel-buttons
 	  t)))))
 
 (defun erc-speedbar-insert-target (buffer depth)
-  (if (with-current-buffer buffer
-	(erc-channel-p (erc-default-target)))
-      (speedbar-make-tag-line
-       'bracket ?+ 'erc-speedbar-expand-channel buffer
-       (buffer-name buffer) 'erc-speedbar-goto-buffer buffer nil
-       depth)
+  (if (erc--target-channel-p (buffer-local-value 'erc--target buffer))
+      (progn
+        (speedbar-make-tag-line
+         'bracket ?+ 'erc-speedbar-expand-channel buffer
+         (erc--target-string (buffer-local-value 'erc--target buffer))
+         'erc-speedbar-goto-buffer buffer nil
+         depth)
+        (save-excursion
+          (forward-line -1)
+          (let ((table (buffer-local-value 'erc-channel-users buffer)))
+            (speedbar-add-indicator (format "(%d)" (hash-table-count table)))
+            (rx "(" (+ (any "0-9")) ")"))))
     ;; Query target
     (speedbar-make-tag-line
      nil nil nil nil
@@ -220,6 +251,13 @@ erc-speedbar-expand-channel
 	    'angle ?i nil nil
 	    (concat "Topic: " topic) nil nil nil
 	    (1+ indent)))
+         (unless (pcase erc-speedbar-hide-mode-topic
+                   ('nil 'show)
+                   ('headerline (null erc-header-line-format)))
+           (save-excursion
+             (goto-char (point-max))
+             (forward-line (if (string= topic "") -1 -2))
+             (put-text-property (pos-bol) (point-max) 'invisible t)))
 	 (let ((names (cond ((eq erc-speedbar-sort-users-type 'alphabetical)
 			     (erc-sort-channel-users-alphabetically
 			      (with-current-buffer channel
@@ -233,17 +271,52 @@ erc-speedbar-expand-channel
 	   (when names
 	     (speedbar-with-writable
 	      (dolist (entry names)
-		(erc-speedbar-insert-user entry ?+ (1+ indent))))))))))
+                (erc-speedbar-insert-user entry ?+ (1+ indent) channel)))))))))
    ((string-search "-" text)
     (speedbar-change-expand-button-char ?+)
     (speedbar-delete-subblock indent))
    (t (error "Ooops... not sure what to do")))
   (speedbar-center-buffer-smartly))
 
-(defun erc-speedbar-insert-user (entry exp-char indent)
+(defvar erc-speedbar--nick-face-function #'erc-speedbar--highlight-self-and-ops
+  "Function called when finding a face for fontifying nicks.
+Called with the proposed nick, the `erc-server-user', and the
+`erc-channel-user'.  Should return any valid face, possibly
+composed or anonymous, or nil.")
+
+(defun erc-speedbar--highlight-self-and-ops (buffer user cuser)
+  "Highlight own nick and op'd users in the speedbar."
+  (with-current-buffer buffer
+    (if (erc-current-nick-p (erc-server-user-nickname user))
+        (pcase erc-speedbar-my-nick-face
+          ('t (if (featurep 'erc-match)
+                  'erc-current-nick-face
+                'erc-my-nick-face))
+          (v v))
+      ;; FIXME overload `erc-channel-user-owner-p' and friends to
+      ;; accept an `erc-channel-user' object and replace this unrolled
+      ;; stuff with a single call to `erc-get-user-mode-prefix'.
+      (and cuser (or (erc-channel-user-owner cuser)
+                     (erc-channel-user-admin cuser)
+                     (erc-channel-user-op cuser)
+                     (erc-channel-user-halfop cuser)
+                     (erc-channel-user-voice cuser))
+           erc-button-nickname-face))))
+
+(defun erc-speedbar--on-click (nick sbtoken _indent)
+  ;; 0: finger, 1: name, 2: info, 3: buffer-name
+  (with-current-buffer (nth 3 sbtoken)
+    (erc-nick-popup (string-trim-left nick "[~&@%+]+"))))
+
+(defun erc-speedbar-insert-user (entry exp-char indent &optional buffer)
   "Insert one user based on the channel member list ENTRY.
-EXP-CHAR is the expansion character to use.
-INDENT is the current indentation level."
+Expect EXP-CHAR to be the expansion character to use, INDENT the
+current indentation level, and BUFFER the associated channel or
+query buffer.  Set the `speedbar-function' text property to
+`erc-speedbar--on-click', which is called with the formatted
+nick, a so-called \"token\", and the indent level.  The token is
+a list of four items: the userhost, the GECOS, the current
+`erc-server-user' info slot, and the associated buffer."
   (let* ((user (car entry))
 	 (cuser (cdr entry))
 	 (nick (erc-server-user-nickname user))
@@ -255,11 +328,12 @@ erc-speedbar-insert-user
 	 (op (and cuser (erc-channel-user-op cuser)))
 	 (nick-str (concat (if op "@" "") (if voice "+" "") nick))
 	 (finger (concat login (when (or login host) "@") host))
-	 (sbtoken (list finger name info)))
+         (sbtoken (list finger name info (buffer-name buffer))))
     (if (or login host name info) ; we want to be expandable
 	(speedbar-make-tag-line
 	 'bracket ?+ 'erc-speedbar-expand-user sbtoken
-	 nick-str nil sbtoken nil
+         nick-str #'erc-speedbar--on-click sbtoken
+         (funcall erc-speedbar--nick-face-function buffer user cuser)
 	 indent)
       (when (equal exp-char ?-)
 	(forward-line -1)
@@ -357,6 +431,180 @@ erc-speedbar-item-info
 	  (t
 	   (message "%s" txt)))))
 
+
+;;;; Status-sidebar integration
+
+(defvar erc-status-sidebar-buffer-name)
+(declare-function erc-status-sidebar-set-window-preserve-size
+                  "erc-status-sidebar" nil)
+(declare-function erc-status-sidebar-mode--unhook "erc-status-sidebar" nil)
+
+(defvar erc-speedbar--buffer-options
+  '((speedbar-update-flag . t)
+    (speedbar-use-images . nil)
+    (speedbar-hide-button-brackets-flag . t)))
+
+(defvar erc-speedbar--hidden-speedbar-frame nil)
+
+(defun erc-speedbar--emulate-sidebar-set-window-preserve-size ()
+  (let ((erc-status-sidebar-buffer-name (buffer-name speedbar-buffer))
+        (display-buffer-overriding-action
+         `(display-buffer-in-side-window
+           . ((side . right)
+              (window-width . ,erc-speedbar-nicknames-window-width)))))
+    (erc-status-sidebar-set-window-preserve-size)
+    (when-let ((window (get-buffer-window speedbar-buffer)))
+      (set-window-parameter window 'no-other-window nil)
+      (internal-show-cursor window t))))
+
+(defun erc-speedbar--status-sidebar-mode--unhook ()
+  "Remove hooks installed by `erc-status-sidebar-mode'."
+  (remove-hook 'window-configuration-change-hook
+               #'erc-speedbar--emulate-sidebar-set-window-preserve-size))
+
+(defun erc-speedbar--emulate-sidebar ()
+  (require 'erc-status-sidebar)
+  (cl-assert speedbar-frame)
+  (cl-assert (eq speedbar-buffer (current-buffer)))
+  (cl-assert (eq speedbar-frame (selected-frame)))
+  (setq erc-speedbar--hidden-speedbar-frame speedbar-frame
+        dframe-controlled #'erc-speedbar--dframe-controlled)
+  (add-hook 'window-configuration-change-hook
+            #'erc-speedbar--emulate-sidebar-set-window-preserve-size nil t)
+  (add-hook 'kill-buffer-hook
+            #'erc-speedbar--status-sidebar-mode--unhook nil t)
+  (with-current-buffer speedbar-buffer
+    (pcase-dolist (`(,var . ,val) erc-speedbar--buffer-options)
+      (set (make-local-variable var) val)))
+  (when (memq 'nicks erc-modules)
+    (with-current-buffer speedbar-buffer
+      (add-function :around (local 'erc-speedbar--nick-face-function)
+                    #'erc-speedbar--compose-nicks-face))))
+
+(defun erc-speedbar--toggle-nicknames-sidebar (arg)
+  (let ((force (numberp arg)))
+    (if speedbar-buffer
+        (progn
+          (cl-assert (buffer-live-p speedbar-buffer))
+          (if (or (and force (< arg 0))
+                  (and (not force) (get-buffer-window speedbar-buffer nil)))
+              (erc-speedbar-close-nicknames-window nil)
+            (when (or (not force) (>= arg 0))
+              (with-selected-frame speedbar-frame
+                (erc-speedbar--emulate-sidebar-set-window-preserve-size)))))
+      (when (or (not force) (>= arg 0))
+        (let ((speedbar-frame-parameters (backquote-list*
+                                          '(visibility . nil)
+                                          '(no-other-frame . t)
+                                          speedbar-frame-parameters))
+              (speedbar-after-create-hook #'erc-speedbar--emulate-sidebar))
+          (erc-speedbar-browser)
+          ;; If we put the remaining parts in the "create hook" along
+          ;; with everything else, the frame with `window-main-window'
+          ;; gets raised and steals focus if you've switched away from
+          ;; Emacs in the meantime.
+          (make-frame-invisible speedbar-frame)
+          (select-frame (setq speedbar-frame (previous-frame)))
+          (erc-speedbar--emulate-sidebar-set-window-preserve-size))))))
+
+(defun erc-speedbar--ensure (&optional force)
+  (when (or (erc-server-buffer) force)
+    (erc-speedbar--toggle-nicknames-sidebar +1)
+    (speedbar-enable-update)))
+
+;;;###autoload(autoload 'erc-nickbar-mode "erc-speedbar" nil t)
+(define-erc-module nickbar nil
+  "Show nicknames in a side window.
+When enabling, create a speedbar session if one doesn't exist and
+show its buffer in an `erc-status-sidebar' window instead of a
+separate frame.  When disabling, close the window or, with a
+negative prefix arg, destroy the session.
+
+WARNING: it's suspected that this module may perform unwanted
+side effects like raising frames and/or stealing input focus.  If
+you witness such an occurrence, and can reproduce it, please file
+a bug report with \\[erc-bug]."
+  ((add-hook 'erc--setup-buffer-hook #'erc-speedbar--ensure)
+   (erc-speedbar--ensure)
+   (unless (or erc--updating-modules-p
+               (and-let* ((speedbar-buffer)
+                          (win (get-buffer-window speedbar-buffer 'all-frames))
+                          ((eq speedbar-frame (window-frame win))))))
+     (if speedbar-buffer
+         (erc-speedbar--ensure 'force)
+       (cl-assert (not (derived-mode-p 'erc-mode)))
+       (setq erc-nickbar-mode nil)
+       (user-error "Cannot initialize `%s' in a non-ERC buffer"
+                   'erc-nickbar-mode))))
+  ((remove-hook 'erc--setup-buffer-hook #'erc-speedbar--ensure)
+   (speedbar-disable-update)
+   (erc-speedbar--toggle-nicknames-sidebar -1)
+   (when-let* ((arg erc--module-toggle-prefix-arg)
+               ((numberp arg))
+               ((< arg 0)))
+     (erc-speedbar-close-nicknames-window 'kill))))
+
+(defun erc-speedbar--dframe-controlled (arg)
+  (when (and erc-speedbar--hidden-speedbar-frame (numberp arg) (< arg 0))
+    (when erc-nickbar-mode
+      (erc-nickbar-mode -1))
+    (setq speedbar-frame erc-speedbar--hidden-speedbar-frame
+          erc-speedbar--hidden-speedbar-frame nil)
+    ;; It's unknown whether leaving the frame invisible interferes
+    ;; with the upstream teardown procedure.
+    (when (display-graphic-p)
+      (make-frame-visible speedbar-frame))
+    (speedbar-frame-mode arg)
+    (when speedbar-buffer
+      (kill-buffer speedbar-buffer)
+      (setq speedbar-buffer nil))))
+
+(defun erc-speedbar-toggle-nicknames-window-lock ()
+  "Toggle whether nicknames window is selectable with \\[other-window]."
+  (interactive)
+  (unless erc-nickbar-mode
+    (user-error "`erc-nickbar-mode' inactive"))
+  (when-let* ((window (get-buffer-window speedbar-buffer)))
+    (let ((val (window-parameter window 'no-other-window)))
+      (set-window-parameter window 'no-other-window (not val))
+      (message "nick-window: %s" (if val "selectable" "protected")))))
+
+(defun erc-speedbar-close-nicknames-window (kill)
+  (interactive "P")
+  (if kill
+      (with-current-buffer speedbar-buffer
+        (dframe-close-frame)
+        (cl-assert (not erc-nickbar-mode))
+        (setq erc-speedbar--hidden-speedbar-frame nil))
+    (dolist (window (get-buffer-window-list speedbar-buffer nil t))
+      (unless (frame-root-window-p window)
+        (when erc-speedbar--hidden-speedbar-frame
+          (cl-assert (not (eq (window-frame window)
+                              erc-speedbar--hidden-speedbar-frame))))
+        (delete-window window)))))
+
+
+;;;; Nicks integration
+
+(defvar erc-nicks--phony-face)
+(declare-function erc-nicks--highlight "erc-nicks" (nick-object))
+
+(defun erc-speedbar--compose-nicks-face (orig buffer user cuser)
+  (require 'erc-nicks)
+  (let ((rv (funcall orig buffer user cuser)))
+    (if-let* ((nobj (make-erc-button--nick
+                     :downcased (erc-downcase (erc-server-user-nickname user))
+                     :user user
+                     :cuser cuser))
+              (erc-nicks--phony-face (or rv t))
+              (nobj (with-current-buffer buffer
+                      (erc-nicks--highlight nobj)))
+              (face (erc-button--nick-erc-button-nickname-face nobj))
+              ((not (eq face erc-button-nickname-face))))
+        (cons face (ensure-list rv))
+      rv)))
+
+
 (provide 'erc-speedbar)
 ;;; erc-speedbar.el ends here
 ;;
diff --git a/test/lisp/erc/erc-scenarios-status-sidebar.el b/test/lisp/erc/erc-scenarios-status-sidebar.el
index aec61333e0b..6ac1821ebb7 100644
--- a/test/lisp/erc/erc-scenarios-status-sidebar.el
+++ b/test/lisp/erc/erc-scenarios-status-sidebar.el
@@ -90,4 +90,80 @@ erc-scenarios-status-sidebar--bufbar
         (erc-status-sidebar-kill)
         (should-not (get-buffer "*ERC Status*"))))))
 
+;; No need to pollute the global obarray since we can't currently run
+;; this on EMBA (it requires a terminal).  Please try running this
+;; test interactively with both graphical Emacs and non.
+(declare-function erc-nickbar-mode "erc-speedbar" (arg))
+(declare-function erc-speedbar-close-nicknames-window "erc-speedbar" (kill))
+(declare-function speedbar-timer-fn "speedbar" nil)
+(defvar erc-nickbar-mode)
+(defvar speedbar-buffer)
+
+(ert-deftest erc-scenarios-status-sidebar--nickbar ()
+  :tags '(:unstable :expensive-test)
+  (when noninteractive (ert-skip "Interactive only"))
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/gapless-connect")
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-penalty erc-server-flood-penalty)
+       (erc-modules `(nickbar ,@erc-modules))
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to two different endpoints")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "MOTD File is missing"))
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "barnet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "marked as being away")))
+
+    (erc-d-t-wait-for 20 (get-buffer "#bar"))
+    (with-current-buffer (pop-to-buffer "#bar")
+      (funcall expect 10 "was created on")
+      (funcall expect 2 "his second fit")
+      (erc-d-t-wait-for 10 (and speedbar-buffer (get-buffer speedbar-buffer)))
+      (speedbar-timer-fn)
+      (with-current-buffer speedbar-buffer
+        (funcall expect 10 "#bar (3)")
+        (funcall expect 10 '(| "@mike" "joe"))
+        (funcall expect 10 '(| "@mike" "joe"))
+        (funcall expect 10 "tester")))
+
+    (erc-d-t-wait-for 20 (get-buffer "#foo"))
+    (with-current-buffer (pop-to-buffer "#foo")
+      (delete-other-windows)
+      (funcall expect 10 "was created on")
+      (funcall expect 2 "no use of him")
+      (speedbar-timer-fn)
+      (with-current-buffer speedbar-buffer
+        (funcall expect 10 "#foo (3)")
+        (funcall expect 10 '(| "alice" "@bob"))
+        (funcall expect 10 '(| "alice" "@bob"))
+        (funcall expect 10 "tester")))
+
+    (with-current-buffer "#foo"
+      (ert-info ("Core toggle and kill commands work")
+        ;; Avoid using API, e.g., `erc-status-sidebar-buffer-exists-p',
+        ;; etc. for testing commands that call those same functions.
+        (erc-nickbar-mode -1)
+        (should-not (and speedbar-buffer
+                         (get-buffer-window speedbar-buffer)))
+        (erc-nickbar-mode +1)
+        (should (and speedbar-buffer
+                     (get-buffer-window speedbar-buffer)))
+        (should (get-buffer " SPEEDBAR"))
+        (erc-speedbar-close-nicknames-window 'kill)
+        (should-not (get-buffer " SPEEDBAR"))
+        (should-not erc-nickbar-mode)
+        (should-not (cdr (frame-list)))))))
+
 ;;; erc-scenarios-status-sidebar.el ends here
-- 
2.40.0

From 93f31ce4e062acd8ea6e87ea8023707b9e4b383b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 4 May 2023 00:01:11 -0700
Subject: [PATCH 5/5] [5.6] Add new ERC modules bufbar and nickbar

* doc/misc/erc.texi: Add `bufbar' and `nickbar' to known modules.
* erc/ERC-NEWS: Mention erc-status-sidebar and erc-speedbar
improvements.
* lisp/erc/erc.el (erc-modules): Add `bufbar' and `nickbar' to
selection of offered modules.
* test/lisp/erc/erc-tests.el (erc-tests--modules): Add `bufbar' and
`nickbar'.
---
 doc/misc/erc.texi          |  9 +++++++++
 etc/ERC-NEWS               | 16 ++++++++++++++++
 lisp/erc/erc.el            |  2 ++
 test/lisp/erc/erc-tests.el |  4 ++--
 4 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index f7036e57638..7c625c2fd02 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -418,6 +418,10 @@ Modules
 @item bbdb
 Integrate with the Big Brother Database
 
+@cindex modules, bufbar
+@item bufbar
+List ERC buffers belonging to a live connection in a side window
+
 @cindex modules, button
 @item button
 Buttonize URLs, nicknames, and other text
@@ -463,6 +467,11 @@ Modules
 @item nicks
 Automatically colorize nicks
 
+@cindex modules, nickbar
+@item nickbar
+List participating nicks for the current target buffer in a side
+window
+
 @cindex modules, noncommands
 @item noncommands
 Don't display non-IRC commands after evaluation
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 41af8b88277..6922ae22482 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -72,6 +72,22 @@ widget has been an age-old annoyance for new users.  Previously
 ineffective, this method now actually works, but it also admonishes
 users to edit the 'erc-modules' widget instead.
 
+** ERC's status-sidebar now has an accompanying module.
+Users can now add 'bufbar' to 'erc-modules' to achieve the same effect
+as toggling 'erc-status-sidebar-open' manually at the start of an IRC
+session.  The module has also been outfitted to show channels and
+queries under their respective servers by default.  To avoid
+confusion, the major mode used for the sidebar buffer itself,
+'erc-status-sidebar-mode', is no longer available interactively.
+
+** A new spin on a classic integration in erc-speedbar.
+Add 'nickbar' to 'erc-modules' to spawn a dynamically updating side
+window listing all the users in any target buffer.  It's powered by
+the same speedbar.el integration you've always known, except this
+one's optionally accessible from the keyboard, just like any other
+side window.  Hit '<RET>' over a nick to spawn a "/QUERY" or a
+"Lastlog" (Occur) session.  See 'erc-nickbar-mode' for more.
+
 ** The option 'erc-timestamp-use-align-to' is more versatile.
 While this option has always offered to right-align stamps via the
 'display' text property, it's now more effective at doing so when set
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 5a3b312b53b..2f26985f74d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2006,6 +2006,7 @@ erc-modules
     :greedy t
     (const :tag "autoaway: Set away status automatically" autoaway)
     (const :tag "autojoin: Join channels automatically" autojoin)
+    (const :tag "bufbar: Show ERC buffers in a side window" bufbar)
     (const :tag "button: Buttonize URLs, nicknames, and other text" button)
     (const :tag "capab: Mark unidentified users on servers supporting CAPAB"
            capab-identify)
@@ -2026,6 +2027,7 @@ erc-modules
            move-to-prompt)
     (const :tag "netsplit: Detect netsplits" netsplit)
     (const :tag "networks: Provide data about IRC networks" networks)
+    (const :tag "nickbar: Show nicknames in a dyamic side window" nickbar)
     (const :tag "nicks: Uniquely colorize nicknames in target buffers" nicks)
     (const :tag "noncommands: Don't display non-IRC commands after evaluation"
            noncommands)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index de472527bde..4432dc58082 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1951,9 +1951,9 @@ erc-handle-irc-url
     (kill-buffer "#chan")))
 
 (defconst erc-tests--modules
-  '( autoaway autojoin button capab-identify completion dcc fill identd
+  '( autoaway autojoin bufbar button capab-identify completion dcc fill identd
      imenu irccontrols keep-place list log match menu move-to-prompt netsplit
-     networks nicks noncommands notifications notify page readonly
+     networks nickbar nicks noncommands notifications notify page readonly
      replace ring sasl scrolltobottom services smiley sound
      spelling stamp track truncate unmorse xdcc))
 
-- 
2.40.0