DEV Community

Dev TNG
Dev TNG

Posted on

Building Cross-Language TUIs: One Go Binary to Rule Them All published

Building Cross-Language TUIs: One Go Binary to Rule Them All

A single Go binary can provide terminal UI components for CLI tools written in any language. By using subprocess calls with JSON communication, we reduced UI development time by 3x and eliminated inconsistencies across Python, Ruby, and JavaScript implementations—all while maintaining a 10-50ms overhead that's imperceptible to users.

What Is a Cross-Language TUI Architecture?

A cross-language TUI (Terminal User Interface) architecture uses one compiled binary to handle all terminal UI rendering for CLI tools built in different programming languages. Instead of maintaining separate TUI implementations in Python (Rich/Textual), Ruby (TTY), JavaScript (Ink/Blessed), and Go (Bubble Tea), you build once in Go and call it via subprocess from any language.

The approach works through three components: a standalone Go binary with UI components, JSON-based data exchange, and thin wrapper libraries in each target language that hide subprocess complexity.

Why Build a Shared TUI Binary?

The Multi-Language CLI Problem

When building CLI tools across multiple languages, terminal UIs become a maintenance bottleneck. For TNG (our test generation tool), we needed consistent, polished terminal UIs across Python, Ruby, JavaScript, and Go implementations.

Traditional approaches meant:

  • 4x development cost for every new UI component
  • Inconsistent user experience with different keybindings and styling
  • Language-specific bugs that required debugging in four separate codebases
  • Slower feature delivery as changes propagated across implementations

The go-ui Solution

We built go-ui—a single Go binary that provides terminal UI components for all our CLI tools, regardless of their implementation language. Based on our deployment across three production CLI tools, this reduced UI feature delivery time by 3x and cut UI-related bugs by 50%.

How Cross-Language TUI Works

Architecture Overview

go-ui is a standalone binary built with Bubble Tea. Instead of importing TUI libraries in each language, we call the binary as a subprocess with JSON for communication:

Python (tng-python):

result = subprocess.run(
    ["go-ui", "menu", "--data", json_data, "--output", output_file],
    check=False
)
choice = open(output_file).read()
Enter fullscreen mode Exit fullscreen mode

Ruby (tng):

system(@binary_path, "list-view", "--data", json_data, "--output", tmpfile.path)
selected = File.read(tmpfile.path).strip
Enter fullscreen mode Exit fullscreen mode

JavaScript (tng-js):

execSync(`go-ui progress --title "${title}" --control ${controlFile}`)
Enter fullscreen mode Exit fullscreen mode

Communication Protocol

The binary accepts JSON input via command-line arguments or files and returns results through stdout or designated output files. For complex workflows requiring real-time updates (like progress bars), we use a control file that both processes can read/write.

Available UI Components

Component Purpose Input Format Output Format
menu Interactive selection menus JSON array of items Plain text selection
list-view Searchable, paginated lists JSON with title and items Selected item JSON
progress Multi-step progress bars Control file path Status updates
spinner Loading indicators Title string Completion signal
stats Formatted statistics displays JSON metrics object Rendered table
post-generation-menu Success screens with actions File path and commands User choice

Real-World Implementation Example

In TNG-Python, generating tests involves multiple UI interactions that all use the same go-ui binary:

# Show file selection
selected_file = go_ui_session.show_list_view(
    "Select Python File",
    [{"name": file.name, "path": str(file.parent)} for file in files]
)

# Show progress with live updates
def progress_handler(updater):
    updater.update("Analyzing code...", 10)
    updater.update("Calling API...", 40)
    updater.update("Generating tests...", 80)
    return {"message": "Done!", "result": data}

result = go_ui_session.show_progress("Generating tests", progress_handler)

# Show post-generation options
choice = go_ui_session.show_post_generation_menu(file_path, run_command)
Enter fullscreen mode Exit fullscreen mode

All three calls use the same go-ui binary that TNG-Ruby and TNG-JS also use with identical behavior.

Benefits of Shared TUI Architecture

Single Source of Truth

  • One codebase for all TUI components
  • Fix a bug once, fixed everywhere
  • Add a feature once, available everywhere

Consistent User Experience

  • Identical look and feel across Python, Ruby, and JS CLIs
  • Same keybindings (arrow keys, enter, esc, q)
  • Same visual styling and animations

Language-Agnostic Design

  • No need to learn language-specific TUI libraries
  • Works with any language that can spawn subprocesses
  • Portable across platforms with compiled binaries

Development Velocity

  • New UI features don't require changes to 4 codebases
  • Designers can iterate on a single implementation
  • Less testing surface area

Performance Characteristics

  • Go's compiled binaries provide fast startup
  • Bubble Tea is highly optimized for terminal rendering
  • No runtime overhead from interpreted languages

Performance Trade-offs and Considerations

Process Spawning Overhead

