dotemacs

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

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