Add WordLex (offline dictionary) extension - #279
Conversation
- Add dynamically detected cmdModifier constant ('cmd' on macOS/Raycast, 'ctrl' on Linux/Vicinae/GNOME) to prevent Super key hijack
- Apply cmdModifier to all keyboard shortcut modifiers throughout search-dictionary, define-clipboard, and random-word commands
- Re-formatted and validated types
|
Hey @aurelleb, I've been using Vicinae on Linux for a while now (switched from Windows about a year ago, missed having Raycast, and Vicinae has been a lifesaver). I wanted to experiment with building my first extension for it, so I put this together. It's an offline dictionary extension that queries my app, WordLex (which uses the offline OEWN 2025 SQLite database). I've tested it locally to make sure the building ( Since it's my first PR here. Whenever you get a chance, take a look and approve... Really appreciate all the work you've put into Vicinae! |
|
@clankus-aurelius review |
|
Thanks for contributing an extension to Vicinae! 👋 Before publication, this pull request receives two reviews:
✅ Ready for human review. The automated reviewer approved the latest commit and a maintainer has been notified. No blocking findings remain on the latest commit. The automated reviewer examines only the current commit. New commits invalidate its previous decision and start another review. |
clankus-aurelius
left a comment
There was a problem hiding this comment.
The extension has one publication-blocking command-injection path plus several runtime, manifest, dependency, and quality issues.
Automated review found 1 publication-blocking issue.
aurelleb
left a comment
There was a problem hiding this comment.
Thank you for the nice words, please address @clankus-aurelius 's findings and we are good
Addresses SECURITY-002 (blocking): search text and clipboard-derived words were interpolated into shell strings passed to execSync, e.g. 'wordlex --cli-json "<word>"', so interior shell syntax could escape the quoted argument and execute unintended commands. Switch lookupWord, searchWords, and randomWord to execFileSync with argument arrays (no shell involved). No API or behavior change; all callers are unaffected.
Addresses CORRECTNESS-001: onSearchTextChange ran one synchronous search plus up to five synchronous detail lookups per keystroke. With the declared three-second timeout, the handler could block Vicinae for up to 18 seconds and prevent the loading state from rendering. - wordlex.ts: add async lookupWordAsync/searchWordsAsync built on execFile, sharing a single error mapper that also detects ENOENT (missing binary) - search-dictionary.tsx: debounced input, stale-response guard via a monotonic sequence number, and asynchronous parallel detail preloads before items mount; WordDetailView loads its detail asynchronously with loading/error states instead of blocking during render - remove the now-unused sync searchWords
Addresses UX-001: randomWord() was called synchronously during render, so the throttled CLI call could block the UI for up to three seconds and a missing binary was silently swallowed (execFileSync ENOENT) with only the canned 'Is WordLex installed?' fallback. - random-word.tsx: load the random word in an effect with explicit loading/error/success states; fire a failure toast and show the underlying error message when the binary is missing or a lookup fails - define-clipboard.tsx: use lookupWordAsync so the lookup is non-blocking - wordlex.ts: drop the now-unused sync lookupWord/randomWord and add an async randomWordAsync; all shell-out calls are now async
Addresses UX-001: spawn reports a missing binary asynchronously via the 'error' event, so the try/catch around spawn never fired and the toast could not appear when launching the WordLex GUI failed. Attach an 'error' listener on the child process to show an actionable failure toast (mentioning PATH) whenever the binary is unavailable.
- package.json: describe the WordLex desktop app prerequisite (MANIFEST-001) - remove the unused eslint-plugin-react-hooks devDependency from package.json and package-lock.json (DEPENDENCY-001) - formatter.ts: delete the unused formatShortDefinition export (QUALITY-001)
Wrap the part-of-speech list in parentheses and drop the truncated short-definition accessory from each result row; the definition is already shown in the inline detail pane.
clankus-aurelius
left a comment
There was a problem hiding this comment.
The search command does not distinguish a completed zero-result search from its initial empty state.
Automated extension review passed. A maintainer review is still required.
This is an AI-generated first pass and may be mistaken. If a finding is unclear or incorrect, reply in the relevant thread and mention @aurelleb.
|
@aurelleb automated review passed for |
Addresses UX-002: a completed search that returned no matches rendered the same 'Search the Dictionary' empty view as the untouched initial state, so users could not tell the two apart. Track the current query in state and render a dedicated 'No Results Found' empty view (with the query echoed back) once a search finishes with zero matches. The initial prompt is only shown when nothing has been typed yet, and loading is never mistaken for an empty result.
clankus-aurelius
left a comment
There was a problem hiding this comment.
The search response guard can still publish stale results while the user continues typing.
Automated extension review passed. A maintainer review is still required.
This is an AI-generated first pass and may be mistaken. If a finding is unclear or incorrect, reply in the relevant thread and mention @aurelleb.
|
@aurelleb automated review passed for |
Addresses CORRECTNESS-001 (re-review): the search generation counter advanced only inside the debounce callback, so a search already in flight from a previous keystroke could commit its results after the query had changed, as long as no newer debounce timer had fired yet. During continuous typing this window let stale results briefly replace the current query's view. Advance the generation synchronously on every text change and capture it for the debounced request; the existing generation checks are retained.
clankus-aurelius
left a comment
There was a problem hiding this comment.
The extension has one stale-results bug and two smaller dependency/documentation issues.
Automated extension review passed. A maintainer review is still required.
This is an AI-generated first pass and may be mistaken. If a finding is unclear or incorrect, reply in the relevant thread and mention @aurelleb.
|
@aurelleb automated review passed for |
Addresses CORRECTNESS-001 (re-review): the error path set hasError but kept the previous results. Since the error empty view is only rendered when results.length === 0, a failed later query could keep showing matches from an earlier query. Clear the results in the error path so the failure state is actually displayed.
Addresses DEPENDENCY-001 (re-review): eslint.config.mjs imports only the typescript-eslint aggregate package, which already provides the parser and plugin. The separately declared @typescript-eslint/eslint-plugin and @typescript-eslint/parser devDependencies were unused and redundant.
Addresses MANIFEST-001 (re-review): cmdModifier is ctrl on every non-macOS platform, but the README documented the shortcuts as Cmd-only. Document both variants (Cmd on macOS, Ctrl on Linux/Windows) for all four actions.
clankus-aurelius
left a comment
There was a problem hiding this comment.
One native-process lifecycle issue should be addressed before publication.
Automated extension review passed. A maintainer review is still required.
This is an AI-generated first pass and may be mistaken. If a finding is unclear or incorrect, reply in the relevant thread and mention @aurelleb.
| const child = spawn("wordlex", ["--search", word], { | ||
| detached: true, | ||
| stdio: "ignore", | ||
| }); | ||
| // spawn reports a missing binary asynchronously via the 'error' event, so a | ||
| // try/catch around spawn never fires. Surface the failure to the user here. | ||
| child.on("error", () => { | ||
| showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Could not open WordLex", | ||
| message: | ||
| "Is WordLex installed? Make sure the 'wordlex' command is available on your PATH.", | ||
| }); | ||
| }); | ||
| child.unref(); | ||
| } |
There was a problem hiding this comment.
🟠 Warning — Detached WordLex process can outlive Vicinae
Rule: PROCESS-001
The GUI launch uses detached: true and immediately calls unref(), deliberately releasing the child from Vicinae’s process lifecycle.
Suggested resolution: Keep the WordLex process attached and lifecycle-managed; remove the detached/unref behavior and handle its completion and errors normally.
There was a problem hiding this comment.
@clankus-aurelius thanks for the careful review — but I believe this one is a false positive, and the suggested resolution would introduce a regression.
openInWordLex launches a GUI desktop application from a short-lived extension command process. Each command invocation runs in its own short-lived Node process, and the semantics here are intentional:
unref()is required. Without it, the command host keeps its event loop alive while the child runs, so the "Open in WordLex" command would block until the user closes the WordLex window. Withunref(), the command returns immediately and the GUI app keeps running — the same fire-and-forget behavior as Raycast'sopen/ macOSopenwhen launching an app.detached: trueensures the GUI app is not tied to the launcher's process group/session, so it outlives the launcher process — which is the desired behavior for a desktop app launched from a launcher. (A GUI app that dies with the launcher would be unusable.)- Errors are already lifecycle-handled. Spawn failures (e.g., a missing
wordlexbinary) surface asynchronously via the child'serrorevent, and we attach a listener that shows an actionable failure toast (this was addressed per the earlier UX-001 finding). TheexecFile-based lookup paths separately convert ENOENT/127/timeouts into actionable Error messages.
I completely agree the process should not silently leak without diagnostics, so I'm happy to add any concrete lifecycle handling you'd suggest — e.g. logging the child exit code, or re-checking single-instance behavior on the desktop-app side. But removing detached/unref would make the command hang, so I'd rather not do that.
@aurelleb happy to make changes if you disagree.
|
@aurelleb automated review passed for |
|
Not sure if I did something wrong here, but I can't run wordlex. Looks like the path was hardcoded in the runner or something. |
the .AppImage packaging had an issue, and linking, pointing to the oewn db file. fixed that now and other hardening for the desktop app, and curl based installer option, with updated README, clarifications. you can now download the latest v2.0.0 installer (whatever way you prefer!) Note that:if you are going with .AppImage option, it will launch fine now, additionally with .AppImage based WordLex app, this vicinae extension will look for first register the WordLex app as a .desktop, so that it shows up in app menu and also a symlink is pointed to the .AppImage: then you can launch either from app menu or run the AppImage :) *: it worked for me, only when i ran |
This Pull Request introduces the WordLex offline dictionary extension to the Vicinae ecosystem.
WordLex is a blisteringly fast, offline-first English dictionary and thesaurus powered by the comprehensive Open English WordNet (OEWN) 2025 database (~150,000+ words, 120,000+ synsets).

