tmux client library for Go
Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, watch for changes via control mode, capture pane contents.
Quickstart
Install the package and run a complete program.
Guides
Task guides for Go.
Topics
Subject-by-subject, with Go in every code block.
Examples
Complete programs, sourced from the code each port tests.
API reference
Types and functions of the Go API.
Testing
Test code that uses tmux with an isolated server.
Install
go get github.com/libtmux/libtmux-goAt a glance
Server → Session → Window → Pane. Each level of the tmux hierarchy has its own entry in the Go API, with the tmux object it wraps.
| Type | Wraps |
|---|---|
tmux.Server | A tmux server |
tmux.Session | A session, ID $n |
tmux.Window | A window in a session, ID @n |
tmux.Pane | A pane in a window, ID %n |
What it does
Capability: Navigate tmux via a typed object graph
// Command quickstart runs a command in tmux and prints what it showed.package main
import ( "context" "fmt" "log" "os"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := run(context.Background()); err != nil { log.Fatal(err) }}
func run(ctx context.Context) error { // A private server: a socket and an empty config of its own, so this // neither touches nor is changed by a tmux you already use. // The process ID in the socket name keeps one run apart from the next. server, err := tmux.NewServer(tmux.ServerOptions{ SocketName: fmt.Sprintf("libtmux-quickstart-%d", os.Getpid()), ConfigFile: os.DevNull, }) if err != nil { return err } session, err := server.NewSession(ctx, tmux.NewSessionRequest{ Name: "demo", }) if err != nil { return err }
// The command runs in a window of its own, with a terminal. result, err := session.Run( ctx, "echo hello from tmux", tmux.RunOptions{}, ) if err != nil { return err } for _, line := range result.Lines { fmt.Println(line) } fmt.Println("exit status:", result.Status) return nil}// Command quickstart runs a command in tmux and prints what it showed.package main
import ( "context" "fmt" "log" "os"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := run(context.Background()); err != nil { log.Fatal(err) }}
func run(ctx context.Context) error { // A private server: a socket and an empty config of its own, so this // neither touches nor is changed by a tmux you already use. // The process ID in the socket name keeps one run apart from the next. server, err := tmux.NewServer(tmux.ServerOptions{ SocketName: fmt.Sprintf("libtmux-quickstart-%d", os.Getpid()), ConfigFile: os.DevNull, }) if err != nil { return fmt.Errorf("configure tmux: %w", err) } session, err := server.NewSession(ctx, tmux.NewSessionRequest{ Name: "demo", }) if err != nil { return fmt.Errorf("create session: %w", err) }
// The command runs in a window of its own, with a terminal. result, err := session.Run( ctx, "echo hello from tmux", tmux.RunOptions{}, ) if err != nil { return fmt.Errorf("run command: %w", err) } for _, line := range result.Lines { fmt.Println(line) } fmt.Println("exit status:", result.Status) return nil}// Command quickstart runs a command in tmux and prints what it showed.package main
import ( "context" "errors" "fmt" "log" "os"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := run(context.Background()); err != nil { log.Fatal(err) }}
func run(ctx context.Context) (err error) { // A private server: a socket and an empty config of its own, so this // neither touches nor is changed by a tmux you already use. // The process ID in the socket name keeps one run apart from the next. server, err := tmux.NewServer(tmux.ServerOptions{ SocketName: fmt.Sprintf("libtmux-quickstart-%d", os.Getpid()), ConfigFile: os.DevNull, }) if err != nil { return err } session, err := server.NewSession(ctx, tmux.NewSessionRequest{ Name: "demo", }) if err != nil { return err } // Cleanup runs even when ctx is the reason the program is leaving. defer func() { err = errors.Join(err, server.Kill(context.WithoutCancel(ctx))) }()
// The command runs in a window of its own, with a terminal. result, err := session.Run( ctx, "echo hello from tmux", tmux.RunOptions{}, ) if err != nil { return err } for _, line := range result.Lines { fmt.Println(line) } fmt.Println("exit status:", result.Status) return nil}// Command quickstart runs a command in tmux and prints what it showed.package main
import ( "context" "errors" "fmt" "log" "os"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := run(context.Background()); err != nil { log.Fatal(err) }}
func run(ctx context.Context) (err error) { // A private server: a socket and an empty config of its own, so this // neither touches nor is changed by a tmux you already use. // The process ID in the socket name keeps one run apart from the next. server, err := tmux.NewServer(tmux.ServerOptions{ SocketName: fmt.Sprintf("libtmux-quickstart-%d", os.Getpid()), ConfigFile: os.DevNull, }) if err != nil { return fmt.Errorf("configure tmux: %w", err) } session, err := server.NewSession(ctx, tmux.NewSessionRequest{ Name: "demo", }) if err != nil { return fmt.Errorf("create session: %w", err) } // Cleanup runs even when ctx is the reason the program is leaving. defer func() { err = errors.Join(err, server.Kill(context.WithoutCancel(ctx))) }()
// The command runs in a window of its own, with a terminal. result, err := session.Run( ctx, "echo hello from tmux", tmux.RunOptions{}, ) if err != nil { return fmt.Errorf("run command: %w", err) } for _, line := range result.Lines { fmt.Println(line) } fmt.Println("exit status:", result.Status) return nil}Run this example
POSIX shell, tmux 3.3a or newer and Go 1.26.5.
Save the program above as quickstart.go in a new directory, along with the files below.
Cleanup is omitted; the private tmux server and its temporary files may remain. Select Cleanup to remove them when the program finishes. The program stops its private tmux server and removes any temporary files it created when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
go.mod
module example.com/libtmux-home
go 1.26.0
require github.com/libtmux/libtmux-go v0.1.0Setup and run
go mod tidygo build -p 2 -o quickstart quickstart.gounset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh./quickstartExpected output
hello from tmuxexit status: 0Library revision: c21afc69867e.
// Command control-mode-subscribe watches a persistent tmux control client// receive changes without polling.package main
import ( "context" "errors" "fmt" "log" "os" "time"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := start(); err != nil { log.Fatal(err) }}
// start owns cleanup because log.Fatal skips deferred calls in main.func start() error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()
// Choose an unused socket name: cleanup kills the server selected here. // The empty config keeps personal settings out of a new server. server, err := tmux.NewServer(tmux.ServerOptions{ SocketName: "libtmux-control-mode-subscribe", ConfigFile: os.DevNull, }) if err != nil { return fmt.Errorf("configure tmux server: %w", err) } return run(ctx, server)}
// run accepts injected server state so tests can isolate the example.func run(ctx context.Context, server tmux.Server) (err error) { session, err := server.NewSession(ctx, tmux.NewSessionRequest{Name: "libtmux-control"}) if err != nil { return fmt.Errorf("create session: %w", err) } defer func() { cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second) defer cleanupCancel() err = errors.Join(err, session.Kill(cleanupCtx)) }()
// docs:watching given:ctx context.Context; session tmux.Session stream, err := session.OpenNotifications(ctx, tmux.NotificationOptions{}) if err != nil { return fmt.Errorf("open notification stream: %w", err) } defer func() { err = errors.Join(err, stream.Close()) }()
// Rename after subscribing; notifications do not include earlier changes. if _, err := session.Rename(ctx, "control-example"); err != nil { return fmt.Errorf("rename session: %w", err) }
for { notification, err := stream.Next(ctx) if err != nil { return fmt.Errorf("read notification: %w", err) } fmt.Printf("notification: %s\n", notification.Kind()) if notification.Kind() == tmux.ControlNotificationSessionRenamed { fmt.Println("heard the rename") break } } // docs:end
// docs:subscribing given:ctx context.Context; session tmux.Session; stream *tmux.NotificationStream // A subscription is a format tmux evaluates for you: it reports the value // when it first looks, about a second later, and then each time it changes. if err := stream.Subscribe(ctx, tmux.SubscriptionRequest{ Name: "windows", Format: "#{session_windows}", }); err != nil { return fmt.Errorf("subscribe: %w", err) } if _, err := session.NewWindow(ctx, tmux.NewWindowRequest{}); err != nil { return fmt.Errorf("open window: %w", err) } for { notification, err := stream.Next(ctx) if err != nil { return fmt.Errorf("read notification: %w", err) } if change, ok := notification.Subscription(); ok && change.Value == "2" { fmt.Println("session has", change.Value, "windows") return nil } } // docs:end}
The whole program, examples/control-mode-subscribe/main.go. The port's CI runs it in .github/workflows/tests.yml from examples/: go test -race -count=1.
// Command run-to-completion runs a program in a tmux pane and reads its exit// status and screen back, the way os/exec would if the program had a terminal.package main
import ( "context" "errors" "fmt" "log" "os" "time"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := start(); err != nil { log.Fatal(err) }}
// start owns cleanup because log.Fatal skips deferred calls in main.func start() error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()
// Choose an unused socket name: cleanup kills the server selected here. // The empty config keeps personal settings out of a new server. server, err := tmux.NewServer(tmux.ServerOptions{ SocketName: "libtmux-run-to-completion", ConfigFile: os.DevNull, }) if err != nil { return fmt.Errorf("configure tmux server: %w", err) } return run(ctx, server)}
// run accepts injected server state so tests can isolate the example.func run(ctx context.Context, server tmux.Server) (err error) { session, err := server.NewSession(ctx, tmux.NewSessionRequest{Name: "libtmux-run"}) if err != nil { return fmt.Errorf("create session: %w", err) } defer func() { cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second) defer cleanupCancel() err = errors.Join(err, session.Kill(cleanupCtx)) }()
// docs:run-to-completion given:ctx context.Context; session tmux.Session // The command runs with a terminal, so test -t holds where a pipe fails. command := "test -t 1 && echo stdout is a terminal; exit 3" result, err := session.Run(ctx, command, tmux.RunOptions{}) if err != nil { return fmt.Errorf("run command: %w", err) } for _, line := range result.Lines { fmt.Println("screen:", line) } fmt.Println("exited", result.Status) // docs:end
// docs:run-streaming given:ctx context.Context; session tmux.Session // Start returns while the command is still running, so its output can be // followed and it can be stopped from another goroutine. The stream begins // where StreamTo opens it, so this command waits before its first line; // result.Lines holds the screen either way. running, err := session.Start(ctx, "sleep 1; seq 1 3; sleep 30", tmux.RunOptions{}) if err != nil { return fmt.Errorf("start command: %w", err) } stopped := make(chan error, 1) go func() { time.Sleep(2 * time.Second) stopped <- running.Kill(ctx) }() streamed, err := running.StreamTo(ctx, os.Stdout) if err != nil { return fmt.Errorf("stream command: %w", err) } if err := <-stopped; err != nil { return fmt.Errorf("stop command: %w", err) } fmt.Println("stopped:", streamed.Signal) // docs:end return nil}
The whole program, examples/run-to-completion/main.go. The port's CI runs it in .github/workflows/tests.yml from examples/: go test -race -count=1.
Typed API
- Typed objects instead of parsed format strings:
tmux.Pane - Addressing by tmux ID:
tmux.PaneID - Filtering and queries over tmux objects:
tmux.PaneFilter - Typed errors instead of exit codes and stderr:
tmux.CommandError
What you get over the tmux CLI
- Connect to or start a tmux server:
tmux.Server.Start - Traversal from a server down to its panes:
tmux.Server.Panes - Send keys to a pane:
tmux.Pane.SendKeys - Watch for changes through tmux control mode:
tmux.ControlClient - Capture pane contents:
tmux.Pane.Capture - Arguments passed as a list, with no shell quoting:
tmux.Server.Cmd - A typed error when the running tmux lacks a feature:
tmux.VersionTooLowError
Know where you're running
Inside tmux, find the pane your program runs in from TMUX and TMUX_PANE: tmux.PaneFromEnv.
Testing
Run tests against an isolated tmux server: tmuxtest.NewServer.