dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

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