1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
;;; undead-buffers.el --- Bury Rather Than Kill These Buffers -*- lexical-binding: t; coding: utf-8; -*-
;;; Commentary:
;;
;; Layer: 2 (Core UX).
;; Category: C.
;; Load shape: eager.
;; Eager reason: global kill-buffer remap and window-kill bindings wanted from
;; the first session.
;; Top-level side effects: three keymap-global-set (remaps kill-buffer; binds
;; M-S-o, M-S-m).
;; Runtime requires: none.
;; Direct test load: yes.
;;
;; This library allows for "burying" selected buffers instead of killing them.
;; Since they won't be killed, I'm calling them "undead buffers".
;; The main function cj/kill-buffer-or-bury-alive replaces kill-buffer.
;;
;; Additional helper commands and key bindings:
;; - C-; b k (=cj/kill-buffer-and-window=): delete this window and bury/kill its buffer.
;; - C-; b K (=cj/kill-other-window-buffer=): bury/kill the other window's buffer,
;; keeping that window and the split intact.
;; - M-O (=cj/kill-other-window=): delete the next window and bury/kill its buffer.
;; - M-M (=cj/kill-all-other-buffers-and-windows=): kill or bury all buffers except
;; the current one and delete all other windows.
;;
;; Add to the list of "undead buffers" by adding to the cj/buffer-bury-alive-list
;; variable.
;;
;;; Code:
(defvar cj/undead-buffer-list
'("*scratch*" "*EMMS-Playlist*" "*Messages*" "*ert*"
"*AI-Assistant*"
;; *Warnings* stays alive because Emacs 31's warnings.el defers daemon
;; startup warnings into an `after-make-frame-functions' closure that
;; holds this buffer object until the first client frame. The startup
;; sweep in `cj/dashboard-only' used to kill it; the closure then failed
;; inside `make-frame', server.el reported the window system as
;; unsupported, and emacsclient silently retried on $DISPLAY, so the first
;; frame of every 31.1 session opened on XWayland. I bury it instead, the
;; same choice desktop.el makes in `desktop-clear-preserve-buffers'.
"*Warnings*"
;; The async native-compile log stays alive for the same reason, one
;; buffer over. comp-run parks every compile worker on this buffer and
;; the worker's sentinel reads it back before starting the next job.
;; The startup sweep killed it, which SIGHUPs every :noquery worker under
;; it; each sentinel then died in `with-current-buffer' on the dead
;; buffer and `comp--run-async-workers' never ran again, so the queue
;; sat stranded for the life of the daemon, nothing was ever cached, and
;; every boot re-ran the same compile storm at the first frame.
"*Async-native-compile-log*")
"Buffer names to bury instead of killing (exact match).")
(defvar cj/undead-buffer-regexps nil
"Regexps for buffer names to bury instead of killing, alongside
`cj/undead-buffer-list'. Use for dynamically-named buffer families where an
exact name can't be pre-listed -- e.g. ai-term agents, named \"agent [<project>]\".
Register one with `cj/make-buffer-pattern-undead'.")
(defun cj/make-buffer-undead (name)
"Append NAME to `cj/undead-buffer-list' if not present.
Signal an error if NAME is not a non-empty string. Return the updated list."
(unless (and (stringp name) (> (length name) 0))
(error "cj/bury-alive-add: NAME must be a non-empty string"))
(add-to-list 'cj/undead-buffer-list name t))
(defun cj/make-buffer-pattern-undead (regexp)
"Append REGEXP to `cj/undead-buffer-regexps' if not present.
A buffer whose name matches REGEXP is buried instead of killed. Signal an
error if REGEXP is not a non-empty string. Return the updated list."
(unless (and (stringp regexp) (> (length regexp) 0))
(error "cj/make-buffer-pattern-undead: REGEXP must be a non-empty string"))
(add-to-list 'cj/undead-buffer-regexps regexp t))
(defun cj/--buffer-undead-p (name)
"Return non-nil when buffer NAME should be buried instead of killed.
NAME is undead when it is in `cj/undead-buffer-list' (exact) or matches any
regexp in `cj/undead-buffer-regexps'."
(and (stringp name)
(or (member name cj/undead-buffer-list)
(seq-some (lambda (re) (string-match-p re name))
cj/undead-buffer-regexps))))
(defun cj/kill-buffer-or-bury-alive (buffer)
"Kill BUFFER, or bury it when it is in `cj/undead-buffer-list'.
With a prefix argument (e.g. \\`C-u'), instead add BUFFER's name to
`cj/undead-buffer-list' and report it, so the buffer is buried rather than
killed on later kill attempts."
(interactive "bBuffer to kill or bury: ")
(with-current-buffer buffer
(if current-prefix-arg
(progn
(add-to-list 'cj/undead-buffer-list (buffer-name))
(message "Added %s to bury-alive-list" (buffer-name)))
(if (cj/--buffer-undead-p (buffer-name))
(bury-buffer)
(kill-buffer)))))
(keymap-global-set "<remap> <kill-buffer>" #'cj/kill-buffer-or-bury-alive)
(defun cj/undead-buffer-p ()
"Replacement for `save-some-buffers' skips undead-buffers.
Undead-buffers are buffers in `cj/undead-buffer-list'."
(let* ((buf (current-buffer))
(name (buffer-name buf)))
(and
(not (cj/--buffer-undead-p name))
(buffer-file-name buf)
(buffer-modified-p buf))))
(defun cj/kill-buffer-and-window ()
"Delete window and kill or bury its buffer."
(interactive)
(let ((buf (current-buffer)))
(unless (one-window-p)
(delete-window))
;; The delegate reads current-prefix-arg; a C-u meant for this wrapper
;; must not flip it into add-to-undead-list mode.
(let ((current-prefix-arg nil))
(cj/kill-buffer-or-bury-alive buf))))
;; Keybinding moved to custom-buffer-file.el (C-; b k)
(defun cj/kill-other-window ()
"Delete the next window and kill or bury its buffer.
Signal a `user-error' in a single-window frame, where there is no other
window and acting would kill the buffer being viewed."
(interactive)
(when (one-window-p)
(user-error "No other window"))
(other-window 1)
(let ((buf (current-buffer)))
(delete-window)
(let ((current-prefix-arg nil))
(cj/kill-buffer-or-bury-alive buf))))
(keymap-global-set "M-S-o" #'cj/kill-other-window)
(defun cj/kill-other-window-buffer ()
"Kill or bury the buffer shown in the other window, keeping that window
and the split intact -- the window then shows whatever bury/kill surfaces
next. With more than two windows, acts on `next-window'.
Sibling of `cj/kill-other-window', which deletes the other window; here the
split is preserved. Buffers in `cj/undead-buffer-list' are buried."
(interactive)
(if (one-window-p)
(user-error "No other window")
(with-selected-window (next-window)
(let ((current-prefix-arg nil))
(cj/kill-buffer-or-bury-alive (current-buffer))))))
;; Keybinding in custom-buffer-file.el (C-; b K)
(defun cj/kill-all-other-buffers-and-windows ()
"Kill or bury all other buffers, then delete other windows."
(interactive)
(save-some-buffers nil #'cj/undead-buffer-p)
(delete-other-windows)
(let ((current-prefix-arg nil))
(mapc #'cj/kill-buffer-or-bury-alive
(delq (current-buffer) (buffer-list)))))
(keymap-global-set "M-S-m" #'cj/kill-all-other-buffers-and-windows) ;; was M-M
(provide 'undead-buffers)
;;; undead-buffers.el ends here
|