DictShelf (1)

Download OpenAPI specification:

URL: https://github.com/DMSHLabs/DictShelf License: GPL-3.0-or-later

A local dictionary server for MDX/MDD, StarDict, Slob, DSL and BGL files.

WuWeiDict serves your own dictionary files over HTTP, on your own machine.

Streaming. /api/dicts, /api/search and /api/rescan answer with NDJSON: one JSON object per line, flushed the moment it is ready. Read them line by line; do not wait for the body to end. That is the whole point - the first dictionary's results arrive while the slowest one is still opening. Every stream opens with a begin line and closes with an end line.

/api/ingest streams Server-Sent Events instead, because it reports progress rather than results.

Switches. Every boolean query parameter reads the same spellings: off for 0, false, no, off or an empty value, on for anything else.

Two surfaces. Three read-only endpoints answer a browser extension cross-origin: /api/dicts, /api/search and /res/. Everything else is same-origin, is marked internal below, and may change between releases.

An access key, when the server is reachable. On the default 127.0.0.1 bind there is nothing to authenticate and no key is asked for. On any other address - and Go's wildcard listen is dual-stack, so 0.0.0.0 includes every globally routable IPv6 address the machine holds - AUTH defaults to requiring one on every endpoint EXCEPT the three read-only ones above, which stay open to the extensions already granted them. Send it as Authorization: Bearer <key>, or open the link dictshelf token prints once and let the browser keep the cookie it sets. Without it: 401, with {"auth":"required"}. AUTH=off serves everything to anyone who can reach the port.

Address it by address. Every request must carry a Host of an IP literal or localhost; any other name is answered 421 Misdirected Request unless TRUSTED_HOSTS lists it. This is what stops a web page from re-pointing its own domain at your machine (DNS rebinding) and reading the API as same-origin, which no CORS rule can prevent.

And from this origin. A browser states who started a request in Sec-Fetch-Site. Anything it marks cross-site is answered 403 unless it addresses one of the three read-only endpoints above (or their preflight), or is a top-level navigation to a page rather than to /api/. This closes the state-changing GET: GET /api/rescan and GET /api/setup?save=1 are otherwise reachable from an <img> tag on any origin, carrying the cookie, and CORS never blocks a request - only the reading of its response. Clients that send no fetch metadata (curl, the Android shell, anything that is not a browser) are unaffected.

search

The client API. Read-only, and reachable from a browser extension.

Search every dictionary, streaming

Queries the selected dictionaries concurrently and streams one line per dictionary as it finishes. Lines arrive in completion order, never in slot order: each hit carries i, its index in the begin line's slots array.

A dictionary that fails, that cannot serve the requested mode, or whose index is not ready reports that on its own line. None of those is a request failure, and the other dictionaries still answer.

A search runs until it finishes or the reader stops it: there is no clock on it.

query Parameters
q
required
string
Example: q=flight

The search text. Blank is a 400.

mode
string
Default: "prefix"
Enum: "exact" "prefix" "contains" "fts" "wildcards" "fuzzy"

exact · prefix · contains (trigram substring over folded headwords) · fts (full text) · wildcards (whole headword, with ? for one character and * for zero or more). Wildcard searches use the optional Contains index to narrow candidates when available; otherwise they scan headwords and broad patterns can be slow. Exact, prefix and contains keep their regular query behavior; full-text does not accept wildcard patterns. Wildcard queries are not lemmatized. The previous mask mode name remains accepted. fuzzy is accepted as a legacy alias and now means prefix.

contains and fts need the dictionary to carry that index; ask /api/dicts for caps first, and expect skipped if you do not.

A multi-word fts query is a PHRASE first. It is retried as a proximity and then as separate words only when the stricter reading returns nothing, and the hit says which reading answered (rung). Quotes and the operators AND, OR, NOT and NEAR(a b, n) are parsed when present, and an explicit query like "faux ami" NOT linguistics is answered exactly as written, without relaxation. Anything unparseable is read as plain words rather than rejected.

hl
boolean
Default: false

Mark the query's words inside each article body, wrapped in <mark class="wu-hl">. Read ONLY in mode=fts, where the reason a result matched is invisible otherwise; ignored in every other mode, where the match is the headword itself.

Off unless you ask, so an article you fetch is the dictionary's own bytes by default. DictShelf's own UI sends hl=1 from a remembered preference, which is why the default here is off and the default there is on. Marking is prefix-based and whole-token, matching what the full-text index matched, and is capped at 500 marks per article

  • past the cap the article is still returned complete, just no longer marked. format=text drops the marks with all other markup.
returnCacheID
string

Optional UI navigation snapshot id. Normal searches with this value retain their completed event stream in a temporary SQLite database; cacheAction=returnReplay&cacheID=<id> replays it when returning from Browse. Snapshots are process-local and are not an API result cache for clients that need persistence across restarts.

returnCacheStorage
string
Enum: "memory" "disk"

Snapshot storage selected by the UI (memory or disk); omitted requests use the saved System Settings value.

format
string
Default: "raw"
Enum: "raw" "clean" "text"

raw is the dictionary's own HTML. clean keeps structure, emphasis and media and drops scripts, styles and presentation, at roughly half the bytes; it also rewrites root-absolute links such as /res/… into absolute URLs, so the payload works inside a page served from somewhere else. text is markup-free, roughly 0.4.

Ask for clean unless you render the article with the dictionary's own stylesheet.

dict
string
Default: "all"
Example: dict=oxford,webster

all, one dictionary id, or a comma-separated list of ids in the order you want them back. Ids come from /api/dicts. A list where no id resolves is a 404; unknown ids inside a resolving list are skipped.

n
integer >= 1
Default: 20

Maximum results per dictionary.

Responses

Request samples

curl -sN 'http://127.0.0.1:6888/api/search?q=flight&mode=prefix&format=clean&n=5'

# -N is the whole point: without it curl buffers, and the lines that
# were sent one at a time arrive together at the end.

Response samples

Content type
application/x-ndjson
"{\"t\":\"begin\",\"i\":0,\"slots\":[{\"dict\":\"oxford\",\"name\":\"oxford\"},{\"dict\":\"webster\",\"name\":\"webster\"}]}\n{\"t\":\"hit\",\"i\":1,\"dict\":\"webster\",\"name\":\"Webster's Revised Unabridged\",\"results\":[{\"Headword\":\"flight\",\"Body\":\"<div>…</div>\"}]}\n{\"t\":\"hit\",\"i\":0,\"dict\":\"oxford\",\"name\":\"Oxford Advanced Learner's\",\"results\":[]}\n{\"t\":\"end\",\"i\":0}\n"

CORS preflight

Answers 204 with the extension grant. The extension's own requests are CORS-simple and never preflight; this exists so a future one does not meet a 405 that explains nothing.

Responses

List every dictionary, streaming

With progress=1, checking events report how many dictionary files have been checked before row resolution begins. Then begin carries the total from registry ids, and dict rows follow in completion order.

