notes

quotidian manual

First published: 2951 words · 31 lines of code

Overview

quotidian.el shows quotations drawn from a corpus of your own choosing. Quotations live in plain JSON files, so the corpus is yours to extend, and the package reads them once per Emacs session and keeps them in memory. A small corpus of public-domain quotations is bundled with the package (Quotations and copyright).

There are two ways to read them:

  • In the echo area, one quotation at a time, while you wait for something else to finish. The command quotidian-echo writes a quotation where Emacs writes its own messages, without adding it to *Messages*, and is meant to be called from a hook that fires when a long operation starts (Showing a quotation in the echo area).
  • In a review buffer, one quotation to a screen, centred horizontally and vertically in a frame with no other window. The command quotidian opens it, and single keys move through the quotations, copy them, and leave (Reading quotations in a review buffer).

The two entry points share one corpus, one formatter, and three faces, so a quotation looks the same wherever it appears: the text first, then the author and date, then the source on an indented line of its own (Faces).

The package requires Emacs 29.1 or later and depends only on libraries bundled with Emacs: cl-lib, seq, subr-x, and the built-in JSON parser.

The development repository is on GitHub.

Quotations and copyright

The package ships public-domain quotations only. The bundled corpus, data/quotidian-sample.json, holds twenty of them.

A dictionary of quotations that is still under copyright cannot be redistributed here, and none of its contents are. If you want to read from one, transcribe the material into a corpus file of your own, keep that file private, and add it to quotidian-corpus-files (Corpus files). The package treats your files and the bundled one alike.

Installation

The data directory is part of the package

The bundled corpus lives in the data subdirectory of the repository, and the default value of quotidian-corpus-files names it through the symbol bundled, which is resolved relative to the file the package was loaded from (The corpus files).

