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()
Ruby (tng):
system(@binary_path, "list-view", "--data", json_data, "--output", tmpfile.path)
selected = File.read(tmpfile.path).strip
JavaScript (tng-js):
execSync(`go-ui progress --title "${title}" --control ${controlFile}`)
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)
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()
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
3. Provide Wrapper Classes
Hide subprocess details from end users with clean APIs:
class GoUISession:
def show_menu(self, items):
# Abstract away subprocess complexity
...
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: ")
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)