Subject: bug#77361: [PATCH] New user option to hide minor mode lighters

From: Pengji Zhang <[email-masked]>
Date: Sun, 06 Apr 2025 16:05:00 +0800

Eli Zaretskii <[email-masked]> writes:

> Please in future submissions of the patch mention the bug number in
> the log message.

Thanks for the review! Bug number added in the updated patch.

>> --- a/doc/lispref/modes.texi
>> +++ b/doc/lispref/modes.texi
>> @@ -2163,6 +2163,18 @@ Mode Line Basics
>>  variable can be buffer-local to only compress mode-lines in certain
>>  buffers.
>>  
>> +@vindex mode-line-collapse-minor-modes
>> +  To further ``compress'' the mode line, you may customize the
>> +@code{mode-line-collapse-minor-modes} option to a non-@code{nil} value,
>> +and Emacs will hide some minor mode indicators on the mode line by
>> +collapsing them into a single clickable button.  The option can also be
>> +a list of symbols to select minor modes indicators to hide or show.  If
>> +the list starts with the symbol @code{not}, it specifies minor modes to
>> +show, otherwise it means minor modes to hide.  For example, setting it
>> +to @code{(not flymake-mode)} makes only the indicator for Flymake mode
>> +shown, and setting it to @code{(eldoc-mode)} hides only the indicator
>> +for ElDoc mode.
>
> I think this text and the preceding paragraph should be moved to the
> Emacs user manual, into the "Optional Mode Line" node.  These are
> user-facing features, so their place is not in the ELisp Reference
> manual.
>
> For the user manual, the above description of the possible values of
> mode-line-collapse-minor-modes is too detailed.  I suggest to have
> there only the first sentence, and then refer to the doc string for
> the various alternative forms of the value.

I have moved the paragraph for 'mode-line-compact' to the "Optional Mode
Line" node, and added the first sentence for
'mode-line-collapse-minor-modes' to that paragraph.

>> ++++
>> +*** New user option 'mode-line-collapse-minor-modes'.
>> +If non-nil, minor mode lighters on the mode line are collapsed into a
>> +single button.  It could also be a list to specify minor mode lighters
>                    ^^^^^^^^^^^^^^^^^^^^^^^
> "The value could also be a list..."

Fixed.

>> +(defcustom mode-line-collapse-minor-modes nil
>> +  "Minor modes for which mode line lighters are hidden.
>> +Hidden lighters are collapsed into one.
>> +
>> +The value could be a list (MODES ...) which means to collapse lighters
>> +only for MODES, or a list (not MODES ...) which means to collapse all
>> +lighters for minor modes not in MODES.  Other non-nil values make all
>> +lighters hidden."
>> +  :type '(choice (const :tag "No modes" nil)
>> +                 (repeat :tag "Modes" symbol)
>> +                 (cons :tag "All modes except"
>> +                       (const not) (repeat symbol))
>> +                 (const :tag "All modes" t))
>> +  :group 'mode-line)
>
> The :version tag is missing.

Added.

