diff --git a/custom-lisp/prot-common.el b/custom-lisp/prot-common.el new file mode 100644 index 0000000..08a80ca --- /dev/null +++ b/custom-lisp/prot-common.el @@ -0,0 +1,422 @@ +;;; prot-common.el --- Common functions for my dotemacs -*- lexical-binding: t -*- + +;; Copyright (C) 2020-2024 Protesilaos Stavrou + +;; Author: Protesilaos Stavrou <info@protesilaos.com> +;; URL: https://protesilaos.com/emacs/dotemacs +;; Version: 0.1.0 +;; Package-Requires: ((emacs "30.1")) + +;; This file is NOT part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or (at +;; your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; Common functions for my Emacs: <https://protesilaos.com/emacs/dotemacs/>. +;; +;; Remember that every piece of Elisp that I write is for my own +;; educational and recreational purposes. I am not a programmer and I +;; do not recommend that you copy any of this if you are not certain of +;; what it does. + +;;; Code: + +(eval-when-compile + (require 'subr-x) + (require 'cl-lib)) + +(defgroup prot-common () + "Auxiliary functions for my dotemacs." + :group 'editing) + +;;;###autoload +(defun prot-common-number-even-p (n) + "Test if N is an even number." + (if (numberp n) + (= (% n 2) 0) + (error "%s is not a number" n))) + +;;;###autoload +(defun prot-common-number-integer-p (n) + "Test if N is an integer." + (if (integerp n) + n + (error "%s is not an integer" n))) + +;;;###autoload +(defun prot-common-number-integer-positive-p (n) + "Test if N is a positive integer." + (if (prot-common-number-integer-p n) + (> n 0) + (error "%s is not a positive integer" n))) + +;; Thanks to Gabriel for providing a cleaner version of +;; `prot-common-number-negative': <https://github.com/gabriel376>. +;;;###autoload +(defun prot-common-number-negative (n) + "Make N negative." + (if (and (numberp n) (> n 0)) + (* -1 n) + (error "%s is not a valid positive number" n))) + +;;;###autoload +(defun prot-common-reverse-percentage (number percent change-p) + "Determine the original value of NUMBER given PERCENT. + +CHANGE-P should specify the increase or decrease. For simplicity, +nil means decrease while non-nil stands for an increase. + +NUMBER must satisfy `numberp', while PERCENT must be `natnump'." + (unless (numberp number) + (user-error "NUMBER must satisfy numberp")) + (unless (natnump percent) + (user-error "PERCENT must satisfy natnump")) + (let* ((pc (/ (float percent) 100)) + (pc-change (if change-p (+ 1 pc) pc)) + (n (if change-p pc-change (float (- 1 pc-change))))) + ;; FIXME 2021-12-21: If float, round to 4 decimal points. + (/ number n))) + +;;;###autoload +(defun prot-common-percentage-change (n-original n-final) + "Find percentage change between N-ORIGINAL and N-FINAL numbers. + +When the percentage is not an integer, it is rounded to 4 +floating points: 16.666666666666664 => 16.667." + (unless (numberp n-original) + (user-error "N-ORIGINAL must satisfy numberp")) + (unless (numberp n-final) + (user-error "N-FINAL must satisfy numberp")) + (let* ((difference (float (abs (- n-original n-final)))) + (n (* (/ difference n-original) 100)) + (round (floor n))) + ;; FIXME 2021-12-21: Any way to avoid the `string-to-number'? + (if (> n round) (string-to-number (format "%0.4f" n)) round))) + +;; REVIEW 2023-04-07 07:43 +0300: I just wrote the conversions from +;; seconds. Hopefully they are correct, but I need to double check. +(defun prot-common-seconds-to-minutes (seconds) + "Convert a number representing SECONDS to MM:SS notation." + (let ((minutes (/ seconds 60)) + (seconds (% seconds 60))) + (format "%.2d:%.2d" minutes seconds))) + +(defun prot-common-seconds-to-hours (seconds) + "Convert a number representing SECONDS to HH:MM:SS notation." + (let* ((hours (/ seconds 3600)) + (minutes (/ (% seconds 3600) 60)) + (seconds (% seconds 60))) + (format "%.2d:%.2d:%.2d" hours minutes seconds))) + +;;;###autoload +(defun prot-common-seconds-to-minutes-or-hours (seconds) + "Convert SECONDS to either minutes or hours, depending on the value." + (if (> seconds 3599) + (prot-common-seconds-to-hours seconds) + (prot-common-seconds-to-minutes seconds))) + +;;;###autoload +(defun prot-common-rotate-list-of-symbol (symbol) + "Rotate list value of SYMBOL by moving its car to the end. +Return the first element before performing the rotation. + +This means that if `sample-list' has an initial value of `(one +two three)', this function will first return `one' and update the +value of `sample-list' to `(two three one)'. Subsequent calls +will continue rotating accordingly." + (unless (symbolp symbol) + (user-error "%s is not a symbol" symbol)) + (when-let* ((value (symbol-value symbol)) + (list (and (listp value) value)) + (first (car list))) + (set symbol (append (cdr list) (list first))) + first)) + +;;;###autoload +(defun prot-common-empty-buffer-p () + "Test whether the buffer is empty." + (or (= (point-min) (point-max)) + (save-excursion + (goto-char (point-min)) + (while (and (looking-at "^\\([a-zA-Z]+: ?\\)?$") + (zerop (forward-line 1)))) + (eobp)))) + +;;;###autoload +(defun prot-common-minor-modes-active () + "Return list of active minor modes for the current buffer." + (let ((active-modes)) + (mapc (lambda (m) + (when (and (boundp m) (symbol-value m)) + (push m active-modes))) + minor-mode-list) + active-modes)) + +;;;###autoload +(defun prot-common-truncate-lines-silently () + "Toggle line truncation without printing messages." + (let ((inhibit-message t)) + (toggle-truncate-lines t))) + +;; NOTE 2023-08-12: I tried the `clear-message-function', but it did +;; not work. What I need is very simple and this gets the job done. +;;;###autoload +(defun prot-common-clear-minibuffer-message (&rest _) + "Print an empty message to clear the echo area. +Use this as advice :after a noisy function." + (message "")) + +;;;###autoload +(defun prot-common-disable-hl-line () + "Disable Hl-Line-Mode (for hooks)." + (hl-line-mode -1)) + +;;;###autoload +(defun prot-common-window-bounds () + "Return start and end points in the window as a cons cell." + (cons (window-start) (window-end))) + +;;;###autoload +(defun prot-common-page-p () + "Return non-nil if there is a `page-delimiter' in the buffer." + (or (save-excursion (re-search-forward page-delimiter nil t)) + (save-excursion (re-search-backward page-delimiter nil t)))) + +;;;###autoload +(defun prot-common-window-small-p () + "Return non-nil if window is small. +Check if the `window-width' or `window-height' is less than +`split-width-threshold' and `split-height-threshold', +respectively." + (or (and (numberp split-width-threshold) + (< (window-total-width) split-width-threshold)) + (and (numberp split-height-threshold) + (> (window-total-height) split-height-threshold)))) + +(defun prot-common-window-narrow-p () + "Return non-nil if window is narrow. +Check if the `window-width' is less than `split-width-threshold'." + (and (numberp split-width-threshold) + (< (window-total-width) split-width-threshold))) + +;;;###autoload +(defun prot-common-three-or-more-windows-p (&optional frame) + "Return non-nil if three or more windows occupy FRAME. +If FRAME is non-nil, inspect the current frame." + (>= (length (window-list frame :no-minibuffer)) 3)) + +;;;###autoload +(defun prot-common-read-data (file) + "Read Elisp data from FILE." + (with-temp-buffer + (insert-file-contents file) + (read (current-buffer)))) + +;;;###autoload +(defun prot-common-completion-category () + "Return completion category." + (when-let* ((window (active-minibuffer-window))) + (with-current-buffer (window-buffer window) + (completion-metadata-get + (completion-metadata (buffer-substring-no-properties + (minibuffer-prompt-end) + (max (minibuffer-prompt-end) (point))) + minibuffer-completion-table + minibuffer-completion-predicate) + 'category)))) + +;; Thanks to Omar Antolín Camarena for providing this snippet! +;;;###autoload +(defun prot-common-completion-table (category candidates) + "Pass appropriate metadata CATEGORY to completion CANDIDATES. + +This is intended for bespoke functions that need to pass +completion metadata that can then be parsed by other +tools (e.g. `embark')." + (lambda (string pred action) + (if (eq action 'metadata) + `(metadata (category . ,category)) + (complete-with-action action candidates string pred)))) + +;;;###autoload +(defun prot-common-completion-table-no-sort (category candidates) + "Pass appropriate metadata CATEGORY to completion CANDIDATES. +Like `prot-common-completion-table' but also disable sorting." + (lambda (string pred action) + (if (eq action 'metadata) + `(metadata (category . ,category) + (display-sort-function . ,#'identity)) + (complete-with-action action candidates string pred)))) + +;; Thanks to Igor Lima for the `prot-common-crm-exclude-selected-p': +;; <https://github.com/0x462e41>. +;; This is used as a filter predicate in the relevant prompts. +(defvar crm-separator) + +;;;###autoload +(defun prot-common-crm-exclude-selected-p (input) + "Filter out INPUT from `completing-read-multiple'. +Hide non-destructively the selected entries from the completion +table, thus avoiding the risk of inputting the same match twice. + +To be used as the PREDICATE of `completing-read-multiple'." + (if-let* ((pos (string-match-p crm-separator input)) + (rev-input (reverse input)) + (element (reverse + (substring rev-input 0 + (string-match-p crm-separator rev-input)))) + (flag t)) + (progn + (while pos + (if (string= (substring input 0 pos) element) + (setq pos nil) + (setq input (substring input (1+ pos)) + pos (string-match-p crm-separator input) + flag (when pos t)))) + (not flag)) + t)) + +;; The `prot-common-line-regexp-p' and `prot-common--line-regexp-alist' +;; are contributed by Gabriel: <https://github.com/gabriel376>. They +;; provide a more elegant approach to using a macro, as shown further +;; below. +(defvar prot-common--line-regexp-alist + '((empty . "[\s\t]*$") + (indent . "^[\s\t]+") + (non-empty . "^.+$") + (list . "^\\([\s\t#*+]+\\|[0-9]+[^\s]?[).]+\\)") + (heading . "^[=-]+")) + "Alist of regexp types used by `prot-common-line-regexp-p'.") + +(defun prot-common-line-regexp-p (type &optional n) + "Test for TYPE on line. +TYPE is the car of a cons cell in +`prot-common--line-regexp-alist'. It matches a regular +expression. + +With optional N, search in the Nth line from point." + (save-excursion + (goto-char (line-beginning-position)) + (and (not (bobp)) + (or (beginning-of-line n) t) + (save-match-data + (looking-at + (alist-get type prot-common--line-regexp-alist)))))) + +;; The `prot-common-shell-command-with-exit-code-and-output' function is +;; courtesy of Harold Carr, who also sent a patch that improved +;; `prot-eww-download-html' (from the `prot-eww.el' library). +;; +;; More about Harold: <http://haroldcarr.com/about/>. +(defun prot-common-shell-command-with-exit-code-and-output (command &rest args) + "Run COMMAND with ARGS. +Return the exit code and output in a list." + (with-temp-buffer + (list (apply 'call-process command nil (current-buffer) nil args) + (buffer-string)))) + +(defvar prot-common-url-regexp + (concat + "~?\\<\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]*\\)" + "[.@]" + "\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]+\\)\\>/?") + "Regular expression to match (most?) URLs or email addresses.") + +(autoload 'auth-source-search "auth-source") + +;;;###autoload +(defun prot-common-auth-get-field (host prop) + "Find PROP in `auth-sources' for HOST entry." + (when-let* ((source (auth-source-search :host host))) + (if (eq prop :secret) + (funcall (plist-get (car source) prop)) + (plist-get (flatten-list source) prop)))) + +;;;###autoload +(defun prot-common-parse-file-as-list (file) + "Return the contents of FILE as a list of strings. +Strings are split at newline characters and are then trimmed for +negative space. + +Use this function to provide a list of candidates for +completion (per `completing-read')." + (split-string + (with-temp-buffer + (insert-file-contents file) + (buffer-substring-no-properties (point-min) (point-max))) + "\n" :omit-nulls "[\s\f\t\n\r\v]+")) + +(defun prot-common-ignore (&rest _) + "Use this as override advice to make a function do nothing." + nil) + +;; NOTE 2023-06-02: The `prot-common-wcag-formula' and +;; `prot-common-contrast' are taken verbatim from my `modus-themes' +;; and renamed to have the prefix `prot-common-' instead of +;; `modus-themes-'. This is all my code, of course, but I do it this +;; way to ensure that this file is self-contained in case someone +;; copies it. + +;; This is the WCAG formula: <https://www.w3.org/TR/WCAG20-TECHS/G18.html>. +(defun prot-common-wcag-formula (hex) + "Get WCAG value of color value HEX. +The value is defined in hexadecimal RGB notation, such #123456." + (cl-loop for k in '(0.2126 0.7152 0.0722) + for x in (color-name-to-rgb hex) + sum (* k (if (<= x 0.03928) + (/ x 12.92) + (expt (/ (+ x 0.055) 1.055) 2.4))))) + +;;;###autoload +(defun prot-common-contrast (c1 c2) + "Measure WCAG contrast ratio between C1 and C2. +C1 and C2 are color values written in hexadecimal RGB." + (let ((ct (/ (+ (prot-common-wcag-formula c1) 0.05) + (+ (prot-common-wcag-formula c2) 0.05)))) + (max ct (/ ct)))) + +;;;; EXPERIMENTAL macros (not meant to be used anywhere) + +;; TODO 2023-09-30: Try the same with `cl-defmacro' and &key +(defmacro prot-common-if (condition &rest consequences) + "Separate the CONSEQUENCES of CONDITION semantically. +Like `if', `when', `unless' but done by using `:then' and `:else' +keywords. The forms under each keyword of `:then' and `:else' +belong to the given subset of CONSEQUENCES. + +- The absence of `:else' means: (if CONDITION (progn CONSEQUENCES)). +- The absence of `:then' means: (if CONDITION nil CONSEQUENCES). +- Otherwise: (if CONDITION (progn then-CONSEQUENCES) else-CONSEQUENCES)." + (declare (indent 1)) + (let (then-consequences else-consequences last-kw) + (dolist (elt consequences) + (let ((is-keyword (keywordp elt))) + (cond + ((and (not is-keyword) (eq last-kw :then)) + (push elt then-consequences)) + ((and (not is-keyword) (eq last-kw :else)) + (push elt else-consequences)) + ((and is-keyword (eq elt :then)) + (setq last-kw :then)) + ((and is-keyword (eq elt :else)) + (setq last-kw :else))))) + `(if ,condition + ,(if then-consequences + `(progn ,@(nreverse then-consequences)) + nil) + ,@(nreverse else-consequences)))) + +(provide 'prot-common) +;;; prot-common.el ends here diff --git a/custom-lisp/prot-window.el b/custom-lisp/prot-window.el new file mode 100644 index 0000000..3a77aa3 --- /dev/null +++ b/custom-lisp/prot-window.el @@ -0,0 +1,236 @@ +;;; prot-window.el --- Display-buffer and window-related extensions for my dotemacs -*- lexical-binding: t -*- + +;; Copyright (C) 2023-2024 Protesilaos Stavrou + +;; Author: Protesilaos Stavrou <info@protesilaos.com> +;; URL: https://protesilaos.com/emacs/dotemacs +;; Version: 0.1.0 +;; Package-Requires: ((emacs "30.1")) + +;; This file is NOT part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; This covers my window and display-buffer extensions, for use in my +;; Emacs setup: https://protesilaos.com/emacs/dotemacs. +;; +;; Remember that every piece of Elisp that I write is for my own +;; educational and recreational purposes. I am not a programmer and I +;; do not recommend that you copy any of this if you are not certain of +;; what it does. + +;;; Code: + +(require 'prot-common) + +(defvar prot-window-window-sizes + '( :max-height (lambda () (floor (frame-height) 3)) + :min-height 10 + :max-width (lambda () (floor (frame-width) 4)) + :min-width 20) + "Property list of maximum and minimum window sizes. +The property keys are `:max-height', `:min-height', `:max-width', +and `:min-width'. They all accept a value of either a +number (integer or floating point) or a function.") + +(defun prot-window--get-window-size (key) + "Extract the value of KEY from `prot-window-window-sizes'." + (when-let* ((value (plist-get prot-window-window-sizes key))) + (cond + ((functionp value) + (funcall value)) + ((numberp value) + value) + (t + (error "The value of `%s' is neither a number nor a function" key))))) + +(defun prot-window-select-fit-size (window) + "Select WINDOW and resize it. +The resize pertains to the maximum and minimum values for height +and width, per `prot-window-window-sizes'. + +Use this as the `body-function' in a `display-buffer-alist' entry." + (select-window window) + (fit-window-to-buffer + window + (prot-window--get-window-size :max-height) + (prot-window--get-window-size :min-height) + (prot-window--get-window-size :max-width) + (prot-window--get-window-size :min-width)) + ;; If we did not use `display-buffer-below-selected', then we must + ;; be in a lateral window, which has more space. Then we do not + ;; want to dedicate the window to this buffer, because we will be + ;; running out of space. + (when (or (window-in-direction 'above) (window-in-direction 'below)) + (set-window-dedicated-p window t))) + +(defun prot-window--get-display-buffer-below-or-pop () + "Return list of functions for `prot-window-display-buffer-below-or-pop'." + (list + #'display-buffer-reuse-mode-window + (if (or (prot-common-window-small-p) + (prot-common-three-or-more-windows-p)) + #'display-buffer-below-selected + #'display-buffer-pop-up-window))) + +(defun prot-window-display-buffer-below-or-pop (&rest args) + "Display buffer below current window or pop a new window. +The criterion for choosing to display the buffer below the +current one is a non-nil return value for +`prot-common-window-small-p'. + +Apply ARGS expected by the underlying `display-buffer' functions. + +This as the action function in a `display-buffer-alist' entry." + (let ((functions (prot-window--get-display-buffer-below-or-pop))) + (catch 'success + (dolist (fn functions) + (when (apply fn args) + (throw 'success fn)))))) + +(defun prot-window-shell-or-term-p (buffer &rest _) + "Check if BUFFER is a shell or terminal. +This is a predicate function for `buffer-match-p', intended for +use in `display-buffer-alist'." + (when (string-match-p "\\*.*\\(e?shell\\|v?term\\).*" (buffer-name (get-buffer buffer))) + (with-current-buffer buffer + ;; REVIEW 2022-07-14: Is this robust? + (and (not (derived-mode-p 'message-mode 'text-mode)) + (derived-mode-p 'eshell-mode 'shell-mode 'comint-mode 'fundamental-mode))))) + +(defun prot-window-remove-dedicated (&rest _) + "Remove dedicated window parameter. +Use this as :after advice to `delete-other-windows' and +`delete-window'." + (when (one-window-p :no-mini) + (set-window-dedicated-p nil nil))) + +(mapc + (lambda (fn) + (advice-add fn :after #'prot-window-remove-dedicated)) + '(delete-other-windows delete-window)) + +(defmacro prot-window-define-full-frame (name &rest args) + "Define command to call ARGS in new frame with `display-buffer-full-frame' bound. +Name the function prot-window- followed by NAME. If ARGS is nil, +call NAME as a function." + (declare (indent 1)) + `(defun ,(intern (format "prot-window-%s" name)) () + ,(format "Call `prot-window-%s' in accordance with `prot-window-define-full-frame'." name) + (interactive) + (let ((display-buffer-alist '((".*" (display-buffer-full-frame))))) + (with-selected-frame (make-frame) + ,(if args + `(progn ,@args) + `(funcall ',name)) + (modify-frame-parameters nil '((buffer-list . nil))))))) + +(defun prot-window--get-shell-buffers () + "Return list of `shell' buffers." + (seq-filter + (lambda (buffer) + (with-current-buffer buffer + (derived-mode-p 'shell-mode))) + (buffer-list))) + +(defun prot-window--get-new-shell-buffer () + "Return buffer name for `shell' buffers." + (if-let* ((buffers (prot-window--get-shell-buffers)) + (buffers-length (length buffers)) + ((>= buffers-length 1))) + (format "*shell*<%s>" (1+ buffers-length)) + "*shell*")) + +;;;###autoload (autoload 'prot-window-shell "prot-window") +(prot-window-define-full-frame shell + (let ((name (prot-window--get-new-shell-buffer))) + (shell name) + (set-frame-name name) + (when-let* ((buffer (get-buffer name))) + (with-current-buffer buffer + (add-hook + 'delete-frame-functions + (lambda (_) + ;; FIXME 2023-09-09: Works for multiple frames (per + ;; `make-frame-command'), but not if the buffer is in two + ;; windows in the same frame. + (unless (> (safe-length (get-buffer-window-list buffer nil t)) 1) + (let ((kill-buffer-query-functions nil)) + (kill-buffer buffer)))) + nil + :local))))) + +;;;###autoload (autoload 'prot-window-coach "prot-window") +(prot-window-define-full-frame coach + (let ((buffer (get-buffer-create "*scratch for coach*"))) + (with-current-buffer buffer + (funcall initial-major-mode)) + (display-buffer buffer) + (set-frame-name "Coach"))) + +;; REVIEW 2023-06-25: Does this merit a user option? I don't think I +;; will ever set it to the left. It feels awkward there. +(defun prot-window-scroll-bar-placement () + "Control the placement of scroll bars." + (when scroll-bar-mode + (setq default-frame-scroll-bars 'right) + (set-scroll-bar-mode 'right))) + +(add-hook 'scroll-bar-mode-hook #'prot-window-scroll-bar-placement) + +(defun prot-window-no-minibuffer-scroll-bar (frame) + "Remove the minibuffer scroll bars from FRAME." + (set-window-scroll-bars (minibuffer-window frame) nil nil nil nil :persistent)) + +(add-hook 'after-make-frame-functions 'prot-window-no-minibuffer-scroll-bar) + +;;;; Run commands in a popup frame (via emacsclient) + +(defun prot-window-delete-popup-frame (&rest _) + "Kill selected selected frame if it has parameter `prot-window-popup-frame'. +Use this function via a hook." + (when (frame-parameter nil 'prot-window-popup-frame) + (delete-frame))) + +(defmacro prot-window-define-with-popup-frame (command) + "Define function which calls COMMAND in a new frame. +Make the new frame have the `prot-window-popup-frame' parameter." + `(defun ,(intern (format "prot-window-popup-%s" command)) () + ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter. +Also see `prot-window-delete-popup-frame'." command) + (interactive) + (let ((frame (make-frame '((prot-window-popup-frame . t))))) + (select-frame frame) + (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame") + (condition-case nil + (call-interactively ',command) + ((quit error user-error) + (delete-frame frame)))))) + +(declare-function org-capture "org-capture" (&optional goto keys)) +(defvar org-capture-after-finalize-hook) + +;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window") +(prot-window-define-with-popup-frame org-capture) + +(declare-function tmr "tmr" (time &optional description acknowledgep)) +(defvar tmr-timer-created-functions) + +;;;###autoload (autoload 'prot-window-popup-tmr "prot-window") +(prot-window-define-with-popup-frame tmr) + +(provide 'prot-window) +;;; prot-window.el ends here diff --git a/unravel-emacs.org b/unravel-emacs.org index fb45270..0862b3a 100644 --- a/unravel-emacs.org +++ b/unravel-emacs.org @@ -2062,6 +2062,936 @@ I use ~vertico-repeat~ to mimic the functionality that ~helm-resume~ would provi #+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el" (provide 'unravel-completion) #+end_src +* The =unravel-search.el= module +:PROPERTIES: +:CUSTOM_ID: h:e0f9c30e-3a98-4479-b709-7008277749e4 +:END: + +[ Watch Prot's talk: [[https://protesilaos.com/codelog/2023-06-10-emacs-search-replace-basics/][Emacs: basics of search and replace]] (2023-06-10). ] + +#+begin_quote +Emacs provides lots of useful facilities to search the contents of +buffers or files. The most common scenario is to type =C-s= +(~isearch-forward~) to perform a search forward from point or =C-r= +(~isearch-backward~) to do so in reverse. These commands pack a ton of +functionality and they integrate nicely with related facilities, such +as those of (i) permanently highlighting the thing being searched, +(ii) putting all results in a buffer that is useful for navigation +purposes, among others, and (iii) replacing the given matching items +with another term. + +Here I summarise the functionality, though do check the video I did on +the basics of search and replace: + +- =C-s= (~isearch-forward~) :: Search forward from point (incremental + search); retype =C-s= to move forth. + +- =C-r= (~isearch-backward~) :: Search backward from point + (incremental); retype =C-r= to move back. While using either =C-s= + and =C-r= you can move in the opposite direction with either of + those keys when performing a repeat. + +- =C-M-s= (~isearch-forward-regexp~) :: Same as =C-s= but matches a + regular expression. The =C-s= and =C-r= motions are the same after + matches are found. + +- =C-M-r= (~isearch-backward-regexp~) :: The counterpart of the above + =C-M-s= for starting in reverse. + +- =C-s C-w= (~isearch-yank-word-or-char~) :: Search forward for + word-at-point. Again, =C-s= and =C-r= move forth and back, + respectively. + +- =C-r C-w= (~isearch-yank-word-or-char~) :: Same as above, but + backward. + +- =M-s o= (~occur~) :: Search for the given regular expression + throughout the buffer and collect the matches in an =*occur*= + buffer. Also check what I am doing with this in my custom + extensions: [[#h:b902e6a3-cdd2-420f-bc99-3d973c37cd20][The =unravel-search.el= extras provided by the =prot-search.el= library]]. + +- =C-u 5 M-s o= (~occur~) :: Like the above, but give it N lines of + context when N is the prefix numeric argument (5 in + this example). + +- =C-s SEARCH= followed by =M-s o= (~isearch-forward~ --> ~occur~) :: + Like =C-s= but then put the matches in an *occur* buffer. + +- =C-s SEARCH= followed by =C-u 5 M-s o= (~isearch-forward~ --> + ~occur~) :: Same as above, but now with N lines of context (5 in + this example). + +- =M-%= (~query-replace~) :: Prompt for target to replace and then + prompt for its replacement (see explanation) + +- =C-M-%= (~query-replace-regexp~) :: Same as above, but for REGEXP + +- =C-s SEARCH= followed by =M-%= (~isearch-forward~ --> ~query-replace~) :: Search + with =C-s= and then perform a query-replace for the following + matches. + +- =C-M-s SEARCH M-%= (~isearch-forward-regexp~ --> + ~query-replace-regexp~) :: As above, but regexp-aware. + +- =C-s SEARCH C-M-%= (~isearch-forward~ --> ~query-replace-regexp~) :: Same + as above. + +- =M-s h r= (~highlight-regexp~) :: Prompt for a face (like + ~hi-yellow~) to highlight the given regular expression. + +- =M-s h u= (~unhighlight-regexp~) :: Prompt for an already + highlighted regular expression to unhighlight (do it after the + above). + +For starters, just learn: + +- =C-s= +- =C-r= +- =M-s o= +- =M-%= + +Now on to the configurations. +#+end_quote + +** The =unravel-search.el= on isearch lax space +:PROPERTIES: +:CUSTOM_ID: h:95947b37-2071-4ee7-a201-8e19bf3322e9 +:END: + +#+begin_quote +The first thing I want to do for Isearch, is make it more convenient +for me to match words that occur in sequence but are not necessarily +following each other. By default, we can do that with something like +=C-M-s= (~isearch-forward-regexp~) followed by =one.*two=. Though it +is inconvenient to be a regexp-aware search mode when all we want is +to just type =one two= and have the space be interpreted as +"intermediate characters" rather than a literal space. The following +do exactly this for regular =C-s= (~isearch-forward~) and =C-r= +(~isearch-backward~). +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" :mkdirp yes + ;;; Isearch, occur, grep + (use-package isearch + :ensure nil + :demand t + :config + (setq search-whitespace-regexp ".*?" ; one `setq' here to make it obvious they are a bundle + isearch-lax-whitespace t + isearch-regexp-lax-whitespace nil)) +#+end_src + +** The =unravel-search.el= settings for isearch highlighting +:PROPERTIES: +:CUSTOM_ID: h:ed1307e7-f8a0-4b0a-8d91-2de9c1e2479c +:END: + +#+begin_quote +Here I am just tweaking the delay that affects when deferred +highlights are applied. The current match is highlighted immediately. +The rest are done after ~lazy-highlight-initial-delay~ unless they are +longer in character count than ~lazy-highlight-no-delay-length~. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" +(use-package isearch + :ensure nil + :demand t + :config + (setq search-highlight t) + (setq isearch-lazy-highlight t) + (setq lazy-highlight-initial-delay 0.5) + (setq lazy-highlight-no-delay-length 4)) +#+end_src + +** The =unravel-search.el= on isearch match counter +:PROPERTIES: +:CUSTOM_ID: h:acfdc17f-7ffb-48d3-90ff-49bd00463934 +:END: + +#+begin_quote +I think the following options should be enabled by default. They +produce a counter next to the isearch prompt that shows the position +of the current match relative to the total count (like =5/20=). As we +move to the next/previous match, the counter is updated accordingly. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" +(use-package isearch + :ensure nil + :demand t + :config + (setq isearch-lazy-count t) + (setq lazy-count-prefix-format "(%s/%s) ") + (setq lazy-count-suffix-format nil)) +#+end_src + +** The =unravel-search.el= tweaks for the occur buffer +:PROPERTIES: +:CUSTOM_ID: h:85aca4da-b89b-4fbe-89e9-3ec536ad7b0d +:END: + +#+begin_quote +Here I am making some minor tweaks to =*occur*= buffer (remember to +read the introduction to this section ([[#h:e0f9c30e-3a98-4479-b709-7008277749e4][The =unravel-search.el= module]])). +I always want (i) the cursor to be at the top of the buffer, (ii) the +current line to be highlighted, as it is easier for selection +purposes ... +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" +(use-package isearch + :ensure nil + :demand t + :config + (setq list-matching-lines-jump-to-current-line nil) ; do not jump to current line in `*occur*' buffers + (add-hook 'occur-mode-hook #'hl-line-mode)) +#+end_src + +** The =unravel-search.el= modified isearch and occur key bindings +:PROPERTIES: +:CUSTOM_ID: h:5ce6216d-f318-4191-9d4f-9681c92f7582 +:END: + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" +(use-package isearch + :ensure nil + :demand t + :bind + ( :map minibuffer-local-isearch-map + ("M-/" . isearch-complete-edit) + :map occur-mode-map + ("t" . toggle-truncate-lines) + :map isearch-mode-map + ("C-g" . isearch-cancel) ; instead of `isearch-abort' + ("M-/" . isearch-complete))) +#+end_src + +** The =unravel-search.el= tweaks to ~xref~, ~re-builder~ and ~grep~ +:PROPERTIES: +:CUSTOM_ID: h:ceb286c5-a5f7-4cc8-b883-89d20a75ea02 +:END: + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" +;;; grep and xref +(use-package re-builder + :ensure nil + :commands (re-builder regexp-builder) + :config + (setq reb-re-syntax 'read)) + +(use-package xref + :ensure nil + :commands (xref-find-definitions xref-go-back) + :config + ;; All those have been changed for Emacs 28 + (setq xref-show-definitions-function #'xref-show-definitions-completing-read) ; for M-. + (setq xref-show-xrefs-function #'xref-show-definitions-buffer) ; for grep and the like + (setq xref-file-name-display 'project-relative)) + +(use-package grep + :ensure nil + :commands (grep lgrep rgrep) + :config + (setq grep-save-buffers nil) + (setq grep-use-headings t) ; Emacs 30 + + (let ((executable (or (executable-find "rg") "grep")) + (rgp (string-match-p "rg" grep-program))) + (setq grep-program executable) + (setq grep-template + (if rgp + "/usr/bin/rg -nH --null -e <R> <F>" + "/usr/bin/grep <X> <C> -nH --null -e <R> <F>")) + (setq xref-search-program (if rgp 'ripgrep 'grep)))) +#+end_src + +** The =unravel-search.el= setup for editable grep buffers (~grep-edit-mode~ or ~wgrep~) +:PROPERTIES: +:CUSTOM_ID: h:9a3581df-ab18-4266-815e-2edd7f7e4852 +:END: + +#+begin_quote +Starting with Emacs 31, buffers using ~grep-mode~ can now be edited +directly. The idea is to collect the results of a search in one place +and quickly apply a change across all or some of them. We have the +same concept with occur (=M-x occur=) as well as with Dired buffers +([[#h:1b53bc10-8b1b-4f68-bbec-165909761e43][The =unravel-dired.el= section about ~wdired~ (writable Dired)]]). + +For older versions of Emacs, we have the ~wgrep~ package by Masahiro +Hayashi. I configure it to have key bindings like those of the ~occur~ +edit mode, which ~grep-edit-mode~ also uses. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" +;;; wgrep (writable grep) +;; See the `grep-edit-mode' for the new built-in feature. +(unless (>= emacs-major-version 31) + (use-package wgrep + :ensure t + :after grep + :bind + ( :map grep-mode-map + ("e" . wgrep-change-to-wgrep-mode) + ("C-x C-q" . wgrep-change-to-wgrep-mode) + ("C-c C-c" . wgrep-finish-edit)) + :config + (setq wgrep-auto-save-buffer t) + (setq wgrep-change-readonly-file t))) +#+end_src + +** Finally, we provide the =unravel-search.el= module +:PROPERTIES: +:CUSTOM_ID: h:c8b2f021-fe5a-4f6b-944c-20340f764fb2 +:END: + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" +(provide 'unravel-search) +#+end_src + +* The =unravel-dired.el= module +:PROPERTIES: +:CUSTOM_ID: h:f8b08a77-f3a8-42fa-b1a9-f940348889c3 +:END: + +[ Watch Prot's talk: <https://protesilaos.com/codelog/2023-06-26-emacs-file-dired-basics/> (2023-06-26) ] + +#+begin_quote +Dired is probably my favourite Emacs tool. It exemplifies how I see +Emacs as a whole: a layer of interactivity on top of Unix. The ~dired~ +interface wraps---and puts to synergy---standard commands like ~ls~, +~cp~, ~mv~, ~rm~, ~mkdir~, ~chmod~, and related. All while granting +access to many other conveniences, such as (i) marking files to +operate on (individually, with a regexp, etc.), (ii) bulk renaming +files by making the buffer writable and editing it like a regular +file, (iii) showing only files you want, (iv) listing the contents of +any subdirectory, such as to benefit from the bulk-renaming +capability, (v) running a keyboard macro that edits file contents +while using Dired to navigate the file listing, (vi) open files in an +external application, and more. + +Dired lets us work with our files in a way that still feels close to +the command-line, yet has more powerful interactive features than even +fully fledged, graphical file managers. +#+end_quote + +** The =unravel-dired.el= settings for common operations +:PROPERTIES: +:CUSTOM_ID: h:39fb0eab-54bb-4e5b-8e38-9443dbe5c5ee +:END: + +#+begin_quote +I add two settings which make all copy, rename/move, and delete +operations more intuitive. I always want to perform those actions in a +recursive manner, as this is the intent I have when I am targeting +directories. + +The ~delete-by-moving-to-trash~ is a deviation from the behaviour of +the ~rm~ program, as it sends the file into the virtual trash folder. +Depending on the system, files in the trash are either removed +automatically after a few days, or we still have to permanently delete +them manually. I prefer this extra layer of safety. Plus, we have the +~trashed~ package to navigate the trash folder in a Dired-like way +([[#h:2e005bd1-d098-426d-91f9-2a31a6e55caa][The =unravel-dired.el= section about =trashed.el=]]). +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" :mkdirp yes +;;; Dired file manager and prot-dired.el extras +(use-package dired + :ensure nil + :commands (dired) + :config + (setq dired-recursive-copies 'always) + (setq dired-recursive-deletes 'always) + (setq delete-by-moving-to-trash t)) +#+end_src + +** The =unravel-dired.el= switches for ~ls~ (how files are listed) +:PROPERTIES: +:CUSTOM_ID: h:679e4460-b306-450f-aa20-497243057e02 +:END: + +#+begin_quote +As I already explained, Dired is a layer of interactivity on top of +standard Unix tools ([[#h:f8b08a77-f3a8-42fa-b1a9-f940348889c3][The =unravel-dired.el= module]]). We can see +this in how Dired produces the file listing and how we can affect it. +The ~ls~ program accepts an =-l= flag for a "long", detailed list of +files. This is what Dired uses. But we can pass more flags by setting +the value of ~dired-listing-switches~. Do =M-x man= and then search +for the ~ls~ manpage to learn about what I have here. In short: + +- =-A= :: Show hidden files ("dotfiles"), such as =.bashrc=, but omit + the implied =.= and =..= targets. The latter two refer to the + present and parent directory, respectively. + +- =-G= :: Do not show the group name in the long listing. Only show + the owner of the file. + +- =-F= :: Differentiate regular from special files by appending a + character to them. The =*= is for executables, the =/= is for + directories, the =|= is for a named pipe, the ~=~ is for a socket, + the =@= and the =>= are for stuff I have never seen. + +- =-h= :: Make file sizes easier to read, such as =555k= instead of + =568024= (the size of =unravel.org= as of this writing). + +- =-l= :: Produce a long, detailed listing. Dired requires this. + +- =-v= :: Sort files by version numbers, such that =file1=, =file2=, + and =file10= appear in this order instead of 1, 10, 2. The latter is + called "lexicograhic" and I have not found a single case where it is + useful to me. + +- =--group-directories-first= :: Does what it says to place all + directories before files in the listing. I prefer this over a strict + sorting that does not differentiate between files and directories. + +- =--time-style=long-iso= :: Uses the international standard for time + representation in the file listing. So we have something like + =2023-12-30 06:38= to show the last modified time. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" + (use-package dired + :ensure nil + :commands (dired) + :config + (setq dired-listing-switches + "-AGFhlv --group-directories-first --time-style=long-iso")) +#+end_src + +** The =unravel-dired.el= setting for dual-pane Dired +:PROPERTIES: +:CUSTOM_ID: h:8225364c-3856-48bc-bf64-60d40ddd3320 +:END: + +#+begin_quote +I often have two Dired buffers open side-by-side and want to move +files between them. By setting ~dired-dwim-target~ to a ~t~ value, +we get the other buffer as the default target of the current rename or +copy operation. This is exactly what I want. + +If there are more than two windows showing Dired buffers, the default +target is the previously visited window. + +Note that this only affects how quickly we can access the default +value, as we can always type =M-p= (~previous-history-element~) and +=M-n= (~next-history-element~) to cycle through the minibuffer +history ([[#h:25765797-27a5-431e-8aa4-cc890a6a913a][The =unravel-completion.el= settings for saving the history (~savehist-mode~)]]). +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" +(use-package dired + :ensure nil + :commands (dired) + :config + (setq dired-dwim-target t)) +#+end_src + +** The =unravel-dired.el= miscellaneous tweaks +:PROPERTIES: +:CUSTOM_ID: h:6327e6ba-a468-416f-ad26-b530c32fe235 +:END: + +#+begin_quote +These are some minor tweaks that I do not really care about. The only +one which is really nice in my opinion is the hook that involves the +~dired-hide-details-mode~. This is the command that hides the noisy +output of the ~ls~ =-l= flag, leaving only the file names in the list +([[#h:679e4460-b306-450f-aa20-497243057e02][The =unravel-dired.el= switches for ~ls~ (how files are listed)]]). +We can toggle this effect at any time with the =(= key, by default. + +I disable the repetition of the =j= key as I do use ~repeat-mode~ +([[#h:fbe6f9da-25ee-46a3-bb03-8fa7c1d48dab][The =unravel-essentials.el= settings for ~repeat-mode~]]). +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" +(use-package dired + :ensure nil + :commands (dired) + :config + (setq dired-auto-revert-buffer #'dired-directory-changed-p) ; also see `dired-do-revert-buffer' + (setq dired-make-directory-clickable t) ; Emacs 29.1 + (setq dired-free-space nil) ; Emacs 29.1 + (setq dired-mouse-drag-files t) ; Emacs 29.1 + + (add-hook 'dired-mode-hook #'dired-hide-details-mode) + (add-hook 'dired-mode-hook #'hl-line-mode) + + ;; In Emacs 29 there is a binding for `repeat-mode' which lets you + ;; repeat C-x C-j just by following it up with j. For me, this is a + ;; problem as j calls `dired-goto-file', which I often use. + (define-key dired-jump-map (kbd "j") nil)) +#+end_src + +** The =unravel-dired.el= section about various conveniences +:PROPERTIES: +:CUSTOM_ID: h:6758bf16-e47e-452e-b39d-9d67c2b9aa4b +:END: + +#+begin_quote +The =dired-aux.el= and =dired-x.el= are two built-in libraries that +provide useful extras for Dired. The highlights from what I have here +are: + +- the user option ~dired-create-destination-dirs~ and + ~dired-create-destination-dirs-on-trailing-dirsep~, which offer to + create the specified directory path if it is missing. + +- the user options ~dired-clean-up-buffers-too~ and + ~dired-clean-confirm-killing-deleted-buffers~ which cover the + deletion of buffers related to files that we delete from Dired. + +- the key binding for ~dired-do-open~, which opens the file or + directory externally ([[#h:d7d5e619-3d50-4d9e-a951-7262462a60c9][The =unravel-dired.el= settings to open files externally]]). +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" +(use-package dired-aux + :ensure nil + :after dired + :bind + ( :map dired-mode-map + ("C-+" . dired-create-empty-file) + ("M-s f" . nil) + ("C-<return>" . dired-do-open) ; Emacs 30 + ("C-x v v" . dired-vc-next-action)) ; Emacs 28 + :config + (setq dired-isearch-filenames 'dwim) + (setq dired-create-destination-dirs 'ask) ; Emacs 27 + (setq dired-vc-rename-file t) ; Emacs 27 + (setq dired-do-revert-buffer (lambda (dir) (not (file-remote-p dir)))) ; Emacs 28 + (setq dired-create-destination-dirs-on-trailing-dirsep t)) ; Emacs 29 + +(use-package dired-x + :ensure nil + :after dired + :bind + ( :map dired-mode-map + ("I" . dired-info)) + :config + (setq dired-clean-up-buffers-too t) + (setq dired-clean-confirm-killing-deleted-buffers t) + (setq dired-x-hands-off-my-keys t) ; easier to show the keys I use + (setq dired-bind-man nil) + (setq dired-bind-info nil)) +#+end_src + +** The =unravel-dired.el= section about ~dired-subtree~ +:PROPERTIES: +:CUSTOM_ID: h:3a4a29bc-3491-4d01-9d64-1cef63b3116a +:END: + +#+begin_quote +The ~dired-subtree~ package by Matúš Goljer provides the convenience +of quickly revealing the contents of the directory at point. We do not +have to insert its contents below the current listing, as we would +normally do in Dired, nor do we have to open it in another buffer just +to check if we need to go further. + +I do not use this feature frequently, though I appreciate it when I do +need it. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" +(use-package dired-subtree + :ensure t + :after dired + :bind + ( :map dired-mode-map + ("<tab>" . dired-subtree-toggle) + ("TAB" . dired-subtree-toggle) + ("<backtab>" . dired-subtree-remove) + ("S-TAB" . dired-subtree-remove)) + :config + (setq dired-subtree-use-backgrounds nil)) +#+end_src + +** The =unravel-dired.el= section about ~wdired~ (writable Dired) +:PROPERTIES: +:CUSTOM_ID: h:1b53bc10-8b1b-4f68-bbec-165909761e43 +:END: + +#+begin_quote +As noted in the introduction, Dired can be made writable +([[#h:f8b08a77-f3a8-42fa-b1a9-f940348889c3][The =unravel-dired.el= module]]). This way, we can quickly rename +multiple files using Emacs' panoply of editing capabilities. + +Both of the variables I configure here have situational usage. I +cannot remember the last time I benefited from them. + +Note that we have a variant of ~wdired~ for ~grep~ buffers +([[#h:9a3581df-ab18-4266-815e-2edd7f7e4852][The =unravel-search.el= setup for editable grep buffers (~wgrep~)]]). +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" +(use-package wdired + :ensure nil + :commands (wdired-change-to-wdired-mode) + :config + (setq wdired-allow-to-change-permissions t) + (setq wdired-create-parent-directories t)) +#+end_src + +** The =unravel-dired.el= section about ~trashed~ +:PROPERTIES: +:CUSTOM_ID: h:2e005bd1-d098-426d-91f9-2a31a6e55caa +:END: + +The ~trashed~ package by Shingo Tanaka provides a Dired-like interface +to the system's virtual trash directory. The few times I need to +restore a file, I do =M-x trashed=, then type =r= to mark the file to be +restored (=M-x trashed-flag-restore=), and then type =x= (=M-x trashed-do-execute=) +to apply the effect. + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" +;;; dired-like mode for the trash (trashed.el) +(use-package trashed + :ensure t + :commands (trashed) + :config + (setq trashed-action-confirmer 'y-or-n-p) + (setq trashed-use-header-line t) + (setq trashed-sort-key '("Date deleted" . t)) + (setq trashed-date-format "%Y-%m-%d %H:%M:%S")) +#+end_src + +** Finally, we provide the =unravel-dired.el= module +:PROPERTIES: +:CUSTOM_ID: h:c8b2f021-fe5a-4f6b-944c-20340f764fb2 +:END: + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" +(provide 'unravel-dired) +#+end_src + +* The =unravel-window.el= module +:PROPERTIES: +:CUSTOM_ID: h:b5fa481d-8549-4424-869e-91091cdf730b +:END: + +#+begin_quote +This module is all about buffers and windows. How they are managed and +displayed. +#+end_quote + +** The =unravel-window.el= section about uniquifying buffer names +:PROPERTIES: +:CUSTOM_ID: h:cfbea29c-3290-4fd1-a02a-d7e887c15674 +:END: + +#+begin_quote +When a buffer name is reserved, Emacs tries to produce the new buffer +by finding a suitable variant of the original name. The doc string of +the variable ~uniquify-buffer-name-style~ does a good job at +explaining the various patterns: + +#+begin_example +For example, the files ‘/foo/bar/mumble/name’ and ‘/baz/quux/mumble/name’ +would have the following buffer names in the various styles: + + forward bar/mumble/name quux/mumble/name + reverse name\mumble\bar name\mumble\quux + post-forward name|bar/mumble name|quux/mumble + post-forward-angle-brackets name<bar/mumble> name<quux/mumble> + nil name name<2> +#+end_example + +I use the =forward= style, which is the closest to the actual file +name. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el" +;;; General window and buffer configurations +(use-package uniquify + :ensure nil + :config +;;;; `uniquify' (unique names for buffers) + (setq uniquify-buffer-name-style 'forward) + (setq uniquify-strip-common-suffix t) + (setq uniquify-after-kill-buffer-p t)) +#+end_src + +** The =unravel-window.el= rules for displaying buffers (~display-buffer-alist~) +:PROPERTIES: +:CUSTOM_ID: h:50f8b1e4-b14e-453f-a37e-1c0e495ab80f +:END: + +[ Watch Prot's talk: [[https://protesilaos.com/codelog/2024-02-08-emacs-window-rules-display-buffer-alist/][control where buffers are displayed (the ~display-buffer-alist~)]] (2024-02-08). ] + +#+begin_quote +The ~display-buffer-alist~ is a powerful user option and somewhat hard +to get started with. The reason for its difficulty comes from the +knowledge required to understand the underlying ~display-buffer~ +mechanism. + +Here is the gist of what we do with it: + +- The alist is a list of lists. +- Each element of the alist (i.e. one of the lists) is of the + following form: + + #+begin_example + (BUFFER-MATCHER + FUNCTIONS-TO-DISPLAY-BUFFER + OTHER-PARAMETERS) + #+end_example + +- The =BUFFER-MATCHER= is either a regular expression to match the + buffer by its name or a method to get the buffer whose major mode is + the one specified. In the latter case, you will see the use of cons + cells (like =(one . two)=) involving the ~derived-mode~ symbol + (remember that I build Emacs from source, so ~derived-mode~ may not + exist in your version of Emacs). + +- The =FUNCTIONS-TO-DISPLAY-BUFFER= is a list of ~display-buffer~ + functions that are tried in the order they appear in until one + works. The list can be of one element, as you will notice with some + of my entries. + +- The =OTHER-PARAMETERS= are enumerated in the Emacs Lisp Reference + Manual. Evaluate: + + #+begin_src emacs-lisp + (info "(elisp) Buffer Display Action Alists") + #+end_src + +In my =prot-window.el= library, I define functions that determine how +a buffer should be displayed, given size considerations ([[#h:35b8a0a5-c447-4301-a404-bc274596238d][The =prot-window.el= library]]). +You will find the functions ~prot-window-shell-or-term-p~ to determine +what a shell or terminal is, ~prot-window-display-buffer-below-or-pop~ +to display the buffer below the current one or to its side depending +on how much width is available, and ~prot-window-select-fit-size~ to +perform the two-fold task of selecting a window and making it fit up +to a certain height. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el" + ;;;; `window', `display-buffer-alist', and related + (use-package prot-window + :ensure nil + :demand t + :config + ;; NOTE 2023-03-17: Remember that I am using development versions of + ;; Emacs. Some of my `display-buffer-alist' contents are for Emacs + ;; 29+. + (setq display-buffer-alist + `(;; no window + ("\\`\\*Async Shell Command\\*\\'" + (display-buffer-no-window)) + ("\\`\\*\\(Warnings\\|Compile-Log\\|Org Links\\)\\*\\'" + (display-buffer-no-window) + (allow-no-window . t)) + ;; bottom side window + ("\\*Org \\(Select\\|Note\\)\\*" ; the `org-capture' key selection and `org-add-log-note' + (display-buffer-in-side-window) + (dedicated . t) + (side . bottom) + (slot . 0) + (window-parameters . ((mode-line-format . none)))) + ;; bottom buffer (NOT side window) + ((or . ((derived-mode . flymake-diagnostics-buffer-mode) + (derived-mode . flymake-project-diagnostics-mode) + (derived-mode . messages-buffer-mode) + (derived-mode . backtrace-mode))) + (display-buffer-reuse-mode-window display-buffer-at-bottom) + (window-height . 0.3) + (dedicated . t) + (preserve-size . (t . t))) + ("\\*Embark Actions\\*" + (display-buffer-reuse-mode-window display-buffer-below-selected) + (window-height . fit-window-to-buffer) + (window-parameters . ((no-other-window . t) + (mode-line-format . none)))) + ("\\*\\(Output\\|Register Preview\\).*" + (display-buffer-reuse-mode-window display-buffer-at-bottom)) + ;; below current window + ("\\(\\*Capture\\*\\|CAPTURE-.*\\)" + (display-buffer-reuse-mode-window display-buffer-below-selected)) + ("\\*\\vc-\\(incoming\\|outgoing\\|git : \\).*" + (display-buffer-reuse-mode-window display-buffer-below-selected) + (window-height . 0.1) + (dedicated . t) + (preserve-size . (t . t))) + ((derived-mode . reb-mode) ; M-x re-builder + (display-buffer-reuse-mode-window display-buffer-below-selected) + (window-height . 4) ; note this is literal lines, not relative + (dedicated . t) + (preserve-size . (t . t))) + ((or . ((derived-mode . occur-mode) + (derived-mode . grep-mode) + (derived-mode . Buffer-menu-mode) + (derived-mode . log-view-mode) + (derived-mode . help-mode) ; See the hooks for `visual-line-mode' + "\\*\\(|Buffer List\\|Occur\\|vc-change-log\\|eldoc.*\\).*" + prot-window-shell-or-term-p + ;; ,world-clock-buffer-name + )) + (prot-window-display-buffer-below-or-pop) + (body-function . prot-window-select-fit-size)) + ("\\*\\(Calendar\\|Bookmark Annotation\\|ert\\).*" + (display-buffer-reuse-mode-window display-buffer-below-selected) + (dedicated . t) + (window-height . fit-window-to-buffer)) + ;; same window + ;; NOTE 2023-02-17: `man' does not fully obey the + ;; `display-buffer-alist'. It works for new frames and for + ;; `display-buffer-below-selected', but otherwise is + ;; unpredictable. See `Man-notify-method'. + ((or . ((derived-mode . Man-mode) + (derived-mode . woman-mode) + "\\*\\(Man\\|woman\\).*")) + (display-buffer-same-window))))) +#+end_src + +#+begin_quote +The following settings are relevant for the ~display-buffer-alist~ we +saw right above. Notice, in particular, the ~split-height-threshold~ +and ~split-width-threshold~ which determine when to split the frame by +height or width. These are relevant for ~prot-window-display-buffer-below-or-pop~ +and the other more basic functions I have defined for this purpose. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el" +(use-package prot-window + :ensure nil + :demand t + :config + (setq window-combination-resize t) + (setq even-window-sizes 'height-only) + (setq window-sides-vertical nil) + (setq switch-to-buffer-in-dedicated-window 'pop) + (setq split-height-threshold 80) + (setq split-width-threshold 125) + (setq window-min-height 3) + (setq window-min-width 30)) +#+end_src + +** The =unravel-window.el= section about ~beframe~ +:PROPERTIES: +:CUSTOM_ID: h:77e4f174-0c86-460d-8a54-47545f922ae9 +:END: + +[ Also see: [[#h:7dcbcadf-8af6-487d-b864-e4ce56d69530][The =unravel-git.el= section about =project.el=]]. ] + +#+begin_quote +My ~beframe~ package enables a frame-oriented Emacs workflow where +each frame has access to the list of buffers visited therein. In the +interest of brevity, we call buffers that belong to frames "beframed". +Check the video demo I did and note that I consider this one of the +best changes I ever did to boost my productivity: +<https://protesilaos.com/codelog/2023-02-28-emacs-beframe-demo/>. +#+end_quote + ++ Package name (GNU ELPA): ~beframe~ ++ Official manual: <https://protesilaos.com/emacs/beframe> ++ Change log: <https://protesilaos.com/emacs/beframe-changelog> ++ Git repositories: + - GitHub: <https://github.com/protesilaos/beframe> + - GitLab: <https://gitlab.com/protesilaos/beframe> ++ Backronym: Buffers Encapsulated in Frames Realise Advanced + Management of Emacs. + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el" + ;;; Frame-isolated buffers + ;; Another package of mine. Read the manual: + ;; <https://protesilaos.com/emacs/beframe>. + (use-package beframe + :ensure t + :hook (after-init . beframe-mode) + :bind + ("C-x f" . other-frame-prefix) + ("C-c b" . beframe-prefix-map) + ;; Replace the generic `buffer-menu'. With a prefix argument, this + ;; commands prompts for a frame. Call the `buffer-menu' via M-x if + ;; you absolutely need the global list of buffers. + ("C-x C-b" . beframe-buffer-menu) + ("C-x B" . select-frame-by-name) + :config + (setq beframe-functions-in-frames '(project-prompt-project-dir))) +#+end_src + +** The =unravel-window.el= configuration of ~undelete-frame-mode~ and ~winner-mode~ +:PROPERTIES: +:CUSTOM_ID: h:2df15080-77f9-45f8-a3b5-1adddc70a512 +:END: + +#+begin_quote +Since I am using my ~beframe~ package to isolate buffers per frame +([[#h:77e4f174-0c86-460d-8a54-47545f922ae9][The =unravel-window.el= section about ~beframe~]]), I appreciate the +feature of Emacs 29 to undo the deletion of frames. Note the key +binding I use for this purpose. It overrides one of the alternatives +for the standard ~undo~ command, though I personally only ever use +=C-/=: everything else is free to use as I see fit. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el" +;;; Frame history (undelete-frame-mode) +(use-package frame + :ensure nil + :bind ("C-x u" . undelete-frame) ; I use only C-/ for `undo' + :hook (after-init . undelete-frame-mode)) +#+end_src + +#+begin_quote +The ~winner-mode~ is basically the same idea as ~undelete-frame-mode~ +but for window layouts. Or maybe I should phrase this the other way +round, given that ~winner~ is the older package. But the point is that +we can quickly go back to an earlier arrangement of windows in a +frame. +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el" +;;; Window history (winner-mode) +(use-package winner + :ensure nil + :hook (after-init . winner-mode) + :bind + (("C-x <right>" . winner-redo) + ("C-x <left>" . winner-undo))) +#+end_src + +** The =unravel-window.el= use of contextual header line (~breadcrumb~) +:PROPERTIES: +:CUSTOM_ID: h:29ced61b-4b5a-4f63-af00-fe311468d1cd +:END: + +#+begin_quote +The ~breadcrumb~ package by João Távora lets us display contextual +information about the current heading or code definition in the header +line. The header line is displayed above the contents of each buffer +in the given window. When we are editing an Org file, for example, we +see the path to the file, followed by a reference to the tree that +leads to the current heading. Same idea for programming modes. Neat! +#+end_quote + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el" +;;; Header line context of symbol/heading (breadcrumb.el) +(use-package breadcrumb + :ensure t + :functions (prot/breadcrumb-local-mode) + :hook ((text-mode prog-mode) . prot/breadcrumb-local-mode) + :config + (setq breadcrumb-project-max-length 0.5) + (setq breadcrumb-project-crumb-separator "/") + (setq breadcrumb-imenu-max-length 1.0) + (setq breadcrumb-imenu-crumb-separator " > ") + + (defun prot/breadcrumb-local-mode () + "Enable `breadcrumb-local-mode' if the buffer is visiting a file." + (when buffer-file-name + (breadcrumb-local-mode 1)))) +#+end_src + +** Finally, we provide the =unravel-window.el= module +:PROPERTIES: +:CUSTOM_ID: h:2124c200-734d-49c4-aeb1-513caaf957ae +:END: + +#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el" +(provide 'unravel-window) +#+end_src + * The =unravel-git.el= module :PROPERTIES: :CUSTOM_ID: h:65e3eff5-0bff-4e1f-b6c5-0d3aa1a0d232 @@ -2292,6 +3222,8 @@ Finally, we ~provide~ the module. :CUSTOM_ID: h:d799c3c0-bd6a-40bb-bd1a-ba4ea5367840 :END: +Watch these talks that I've given about Org: + Watch these talks by Prot: - [[https://protesilaos.com/codelog/2023-12-18-emacs-org-advanced-literate-conf/][Advanced literate configuration with Org]] (2023-12-18) @@ -2299,6 +3231,7 @@ Watch these talks by Prot: - [[https://protesilaos.com/codelog/2021-12-09-emacs-org-block-agenda/][Demo of my custom Org block agenda]] (2021-12-09) - [[https://protesilaos.com/codelog/2020-02-04-emacs-org-capture-intro/][Primer on "org-capture"]] (2020-02-04) + #+begin_quote At its core, Org is a plain text markup language. By "markup language", we refer to the use of common characters to apply styling, @@ -3880,6 +4813,436 @@ Prot is the developer of this package. #+end_src * Custom libraries +** The =prot-common.el= library +:PROPERTIES: +:CUSTOM_ID: h:3fccfadf-22e9-457f-b9fd-ed1b48600d23 +:END: + +#+begin_src emacs-lisp :tangle "custom-lisp/prot-common.el" :mkdirp yes + ;;; prot-common.el --- Common functions for my dotemacs -*- lexical-binding: t -*- + + ;; Copyright (C) 2020-2024 Protesilaos Stavrou + + ;; Author: Protesilaos Stavrou <info@protesilaos.com> + ;; URL: https://protesilaos.com/emacs/dotemacs + ;; Version: 0.1.0 + ;; Package-Requires: ((emacs "30.1")) + + ;; This file is NOT part of GNU Emacs. + + ;; This program is free software; you can redistribute it and/or modify + ;; it under the terms of the GNU General Public License as published by + ;; the Free Software Foundation, either version 3 of the License, or (at + ;; your option) any later version. + ;; + ;; This program is distributed in the hope that it will be useful, + ;; but WITHOUT ANY WARRANTY; without even the implied warranty of + ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ;; GNU General Public License for more details. + ;; + ;; You should have received a copy of the GNU General Public License + ;; along with this program. If not, see <https://www.gnu.org/licenses/>. + + ;;; Commentary: + ;; + ;; Common functions for my Emacs: <https://protesilaos.com/emacs/dotemacs/>. + ;; + ;; Remember that every piece of Elisp that I write is for my own + ;; educational and recreational purposes. I am not a programmer and I + ;; do not recommend that you copy any of this if you are not certain of + ;; what it does. + + ;;; Code: + + (eval-when-compile + (require 'subr-x) + (require 'cl-lib)) + + (defgroup prot-common () + "Auxiliary functions for my dotemacs." + :group 'editing) + + ;;;###autoload + (defun prot-common-number-even-p (n) + "Test if N is an even number." + (if (numberp n) + (= (% n 2) 0) + (error "%s is not a number" n))) + + ;;;###autoload + (defun prot-common-number-integer-p (n) + "Test if N is an integer." + (if (integerp n) + n + (error "%s is not an integer" n))) + + ;;;###autoload + (defun prot-common-number-integer-positive-p (n) + "Test if N is a positive integer." + (if (prot-common-number-integer-p n) + (> n 0) + (error "%s is not a positive integer" n))) + + ;; Thanks to Gabriel for providing a cleaner version of + ;; `prot-common-number-negative': <https://github.com/gabriel376>. + ;;;###autoload + (defun prot-common-number-negative (n) + "Make N negative." + (if (and (numberp n) (> n 0)) + (* -1 n) + (error "%s is not a valid positive number" n))) + + ;;;###autoload + (defun prot-common-reverse-percentage (number percent change-p) + "Determine the original value of NUMBER given PERCENT. + + CHANGE-P should specify the increase or decrease. For simplicity, + nil means decrease while non-nil stands for an increase. + + NUMBER must satisfy `numberp', while PERCENT must be `natnump'." + (unless (numberp number) + (user-error "NUMBER must satisfy numberp")) + (unless (natnump percent) + (user-error "PERCENT must satisfy natnump")) + (let* ((pc (/ (float percent) 100)) + (pc-change (if change-p (+ 1 pc) pc)) + (n (if change-p pc-change (float (- 1 pc-change))))) + ;; FIXME 2021-12-21: If float, round to 4 decimal points. + (/ number n))) + + ;;;###autoload + (defun prot-common-percentage-change (n-original n-final) + "Find percentage change between N-ORIGINAL and N-FINAL numbers. + + When the percentage is not an integer, it is rounded to 4 + floating points: 16.666666666666664 => 16.667." + (unless (numberp n-original) + (user-error "N-ORIGINAL must satisfy numberp")) + (unless (numberp n-final) + (user-error "N-FINAL must satisfy numberp")) + (let* ((difference (float (abs (- n-original n-final)))) + (n (* (/ difference n-original) 100)) + (round (floor n))) + ;; FIXME 2021-12-21: Any way to avoid the `string-to-number'? + (if (> n round) (string-to-number (format "%0.4f" n)) round))) + + ;; REVIEW 2023-04-07 07:43 +0300: I just wrote the conversions from + ;; seconds. Hopefully they are correct, but I need to double check. + (defun prot-common-seconds-to-minutes (seconds) + "Convert a number representing SECONDS to MM:SS notation." + (let ((minutes (/ seconds 60)) + (seconds (% seconds 60))) + (format "%.2d:%.2d" minutes seconds))) + + (defun prot-common-seconds-to-hours (seconds) + "Convert a number representing SECONDS to HH:MM:SS notation." + (let* ((hours (/ seconds 3600)) + (minutes (/ (% seconds 3600) 60)) + (seconds (% seconds 60))) + (format "%.2d:%.2d:%.2d" hours minutes seconds))) + + ;;;###autoload + (defun prot-common-seconds-to-minutes-or-hours (seconds) + "Convert SECONDS to either minutes or hours, depending on the value." + (if (> seconds 3599) + (prot-common-seconds-to-hours seconds) + (prot-common-seconds-to-minutes seconds))) + + ;;;###autoload + (defun prot-common-rotate-list-of-symbol (symbol) + "Rotate list value of SYMBOL by moving its car to the end. + Return the first element before performing the rotation. + + This means that if `sample-list' has an initial value of `(one + two three)', this function will first return `one' and update the + value of `sample-list' to `(two three one)'. Subsequent calls + will continue rotating accordingly." + (unless (symbolp symbol) + (user-error "%s is not a symbol" symbol)) + (when-let* ((value (symbol-value symbol)) + (list (and (listp value) value)) + (first (car list))) + (set symbol (append (cdr list) (list first))) + first)) + + ;;;###autoload + (defun prot-common-empty-buffer-p () + "Test whether the buffer is empty." + (or (= (point-min) (point-max)) + (save-excursion + (goto-char (point-min)) + (while (and (looking-at "^\\([a-zA-Z]+: ?\\)?$") + (zerop (forward-line 1)))) + (eobp)))) + + ;;;###autoload + (defun prot-common-minor-modes-active () + "Return list of active minor modes for the current buffer." + (let ((active-modes)) + (mapc (lambda (m) + (when (and (boundp m) (symbol-value m)) + (push m active-modes))) + minor-mode-list) + active-modes)) + + ;;;###autoload + (defun prot-common-truncate-lines-silently () + "Toggle line truncation without printing messages." + (let ((inhibit-message t)) + (toggle-truncate-lines t))) + + ;; NOTE 2023-08-12: I tried the `clear-message-function', but it did + ;; not work. What I need is very simple and this gets the job done. + ;;;###autoload + (defun prot-common-clear-minibuffer-message (&rest _) + "Print an empty message to clear the echo area. + Use this as advice :after a noisy function." + (message "")) + + ;;;###autoload + (defun prot-common-disable-hl-line () + "Disable Hl-Line-Mode (for hooks)." + (hl-line-mode -1)) + + ;;;###autoload + (defun prot-common-window-bounds () + "Return start and end points in the window as a cons cell." + (cons (window-start) (window-end))) + + ;;;###autoload + (defun prot-common-page-p () + "Return non-nil if there is a `page-delimiter' in the buffer." + (or (save-excursion (re-search-forward page-delimiter nil t)) + (save-excursion (re-search-backward page-delimiter nil t)))) + + ;;;###autoload + (defun prot-common-window-small-p () + "Return non-nil if window is small. + Check if the `window-width' or `window-height' is less than + `split-width-threshold' and `split-height-threshold', + respectively." + (or (and (numberp split-width-threshold) + (< (window-total-width) split-width-threshold)) + (and (numberp split-height-threshold) + (> (window-total-height) split-height-threshold)))) + + (defun prot-common-window-narrow-p () + "Return non-nil if window is narrow. + Check if the `window-width' is less than `split-width-threshold'." + (and (numberp split-width-threshold) + (< (window-total-width) split-width-threshold))) + + ;;;###autoload + (defun prot-common-three-or-more-windows-p (&optional frame) + "Return non-nil if three or more windows occupy FRAME. + If FRAME is non-nil, inspect the current frame." + (>= (length (window-list frame :no-minibuffer)) 3)) + + ;;;###autoload + (defun prot-common-read-data (file) + "Read Elisp data from FILE." + (with-temp-buffer + (insert-file-contents file) + (read (current-buffer)))) + + ;;;###autoload + (defun prot-common-completion-category () + "Return completion category." + (when-let* ((window (active-minibuffer-window))) + (with-current-buffer (window-buffer window) + (completion-metadata-get + (completion-metadata (buffer-substring-no-properties + (minibuffer-prompt-end) + (max (minibuffer-prompt-end) (point))) + minibuffer-completion-table + minibuffer-completion-predicate) + 'category)))) + + ;; Thanks to Omar Antolín Camarena for providing this snippet! + ;;;###autoload + (defun prot-common-completion-table (category candidates) + "Pass appropriate metadata CATEGORY to completion CANDIDATES. + + This is intended for bespoke functions that need to pass + completion metadata that can then be parsed by other + tools (e.g. `embark')." + (lambda (string pred action) + (if (eq action 'metadata) + `(metadata (category . ,category)) + (complete-with-action action candidates string pred)))) + + ;;;###autoload + (defun prot-common-completion-table-no-sort (category candidates) + "Pass appropriate metadata CATEGORY to completion CANDIDATES. + Like `prot-common-completion-table' but also disable sorting." + (lambda (string pred action) + (if (eq action 'metadata) + `(metadata (category . ,category) + (display-sort-function . ,#'identity)) + (complete-with-action action candidates string pred)))) + + ;; Thanks to Igor Lima for the `prot-common-crm-exclude-selected-p': + ;; <https://github.com/0x462e41>. + ;; This is used as a filter predicate in the relevant prompts. + (defvar crm-separator) + + ;;;###autoload + (defun prot-common-crm-exclude-selected-p (input) + "Filter out INPUT from `completing-read-multiple'. + Hide non-destructively the selected entries from the completion + table, thus avoiding the risk of inputting the same match twice. + + To be used as the PREDICATE of `completing-read-multiple'." + (if-let* ((pos (string-match-p crm-separator input)) + (rev-input (reverse input)) + (element (reverse + (substring rev-input 0 + (string-match-p crm-separator rev-input)))) + (flag t)) + (progn + (while pos + (if (string= (substring input 0 pos) element) + (setq pos nil) + (setq input (substring input (1+ pos)) + pos (string-match-p crm-separator input) + flag (when pos t)))) + (not flag)) + t)) + + ;; The `prot-common-line-regexp-p' and `prot-common--line-regexp-alist' + ;; are contributed by Gabriel: <https://github.com/gabriel376>. They + ;; provide a more elegant approach to using a macro, as shown further + ;; below. + (defvar prot-common--line-regexp-alist + '((empty . "[\s\t]*$") + (indent . "^[\s\t]+") + (non-empty . "^.+$") + (list . "^\\([\s\t#*+]+\\|[0-9]+[^\s]?[).]+\\)") + (heading . "^[=-]+")) + "Alist of regexp types used by `prot-common-line-regexp-p'.") + + (defun prot-common-line-regexp-p (type &optional n) + "Test for TYPE on line. + TYPE is the car of a cons cell in + `prot-common--line-regexp-alist'. It matches a regular + expression. + + With optional N, search in the Nth line from point." + (save-excursion + (goto-char (line-beginning-position)) + (and (not (bobp)) + (or (beginning-of-line n) t) + (save-match-data + (looking-at + (alist-get type prot-common--line-regexp-alist)))))) + + ;; The `prot-common-shell-command-with-exit-code-and-output' function is + ;; courtesy of Harold Carr, who also sent a patch that improved + ;; `prot-eww-download-html' (from the `prot-eww.el' library). + ;; + ;; More about Harold: <http://haroldcarr.com/about/>. + (defun prot-common-shell-command-with-exit-code-and-output (command &rest args) + "Run COMMAND with ARGS. + Return the exit code and output in a list." + (with-temp-buffer + (list (apply 'call-process command nil (current-buffer) nil args) + (buffer-string)))) + + (defvar prot-common-url-regexp + (concat + "~?\\<\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]*\\)" + "[.@]" + "\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]+\\)\\>/?") + "Regular expression to match (most?) URLs or email addresses.") + + (autoload 'auth-source-search "auth-source") + + ;;;###autoload + (defun prot-common-auth-get-field (host prop) + "Find PROP in `auth-sources' for HOST entry." + (when-let* ((source (auth-source-search :host host))) + (if (eq prop :secret) + (funcall (plist-get (car source) prop)) + (plist-get (flatten-list source) prop)))) + + ;;;###autoload + (defun prot-common-parse-file-as-list (file) + "Return the contents of FILE as a list of strings. + Strings are split at newline characters and are then trimmed for + negative space. + + Use this function to provide a list of candidates for + completion (per `completing-read')." + (split-string + (with-temp-buffer + (insert-file-contents file) + (buffer-substring-no-properties (point-min) (point-max))) + "\n" :omit-nulls "[\s\f\t\n\r\v]+")) + + (defun prot-common-ignore (&rest _) + "Use this as override advice to make a function do nothing." + nil) + + ;; NOTE 2023-06-02: The `prot-common-wcag-formula' and + ;; `prot-common-contrast' are taken verbatim from my `modus-themes' + ;; and renamed to have the prefix `prot-common-' instead of + ;; `modus-themes-'. This is all my code, of course, but I do it this + ;; way to ensure that this file is self-contained in case someone + ;; copies it. + + ;; This is the WCAG formula: <https://www.w3.org/TR/WCAG20-TECHS/G18.html>. + (defun prot-common-wcag-formula (hex) + "Get WCAG value of color value HEX. + The value is defined in hexadecimal RGB notation, such #123456." + (cl-loop for k in '(0.2126 0.7152 0.0722) + for x in (color-name-to-rgb hex) + sum (* k (if (<= x 0.03928) + (/ x 12.92) + (expt (/ (+ x 0.055) 1.055) 2.4))))) + + ;;;###autoload + (defun prot-common-contrast (c1 c2) + "Measure WCAG contrast ratio between C1 and C2. + C1 and C2 are color values written in hexadecimal RGB." + (let ((ct (/ (+ (prot-common-wcag-formula c1) 0.05) + (+ (prot-common-wcag-formula c2) 0.05)))) + (max ct (/ ct)))) + + ;;;; EXPERIMENTAL macros (not meant to be used anywhere) + + ;; TODO 2023-09-30: Try the same with `cl-defmacro' and &key + (defmacro prot-common-if (condition &rest consequences) + "Separate the CONSEQUENCES of CONDITION semantically. + Like `if', `when', `unless' but done by using `:then' and `:else' + keywords. The forms under each keyword of `:then' and `:else' + belong to the given subset of CONSEQUENCES. + + - The absence of `:else' means: (if CONDITION (progn CONSEQUENCES)). + - The absence of `:then' means: (if CONDITION nil CONSEQUENCES). + - Otherwise: (if CONDITION (progn then-CONSEQUENCES) else-CONSEQUENCES)." + (declare (indent 1)) + (let (then-consequences else-consequences last-kw) + (dolist (elt consequences) + (let ((is-keyword (keywordp elt))) + (cond + ((and (not is-keyword) (eq last-kw :then)) + (push elt then-consequences)) + ((and (not is-keyword) (eq last-kw :else)) + (push elt else-consequences)) + ((and is-keyword (eq elt :then)) + (setq last-kw :then)) + ((and is-keyword (eq elt :else)) + (setq last-kw :else))))) + `(if ,condition + ,(if then-consequences + `(progn ,@(nreverse then-consequences)) + nil) + ,@(nreverse else-consequences)))) + + (provide 'prot-common) + ;;; prot-common.el ends here +#+end_src + ** The =prot-embark.el= library :PROPERTIES: :CUSTOM_ID: h:fb034be5-c316-4c4f-a46f-cebcab332a47 @@ -4026,3 +5389,247 @@ Prot is the developer of this package. ;;; prot-embark.el ends here #+end_src +** The =prot-window.el= library +:PROPERTIES: +:CUSTOM_ID: h:35b8a0a5-c447-4301-a404-bc274596238d +:END: + +#+begin_src emacs-lisp :tangle "custom-lisp/prot-window.el" :mkdirp yes +;;; prot-window.el --- Display-buffer and window-related extensions for my dotemacs -*- lexical-binding: t -*- + +;; Copyright (C) 2023-2024 Protesilaos Stavrou + +;; Author: Protesilaos Stavrou <info@protesilaos.com> +;; URL: https://protesilaos.com/emacs/dotemacs +;; Version: 0.1.0 +;; Package-Requires: ((emacs "30.1")) + +;; This file is NOT part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; This covers my window and display-buffer extensions, for use in my +;; Emacs setup: https://protesilaos.com/emacs/dotemacs. +;; +;; Remember that every piece of Elisp that I write is for my own +;; educational and recreational purposes. I am not a programmer and I +;; do not recommend that you copy any of this if you are not certain of +;; what it does. + +;;; Code: + +(require 'prot-common) + +(defvar prot-window-window-sizes + '( :max-height (lambda () (floor (frame-height) 3)) + :min-height 10 + :max-width (lambda () (floor (frame-width) 4)) + :min-width 20) + "Property list of maximum and minimum window sizes. +The property keys are `:max-height', `:min-height', `:max-width', +and `:min-width'. They all accept a value of either a +number (integer or floating point) or a function.") + +(defun prot-window--get-window-size (key) + "Extract the value of KEY from `prot-window-window-sizes'." + (when-let* ((value (plist-get prot-window-window-sizes key))) + (cond + ((functionp value) + (funcall value)) + ((numberp value) + value) + (t + (error "The value of `%s' is neither a number nor a function" key))))) + +(defun prot-window-select-fit-size (window) + "Select WINDOW and resize it. +The resize pertains to the maximum and minimum values for height +and width, per `prot-window-window-sizes'. + +Use this as the `body-function' in a `display-buffer-alist' entry." + (select-window window) + (fit-window-to-buffer + window + (prot-window--get-window-size :max-height) + (prot-window--get-window-size :min-height) + (prot-window--get-window-size :max-width) + (prot-window--get-window-size :min-width)) + ;; If we did not use `display-buffer-below-selected', then we must + ;; be in a lateral window, which has more space. Then we do not + ;; want to dedicate the window to this buffer, because we will be + ;; running out of space. + (when (or (window-in-direction 'above) (window-in-direction 'below)) + (set-window-dedicated-p window t))) + +(defun prot-window--get-display-buffer-below-or-pop () + "Return list of functions for `prot-window-display-buffer-below-or-pop'." + (list + #'display-buffer-reuse-mode-window + (if (or (prot-common-window-small-p) + (prot-common-three-or-more-windows-p)) + #'display-buffer-below-selected + #'display-buffer-pop-up-window))) + +(defun prot-window-display-buffer-below-or-pop (&rest args) + "Display buffer below current window or pop a new window. +The criterion for choosing to display the buffer below the +current one is a non-nil return value for +`prot-common-window-small-p'. + +Apply ARGS expected by the underlying `display-buffer' functions. + +This as the action function in a `display-buffer-alist' entry." + (let ((functions (prot-window--get-display-buffer-below-or-pop))) + (catch 'success + (dolist (fn functions) + (when (apply fn args) + (throw 'success fn)))))) + +(defun prot-window-shell-or-term-p (buffer &rest _) + "Check if BUFFER is a shell or terminal. +This is a predicate function for `buffer-match-p', intended for +use in `display-buffer-alist'." + (when (string-match-p "\\*.*\\(e?shell\\|v?term\\).*" (buffer-name (get-buffer buffer))) + (with-current-buffer buffer + ;; REVIEW 2022-07-14: Is this robust? + (and (not (derived-mode-p 'message-mode 'text-mode)) + (derived-mode-p 'eshell-mode 'shell-mode 'comint-mode 'fundamental-mode))))) + +(defun prot-window-remove-dedicated (&rest _) + "Remove dedicated window parameter. +Use this as :after advice to `delete-other-windows' and +`delete-window'." + (when (one-window-p :no-mini) + (set-window-dedicated-p nil nil))) + +(mapc + (lambda (fn) + (advice-add fn :after #'prot-window-remove-dedicated)) + '(delete-other-windows delete-window)) + +(defmacro prot-window-define-full-frame (name &rest args) + "Define command to call ARGS in new frame with `display-buffer-full-frame' bound. +Name the function prot-window- followed by NAME. If ARGS is nil, +call NAME as a function." + (declare (indent 1)) + `(defun ,(intern (format "prot-window-%s" name)) () + ,(format "Call `prot-window-%s' in accordance with `prot-window-define-full-frame'." name) + (interactive) + (let ((display-buffer-alist '((".*" (display-buffer-full-frame))))) + (with-selected-frame (make-frame) + ,(if args + `(progn ,@args) + `(funcall ',name)) + (modify-frame-parameters nil '((buffer-list . nil))))))) + +(defun prot-window--get-shell-buffers () + "Return list of `shell' buffers." + (seq-filter + (lambda (buffer) + (with-current-buffer buffer + (derived-mode-p 'shell-mode))) + (buffer-list))) + +(defun prot-window--get-new-shell-buffer () + "Return buffer name for `shell' buffers." + (if-let* ((buffers (prot-window--get-shell-buffers)) + (buffers-length (length buffers)) + ((>= buffers-length 1))) + (format "*shell*<%s>" (1+ buffers-length)) + "*shell*")) + +;;;###autoload (autoload 'prot-window-shell "prot-window") +(prot-window-define-full-frame shell + (let ((name (prot-window--get-new-shell-buffer))) + (shell name) + (set-frame-name name) + (when-let* ((buffer (get-buffer name))) + (with-current-buffer buffer + (add-hook + 'delete-frame-functions + (lambda (_) + ;; FIXME 2023-09-09: Works for multiple frames (per + ;; `make-frame-command'), but not if the buffer is in two + ;; windows in the same frame. + (unless (> (safe-length (get-buffer-window-list buffer nil t)) 1) + (let ((kill-buffer-query-functions nil)) + (kill-buffer buffer)))) + nil + :local))))) + +;;;###autoload (autoload 'prot-window-coach "prot-window") +(prot-window-define-full-frame coach + (let ((buffer (get-buffer-create "*scratch for coach*"))) + (with-current-buffer buffer + (funcall initial-major-mode)) + (display-buffer buffer) + (set-frame-name "Coach"))) + +;; REVIEW 2023-06-25: Does this merit a user option? I don't think I +;; will ever set it to the left. It feels awkward there. +(defun prot-window-scroll-bar-placement () + "Control the placement of scroll bars." + (when scroll-bar-mode + (setq default-frame-scroll-bars 'right) + (set-scroll-bar-mode 'right))) + +(add-hook 'scroll-bar-mode-hook #'prot-window-scroll-bar-placement) + +(defun prot-window-no-minibuffer-scroll-bar (frame) + "Remove the minibuffer scroll bars from FRAME." + (set-window-scroll-bars (minibuffer-window frame) nil nil nil nil :persistent)) + +(add-hook 'after-make-frame-functions 'prot-window-no-minibuffer-scroll-bar) + +;;;; Run commands in a popup frame (via emacsclient) + +(defun prot-window-delete-popup-frame (&rest _) + "Kill selected selected frame if it has parameter `prot-window-popup-frame'. +Use this function via a hook." + (when (frame-parameter nil 'prot-window-popup-frame) + (delete-frame))) + +(defmacro prot-window-define-with-popup-frame (command) + "Define function which calls COMMAND in a new frame. +Make the new frame have the `prot-window-popup-frame' parameter." + `(defun ,(intern (format "prot-window-popup-%s" command)) () + ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter. +Also see `prot-window-delete-popup-frame'." command) + (interactive) + (let ((frame (make-frame '((prot-window-popup-frame . t))))) + (select-frame frame) + (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame") + (condition-case nil + (call-interactively ',command) + ((quit error user-error) + (delete-frame frame)))))) + +(declare-function org-capture "org-capture" (&optional goto keys)) +(defvar org-capture-after-finalize-hook) + +;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window") +(prot-window-define-with-popup-frame org-capture) + +(declare-function tmr "tmr" (time &optional description acknowledgep)) +(defvar tmr-timer-created-functions) + +;;;###autoload (autoload 'prot-window-popup-tmr "prot-window") +(prot-window-define-with-popup-frame tmr) + +(provide 'prot-window) +;;; prot-window.el ends here +#+end_src + diff --git a/unravel-modules/unravel-dired.el b/unravel-modules/unravel-dired.el new file mode 100644 index 0000000..2cd18f6 --- /dev/null +++ b/unravel-modules/unravel-dired.el @@ -0,0 +1,98 @@ +;;; Dired file manager and prot-dired.el extras +(use-package dired + :ensure nil + :commands (dired) + :config + (setq dired-recursive-copies 'always) + (setq dired-recursive-deletes 'always) + (setq delete-by-moving-to-trash t)) + +(use-package dired + :ensure nil + :commands (dired) + :config + (setq dired-listing-switches + "-AGFhlv --group-directories-first --time-style=long-iso")) + +(use-package dired + :ensure nil + :commands (dired) + :config + (setq dired-dwim-target t)) + +(use-package dired + :ensure nil + :commands (dired) + :config + (setq dired-auto-revert-buffer #'dired-directory-changed-p) ; also see `dired-do-revert-buffer' + (setq dired-make-directory-clickable t) ; Emacs 29.1 + (setq dired-free-space nil) ; Emacs 29.1 + (setq dired-mouse-drag-files t) ; Emacs 29.1 + + (add-hook 'dired-mode-hook #'dired-hide-details-mode) + (add-hook 'dired-mode-hook #'hl-line-mode) + + ;; In Emacs 29 there is a binding for `repeat-mode' which lets you + ;; repeat C-x C-j just by following it up with j. For me, this is a + ;; problem as j calls `dired-goto-file', which I often use. + (define-key dired-jump-map (kbd "j") nil)) + +(use-package dired-aux + :ensure nil + :after dired + :bind + ( :map dired-mode-map + ("C-+" . dired-create-empty-file) + ("M-s f" . nil) + ("C-<return>" . dired-do-open) ; Emacs 30 + ("C-x v v" . dired-vc-next-action)) ; Emacs 28 + :config + (setq dired-isearch-filenames 'dwim) + (setq dired-create-destination-dirs 'ask) ; Emacs 27 + (setq dired-vc-rename-file t) ; Emacs 27 + (setq dired-do-revert-buffer (lambda (dir) (not (file-remote-p dir)))) ; Emacs 28 + (setq dired-create-destination-dirs-on-trailing-dirsep t)) ; Emacs 29 + +(use-package dired-x + :ensure nil + :after dired + :bind + ( :map dired-mode-map + ("I" . dired-info)) + :config + (setq dired-clean-up-buffers-too t) + (setq dired-clean-confirm-killing-deleted-buffers t) + (setq dired-x-hands-off-my-keys t) ; easier to show the keys I use + (setq dired-bind-man nil) + (setq dired-bind-info nil)) + +(use-package dired-subtree + :ensure t + :after dired + :bind + ( :map dired-mode-map + ("<tab>" . dired-subtree-toggle) + ("TAB" . dired-subtree-toggle) + ("<backtab>" . dired-subtree-remove) + ("S-TAB" . dired-subtree-remove)) + :config + (setq dired-subtree-use-backgrounds nil)) + +(use-package wdired + :ensure nil + :commands (wdired-change-to-wdired-mode) + :config + (setq wdired-allow-to-change-permissions t) + (setq wdired-create-parent-directories t)) + +;;; dired-like mode for the trash (trashed.el) +(use-package trashed + :ensure t + :commands (trashed) + :config + (setq trashed-action-confirmer 'y-or-n-p) + (setq trashed-use-header-line t) + (setq trashed-sort-key '("Date deleted" . t)) + (setq trashed-date-format "%Y-%m-%d %H:%M:%S")) + +(provide 'unravel-dired) diff --git a/unravel-modules/unravel-search.el b/unravel-modules/unravel-search.el new file mode 100644 index 0000000..b4186e4 --- /dev/null +++ b/unravel-modules/unravel-search.el @@ -0,0 +1,93 @@ +;;; Isearch, occur, grep +(use-package isearch + :ensure nil + :demand t + :config + (setq search-whitespace-regexp ".*?" ; one `setq' here to make it obvious they are a bundle + isearch-lax-whitespace t + isearch-regexp-lax-whitespace nil)) + +(use-package isearch + :ensure nil + :demand t + :config + (setq search-highlight t) + (setq isearch-lazy-highlight t) + (setq lazy-highlight-initial-delay 0.5) + (setq lazy-highlight-no-delay-length 4)) + +(use-package isearch + :ensure nil + :demand t + :config + (setq isearch-lazy-count t) + (setq lazy-count-prefix-format "(%s/%s) ") + (setq lazy-count-suffix-format nil)) + +(use-package isearch + :ensure nil + :demand t + :config + (setq list-matching-lines-jump-to-current-line nil) ; do not jump to current line in `*occur*' buffers + (add-hook 'occur-mode-hook #'hl-line-mode)) + +(use-package isearch + :ensure nil + :demand t + :bind + ( :map minibuffer-local-isearch-map + ("M-/" . isearch-complete-edit) + :map occur-mode-map + ("t" . toggle-truncate-lines) + :map isearch-mode-map + ("C-g" . isearch-cancel) ; instead of `isearch-abort' + ("M-/" . isearch-complete))) + +;;; grep and xref +(use-package re-builder + :ensure nil + :commands (re-builder regexp-builder) + :config + (setq reb-re-syntax 'read)) + +(use-package xref + :ensure nil + :commands (xref-find-definitions xref-go-back) + :config + ;; All those have been changed for Emacs 28 + (setq xref-show-definitions-function #'xref-show-definitions-completing-read) ; for M-. + (setq xref-show-xrefs-function #'xref-show-definitions-buffer) ; for grep and the like + (setq xref-file-name-display 'project-relative)) + +(use-package grep + :ensure nil + :commands (grep lgrep rgrep) + :config + (setq grep-save-buffers nil) + (setq grep-use-headings t) ; Emacs 30 + + (let ((executable (or (executable-find "rg") "grep")) + (rgp (string-match-p "rg" grep-program))) + (setq grep-program executable) + (setq grep-template + (if rgp + "/usr/bin/rg -nH --null -e <R> <F>" + "/usr/bin/grep <X> <C> -nH --null -e <R> <F>")) + (setq xref-search-program (if rgp 'ripgrep 'grep)))) + +;;; wgrep (writable grep) +;; See the `grep-edit-mode' for the new built-in feature. +(unless (>= emacs-major-version 31) + (use-package wgrep + :ensure t + :after grep + :bind + ( :map grep-mode-map + ("e" . wgrep-change-to-wgrep-mode) + ("C-x C-q" . wgrep-change-to-wgrep-mode) + ("C-c C-c" . wgrep-finish-edit)) + :config + (setq wgrep-auto-save-buffer t) + (setq wgrep-change-readonly-file t))) + +(provide 'unravel-search) diff --git a/unravel-modules/unravel-window.el b/unravel-modules/unravel-window.el new file mode 100644 index 0000000..a9acaf1 --- /dev/null +++ b/unravel-modules/unravel-window.el @@ -0,0 +1,146 @@ +;;; General window and buffer configurations +(use-package uniquify + :ensure nil + :config +;;;; `uniquify' (unique names for buffers) + (setq uniquify-buffer-name-style 'forward) + (setq uniquify-strip-common-suffix t) + (setq uniquify-after-kill-buffer-p t)) + +;;;; `window', `display-buffer-alist', and related +(use-package prot-window + :ensure nil + :demand t + :config + ;; NOTE 2023-03-17: Remember that I am using development versions of + ;; Emacs. Some of my `display-buffer-alist' contents are for Emacs + ;; 29+. + (setq display-buffer-alist + `(;; no window + ("\\`\\*Async Shell Command\\*\\'" + (display-buffer-no-window)) + ("\\`\\*\\(Warnings\\|Compile-Log\\|Org Links\\)\\*\\'" + (display-buffer-no-window) + (allow-no-window . t)) + ;; bottom side window + ("\\*Org \\(Select\\|Note\\)\\*" ; the `org-capture' key selection and `org-add-log-note' + (display-buffer-in-side-window) + (dedicated . t) + (side . bottom) + (slot . 0) + (window-parameters . ((mode-line-format . none)))) + ;; bottom buffer (NOT side window) + ((or . ((derived-mode . flymake-diagnostics-buffer-mode) + (derived-mode . flymake-project-diagnostics-mode) + (derived-mode . messages-buffer-mode) + (derived-mode . backtrace-mode))) + (display-buffer-reuse-mode-window display-buffer-at-bottom) + (window-height . 0.3) + (dedicated . t) + (preserve-size . (t . t))) + ("\\*Embark Actions\\*" + (display-buffer-reuse-mode-window display-buffer-below-selected) + (window-height . fit-window-to-buffer) + (window-parameters . ((no-other-window . t) + (mode-line-format . none)))) + ("\\*\\(Output\\|Register Preview\\).*" + (display-buffer-reuse-mode-window display-buffer-at-bottom)) + ;; below current window + ("\\(\\*Capture\\*\\|CAPTURE-.*\\)" + (display-buffer-reuse-mode-window display-buffer-below-selected)) + ("\\*\\vc-\\(incoming\\|outgoing\\|git : \\).*" + (display-buffer-reuse-mode-window display-buffer-below-selected) + (window-height . 0.1) + (dedicated . t) + (preserve-size . (t . t))) + ((derived-mode . reb-mode) ; M-x re-builder + (display-buffer-reuse-mode-window display-buffer-below-selected) + (window-height . 4) ; note this is literal lines, not relative + (dedicated . t) + (preserve-size . (t . t))) + ((or . ((derived-mode . occur-mode) + (derived-mode . grep-mode) + (derived-mode . Buffer-menu-mode) + (derived-mode . log-view-mode) + (derived-mode . help-mode) ; See the hooks for `visual-line-mode' + "\\*\\(|Buffer List\\|Occur\\|vc-change-log\\|eldoc.*\\).*" + prot-window-shell-or-term-p + ;; ,world-clock-buffer-name + )) + (prot-window-display-buffer-below-or-pop) + (body-function . prot-window-select-fit-size)) + ("\\*\\(Calendar\\|Bookmark Annotation\\|ert\\).*" + (display-buffer-reuse-mode-window display-buffer-below-selected) + (dedicated . t) + (window-height . fit-window-to-buffer)) + ;; same window + ;; NOTE 2023-02-17: `man' does not fully obey the + ;; `display-buffer-alist'. It works for new frames and for + ;; `display-buffer-below-selected', but otherwise is + ;; unpredictable. See `Man-notify-method'. + ((or . ((derived-mode . Man-mode) + (derived-mode . woman-mode) + "\\*\\(Man\\|woman\\).*")) + (display-buffer-same-window))))) + +(use-package prot-window + :ensure nil + :demand t + :config + (setq window-combination-resize t) + (setq even-window-sizes 'height-only) + (setq window-sides-vertical nil) + (setq switch-to-buffer-in-dedicated-window 'pop) + (setq split-height-threshold 80) + (setq split-width-threshold 125) + (setq window-min-height 3) + (setq window-min-width 30)) + +;;; Frame-isolated buffers +;; Another package of mine. Read the manual: +;; <https://protesilaos.com/emacs/beframe>. +(use-package beframe + :ensure t + :hook (after-init . beframe-mode) + :bind + ("C-x f" . other-frame-prefix) + ("C-c b" . beframe-prefix-map) + ;; Replace the generic `buffer-menu'. With a prefix argument, this + ;; commands prompts for a frame. Call the `buffer-menu' via M-x if + ;; you absolutely need the global list of buffers. + ("C-x C-b" . beframe-buffer-menu) + ("C-x B" . select-frame-by-name) + :config + (setq beframe-functions-in-frames '(project-prompt-project-dir))) + +;;; Frame history (undelete-frame-mode) +(use-package frame + :ensure nil + :bind ("C-x u" . undelete-frame) ; I use only C-/ for `undo' + :hook (after-init . undelete-frame-mode)) + +;;; Window history (winner-mode) +(use-package winner + :ensure nil + :hook (after-init . winner-mode) + :bind + (("C-x <right>" . winner-redo) + ("C-x <left>" . winner-undo))) + +;;; Header line context of symbol/heading (breadcrumb.el) +(use-package breadcrumb + :ensure t + :functions (prot/breadcrumb-local-mode) + :hook ((text-mode prog-mode) . prot/breadcrumb-local-mode) + :config + (setq breadcrumb-project-max-length 0.5) + (setq breadcrumb-project-crumb-separator "/") + (setq breadcrumb-imenu-max-length 1.0) + (setq breadcrumb-imenu-crumb-separator " > ") + + (defun prot/breadcrumb-local-mode () + "Enable `breadcrumb-local-mode' if the buffer is visiting a file." + (when buffer-file-name + (breadcrumb-local-mode 1)))) + +(provide 'unravel-window)