dict

command module
v0.0.0-...-e0a76db Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: GPL-3.0 Imports: 4 Imported by: 0

README

dict

Spell-check and dictionary lookup in the terminal, with Webster's 1913, WordNet, Wiktionary and Wikipedia built in.

Live demo — the whole thing as a wasm terminal, interactive picker included, answering the same words as the installed binary.

dict in the browser

It replaces the shell function

dict() { fzf -q "$1" < /usr/share/dict/words; }

with a matcher that also handles the errors fzf cannot. fzf matches a subsequence, so a transposed pair such as recieve finds nothing at all — above, it is the first result.

Matching

Candidates are ranked in tiers, so an exact hit never loses to a fuzzy one:

  • exact, then case- and accent-folded exact
  • prefix, then subsequence (what fzf does)
  • edit distance, which is what catches transpositions and doubled letters
  • phonetic, for a word heard rather than read

-t labels each match with the tier that found it, which is the quickest way to see why something ranked where it did.

Usage

dict [query] [flags]
flag meaning
-w, --words word list to search (default: the built-in list)
-l, --lang language list under /usr/share/dict, e.g. french
-f, --filter print matches and exit, without the interactive view
-n, --num maximum matches to print (0 for all)
-t, --tier label each match with why it matched
-d, --define print the definition of the best match and exit
-D, --definition-only print only the definition text, no headword or source
-r, --random print a random word and exit
-R, --random-define print a random word with its definition
-p, --pos with -r or -R, draw only a noun, verb, vt, vi, adj, adv, name, person or place
--no-defs hide the definition pane in the interactive view
--reverse prompt at the top and list running down, as with fzf --layout=reverse

Three subcommands report on the data rather than search it: :languages lists other-language word lists installed on the system, :sources shows which word list and dictionaries are in use, and :licenses prints the licenses of the Definitions are looked up in Webster's 1913, WordNet, FOLDOC, the Jargon File and the Elements, in that order. Names and places come next, then Wiktionary for ordinary vocabulary the dictionaries lack, and Wikipedia summaries last, for brands, companies and people. They are consulted only when nothing earlier has the word, and :sources lists the dictionaries in use. A dictd database installed under /usr/share/dictd, /usr/lib/dict or /usr/local/share/dictd takes precedence over the embedded copy.

The remaining subcommands are :unicode (below), :adlib (after that) and :rain.

The picker

The screen is a table — headed index | word | definition, ruled underneath, and with the same vertical rule between every pair of columns.

The margin says which entry each row is: for a word its place in the list, so the list can be used as an index and not only as a filter, and for a character its code point. That is the one thing that tells two characters apart when a terminal draws both as the same smudge, and the thing to type into a program that wants one. In dict :unicode, where every row is a character, the header reads code | character | definition instead.

The list does not end at Zzz. The 41,293 Unicode characters follow the words, so you can scroll off the end of the dictionary straight into U+0000. A character is its own headword — the row is the character, and its name is the first line of the definition, the same shape every other row has. It is searched by name, because a name is the only handle anyone has on a character they cannot type, but the name is no more the thing being looked up than a definition is.

The characters are a tail, ranked below every word however well they match, so no spelling lookup can lose to a character name; they are simply what is there once the words run out. dict -r stays among the words. What Enter gives you is the character itself, ready to paste — dict -f prints the safe drawing of it instead, since a listing should not put a NUL byte in a pipe.

Typing narrows towards a word; erasing widens back out again and leaves the selection on the word you had reached, rather than returning to the top. So a query is also a way of getting somewhere: type enough of a word to land near it, erase it, and go on from there with the arrow keys.

Tab completes the query to the highlighted word, Ctrl-U clears it, and Enter prints it and exits. -f skips the picker entirely, as does a redirected stdout.

Characters

A dictionary answers what a word means. :unicode answers what a character is, which turns out to be the same errand one level down: something is not what it appears to be, and the name is the only handle on it.

$ dict :unicode ’
’  RIGHT SINGLE QUOTATION MARK

code    U+2019
utf-8   E2 80 99
kind    punctuation, final quote
block   General Punctuation

