notmuch-jump.el (7782B)
1 ;;; notmuch-jump.el --- User-friendly shortcut keys -*- lexical-binding: t -*- 2 ;; 3 ;; Copyright © Austin Clements 4 ;; 5 ;; This file is part of Notmuch. 6 ;; 7 ;; Notmuch is free software: you can redistribute it and/or modify it 8 ;; under the terms of the GNU General Public License as published by 9 ;; the Free Software Foundation, either version 3 of the License, or 10 ;; (at your option) any later version. 11 ;; 12 ;; Notmuch is distributed in the hope that it will be useful, but 13 ;; WITHOUT ANY WARRANTY; without even the implied warranty of 14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 ;; General Public License for more details. 16 ;; 17 ;; You should have received a copy of the GNU General Public License 18 ;; along with Notmuch. If not, see <https://www.gnu.org/licenses/>. 19 ;; 20 ;; Authors: Austin Clements <aclements@csail.mit.edu> 21 ;; David Edmondson <dme@dme.org> 22 23 ;;; Code: 24 25 (require 'notmuch-lib) 26 (require 'notmuch-hello) 27 28 (declare-function notmuch-search "notmuch") 29 (declare-function notmuch-tree "notmuch-tree") 30 (declare-function notmuch-unthreaded "notmuch-tree") 31 32 ;;;###autoload 33 (defun notmuch-jump-search () 34 "Jump to a saved search by shortcut key. 35 36 This prompts for and performs a saved search using the shortcut 37 keys configured in the :key property of `notmuch-saved-searches'. 38 Typically these shortcuts are a single key long, so this is a 39 fast way to jump to a saved search from anywhere in Notmuch." 40 (interactive) 41 ;; Build the action map 42 (let (action-map) 43 (dolist (saved-search notmuch-saved-searches) 44 (let* ((saved-search (notmuch-hello-saved-search-to-plist saved-search)) 45 (key (plist-get saved-search :key))) 46 (when key 47 (let ((name (plist-get saved-search :name)) 48 (query (plist-get saved-search :query)) 49 (oldest-first 50 (cl-case (plist-get saved-search :sort-order) 51 (newest-first nil) 52 (oldest-first t) 53 (otherwise (default-value 'notmuch-search-oldest-first)))) 54 (exclude (cl-case (plist-get saved-search :excluded) 55 (hide t) 56 (show nil) 57 (otherwise notmuch-search-hide-excluded)))) 58 (push (list key name 59 (cond 60 ((eq (plist-get saved-search :search-type) 'tree) 61 (lambda () (notmuch-tree query nil nil nil nil nil nil 62 oldest-first exclude))) 63 ((eq (plist-get saved-search :search-type) 'unthreaded) 64 (lambda () (notmuch-unthreaded query nil nil nil nil 65 oldest-first exclude))) 66 (t 67 (lambda () (notmuch-search query oldest-first exclude))))) 68 action-map))))) 69 (setq action-map (nreverse action-map)) 70 (if action-map 71 (notmuch-jump action-map "Search: ") 72 (error "To use notmuch-jump, %s" 73 "please customize shortcut keys in notmuch-saved-searches.")))) 74 75 (defface notmuch-jump-key 76 '((t :inherit minibuffer-prompt)) 77 "Default face used for keys in `notmuch-jump' and related." 78 :group 'notmuch-faces) 79 80 (defvar notmuch-jump--action nil) 81 82 ;;;###autoload 83 (defun notmuch-jump (action-map prompt) 84 "Interactively prompt for one of the keys in ACTION-MAP. 85 86 Displays a summary of all bindings in ACTION-MAP in the 87 minibuffer, reads a key from the minibuffer, and performs the 88 corresponding action. The prompt can be canceled with C-g or 89 RET. PROMPT must be a string to use for the prompt. PROMPT 90 should include a space at the end. 91 92 ACTION-MAP must be a list of triples of the form 93 (KEY LABEL ACTION) 94 where KEY is a key binding, LABEL is a string label to display in 95 the buffer, and ACTION is a nullary function to call. LABEL may 96 be null, in which case the action will still be bound, but will 97 not appear in the pop-up buffer." 98 (let* ((items (notmuch-jump--format-actions action-map)) 99 ;; Format the table of bindings and the full prompt 100 (table 101 (with-temp-buffer 102 (notmuch-jump--insert-items (window-body-width) items) 103 (buffer-string))) 104 (full-prompt 105 (concat table "\n\n" 106 (propertize prompt 'face 'minibuffer-prompt))) 107 ;; By default, the minibuffer applies the minibuffer face to 108 ;; the entire prompt. However, we want to clearly 109 ;; distinguish bindings (which we put in the prompt face 110 ;; ourselves) from their labels, so disable the minibuffer's 111 ;; own re-face-ing. 112 (minibuffer-prompt-properties 113 (notmuch-plist-delete 114 (copy-sequence minibuffer-prompt-properties) 115 'face)) 116 ;; Build the keymap with our bindings 117 (minibuffer-map (notmuch-jump--make-keymap action-map prompt)) 118 ;; The bindings save the the action in notmuch-jump--action 119 (notmuch-jump--action nil)) 120 ;; Read the action 121 (read-from-minibuffer full-prompt nil minibuffer-map) 122 ;; If we got an action, do it 123 (when notmuch-jump--action 124 (funcall notmuch-jump--action)))) 125 126 (defun notmuch-jump--format-actions (action-map) 127 "Format the actions in ACTION-MAP. 128 129 Returns a list of strings, one for each item with a label in 130 ACTION-MAP. These strings can be inserted into a tabular 131 buffer." 132 ;; Compute the maximum key description width 133 (let ((key-width 1)) 134 (pcase-dolist (`(,key ,_desc) action-map) 135 (setq key-width 136 (max key-width 137 (string-width (format-kbd-macro key))))) 138 ;; Format each action 139 (mapcar (pcase-lambda (`(,key ,desc)) 140 (setq key (format-kbd-macro key)) 141 (concat (propertize key 'face 'notmuch-jump-key) 142 (make-string (- key-width (length key)) ? ) 143 " " desc)) 144 action-map))) 145 146 (defun notmuch-jump--insert-items (width items) 147 "Make a table of ITEMS up to WIDTH wide in the current buffer." 148 (let* ((nitems (length items)) 149 (col-width (+ 3 (apply #'max (mapcar #'string-width items)))) 150 (ncols (if (> (* col-width nitems) width) 151 (max 1 (/ width col-width)) 152 ;; Items fit on one line. Space them out 153 (setq col-width (/ width nitems)) 154 (length items)))) 155 (while items 156 (dotimes (col ncols) 157 (when items 158 (let ((item (pop items))) 159 (insert item) 160 (when (and items (< col (- ncols 1))) 161 (insert (make-string (- col-width (string-width item)) ? )))))) 162 (when items 163 (insert "\n"))))) 164 165 (defvar notmuch-jump-minibuffer-map 166 (let ((map (make-sparse-keymap))) 167 (set-keymap-parent map minibuffer-local-map) 168 ;; Make this like a special-mode keymap, with no self-insert-command 169 (suppress-keymap map) 170 (define-key map (kbd "DEL") 'exit-minibuffer) 171 map) 172 "Base keymap for notmuch-jump's minibuffer keymap.") 173 174 (defun notmuch-jump--make-keymap (action-map prompt) 175 "Translate ACTION-MAP into a minibuffer keymap." 176 (let ((map (make-sparse-keymap))) 177 (set-keymap-parent map notmuch-jump-minibuffer-map) 178 (pcase-dolist (`(,key ,_name ,fn) action-map) 179 (when (= (length key) 1) 180 (define-key map key 181 (lambda () 182 (interactive) 183 (setq notmuch-jump--action fn) 184 (exit-minibuffer))))) 185 ;; By doing this in two passes (and checking if we already have a 186 ;; binding) we avoid problems if the user specifies a binding which 187 ;; is a prefix of another binding. 188 (pcase-dolist (`(,key ,_name ,_fn) action-map) 189 (when (> (length key) 1) 190 (let* ((key (elt key 0)) 191 (keystr (string key)) 192 (new-prompt (concat prompt (format-kbd-macro keystr) " ")) 193 (action-submap nil)) 194 (unless (lookup-key map keystr) 195 (pcase-dolist (`(,k ,n ,f) action-map) 196 (when (= key (elt k 0)) 197 (push (list (substring k 1) n f) action-submap))) 198 ;; We deal with backspace specially 199 (push (list (kbd "DEL") 200 "Backup" 201 (apply-partially #'notmuch-jump action-map prompt)) 202 action-submap) 203 (setq action-submap (nreverse action-submap)) 204 (define-key map keystr 205 (lambda () 206 (interactive) 207 (setq notmuch-jump--action 208 (apply-partially #'notmuch-jump 209 action-submap 210 new-prompt)) 211 (exit-minibuffer))))))) 212 map)) 213 214 (provide 'notmuch-jump) 215 216 ;;; notmuch-jump.el ends here