end carries etag when the answer may be kept. Send it back as If-None-Match and an unchanged list is a 304 with no body, decided without opening any dictionary. There is no ETag header: whether an answer may be kept is known only after its last row.

query Parameters
progress
boolean
Default: false

Emit file-check progress before the dictionary rows when no If-None-Match validator is supplied.

header Parameters
If-None-Match
string

The etag of an earlier answer's end line.

Responses

Request samples

curl -sN 'http://127.0.0.1:6888/api/dicts'

# just the ids, which is what /api/search wants
curl -sN 'http://127.0.0.1:6888/api/dicts' | jq -r 'select(.t=="dict") | .dict.id'

# has anything changed since the answer that ended with this etag?
curl -s -o /dev/null -w '%{http_code}\n' -H 'If-None-Match: "ac4fd9fecabd8ec4cf544212"' \
  'http://127.0.0.1:6888/api/dicts'

Response samples

Content type
application/x-ndjson
"{\"t\":\"begin\",\"total\":2}\n{\"t\":\"dict\",\"dict\":{\"id\":\"oxford\",\"name\":\"Oxford Advanced Learner's\",\"format\":\"mdx\",\"path\":\"/Users/me/Dicts/oald.mdx\",\"entries\":184000,\"caps\":{\"Exact\":true,\"Prefix\":true,\"Contains\":false,\"FTS\":false}}}\n{\"t\":\"end\",\"etag\":\"\\\"ac4fd9fecabd8ec4cf544212\\\"\"}\n"

CORS preflight

Responses

One file from inside a dictionary

Images, stylesheets, scripts and audio, as the article references them.

path is the whole remainder of the URL and may contain slashes, which an OpenAPI path parameter cannot express: send it unencoded, exactly as the article asks for it.

Speex audio is transcoded to WAV and answered as audio/wav. A .spx that cannot be transcoded, or one supplied in a res/ override folder, is answered raw as audio/ogg. Files from a res/ folder are served uncached, so an edit takes effect on reload.

path Parameters
dict
required
string

Dictionary id, from /api/dicts.

path
required
string
Example: audio/flight__gb.mp3

The resource path inside the dictionary, subfolders included.

Responses

Request samples

# the path after the id is the article's own path, slashes and all
curl -s 'http://127.0.0.1:6888/res/oxford/audio/flight__gb.mp3' -o flight.mp3

Response samples

Content type
application/json
{
  • "error": "string"
}

CORS preflight

path Parameters
dict
required
string
path
required
string

Responses

This document

The contract of the running build, served by it.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/openapi.yaml' -o openapi.yaml

internal

Same-origin endpoints that serve the web page and the platform shell. Documented for completeness; not a stable contract.

Start or prioritize a dictionary's required headword index

Request Body schema: application/json
required
dict
required
string
explicit
required
boolean
priority
boolean

Responses

Request samples

Content type
application/json
{
  • "dict": "string",
  • "explicit": true,
  • "priority": true
}

Add opt-in Browse navigation, page resources, settings bootstrap, dictionary picker, Wildcards cache, memory, or progress timings to the system log

Request Body schema: application/json
required
event
required
string
stage
string

Navigation stage when event is article_transition or browse_navigation

resource
string

Same-origin asset path when event is navigation_resource

dict
string
page
integer
startedAt
integer <int64>

Client navigation start time in Unix milliseconds

elapsedMs
integer

Elapsed milliseconds since navigation start

durationMs
integer

Duration of the reported stage in milliseconds

words
integer
pageBytes
integer

Approximate JavaScript heap size of one cached page

pageJsonBytes
integer

UTF-8 JSON size of one cached page

cachePages
integer
cacheBytes
integer

Approximate JavaScript heap size of cached pages for this dictionary

cacheJsonBytes
integer

UTF-8 JSON size of cached pages for this dictionary

cacheStoredPages
integer

Number of page records persisted in session storage

cacheStoredBytes
integer

UTF-8 JSON size of persisted page records

totalPages
integer
estimatedAllPagesBytes
integer

Extrapolated JavaScript heap size if every page were cached

letters
integer
bytes
integer
readMs
integer
fetchMs
integer
totalMs
integer
wordsMs
integer
stripBuildMs
integer
stripMarkMs
integer
status
integer
restored
boolean
streamAlphabet
boolean
active
integer

Wildcards dictionaries currently scanning

completed
integer

Wildcards dictionaries with finished scans

dictionaries
integer

Wildcards dictionaries in this search

Responses

Request samples

Content type
application/json
{
  • "event": "string",
  • "stage": "string",
  • "resource": "string",
  • "dict": "string",
  • "page": 0,
  • "startedAt": 0,
  • "elapsedMs": 0,
  • "durationMs": 0,
  • "words": 0,
  • "pageBytes": 0,
  • "pageJsonBytes": 0,
  • "cachePages": 0,
  • "cacheBytes": 0,
  • "cacheJsonBytes": 0,
  • "cacheStoredPages": 0,
  • "cacheStoredBytes": 0,
  • "totalPages": 0,
  • "estimatedAllPagesBytes": 0,
  • "letters": 0,
  • "bytes": 0,
  • "readMs": 0,
  • "fetchMs": 0,
  • "totalMs": 0,
  • "wordsMs": 0,
  • "stripBuildMs": 0,
  • "stripMarkMs": 0,
  • "status": 0,
  • "restored": true,
  • "streamAlphabet": true,
  • "active": 0,
  • "completed": 0,
  • "dictionaries": 0
}

Clear selected dictionary indexes and obsolete prepared data

Requires local access or ALLOW_REMOTE_DELETE. Source dictionaries and their resources are preserved. Index rebuilds atomically replace the main index, retaining optional indexes unless selected for removal. Obsolete cleanup includes unused prepared folders regardless of keep markers, duplicates, incomplete indexes, abandoned ingest files and packed media. Obsolete active indexes are refreshed from their sources. Database dictionaries supplied as sources are never cleared.

Request Body schema: application/json
required
index
boolean
Default: false
contains
boolean
Default: false
fullText
boolean
Default: false
obsolete
boolean
Default: false

Responses

Request samples

Content type
application/json
{
  • "index": false,
  • "contains": false,
  • "fullText": false,
  • "obsolete": false
}

Response samples

Content type
application/json
{
  • "failed": [
    ]
}

Rescan folders and apply the selected index actions

New dictionaries receive a main index and only the selected optional indexes, respecting configured DSL variants. Existing optional indexes are updated only when already present; deletion cancels pending creation. Keep preserves existing feature presence. Cleanup always removes unused or disabled prepared indexes and legacy packed media. Sources survive.

Request Body schema: application/json
required
required
object
required
object

Responses

Request samples

Content type
application/json
{
  • "new": {
    },
  • "existing": {
    }
}

Response samples

Content type
application/json
{
  • "failed": [
    ]
}

Finish the current dictionary operation and stop the remaining rescan queue

Stop is cooperative. The active dictionary reaches a consistent state, then unstarted dictionaries are skipped and cleanup and registry refresh finish.

Responses