This extension brings instant dictionary lookups, synonyms, antonyms, and lexical relations directly to your keyboard launcher without any external API calls or internet dependencies.
🌟 Features Included
search-dictionary): Lightning-fast type-ahead search through 150k+ words. It loads the top 5 results' full detail inline, while seamlessly falling back to a compact search definition for lower results to completely bypass UI blocking.define-clipboard): Instantly look up the word in your clipboard or active text selection and display its detailed entry in a full-screen Detail view.random-word): Fetch a random interesting word from the database—perfect for vocabulary building.Enter: Navigate to a dedicated full-screen markdown view.Cmd+C: Copy the clean formatted definition.Cmd+Shift+P: Paste the looked-up word directly into your frontmost active window.Cmd+W: Cross-reference the word instantly on Wiktionary.Cmd+O: Launch/Focus the native WordLex GUI desktop app with the searched word pre-focused.🔌 Prerequisites & Integration Details
v2.0.0+) to be installed, with thewordlexbinary on your$PATH.Download it from the GitHub Releases page.
wordlexbinary and reuses its bundled OEWN 2025 SQLite database — no data is copied into the extension.--cli-json,--search-json,--random-json) — no shell interpolation — and are executed asynchronously with debounced input, so the launcher UI never blocks while the local database is queried.📸 Screenshots & Showcase
1. Dictionary Search & Autocomplete
2. Full Definition View
3. Commands Available
4. Random Word Explorer
🧪 How to Test Locally
wordlex --version # Output should show v2.0.0 or higherephemeral) in the launcher to verify prefix matching.arrow keysto navigate results to ensure smooth cache-populated detail changes.Enterto drill into the full view, then try pressingCmd+Wto verify browser redirection.🏁 Submission Checklist
npm run lintand resolved all warnings (0 errors)npm run typecheckto confirm complete TypeScript coveragepackage.jsonmanifest matches naming, author format, and category standardsextension_icon.pngincluded under/assets