200 lines
7.3 KiB
EmacsLisp
200 lines
7.3 KiB
EmacsLisp
;;; buffer-table.el --- Tabular view of buffers and recent files -*- lexical-binding: t; -*-
|
|
|
|
(require 'cl-lib)
|
|
(require 'recentf)
|
|
|
|
(defgroup buffer-table nil
|
|
"Tabular view of buffers and recent files."
|
|
:group 'convenience)
|
|
|
|
;; Faces for distinction
|
|
(defface buffer-table-open '((t :inherit font-lock-keyword-face :bold t)) "Face for open files.")
|
|
(defface buffer-table-recent '((t :inherit shadow)) "Face for recent closed files.")
|
|
(defface buffer-table-special '((t :inherit font-lock-comment-face)) "Face for internal buffers.")
|
|
(defface buffer-table-modified '((t :inherit error :bold t)) "Face for modified flag.")
|
|
(defface buffer-table-marked '((t :inherit warning :bold t)) "Face for marked entries.")
|
|
|
|
;; Mark tracking - set of entry IDs currently marked
|
|
(defvar buffer-table--marked nil
|
|
"Set of marked entry IDs (buffer objects or file paths).")
|
|
|
|
(defun buffer-table--get-entries ()
|
|
"Collect all items and format them for tabulated-list-entries."
|
|
(let ((file-entries nil)
|
|
(recent-entries nil)
|
|
(special-entries nil)
|
|
(processed-files (make-hash-table :test 'equal)))
|
|
|
|
;; 1. Open File-visiting Buffers
|
|
(dolist (buf (buffer-list))
|
|
(let ((file (buffer-file-name buf)))
|
|
(when (and file (not (string-prefix-p " " (buffer-name buf))))
|
|
(puthash file t processed-files)
|
|
(push (list buf
|
|
(vector
|
|
(if (buffer-modified-p buf) (propertize "*" 'face 'buffer-table-modified) " ")
|
|
(propertize "File" 'face 'buffer-table-open)
|
|
(propertize (buffer-name buf) 'face 'buffer-table-open)
|
|
(abbreviate-file-name (file-name-directory file))
|
|
(if (gethash buf buffer-table--marked)
|
|
(propertize "☑" 'face 'buffer-table-marked)
|
|
"☐")))
|
|
file-entries))))
|
|
|
|
;; 2. Recent Files (Closed)
|
|
(recentf-mode 1)
|
|
(dolist (file recentf-list)
|
|
(unless (gethash file processed-files)
|
|
(push (list file
|
|
(vector
|
|
" "
|
|
(propertize "Recent" 'face 'buffer-table-recent)
|
|
(propertize (file-name-nondirectory file) 'face 'buffer-table-recent)
|
|
(abbreviate-file-name (file-name-directory file))
|
|
(if (gethash file buffer-table--marked)
|
|
(propertize "☑" 'face 'buffer-table-marked)
|
|
"☐")))
|
|
recent-entries)))
|
|
|
|
;; 3. Special Buffers
|
|
(dolist (buf (buffer-list))
|
|
(unless (or (buffer-file-name buf) (string-prefix-p " " (buffer-name buf)))
|
|
(push (list buf
|
|
(vector
|
|
(if (buffer-modified-p buf) (propertize "*" 'face 'buffer-table-modified) " ")
|
|
(propertize "Special" 'face 'buffer-table-special)
|
|
(propertize (buffer-name buf) 'face 'buffer-table-special)
|
|
""
|
|
(if (gethash buf buffer-table--marked)
|
|
(propertize "☑" 'face 'buffer-table-marked)
|
|
"☐")))
|
|
special-entries)))
|
|
|
|
;; Return in requested order: Special, then File, then Recent
|
|
(append (nreverse special-entries)
|
|
(nreverse file-entries)
|
|
(nreverse recent-entries))))
|
|
|
|
(defun buffer-table--refresh ()
|
|
"Refresh the list."
|
|
(setq tabulated-list-entries (buffer-table--get-entries))
|
|
(tabulated-list-print t))
|
|
|
|
(defun buffer-table-mark ()
|
|
"Mark the entry at point."
|
|
(interactive)
|
|
(let ((id (tabulated-list-get-id)))
|
|
(when id
|
|
(puthash id t buffer-table--marked)
|
|
(buffer-table--refresh))))
|
|
|
|
(defun buffer-table-unmark ()
|
|
"Unmark the entry at point."
|
|
(interactive)
|
|
(let ((id (tabulated-list-get-id)))
|
|
(when id
|
|
(remhash id buffer-table--marked)
|
|
(buffer-table--refresh))))
|
|
|
|
(defun buffer-table-mark-all ()
|
|
"Mark all entries."
|
|
(interactive)
|
|
(dolist (entry tabulated-list-entries)
|
|
(puthash (car entry) t buffer-table--marked))
|
|
(buffer-table--refresh))
|
|
|
|
(defun buffer-table-unmark-all ()
|
|
"Unmark all entries."
|
|
(interactive)
|
|
(setq buffer-table--marked (make-hash-table))
|
|
(buffer-table--refresh))
|
|
|
|
(defun buffer-table-kill-marked ()
|
|
"Kill all marked buffers. For recent files, remove from recentf list."
|
|
(interactive)
|
|
(unless (hash-table-p buffer-table--marked)
|
|
(user-error "No entries marked"))
|
|
(when (or (null (buffer-table--marked-count))
|
|
(zerop (buffer-table--marked-count)))
|
|
(user-error "No entries marked"))
|
|
|
|
(let ((buffers-to-kill nil)
|
|
(recent-to-remove nil))
|
|
(maphash (lambda (id _)
|
|
(if (bufferp id)
|
|
(push id buffers-to-kill)
|
|
(push id recent-to-remove)))
|
|
buffer-table--marked)
|
|
|
|
;; Kill buffers
|
|
(when buffers-to-kill
|
|
(let ((count (length buffers-to-kill)))
|
|
(dolist (buf buffers-to-kill)
|
|
(when (buffer-live-p buf)
|
|
(kill-buffer buf)))
|
|
(message "Killed %d buffer%s" count (if (> count 1) "s" ""))))
|
|
|
|
;; Remove from recentf
|
|
(when recent-to-remove
|
|
(let ((count (length recent-to-remove)))
|
|
(dolist (file recent-to-remove)
|
|
(setq recentf-list (delq file recentf-list)))
|
|
(recentf-save-list)
|
|
(message "Removed %d file%s from recentf" count (if (> count 1) "s" ""))))
|
|
|
|
(setq buffer-table--marked (make-hash-table))
|
|
(buffer-table--refresh)))
|
|
|
|
(defun buffer-table--marked-count ()
|
|
"Return the number of marked entries."
|
|
(hash-table-count buffer-table--marked))
|
|
|
|
(defun buffer-table-visit ()
|
|
"Visit the buffer or file at point."
|
|
(interactive)
|
|
(let ((id (tabulated-list-get-id)))
|
|
(cond
|
|
((bufferp id) (switch-to-buffer id))
|
|
((stringp id) (find-file id))
|
|
(t (user-error "Nothing to visit here")))))
|
|
|
|
(defun buffer-table-revert ()
|
|
"Revert the table, clearing marks."
|
|
(interactive)
|
|
(setq buffer-table--marked (make-hash-table))
|
|
(buffer-table--refresh))
|
|
|
|
(define-derived-mode buffer-table-mode tabulated-list-mode "Buffer-Table"
|
|
"Major mode for a unified buffer and file list."
|
|
(setq buffer-table--marked (make-hash-table))
|
|
(setq tabulated-list-format
|
|
[("S" 1 t) ; Status (Modified)
|
|
("Type" 8 t) ; File, Recent, or Special
|
|
("Name" 35 t) ; File/Buffer name
|
|
("Path" 0 t) ; Directory path (flexible width)
|
|
("M" 3 t)]) ; Mark column
|
|
(setq tabulated-list-padding 2)
|
|
(setq tabulated-list-sort-key '("Type" . nil))
|
|
(add-hook 'tabulated-list-revert-hook #'buffer-table--refresh nil t)
|
|
(tabulated-list-init-header))
|
|
|
|
(define-key buffer-table-mode-map (kbd "RET") #'buffer-table-visit)
|
|
(define-key buffer-table-mode-map (kbd "g") #'buffer-table-revert)
|
|
(define-key buffer-table-mode-map (kbd "m") #'buffer-table-mark)
|
|
(define-key buffer-table-mode-map (kbd "M") #'buffer-table-mark-all)
|
|
(define-key buffer-table-mode-map (kbd "u") #'buffer-table-unmark-all)
|
|
(define-key buffer-table-mode-map (kbd "U") #'buffer-table-unmark)
|
|
(define-key buffer-table-mode-map (kbd "D") #'buffer-table-kill-marked)
|
|
|
|
;;;###autoload
|
|
(defun buffer-table ()
|
|
"Display the unified buffer and file table."
|
|
(interactive)
|
|
(let ((buf (get-buffer-create "*Buffer Table*")))
|
|
(with-current-buffer buf
|
|
(buffer-table-mode)
|
|
(buffer-table--refresh))
|
|
(switch-to-buffer buf)))
|
|
|
|
(provide 'buffer-table)
|