Re-scan dictionary folders or read index-update status

With status=1, returns live progress for the server-owned index job. Otherwise reads the dictionary folders again, warms the registry, and answers exactly as /api/dicts does. Removes unused prepared folders (including previously kept caches), abandoned indexes and packed media. Source files and both discovered DSL variants are preserved. Requires removal permission, like POST /api/clear-database.

query Parameters
status
string
Value: "1"

Return current index-update status instead of rescanning.

Responses

Request samples

# picks up files added to the dictionary folders, then lists
curl -sN 'http://127.0.0.1:6888/api/rescan' | head -1

Response samples

Content type
application/x-ndjson
Example
{
  • "t": "DictsBegin",
  • "total": 0,
  • "groupsProblems": 0
}

Put an editable copy of the built-in howto into the import folder

Copies the built-in guide (wudict-howto.wudict.md and its wudict-howto.wudict.files/) into the folder imports go to - the first dictionary folder - then rescans. The copy is listed in place of the built-in guide. An existing copy is never overwritten. With restore=1 it instead brings the built-in guide back after the user removed it (DELETE /api/library?dict=wudict-howto, which records the removal so it is not written back at the next start).

query Parameters
restore
string

Bring the removed built-in guide back instead of copying it. A switch.

Responses

Response samples

Content type
application/json
{
  • "path": "string"
}

Prepare a dictionary, or add an index to it

Brings one dictionary to the feature state you ask for, streaming Server-Sent Events:

event: progress   data: {"done":N,"total":M}
event: done       data: {…a /api/dicts row…}
event: error      data: {"error":"…"}

Each switch is a state, not a verb: a parameter you leave out keeps whatever that feature is now, so toggling one thing cannot strip another, and a request naming none prepares an unprepared dictionary with its headword index only. Progress is emitted at most every 200 ms.

The change runs as the dictionary's job on the server, not inside the request: a client that disconnects loses only the stream, and the work goes on. While a change to a dictionary runs, its /api/dicts row carries job, and a request for that dictionary follows the running change instead of starting another - what it asked for is not queued, and the done it receives carries what the dictionary now has.

query Parameters
dict
required
string

Dictionary id.

contains
string

Trigram substring index over headwords. A switch.

fts
string

Full-text index over article text - the largest one. A switch.

media
string

Pack the dictionary's media into media.db. A switch.

Responses

Request samples

# add the substring index to one dictionary and watch it build
curl -sN 'http://127.0.0.1:6888/api/ingest?dict=oxford&contains=1'

# leave a switch out and it keeps its current state; this only packs media
curl -sN 'http://127.0.0.1:6888/api/ingest?dict=oxford&media=1'

Response samples

Content type
application/json
{
  • "error": "string"
}

Finish the current dictionary index change and report it as stopped

query Parameters
dict
required
string

Responses

List the prepared dictionaries on disk

Everything in the library folder, whether or not it is currently enrolled. Listing is not consent: reading this never adds anything to the registry.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/library' | jq '.dir, .count'

Response samples

Content type
application/json
{
  • "dir": "string",
  • "count": 0,
  • "useCached": true,
  • "entries": [
    ]
}

Delete one dictionary

Removes the prepared folder, the original files, or both. Both default to on, which is "remove this dictionary".

Irreversible. Files are unlinked, not moved to a Trash or Recycle Bin, and nothing here can be undone.

From the machine running DictShelf this is always allowed - it is your library and your disk. From a browser on another machine it follows ALLOW_REMOTE_DELETE (default off); with that off, the remote caller gets a 403 and canDelete: false in GET /api/config.

query Parameters
dict
required
string

Dictionary id.

prepared
string
Default: "1"

Delete the prepared library folder; off keeps it. A switch, on when absent.

source
string
Default: "1"

Delete the original dictionary files; off keeps them. A switch, on when absent.

Responses

Request samples

# remove one dictionary: prepared folder and original files
curl -s -X DELETE 'http://127.0.0.1:6888/api/library?dict=oxford'

# free the indexes only, keeping the original file
curl -s -X DELETE 'http://127.0.0.1:6888/api/library?dict=oxford&source=0'

# There is no dry run here - `dictshelf rm` is the one that previews.

Response samples

Content type
application/json
{
  • "name": "string",
  • "folder": "string",
  • "sources": [
    ],
  • "freed": 0,
  • "gone": true,
  • "note": "string"
}

List prepared dictionaries whose source file is gone

Every prepared folder in the library whose recorded source file is no longer on disk, whatever the reason (deleted, renamed, on a drive that is not connected), except those kept on purpose. A source that was only moved inside a dictionary folder is not listed: a rescan re-links the folder to it. The built-in guide is never listed.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/orphans' | jq '.orphans[].name, .size'

Response samples

Content type
application/json
{
  • "orphans": [
    ],
  • "size": 0
}

Delete or keep orphaned prepared dictionaries