That is the whole of why it is here. A no-break space and a space are the same picture, an en dash and a hyphen are nearly one, and a Cyrillic а is an a that no search will ever find. None of it is visible; all of it is answerable.

With nothing to say it reads standard input and names what arrives, each distinct character once, which is how to find out what is actually in a line that will not behave:

$ printf 'na\xc3\xafve\xc2\xa0caf\xc3\xa9' | dict :unicode
n   U+006E   LATIN SMALL LETTER N
a   U+0061   LATIN SMALL LETTER A
ï   U+00EF   LATIN SMALL LETTER I WITH DIAERESIS
v   U+0076   LATIN SMALL LETTER V
e   U+0065   LATIN SMALL LETTER E
    U+00A0   NO-BREAK SPACE
c   U+0063   LATIN SMALL LETTER C
f   U+0066   LATIN SMALL LETTER F
é   U+00E9   LATIN SMALL LETTER E WITH ACUTE

Nothing it prints can act on the terminal it prints to, which is what makes it safe to point at a file nobody has read: a control character is shown as its Control Pictures glyph — ESC as ␛ — and a combining mark is given a dotted circle to sit on rather than the character before it.

-f reads a file and - asks for standard input outright. The bare pipe above needs neither, because a process can see that its stdin is not a terminal; the shell in the demo page cannot, so - is how to say it there.

An argument can also be a code point or a name, and a name is matched by the same tiered matcher the word list is, so half-remembering it is enough:

dict :unicode U+00A0          # a code point by number
dict :unicode snowman         # ☃, by name
dict :unicode "MULTIPLCATION SIGN"   # × — the misspelling still finds it
dict :unicode -b Emoticons    # a block, listed
dict :unicode --blocks        # the 353 block names

Run with no arguments on a terminal, it opens the same interactive picker the word list uses, restricted to the characters. As there, the row is the character and its name is the definition, and what it prints on the way out is the character — knowing that the one you want is called MULTIPLICATION SIGN is rarely the end of the errand, and having × is.

The root command takes a character too, so the common case needs no subcommand at all — dict ’ and dict U+2019 both answer the above. Only two kinds of argument are read this way: a code point written out, and a single non-ASCII character. A lone ASCII letter stays a word, and so does dict beef, which is a valid hex number and also a thing you can eat.

The whole of Unicode 18.0 is built in, as the dictionaries are: 41,293 names in 260 KB, nothing fetched and nothing installed. The CJK ideographs and Hangul syllables are not stored one by one — their names are generated from their code points, which is how 110,000 characters cost a dozen lines instead of a hundred thousand.

Parts of speech

Every entry says what it is — GCIDE opens with Deserter, n., WordNet stamps each sense n 1: — so -p can ask for a particular one:

dict -r -p verb        # reallocate
dict -r -p adv -n 5    # quickly, demurely, slyly, prolixly, nay
dict -r -p vt          # a verb that takes an object: trammel, envelop, grill
dict -r -p vi          # one that does not: echo, moot, dampen, quarrel
dict -r -p person -n 4 # Edwardo, Spiro, Finley, Jenner
dict -r -p place -n 4  # Srinagar, Essex, Sejong, Mensa

A sixth of the word list — 21,567 of its 124,000 entries — is names, and the list is where the evidence is: it writes Zamenhof and Gaborone with a capital and zealot without. Which kind of name it is comes from the gloss, where the wording is formulaic: the name list says "male given name or surname" and "city in California" outright, and WordNet's people carry a trade or a pair of dates. Whichever the gloss mentions first wins, so Abernathy, surname … Abernathy, city in Texas is a person and Basseterre, the capital of Saint Kitts is a place.

The word that comes back is the headword the entry is filed under, not the inflection that was drawn: a lookup landing on Accumulate, v. t. [imp. & p. p. Accumulated …] yields accumulate, which is the form that can follow "will".

