Subject: [PATCH] ibuffer: Display column titles in header line

From: Daniel Mendler <[email-masked]>
Date: Sat, 11 Jan 2025 17:01:04 +0100

Tags: patch

Add new option value `title' to the customization option
`ibuffer-use-header-line'.  For this value show the column titles in the
Ibuffer header line.

>From d4f745ce0409f1d9ae956ca8cbaf102eecea8563 Mon Sep 17 00:00:00 2001
From: Daniel Mendler <[email-masked]>
Date: Sat, 11 Jan 2025 16:36:56 +0100
Subject: [PATCH] ibuffer: Display column titles in header line

If the option `ibuffer-use-header-line' is set to `title',
display column titles in the header line.

* lisp/ibuffer.el (ibuffer--format-title)
(ibuffer--format-summary): New functions extracted from
`ibuffer-update-title-and-summary'.
(ibuffer-update-title-and-summary): Use them.
(ibuffer-update): Do not always override `header-line-format'.
(ibuffer-use-header-line): Update docstring and option `:type'.
* etc/NEWS: Announce the change.
---
 etc/NEWS        |   4 ++
 lisp/ibuffer.el | 166 ++++++++++++++++++++++++++----------------------
 2 files changed, 94 insertions(+), 76 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index d20c71897bc..46925cd8a4f 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -338,6 +338,10 @@ modal editing packages.
 The variable 'ibuffer-formats' configures the Ibuffer formats.  Add
 'recency' to the format to display the column.
 
+*** New value 'title' for the user option 'ibuffer-use-header-line'.
+Display column titles in the header line if 'ibuffer-use-header-line' is
+set to 'title'.
+
 *** New user option 'ibuffer-human-readable-size'.
 When non-nil, buffer sizes are shown in human readable format.
 
diff --git a/lisp/ibuffer.el b/lisp/ibuffer.el
index 65f8ca53693..014d8c59b25 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -308,8 +308,10 @@ ibuffer-jump-offer-only-visible-buffers
   :type 'boolean)
 
 (defcustom ibuffer-use-header-line t
-  "If non-nil, display a header line containing current filters."
-  :type 'boolean)
+  "If non-nil, display a header line.
+If the variable has the value t, the header line displays the current
+filters.  For the value `title', the column titles are displayed."
+  :type '(choice boolean (const :tag "Column titles" :value title)))
 
 (defcustom ibuffer-default-directory nil
   "The default directory to use for a new Ibuffer buffer.
@@ -2045,59 +2047,90 @@ ibuffer-switch-format
   (ibuffer-update-format)
   (ibuffer-redisplay t))
 