Deletes the prepared folders named in delete and marks those in keep as standalone (keep = standalone in the folder's info.txt), after which they are never listed again. Folders are named by Orphan.folder, never by path, and each is checked again when it is acted on: one that is no longer an orphan is reported under failed. A folder named twice, or in both lists, fails too. Body is capped at 1 MiB.

Deleting is irreversible. Files are unlinked, not moved to a Trash or Recycle Bin. Allowed on the machine running DictShelf; from another machine only with ALLOW_REMOTE_DELETE, which also governs keep.

Request Body schema: application/json
required
delete
Array of strings

Folder names to delete.

keep
Array of strings

Folder names to keep for good.

Responses

Request samples

Content type
application/json
{
  • "delete": [
    ],
  • "keep": [
    ]
}

Response samples

Content type
application/json
{
  • "deleted": [
    ],
  • "kept": [
    ],
  • "failed": [
    ],
  • "freed": 0
}

List installable and installed lemma data

One row per language, merging the published catalogue (LEMMA_URL) with what is present in LEMMA_DIR, plus the state of any download running right now. Always answers 200: what is installed is the half of the answer that needs no network, and an unreachable catalogue is reported in error rather than by failing.

query Parameters
refresh
string

Re-read the catalogue instead of using the cached copy. The catalogue is otherwise cached for ten minutes, so a page polling during a download does not ask its host once a second.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/lemmas' |
  jq -r '.languages[] | select(.installed) | .code'

Response samples

Content type
application/json
{
  • "dir": "string",
  • "url": "string",
  • "enabled": true,
  • "cacheSize": 0,
  • "attribution": "string",
  • "error": "string",
  • "shadowed": [
    ],
  • "languages": [
    ]
}

Install one language's lemma data

Starts a download and returns at once; follow it with GET /api/lemmas. The download runs in the server, not in this request, so it survives the page being reloaded, closed, or rotated away on Android.

The catalogue is never taken from the caller: only the configured LEMMA_URL is fetched. A second call for a language already downloading joins the running download instead of starting another.

On success the language is searchable immediately - no restart, because the lemma folder is re-indexed in place.

query Parameters
code
required
string

Language code or English name ("pl", "polish").

Responses

Request samples

curl -s -X POST 'http://127.0.0.1:6888/api/lemmas?code=pl'

Response samples

Content type
application/json
{
  • "code": "string",
  • "state": "string"
}

Delete one language's lemma data

Removes every file in LEMMA_DIR supplying this language, not only the one in use, and re-indexes the folder. Deleting English falls back to the pack compiled into the binary rather than losing the language. Unlike dictionary removal this is offered on every platform: these are files DictShelf downloaded, not files the user brought.

query Parameters
code
required
string

Language code or English name.

Responses

Request samples

curl -s -X DELETE 'http://127.0.0.1:6888/api/lemmas?code=pl'

Response samples

Content type
application/json
{
  • "code": "string",
  • "builtin": true,
  • "removed": [
    ]
}

Import dictionaries from an archive

Two acts on one route. Without confirm it ACQUIRES an archive and reports the dictionaries inside it, decompressing nothing: a zip is answered from its central directory and a 7z from its header, so listing a multi-gigabyte bundle costs a seek. With confirm it extracts the ones the caller chose.

A plain dictionary file is read too - an .mdx, an .ifo, a .dsl, a .slob, a .bgl - and so are the companions lying beside it in the same folder, because that is the same grouping rule the library uses. Nothing is unpacked in that case; the files are copied into place.

.zip and .7z are read. A 7z is decoded strictly in archive order on one goroutine: a 7z folder is a single stream holding several files end to end, so that order is the difference between reading it once and decompressing it again per file. A password-protected archive is refused rather than half-read.

The archive arrives either as a path already on disk (path, used by the Android shell, which hands over a download without copying it) or as the request body. A streamed body is spooled inside the destination folder, never into the temporary directory: on Android that is a small cache partition, and on every platform it is a different filesystem, which would make the install a second full copy instead of a rename.

One import runs at a time, everywhere. Extraction outlives this request and is followed with GET /api/intake, for the same reason a lemma download is: on Android the page dies whenever the device rotates.

A url is fetched by the server instead, which is how a phone imports: the shell has no networking of its own, and a download that lived in the page would die with it. It answers 202 at once and downloads behind the same poll, sniffing when it lands. Any site, unless IMPORT_URL_HOSTS restricts it; https only and no address on the local network, unless IMPORT_INSECURE lifts both; an unfinished download is resumed by the next attempt.

A url may also name SEVERAL dictionaries: a web folder page (every link on it to a dictionary, a companion or an archive), a .txt list (one file name or link per line, the first # line its title), or several links pasted at once - and a share link https://legbehindneck.com/wudict#<link> stands for the link after # (#/dict/x/ is short for a path on that site), or for several links after # separated by commas. A Google Drive file link (drive.google.com/file/d/<id>/view) is fetched from Drive's download address, and named by Drive's answer. Such a job is a collection: ready arrives with nothing downloaded, one candidate per dictionary sized and dated from the site's own headers, and each dictionary's media offered in extras. Confirming downloads and installs each chosen dictionary in turn; one that fails is named in error and the job still ends done with the rest installed.

A dictionary the library already holds is reported as such on the candidate (existing, or elsewhere for one in another configured folder; unchanged when the files are the same) and, when the caller picks it, is REPLACED - its folder, or its files in place - rather than installed a second time. Not picking it is skipping it. Nothing is ever installed or downloaded under a numbered name.

A downloaded plain file has its companions LOOKED FOR at the sibling URLs the format tables name - oxford.mdd, oxford.1.mdd, and so on, stopping at the first that is not there. What is found is reported in extras and downloaded only when the caller asks for it by index: a companion can be several hundred megabytes of somebody's phone data, so it is offered on the screen that asks which dictionaries to install and never taken on the strength of the probe.

The source file's fate follows IMPORT_KEEP; keep answers it only when that is ask. A FAILED import always keeps the source, which is policy and not a setting - deleting somebody's only copy after an import that did not work cannot be undone.

query Parameters
path
string

An archive already on this machine, read in place. Mutually exclusive with a request body.

url
string <uri>

An https link the server downloads and then sniffs. Mutually exclusive with path and with a request body.

name
string

File name for a streamed body, used for the extension and for what the page displays. The extension decides which reader is used, so a 7z body must be named .7z. Defaults to archive.zip.

confirm
string

Any value installs the sniffed job instead of starting a new one.

pick
string

Comma-separated indexes into candidates, for a caller with no JSON body to send. Used only with confirm.

keep
string
Enum: "0" "1"

Whether the caller's own source file survives a SUCCESSFUL import. Ignored unless IMPORT_KEEP is ask. Used only with confirm.

extras
string

Comma-separated indexes into extras: the companion files to download before installing. For a caller with no JSON body to send. Used only with confirm.

Request Body schema:
optional

The archive itself, streamed. Absent when path is given, and when confirming - where the body is instead the selection.

string <binary>

Responses

Request samples

Content type
No sample

Response samples

Content type
application/json
{
  • "id": "string",
  • "state": "",
  • "source": "string",
  • "host": "string",
  • "dir": "string",
  • "keep": "ask",
  • "candidates": [
    ],
  • "extras": [
    ],
  • "installed": [
    ],
  • "done": 0,
  • "total": 0,
  • "error": "string",
  • "collection": true
}

What the current import is doing

The poll. Always 200: an empty state means nothing is happening, which is an answer the page acts on and must not have to tell apart from a request that did not arrive. done and total are bytes.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/intake' | jq -r '.state'

Response samples

Content type
application/json
{
  • "id": "string",
  • "state": "",
  • "source": "string",
  • "host": "string",
  • "dir": "string",
  • "keep": "ask",
  • "candidates": [
    ],
  • "extras": [
    ],
  • "installed": [
    ],
  • "done": 0,
  • "total": 0,
  • "error": "string",
  • "collection": true
}

Cancel the current import

Stops an extraction and forgets a sniffed archive that was never confirmed. Everything staged is removed; dictionaries already renamed into the library stay, because they are whole. Always 200 - cancelling nothing is not a failure. With id, only that job is cancelled, and only while it is still the current one: a client releasing its own job never cancels one another client started since. A download that nobody has confirmed and nobody has polled for 75 seconds is not "running" any more: a new import replaces it instead of being refused. A confirmed import is never replaced.

query Parameters
id
string

The job to cancel (id from the job). Omit to cancel whatever is current.

Responses

Request samples

curl -s -X DELETE 'http://127.0.0.1:6888/api/intake'

Response samples

Content type
application/json
{
  • "id": "string",
  • "state": "",
  • "source": "string",
  • "host": "string",
  • "dir": "string",
  • "keep": "ask",
  • "candidates": [
    ],
  • "extras": [
    ],
  • "installed": [
    ],
  • "done": 0,
  • "total": 0,
  • "error": "string",
  • "collection": true
}

Rebuild every outdated dictionary

Rebuilds each dictionary /api/dicts reports as outdated - prepared by an older build, from a source edited since, or unreadable by this one - with the indexes it already has. A packed media.db the rebuild leaves unpaired is packed again. Dictionaries that are current are not touched, and one whose original files are gone is never outdated, since nothing can rebuild it.

Never started by the server itself: on a large library this is hours of work, so an upgrade only marks the library outdated and waits for this request. It runs in the background indexing lane, one dictionary at a time, and outlives the request; follow it with GET /api/reindex. One job at a time: while one runs, this answers its status instead of starting another.

Responses

Request samples

curl -s -X POST 'http://127.0.0.1:6888/api/reindex'

Response samples

Content type
application/json
{
  • "running": true,
  • "active": 0,
  • "activeDicts": [
    ],
  • "done": 0,
  • "total": 0,
  • "current": "string",
  • "currentDone": 0,
  • "currentTotal": 0,
  • "failed": [
    ],
  • "canceled": true
}

What the rebuild is doing

The poll. Always 200; running: false with total: 0 means no rebuild has run in this process.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/reindex' | jq '{done, total, current}'

Response samples

Content type
application/json
{
  • "running": true,
  • "active": 0,
  • "activeDicts": [
    ],
  • "done": 0,
  • "total": 0,
  • "current": "string",
  • "currentDone": 0,
  • "currentTotal": 0,
  • "failed": [
    ],
  • "canceled": true
}

Stop the rebuild

Stops the job after the dictionary it is rebuilding now. What is finished stays finished; each rebuild replaces the old data only when it completes, so nothing is left half-built. Always 200.

Responses

Request samples

curl -s -X DELETE 'http://127.0.0.1:6888/api/reindex'

Response samples

Content type
application/json
{
  • "running": true,
  • "active": 0,
  • "activeDicts": [
    ],
  • "done": 0,
  • "total": 0,
  • "current": "string",
  • "currentDone": 0,
  • "currentTotal": 0,
  • "failed": [
    ],
  • "canceled": true
}

Validate a dictionary folder, and optionally save it

Drives the first-run page. Without save it only reports what the folder holds, which is what live typing needs. With save=1 it switches the registry over and writes DICT_DIR. Repeat path for several folders; found is then the deduplicated total across all of them, because overlapping folders must not be counted twice. Saving runs as a server-owned job and survives closing the page. With async=1, poll /api/index-work?id=folder-setup for progress and the retained result. migration=status reports an optional Windows first-run import choice; migration=import|copy|move applies that choice before folders are saved.

query Parameters
path
string

A dictionary folder. Repeatable. ~ is expanded.

save
string

On persists the folders to the config file. A switch.

async
string
Enum: "0" "1"

With save=1, return immediately after starting the job.

useCached
string
Enum: "0" "1"

On enrolls the previously prepared dictionaries and persists USE_CACHED; off turns that off; absent leaves it alone. A switch.

migration
string
Enum: "status" "import" "copy" "move"

Check or apply the first-run Windows data migration choice.

Responses

Request samples

# what this folder holds, without changing anything
curl -s 'http://127.0.0.1:6888/api/setup?path=~/Dictionaries'

# adopt two folders and write them to the config file
curl -s 'http://127.0.0.1:6888/api/setup?path=~/Dictionaries&path=/Volumes/Ext/Dicts&save=1'

Response samples

Content type
application/json
{
  • "path": "string",
  • "found": 0,
  • "dirs": [
    ],
  • "saved": true,
  • "useCached": true,
  • "error": "string",
  • "available": true,
  • "forked": true
}

Apply the first-run Windows data migration choice

query Parameters
migration
required
string
Enum: "import" "copy" "move"

Select upstream import, fork settings copy, or fork settings move.

Responses

The effective settings, and where each came from

What the panel's folders section shows, including which layer set DICT_DIR - so the page can say that saving will not take effect, rather than silently doing nothing.

Responses

Request samples

# where the settings came from, and which folders are scanned
curl -s 'http://127.0.0.1:6888/api/config' | jq '.configPath, .dictDirOrigin, .roots'

Response samples

Content type
application/json
{
  • "roots": [
    ],
  • "libDir": "string",
  • "prepared": 0,
  • "useCached": true,
  • "configPath": "string",
  • "howtoRemoved": true,
  • "total": 0,
  • "dictDirOrigin": "flag",
  • "dictDirEditable": true,
  • "configProblems": [
    ],
  • "revealLabel": "string",
  • "canReveal": true,
  • "canExit": true,
  • "canDelete": true,
  • "pathAliases": [
    ]
}

Read the interface-language choice

Defaults to System, which follows the requesting app or browser's preferred language when supported (English or Russian), then falls back to English. An explicit English or Russian choice overrides it. Dictionary languages are unchanged.

Responses

Response samples

Content type
application/json
{
  • "language": "system",
  • "resolved": "en"
}

Save the interface language for subsequent page loads

Saves System, English or Russian. System follows the requesting app or browser's preferred language. Reload the page to apply.

Request Body schema: application/json
required
language
required
string
Enum: "system" "en" "ru"

Responses

Request samples

Content type
application/json
{
  • "language": "system"
}

Response samples

Content type
application/json
{
  • "language": "system",
  • "resolved": "en"
}

Dictionary order, on/off switches and article text size

Healed against the dictionaries that exist right now. exists is false on a first run, which is the client's cue to adopt what an older build left in localStorage, once, instead of starting the user over.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/prefs' | jq '.dicts'

Response samples

Content type
application/json
{
  • "exists": true,
  • "dicts": [
    ],
  • "ui": {
    }
}

Replace the order and the enabled set, change UI settings

Array order is the user's order. Entries record the path as well as the id, because ids are derived from the path: moving a dictionary folder changes every id at once, and the path is what re-attaches the state instead of resetting it. Body is capped at 1 MiB.

Request Body schema: application/json
required
Array of objects (DictPref)

The whole list, in the user's order. Omit it (or send null) to keep the stored list untouched, as when only a UI setting changed.

object

The UI fields to change. Each field you send replaces the stored one; each field you omit keeps its stored value, so send false, 0 or [] to clear one. Omit ui (or send null or {}) to leave the stored UI record untouched.

Responses

Request samples

Content type
application/json
{
  • "dicts": [
    ],
  • "ui": {
    }
}

Response samples

Content type
application/json
{
  • "exists": true,
  • "dicts": [
    ],
  • "ui": {
    }
}

Read file-backed interface settings and search history

Responses

Response samples

Content type
application/json
{
  • "property1": "string",
  • "property2": "string"
}

Save a complete interface settings and search history snapshot

Request Body schema: application/json
required
Array (<= 65536 items)
Array (= 2 items)
string

Responses

Request samples

Content type
application/json
[
  • [
    ]
]

Set optional indexes for newly added dictionaries

Index is always enabled. Existing dictionaries are not changed. Pending preparation plans survive restart. GET /api/config returns these defaults as indexDefaults.

Request Body schema: application/json
required
index
boolean
contains
boolean
fullText
boolean

Responses

Request samples

Content type
application/json
{
  • "index": true,
  • "contains": true,
  • "fullText": true
}

The user's global stylesheets

Two CSS files the user owns, applied to everything DictShelf renders. app styles the page itself - its colours, its own layout - and its :root custom properties reach articles too, because they inherit across the shadow boundary. article styles what dictionaries render, in both article flavours (shadow root and sandboxed iframe).

The split is what keeps body{}, p{} and a{} - written with articles in mind - from also restyling the chrome.

dir is where the two files live on disk, beside the wudict.toml in effect, so they can be edited with an ordinary text editor as well; writable is false when there is no config directory at all, and the editor says so instead of failing to save.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/style' | jq -r .app

Response samples

Content type
application/json
{
  • "app": "string",
  • "article": "string",
  • "dir": "string",
  • "writable": true
}

Replace either stylesheet, or both

Omit a field to leave that file untouched: absent and empty are different, and the App tab and the Article tab are one editor sending different bodies. An empty string REMOVES the file, so "I cleared the box" and "there is a file here containing nothing" do not become two states that behave the same but read differently.

Each file is capped at 256 KiB. A stylesheet that hides the app can be stepped around with GET /?style=off, which omits it.

Request Body schema: application/json
required
app
string

The chrome stylesheet.

article
string

The article stylesheet.

Responses

Request samples

Content type
application/json
{
  • "app": "string",
  • "article": "string"
}

Response samples

Content type
application/json
{
  • "app": "string",
  • "article": "string",
  • "dir": "string",
  • "writable": true
}

The groups the dictionary picker offers

groups.ini, beside the dictshelf.toml in effect: a [Section] line is a "Group by" heading (at most 32, names of at most 64 bytes), and Name = text, text a group under it that a dictionary joins when one of the texts (at least 2 characters) is anywhere in its title or file name, in any case. An item between backticks is a regular expression (Go RE2, case-insensitive), read verbatim and tested against the title and the file name separately. ; and # start a comment outside backticks. A group line before any section belongs to [My groups]. While there is no file the built-in list is in effect (custom false); once saved, the file is the whole list.

Ids are the names, trimmed and lower-cased: a dictionary row's groups[].f and groups[].v (GET /api/dicts) are exactly these. problems lists the lines and items that were skipped (an invalid regular expression, a line with no =, a one-character item, more than 500 regular expressions); the rest of the file still applies. At most 100 are listed: a last entry with more stands for the rest. text and msg are clipped to 200 bytes. unusable means the file exists but could not be read: the built-in list is in effect, and problems has one entry, with no line, saying why.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/groups' | jq -r .text

Response samples

Content type
application/json
{
  • "text": "string",
  • "custom": true,
  • "unusable": true,
  • "file": "string",
  • "writable": true,
  • "problems": [
    ],
  • "facets": [
    ]
}

Replace groups.ini

The body is the whole file, as text. Lines that do not parse are reported in problems, never refused. A body identical to the built-in list removes the file instead, so an unchanged copy does not stop the list from following the built-in one. Capped at 256 KiB. While the current file is unusable, a save is refused unless replace=1 is given: the file is the user's, and the editor could not show it.

query Parameters
replace
string
Value: "1"

1 overwrites a groups.ini that exists but cannot be read.

Request Body schema: text/plain
required
string

Responses

Response samples

Content type
application/json
{
  • "text": "string",
  • "custom": true,
  • "unusable": true,
  • "file": "string",
  • "writable": true,
  • "problems": [
    ],
  • "facets": [
    ]
}

Back to the built-in groups

Removes groups.ini; the built-in list is in effect again.

Responses

Response samples

Content type
application/json
{
  • "text": "string",
  • "custom": true,
  • "unusable": true,
  • "file": "string",
  • "writable": true,
  • "problems": [
    ],
  • "facets": [
    ]
}

The built-in style presets and which are enabled

The presets the Appearance sheet offers as ready-made layers: a background look, palettes, layout and text tweaks, article roles. Each preset carries its CSS inline (app and/or article, the two scopes a stylesheet can reach), because the page composes the article layer itself and must not fetch preset files one by one.

The groups are the conflict model: presets inside one group claim the same surfaces, so at most ONE of them can be enabled - enabling one switches its group-mates off, radio-style. A preset that conflicts with nothing lives in a group of its own and behaves as a plain toggle. writable is false when there is no config directory, and the pane says so instead of offering switches that cannot stick.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/presets' | jq -r '.enabled[]'

Enable or disable one preset

Switching a preset on inside a conflict group switches its group-mates off; the fresh state comes back, so a client never has to guess what the radio rule did. The choice is remembered in the config folder beside the stylesheets it composes with.

Request Body schema: application/json
required
id
required
string

The preset's id.

on
required
boolean

Enable (true) or disable (false).

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "on": true
}

The saved appearances, and which one is in force

A look is the whole Appearance sheet under a name: the article's size and weight, which built-in preset layers are on, the window's colour and wallpaper per theme, what the screen margins and the bars do, and the reader's own stylesheets. Three ship with the app - Clean, Sepia and Old paper - and the reader can save their own beside them.

Only names are listed. A look's state is the server's to read - the four stylesheets it speaks for are files, not fields a menu needs - and current is the look in force, empty when none has been applied yet. writable is false when there is no config directory, and the row says so instead of offering a list that cannot stick.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/looks' | jq -r '.looks[].name'

Save what is on screen now under a name

The state is assembled by the server: the settings and the four stylesheets it owns, plus the shell's half as the client sends it - the backdrop of each theme, the margin mode and colour, the bar mask. A client with no shell (a browser) simply omits shell, and the look it saves is a look with no backdrop.

The new look becomes the one in force, because it IS what is in force.

Request Body schema: application/json
required
name
required
string

1–100 characters, no control characters.

shell
object

The shell's half, as the client sees it.

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "shell": { }
}

