denote-menu.el (10511B)
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.1.1 7 ;; URL: https://github.com/namilus/denote-menu 8 ;; Package-Requires: ((emacs "28.1") (denote "1.2.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 48 (defgroup denote-menu () 49 "View Denote files" 50 :group 'files) 51 52 (defcustom denote-menu-date-column-width 17 53 "Width for the date column." 54 :type 'number 55 :group 'denote-menu) 56 57 (defcustom denote-menu-title-column-width 85 58 "Width for the title column." 59 :type 'number 60 :group 'denote-menu) 61 62 63 (defcustom denote-menu-keywords-column-width 30 64 "Width for the keywords column." 65 :type 'number 66 :group 'denote-menu) 67 68 69 (defcustom denote-menu-action (lambda (path) (find-file path)) 70 "Function to execute when a denote file button action is 71 invoked. Takes a single argument which is the path of the 72 denote file corresponding to the button." 73 :type 'function 74 :group 'denote-menu) 75 76 (defcustom denote-menu-initial-regex "." 77 "Regex used to initially populate the buffer with matching denote files." 78 :type 'string 79 :group 'denote-menu) 80 81 82 (defcustom denote-menu-show-file-type t 83 "Whether to show the denote file type" 84 :type 'boolean 85 :group 'denote-menu) 86 87 (defvar denote-menu-current-regex denote-menu-initial-regex 88 "The current regex used to match denote filenames.") 89 90 ;;;###autoload 91 (defun denote-menu-list-notes () 92 "Display list of Denote files in variable `denote-directory'." 93 (interactive) 94 (let ((buffer (get-buffer-create "*Denote*"))) 95 (with-current-buffer buffer 96 (setq buffer-file-coding-system 'utf-8) 97 (setq denote-menu-current-regex denote-menu-initial-regex) 98 (denote-menu-mode)) 99 100 (pop-to-buffer-same-window buffer))) 101 102 (defalias 'list-denotes 'denote-menu-list-notes 103 "Alias of `denote-menu-list-notes' command.") 104 105 (defun denote-menu-update-entries () 106 "Sets `tabulated-list-entries' to a function that maps currently 107 displayed denote file names matching the value of 108 `denote-menu-current-regex' to a tabulated list entry following 109 the defined form. Then updates the buffer." 110 (if tabulated-list-entries 111 (progn 112 (let 113 ((current-entry-paths (denote-menu--entries-to-paths))) 114 (setq tabulated-list-entries 115 (lambda () 116 (let ((matching-denote-files 117 (denote-menu-files-matching-regexp current-entry-paths denote-menu-current-regex))) 118 (mapcar #'denote-menu--path-to-entry matching-denote-files)))))) 119 (setq tabulated-list-entries 120 (lambda () 121 (let ((matching-denote-files 122 (denote-directory-files-matching-regexp denote-menu-current-regex))) 123 (mapcar #'denote-menu--path-to-entry matching-denote-files))))) 124 125 (revert-buffer)) 126 127 (defun denote-menu--entries-to-filenames () 128 "Return list of file names present in the *Denote* buffer." 129 (mapcar (lambda (entry) 130 (let* ((list-entry-identifier (car entry)) 131 (list-entry-denote-identifier (car (split-string list-entry-identifier "-"))) 132 (list-entry-denote-file-type (cadr (split-string list-entry-identifier "-")))) 133 (file-name-nondirectory (denote-menu-get-path-by-id list-entry-denote-identifier 134 list-entry-denote-file-type)))) 135 (funcall tabulated-list-entries))) 136 137 (defun denote-menu--entries-to-paths () 138 "Return list of file paths present in the *Denote* buffer." 139 (mapcar (lambda (entry) 140 (let* ((list-entry-identifier (car entry)) 141 (list-entry-denote-identifier (car (split-string list-entry-identifier "-"))) 142 (list-entry-denote-file-type (cadr (split-string list-entry-identifier "-")))) 143 (denote-menu-get-path-by-id list-entry-denote-identifier list-entry-denote-file-type))) 144 (funcall tabulated-list-entries))) 145 146 (defun denote-menu-get-path-by-id (id file-type) 147 "Return absolute path of denote file with ID timestamp and 148 FILE-TYPE in `denote-directory-files'." 149 (let* ((files (denote-directory-files)) 150 (matching-files-with-id (seq-filter (lambda (f) (and (string-prefix-p id (file-name-nondirectory f)))) files))) 151 (car (seq-filter (lambda (f) (string-match-p (concat "\\." file-type) f)) matching-files-with-id)))) 152 153 (defun denote-menu-files-matching-regexp (files regexp) 154 "Return list of files matching REGEXP from FILES." 155 (seq-filter (lambda (f) (string-match-p regexp f)) files)) 156 157 (defun denote-menu--path-to-unique-identifier (path) 158 "Convert PATH to a unique identifier to be used for 159 `tabulated-list-entries'. Done by taking the denote identifier of 160 PATH and appending the filename extension." 161 (let ((path-identifier (denote-retrieve-filename-identifier path)) 162 (extension (file-name-extension path))) 163 (format "%s-%s" path-identifier extension))) 164 165 166 (defun denote-menu--path-to-entry (path) 167 "Convert PATH to an entry matching the form of `tabulated-list-entries'." 168 `(,(denote-menu--path-to-unique-identifier path) 169 [(,(denote-menu-date path) . (action ,(lambda (button) (funcall denote-menu-action path)))) 170 ,(denote-menu-title path) 171 ,(propertize (format "%s" (denote-extract-keywords-from-path path)) 'face 'italic)])) 172 173 (defun denote-menu-date (path) 174 "Return human readable date from denote PATH identifier." 175 (let* ((timestamp (split-string (denote-retrieve-filename-identifier path) "T")) 176 (date (car timestamp)) 177 (year (substring date 0 4)) 178 (month (substring date 4 6)) 179 (day (substring date 6 8)) 180 181 (time (cadr timestamp)) 182 (hour (substring time 0 2)) 183 (seconds (substring time 2 4))) 184 185 (format "%s-%s-%s %s:%s" year month day hour seconds))) 186 187 188 (defun denote-menu-type (path) 189 "Return file type of PATH" 190 (file-name-extension (file-name-nondirectory path))) 191 192 (defun denote-menu-title (path) 193 "Return title of PATH. 194 If the denote file PATH has no title, return the string \"(No 195 Title)\". Otherwise return PATH's title. 196 197 Determine whether a denote file has a title based on the 198 following rule derived from the file naming scheme: 199 200 1. If the path does not have a \"--\", it has no title." 201 202 (let* ((title (if (or (not (string-match-p "--" path))) 203 (propertize "(No Title)" 'face 'font-lock-comment-face) 204 (denote-retrieve-filename-title path))) 205 (file-type (propertize (concat "." (denote-menu-type path)) 'face 'font-lock-keyword-face))) 206 (if denote-menu-show-file-type 207 (concat title " " file-type) 208 title))) 209 210 (defun denote-menu-filter (regexp) 211 "Filter `tabulated-list-entries' matching REGEXP. 212 When called interactively, prompt for REGEXP. 213 214 Revert the *Denotes* buffer to include only the matching entries." 215 (interactive (list (read-regexp "Filter regex: "))) 216 (setq denote-menu-current-regex regexp) 217 (denote-menu-update-entries)) 218 219 (defun denote-menu-filter-by-keyword (keywords) 220 "Prompt for KEYWORDS and filters the list accordingly. 221 When called from Lisp, KEYWORDS is a list of strings." 222 (interactive (list (denote-keywords-prompt))) 223 (let ((regex (concat "\\(" (mapconcat (lambda (keyword) (format "_%s" keyword)) keywords "\\|") "\\)"))) 224 (setq denote-menu-current-regex regex) 225 (denote-menu-update-entries))) 226 227 (defun denote-menu-clear-filters () 228 "Reset filters to `denote-menu-initial-regex' and update buffer." 229 (interactive) 230 (setq denote-menu-current-regex denote-menu-initial-regex) 231 (setq tabulated-list-entries nil) 232 (denote-menu-update-entries) ) 233 234 (defun denote-menu-export-to-dired () 235 "Switch to variable `denote-directory' and mark filtered *Denotes* 236 files." 237 (interactive) 238 (let ((files-to-mark (denote-menu--entries-to-filenames))) 239 (dired denote-directory) 240 (revert-buffer) 241 (dired-unmark-all-marks) 242 (dired-mark-if 243 (and (not (looking-at-p dired-re-dot)) 244 (not (eolp)) ; empty line 245 (let ((fn (dired-get-filename t t))) 246 (and fn (member fn files-to-mark)))) 247 "matching file"))) 248 249 (define-derived-mode denote-menu-mode tabulated-list-mode "Denote Menu" 250 "Major mode for browsing a list of Denote files." 251 :interactive nil 252 (setq tabulated-list-format `[("Date" ,denote-menu-date-column-width t) 253 ("Title" ,denote-menu-title-column-width nil) 254 ("Keywords" ,denote-menu-keywords-column-width nil)]) 255 256 (denote-menu-update-entries) 257 (setq tabulated-list-sort-key '("Date" . t)) 258 (tabulated-list-init-header) 259 (tabulated-list-print)) 260 261 (provide 'denote-menu) 262 ;;; denote-menu.el ends here