Elpaca does not copy that directory into the build directory it loads the package from. Like several package managers, it builds a package from the files its recipe names, and the default set is Lisp files and documentation and nothing else. The consequence is not subtle: the default corpus path resolves to a file that is not there, and the package finds no quotations at all. quotidian signals No quotations found; check `quotidian-corpus-files', and quotidian-echo shows nothing, on an installation that is otherwise perfectly healthy.

The recipe must therefore name the data directory explicitly, as the Elpaca snippet below does. If you install with package-vc, or by cloning the repository yourself, the whole repository is on disk already and no files directive is needed.

Manual installation

Clone the repository, keeping its data directory, add it to your load path, and require the package:

(add-to-list 'load-path "/path/to/quotidian/")
(require 'quotidian)
(require 'quotidian-review)

Requiring quotidian-review.el is what defines the quotidian command. Under a package manager no require is needed: all three entry points — quotidian, quotidian-echo and quotidian-reload — are autoloaded, so :defer t is safe and M-x finds each of them before anything has loaded the package. The commands of the review buffer are not autoloaded and do not need to be, since they only ever run in a buffer that quotidian has already opened.

Installation with use-package

If you use use-package, add one of the following snippets to your init file:

;; with elpaca
(use-package quotidian
  :ensure (:host github
           :repo "benthamite/quotidian"
           :files (:defaults "data"))
  :defer t)

;; with vc (Emacs 30 or later)
(use-package quotidian
  :vc (:url "https://github.com/benthamite/quotidian"))

The Elpaca recipe is the one the package is developed against, and its :files entry is there for the reason given above. Under any other package manager that builds from a files directive of its own, add the data directory to that directive in whatever form the manager documents; under one that puts the whole repository on the load path, nothing needs adding.

Corpus files

File format

A corpus file is a JSON array of objects, one object per quotation:

[
  {
    "text": "The unexamined life is not worth living.",
    "author": "Socrates",
    "source": "Plato, Apology 38a",
    "date": "399 BC",
    "tags": ["philosophy"]
  }
]
KeyRequiredTypeMeaning
textyesnon-empty stringThe quotation itself
authoryesnon-empty stringWho said or wrote it
sourcenostringWhere the quotation is from
datenostringWhen it was said or written
tagsnolistCategory strings

Both date and tags are free-form: date is a string, not a number, so 399 BC and 1777 are equally acceptable, but 1777 without the quotation marks is not. The package keeps tags on each quotation but nothing reads it yet; it is there for filtering that has not been written.

Entries and files that are rejected

An entry is dropped, and the rest of the file is kept, when any of the following holds:

  • text is missing, is not a string, or is the empty string.
  • author is missing, is not a string, or is the empty string.
  • source or date is present but is not a string.
  • tags is present but is not a list.

A whole file is skipped, and none of its quotations are read, for any of three reasons:

  • It cannot be read: it is missing, it is not readable, or it fails for some other file error.
  • Its contents are not valid JSON.
  • Its contents are valid JSON but not an array — a bare number or string, or an object.

The last of those is worth a word, because wrapping the array in an object is a natural first attempt:

{ "quotes": [ { "text": "…", "author": "…" } ] }

That file is skipped whole, with the warning Corpus file is a JSON object, not an array, skipping it. The entries inside are never looked at and are not counted as dropped, since there is nothing wrong with them: the outer shape is the mistake. Remove the wrapper and leave the array at the top level.

Warnings and reloading

Neither kind of failure signals an error and neither interrupts what you were doing. Both are logged to *Warnings*, quietly: the buffer is written to but never popped up. A skipped file is logged with its name and the reason; dropped entries are logged as one line per file, giving the number dropped and the file they came from, not the entries themselves. To find the offending entry you must inspect the file.

The corpus is read on first use and then cached for the rest of the session. If you edit a corpus file, or add one to quotidian-corpus-files, call M-x quotidian-reload. It discards the cache, reads the files again, and reports the outcome in the echo area:

Quotidian: 20 quotations, 0 files skipped, 0 entries dropped

Since the counts are the visible half of a log that never shows itself, quotidian-reload is also the way to check on a corpus you have just written: a number of dropped entries greater than zero tells you to read *Warnings*.

User options

All user options belong to the quotidian customization group. You can browse them interactively with M-x customize-group RET quotidian RET.

The corpus files

The user option quotidian-corpus-files is the list of files holding quotations, read in order. An element is either the name of a file or the symbol bundled, which stands for the corpus file shipped with the package, data/quotidian-sample.json (The data directory is part of the package). The default value is the one-element list (bundled).

The symbol is there so that the default holds no absolute path. A path would be written to your custom file the first time you customized the option, and under a package manager that puts the version number in the installation directory — package.el does — the next upgrade would leave it pointing at a directory that no longer exists, and the bundled quotations would quietly disappear. The symbol is resolved afresh every time the corpus is read.

Add your own files to the list to read more than the bundled twenty:

(add-to-list 'quotidian-corpus-files "~/quotations/oxford.json" t)

Every file is read and the quotations are pooled; a quotation is then chosen from the pool at random, so a file ten times the size of another contributes ten times as often. The draw is with replacement, and the package keeps no record of what you have already been shown, so the same quotation can come up twice in a row; on a small corpus it will, often. Removing bundled from the list leaves only your own. Changing the option takes effect after quotidian-reload (Warnings and reloading).

The height of the echo area

The user option quotidian-echo-max-lines is the greatest height, in lines, of a quotation shown in the echo area. The value is a non-negative integer and the default is 4.

A quotation that needs more lines than this is passed over: quotidian-echo draws another one instead of shortening the one it drew (Showing a quotation in the echo area). Raise the option to see long quotations, at the price of a mini-window that jumps up four or five lines while you are working; lower it to 2 to keep the interruption to the size of an ordinary message.

Emacs will not necessarily give you the height you ask for. The echo area cannot grow past what resize-mini-windows and max-mini-window-height allow, and the package uses whichever of the two limits is smaller, so a quotation it decides to show is never cut off. On a short frame, or with resize-mini-windows set to nil — which pins the echo area to a single line — the effective limit is below quotidian-echo-max-lines and short quotations are the only ones you will see.

The width of the review buffer

The user option quotidian-review-fill-column is the width, in columns, at which quotations are wrapped in the review buffer. The value is a non-negative integer and the default is 64.

This is a measure of line length for reading, not a use of the frame’s width: the wrapped block is centred in the window, so a wide frame gives wide margins rather than long lines. The option is defined in quotidian-review.el and has no effect on quotidian-echo, which wraps to the width of the echo area instead.

Faces

Three faces style the parts of a quotation, in the echo area and the review buffer alike.

FaceInherits fromApplies to
quotidian-quotedefaultThe text of the quotation
quotidian-authorfont-lock-string-faceThe author, and the date if any
quotidian-sourceshadowThe source, if any

The defaults inherit rather than set attributes, so they follow your theme. Customize them when you want the attribution to recede further, or the text of the quotation to stand out:

(custom-set-faces
 '(quotidian-quote ((t :slant italic))))

Prefer attributes that leave the character grid alone. Both the line counting that decides whether a quotation fits in the echo area and the centring in the review buffer measure text in columns, so a face that changes the height or the width of a character makes those measurements approximate.

Commands

Showing a quotation in the echo area

The command quotidian-echo shows one quotation, chosen at random, in the echo area. It is written to be called from a hook that fires when you are about to wait — a compilation, a long shell-command, a package update:

(add-hook 'compilation-start-hook (lambda (_process) (quotidian-echo)))

Because it is meant to run unattended, the command is quiet in every failing case rather than signalling. It does nothing at all when the minibuffer is in use, so it cannot overwrite a prompt you are answering; it does nothing when the corpus is empty; and it does nothing when no quotation fits. In all cases it returns nil, which makes it safe on a hook whose return value matters.

Fitting is decided by drawing. The command draws a quotation at random, wraps it to the width of the echo area, and counts the display lines the result occupies — counting a line too wide to fit as the several lines Emacs will wrap it into. If the count is within the limit described under quotidian-echo-max-lines (The height of the echo area), that quotation is shown; otherwise the command draws again, up to twenty times, and then gives up silently. Nothing is ever truncated, and nothing is ever abbreviated with an ellipsis: a quotation either appears whole or does not appear.

The quotation is not written to *Messages*. This is deliberate: a quotation shown once while you waited should not push your real messages out of the log.

Malformed corpus entries cannot make this command signal, because entries are validated when the corpus is read, not when a quotation is displayed (Entries and files that are rejected).

Reading quotations in a review buffer

The command quotidian opens the *quotidian* buffer, deletes the other windows, and draws a quotation in the middle of the frame: wrapped at quotidian-review-fill-column (The width of the review buffer), centred horizontally, and padded from the top so that it sits at the vertical centre. The buffer is in quotidian-review-mode, a mode derived from special-mode; the cursor is hidden, and the key hints occupy the mode line rather than a line of buffer text. Resizing the window redraws the quotation at the new size.

The command signals a user-error if the corpus holds no quotations, since there is nothing to open the buffer for. A corpus that reads as empty under a package manager is usually the missing data directory (The data directory is part of the package).

Quotations you have seen are kept in a history, one per review buffer, so moving forwards and backwards walks the same sequence rather than drawing at random each time. Each fresh draw is nevertheless independent of the ones before it, so the history is not a tour of the corpus without repeats: a quotation can be drawn again while it is still in the history, and on a small corpus it soon will be. Calling quotidian again while the review buffer is already open adds a new quotation to the end of that history and shows it; it does not reset the history, and it does not overwrite the window layout the buffer will restore.

Key bindings

The keymap quotidian-review-mode-map binds five keys:

KeyCommandDescription
nquotidian-review-nextShow the next quotation
SPCquotidian-review-nextThe same, under the thumb
pquotidian-review-previousShow the quotation seen before this one
wquotidian-review-copyCopy the quotation and its attribution
qquotidian-review-quitLeave, restoring the previous window layout

Every other key keeps the binding special-mode gives it. One of those is worth knowing: g runs revert-buffer, and the mode makes that draw the current quotation again rather than fail for want of a file to revert. Use it after changing quotidian-review-fill-column (The width of the review buffer) to see the new width without moving on to another quotation.

The command quotidian-review-next steps forward through the history when there is anything ahead, and draws a new quotation at random when there is not. It signals a user-error if the corpus has become empty since the buffer was opened — if you emptied quotidian-corpus-files and reloaded, for instance — rather than showing an empty screen.

The command quotidian-review-previous steps back one quotation. At the oldest quotation in the history it reports No earlier quotation and stays where it is.

The command quotidian-review-copy copies the current quotation and its attribution to the kill ring, without text properties. What you get is the quotation as quotidian-format builds it (Formatting a quotation) — neither wrapped nor centred, unlike the buffer you are looking at — so that it lands in whatever you paste it into at that text’s own width. With a prefix argument (C-u w) it copies the text of the quotation alone, which is what you want when you are quoting inside your own sentence. It reports Copied either way.

The command quotidian-review-quit kills the review buffer and puts the windows back as they were before the review began. The layout is restored by a kill-buffer-hook, so it is restored equally when you kill the buffer some other way — C-x k, kill-some-buffers, or a session cleanup.

Two cases are deliberately left alone, because in both of them the layout the buffer remembers is not the one in front of you. Killing a review buffer that is not displayed, from the buffer list say, leaves your windows as they are: by then you have moved on to a layout later than the one the review replaced. Killing one that is displayed on a second frame leaves both frames as they are, for that reason and one more — the remembered layout belongs to the frame the review began on, so restoring it would undo that frame’s later work and select the frame as well, taking you off the one you are using. In short, the layout comes back when you end the review on the frame you started it on.

The command refuses, with a user-error, if it is somehow called outside a review buffer.

Reloading the corpus

The command quotidian-reload discards the cached quotations, reads quotidian-corpus-files again, and reports how many quotations were loaded, how many files were skipped, and how many entries were dropped. Call it after editing a corpus file, after adding one to the option, and whenever the number of quotations on offer surprises you (Warnings and reloading).

Functions

Formatting a quotation

The function quotidian-format takes a quotation — a plist with the keys :text and :author, and optionally :source and :date — and returns it as a propertized string, faced as described under Faces. The text comes first; the author, followed by the date if there is one, occupies the next line after an em dash; the source, if there is one, occupies a third line, indented by two spaces:

The unexamined life is not worth living.
— Socrates, 399 BC
  Plato, Apology 38a

The string is unwrapped: the caller decides the width. This is the function to call if you want to show a quotation somewhere the package does not, such as a dashboard or a scratch buffer, and it is what quotidian-review-copy copies (Key bindings).