Replace a saved look with what is on screen now

"Save my changes into this one." Built-in looks are refused: they are the app's, and a look called "Clean" that means something else is worse than no look at all.

Request Body schema: application/json
required
id
required
string

The look's id.

shell
object

The shell's half, as the client sees it.

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "shell": { }
}

Forget a saved look

The built-ins cannot be deleted. Deleting the look in force leaves nothing in force, which is honest: what is on screen came from somewhere the reader has just thrown away.

Request Body schema: application/json
required
id
required
string

The look's id.

Responses

Request samples

Content type
application/json
{
  • "id": "string"
}

Put a look on, asking first when something unsaved would go

Two passes over one endpoint. With confirm false the server only ANSWERS: needsConfirm says whether the screen has drifted from the look in force, which is the client's cue to offer saving it first. Nothing is written on that pass, which is what makes the question safe to ask on every switch. With confirm true the look is written and shell comes back for the client to push into the shell it owns.

Re-applying the look already in force is never a question: that is the "put it back how it was" gesture, and it has no doubt in it. Every field counts in the comparison - the stylesheets included - because an apply restores all of them and none can be guessed back.

Request Body schema: application/json
required
id
required
string

The look's id.

confirm
boolean

Apply rather than ask.

shell
object

The shell's half, as the client sees it now.

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "confirm": true,
  • "shell": { }
}

