jqp: Building a Real-Time jq Playground with Go and Bubbletea
Hook
The average developer spends 15 minutes iterating on a complex jq query through trial and error at the command line. jqp reduces that to seconds with instant visual feedback—no more context switching between editor, terminal, and documentation.
Context
If you’ve worked with JSON at the command line, you know the jq learning curve is steep. The tool is powerful—capable of transforming, filtering, and reshaping JSON with elegant one-liners—but developing queries is painful. You craft a filter, pipe your JSON through it, squint at the output, realize you got the array index wrong, edit the command, run it again, discover you need a different operator, and repeat. Each iteration breaks your flow.
The command-line jq workflow assumes you already know what you’re doing. There’s no feedback until you execute, no syntax highlighting in your query, and no easy way to see both your input structure and output simultaneously. Web-based jq playgrounds exist, but they require leaving the terminal, copying data, and context-switching. What developers needed was an interactive environment that lived where they already work—in the terminal—with the instant feedback loop of a REPL but the visual clarity of a GUI. jqp fills this gap by turning jq experimentation from a frustrating loop into an interactive exploration.
Technical Insight
The architecture of jqp is a masterclass in modern Go TUI development, built entirely on the Charm ecosystem. At its core, jqp uses the Bubbletea framework, which implements the Elm Architecture pattern: the entire application is a state machine where user inputs trigger messages, those messages update the model, and the view renders based on the new state. This functional approach eliminates the callback spaghetti common in traditional UI frameworks.
The most critical architectural decision was using gojq instead of shelling out to the native jq binary. Itchyny’s gojq is a pure Go implementation of jq that provides the same query language without external dependencies. This means jqp compiles to a single static binary with no installation requirements beyond the executable itself. The integration looks like this:
import "github.com/itchyny/gojq"
func executeQuery(queryStr string, input interface{}) ([]interface{}, error) {
query, err := gojq.Parse(queryStr)
if err != nil {
return nil, fmt.Errorf("parse error: %w", err)
}
code, err := gojq.Compile(query)
if err != nil {
return nil, fmt.Errorf("compile error: %w", err)
}
var results []interface{}
iter := code.Run(input)
for {
v, ok := iter.Next()
if !ok {
break
}
if err, ok := v.(error); ok {
return nil, err
}
results = append(results, v)
}
return results, nil
}
The gojq API provides parse, compile, and execution phases separately, allowing jqp to show syntax errors immediately as you type (parse errors) versus runtime errors (execution errors). This separation enables the real-time feedback that makes the tool so effective.
The UI layout demonstrates thoughtful use of Bubbles components—reusable UI widgets from the Charm ecosystem. The interface splits into three synchronized viewports: the query input (a textarea component), the JSON input preview (a viewport showing your source data), and the output panel (another viewport displaying results). Each viewport independently handles scrolling, but they’re coordinated through Bubbletea’s message passing:
type QueryChangedMsg struct {
Query string
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case QueryChangedMsg:
// Execute query and update output viewport
results, err := executeQuery(msg.Query, m.jsonInput)
m.output = results
m.error = err
return m, nil
case tea.KeyMsg:
// Delegate to textarea component
var cmd tea.Cmd
m.queryInput, cmd = m.queryInput.Update(msg)
// Check if query content changed
if m.lastQuery != m.queryInput.Value() {
m.lastQuery = m.queryInput.Value()
return m, func() tea.Msg {
return QueryChangedMsg{Query: m.queryInput.Value()}
}
}
return m, cmd
}
return m, nil
}
This message-passing architecture means the query execution is triggered by state changes, not by hooking into keyboard events directly. It’s declarative and testable—you can verify behavior by sending messages to the model and inspecting the resulting state.
Syntax highlighting leverages the chroma library, which provides lexers for numerous languages including both JSON and jq. Lipgloss (Charm’s styling library) applies the chroma color schemes to rendered text, creating a cohesive visual experience. The theming system supports over 40 color schemes, all configurable through command-line flags or environment variables, making jqp adaptable to any terminal aesthetic.
The application also handles both regular JSON and newline-delimited JSON (NDJSON) gracefully. For NDJSON, each line is parsed as a separate JSON object and stored in an array, allowing you to write queries like .[].field to process streaming data sources. This makes jqp practical for real-world scenarios like analyzing log files or API responses that arrive line-by-line.
Gotcha
The most significant limitation is gojq compatibility. While gojq implements the vast majority of jq’s functionality, it’s not the canonical implementation. If you’re using bleeding-edge jq features or relying on specific behavior quirks of the C implementation, you might encounter subtle differences. The gojq maintainer is responsive and the implementation is mature, but it’s not a perfect drop-in replacement. Always test your final queries with the actual jq binary if you’re deploying them to production systems.
Performance is another consideration. TUI applications render the entire interface on every frame, and while Bubbletea is efficient, processing massive JSON files (multi-gigabyte datasets) will bog down both the parsing and rendering. The application needs to hold your JSON in memory and re-render output on every keystroke, which becomes impractical beyond a certain data size. For large files, you’re better off using command-line jq with a subset of your data, then moving to jqp once you’ve narrowed down the problem. Additionally, clipboard integration on Linux requires xclip or xsel installed on your system—it’s not built into the terminal like it is on macOS. This is a common limitation of terminal applications, but it can surprise users who expect clipboard support to just work.
Verdict
Use if: You’re learning jq syntax and need immediate feedback on query results; you regularly explore unfamiliar JSON structures and want to iterate quickly on transformations; you value terminal-native workflows and want to avoid context-switching to web tools; or you need to experiment with complex jq queries where seeing the intermediate steps is crucial. jqp excels as a development and exploration tool that makes jq accessible and debuggable. Skip if: You’re processing extremely large JSON files that would overwhelm a TUI’s memory and rendering capabilities; you need absolute compatibility with the latest jq features that gojq hasn’t implemented; you’re writing automated scripts where the standard jq CLI is more appropriate; or you prefer GUI applications over terminal interfaces. jqp is for interactive development, not production data pipelines.