OrgNV - Org Navigator

Philosophy

OrgNV is a grep-based navigator over notes written in text format. OrgNV assumes that your notes are scattered over a number of directories, enumerated in the global variable orgnv-directories. It uses the program grep to search those directories for notes that match certain criteria.

The search shows matching files in a useful representation that does not rely on file names. Instead, it attempts to show the title and a summary of the notes, assuming that the notes have a format like the one below. The user can navigate over the results with the arrows, with M-p, M-n, or writing text to narrow the search to the results incrementally.

Org mode:

#+TITLE: Some important title
#+KEYWORDS: tag1, tag2, tag3

Some description lines.
There may be more.
It is up to ~orgnv-description-limit~.

Matching is not case sensitive, but whitespace matters: both Title: and Keywords: must be at the beginning of the line. Support for markdown and text modes will be implemented in the future.

Assuming this, OrgNV provides functions to

  • Collect a database of notes, with titles and brief descriptions
  • Interrogate the database, searching for words incrementally, similar to deft.
  • Process the output of that interrogation to create new notes, embed links, etc.

This is done in a way that is "programmatic", that is, the library itself allows you to write other functions that interrogate the database in different ways and do different things with the output.

Prelude

Download the source code here. I still have not created a github project for this.

License

;; -*- lexical-binding: t -*-
;;; orgnv.el --- notes database based on grep
;;;
;;; Copyright (C) 2020 Juan Jose Garcia-Ripoll
;;
;; All rights reserved.

;; Redistribution and use in source and binary forms, with or without
;; modification, are permitted provided that the following conditions are met:
;; 1. Redistributions of source code must retain the above copyright
;;    notice, this list of conditions and the following disclaimer.
;; 2. Redistributions in binary form must reproduce the above copyright
;;    notice, this list of conditions and the following disclaimer in the
;;    documentation  and/or other materials provided with the distribution.
;; 3. Neither the names of the copyright holders nor the names of any
;;    contributors may be used to endorse or promote products derived from
;;    this software without specific prior written permission.
;;
;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
;; AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
;; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
;; ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
;; LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
;; CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
;; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
;; POSSIBILITY OF SUCH DAMAGE.
;;
;;; Version: 0.1
;;; Author: Juan Jose Garcia-Ripoll <[email protected]>
;;; Keywords: org mode, plain text, notes, Deft, Simplenote, Notational Velocity

;; This file is not part of GNU Emacs.

