Ghost Text Completion: Bringing Modern Editor UX to the Terminal
August 14, 2026
If you’ve used VS Code, fish shell, or Google’s search bar, you’ve seen ghost text — that dim, semi-transparent suggestion that appears ahead of your cursor as you type. You ignore it if it’s wrong. You press Tab to accept it if it’s right. It never gets in your way, but when it works, it saves you keystrokes.
We just shipped ghost text completion in jterm and Phosphor BBS. Every text input — chat messages, board posts, mail compose, even the mail “To” field — now suggests completions as you type. Press Tab to accept. Press anything else to dismiss.
No popups. No menus. No context switches. Just a dim hint ahead of your cursor.
How It Works
The completion system has three layers:
1. Trie (prefix tree) — A classic data structure for O(k) prefix lookups, where k is the length of the prefix being typed. We load a word list into the trie at startup — common English words, BBS usernames, chat channel names — and every keystroke triggers a prefix lookup against it. With 50,000+ words in the trie, lookups are still instantaneous because the cost scales with prefix length, not dictionary size.
2. CompletionProvider — A functional interface (String suggest(String text, int cursorPos)) that any widget can attach. The provider examines what you’ve typed before the cursor, finds the shortest word in the trie that starts with your prefix, and returns the suffix. If you’ve already typed a complete word with no longer alternatives, it returns null — no suggestion.
3. GhostTextSupport — The rendering layer. It draws the suggestion suffix in dim text (bright black on the default background) starting one cell after the cursor. It handles both single-line (TextBox) and multi-line (TextArea) widgets, with wrapping support for long suggestions that extend past the end of a line.
Context-Aware Completions in the BBS
Phosphor BBS extends this with BbsCompletionProvider — a context-aware provider that loads different word sources depending on where you’re typing:
- Chat input — usernames + channel names + English words. Type
@jand it suggests@joseph. Type#sand it suggests#sysop. - Mail “To” field — usernames only. No point suggesting English words in a recipient field.
- Board post subject/body — English words only. You’re writing content, not addressing people.
- Custom combinations — A builder API lets you mix any word sources:
BbsCompletionProvider.builder().addUsernames(authService).addChannels(chatBus).addEnglishWords().build()
The provider is queried after every keystroke, but the trie lookup is so fast that there’s no perceptible delay. The suggestion appears in the same frame as the character you typed.
The Interaction Model
We followed the fish shell / VS Code convention:
- Tab accepts the suggestion and inserts the full word
- Space always types a literal space — it dismisses the suggestion but never accepts it. This prevents accidental completions when you’re typing a word that happens to be a prefix of another word.
- Any other key dismisses the suggestion and types normally
- Escape dismisses without inserting
This is important: ghost text is a hint, not a prompt. It never interrupts you. It never blocks input. It never shows a popup that steals focus from the text field. If you ignore it completely and keep typing, it updates or disappears on its own.
Why Not a Popup?
Traditional autocomplete uses a dropdown list below the cursor. That works in GUI editors but is terrible in a terminal:
- Popups obscure the content below the cursor
- They require arrow keys to navigate, stealing keystrokes from the text field
- They need a separate render layer on top of the screen buffer
- They’re visually noisy for a feature that should be subtle
Ghost text is simpler. One suggestion, rendered inline, no UI chrome. If it’s right, Tab. If it’s wrong, keep typing. The entire interaction is one keystroke.
The Trie Advantage
We chose a trie over a sorted list or hash set for one reason: prefix matching. When you type com, we need to find words starting with com — computer, community, complete, configuration. A hash set can’t do prefix lookups. A sorted list requires binary search plus scanning forward. A trie walks three nodes deep and returns the answer.
The trie also gives us the shortest completion for free — a BFS from the prefix node finds the nearest terminal node. This means when you type app, you get apple (5 letters) not application (11 letters). Shorter completions are more likely to be what you meant, and if they’re not, you keep typing and the suggestion refines.
Building on It: Per-Character Coloring
The ghost text system proved that per-cell styling works in the terminal render pipeline. We’re now building on that foundation with TextStyleResolver — a functional interface that returns per-character colors for any text widget. This enables spellcheck highlighting (misspelled words in yellow), syntax highlighting, diff views, and search match highlighting — all using the same TextGraphics buffer that already powers every screen.
The pattern is the same: a resolver function that the widget calls during drawComponent. If it returns null, the character gets the default style. If it returns a TextCell, that cell’s colors are used. Backward compatible — no resolver means existing behavior.
Try It
Connect to Phosphor BBS and start typing in any text field. The dim suggestion ahead of your cursor is the ghost text. Press Tab to accept.
telnet phosphorbbs.net 2323
# or
ssh [email protected] -p 2222
jterm is open source under Apache 2.0. The completion system is in io.jterm.completion — drop it into any jterm widget with setCompletionProvider().
#BBS #terminal #UX #opensource #self-hosted #digitalthirdplace #dtp