The user's own file store

Files the user uploaded so that something they wrote can reference them by URL: an image for a background-image in their custom CSS, a font for an @font-face, an imported stylesheet, a script, a PDF a private dictionary links to. They live beside the two stylesheets, in an assets folder under the same directory GET /api/style reports, and are served from /files/<name>.

No type restriction: the store is "files the page can reach by URL", and an extension allowlist would only mean the feature is missing for whoever needed the entry it lacks. limits is reported rather than assumed, so a client's message about a refused upload cannot drift from the rule that refused it.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/files' | jq -r '.files[].name'

Response samples

Content type
application/json
{
  • "files": [
    ],
  • "dir": "string",
  • "writable": true,
  • "total": 0,
  • "limits": {
    }
}

Add or replace one file

The request body is the file itself, raw - there is one file per request and no other field, so multipart would add a parser to carry nothing. The name comes from name, and is one path segment of A-Z a-z 0-9 . _ - starting with a letter or digit, at most 64 characters; it is an allowlist, not a cleaning pass, so nothing here can name a path.

Uploading onto an existing name is refused with 409 unless replace=1: a silent overwrite is the one mistake this endpoint can make that the user cannot undo, because their stylesheet already points at that name.

query Parameters
name
required
string

The file name, which is also its URL under /files/.