The same entry carries more than its part of speech. GCIDE prints the principal parts of six thousand verbs — Rend (r[e^]nd), v. t. [imp. & p. p. {Rent}; p. pr. & vb. n. {Rending}] — and the plural of three thousand nouns, irregulars included, so those can be read rather than guessed. Regular rules fill in for everything else.

Rain

:rain prints one frame of the matrix code rain with the word list as its alphabet, so each falling stream spells a word. It prints even when piped. --tui animates it full-screen (Esc or Ctrl-C to leave), --speed scales the fall rate (--tui only), --gap puts blank cells between repeats of a word, --glow holds lit any word that appears reading across the rain (--tui only), and --seed reproduces a still exactly.

Ad lib

:adlib is what all of that is for. A frame is who is telling you about it, a clause is what happened, and a clause is a subject and a predicate drawn separately — a subject from a dozen shapes, a predicate from four sets that differ in tense, voice and polarity.

$ dict :adlib -n 8
The pew went on glistering like a hind amplification.
Raoul left a note. It said: 'You cannot swat every insusceptible propellant with the technology that glutted Mckinney.'
According to the restrainer that fudged Emory, one embrocation falsifies while another recants.
In the summer of the hydroponic duel, no earnestness should condition the postural zipper from Badlands frailly.
Okayama never forgave Brandi for the day every dominatrix in Siberian knows that the tart refectory from Pellitory always ravishes another brochure, in the crux at Mohawk.
The semi of Taiyuan has proved that nothing wild can merchandise without the stetson.
'The irony agent from Milwaukee remains complete, whatever the shah drives?' asked Spica's linchpin.
Is it true that the pilgrim hostel would rather strap that liquefaction of Eurasian's than parley?

None of that is a list of sentences. A list wears out faster than it looks: the vocabulary is 124,000 words deep and never repeats a word, so the only thing a reader can recognize twice on a page is the shape, and shapes written out by hand run to a few hundred. Composed instead — [optional] pieces doubling the count, (a|b|c) choices multiplying it — they stop being countable. Over 400 sentences, 399 have a distinct skeleton with the content words blanked out, no three-word opening accounts for more than one line in twenty, and the lengths run from 6 words to 39.

About one line in four asks rather than states. A question frame always holds its clause subordinately — after "that", "if", "now that" — because English keeps declarative word order there, and inverting a clause the generator has already put in the past tense would mean knowing how to unmake it.

Nothing there is chosen for sense, only for shape: an object only goes to a verb that can take one, a plural is the dictionary's plural, and a past tense is the dictionary's past tense. -n prints several and --seed repeats a run exactly.

It reads aloud, which is most of the point:

while :; do s="$(dict :adlib)"; echo "$s"; festival -b "(voice_cmu_us_slt_cg)" "(SayText \"$s\")"; done

Install

go install github.com/0magnet/dict@latest

The demo

make demo builds what is committed under docs/ and takes about two seconds:

make demo     # standard Go, ~2s
make serve    # build, then serve it at http://127.0.0.1:8791

make demo-tinygo builds the same page a third the size, but takes minutes rather than seconds — TinyGo's compile-time interpreter folds the embedded corpus on every build. A demo that is expensive to rebuild is a demo that goes stale, so the fast one is the committed one.

The page is built with -tags dictlite, which embeds the full word list plus FOLDOC, the Jargon File and Elements. Webster's 1913 and WordNet are the other 24.7 MB and are not embedded — they are served beside the page and read over HTTP, one dictzip chunk at a time.

That works because a .dict.dz is deflated in independent chunks with a table of where each one starts, so a definition is a ranged GET of about 58 KB rather than the whole file. Nothing is fetched until something is looked up; the first definition from a given dictionary also pulls its index (1.6 MB for GCIDE, 1.4 MB for WordNet), and after that a lookup is one chunk. dict :sources says which dictionaries were fetched and which are built in.

The upshot is that the page answers the same words, from the same dictionaries, in the same order as the installed binary, having downloaded almost none of the corpus. GitHub Pages serves the repository root so that data/dictd/ is reachable from the page; that is why index.html lives at the top level.

The character table is the exception to all of that: it is embedded in the page build too, because a quarter of a megabyte would cost more in round trips than it saved, and because the answer to "what is this character" is wanted before a download finishes or not at all.

