▍ humdrum codex / glint v1.0.2
license AGPL-3.0

chore: add TASK-020 spellcheck (undercurl + suggest-and-replace, release-2)

e9083c8c33a3e2f7ba44d89de34c93eae1a4a403
humdrum <me@humdrum.me> · 2026-06-29 11:23

parent 2ba331e1

chore: add TASK-020 spellcheck (undercurl + suggest-and-replace, release-2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

1 files changed

- → Spellcheck-with-undercurl-underlines.md +61 −0
@@ -0,0 +1,61 @@
+---
+id: TASK-020
+title: Spellcheck with undercurl underlines
+status: "\U0001F7E6 Backlog"
+assignee: []
+created_date: '2026-06-29 18:11'
+updated_date: '2026-06-29 18:23'
+labels:
+  - feature
+  - release-2
+dependencies: []
+priority: medium
+ordinal: 20000
+---
+
+## Description
+
+<!-- SECTION:DESCRIPTION:BEGIN -->
+Lightweight inline spellcheck: red wavy (undercurl) underline under misspelled words, with one-key suggest-and-replace. Catch obvious typos only — not an intense/morphological checker. Must stay zero-cgo so the Homebrew/go-install build keeps working, and must not bloat the binary much.
+
+RENDERING (undercurl)
+- Misspelled-word spans emit raw SGR: undercurl '\e[4:3m' + underline color '\e[58:2::R:G:Bm' (theme-driven red, e.g. Flexoki red 217,54,42), reset '\e[59;4:0m'.
+- lipgloss/termenv DON'T expose undercurl or underline-color — add a raw-escape span attribute in the editor scanner. Preserve the markup-visible invariant: span text still concatenates to the raw line; only the SGR wrapper changes.
+- Degrade gracefully: terminals without 4:3 show straight underline or ignore (Ghostty/kitty/WezTerm/foot/VTE support it). tmux needs terminal-features passthrough — document it.
+
+DICTIONARY (lightweight, pure-Go)
+- Embed a single common-English wordlist (SCOWL-derived, ~50-60k most-common words; gzip-embedded via embed.FS, decompressed into a hashset at startup). Target small added binary weight, not full coverage.
+- No cgo, no hunspell — keeps 'go build .' / brew formula clean.
+- Case-insensitive membership; treat possessives/simple plurals leniently if cheap.
+
+SUGGEST & REPLACE
+- Build a BK-tree from the same embedded dict at startup (edit-distance <=2 lookup; modest RAM, no extra binary weight; only queried on user trigger, never per render).
+- Trigger key (suggest Ctrl+; TBD) when the cursor is on/adjacent to a flagged word opens a small popup of the top ~5 suggestions ranked by edit distance (tie-break by word frequency/length).
+- Pick with arrows+Enter or a number key -> replace the misspelled word span in the buffer, mark dirty, clear its underline. Esc dismisses.
+- The same popup includes an 'Add to dictionary' entry (writes the word to dict.txt) so add-word and replace share one UI.
+
+WHAT TO SKIP (no false positives)
+- Code fences and inline code, URLs, wikilinks [[...]], markdown link targets, YAML frontmatter values, and (when on in a code file) anything non-prose.
+
+PERSONAL DICTIONARY (easy add)
+- Plain file ~/.config/glint/dict.txt, one word per line, user-editable by hand.
+- 'Add to dictionary' from the suggestion popup (or a direct add-word key) appends to dict.txt and clears the underline live.
+
+TOGGLE / DEFAULTS
+- On/off toggle (key TBD) for the session.
+- Default ON for .md/.markdown/.txt and unnamed buffers; default OFF for recognized code-file extensions (driven by the same extension map as TASK-018 syntax highlighting — share it).
+- Config key to override the default (e.g. spellcheck = auto|on|off).
+
+PERF
+- Check only visible-viewport words per render; cache word->ok results so typing doesn't re-check the whole doc each keystroke; invalidate a word's cache entry when added to the personal dict. Suggestion lookups run only on trigger.
+<!-- SECTION:DESCRIPTION:END -->
+
+## Acceptance Criteria
+<!-- AC:BEGIN -->
+- [ ] #1 Misspelled prose words show a red undercurl underline in Ghostty; unsupported terminals degrade to plain/no underline
+- [ ] #2 Dictionary is pure-Go embedded (no cgo); binary size increase is modest and 'go build .' + brew formula still work
+- [ ] #3 Code fences, inline code, URLs, wikilinks, link targets, and frontmatter values are never flagged
+- [ ] #4 Adding the word under the cursor appends to ~/.config/glint/dict.txt and removes the underline live; hand-editing dict.txt also works
+- [ ] #5 Spellcheck defaults ON for md/txt/unnamed buffers and OFF for code files, with a config override and a session toggle
+- [ ] #6 Triggering on a flagged word shows ~5 ranked suggestions; picking one replaces the word in place, marks the buffer dirty, and clears the underline
+<!-- AC:END -->