A Go client for programmatic interaction with tmux terminal multiplexer.
Existing Go tmux libraries (gotmux, go-tmux, gomux) focus on workspace setup (session/window/pane creation) but lack features needed for programmatic interaction with running CLI applications:
| Feature | Existing libraries | This package |
|---|---|---|
| Multiline text via paste-buffer | No | Yes |
| Pane PID extraction | No | Yes |
| pipe-pane output capture | No | Yes |
| Context support for cancellation | No | Yes |
| Custom error types | No | Yes |
| Atomic text + Enter send | No | Yes |
go get github.com/dlorenc/multiclaude/pkg/tmuxpackage main
import (
"context"
"log"
"github.com/dlorenc/multiclaude/pkg/tmux"
)
func main() {
ctx := context.Background()
client := tmux.NewClient()
// Check if tmux is available
if !client.IsTmuxAvailable() {
log.Fatal("tmux is not installed")
}
// Create a detached session
if err := client.CreateSession(ctx, "my-session", true); err != nil {
log.Fatal(err)
}
defer client.KillSession(ctx, "my-session")
// Send a command
if err := client.SendKeys(ctx, "my-session", "0", "echo hello"); err != nil {
log.Fatal(err)
}
}All I/O methods accept a context.Context as the first parameter, enabling:
- Request cancellation
- Timeouts
- Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// This will fail if it takes longer than 5 seconds
if err := client.CreateSession(ctx, "my-session", true); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println("Operation timed out")
}
}The package provides custom error types for programmatic error handling:
import "errors"
err := client.CreateWindow(ctx, "nonexistent", "window")
if err != nil {
var cmdErr *tmux.CommandError
if errors.As(err, &cmdErr) {
log.Printf("tmux command %s failed: %v", cmdErr.Op, cmdErr.Err)
}
}
// Helper functions for common checks
if tmux.IsSessionNotFound(err) {
// Handle missing session
}
if tmux.IsWindowNotFound(err) {
// Handle missing window
}The killer feature of this package. When interacting with CLI applications that process input on Enter, you need a way to send multiline text without triggering on each line.
// Send multiline text without triggering intermediate processing
message := `This is line 1
This is line 2
This is line 3`
// SendKeysLiteral uses tmux's paste-buffer for multiline text
if err := client.SendKeysLiteral(ctx, "session", "window", message); err != nil {
log.Fatal(err)
}
// Now send Enter to submit
if err := client.SendEnter(ctx, "session", "window"); err != nil {
log.Fatal(err)
}
// Or use the atomic version to avoid race conditions
if err := client.SendKeysLiteralWithEnter(ctx, "session", "window", message); err != nil {
log.Fatal(err)
}How it works: For multiline text, the package uses tmux's paste-buffer mechanism:
tmux set-buffer "..."- stores the entire texttmux paste-buffer -t target- pastes it atomically
This ensures the application receives the complete text before any processing is triggered.
SendKeysLiteralWithEnter sends text and Enter in a single shell command, preventing race conditions where Enter might be lost between separate exec calls:
// Atomic: text + Enter in one operation
if err := client.SendKeysLiteralWithEnter(ctx, "session", "window", "echo hello"); err != nil {
log.Fatal(err)
}Monitor whether a process running in a tmux pane is still alive:
pid, err := client.GetPanePID(ctx, "session", "window")
if err != nil {
log.Fatal(err)
}
// Check if process is alive
process, err := os.FindProcess(pid)
if err != nil {
log.Printf("Process %d not found", pid)
}Capture all output from a tmux pane to a file:
// Start capturing output
if err := client.StartPipePane(ctx, "session", "window", "/tmp/output.log"); err != nil {
log.Fatal(err)
}
// ... run commands in the pane ...
// Stop capturing
if err := client.StopPipePane(ctx, "session", "window"); err != nil {
log.Fatal(err)
}HasSession(ctx context.Context, name string) (bool, error) // Check if session exists
CreateSession(ctx context.Context, name string, detached bool) error // Create new session
KillSession(ctx context.Context, name string) error // Terminate session
ListSessions(ctx context.Context) ([]string, error) // List all sessionsCreateWindow(ctx context.Context, session, name string) error // Create window in session
HasWindow(ctx context.Context, session, name string) (bool, error) // Check if window exists (exact match)
KillWindow(ctx context.Context, session, name string) error // Terminate window
ListWindows(ctx context.Context, session string) ([]string, error) // List windows in sessionSendKeys(ctx context.Context, session, window, text string) error // Send text + Enter
SendKeysLiteral(ctx context.Context, session, window, text string) error // Send text (paste-buffer for multiline)
SendEnter(ctx context.Context, session, window string) error // Send just Enter
SendKeysLiteralWithEnter(ctx context.Context, session, window, text string) error // Atomic text + EnterGetPanePID(ctx context.Context, session, window string) (int, error) // Get process PID in paneStartPipePane(ctx context.Context, session, window, outputFile string) error // Start capturing
StopPipePane(ctx context.Context, session, window string) error // Stop capturingtype SessionNotFoundError struct { Name string }
type WindowNotFoundError struct { Session, Window string }
type CommandError struct { Op, Session, Window string; Err error }
func IsSessionNotFound(err error) bool
func IsWindowNotFound(err error) bool// Use a custom tmux binary path
client := tmux.NewClient(tmux.WithTmuxPath("/usr/local/bin/tmux"))This package was designed for orchestrating multiple Claude Code agents, but is useful for any scenario requiring programmatic control of CLI applications:
- Running multiple AI assistants in parallel
- Automated testing of interactive CLI tools
- CI/CD pipelines that need to interact with terminal applications
- DevOps automation with interactive prompts
- tmux 2.0 or later (uses paste-buffer and pipe-pane)
- Go 1.21 or later
See the main project LICENSE file.