Live Spellcheck in a Terminal: Yellow Highlights, No Popups

August 15, 2026

Every modern editor highlights misspelled words as you type. Squiggly red underlines in Word, yellow backgrounds in Google Docs, dotted lines in VS Code. It’s so standard you forget it’s there — until it isn’t.

Terminals don’t have spellcheck. The terminal is a character grid. There’s no DOM, no CSS, no per-character styling primitive. Or so the conventional wisdom goes.

We just shipped live inline spellcheck in jterm and Phosphor BBS. Misspelled words light up in yellow as you type — in chat, board posts, mail compose, any text field. No popups, no pre-submit prompts, no context switches. Just yellow where the typos are.

The Foundation: Per-Character Coloring

This didn’t start as a spellcheck feature. It started with a question: can we color individual characters in a terminal text widget?

The answer was TextStyleResolver — a functional interface that the render pipeline calls for every character in a text widget:

@FunctionalInterface
public interface TextStyleResolver {
    TextCell resolveStyle(int charIndex, char c, TextCell defaultStyle);
}

Return null and the character gets its default style. Return a TextCell and that cell’s colors are used instead. The widget calls drawStyledString() instead of drawString(), and the resolver is consulted per-character during the render pass.

We already used this for ghost text completion — dim suggestions rendered ahead of your cursor. Spellcheck is the second consumer, and it won’t be the last. Syntax highlighting, diff views, and search match highlighting all use the same mechanism.

How the Spellchecker Works

Three classes, each with one job.

1. DictionaryLoader — Loads the system dictionary at /usr/share/dict/words (236K words on macOS, available on most Linux distros). If the system dictionary isn’t available, it falls back to a bundled english_words.txt resource with ~1,000 common words. All words are lowercased into a HashSet for O(1) lookup. The dictionary knows whether it’s the full system dict or the fallback, so the BBS can log which one loaded at startup.

2. SpellcheckDictionary — Holds the word set and answers one question: isValid(String word). Case-insensitive — lowercase the query, check the set, done. Also exposes size() and isFallback() for diagnostics.

3. SpellcheckResolver — Implements TextStyleResolver. This is where the actual highlighting happens.

The tricky part: resolveStyle() only receives the character index, not the full text. It can’t determine word boundaries from a single character. So the resolver has a setText(String text) method that the widget calls before each render. setText() scans the text, splits it into words, checks each against the dictionary, and pre-computes a list of misspelled character ranges as [start, end) pairs. Then resolveStyle() just checks if the character index falls within one of those ranges — O(log n) binary search per character, effectively instant.

// Before each render, the widget feeds the current text:
if (styleResolver instanceof SpellcheckResolver sr) {
    sr.setText(currentText);
}

// During render, per character:
TextCell resolveStyle(int charIndex, char c, TextCell defaultStyle) {
    if (!enabled) return null;
    if (isInMisspelledRange(charIndex)) {
        return defaultStyle.withForeground(AnsiColor.YELLOW);
    }
    return null; // default style
}

Word Boundaries

Defining what counts as a “word” is where spellcheck gets opinionated. We chose:

  • Words are sequences of ASCII letters only (a–z, A–Z). Apostrophes, hyphens, digits, and punctuation break words. So don't splits into don (valid) and t (one character).
  • Single-character words are always valid. This avoids flagging the t in don't, individual letters, and fragments left by apostrophe splitting. It also avoids flagging a and I — both real words that are one character.
  • Everything is case-insensitive. Hello, HELLO, and hello all match.

This is deliberately simple. No stemming, no lemmatization, no language detection. The system dictionary is English-only. If you need something more sophisticated, the SpellcheckDictionary interface is extensible — swap in a Hunspell dictionary or a language model and the rest of the pipeline stays the same.

Integration: One Method

TextBox and TextArea each get a single convenience method:

textBox.setSpellcheckDictionary(dictionary);

That’s it. The widget creates a SpellcheckResolver internally, wires it as the style resolver, and calls setText() before each render with the current text. No resolver, no styling — backward compatible by default. Spellcheck is off until you explicitly enable it.

For TextArea, which renders line by line, setText() is called per visible line with that line’s text. Words don’t span line breaks in practice, so per-line spellcheck is correct without the complexity of tracking global character offsets across wrapped lines.

Why Not Pre-Submit?

The obvious alternative to live spellcheck is a pre-submit check: user hits send, you scan the text, flag misspelled words, and ask “fix or send anyway?” We considered that first. It’s simpler to implement — no per-character rendering, no resolver, no pre-computation.

But it’s worse UX. It interrupts the user at the moment they’re done writing, forces a decision, and breaks flow. Live highlighting is passive — you see the yellow as you type, and you fix things on your own schedule. If you don’t care about a typo in a chat message, you ignore it. If you’re writing a board post and want it clean, the yellow tells you where to look.

The terminal is a live medium. Spellcheck should be too.

What’s Next

The same TextStyleResolver pipeline enables:

  • Syntax highlighting — A resolver that colors keywords, strings, and comments differently. The Phosphor DSL scripting language is the obvious first target.
  • Search match highlighting — A resolver that highlights all occurrences of a search term within a text view.
  • Diff views — A resolver that colors added lines green and removed lines red, or highlights changed characters within changed lines.

All of these are just different TextStyleResolver implementations. The render pipeline, the widget integration, and the per-character coloring are already done. Spellcheck proved the pattern works for real-time, per-keystroke highlighting. The next ones are easier.

Try It

Connect to Phosphor BBS and type a misspelled word in any text field:

telnet phosphorbbs.net 2323
# or
ssh [email protected] -p 2222

Type helo wrld and watch it light up yellow. Fix the typos and the yellow disappears. No menu, no prompt, no interruption.

jterm is open source under Apache 2.0. The spellchecker is in io.jterm.completion — drop it into any jterm widget with setSpellcheckDictionary().

#BBS #terminal #UX #opensource #self-hosted #digitalthirdplace #dtp