aboutsummaryrefslogtreecommitdiff
path: root/README.org
blob: 993438180cbe3199ab052d0d42dcafa193fa8634 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#+TITLE: gloss — Glossary Lookup with Online-Sourced Selection
#+OPTIONS: toc:nil

[[#features][Features]] | [[#installation][Installation]] | [[#quick-start][Quick Start]] | [[#keybindings][Keybindings]] | [[#configuration][Configuration]] | [[#extending-sources][Extending Sources]] | [[#org-drill][org-drill]] | [[#troubleshooting][Troubleshooting]] | [[#development][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 [[file:docs/design/gloss.org][docs/design/gloss.org]] for the full design and [[file:docs/decisions/][docs/decisions/]] for the recorded ADRs.

* Features
:PROPERTIES:
:CUSTOM_ID: features
:END:

- *Single-file storage.* One git-tracked org file, one =* term= heading 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-c= saves; =C-c C-k= cancels.
- *Edit-in-place.* =C-h g e= jumps to the source org file at the entry's heading. A buffer-local =after-save-hook= refreshes the cache when you save.
- *org-drill export.* Tag every entry as =:drill:= with =:DRILL_CARD_TYPE: twosided= via =C-h g D=. =M-x org-drill= runs 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--sources= alist registry walks fetchers in =gloss-fetch-sources= order; 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
:PROPERTIES:
:CUSTOM_ID: installation
:END:

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+)

#+begin_src emacs-lisp
(unless (package-installed-p 'gloss)
  (package-vc-install "https://github.com/cjennings/gloss"))
(require 'gloss)
(gloss-install-prefix)            ; binds the C-h g sub-map
#+end_src

** use-package with =:vc= (Emacs 29+)

#+begin_src emacs-lisp
(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))
#+end_src

** straight.el

#+begin_src emacs-lisp
(straight-use-package
 '(gloss :type git :host github :repo "cjennings/gloss"))
(require 'gloss)
(gloss-install-prefix)
#+end_src

** Manual installation

#+begin_src bash
git clone https://github.com/cjennings/gloss.git ~/path/to/gloss
#+end_src

Then in your init:

#+begin_src emacs-lisp
(add-to-list 'load-path "~/path/to/gloss")
(require 'gloss)
(gloss-install-prefix)
#+end_src

* Quick Start
:PROPERTIES:
:CUSTOM_ID: quick-start
:END:

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):

1. *Type the term* you want to look up. =gloss-lookup= reads from the minibuffer with =word-at-point= as the default, so just =RET= grabs the word under point.
2. *Cache hit:* a side window opens on the right showing the term and its body.
3. *Cache miss with one definition online:* the package fetches Wiktionary, saves the definition silently with =:source: wiktionary=, and shows it.
4. *Cache miss with multiple definitions:* a picker appears in the minibuffer with each option formatted as ~[wiktionary] ...~. Pick one — that's what gets saved.
5. *Cache miss with no definitions:* the echo area shows ~gloss: no definition found for X~. No save.
6. *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=:

1. Minibuffer prompts =Add term:=. Type the term, =RET=.
2. A side-window buffer opens with the term and a =====~ underline as a read-only header. Point lands in the editable body region underneath.
3. Type the definition.
4. =C-c C-c= saves the entry with =:source: manual= and closes the side window.
5. =C-c C-k= cancels — no save, side window closes.

* Keybindings
:PROPERTIES:
:CUSTOM_ID: keybindings
:END:

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=:

#+begin_src emacs-lisp
(gloss-install-prefix (kbd "C-c g"))
#+end_src

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
:PROPERTIES:
:CUSTOM_ID: configuration
:END:

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:

#+begin_src emacs-lisp
(expand-file-name "gloss.org"
                  (or org-directory user-emacs-directory))
#+end_src

If your =org-directory= is set, the glossary lives next to your other org files. Otherwise it lands under =user-emacs-directory=. To override:

#+begin_src emacs-lisp
(setq gloss-file "~/notes/glossary.org")
#+end_src

The file is created on first save with a =#+TITLE: Glossary= header. Parent directory is created if missing. See [[file:docs/decisions/0001-storage-path-default.org][ADR-1]] for the rationale.

** =gloss-fetch-sources=

The list of online sources to try, in order. Default:

#+begin_src emacs-lisp
(setq gloss-fetch-sources '(wiktionary))
#+end_src

v1 ships with only =wiktionary=. Set to nil to disable all online fetching:

#+begin_src emacs-lisp
(setq gloss-fetch-sources nil)
#+end_src

When more sources land in v2+, you'll be able to reorder them or pick a subset. See [[#extending-sources][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.

#+begin_src emacs-lisp
(setq gloss-fetch-timeout 10)
#+end_src

** =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:

#+begin_src emacs-lisp
(setq gloss-debug t)
#+end_src

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
:PROPERTIES:
:CUSTOM_ID: extending-sources
:END:

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:

#+begin_src emacs-lisp
;; 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...")
#+end_src

Register your fetcher:

#+begin_src emacs-lisp
(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)
#+end_src

The orchestrator walks =gloss-fetch-sources= in order and aggregates each source's result into the user-facing rollup. See [[file:gloss-fetch.el][gloss-fetch.el]] for the Wiktionary fetcher as a worked example.

* org-drill Integration
:PROPERTIES:
:CUSTOM_ID: org-drill
:END:

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:

#+begin_src elisp
M-x package-install RET org-drill RET
#+end_src

Or via =:vc= for the maintained fork:

#+begin_src emacs-lisp
(use-package org-drill
  :vc (:url "https://github.com/cjennings/org-drill" :rev :newest))
#+end_src

See [[file:docs/decisions/0003-drill-direction.org][ADR-3]] for why every export uses =twosided=.

* Troubleshooting
:PROPERTIES:
:CUSTOM_ID: troubleshooting
:END:

** =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
:PROPERTIES:
:CUSTOM_ID: development
:END:

** Test infrastructure

=gloss= uses =Cask= for dependency management and =ert-runner= for tests. Install once:

#+begin_src bash
cask install
#+end_src

Common targets:

#+begin_src bash
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 .elc
#+end_src

The 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 [[file:LICENSE][LICENSE]].