>From ff653f559b9cd984f584756741b7b52815befe8d Mon Sep 17 00:00:00 2001
From: Pengji Zhang <[email-masked]>
Date: Sat, 29 Mar 2025 19:04:58 +0800
Subject: [PATCH] New user option to hide minor mode lighters (bug#77361)

* lisp/bindings.el (mode-line-collapse-minor-modes): New user
option.
(mode-line-minor-modes): New variable to hold mode line
constructs for minor modes.
(mode-line--make-lighter-menu): New helper function to generate
the menu for hidden minor modes.
(mode-line--minor-modes): New helper function to computer mode
line constructs for minor mode lighters.
(mode-line-modes): Use the new variable 'mode-line-minor-modes',
and adjust the order of elements so the indicator for hidden
minor modes is shown towards the end.

* doc/lispref/modes.texi (Mode Line Basics): Move the paragraph
for 'mode-line-compact' from here...
* doc/emacs/display.texi (Optional Mode Line): ...to here, and
document the new user option.
* etc/NEWS: Annouce the new user option.
---
 doc/emacs/display.texi |  15 +++++
 doc/lispref/modes.texi |  11 ----
 etc/NEWS               |   9 +++
 lisp/bindings.el       | 128 ++++++++++++++++++++++++++++++++++++++---
 4 files changed, 145 insertions(+), 18 deletions(-)

diff --git a/doc/emacs/display.texi b/doc/emacs/display.texi
index 520d3289f2d..ad496b5b1cd 100644
--- a/doc/emacs/display.texi
+++ b/doc/emacs/display.texi
@@ -1811,6 +1811,21 @@ Optional Mode Line
 @code{eol-mnemonic-dos}, @code{eol-mnemonic-mac}, and
 @code{eol-mnemonic-undecided} to the strings you prefer.
 
+@vindex mode-line-compact
+@vindex mode-line-collapse-minor-modes
+  Some modes put a lot of data in the mode line, pushing elements at the
+end of the mode line off to the right.  Emacs can ``compress'' the mode
+line if the @code{mode-line-compact} variable is non-@code{nil} by
+turning stretches of spaces into a single space.  If this variable is
+@code{long}, this is only done when the mode line is wider than the
+currently selected window.  (This computation is approximate, based on
+the number of characters, and not their displayed width.)  This variable
+can be buffer-local to only compress mode-lines in certain buffers.  To
+further ``compress'' the mode line, you may customize the
+@code{mode-line-collapse-minor-modes} option to a non-@code{nil} value,
+and Emacs will hide some minor mode indicators on the mode line by
+collapsing them into a single clickable button.
+
 @node Text Display
 @section How Text Is Displayed
 @cindex characters (in text)
diff --git a/doc/lispref/modes.texi b/doc/lispref/modes.texi
index 788d98fdf20..0dd73fe17a8 100644
--- a/doc/lispref/modes.texi
+++ b/doc/lispref/modes.texi
@@ -2152,17 +2152,6 @@ Mode Line Basics
 @end lisp
 @end defun
 
-@vindex mode-line-compact
-  Some modes put a lot of data in the mode line, pushing elements at
-the end of the mode line off to the right.  Emacs can ``compress'' the
-mode line if the @code{mode-line-compact} variable is non-@code{nil}
-by turning stretches of spaces into a single space.  If this variable
-is @code{long}, this is only done when the mode line is wider than the
-currently selected window.  (This computation is approximate, based on
-the number of characters, and not their displayed width.)  This
-variable can be buffer-local to only compress mode-lines in certain
-buffers.
-
 @node Mode Line Data
 @subsection The Data Structure of the Mode Line
 @cindex mode line construct
diff --git a/etc/NEWS b/etc/NEWS
index 35e6edcd712..939dc748f54 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -322,6 +322,15 @@ This will inhibit implied resizing while a new frame is made and can be
 useful on tiling window managers where the initial frame size should be
 specified by external means.
 
+** Mode Line
+
++++
+*** New user option 'mode-line-collapse-minor-modes'.
+If non-nil, minor mode lighters on the mode line are collapsed into a
+single button.  The value could also be a list to specify minor mode
+lighters to hide or show.  The default value is nil, which retains the
+previous behavior of showing all minor mode lighters.
+
 ** Tab Bars and Tab Lines
 
 ---
diff --git a/lisp/bindings.el b/lisp/bindings.el
index 9707ce4b474..2d6e1579e10 100644
--- a/lisp/bindings.el
+++ b/lisp/bindings.el
@@ -429,6 +429,126 @@ bindings--sort-menu-keymap
                            (bindings--menu-item-string (cdr-safe b))))))
     (nconc (make-sparse-keymap prompt) bindings)))
 
