fedora-csb-system-manager
  1;;; auto-side-windows.el --- Simplified buffer management for side windows -*- lexical-binding: t; -*-
  2
  3;; Copyright (C) 2025 Marcel Arpogaus
  4
  5;; Author: Marcel Arpogaus <znepry.necbtnhf@tznvy.pbz>
  6;; Version: 0.1
  7;; Package-Requires: ((emacs "30.1"))
  8;; Keywords: convenience, windows, buffers
  9
 10;;; Commentary:
 11
 12;; `auto-side-windows-mode' allows users to automatically display buffers
 13;; in side windows based on user-defined name or mode rules. This package
 14;; enhances workflow and buffer organization by providing a more predictable
 15;; and organized buffer management.
 16
 17;; The user can define buffers to be displayed in the left, right, top, or
 18;; bottom side windows through a set of buffer name regular expressions and
 19;; major modes. Extra conditions can also be specified to refine these rules
 20;; further.
 21
 22;; Additionally, the package provides commands to toggle side windows or display
 23;; buffers explicitly in one of the four sides manually.
 24
 25;;; Code:
 26(defgroup auto-side-windows nil
 27  "Automatically manage buffer display in side windows."
 28  :group 'windows
 29  :prefix "auto-side-windows-")
 30
 31;;;; Customization Variables
 32(defcustom auto-side-windows-top-buffer-names nil
 33  "List of buffer name regexps to be displayed in top side windows.
 34Each regexp is used to match buffer names. When a buffer's name
 35matches any regex in this list, the buffer will be shown in the
 36top side window."
 37  :type '(repeat string)
 38  :group 'auto-side-windows)
 39
 40(defcustom auto-side-windows-bottom-buffer-names nil
 41  "List of buffer name regexps to be displayed in bottom side windows.
 42Each regexp is used to match buffer names. When a buffer's name
 43matches any regex in this list, the buffer will be shown in the
 44bottom side window."
 45  :type '(repeat string)
 46  :group 'auto-side-windows)
 47
 48(defcustom auto-side-windows-left-buffer-names nil
 49  "List of buffer name regexps to be displayed in left side windows.
 50Each regexp is used to match buffer names. When a buffer's name
 51matches any regex in this list, the buffer will be shown in the
 52left side window."
 53  :type '(repeat string)
 54  :group 'auto-side-windows)
 55
 56(defcustom auto-side-windows-right-buffer-names nil
 57  "List of buffer name regexps to be displayed in right side windows.
 58Each regexp is used to match buffer names. When a buffer's name
 59matches any regex in this list, the buffer will be shown in the
 60right side window."
 61  :type '(repeat string)
 62  :group 'auto-side-windows)
 63
 64(defcustom auto-side-windows-top-buffer-modes nil
 65  "List of major modes for buffers to be displayed in top side windows.
 66When a buffer's major mode matches any symbol in this list,
 67it will be shown in the top side window."
 68  :type '(repeat symbol)
 69  :group 'auto-side-windows)
 70
 71(defcustom auto-side-windows-bottom-buffer-modes nil
 72  "List of major modes for buffers to be displayed in bottom side windows.
 73When a buffer's major mode matches any symbol in this list,
 74it will be shown in the bottom side window."
 75  :type '(repeat symbol)
 76  :group 'auto-side-windows)
 77
 78(defcustom auto-side-windows-left-buffer-modes nil
 79  "List of major modes for buffers to be displayed in left side windows.
 80When a buffer's major mode matches any symbol in this list,
 81it will be shown in the left side window."
 82  :type '(repeat symbol)
 83  :group 'auto-side-windows)
 84
 85(defcustom auto-side-windows-right-buffer-modes nil
 86  "List of major modes for buffers to be displayed in right side windows.
 87When a buffer's major mode matches any symbol in this list,
 88it will be shown in the right side window."
 89  :type '(repeat symbol)
 90  :group 'auto-side-windows)
 91
 92(defcustom auto-side-windows-top-extra-conditions '((category . force-side-top))
 93  "Lists of extra conditions to match top buffers.
 94These extra conditions are checked along with buffer name and major mode
 95rules to determine if a buffer should be displayed in a top side window."
 96  :type '(repeat symbol)
 97  :group 'auto-side-windows)
 98
 99(defcustom auto-side-windows-bottom-extra-conditions '((category . force-side-bottom))
100  "Lists of extra conditions to match bottom buffers.
101These extra conditions are checked along with buffer name and major mode
102rules to determine if a buffer should be displayed in a bottom side window."
103  :type '(repeat symbol)
104  :group 'auto-side-windows)
105
106(defcustom auto-side-windows-left-extra-conditions '((category . force-side-left))
107  "Lists of extra conditions to match left buffers.
108These extra conditions are checked along with buffer name and major mode
109rules to determine if a buffer should be displayed in a left side window."
110  :type '(repeat symbol)
111  :group 'auto-side-windows)
112
113(defcustom auto-side-windows-right-extra-conditions '((category . force-side-right))
114  "Lists of extra conditions to match right buffers.
115These extra conditions are checked along with buffer name and major mode
116rules to determine if a buffer should be displayed in a right side window."
117  :type '(repeat symbol)
118  :group 'auto-side-windows)
119
120(defcustom auto-side-windows-top-window-parameters nil
121  "Custom window parameters for top side windows.
122This alist can be used to specify parameters like the height
123or width of the top side window."
124  :type 'alist
125  :group 'auto-side-windows)
126
127(defcustom auto-side-windows-bottom-window-parameters nil
128  "Custom window parameters for bottom side windows.
129This alist can be used to specify parameters like the height
130or width of the bottom side window."
131  :type 'alist
132  :group 'auto-side-windows)
133
134(defcustom auto-side-windows-left-window-parameters nil
135  "Custom window parameters for left side windows.
136This alist can be used to specify parameters like the height
137or width of the left side window."
138  :type 'alist
139  :group 'auto-side-windows)
140
141(defcustom auto-side-windows-right-window-parameters nil
142  "Custom window parameters for right side windows.
143This alist can be used to specify parameters like the height
144or width of the right side window."
145  :type 'alist
146  :group 'auto-side-windows)
147
148(defcustom auto-side-windows-top-alist '((window-height . (lambda (win) (fit-window-to-buffer win 20 5))))
149  "Custom alist for top side windows.
150This alist contains display properties which will be applied
151when displaying buffers in the top side window."
152  :type 'alist
153  :group 'auto-side-windows)
154
155(defcustom auto-side-windows-bottom-alist nil
156  "Custom alist for bottom side windows.
157This alist contains display properties which will be applied
158when displaying buffers in the bottom side window."
159  :type 'alist
160  :group 'auto-side-windows)
161
162(defcustom auto-side-windows-left-alist nil
163  "Custom alist for left side windows.
164This alist contains display properties which will be applied
165when displaying buffers in the left side window."
166  :type 'alist
167  :group 'auto-side-windows)
168
169(defcustom auto-side-windows-right-alist '((window-width . 80))
170  "Custom alist for right side windows.
171This alist contains display properties which will be applied
172when displaying buffers in the right side window."
173  :type 'alist
174  :group 'auto-side-windows)
175
176(defcustom auto-side-windows-common-window-parameters '((no-other-window . t)
177                                                        (tab-line-format . none)
178                                                        (mode-line-format . none))
179  "Custom window parameters for all side windows.
180These parameters will be applied to all side windows created by
181`auto-side-windows-mode'."
182  :type 'alist
183  :group 'auto-side-windows)
184
185(defcustom auto-side-windows-common-alist '((dedicated . t))
186  "Custom alist for all side windows.
187These parameters will be applied to all side windows created by
188`auto-side-windows-mode`."
189  :type 'alist
190  :group 'auto-side-windows)
191
192(defcustom auto-side-windows-reuse-mode-window '((right . t))
193  "Allow reuse of side windows for same mode on given sides.
194If set, side windows may be reused for buffers of the same major mode."
195  :type 'alist
196  :group 'auto-side-windows)
197
198(defcustom auto-side-windows-before-display-hook nil
199  "Hook run before displaying a buffer in a side window.
200This hook allows users to execute custom code or functions
201before a buffer is placed in a side window."
202  :type 'hook
203  :group 'auto-side-windows)
204
205(defcustom auto-side-windows-after-display-hook nil
206  "Hook run after displaying a buffer in a side window.
207This hook allows users to execute custom code or functions
208after a buffer has been placed in a side window."
209  :type 'hook
210  :group 'auto-side-windows)
211
212(defcustom auto-side-windows-before-toggle-hook nil
213  "Hook run before toggling the display of a buffer.
214This hook allows users to execute custom code or functions
215before the toggle action of a buffer in a side window."
216  :type 'hook
217  :group 'auto-side-windows)
218
219(defcustom auto-side-windows-after-toggle-hook nil
220  "Hook run after toggling the display of a buffer.
221This hook allows users to execute custom code or functions
222after the toggle action of a buffer in a side window."
223  :type 'hook
224  :group 'auto-side-windows)
225
226;;;; Internal Variables
227(defvar auto-side-windows--side-window-functions nil
228  "List of functions added to `display-buffer-alist' by `auto-side-windows-mode'.
229These functions determine how buffers are displayed in side windows.")
230
231;;;; Helper Functions
232(defun auto-side-windows--buffer-match-condition (majormodes &optional buffernames extra-conds)
233  "Get condition to match buffers with given MAJORMODES or BUFFERNAMES.
234MAJORMODES are the major modes to match, while BUFFERNAMES
235are optional regex patterns for buffer names. EXTRA-CONDS are
236additional conditions to refine the matching process."
237  (let ((modes-cond `(or ,@(mapcar (lambda (mode) `(derived-mode . ,mode)) majormodes))))
238    (when buffernames (setq modes-cond `(or (or ,@buffernames) ,modes-cond)))
239    (setq modes-cond (append modes-cond extra-conds))
240    modes-cond))
241
242(defun auto-side-windows--get-buffer-side (buffer &optional args)
243  "Determine which side BUFFER should be displayed in.
244This function checks the buffer against user-defined conditions relative to the
245side windows. It returns `'top', `'bottom', `'left', or `'right',or nil if no
246conditions are met.
247Optional ARGS may contain a category (New in Emacs>30.1)."
248  (cond
249   ((buffer-match-p `(and (not ,@(append auto-side-windows-left-extra-conditions
250                                         auto-side-windows-right-extra-conditions
251                                         auto-side-windows-bottom-extra-conditions)
252                               (category . detached-side-window))
253                          ,(auto-side-windows--buffer-match-condition
254                            auto-side-windows-top-buffer-modes
255                            auto-side-windows-top-buffer-names
256                            auto-side-windows-top-extra-conditions))
257                    buffer args)
258    'top)
259   ((buffer-match-p `(and (not ,@(append auto-side-windows-left-extra-conditions
260                                         auto-side-windows-right-extra-conditions
261                                         auto-side-windows-top-extra-conditions)
262                               (category . detached-side-window))
263                          ,(auto-side-windows--buffer-match-condition
264                            auto-side-windows-bottom-buffer-modes
265                            auto-side-windows-bottom-buffer-names
266                            auto-side-windows-bottom-extra-conditions))
267                    buffer args)
268    'bottom)
269   ((buffer-match-p `(and (not ,@(append auto-side-windows-top-extra-conditions
270                                         auto-side-windows-right-extra-conditions
271                                         auto-side-windows-bottom-extra-conditions)
272                               (category . detached-side-window))
273                          ,(auto-side-windows--buffer-match-condition
274                            auto-side-windows-left-buffer-modes
275                            auto-side-windows-left-buffer-names
276                            auto-side-windows-left-extra-conditions))
277                    buffer args)
278    'left)
279   ((buffer-match-p `(and (not ,@(append auto-side-windows-left-extra-conditions
280                                         auto-side-windows-top-extra-conditions
281                                         auto-side-windows-bottom-extra-conditions)
282                               (category . detached-side-window))
283                          ,(auto-side-windows--buffer-match-condition
284                            auto-side-windows-right-buffer-modes
285                            auto-side-windows-right-buffer-names
286                            auto-side-windows-right-extra-conditions))
287                    buffer args)
288    'right)
289   (t nil)))
290
291(defun auto-side-windows--get-next-free-slot (side)
292  "Return the next free slot number for SIDE.
293Each side window can have multiple slots numbered from 0 to
294MAX-SLOTS-1. This function finds and returns the next available
295slot number for use.
296If no free slot is found return MAX-SLOTS-1."
297  (let* ((max-slots (nth (cond ((eq side 'left) 0)
298                               ((eq side 'top) 1)
299                               ((eq side 'right) 2)
300                               ((eq side 'bottom) 3))
301                         window-sides-slots))
302         used-slots)
303    ;; Collect used slots
304    (dolist (win (window-list))
305      (when (equal (window-parameter win 'window-side) side)
306        (when-let ((slot (window-parameter win 'window-slot)))
307          (setq used-slots (cons slot used-slots)))))
308
309    ;; Find the next free slot
310    (if-let ((next-slot (catch 'next-slot
311                          (dotimes (i max-slots)
312                            (unless (member i used-slots)
313                              (throw 'next-slot i))))))
314        next-slot (1- max-slots))))
315
316(defun auto-side-windows--display-buffer (buffer alist)
317  "Custom display buffer function for `auto-side-windows-mode'.
318BUFFER is the buffer to display and ALIST contains display parameters.
319This function determines the appropriate side for the buffer and
320displays it in the selected side window if conditions are met.
321
322Before displaying the buffer, it runs `auto-side-windows-before-display-hook'.
323After displaying the buffer, it runs `auto-side-windows-after-display-hook'."
324  (when-let* ((side (auto-side-windows--get-buffer-side buffer `(nil . ,alist)))
325              (slot (auto-side-windows--get-next-free-slot side))
326              (window-params (append auto-side-windows-common-window-parameters
327                                     (symbol-value (intern (format "auto-side-windows-%s-window-parameters" (symbol-name side))))))
328              (side-alist (append auto-side-windows-common-alist
329                                  (symbol-value (intern (format "auto-side-windows-%s-alist" (symbol-name side))))))
330              (alist (append alist
331                             side-alist
332                             `((side . ,side)
333                               (slot . ,slot)
334                               (window-parameters . ,window-params)))))
335    (run-hook-with-args 'auto-side-windows-before-display-hook buffer)
336    (let ((window (unless (when (alist-get side auto-side-windows-reuse-mode-window)
337                            (display-buffer-reuse-mode-window buffer alist))
338                    (display-buffer-in-side-window buffer alist))))
339      (run-hook-with-args 'auto-side-windows-after-display-hook buffer window)
340      window)))
341
342;;;; Commands
343(defun auto-side-windows-toggle-side-window nil
344  "Toggle the current buffer as a side window.
345If the current window is already a side window, it will delete
346the window. If not, the buffer will be displayed in a side window.
347
348Before toggling the buffer, it runs `auto-side-windows-before-toggle-hook'.
349After toggling the buffer, it runs `auto-side-windows-after-toggle-hook'."
350  (interactive)
351  (let ((window (selected-window))
352        (buf (current-buffer)))
353    (with-selected-window window
354      (run-hook-with-args 'auto-side-windows-before-toggle-hook buf)
355      (cond
356       ((window-parameter window 'window-side)
357        (progn
358          (setq-local was-side-window t)
359          (display-buffer
360           buf '(display-buffer-use-some-window . ((some-window . mru)
361                                                   (category . detached-side-window))))
362          (delete-window window)))
363       ((local-variable-if-set-p 'was-side-window buf)
364        (progn
365          (kill-local-variable 'was-side-window)
366          (switch-to-prev-buffer window 'bury)
367          (display-buffer buf)))
368       (t
369        (error "Not a side window")))
370      (run-hook-with-args 'auto-side-windows-after-toggle-hook buf))))
371
372(defun auto-side-windows-display-buffer-on-side (side)
373  "Display the current buffer in a window on SIDE.
374This command explicitly places the buffer in the specified side window.
375It runs `auto-side-windows-before-display-hook` before displaying the buffer
376and `auto-side-windows-after-display-hook` after."
377  (interactive (list (intern (completing-read "Select side: " '("left" "right" "top" "bottom")))))
378  (let ((buf (current-buffer))
379        (alist `(nil . ((category . ,(intern (concat "force-side-" (symbol-name side))))))))
380    (if-let* ((window (selected-window))
381              (window-side (window-parameter window 'window-side)))
382        (delete-window window)
383      (switch-to-prev-buffer window 'bury))
384    (display-buffer buf alist)))
385
386(defun auto-side-windows-display-buffer-top ()
387  "Display the current buffer in a top side window."
388  (interactive)
389  (auto-side-windows-display-buffer-on-side 'top))
390
391(defun auto-side-windows-display-buffer-bottom ()
392  "Display the current buffer in a bottom side window."
393  (interactive)
394  (auto-side-windows-display-buffer-on-side 'bottom))
395
396(defun auto-side-windows-display-buffer-left ()
397  "Display the current buffer in a left side window."
398  (interactive)
399  (auto-side-windows-display-buffer-on-side 'left))
400
401(defun auto-side-windows-display-buffer-right ()
402  "Display the current buffer in a right side window."
403  (interactive)
404  (auto-side-windows-display-buffer-on-side 'right))
405
406;;;; Minor Mode
407;;;###autoload
408(define-minor-mode auto-side-windows-mode
409  "Toggle automatic side window management based on buffer rules.
410When enabled, this minor mode allows customized display of buffers
411in defined side windows based on their names or modes. It adds
412provided functions to `display-buffer-alist` to enable this feature."
413  :global t
414  :group 'auto-side-windows
415  (if auto-side-windows-mode
416      (add-to-list 'display-buffer-alist
417                   '(t auto-side-windows--display-buffer))
418    (setq display-buffer-alist
419          (delete '(t auto-side-windows--display-buffer)
420                  display-buffer-alist))))
421
422(provide 'auto-side-windows)
423;;; auto-side-windows.el ends here