What the page does have to fetch is a font. No system font covers Unicode, so a character table in a browser renders as a screen of empty boxes; fonts/ holds GNU Unifont, which is the only font that comes close, at 892 KB for the Basic Multilingual Plane and 593 KB for the planes above it. It sits last in the terminal's font stack, so ordinary text still draws in the system monospace and only the characters nothing else has fall through to it, and the second file is declared over a unicode-range so a session that never shows an emoji never fetches it.

Licenses

The code is MIT, but the binary as a whole is GPL-3.0-or-later, because GCIDE is embedded in it and GCIDE is GPL-3.0-or-later. LICENSE holds the GPL-3 text. The other bundled data keeps its own license, and dict :licenses prints them. See NOTICE and data/licenses/.

The Unicode Character Database is under the Unicode license, and GNU Unifont, which only the demo page uses, is dual-licensed SIL OFL 1.1 and GPL-2.0-or-later with the font embedding exception. See fonts/LICENSE-unifont.

Another Go dictionary that runs in the terminal and the browser:

  • taskusanakirja — a Finnish–English dictionary in Go, in the terminal and in the browser via WebAssembly

Dependency Graph

Made with goda:

# GOOS=js: the import edges of a wasm program live in js/wasm-tagged
# files and are invisible to a host-context run
GOOS=js GOARCH=wasm go run github.com/loov/goda@latest graph github.com/0magnet/dict/... | dot -Tsvg -o docs/dict-goda-graph.svg

Dependency Graph

Lines of Code

Made with gocloc (excludes vendor/, node_modules/, .git/):

gocloc --not-match-d='(vendor|node_modules|\.git)' .
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Go                              57            930           2329           8979
JavaScript                       1             61             36            478
Markdown                         2             84              0            312
Makefile                         1             30             61            126
YAML                             1              0             16            101
HTML                             1              4             22             76
Bourne Shell                     2             17             47             59
JSON                             1              0              0              8
XML                              1              0              0              4
Plain Text                       1              1              0              3
-------------------------------------------------------------------------------
TOTAL                           68           1127           2511          10146
-------------------------------------------------------------------------------

Documentation

Overview

Command dict looks up how a word is spelled, and what it means.

It replaces the shell function

dict() { fzf -q "$1" < /usr/share/dict/words; }

with a matcher that also handles the errors fzf cannot -- fzf matches a subsequence, so a transposed pair such as "recieve" finds nothing at all -- and shows the definition of whatever is highlighted.

Directories

Path Synopsis
Package cmd adlib.go
Package cmd adlib.go
Package data holds the dictionaries and word list built into the binary.
Package data holds the dictionaries and word list built into the binary.
gen command
Command gen builds data/names.tsv.gz: one-line glosses for the words in the built-in list that no dictionary defines.
Command gen builds data/names.tsv.gz: one-line glosses for the words in the built-in list that no dictionary defines.
gen/unicode command
Command unicode builds data/unicode.tsv.gz: the name, general category and block of every Unicode character.
Command unicode builds data/unicode.tsv.gz: the name, general category and block of every Unicode character.
Package dictdb reads dictd-format dictionaries: a sorted .index of headwords and a .dict.dz body compressed for random access.
Package dictdb reads dictd-format dictionaries: a sorted .index of headwords and a .dict.dz body compressed for random access.
Package match ranks dictionary words against a possibly-misspelled query.
Package match ranks dictionary words against a possibly-misspelled query.
Package unidata is the Unicode character database: what a character is called, what kind of thing it is, and which block it belongs to.
Package unidata is the Unicode character database: what a character is called, what kind of thing it is, and which block it belongs to.
web
app
Package app is the dict command, as a websh applet.
Package app is the dict command, as a websh applet.
fetch
Package fetch supplies the dictionaries the page does not carry.
Package fetch supplies the dictionaries the page does not carry.
term command
Command term is the dict demo: a shell in the page with dict in it.
Command term is the dict demo: a shell in the page with dict in it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL