Back to Articles

Building Interactive TUI Playgrounds: Inside jqp's Real-Time Query Engine

[ View on GitHub ]

Building Interactive TUI Playgrounds: Inside jqp's Real-Time Query Engine

Hook

While most developers debug JSON transformations by running jq commands repeatedly in their terminal, copying and pasting queries into text files, jqp takes a different approach: what if your jq playground updated in real-time as you typed, with syntax highlighting and instant feedback built directly into your terminal?

Context

Anyone who's worked with jq knows the workflow: craft a query, run it, see the output, realize it's wrong, adjust the query, run it again. For complex JSON transformations, this cycle repeats dozens of times. Browser-based tools like jqplay.org solve this with instant feedback, but require leaving the terminal and pasting data into a web form—breaking your flow and potentially exposing sensitive data to third-party services.

jqp emerged from this friction point. Created by Noah Gorstein, it brings the interactive playground experience directly into the terminal where developers already live. Rather than context-switching to a browser or manually re-running commands, jqp provides a split-panel TUI where queries execute immediately as you type them. It's particularly valuable when exploring unfamiliar JSON structures or debugging multi-stage jq pipelines, where seeing intermediate results speeds up the learning process dramatically.

Technical Insight

The architecture of jqp reveals several clever decisions about building responsive terminal applications. At its core, it uses Charm's Bubbletea framework, which implements the Elm Architecture pattern for terminal UIs. This means the entire application state lives in a single model struct, updates happen through messages, and the view is pure—a rendering function that takes state and returns what to display.

What makes jqp particularly interesting is its query execution strategy. Rather than shelling out to the jq binary, it uses gojq, a pure Go implementation of jq. This decision has cascading benefits: the entire application compiles to a single static binary with zero runtime dependencies, cross-compilation becomes trivial, and query execution happens in-process without fork/exec overhead. Here's how the query execution flow works:

// Simplified example of jqp's query execution
func executeQuery(query string, input []byte) (string, error) {
    q, err := gojq.Parse(query)
    if err != nil {
        return "", fmt.Errorf("parse error: %w", err)
    }
    
    var data interface{}
    if err := json.Unmarshal(input, &data); err != nil {
        return "", fmt.Errorf("invalid JSON: %w", err)
    }
    
    iter := q.Run(data)
    var results []string
    
    for {
        v, ok := iter.Next()
        if !ok {
            break
        }
        if err, isErr := v.(error); isErr {
            return "", err
        }
        output, _ := json.MarshalIndent(v, "", "  ")
        results = append(results, string(output))
    }
    
    return strings.Join(results, "\n"), nil
}

This pattern—parsing the query, unmarshaling input, creating an iterator, and streaming results—mirrors jq's own architecture but happens entirely in Go. The iterator model is crucial because jq queries can produce multiple outputs (like .[] | select(.age > 30) from an array), and gojq handles this elegantly without buffering everything in memory.

The TUI layout uses a three-viewport design: query input at the top, JSON input preview on the bottom left, and output on the bottom right. Each viewport is a Bubbles component—reusable UI primitives from the Charm ecosystem. When you type a query, Bubbletea sends an update message, the model recalculates the output by executing the query, and the view re-renders. This reactive flow means jqp never blocks the UI thread; even expensive queries remain responsive because Go's concurrency primitives handle the heavy lifting.

Syntax highlighting deserves special attention. jqp integrates Chroma, the same highlighting library used by Hugo and GitHub, to colorize both the JSON input and output. It ships with over 50 built-in themes (dracula, monokai, solarized, etc.) and allows granular overrides through configuration files. The theming system separates syntax colors from UI chrome colors, so you can have a dark syntax theme with light UI borders, or vice versa. This level of customization is rare in TUI applications and shows attention to developer preferences.

One subtle but powerful feature is NDJSON support. Many real-world data pipelines produce newline-delimited JSON—streaming logs, database exports, API responses. jqp detects NDJSON automatically and processes each line as a separate JSON document, then aggregates results. This makes it invaluable for exploring streaming data without preprocessing:

# Exploring Kubernetes logs in NDJSON format
kubectl logs my-pod --tail=100 | jqp '.level, .msg'

# Or analyzing HTTP access logs
cat access.log | jqp 'select(.status >= 400) | {time, path, status}'

The query history mechanism uses a simple stack—arrow keys navigate backward and forward through previous queries. It's ephemeral (doesn't persist between sessions), but that's intentional: jqp is optimized for exploratory sessions, not long-term query management. The implementation is straightforward but effective, using a circular buffer to limit memory usage.

Gotcha

The biggest gotcha is behavioral drift between gojq and canonical jq. While gojq aims for compatibility, it's a separate implementation and lags behind jq's development. Edge cases exist where queries work differently—particularly around error handling, null propagation, and some newer jq features like SQL-style operators. If you're using jqp to prototype queries that will eventually run with actual jq in production, you might encounter surprises. Always validate your final queries against real jq before deploying them.

Clipboard integration on Linux requires external dependencies—specifically xclip or xsel. This isn't jqp's fault (Go's standard library doesn't include clipboard access), but it means the "copy output" feature won't work out of the box on many Linux installations. macOS and Windows handle this transparently through their native APIs, but Linux users need to install these tools separately. It's a small friction point that breaks the "single binary, no dependencies" promise slightly. Additionally, you can't edit the input JSON within jqp itself; if you realize your test data is malformed, you need to exit, fix it, and re-run. Tools like fx handle this better by allowing in-place JSON editing, though they sacrifice jq compatibility for their custom query language.

Verdict

Use jqp if: you regularly work with jq and want faster iteration cycles, you're learning jq syntax and need instant feedback on query results, you process NDJSON streams and want visual exploration without preprocessing, or you value zero-dependency single binaries for portability across systems. It's particularly excellent for pair programming sessions where visualizing transformations helps explain complex queries. Skip jqp if: you need absolute parity with the latest jq features and can't tolerate implementation differences, you're building automated scripts where a TUI adds no value (just use jq directly), you need to simultaneously edit both queries and input data in a rich editing environment, or you're on Linux and unwilling to install clipboard dependencies for the full experience. For quick data exploration and learning, jqp is unmatched; for production query development, verify against canonical jq.