Subprocess calls add 10-50ms latency per UI interaction. In our testing across 500+ user sessions, this overhead is imperceptible for interactive CLIs where users spend seconds making selections. This approach is not suitable for high-frequency updates (>10 per second) or real-time data streaming.

Binary Distribution

You need to bundle platform-specific binaries (darwin-arm64, linux-amd64, windows-amd64, etc.), which adds approximately 5MB per platform to your package size. We ship pre-compiled binaries in each language package and use platform detection at runtime.

Serialization Constraints

Data must be JSON-serializable, which works well for typical CLI data structures. For complex workflows like progress tracking, we use file-based communication where the Go binary reads a control file for updates.

Implementation Best Practices

1. Use File-Based Communication for Complex Data

File-based communication provides better reliability for large payloads and enables bidirectional updates:

with tempfile.NamedTemporaryFile() as f:
    subprocess.run(["go-ui", "menu", "--output", f.name])
    result = f.read()
Enter fullscreen mode Exit fullscreen mode

2. Bundle Platform-Specific Binaries

Detect the user's platform and load the appropriate binary:

def _find_binary():
    system = platform.system().lower()
    machine = platform.machine().lower()
    binary_name = f"go-ui-{system}-{machine}"
    return package_dir / "binaries" / binary_name
Enter fullscreen mode Exit fullscreen mode

3. Provide Wrapper Classes

Hide subprocess details from end users with clean APIs:

class GoUISession:
    def show_menu(self, items):
        # Abstract away subprocess complexity
        ...
Enter fullscreen mode Exit fullscreen mode

4. Handle Binary Failures Gracefully

Always check subprocess return codes and provide fallbacks:

try:
    result = subprocess.run([binary_path, "menu"], 
                          capture_output=True, check=True)
except subprocess.CalledProcessError:
    # Fall back to simple text prompts
    return input("Select option: ")
Enter fullscreen mode Exit fullscreen mode

Measured Results from Production Use

Since adopting go-ui across TNG implementations:

Metric Result
UI feature delivery speed 3x faster (one implementation vs. four)
UI inconsistencies Zero across Python, Ruby, and JavaScript
UI-related bugs 50% reduction in issue trackers
New language support time Hours instead of weeks
Binary size per platform 5MB (acceptable for most CLI distributions)

When to Use This Architecture

Use a shared TUI binary when:

  • Building CLI tools in multiple languages
  • You need consistent UX across implementations
  • UI updates should propagate instantly to all tools
  • Your team lacks TUI expertise in each language
  • Subprocess latency (<50ms) is acceptable

Avoid this approach when:

  • Building a single-language tool (use native libraries)
  • You need sub-10ms UI updates
  • Binary size is critically constrained (<1MB total)
  • Your target platforms don't support subprocess spawning

Frequently Asked Questions

How does subprocess overhead affect user experience?

In our testing, the 10-50ms subprocess latency is imperceptible to users in interactive CLIs. Users typically spend 1-5 seconds reading options and making selections, making the overhead negligible. However, this approach isn't suitable for real-time streaming or high-frequency updates where you need sub-10ms response times.

What platforms does go-ui support?

We ship pre-compiled binaries for macOS (darwin-arm64, darwin-amd64), Linux (linux-amd64, linux-arm64), and Windows (windows-amd64). The Go binary detection automatically selects the correct version at runtime. Adding new platforms requires only compiling for that target architecture.

Can I use this with languages other than Python, Ruby, and JavaScript?

Yes—any language that can spawn subprocesses and read/write JSON works. We've successfully tested with PHP, Rust, and even Bash scripts. The only requirement is subprocess execution capability and JSON parsing support.

How do you handle binary updates across language packages?

We version the go-ui binary and include the version number in each language package's dependency specification. When we update the Go binary, we bump the version in all language packages simultaneously. Each package downloads the appropriate binary during installation.

What happens if the go-ui binary fails or isn't found?

Our wrapper classes catch binary failures and fall back to simple text-based prompts using the language's native input functions. This ensures CLI tools remain functional even if the binary is missing or incompatible, though users lose the enhanced TUI experience.

How do you handle different terminal capabilities?

The Bubble Tea library in go-ui automatically detects terminal capabilities and degrades gracefully. It supports 256-color terminals, true color, and falls back to basic 16-color mode when necessary. No configuration needed from the calling code.

Is this approach suitable for web-based terminals?

Yes, as long as the web terminal supports standard ANSI escape sequences and proper PTY emulation. We've tested successfully in VS Code's integrated terminal, Replit, and GitHub Codespaces.

What about Windows command prompt vs PowerShell?

Both work, though PowerShell provides better rendering for complex UI components. The binary detects the terminal environment and adjusts escape sequences accordingly. Windows Terminal provides the best experience on Windows.

Key Takeaway

Building CLI tools in multiple languages? A shared TUI binary eliminates code duplication while maintaining consistency. We ship one Go binary with all terminal UI components and call it from Python, Ruby, and JavaScript—delivering 3x faster development and zero UI inconsistencies across implementations.

Top comments (0)