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

From: Pengji Zhang <[email-masked]>
Date: Sat, 29 Mar 2025 19:07:40 +0800

Hello,

The attached patch adds a new user option to hide some minor mode
lighters on the mode line (by collapsing them into a menu), which is to
shorten the mode line and prioritize important information when many
minor modes are on.

There exist a few packages that solve the problem. To name a few:

- diminish[1] and delight[2]. Both offer a way to change the minor mode
  lighters by modifying 'minor-mode-alist'. To hide a minor mode
  lighter, one may change it to nil.

- minions[3], which partially inspired this patch. It replaces minor
  mode lighters by a menu to toggle all minor modes.

This patch is different from those solutions in two aspects:

- Unlike diminish or delight, one can still see the lighters by clicking
  the button, instead of hiding them permanently. Besides, this patch is
  compatible with those two packages.

- Unlike minions, this patch focuses on *lighters* for *enabled* minor
  modes. The menu contains only lighters, making it a more space
  efficient replacement for lighters on mode line, instead of a way to
  manage minor modes like minions.

So I hope this patch is still useful given the existing similar
solutions.

Please let me know what you think. Thanks!

Pengji

[1] https://github.com/myrjola/diminish.el
[2] https://elpa.gnu.org/packages/delight.html
[3] https://github.com/tarsius/minions

>From c1e27606247761c31cec3d363714875c74b30277 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

* 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-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.
(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.

* doc/lispref/modes.texi (Mode Line Basics): Document the new
user option.
* etc/NEWS (Note): Annouce the new user option.
---
 doc/lispref/modes.texi |  12 ++++
 etc/NEWS               |   9 +++
 lisp/bindings.el       | 122 ++++++++++++++++++++++++++++++++++++++---
 3 files changed, 136 insertions(+), 7 deletions(-)

diff --git a/doc/lispref/modes.texi b/doc/lispref/modes.texi
index 788d98fdf20..23c363be12c 100644
--- 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.
+
 @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 1bd2fd6d486..8b7dfa0ee04 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -316,6 +316,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.  It 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..a32e3d58dc7 100644
--- a/lisp/bindings.el
+++ b/lisp/bindings.el
@@ -429,6 +429,120 @@ 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)
+
+(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)
+          (when-let* ((menu (mode-line--make-lighter-menu hidden))
+                      (menu-binding (list 'menu-item "Display" menu
+                                          :filter #'bindings--sort-menu-keymap))
+                      (toggle-binding (list 'menu-item "Toggle" mode-line-mode-menu
+                                            :fitler #'bindings--sort-menu-keymap))
+                      (keymap (define-keymap
+                                "<mode-line> <down-mouse-1>" menu-binding
+                                "<mode-line> <mouse-2>" #'describe-mode
+                                "<mode-line> <down-mouse-3>" toggle-binding)))
+            `(: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 +580,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