Building Python-Toolkit-Quality Prompts in Go with go-prompt
Hook
Most CLI tools in Go settle for basic readline functionality, but what if your terminal interface could match the sophistication of IPython or PostgreSQL's psql—with auto-completion, history search, and Emacs shortcuts—all in pure Go?
Context
If you've built CLI tools in Go, you've probably reached for cobra or urfave/cli to parse commands and flags. These libraries excel at non-interactive workflows: run a command, get output, exit. But what about the other category of CLI tools—the ones that keep you engaged in a conversation? Think kubectl with its interactive plugins, database clients like psql, or any REPL where you're exploring an API surface area you don't have memorized.
Historically, Go developers had limited options. You could drop down to bufio.Scanner for basic input, integrate with GNU readline through cgo (sacrificing portability and simplicity), or use minimal libraries like peterh/liner that provide basic history but lack the rich auto-completion and key binding systems found in mature prompts. Python developers, meanwhile, enjoyed python-prompt-toolkit—a library that powers IPython and brings IDE-quality features to the terminal. The c-bata/go-prompt library bridges this gap, implementing the document-based architecture and VT100 rendering model that makes sophisticated prompts possible without external dependencies or platform-specific hacks.
Technical Insight
At its core, go-prompt models user input as a Document—a stateful abstraction that tracks the text buffer and cursor position. This might seem mundane, but it's architecturally significant. Instead of treating input as a stream of characters, the Document provides programmatic access to text before and after the cursor, current lines, words under cursor, and methods for text manipulation. Every key press generates an event that may trigger an action (insert character, delete word, move cursor), which produces a new Document state. This functional approach makes it straightforward to implement features like history search or undo/redo without fighting mutable state bugs.
Here's a minimal example that demonstrates the completion system:
package main
import (
"fmt"
"github.com/c-bata/go-prompt"
)
func completer(d prompt.Document) []prompt.Suggest {
suggestions := []prompt.Suggest{
{Text: "users", Description: "List all users"},
{Text: "deploy", Description: "Deploy application"},
{Text: "status", Description: "Check system status"},
{Text: "logs", Description: "View application logs"},
}
// FilterHasPrefix filters suggestions based on what user typed
return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true)
}
func executor(input string) {
fmt.Printf("Executing: %s\n", input)
}
func main() {
p := prompt.New(
executor,
completer,
prompt.OptionPrefix(">>> "),
prompt.OptionTitle("my-cli"),
)
p.Run()
}
The completer function receives a Document snapshot and returns Suggest structs. The library handles rendering suggestions in a separate pane below the input line, keyboard navigation through suggestions (arrow keys or Ctrl+N/Ctrl+P), and filtering as you type. Critically, your completer has access to the full document state—you can inspect text before the cursor, parse partial commands, and return contextually relevant suggestions. For instance, if building a SQL client, you could parse the current query and suggest table names after "FROM" or column names after "SELECT".
The terminal I/O layer is where go-prompt's platform-aware design shines. On Unix systems, it uses /dev/tty directly rather than os.Stdin, which allows it to work correctly even when input is piped. On Windows, it interfaces with the Console API rather than relying on ANSI escape sequence support (which was incomplete before Windows 10). This abstraction is exposed through the ConsoleParser and ConsoleWriter interfaces, letting the library handle VT100 sequences for cursor movement, color changes, and screen clearing in a cross-platform way.
Key bindings are registered through a flexible system that maps key combinations to actions. The library ships with Emacs-style defaults (Ctrl+A for beginning-of-line, Ctrl+E for end-of-line, Ctrl+K to kill text to end of line), but you can override or extend these:
p := prompt.New(
executor,
completer,
prompt.OptionAddKeyBind(prompt.KeyBind{
Key: prompt.ControlT,
Fn: func(buf *prompt.Buffer) {
// Custom action: transpose characters
buf.SwapCharacters()
},
}),
)
The Buffer type provides high-level operations (InsertText, DeleteBeforeCursor, CursorLeft) that handle the document manipulation for you. Each operation calculates the new cursor position and text state, which go-prompt then renders efficiently by computing the minimal set of terminal escape sequences needed to update the display.
History is maintained automatically with configurable size limits, and the library includes history search (Ctrl+R) that filters commands as you type—similar to bash or zsh reverse-i-search. The history implementation is particularly clever: it maintains a separate "temporary" buffer when you're navigating history, so your in-progress command isn't lost if you scroll up to check a previous command then decide to continue with your current one.
Gotcha
The library's maturity cuts both ways. Development has been relatively quiet since 2020, which signals stability for production use but also means newer terminal features aren't supported. For example, it doesn't handle true color (24-bit RGB) sequences, kitty graphics protocol, or sixel graphics—features that modern terminal emulators like WezTerm or Kitty support. If you're building something that needs to render images or complex graphical elements in the terminal, go-prompt isn't the foundation you want.
More significantly, go-prompt is laser-focused on single-line input scenarios. While you can insert newlines into the buffer programmatically, the rendering and cursor movement logic assumes a horizontal workflow. If you need a multi-line editor—say, for composing SQL queries spanning multiple lines with proper indentation—you'll find yourself fighting the library's design. The Document model tracks a single cursor position, and the terminal rendering doesn't handle scrolling through a multi-line buffer with the sophistication you'd need for a comfortable editing experience. For those scenarios, consider charmbracelet/bubbletea or building a custom solution on top of tcell.
Finally, while the VT100 compatibility is generally excellent, there are edge cases with terminal emulators that implement non-standard behavior. Some older or minimal emulators may not handle the combination of cursor positioning and color codes identically, leading to rendering glitches. The library doesn't include a terminal capability detection system (like terminfo), so it assumes a reasonably modern VT100-compatible environment.
Verdict
Use go-prompt if you're building interactive CLI tools that need sophisticated auto-completion, command history with search, and rich keyboard navigation—essentially any REPL-style interface or CLI tool where users explore complex APIs or data structures. It's ideal for Kubernetes clients, database CLIs, API exploration tools, or interactive debuggers where discoverability through completion is critical. The library's production use in Rancher CLI and kube-prompt validates its reliability for serious applications. Skip it if you need simple yes/no prompts (stdlib or survey are better), require multi-line editing as a core feature, want to render graphical content in modern terminals, or are building traditional non-interactive CLI tools where cobra/urfave/cli are more appropriate. Also skip if you need active development and support for cutting-edge terminal features—consider bubbletea for a more actively maintained full-TUI framework.