Core Library0.xHome

Choose documentation 3

latest

Current version

latest
English

Early release libtmux is released, and its APIs may still change between releases. This site's structure and URLs may change too.

Edit this page on GitHub

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.

Install

go get github.com/libtmux/libtmux-go

At 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.

TypeWraps
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
Example details
// 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.0

Setup and run

go mod tidy
go build -p 2 -o quickstart quickstart.go
unset TMUX TMUX_PANE
export ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh
./quickstart

Expected output

hello from tmux
exit status: 0

Library revision: c21afc69867e.

examples/control-mode-subscribe/main.go
// 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.

examples/run-to-completion/main.go
// 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

What you get over the tmux CLI

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.

Esc

Type to search.

Reference previewOpen reference