denote-menu.el (13062B)
1 ;;; denote-menu.el --- View denote files in a tabulated list. -*- lexical-binding: t -*- 2 3 ;; Copyright (C) 2023 Free Software Foundation, Inc. 4 5 ;; Author: Mohamed Suliman <sulimanm@tcd.ie> 6 ;; Version: 1.2.0 7 ;; URL: https://github.com/namilus/denote-menu 8 ;; Package-Requires: ((emacs "28.1") (denote "2.0.0")) 9 10 ;; This file is NOT part of GNU Emacs. 11 12 ;; This program is free software; you can redistribute it and/or modify 13 ;; it under the terms of the GNU General Public License as published by 14 ;; the Free Software Foundation, either version 3 of the License, or 15 ;; (at your option) any later version. 16 ;; 17 ;; This program is distributed in the hope that it will be useful, 18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 ;; GNU General Public License for more details. 21 ;; 22 ;; You should have received a copy of the GNU General Public License 23 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 24 25 ;;; Commentary: 26 27 ;; `denote-menu' is an extension to the elpa package `denote' and provides 28 ;; an interface for viewing your denote files that goes beyond using the 29 ;; standard `dired' emacs command to view your `denote-directory'. Using 30 ;; dired is a fine method for viewing your denote files (among other 31 ;; things), however denote's file naming scheme tends to clutters the 32 ;; buffer with hyphens and underscores. This package aims to declutter your 33 ;; view of your files by making it easy to view the 3 main components of 34 ;; denote files, that is their timestamp, title, and keywords. Derived from 35 ;; the builtin `tabulated-list-mode', the `*Denote*' buffer that is created 36 ;; with the `list-denotes' command is visually similar to that created by 37 ;; commands like `list-packages' and `list-processes', and provides methods 38 ;; to filter the denote files that are shown, as well as exporting to dired 39 ;; with the denote files that are currently shown for them to be operated 40 ;; upon further. 41 42 ;;; Code: 43 44 (require 'tabulated-list) 45 (require 'denote) 46 (require 'dired) 47 (require 'seq) 48 49 (defgroup denote-menu () 50 "View Denote files" 51 :group 'files) 52 53 (defcustom denote-menu-date-column-width 17 54 "Width for the date column." 55 :type 'number 56 :group 'denote-menu) 57 58 (defcustom denote-menu-signature-column-width 10 59 "Width for the date column." 60 :type 'number 61 :group 'denote-menu) 62 63 (defcustom denote-menu-title-column-width 85 64 "Width for the title column." 65 :type 'number 66 :group 'denote-menu) 67 68 (defcustom denote-menu-keywords-column-width 30 69 "Width for the keywords column." 70 :type 'number 71 :group 'denote-menu) 72 73 (defcustom denote-menu-action (lambda (path) (find-file path)) 74 "Function to execute when a denote file button action is 75 invoked. Takes a single argument which is the path of the 76 denote file corresponding to the button." 77 :type 'function 78 :group 'denote-menu) 79 80 (defcustom denote-menu-initial-regex "." 81 "Regex used to initially populate the buffer with matching denote files." 82 :type 'string 83 :group 'denote-menu) 84 85 (defcustom denote-menu-show-file-type t 86 "Whether to show the denote file type." 87 :type 'boolean 88 :group 'denote-menu) 89 90 (defcustom denote-menu-show-file-signature nil 91 "Whether to show the denote file signature." 92 :type 'boolean 93 :group 'denote-menu) 94 95 (defvar denote-menu-current-regex denote-menu-initial-regex 96 "The current regex used to match denote filenames.") 97 98 ;;;###autoload 99 (defun denote-menu-list-notes () 100 "Display list of Denote files in variable `denote-directory'." 101 (interactive) 102 ;; kill any existing *Denote* buffer 103 (let ((denote-menu-buffer-name (format "*Denote %s*" denote-directory))) 104 (when (get-buffer denote-menu-buffer-name) 105 (kill-buffer denote-menu-buffer-name)) 106 (let ((buffer (get-buffer-create denote-menu-buffer-name))) 107 (with-current-buffer buffer 108 (setq buffer-file-coding-system 'utf-8) 109 (setq denote-menu-current-regex denote-menu-initial-regex) 110 (denote-menu-mode)) 111 112 (pop-to-buffer-same-window buffer)))) 113 114 (defalias 'list-denotes 'denote-menu-list-notes 115 "Alias of `denote-menu-list-notes' command.") 116 117 (defun denote-menu-update-entries () 118 "Sets `tabulated-list-entries' to a function that maps currently 119 displayed denote file names matching the value of 120 `denote-menu-current-regex' to a tabulated list entry following 121 the defined form. Then updates the buffer." 122 (if tabulated-list-entries 123 (progn 124 (let 125 ((current-entry-paths (denote-menu--entries-to-paths))) 126 (setq tabulated-list-entries 127 (lambda () 128 (let ((matching-denote-files 129 (denote-menu-files-matching-regexp current-entry-paths denote-menu-current-regex))) 130 (mapcar #'denote-menu--path-to-entry matching-denote-files)))))) 131 (setq tabulated-list-entries 132 (lambda () 133 (let ((matching-denote-files 134 (denote-directory-files-matching-regexp denote-menu-current-regex))) 135 (mapcar #'denote-menu--path-to-entry matching-denote-files))))) 136 137 (revert-buffer)) 138 139 (defun denote-menu--entries-to-filenames () 140 "Return list of file names present in the *Denote* buffer." 141 (mapcar (lambda (entry) 142 (let* ((list-entry-identifier (car entry)) 143 (list-entry-denote-identifier (car (split-string list-entry-identifier "-"))) 144 (list-entry-denote-file-type (cadr (split-string list-entry-identifier "-")))) 145 (file-name-nondirectory (denote-menu-get-path-by-id list-entry-denote-identifier 146 list-entry-denote-file-type)))) 147 (funcall tabulated-list-entries))) 148 149 (defun denote-menu--entries-to-paths () 150 "Return list of file paths present in the *Denote* buffer." 151 (mapcar (lambda (entry) 152 (let* ((list-entry-identifier (car entry)) 153 (list-entry-denote-identifier (car (split-string list-entry-identifier "-"))) 154 (list-entry-denote-file-type (cadr (split-string list-entry-identifier "-")))) 155 (denote-menu-get-path-by-id list-entry-denote-identifier list-entry-denote-file-type))) 156 (funcall tabulated-list-entries))) 157 158 (defun denote-menu-get-path-by-id (id file-type) 159 "Return absolute path of denote file with ID timestamp and 160 FILE-TYPE in `denote-directory-files'." 161 (let* ((files (denote-directory-files)) 162 (matching-files-with-id (seq-filter (lambda (f) (and (string-prefix-p id (file-name-nondirectory f)))) files))) 163 (car (seq-filter (lambda (f) (string-match-p (concat "\\." file-type) f)) matching-files-with-id)))) 164 165 (defun denote-menu-files-matching-regexp (files regexp) 166 "Return list of files matching REGEXP from FILES." 167 (seq-filter (lambda (f) (string-match-p regexp f)) files)) 168 169 (defun denote-menu--path-to-unique-identifier (path) 170 "Convert PATH to a unique identifier to be used for 171 `tabulated-list-entries'. Done by taking the denote identifier of 172 PATH and appending the filename extension." 173 (let ((path-identifier (denote-retrieve-filename-identifier path)) 174 (extension (file-name-extension path))) 175 (format "%s-%s" path-identifier extension))) 176 177 (defun denote-menu--path-to-entry (path) 178 "Convert PATH to an entry matching the form of `tabulated-list-entries'." 179 (if denote-menu-show-file-signature 180 `(,(denote-menu--path-to-unique-identifier path) 181 [(,(denote-menu-date path) . (action ,(lambda (button) (funcall denote-menu-action path)))) 182 ,(denote-menu-signature path) 183 ,(denote-menu-title path) 184 ,(propertize (format "%s" (denote-extract-keywords-from-path path)) 'face 'italic)]) 185 186 `(,(denote-menu--path-to-unique-identifier path) 187 [(,(denote-menu-date path) . (action ,(lambda (button) (funcall denote-menu-action path)))) 188 ,(denote-menu-title path) 189 ,(propertize (format "%s" (denote-extract-keywords-from-path path)) 'face 'italic)]))) 190 191 (defun denote-menu-date (path) 192 "Return human readable date from denote PATH identifier." 193 (let* ((timestamp (split-string (denote-retrieve-filename-identifier path) "T")) 194 (date (car timestamp)) 195 (year (substring date 0 4)) 196 (month (substring date 4 6)) 197 (day (substring date 6 8)) 198 199 (time (cadr timestamp)) 200 (hour (substring time 0 2)) 201 (seconds (substring time 2 4))) 202 203 (format "%s-%s-%s %s:%s" year month day hour seconds))) 204 205 (defun denote-menu-signature (path) 206 "Return file signature from denote PATH identifier." 207 (let ((signature (denote-retrieve-filename-signature path))) 208 (if signature 209 signature 210 (propertize " " 'face 'font-lock-comment-face)))) 211 212 (defun denote-menu-type (path) 213 "Return file type of PATH" 214 (file-name-extension (file-name-nondirectory path))) 215 216 (defun denote-menu-title (path) 217 "Return title of PATH. 218 If the denote file PATH has no title, return the string \"(No 219 Title)\". Otherwise return PATH's title. 220 221 Determine whether a denote file has a title based on the 222 following rule derived from the file naming scheme: 223 224 1. If the path does not have a \"--\", it has no title." 225 226 (let* ((title (if (or (not (string-match-p "--" path))) 227 (propertize "(No Title)" 'face 'font-lock-comment-face) 228 (denote-retrieve-filename-title path))) 229 (file-type (propertize (concat "." (denote-menu-type path)) 'face 'font-lock-keyword-face))) 230 (if denote-menu-show-file-type 231 (concat title " " file-type) 232 title))) 233 234 (defun denote-menu-filter (regexp) 235 "Filter `tabulated-list-entries' matching REGEXP. 236 When called interactively, prompt for REGEXP. 237 238 Revert the *Denotes* buffer to include only the matching entries." 239 (interactive (list (read-regexp "Filter regex: "))) 240 (setq denote-menu-current-regex regexp) 241 (denote-menu-update-entries)) 242 243 (defun denote-menu-filter-by-keyword (keywords) 244 "Prompt for KEYWORDS and filters the list accordingly. 245 When called from Lisp, KEYWORDS is a list of strings." 246 (interactive (list (denote-keywords-prompt))) 247 (let ((regex (denote-menu--keywords-to-regex keywords))) 248 (setq denote-menu-current-regex regex) 249 (denote-menu-update-entries))) 250 251 (defun denote-menu--keywords-to-regex (keywords) 252 "Converts KEYWORDS into a regex that matches denote files that 253 contain at least one of the keywords." 254 (concat "\\(" (mapconcat (lambda (keyword) (format "_%s" keyword)) keywords "\\|") "\\)")) 255 256 (defun denote-menu-filter-out-keyword (keywords) 257 "Prompt for KEYWORDS and filters out of the list those denote 258 files that contain one of the keywords. When called from Lisp, 259 KEYWORDS is a list of strings." 260 (interactive (list (denote-keywords-prompt))) 261 ;; need to get a list of the denote files that do not include the 262 ;; keywords and set tabulated entries to be those. 263 (let* ((regex (denote-menu--keywords-to-regex keywords)) 264 (non-matching-files (seq-filter 265 (lambda (f) 266 (not (string-match-p regex (denote-get-file-name-relative-to-denote-directory f)))) 267 (denote-menu--entries-to-paths)))) 268 (setq tabulated-list-entries 269 (lambda () 270 (mapcar #'denote-menu--path-to-entry non-matching-files)))) 271 (revert-buffer)) 272 273 (defun denote-menu-clear-filters () 274 "Reset filters to `denote-menu-initial-regex' and update buffer." 275 (interactive) 276 (setq denote-menu-current-regex denote-menu-initial-regex) 277 (setq tabulated-list-entries nil) 278 (denote-menu-update-entries) ) 279 280 (defun denote-menu-export-to-dired () 281 "Switch to variable `denote-directory' and mark filtered *Denotes* 282 files." 283 (interactive) 284 (let ((files-to-mark (denote-menu--entries-to-filenames))) 285 (dired denote-directory) 286 (revert-buffer) 287 (dired-unmark-all-marks) 288 (dired-mark-if 289 (and (not (looking-at-p dired-re-dot)) 290 (not (eolp)) ; empty line 291 (let ((fn (dired-get-filename t t))) 292 (and fn (member fn files-to-mark)))) 293 "matching file"))) 294 295 (define-derived-mode denote-menu-mode tabulated-list-mode "Denote Menu" 296 "Major mode for browsing a list of Denote files." 297 :interactive nil 298 (if denote-menu-show-file-signature 299 (setq tabulated-list-format `[("Date" ,denote-menu-date-column-width t) 300 ("Signature" ,denote-menu-signature-column-width nil) 301 ("Title" ,denote-menu-title-column-width nil) 302 ("Keywords" ,denote-menu-keywords-column-width nil)]) 303 304 (setq tabulated-list-format `[("Date" ,denote-menu-date-column-width t) 305 ("Title" ,denote-menu-title-column-width nil) 306 ("Keywords" ,denote-menu-keywords-column-width nil)])) 307 308 (denote-menu-update-entries) 309 (setq tabulated-list-sort-key '("Date" . t)) 310 (tabulated-list-init-header) 311 (tabulated-list-print)) 312 313 (provide 'denote-menu) 314 ;;; denote-menu.el ends here