Features | Installation | Quick Start | Keybindings | Configuration | Extending Sources | org-drill | Troubleshooting | Development
A personal Emacs glossary. C-h g looks up terms in a single git-tracked org file. On a local miss, gloss fetches candidate definitions from Wiktionary and prompts you to pick which one to save — with provenance recorded. The same org file feeds org-drill for spaced-repetition study.
Status
In active development. v1 not yet released. Core features land; first-week shakedown pending. See docs/design/gloss.org for the full design and docs/decisions/ for the recorded ADRs.
Features
- Single-file storage. One git-tracked org file, one
* termheading per entry, alphabetical order maintained on insert. Diff-clean, hand-editable. - Online fallback on cache miss. Looks up missing terms in Wiktionary, presents candidate definitions in a side-buffer picker, saves the one you chose with provenance.
- Auto-save when there's only one definition. No picker shown; the single definition lands in the glossary and the entry is displayed.
- Manual add via a side-window editor with a read-only header (term + underline) and an editable body region.
C-c C-csaves;C-c C-kcancels. - Edit-in-place.
C-h g ejumps to the source org file at the entry's heading. A buffer-localafter-save-hookrefreshes the cache when you save. - org-drill export. Tag every entry as
:drill:with:DRILL_CARD_TYPE: twosidedviaC-h g D.M-x org-drillruns the session unmodified. - Layered architecture.
gloss-core(data) +gloss-fetch(network) +gloss-display(UI) +gloss-drill(drill export) +gloss(orchestration). Each layer mocks at its own boundary. - Pluggable sources for v2+. The
gloss-fetch--sourcesalist registry walks fetchers ingloss-fetch-sourcesorder; v1 ships only Wiktionary. Adding DictionaryAPI.dev or Wordnik later is one alist entry plus one fetcher function. - Diagnostic
*gloss-debug*opt-in log buffer for layer-prefixed event tracing without polluting*Messages*.
Installation
Not on MELPA. gloss is on GitHub at https://github.com/cjennings/gloss and on cjennings.net.
Requirements: Emacs 27.1+, org-mode 9.3+. Online fetching also requires an Emacs built with libxml2 (most distribution builds ship it).
package-vc-install (Emacs 29+)
(unless (package-installed-p 'gloss)
(package-vc-install "https://github.com/cjennings/gloss"))
(require 'gloss)
(gloss-install-prefix) ; binds the C-h g sub-mapuse-package with :vc (Emacs 29+)
(use-package gloss
:vc (:url "https://github.com/cjennings/gloss" :rev :newest)
:commands (gloss-lookup gloss-add gloss-edit
gloss-fetch-online gloss-list-terms gloss-stats
gloss-reload gloss-drill-export gloss-toggle-debug)
:init
(gloss-install-prefix))straight.el
(straight-use-package
'(gloss :type git :host github :repo "cjennings/gloss"))
(require 'gloss)
(gloss-install-prefix)Manual installation
git clone https://github.com/cjennings/gloss.git ~/path/to/glossThen in your init:
(add-to-list 'load-path "~/path/to/gloss")
(require 'gloss)
(gloss-install-prefix)Quick Start
After installing and running (gloss-install-prefix), C-h g becomes the gloss prefix. The single most common command is C-h g g (lookup):
- Type the term you want to look up.
gloss-lookupreads from the minibuffer withword-at-pointas the default, so justRETgrabs the word under point. - Cache hit: a side window opens on the right showing the term and its body.
- Cache miss with one definition online: the package fetches Wiktionary, saves the definition silently with
:source: wiktionary, and shows it. - Cache miss with multiple definitions: a picker appears in the minibuffer with each option formatted as
[wiktionary] .... Pick one — that's what gets saved. - Cache miss with no definitions: the echo area shows
gloss: no definition found for X. No save. - Network failure: the echo area shows
gloss: couldn't reach any source for X. No save. Try again later.
To add a term manually (no online fetch), C-h g a:
- Minibuffer prompts
Add term:. Type the term,RET. - A side-window buffer opens with the term and a =====~ underline as a read-only header. Point lands in the editable body region underneath.
- Type the definition.
C-c C-csaves the entry with:source: manualand closes the side window.C-c C-kcancels — no save, side window closes.
Keybindings
After (gloss-install-prefix), all commands live under C-h g:
| Key | Command | What it does |
|---|---|---|
C-h g g |
gloss-lookup |
Look up a term; fetch online on miss. |
C-h g a |
gloss-add |
Add a term manually (read-only header + editable body). |
C-h g e |
gloss-edit |
Jump to the source org file at the term's heading. |
C-h g o |
gloss-fetch-online |
Force online fetch, bypassing cache. |
C-h g D |
gloss-drill-export |
Tag every entry for org-drill. |
C-h g l |
gloss-list-terms |
Browse glossary terms via completing-read. |
C-h g s |
gloss-stats |
Show total / by-source / drill-tagged / size / mtime. |
C-h g r |
gloss-reload |
Force reload of the cache from disk. |
C-h g d |
gloss-toggle-debug |
Toggle the *gloss-debug* log buffer. |
You can change the prefix by passing an argument to gloss-install-prefix:
(gloss-install-prefix (kbd "C-c g"))Or skip the helper entirely and bind gloss-prefix-map where you want.
In gloss-add-mode (the *gloss-add: TERM* buffer):
| Key | Command | What it does |
|---|---|---|
C-c C-c |
gloss-add-finish |
Save the body and close the buffer. |
C-c C-k |
gloss-add-abort |
Cancel; close without saving. |
In gloss-mode (the *gloss: TERM* side buffer): inherits special-mode, so q dismisses the window.
Configuration
Four defcustoms. All of them have sensible defaults; configure only if you want different behaviour.
gloss-file
The org file that holds the glossary. Default:
(expand-file-name "gloss.org"
(or org-directory user-emacs-directory))If your org-directory is set, the glossary lives next to your other org files. Otherwise it lands under user-emacs-directory. To override:
(setq gloss-file "~/notes/glossary.org")The file is created on first save with a #+TITLE: Glossary header. Parent directory is created if missing. See ADR-1 for the rationale.
gloss-fetch-sources
The list of online sources to try, in order. Default:
(setq gloss-fetch-sources '(wiktionary))v1 ships with only wiktionary. Set to nil to disable all online fetching:
(setq gloss-fetch-sources nil)When more sources land in v2+, you'll be able to reorder them or pick a subset. See Extending Sources below.
gloss-fetch-timeout
Maximum time in seconds to wait for any single source to respond. Default 5. Bump higher on slow connections, lower if you'd rather fail fast.
(setq gloss-fetch-timeout 10)gloss-debug
When non-nil, gloss writes layer-prefixed diagnostic events to a *gloss-debug* buffer. Off by default. Toggle interactively with C-h g d or set:
(setq gloss-debug t)The debug buffer is opt-in for everything beyond user-facing events; *Messages* still shows the things you actually did or asked for.
Extending Sources
For v2+, register an additional fetcher in the source registry. The shape is an alist mapping a source symbol to a fetcher function. Each fetcher takes a TERM string and returns a per-source result plist:
;; Per-source result shape:
;; (:source SYM :status STATUS [:defs (DEF ...)] [:reason STRING])
;;
;; STATUS values:
;; :ok :defs (def1 def2 ...) — success, defs is a non-empty list
;; :no-defs — server reached, term not there
;; :unreachable — DNS, refused, timeout
;; :server-error — HTTP 5xx, malformed JSON, schema mismatch
;; :rate-limited — HTTP 429
;;
;; A definition shape:
;; (:source SYM :text "definition text...")Register your fetcher:
(defun my-dictionaryapi-fetcher (term)
"Fetch TERM from DictionaryAPI.dev. Return per-source result plist."
;; ... call the API, build the plist ...
)
(add-to-list 'gloss-fetch--sources
'(dictionary-api . my-dictionaryapi-fetcher))
(add-to-list 'gloss-fetch-sources 'dictionary-api t)The orchestrator walks gloss-fetch-sources in order and aggregates each source's result into the user-facing rollup. See gloss-fetch.el for the Wiktionary fetcher as a worked example.
org-drill Integration
Once you have a few entries saved, C-h g D tags every top-level heading with :drill: and adds :DRILL_CARD_TYPE: twosided as a property. M-x org-drill then runs the spaced-repetition session against gloss-file unmodified.
The export is idempotent — running it twice in a row touches nothing on the second pass. To remove the tags and properties, M-x gloss-drill-untag-all reverses every entry.
gloss-drill-export signals a user-error if org-drill isn't installed. Install it with:
M-x package-install RET org-drill RET
Or via :vc for the maintained fork:
(use-package org-drill
:vc (:url "https://github.com/cjennings/org-drill" :rev :newest))See ADR-3 for why every export uses twosided.
Troubleshooting
Online fetch requires Emacs built with libxml2
The Wiktionary fetcher uses libxml-parse-html-region to strip HTML from the raw API response. If your Emacs build doesn't include libxml2, online fetching is disabled package-wide for the session.
Manual gloss-add still works without libxml. To get online fetching, install an Emacs build with libxml support — most distribution packages include it.
gloss: couldn't reach any source for TERM
Network failure. Either the host is unreachable (DNS, no connection, firewall), the request timed out (gloss-fetch-timeout), or the server returned an error (5xx, rate limited).
For technical detail, enable gloss-debug and re-run the lookup. The *gloss-debug* buffer records the per-source :reason string (timeout (5s), HTTP 503, etc.). *Messages* keeps the user-facing rollup only.
gloss: glossary file appears corrupt
The org parser failed on gloss-file. The cache is preserved (so existing lookups still work), but recent changes haven't loaded. Open gloss-file, fix the syntax error, then M-x gloss-reload.
The most common cause is a hand edit that broke an entry's :PROPERTIES: drawer. Find the offending heading, fix the drawer, save.
Term not in glossary for a term you saved earlier
Cache and disk are out of sync. Try M-x gloss-reload first — that re-reads from disk. If it still misses, the term may have been hand-edited under a different heading; check gloss-file directly with M-x gloss-edit.
The cache auto-refreshes on every lookup if gloss-file's mtime advances, so out-of-band edits via git pull or another Emacs session are picked up automatically. gloss-reload is the manual escape hatch.
Side window won't dismiss
In a *gloss: TERM* buffer, q calls quit-window (inherited from special-mode). If your config rebinds q, the window won't dismiss the same way. Use C-x 1 as a fallback, or rebind q back in gloss-mode-map.
Development
Test infrastructure
gloss uses Cask for dependency management and ert-runner for tests. Install once:
cask installCommon targets:
make help # List all targets
make test # Run the full suite
make test-file FILE=tests/test-foo.el # One file
make test-name TEST=pattern # By test name pattern
make validate-parens # check-parens on every .el
make compile # Byte-compile package files
make lint # elisp-lint pass
make clean # Remove .elcThe current suite is 129 tests across the four layers and the orchestration layer. The pure helpers (gloss--orchestrate-fetch-result, gloss-display--format-candidate, gloss-display--render-entry, gloss--add-finish-internal, gloss--stats-text) get full Normal/Boundary/Error coverage. Mode-glue gets smoke tests only — Emacs already tests its own prompts and major-mode mechanics.
Contributing
Pull requests welcome. Match the existing test style: per-function file, three category coverage, real production code via require (never inlined). Mock at boundaries (url-retrieve-synchronously, completing-read, display-buffer); never mock internal helpers.
For non-trivial changes, open an issue first to discuss the design.
License
GPL-3.0-or-later. See LICENSE.