(require 'org)

Actual OrgNV code

Operation parameters

The following variables modify the way OrgNV operates. The most important ones are

  • orgnv-directories, which specifies where notes are located.
  • orgnv-recursive, which instructs OrgNV to search for notes in subfolders.
  • orgnv-grep, the implementation of GNU grep in your system.
(defgroup orgnv nil
  "Emacs OrgNV mode."
  :group 'local)

(defcustom orgnv-directories (or (expand-file-name org-directory)
                                 (expand-file-name "~/.orgnv/"))
  "List of directories where OrgNV looks for notes."
  :type 'directory
  :safe 'stringp
  :group 'orgnv)

(defcustom orgnv-recursive t
  "If true, recursively search the orgnv-directories and its subfolders looking for notes."
  :type 'boolean
  :group 'orgnv)

(defcustom orgnv-ignore-case t
  "If true (the default value), ignore cases when searching notes."
  :type 'boolean
  :group 'orgnv)

(defcustom orgnv-extensions '("org")
  "File name extensions to consider when looking for notes."
  :type '(repeat string)
  :group 'orgnv)

(defcustom orgnv-grep-command (or (executable-find "grep")
                                  "grep")
  "Executable file that implements the GNU grep utility."
  :type 'file
  :safe 'stringp
  :group 'orgnv)

(defcustom orgnv-description-limit 1000
  "Maximum number of files to display in the browser."
  :type '(choice (integer :tag "Limit number of files displayed")
                 (const :tag "No limit" nil))
  :group 'orgnv)

(defconst orgnv-buffer "*orgnv output*"
  "OrgNV buffer name")

(defcustom orgnv-context-size 3
  "Maximum number of lines to consider after the title."
  :type 'integer
  :safe (lambda (x) (< 0 x 10))
  :group 'orgnv)

(defcustom orgnv-title-pattern "#\\+TITLE\\:"
  "Pattern used for searching titles in notes."
  :type 'string
  :group 'orgnv)

(defcustom orgnv-display-limit 300
  "Maximum number of items do display in the browsing buffer"
  :type 'integer
  :group 'orgnv)

(defcustom orgnv-database-sort-predicate 'orgnv-compare-titles
  "Either NIL, if we do not sort the database, or a function with
two arguments that can be passed to SORT."
  :type '(or function symbol))

(defcustom orgnv-install-keybindings t
  "Whether to install OrgNV under C-x g"
  :type 'boolean
  :group 'orgnv)

Appearance

OrgNV shows results in a way that is similar to Deft's. However, since we rely on the minibuffer for input, we only need to customize the title and description of the note.

(defgroup orgnv-faces nil
  "Faces used in Orgnv mode"
  :group 'orgnv
  :group 'faces)

(defface orgnv-title-face
  '((t :inherit font-lock-function-name-face :bold t))
  "Face for OrgNV file titles."
  :group 'orgnv-faces)

(defface orgnv-separator-face
  '((t :inherit font-lock-comment-delimiter-face))
  "Face for OrgNV separator string."
  :group 'orgnv-faces)

(defface orgnv-summary-face
  '((t :inherit font-lock-comment-face))
  "Face for OrgNV file summary strings."
  :group 'orgnv-faces)

Gathering the database

The core of the library is implemented in two functions:

  • orgnv--grep uses GNU grep to scan all notes that match a pattern.
  • orgnv--format processes the output of ~orgnv–grep~constructing the database.

Grep invocation

We instruct grep to return the results as lines, with the format

[file-name] NULL [content-of-matching-line]
[file-name] NULL [context-line-1]
...
[file-name] NULL [context-line-N]
--

An example of the output from orgnv--grep would be the following excerpt, where ^@ denotes the NULL character

--
c:/Users/juanj/Nextcloud/Documents/Notes/areas.org^@#+TITLE: Areas of interest
c:/Users/juanj/Nextcloud/Documents/Notes/areas.org^@
c:/Users/juanj/Nextcloud/Documents/Notes/areas.org^@* Outreach
c:/Users/juanj/Nextcloud/Documents/Notes/areas.org^@** Public Webinar
--
c:/Users/juanj/Nextcloud/Documents/Notes/calendar.org^@#+TITLE: Calendar
c:/Users/juanj/Nextcloud/Documents/Notes/calendar.org^@
c:/Users/juanj/Nextcloud/Documents/Notes/calendar.org^@* CSIC Calendar
c:/Users/juanj/Nextcloud/Documents/Notes/calendar.org^@** DONE Skype with Peter
--
c:/Users/juanj/Nextcloud/Documents/Notes/Direct2D development.org^@#+TITLE: Direct2D development
c:/Users/juanj/Nextcloud/Documents/Notes/Direct2D development.org^@#+STARTUP: indent
c:/Users/juanj/Nextcloud/Documents/Notes/Direct2D development.org^@
c:/Users/juanj/Nextcloud/Documents/Notes/Direct2D development.org^@* Render targets overview

Database building

The function orgnv-build-database is responsible for invoking grep and starting the parsing of the resulting buffer.

(defun orgnv-build-database (&optional pattern context directories)
  "Scan the notes in the orgnv-directories, creating a temporary
buffer with the output of the command. PATTERN, CONTEXT,
DIRECTORIES default orgnv-title-pattern, orgnv-context-size and
orgnv-directories."
  (with-temp-buffer
    (delete-region (point-min) (point-max))
    (setq context (or context orgnv-context-size)
          pattern (or pattern orgnv-title-pattern))
    (let ((grep-args `(,@(unless (zerop context)
                           (list "-A" (format "%s" context)))
                       ;;"-s" ; Suspend errors
                       "-E" ; Extended syntax
                       "-Z" ; Null separated names
                       "-m" "1" ; One result
                       ,(if orgnv-ignore-case
                            "--ignore-case"
                          "--no-ignore-case")
                       ,@(mapcar (lambda (extension)
                                   (format "--include=*.%s" extension))
                                 orgnv-extensions)
                       ,(if orgnv-recursive
                            "--directories=recurse"
                          "--directories=skip")
                       ,(concat "^" pattern)
                       ,@(mapcar (lambda (d) (expand-file-name "*" d))
                                 (or directories orgnv-directories)))))
      (message "Invoking %s with %S" orgnv-grep-command grep-args)
      (apply 'call-process orgnv-grep-command nil t nil grep-args)
      (orgnv--format pattern))))

(defun orgnv-compare-titles (record1 record2)
  (string< (cadr record1) (cadr record2)))

The function orgnv--format does the actual parsing, optionally sorting the output by title.

(defun orgnv--format (pattern &optional sort-predicate)
  "This function processes the current buffer, scanning for an
output from GREP in the form of

  file-name null-character line-matching-title
  file-name null-character summary-line-1
  ...
  --

It returns a database in the form of an association list,
  ((filename-1 . (title-1 . description-1))
   ...)
This database will be sorted if SORT-PREDICATE is a function
that can compare pairs of records."
  (goto-char (point-min))
  (let ((file-pattern (concat "^\\([^\x0\n]+\\)\x0[ ]*\\(" pattern "\\)?[ ]*\\([^\n]*\\)$"))
        (case-fold-search orgnv-ignore-case)
        (records '())
        lastfile
        record
        description)
    (while (re-search-forward file-pattern nil t)
      (let ((file (match-string 1))
            (string (match-string 3)))
        (cond ((not (equal file lastfile))
               (setq lastfile file
                     description ""
                     record (cons string description)
                     records (cons (cons lastfile record) records)))
              ((or (null string) (zerop (length string)))
               ;; Nothing in the description
               )
              ((null description)
               ;; No more text fits into the description
               )
              (t
               (setq description (concat description string " "))
               (if (>= (length description) orgnv-description-limit)
                   (setf description (substring description 0
                                                orgnv-description-limit)
                         (cdr record) description
                         description nil)
                 (setf (cdr record) description))))))

    (if (setq sort-predicate (or sort-predicate orgnv-database-sort-predicate))
        (sort records sort-predicate)
      records)))

Database interrogation

Filtering

We take a database and extract the the records that match a `pattern`. Currently, we use substring matching, without regular expressions. In the near future, we will have also word-based matching.

(defun orgnv--filter-database (pattern database &optional size-limit)
  "Take an OrgNV database and filter the records that match
PATTERN. If SIZE-LIMIT is not nil, return a database with at most
SIZE-LIMIT elements."
  (cond ((length pattern)
         (let ((i 0)
               (output nil))
           (while (and database (or (null size-limit) (<= i orgnv-display-limit)))
             (let ((record (pop database)))
               (when (string-match pattern (cadr record))
                 (setq i (1+ i))
                 (push record output))))
           (nreverse output)))
        ((null size-limit)
         database)
        (t
         (seq-subseq database 0 size-limit))))

Database display

When we interrogate the OrgNV database, a subset of it is displayed in an auxiliary buffer. orgnv--display-matches is the main function to update the display, which has some characteristics:

  • The buffer has hl-line-mode active.
  • The selected line in the display buffer contains the record to be returned.
(defvar orgnv--database nil
  "Database which is currently being interrogated")
(defvar orgnv--filtered-database nil
  "Subset of elements in the database that are displayed")
(defvar orgnv--buffer nil
  "Buffer where the database is displayed")

(defun orgnv--display-matches (buffer filtered-database &optional first)
  (with-current-buffer buffer
    (delete-region (point-min) (point-max))
    (dolist (l filtered-database)
      (insert (propertize (cadr l) 'face 'orgnv-title-face)
              (propertize " -- " 'face 'orgnv-separator-face)
              (propertize (cddr l) 'face 'orgnv-summary-face)
              "\n"))
    (when first
      (toggle-truncate-lines +1)
      (hl-line-mode +1))
    (goto-char (point-min))
    (hl-line-highlight)))

The following commands change our selection by moving the cursor on the database buffer.

(defun orgnv-previous-line ()
  (interactive)
  (with-current-buffer orgnv--buffer
    (hl-line-unhighlight)
    (forward-line -1)
    (hl-line-highlight)))

(defun orgnv-next-line ()
  (interactive)
  (with-current-buffer orgnv--buffer
    (hl-line-unhighlight)
    (forward-line +1)
    (hl-line-highlight)))

Dynamical selection of records

The main routine is orgnv-completing-read, which prompts the user to select one of the records, using the minibuffer for input and the auxiliary buffer to display the results so far.

(defvar orgnv--selection nil
  "Record selected by the user or nil")

(defvar orgnv--next-action nil
  "Actions to perform once the ORGNV buffer is closed.")

(defun orgnv--cleanup ()
  "Quit the OrgNV and close its window."
  (pop-to-buffer orgnv--buffer)
  (quit-window))

(defun orgnv-completing-read (database)
  "Prompt the user to select a record in the OrgNV. The user will
input text in the minibuffer. This text will be used to refine
queries on the database, with output displayed in
orgnv-buffer. The user can press M-p, M-n, <up> and <down> to
change the selected entries. Finally pressing <RET> implements
the selection."
  (let ((orgnv--database database)
        (orgnv--filtered-database database)
        (orgnv--selection nil))
    (with-temp-buffer
      (let ((orgnv--buffer (current-buffer)))
        (rename-buffer orgnv-buffer t)
        (orgnv--update t)
        (display-buffer orgnv--buffer)
        (unwind-protect
            (let ((map (orgnv--make-minibuffer-map)))
              (read-from-minibuffer "Keywords: " nil map))
          ;; We cleanup our buffers both for natural causes (e.g. pressing
          ;; <enter>) or when aborting (pressing C-g)
          (orgnv--cleanup))))
    orgnv--selection))

Navigation through records

orgnv-completing-read works by creating a minibuffer keymap that implements the following actions:

  • M-p and <up> move cursor upwards by one line in the database view
  • M-n and <down>, do the same downards.
  • <RET> stores the users's selection and stops the minibuffer entry.
  • any other key updates the minibuffer content and refreshes the display
(defun orgnv--make-minibuffer-map ()
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map minibuffer-local-map)
    (define-key map "\t" 'minibuffer-complete)
    ;; Extend the filter string by default.
    (setq i ?\s)
    (while (< i 256)
      (define-key map (vector i) 'orgnv--insert-and-update)
      (setq i (1+ i)))
    ;; M-TAB is already abused for many other purposes, so we should find
    ;; another binding for it.
    ;; (define-key map "\e\t" 'minibuffer-force-complete)
    (define-key map [?\M-p] 'orgnv-previous-line)
    (define-key map [?\M-n] 'orgnv-next-line)
    (define-key map [?\C-h] 'orgnv-help)
    (define-key map (kbd "<up>") 'orgnv-previous-line)
    (define-key map (kbd "<down>") 'orgnv-next-line)
    (define-key map (kbd "<tab>") 'orgnv--update)
    (define-key map [?\C-c ?\C-l] 'orgnv-link)
    (define-key map (kbd "RET") 'orgnv-select)
    map))

(defun orgnv-select ()
  (interactive)
  (orgnv--select (lambda (selection)
                   (switch-to-buffer (find-file-noselect (car selection)
                                                         nil nil nil)))))

(defun orgnv-link ()
  (interactive)
  (orgnv--select (lambda (selection)
                   (org-insert-link nil
                                    (car selection)
                                    (cadr selection)))))

(defun orgnv--select (the-action)
  (let ((filter (minibuffer-contents)))
    (with-current-buffer orgnv--buffer
      (let* ((l (min (line-number-at-pos)
                     (length orgnv--filtered-database)))
             (selection (elt orgnv--filtered-database (1- l))))
        (push
         (if selection
             (lambda ()
               (funcall the-action selection))
           (lambda ()
             (let ((new-note (orgnv-create-note filter)))
               (when new-note
                 (funcall the-action new-note)))))
         orgnv--next-action))))
  (exit-minibuffer))

(defvar orgnv-help nil)

(defun orgnv-help-text ()
  "OrgNV (Org Navigation)
----------------------
Enter keywords to refine search through the OrgNV database. Use the special
keys below to affect the selection of records, move through the database,
create notes, etc.

key               binding
---               -------
C-g               abort
C-h               show / hide this help message
up / M-p          move to previous record
down / M-n        move to next record
<enter>           select current record if it exits, or create new note
C-c C-l           link current record
other keys        continue extending the database query
")

(defun orgnv-help ()
  (interactive)
  (if orgnv-help
      (orgnv--clear-help)
    (with-current-buffer orgnv--buffer
      (setq-local orgnv-help (point))
      (delete-region (point-min) (point-max))
      (insert (orgnv-help-text)))))

(defun orgnv--clear-help ()
  (when orgnv-help
    (with-current-buffer orgnv--buffer
      (orgnv--update)
      (goto-char orgnv-help)
      (setq-local orgnv-help nil))))

(defun orgnv--insert-and-update ()
  (interactive)
  (let ((c (vector last-command-event))
        f)
    (cond ((setq f (lookup-key minibuffer-local-map c))
           (funcall f 1))
          ((setq f (lookup-key global-map c))
           (funcall f 1))
          (t
           (self-insert-command 1)))
    (orgnv--update)))

(defun orgnv--update (&optional first-time)
  (interactive)
  (setq orgnv--filtered-database
        (orgnv--filter-database (minibuffer-contents)
                                orgnv--database))
  (orgnv--display-matches orgnv--buffer orgnv--filtered-database first-time))

Note creation

(defun orgnv-create-note (tentative-title)
  "Create a note, prompting the user for the title."
  (let ((title (read-from-minibuffer "Note title: " tentative-title)))
    (when title
      (let* ((filenames (mapcar (lambda (dir)
                                  (orgnv-make-file-name title dir))
                                orgnv-directories))
             (filename (completing-read "Filename: " filenames
                                        nil nil (car filenames))))
        (when filename
          (orgnv-create-template filename title)
          (cons filename (cons title nil))
          )))))

(defun orgnv-make-file-name (title dir)
  (expand-file-name (concat title ".org") dir))

(defun orgnv-create-template (filename title)
  (let ((buffer (find-file-noselect filename nil nil nil)))
    (when buffer
      (with-current-buffer buffer
        (insert "#+TITLE: " title "\n\n"))
      buffer)))

Applications

Creation of org-mode links

Using our database scheme, we can now create links easily.

(defcustom orgnv-relative-links nil
  "Control how to create org-mode links.
- nil means insert full paths
- t means paths relative to org-directory"
  :type 'boolean
  :group 'orgnv)

(defun orgnv-browse ()
  (interactive)
  (let* ((orgnv--next-action nil)
         (record (orgnv-completing-read (or orgnv--database
                                            (orgnv-build-database)))))
    (dolist (action orgnv--next-action)
      (funcall action))
    record))

(defun orgnv--relative-path (file)
  (if orgnv-relative-links
      (file-relative-name file org-directory)
    file))

Keybindings

If the user allows, we install the previous applications under C-x g (mnemonic for grep).

(bind-key (kbd "C-x g") 'orgnv-browse org-mode-map)

Finish

(provide 'orgnv)

Testing

Lorem Ipsum generator

The following is a simple generator for bogus text, which will be used to create a large database of fake notes.

(defvar lorem-ipsum-sentences
  '("Lorem ipsum dolor sit amet"
    "consectetuer adipiscing elit"
    "sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat"
    "Ut wisi enim ad minim veniam"
    "quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat"
    "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat"
    "vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi"
    "Expetenda tincidunt in sed"
    "ex partem placerat sea"
    "porro commodo ex eam"
    "His putant aeterno interesset at"
    "Usu ea mundi tincidunt"
    "omnium virtute aliquando ius ex"
    "Ea aperiri sententiae duo"
    "Usu nullam dolorum quaestio ei"
    "sit vidit facilisis ea"
    "Per ne impedit iracundia neglegentur"
    "Consetetur neglegentur eum ut"
    "vis animal legimus inimicus id"
    "His audiam deserunt in"
    "eum ubique voluptatibus te"
    "In reque dicta usu"
    "Ne rebum dissentiet eam"
    "vim omnis deseruisse id"
    "Ullum deleniti vituperata at quo"
    "insolens complectitur te eos"
    "ea pri dico munere propriae"
    "Vel ferri facilis ut"
    "qui paulo ridens praesent ad"
    "Possim alterum qui cu"
    "Accusamus consulatu ius te"
    "cu decore soleat appareat usu"
    "Est ei erat mucius quaeque"
    "Ei his quas phaedrum"
    "efficiantur mediocritatem ne sed"
    "hinc oratio blandit ei sed"
    "Blandit gloriatur eam et"
    "Brute noluisse per et"
    "verear disputando neglegentur at quo"
    "Sea quem legere ei"
    "unum soluta ne duo"
    "Ludus complectitur quo te"
    "ut vide autem homero pro"
    "Vis id minim dicant sensibus"
    "Pri aliquip conclusionemque ad"
    "ad malis evertitur torquatos his"
    "Has ei solum harum reprimique"
    "id illum saperet tractatos his"
    "Ei omnis soleat antiopam quo"
    "Ad augue inani postulant mel"
    "mel ea qualisque forensibus"
    "Lorem salutandi eu mea"
    "eam in soleat iriure assentior"
    "Tamquam lobortis id qui"
    "Ea sanctus democritum mei"
    "per eu alterum electram adversarium"
    "Ea vix probo dicta iuvaret"
    "posse epicurei suavitate eam an"
    "nam et vidit menandri"
    "Ut his accusata petentium"
    "Meis vocent signiferumque pri et"
    "Facilis corpora recusabo ne quo"
    "eum ne eruditi blandit suscipiantur"
    "Mazim sapientem sed id"
    "sea debet commune iracundia in"
    "Eius falli propriae te usu"
    "In usu nonummy volumus expetenda"
    "sint quando facilisis ei per"
    "delectus constituto sea te"
    "Wisi forensibus mnesarchum in cum"
    "Per id impetus abhorreant"
    "his no magna definiebas"
    "inani rationibus in quo"
    "Ut vidisse dolores est"
    "ut quis nominavi mel"
    "Ad pri quod apeirian concludaturque"
    "id timeam iudicabit rationibus pri"
    "Erant putant luptatum ex sit"
    "error euismod ad qui"
    "meliore voluptatum complectitur an vix"
    "Clita persius sed et"
    "vix vidit consulatu complectitur ex"
    "Per nonummy postulant assentior an"
    "mea audiam fabellas deserunt id"
    "Cu nam labores lobortis definiebas"
    "ei aliquyam salutatus persequeris quo"
    "cum eu nemore fierent dissentiunt"
    "Per vero dolor id"
    "vide democritum scribentur eu vim"
    "pri erroribus temporibus ex"
    "Euismod molestie offendit has no"
    "Quo te semper invidunt quaestio"
    "per vituperatoribus sadipscing ei"
    "partem aliquyam sensibus in cum"
    "Mei eu mollis albucius"
    "ex nisl contentiones vix"
    "Duo persius volutpat at"
    "cu iuvaret epicuri mei"
    "Duo posse pertinacia no"
    "ex dolor contentiones mea"
    "Nec omnium utamur dignissim ne"
    "Mundi lucilius salutandi an sea"
    "ne sea aeque iudico comprehensam"
    "Populo delicatissimi ad pri"
    "Ex vitae accusam vivendum pro"
    "An vim commodo dolorem volutpat"
    "cu expetendis voluptatum usu"
    "et mutat consul adversarium his"
    "His natum numquam legimus an"
    "diam fabulas mei ut"
    "Melius fabellas sadipscing vel id"
    "Partem diceret mandamus mea ne"
    "has te tempor nostrud"
    "Aeque nostro eum no"
    "Eam ex integre quaeque bonorum"
    "ea assum solet scriptorem pri"
    "et usu nonummy accusata interpretaris"
    "Debitis necessitatibus est no"
    "Eu probo graeco eum"
    "at eius choro sit"
    "possit recusabo corrumpit vim ne"
    "Noster diceret delicata vel id"
    "Dicunt percipit deserunt ut usu"
    "Aliquip delenit an eam"
    "vocent vituperata vim ea"
    "Ei mollis audire interpretaris cum"
    "ei impedit fierent sea"
    "Ius at homero noster prompta"
    "ea sit dignissim vituperata efficiendi"
    "Natum placerat ad mei"
    "At cum soleat disputationi"
    "quo veri admodum vituperata ad"
    "Ea vix ceteros complectitur"
    "vel cu nihil nullam"
    "Nam placerat oporteat molestiae ei"
    "an putant albucius qui"
    "Oblique menandri ei his"
    "mei te mazim oportere comprehensam"
    "In mel saperet expetendis"
    "Vitae urbanitas sadipscing nec ut"
    "at vim quis lorem labitur"
    "Exerci electram has et"
    "vidit solet tincidunt quo ad"
    "moderatius contentiones nec no"
    "Nam et puto abhorreant scripserit"
    "et cum inimicus accusamus"
    "Has maiorum habemus detraxit at"
    "Timeam fabulas splendide et his"
    "Facilisi aliquando sea ad"
    "vel ne consetetur adversarium"
    "Integre admodum et his"
    "nominavi urbanitas et per"
    "alii reprehendunt et qui"
    "His ei meis legere nostro"
    "eu kasd fabellas definiebas mei"
    "in sea augue iriure"
    "Senserit mediocrem vis ex"
    "et dicunt deleniti gubergren mei"
    "Mel id clita mollis repudiare"
    "Sed ad nostro delicatissimi"
    "postea pertinax est an"
    "Adhuc sensibus percipitur sed te"
    "eirmod tritani debitis nec ea"
    "Cu vis quis gubergren"
    "At quaeque adversarium ius"
    "sed at integre persius verterem"
    "Sit summo tibique at"
    "eam et fugit complectitur"
    "vis te natum vivendum mandamus"
    "Iudico quodsi cum ad"
    "dicit everti sensibus in sea"
    "ea eius paulo deterruisset pri"
    "Pro id aliquam hendrerit definitiones"
    "Per et legimus delectus"
    "No his munere interesset"
    "At soluta accusam gloriatur eos"
    "ferri commodo sed id"
    "ei tollit legere nec"
    "Eum et iudico graecis"
    "cu zzril instructior per"
    "usu at augue epicurei"
    "Saepe scaevola takimata vix id"
    "Errem dictas posidonium id vis"
    "ne modo affert incorrupte eos"
    "Virtute equidem ceteros in mel"
    "Id volutpat neglegentur eos"
    "Eu eum facilisis voluptatum"
    "no eam albucius verterem"
    "Sit congue platonem adolescens ut"
    "Offendit reprimique et has"
    "eu mei homero imperdiet"
    "Id sea utamur aperiam"
    "te per choro accusamus consulatu"
    "Brute munere corrumpit ut pri"
    "Ea ipsum appareat erroribus mea"
    "Mei probo inani aliquid ad"
    "Omnis fabulas concludaturque an vix"
    "an noluisse takimata facilisis pro"
    "sit te volumus mandamus"
    "Ad malorum imperdiet duo"
    "ea possim utamur accusamus vix"
    "Lorem ipsum dolor sit amet"
    "consectetuer adipiscing elit"
    "sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat"
    "Ut wisi enim ad minim veniam"
    "quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat"
    "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat"
    "vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi"
    "Expetenda tincidunt in sed"
    "ex partem placerat sea"
    "porro commodo ex eam"
    "His putant aeterno interesset at"
    "Usu ea mundi tincidunt"
    "omnium virtute aliquando ius ex"
    "Ea aperiri sententiae duo"
    "Usu nullam dolorum quaestio ei"
    "sit vidit facilisis ea"
    "Per ne impedit iracundia neglegentur"
    "Consetetur neglegentur eum ut"
    "vis animal legimus inimicus id"
    "His audiam deserunt in"
    "eum ubique voluptatibus te"
    "In reque dicta usu"
    "Ne rebum dissentiet eam"
    "vim omnis deseruisse id"
    "Ullum deleniti vituperata at quo"
    "insolens complectitur te eos"
    "ea pri dico munere propriae"
    "Vel ferri facilis ut"
    "qui paulo ridens praesent ad"
    "Possim alterum qui cu"
    "Accusamus consulatu ius te"
    "cu decore soleat appareat usu"
    "Est ei erat mucius quaeque"
    "Ei his quas phaedrum"
    "efficiantur mediocritatem ne sed"
    "hinc oratio blandit ei sed"
    "Blandit gloriatur eam et"
    "Brute noluisse per et"
    "verear disputando neglegentur at quo"
    "Sea quem legere ei"
    "unum soluta ne duo"
    "Ludus complectitur quo te"
    "ut vide autem homero pro"
    "Vis id minim dicant sensibus"
    "Pri aliquip conclusionemque ad"
    "ad malis evertitur torquatos his"
    "Has ei solum harum reprimique"
    "id illum saperet tractatos his"
    "Ei omnis soleat antiopam quo"
    "Ad augue inani postulant mel"
    "mel ea qualisque forensibus"
    "Lorem salutandi eu mea"
    "eam in soleat iriure assentior"
    "Tamquam lobortis id qui"
    "Ea sanctus democritum mei"
    "per eu alterum electram adversarium"
    "Ea vix probo dicta iuvaret"
    "posse epicurei suavitate eam an"
    "nam et vidit menandri"
    "Ut his accusata petentium"
    "Meis vocent signiferumque pri et"
    "Facilis corpora recusabo ne quo"
    "eum ne eruditi blandit suscipiantur"
    "Mazim sapientem sed id"
    "sea debet commune iracundia in"
    "Eius falli propriae te usu"
    "In usu nonummy volumus expetenda"
    "sint quando facilisis ei per"
    "delectus constituto sea te"
    "Wisi forensibus mnesarchum in cum"
    "Per id impetus abhorreant"
    "his no magna definiebas"
    "inani rationibus in quo"
    "Ut vidisse dolores est"
    "ut quis nominavi mel"
    "Ad pri quod apeirian concludaturque"
    "id timeam iudicabit rationibus pri"
    "Erant putant luptatum ex sit"
    "error euismod ad qui"
    "meliore voluptatum complectitur an vix"
    "Clita persius sed et"
    "vix vidit consulatu complectitur ex"
    "Per nonummy postulant assentior an"
    "mea audiam fabellas deserunt id"
    "Cu nam labores lobortis definiebas"
    "ei aliquyam salutatus persequeris quo"
    "cum eu nemore fierent dissentiunt"
    "Per vero dolor id"
    "vide democritum scribentur eu vim"
    "pri erroribus temporibus ex"
    "Euismod molestie offendit has no"
    "Quo te semper invidunt quaestio"
    "per vituperatoribus sadipscing ei"
    "partem aliquyam sensibus in cum"))

(defvar lorem-ipsum-limit (length lorem-ipsum-sentences))

(defun lorem-ipsum-line ()
  (nth (random lorem-ipsum-limit)
       lorem-ipsum-sentences))

(defun lorem-ipsum-permuted-line (&optional terminator)
  (let ((words (split-string (lorem-ipsum-line) nil t))
        (output nil))
    (while words
      (let ((w (elt words (random (length words)))))
        (setq words (delq w words)
              output (cons (downcase w) (if output (cons " " output) nil)))))
    (let ((string (apply 'concat output)))
      (concat (string (upcase (elt string 0)))
              (substring string 1)
              (or terminator ".")))))

(defun lorem-ipsum-sentence (&optional commas terminator)
  (let* (n
         (output (cond ((or (null commas)
                            (<= (setq n (random 10)) 5))
                        (lorem-ipsum-line))
                       ((<= n 8)
                        (concat (lorem-ipsum-line) ", "
                                (lorem-ipsum-line)))
                       (t
                        (concat (lorem-ipsum-line) ", "
                                (lorem-ipsum-line) ", "
                                (lorem-ipsum-line))))))
    (concat (string (upcase (elt output 0)))
            (downcase (seq-subseq output 1))
            (or terminator "."))))

(defun lorem-ipsum-paragraph (&optional max-length)
  (let (output)
    (dotimes (i (max 3 (random (or max-length 6))))
      (push (lorem-ipsum-sentence t (if output ". " "."))
            output))
    (apply 'concat output)))

Generate template notes

We create 1000 notes with random but legible text in a subdirectory foo under the location of this notebook.

(let ((current-dir (file-name-directory (buffer-file-name))))
  (load (expand-file-name "orgnv.el" current-dir))
  (load (expand-file-name "lorem-ipsum.el" current-dir))
  (setq orgnv-directories (list (expand-file-name "foo" current-dir))))

(let ((directory (car orgnv-directories))
      (max-notes 1000))
  (if (file-directory-p (expand-file-name directory))
      (delete-directory directory t))
  (make-directory directory)
  (with-temp-buffer
    (dotimes (i max-notes)
      (let* ((title (lorem-ipsum-permuted-line ""))
             (filename (expand-file-name (format "%s.org" (downcase title))
                                         directory)))
        (insert "#+TITLE: " title)
        (dotimes (i (random 6))
          (insert "\n\n")
          (insert (lorem-ipsum-paragraph)))
        (write-region (point-min) (point-max) filename)
        (delete-region (point-min) (point-max))))))

Sample tests

This is how fast Deft works at processing those notes

(require 'deft)
(let* ((deft-directory (car orgnv-directories))
       (data (benchmark-run 1 (deft-cache-update-all))))
  (print (apply 'format "Total time:                %s\nGarbage collection events: %s\nTime in GC:                %s"
                data)))

Similar, but now with OrgNV

(let* ((data (benchmark-run 1 (orgnv-build-database))))
  (print (apply 'format "Total time:                %s\nGarbage collection events: %s\nTime in GC:                %s"
                data)))

Sample use:

(orgnv-completing-read (orgnv-build-database))

Configuration