+(defun ibuffer--format-title (element &optional header-line)
+  (if (stringp element)
+      element
+    (pcase-let ((`(,sym ,min ,_max ,align) element))
+      ;; Ignore a negative min when we're inserting the title
+      (when (cl-minusp min)
+	(setq min (- min)))
+      (let* ((name (or (get sym 'ibuffer-column-name)
+		       (error "Unknown column %s in ibuffer-formats" sym)))
+	     (len (length name))
+	     (hmap (get sym 'header-mouse-map))
+	     (strname (if (< len min)
+			  (ibuffer-format-column name
+						 (- min len)
+						 align)
+			name)))
+	(when hmap
+	  (setq
+	   strname
+	   (propertize strname 'mouse-face 'highlight 'keymap
+                       (if header-line
+                           (define-keymap "<header-line>" hmap)
+                         hmap))))
+	strname))))
+
+(defun ibuffer--format-summary (element)
+  (if (stringp element)
+      (make-string (length element) ?\s)
+    (pcase-let ((`(,sym ,min ,_max ,align) element))
+      ;; Ignore a negative min when we're inserting the title.
+      (when (cl-minusp min)
+        (setq min (- min)))
+      (let* ((summary
+              (if (get sym 'ibuffer-column-summarizer)
+                  (funcall (get sym 'ibuffer-column-summarizer)
+                           (get sym 'ibuffer-column-summary))
+                (make-string
+                 (length (get sym 'ibuffer-column-name))
+                 ?\s)))
+             (len (length summary)))
+        (if (< len min)
+            (ibuffer-format-column summary
+                                   (- min len)
+                                   align)
+          summary)))))
+
 (defun ibuffer-update-title-and-summary (format)
   (ibuffer-assert-ibuffer-mode)
   ;; Don't do funky font-lock stuff here
   (let ((inhibit-modification-hooks t))
-    (if (get-text-property (point-min) 'ibuffer-title)
-	(delete-region (point-min)
-		       (next-single-property-change
-			(point-min) 'ibuffer-title)))
-    (goto-char (point-min))
-    (add-text-properties
-     (point)
-     (progn
-       (let ((opos (point)))
-	 ;; Insert the title names.
-	 (dolist (element format)
-	   (insert
-	    (if (stringp element)
-		element
-	      (pcase-let ((`(,sym ,min ,_max ,align) element))
-		;; Ignore a negative min when we're inserting the title
-		(when (cl-minusp min)
-		  (setq min (- min)))
-		(let* ((name (or (get sym 'ibuffer-column-name)
-				 (error "Unknown column %s in ibuffer-formats" sym)))
-		       (len (length name))
-		       (hmap (get sym 'header-mouse-map))
-		       (strname (if (< len min)
-				    (ibuffer-format-column name
-							   (- min len)
-							   align)
-				  name)))
-		  (when hmap
-		    (setq
-		     strname
-		     (propertize strname 'mouse-face 'highlight 'keymap hmap)))
-		  strname)))))
-	 (add-text-properties opos (point) '(ibuffer-title-header t))
-	 (insert "\n")
-	 ;; Add the underlines
-	 (let ((str (save-excursion
-		      (forward-line -1)
-		      (beginning-of-line)
-		      (buffer-substring (point) (line-end-position)))))
-	   (apply #'insert (mapcar
-			    (lambda (c)
-			      (if (not (or (eq c ?\s)
-					   (eq c ?\n)))
-				  ?-
-				?\s))
-			    str)))
-	 (insert "\n"))
-       (point))
-     `(ibuffer-title t font-lock-face ,ibuffer-title-face))
+    ;; Insert the title names.
+    (if (eq ibuffer-use-header-line 'title)
+        (setq header-line-format
+              `("" header-line-indent
+                ,(propertize " " 'display
+                             '(space :align-to header-line-indent-width))
+                ,@(mapcar (lambda (e) (ibuffer--format-title e t)) format)))
+      (if (get-text-property (point-min) 'ibuffer-title)
+	  (delete-region (point-min)
+		         (next-single-property-change
+			  (point-min) 'ibuffer-title)))
+      (goto-char (point-min))
+      (add-text-properties
+       (point)
+       (progn
+         (let ((opos (point)))
+           (apply #'insert (mapcar #'ibuffer--format-title format))
+	   (add-text-properties opos (point) '(ibuffer-title-header t))
+	   (insert "\n")
+	   ;; Add the underlines
+	   (let ((str (save-excursion
+		        (forward-line -1)
+		        (beginning-of-line)
+		        (buffer-substring (point) (line-end-position)))))
+	     (apply #'insert (mapcar
+			      (lambda (c)
+			        (if (not (or (eq c ?\s)
+					     (eq c ?\n)))
+				    ?-
+				  ?\s))
+			      str)))
+	   (insert "\n"))
+         (point))
+       `(ibuffer-title t font-lock-face ,ibuffer-title-face)))
     ;; Now, insert the summary columns.
     (goto-char (point-max))
     (if (get-text-property (1- (point-max)) 'ibuffer-summary)
@@ -2109,27 +2142,7 @@ ibuffer-update-title-and-summary
 	 (point)
 	 (progn
 	   (insert "\n")
-	   (dolist (element format)
-	     (insert
-	      (if (stringp element)
-		  (make-string (length element) ?\s)
-		(pcase-let ((`(,sym ,min ,_max ,align) element))
-                  ;; Ignore a negative min when we're inserting the title.
-                  (when (cl-minusp min)
-                    (setq min (- min)))
-                  (let* ((summary
-                          (if (get sym 'ibuffer-column-summarizer)
-                              (funcall (get sym 'ibuffer-column-summarizer)
-                                       (get sym 'ibuffer-column-summary))
-                            (make-string
-                             (length (get sym 'ibuffer-column-name))
-                             ?\s)))
-                         (len (length summary)))
-                    (if (< len min)
-                        (ibuffer-format-column summary
-                                               (- min len)
-                                               align)
-                      summary))))))
+           (apply #'insert (mapcar #'ibuffer--format-summary format))
 	   (point))
 	 '(ibuffer-summary t)))))
 
@@ -2194,10 +2207,11 @@ ibuffer-update
   ;; I tried to update this automatically from the mode-line-process format,
   ;; but changing nil-ness of header-line-format while computing
   ;; mode-line-format is asking a bit too much it seems.  --Stef
-  (setq header-line-format
-        (and ibuffer-use-header-line
-             ibuffer-filtering-qualifiers
-             ibuffer-header-line-format)))
+  (unless (eq ibuffer-use-header-line 'title)
+    (setq header-line-format
+          (and ibuffer-use-header-line
+               ibuffer-filtering-qualifiers
+               ibuffer-header-line-format))))
 
 (defun ibuffer-sort-bufferlist (bmarklist)
   (unless ibuffer-sorting-functions-alist
-- 
2.45.2