+(defcustom mode-line-collapse-minor-modes nil
+  "Minor modes for which mode line lighters are hidden.
+Hidden lighters are collapsed into one.
+
+The value could be a list (MODES ...) which means to collapse lighters
+only for MODES, or a list (not MODES ...) which means to collapse all
+lighters for minor modes not in MODES.  Other non-nil values make all
+lighters hidden."
+  :type '(choice (const :tag "No modes" nil)
+                 (repeat :tag "Modes" symbol)
+                 (cons :tag "All modes except"
+                       (const not) (repeat symbol))
+                 (const :tag "All modes" t))
+  :group 'mode-line
+  :version "31.1")
+
+(defvar mode-line-minor-modes '(:eval (mode-line--minor-modes))
+  "Mode line construct for minor mode lighters.")
+;;;###autoload
+(put 'mode-line-minor-modes 'risky-local-variable t)
+
+(defun mode-line--make-lighter-menu (alist)
+  "Return a menu keymap for minor mode lighters in ALIST.
+ALIST should be in the same format as `minor-mode-alist'.
+
+Return nil if no lighters in ALIST should be visible, for example, there
+are no active minor modes or non-empty lighters."
+  (let ((menu (make-sparse-keymap "Minor Modes"))
+        (empty t))
+    (dolist (item alist)
+      (when-let* ((variable (car item))
+                  ((and (boundp variable)
+                        (symbol-value variable)))
+                  (lighter (format-mode-line `("" ,@(cdr-safe item))))
+                  ((not (string= lighter "")))
+                  (toggle (or (get variable :minor-mode-function) variable))
+                  ;; Follow the format in `mouse-minor-mode-menu'
+                  (name (format "%s - %s" lighter
+                                (capitalize
+                                 (string-replace
+                                  "-" " " (symbol-name toggle))))))
+        (when (eq ?  (aref name 0))
+          (setq name (substring name 1)))
+        (let* ((map (cdr-safe (assq variable minor-mode-map-alist)))
+               (mm-menu (and (keymapp map)
+                             (keymap-lookup map "<menu-bar>"))))
+          (setq mm-menu
+                (cond (mm-menu (mouse-menu-non-singleton mm-menu))
+                      ((fboundp toggle)
+                       (define-keymap :name name
+                         "<help>" (list 'menu-item
+                                        "Help for minor mode"
+                                        (lambda () (interactive)
+                                          (describe-function toggle)))
+                         "<turn-off>" (list 'menu-item
+                                            "Turn off minor mode"
+                                            toggle)))
+                      ;; No menu and not a minor mode function, so just
+                      ;; display the label without a sub-menu.
+                      (t nil)))
+          (keymap-set menu (format "<%s>" toggle)
+                      (list 'menu-item name mm-menu))
+          (setq empty nil))))
+    (and (not empty) menu)))
+
+(defun mode-line--minor-modes ()
+  "Compute mode line constructs for minor mode lighters."
+  (let (visible hidden)
+    (cond
+     ((not mode-line-collapse-minor-modes)
+      (setq visible minor-mode-alist
+            hidden nil))
+     ((eq 'not (car-safe mode-line-collapse-minor-modes))
+      (let ((modes (cdr mode-line-collapse-minor-modes)))
+        (dolist (item minor-mode-alist)
+          (if (memq (car item) modes)
+              (push item visible)
+            (push item hidden)))
+        (setq visible (nreverse visible)
+              hidden (nreverse hidden))))
+     ((listp mode-line-collapse-minor-modes)
+      (let ((modes mode-line-collapse-minor-modes))
+        (dolist (item minor-mode-alist)
+          (if (memq (car item) modes)
+              (push item hidden)
+            (push item visible)))
+        (setq visible (nreverse visible)
+              hidden (nreverse hidden))))
+     (t (setq visible nil
+              hidden minor-mode-alist)))
+    (list ""
+          `(:propertize ("" ,visible)
+                        mouse-face mode-line-highlight
+                        help-echo "Minor mode\n\
+mouse-1: Display minor mode menu\n\
+mouse-2: Show help for minor mode\n\
+mouse-3: Toggle minor modes"
+                        local-map ,mode-line-minor-mode-keymap)
+          (unless (string= "" (format-mode-line `("" ,hidden)))
+            (let* ((menu
+                    ;; FIXME: This is to defer the computation of the
+                    ;; menu, but may not play well with touchscreen.
+                    (lambda (e)
+                      (interactive "@e")
+                      (if-let* ((m (mode-line--make-lighter-menu hidden)))
+                          (popup-menu m e)
+                        (message "No menu available"))))
+                   (keymap
+                    (define-keymap
+                      :parent mode-line-minor-mode-keymap
+                      "<mode-line> <down-mouse-1>" menu
+                      "<mode-line> <mouse-2>" #'describe-mode)))
+              `(:propertize ,(if (char-displayable-p ?…) " …" " ...")
+                            mouse-face mode-line-highlight
+                            help-echo "Hidden minor modes\n\
+mouse-1: Display hidden minor modes\n\
+mouse-2: Show help for enabled minor modes\n\
+mouse-3: Toggle minor modes"
+                            local-map ,keymap))))))
+
 (defvar mode-line-major-mode-keymap
   (let ((map (make-sparse-keymap)))
     (define-key map [mode-line down-mouse-1]
@@ -466,17 +586,11 @@ mode-line-modes
 			mouse-face mode-line-highlight
 			local-map ,mode-line-major-mode-keymap)
 	  '("" mode-line-process)
-	  `(:propertize ("" minor-mode-alist)
-			mouse-face mode-line-highlight
-			help-echo "Minor mode\n\
-mouse-1: Display minor mode menu\n\
-mouse-2: Show help for minor mode\n\
-mouse-3: Toggle minor modes"
-			local-map ,mode-line-minor-mode-keymap)
 	  (propertize "%n" 'help-echo "mouse-2: Remove narrowing from buffer"
 		      'mouse-face 'mode-line-highlight
 		      'local-map (make-mode-line-mouse-map
 				  'mouse-2 #'mode-line-widen))
+	  '("" mode-line-minor-modes)
 	  ")"
 	  (propertize "%]" 'help-echo recursive-edit-help-echo)
 	  " "))
-- 
2.49.0