;;; 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)