replace
string

Overwrite a file that is already there. A switch.

Request Body schema: application/octet-stream
required
string <binary>

Responses

Request samples

curl -s -X POST --data-binary @w1.jpg \
  'http://127.0.0.1:6888/api/files?name=w1.jpg'

Response samples

Content type
application/json
{
  • "files": [
    ],
  • "dir": "string",
  • "writable": true,
  • "total": 0,
  • "limits": {
    }
}

Delete one file

Removing what is not there is a success: the caller asked for it to be gone and it is. Nothing checks whether a stylesheet still references the name - that is the caller's warning to give, before it asks.

query Parameters
name
required
string

The file to remove.

Responses

Request samples

curl -s -X DELETE 'http://127.0.0.1:6888/api/files?name=w1.jpg'

Response samples

Content type
application/json
{
  • "files": [
    ],
  • "dir": "string",
  • "writable": true,
  • "total": 0,
  • "limits": {
    }
}

What a dictionary says about itself and its source files

The dictionary's own annotation: the publisher's blurb, the edition, the copyright. Composed from the live sidecar beside the source file where the format ships one (Lingvo's <stem>.ann) and otherwise from the description its header declared, then normalised server-side - plain text escaped, markup reduced to prose elements - so a client renders html as-is and needs no sanitiser of its own. The response also gives source companion paths (including linked abbreviation files) and any cheap headword/article counts already available.

Same-origin only. It is not part of the client API a browser extension reaches (D69): an annotation names a file on the user's disk. It is also deliberately absent from /api/dicts, which is built without opening any dictionary; this endpoint is fetched lazily, per dictionary.

query Parameters
dict
required
string

Dictionary id, as returned by /api/dicts.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/about?dict=<id>'

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "format": "string",
  • "html": "string",
  • "source": "string",
  • "sourceFiles": [
    ],
  • "headwords": 0,
  • "entries": 0,
  • "indexLang": "string",
  • "contentsLang": "string"
}

One page of the dictionary's headword list

The reading view, as opposed to /api/search's answering view: the dictionary as an ordered, addressable list of its own headwords, turned a page at a time. Article bodies are never included - this lists what a dictionary contains, it does not answer with it.

Index-backed only. The order, the page offsets and the letter strip are three reads of the prepared index (idx_entry_w), which is what makes a jump to M in a 2.9 M-entry dictionary a seek rather than a scan; a dictionary that has not been prepared answers 409 instead of a slower browse. Browse order is that index's order - case-insensitive by headword - and NOT the source file's entry order, which is what dictshelf keys reads.

size is fixed by the server and is not a parameter: the page number is the whole address of a browse view, so ?p=37 has to mean the same place tomorrow as it does today.

Same-origin only. It is not part of the client API a browser extension reaches (D69): that grant is lookup, and this dumps a dictionary a page at a time.

query Parameters
dict
required
string

Dictionary id, as returned by /api/dicts.

p
integer >= 1
Default: 1

1-based page number. Out of range is clamped to the last page rather than refused, so a stale bookmark still opens.

at
string

Jump to the page holding this word, or the first one after it. Overrides p. The reader lands on a page boundary, not on the word: a page that started mid-letter would have no stable address.

stream
string
Value: "1"

Set to 1 for NDJSON events: the page first, then alphabet strip batches and progress, followed by done. Closing the request stops the prepared headword-index walk.

strip
string
Value: "0"

Set to 0 to omit the alphabet strip when the caller already has it cached.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/browse?dict=<id>&p=1'

Response samples

Content type
{
  • "dict": "string",
  • "name": "string",
  • "total": 0,
  • "page": 0,
  • "pages": 0,
  • "size": 0,
  • "at": 0,
  • "words": [
    ],
  • "alphabet": [
    ]
}

Search all headwords in one dictionary

By default, matches the prefix of any word in a multiword headword. matchmode selects prefix, suffix, contains, complete-token word, complete-token word with lemmatization, whole-headword exact, exact with lemmatization, or whole-headword wildcards matching. Prefix, suffix, contains, and word modes match query words against tokens in the headword. Word with lemmatization also matches the dictionary-language lemma. Exact with lemmatization matches both the typed form and its dictionary-language lemma as complete headwords. Returns distinct matching headwords in browse order, in fixed-size result pages. Search is over the entire dictionary, not the current browse page. With matchmode=wildcards (or legacy wildcards=1), q is a whole-headword wildcard pattern (? for one Unicode character, * for any number); this path can scan a prepared or direct dictionary and also returns alphabet sections. In wildcard results, within filters by withinMode (default prefix), which supports prefix, suffix, contains, whole-word, exact, and wildcard matching. Positions then address each returned word in the unfiltered wildcard result. Same-origin only.

query Parameters
dict
required
string
q
required
string <= 256 characters
p
integer >= 1
Default: 1
wildcards
string
Value: "1"

Set to 1 to interpret q as a whole-headword wildcard pattern.

matchmode
string
Default: "prefix"
Enum: "prefix" "suffix" "contains" "word" "wordLemma" "exact" "exactLemma" "wildcards"

How q matches headwords. Defaults to prefix.

within
string <= 256 characters

Additional headword search inside q's wildcard results.

withinWildcards
string
Value: "1"

Set to 1 to interpret within as a whole-headword wildcard pattern.

withinMode
string
Default: "prefix"
Enum: "prefix" "suffix" "contains" "word" "wordLemma" "exact" "exactLemma" "wildcards"

How within matches wildcard results. Defaults to prefix.

lemmaQueries
string <= 16384 characters

Optional JSON array of lemma forms collected from the separate-dictionary search for a combined-list lemmatization search.

groupwithin
string
Value: "1"

With stream=1 and a complete separate-dictionary wildcard cache, search all cached dictionaries in one stream. Completion events include dict and the effectiveQueries used for per-dictionary lemmatization; matching pages remain addressable per dictionary.

wildcache
string [ 16 .. 80 ] characters

ID of the temporary SQLite cache for this wildcard pattern and group. Headword blocks and optional within-filter blocks share this database.

wildgroup
string <= 256 characters

Selected dictionary group, stored in the temporary cache metadata.

wildorder
integer [ 0 .. 9999 ]

Dictionary position in the selected group's ordered result list.

wildcount
integer [ 1 .. 10000 ]

Number of dictionaries in the selected group.

stream
string
Value: "1"

Set to 1 to receive NDJSON progress and matching headword blocks as the scan runs.

Responses

Response samples

Content type
{
  • "words": [
    ],
  • "alphabet": [
    ],
  • "positions": [
    ],
  • "total": 0,
  • "page": 0,
  • "pages": 0,
  • "size": 0,
  • "partial": true
}

Show a file or folder in the desktop's file manager

Loopback only, and limited to the paths this app already displays: a remote browser must not open a window on someone else's desktop, and an arbitrary path must never reach the shell.

query Parameters
path
required
string

An absolute path the app is showing.

Responses

Request samples

# opens a file manager window on the machine running the server
curl -s 'http://127.0.0.1:6888/api/reveal?path=/Users/me/.dmshlabs/dictshelf/db'

# Loopback only: the request must come from this machine.

Response samples

Content type
application/json
{
  • "revealed": "string"
}

Read power state and pending user-requested work

Loopback-only status used by the Android shell before exiting, including an adopted server.

Responses

Response samples

Content type
application/json
{
  • "state": "active",
  • "busy": true
}

Report the platform's power state

How the Android shell tells the server what the platform just told it - onStop, onTrimMemory, thermal throttling, battery saver - which an exec'd server has no other way of learning.

active is normal. background stops automatic indexing and closes reopenable handles while prepared databases stay open. saving lowers the CPU ceiling for user-requested work. thermal-moderate lowers it further, and thermal-severe or restricted limits execution to one processor; thermal states also release prepared database handles.

Loopback only, and POST because it changes state: a GET here would be followed by every link prefetcher on the network.

query Parameters
state
required
string
Enum: "active" "background" "saving" "restricted" "thermal-moderate" "thermal-severe"

Responses

Request samples

# what the Android shell sends when the app leaves the screen
curl -s -X POST 'http://127.0.0.1:6888/api/power?state=background'
curl -s -X POST 'http://127.0.0.1:6888/api/power?state=active'

# Loopback only, like /api/reveal.

Response samples

Content type
application/json
{
  • "state": "active"
}

Export indexed headwords

Start, inspect, or stop a headword export job. The request body uses action start, status, or stop; starting requires selected dictionary IDs and an existing output folder.

Request Body schema: application/json
required
action
required
string
Enum: "start" "status" "stop"
dictionaries
Array of strings
path
string
separate
boolean

Responses

Request samples

Content type
application/json
{
  • "action": "start",
  • "dictionaries": [
    ],
  • "path": "string",
  • "separate": true
}

Response samples

Content type
application/json
{ }

Stop the local Windows desktop server and exit the process

Responses

Read the active dictionary indexing progress across pages.

done counts attempted operations, including failures. dictionaryDone and dictionaryTotal count completed dictionaries out of the queue, and elapsedMs reports total job time. Each busyProgress row includes that dictionary’s elapsed time. failed lists per-operation errors; other queued operations continue. error describes a fatal job error.

Responses

Start a server-owned sequence of index operations.

A failed dictionary operation is recorded and skipped; the remaining queue continues. Stop still finishes the current atomic operation safely.

Responses

Stop after completing the current index operation safely.

Responses

Download persistent server diagnostics, including the previous rotation

Responses

Clear retained System Log history and older crash details

Responses

List user groups and the automatic All Dictionaries group

Private collection API. Members are current dictionary IDs, including globally disabled dictionaries. A linked group's membership follows its filter; new matches enter its alphabetical unpinned tail or append alphabetically after a manually arranged list. If the filter disappears, the last saved membership becomes an ordinary editable group. Each group's customOrder flag records the mode; false means alphabetical. In state.json, each ordered member stores its own pinned flag; the pinned ID list in this response is derived from those entries.

Responses

Create a user dictionary group, optionally from a filter

Request Body schema: application/json
required
name
required
string [ 1 .. 100 ] characters
object
linked
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "filter": {
    },
  • "linked": true
}

Delete a user group without deleting dictionaries

Request Body schema: application/json
required
group
required
string

Responses

Request samples

Content type
application/json
{
  • "group": "string"
}

Rename a user group and optionally update its filter link

Request Body schema: application/json
required
group
required
string
name
required
string
object or null

Selected filter retained for later relinking.

linked
boolean

Whether to link the group to filter; omit to change only the name.

Responses

Request samples

Content type
application/json
{
  • "group": "string",
  • "name": "string",
  • "filter": {
    },
  • "linked": true
}

Set one dictionary's membership without changing search settings

Request Body schema: application/json
required
group
required
string
dict
required
string
member
required
boolean

Responses

Request samples

Content type
application/json
{
  • "group": "string",
  • "dict": "string",
  • "member": true
}

Apply a group's staged membership and dictionary order atomically

Dictionaries that have disappeared from the registry are retained. customOrder selects alphabetical or manual order; manualOrder records an explicit rearrangement in this edit. Optional pinned IDs update the pinned members together with the order.

Request Body schema: application/json
required
group
required
string
members
required
Array of strings
pinned
Array of strings
customOrder
boolean
manualOrder
boolean

Responses

Request samples

Content type
application/json
{
  • "group": "string",
  • "members": [
    ],
  • "pinned": [
    ],
  • "customOrder": true,
  • "manualOrder": true
}

Add several dictionaries to a user group in one update

Request Body schema: application/json
required
group
required
string
members
required
Array of strings

Responses

Request samples

Content type
application/json
{
  • "group": "string",
  • "members": [
    ]
}

Save the order of currently available members of one user group

Unavailable members remain in the group. Linked groups can also be reordered. New members enter the alphabetical unpinned tail when customOrder is false; otherwise they append alphabetically. Optional pinned IDs update the per-member pinned flags stored alongside each member's order. Does not change global dictionary order or membership.

Request Body schema: application/json
required
group
required
string
members
required
Array of strings
pinned
Array of strings
customOrder
boolean

False sorts the unpinned tail alphabetically; true preserves manual order.

Responses

Request samples

Content type
application/json
{
  • "group": "string",
  • "members": [
    ],
  • "pinned": [
    ],
  • "customOrder": true
}

Link a user group to a filter, change its filter, or detach it

A null filter detaches the group and keeps its current membership and selected filter for later relinking.

Request Body schema: application/json
required
group
required
string
required
object or null

Responses

Request samples

Content type
application/json
{
  • "group": "string",
  • "filter": {
    }
}