{
  "markdown": "# libtmux for Go\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/libtmux/libtmux-go/tmux.svg)](https://pkg.go.dev/github.com/libtmux/libtmux-go/tmux)\n[![tests](https://github.com/libtmux/libtmux-go/actions/workflows/tests.yml/badge.svg)](https://github.com/libtmux/libtmux-go/actions/workflows/tests.yml)\n\nAlpha software. Releases carry an -alpha prerelease tag, the API is not\nsettled, and any release may change or remove exported identifiers without a\ndeprecation period. Pin an exact version. Not recommended for production.\n\nDrive tmux from Go: sessions, windows, and panes as typed values, every tmux\noption and hook as a typed accessor, and errors classified by what tmux actually\nrefused.\n\n- **No runtime dependencies.** The core module imports only the standard library.\n- **Go 1.26+**, tmux **3.2a through 3.7c** across the core, workspace, and MCP\n  modules. The compatibility matrix checks every release in that range.\n  The Go floor tracks upstream's support window, which covers the two most\n  recent releases.\n- **Records never refresh behind you.** A `Session` you hold is what tmux said\n  when you asked, not a live handle that changes underneath.\n\n```console\n$ go get github.com/libtmux/libtmux-go/tmux@latest\n```\n\nModules are tagged per directory, so each consumer carries its own version:\nthe tags are `mcp/vN` and `workspace/vN` beside the core's plain `vN`. Pin the\nexact ones you want in your own go.mod; the commands here fetch the newest.\n\n**Contents** — [Quick start](#quick-start) · [Querying](#what-querying-looks-like)\n· [Choosing a mode](#choosing-a-mode) · [Watching tmux](#watching-tmux) ·\n[Packages](#packages) · [For agents](#for-agents) ·\n[Testing your code](#testing-your-own-code) · [Documentation](#documentation)\n\n## Quick start\n\nMake a window, split it, send a command into the new pane:\n\n<!-- docs:quickstart -->\n\n```go\nwindowName := \"work\"\nwindow, err := session.NewWindow(ctx, tmux.NewWindowRequest{Name: &windowName})\nif err != nil {\n\treturn fmt.Errorf(\"create window: %w\", err)\n}\npane, err := window.SplitPane(ctx, tmux.SplitPaneRequest{\n\tDirection: tmux.PaneDirectionRight,\n})\nif err != nil {\n\treturn fmt.Errorf(\"split window: %w\", err)\n}\ncommand := \"printf 'libtmux ready\\\\n'\"\nif err := pane.SendKeys(ctx, tmux.SendKeysRequest{Command: &command, Literal: true}); err != nil {\n\treturn fmt.Errorf(\"send command: %w\", err)\n}\n```\n\n<!-- docs:end -->\n\nEvery Go block below marked this way is generated from a program in\n[`examples/`](examples/) that is compiled, linted, run against a real tmux, and\nswept across every supported release — so none of it can drift from code that\nworks.\n\nRunnable: [`examples/quickstart`](examples/quickstart) — `go -C examples run ./quickstart`.\n\n## What querying looks like\n\nTwo ways to ask, and they answer the same question at different costs.\n\n**Let tmux filter**, which sends one command and gets back only matches:\n\n<!-- docs:query-in-tmux -->\n\n```go\nlive := tmux.TmuxFilter(\"#{==:#{session_name},libtmux-filter}\")\nsessions, err := server.SearchSessions(ctx, &live)\n```\n\n<!-- docs:end -->\n\n**Or read once and filter in Go**, when you want several answers from one read:\n\n<!-- docs:query-in-go -->\n\n```go\nsnapshot, err := server.Snapshot(ctx)\nif err != nil {\n\treturn err\n}\npredicate, err := tmux.PaneActiveIs(true).Predicate()\nif err != nil {\n\treturn err\n}\nactive := tmuxq.Where(snapshot.Panes(), predicate)\n```\n\n<!-- docs:end -->\n\nTyped filters compose, and the generated ones push down into tmux's own `-f`\nwhere tmux can evaluate them:\n\n```go\nfilter := tmux.PaneFilter{\n\tActive:      tmux.Ptr(true),\n\tCurrentPath: tmux.Ptr(\"/home/you/project\"),\n}\npanes, err := server.SearchPanes(ctx, &filter)\n```\n\nRunnable: [`examples/filter-query`](examples/filter-query).\n\n## Choosing an execution path\n\nA plain `Server` uses the executable, environment, working directory, and\nsocket selection frozen by `NewServer`. Values derived from it retain that\nsubprocess binding. Guards on materialized values assume stable, trusted tmux\nparser primitives and aliases. Establish a connection before socket\nreplacement when exact-daemon ownership is required.\n\n| Path | Construct it with | Cost | Reach for it |\n| --- | --- | --- | --- |\n| process | `NewServer` | one tmux process per operation | one-shot commands |\n| connection | `Session.OpenControl` | one tmux client per lane | repeated commands |\n| concurrent | `ConnectionOptions{Lanes: N}` | N tmux clients | parallel readers |\n| chained | `NewPlan` then `Run` | fewer process starts | builds and layouts |\n| streaming | `Session.OpenNotifications(ctx, NotificationOptions{})` | one tmux client | watching what tmux does |\n\nPlans run over either a plain server or a connection-bound server. Unsupported\ncapability policy is separate: `ServerOptions.Unsupported` decides whether a\nrequest naming an unavailable tmux flag is refused — the default — or\ncarried out without it and reported to a warning handler.\n\nA connection carries commands without starting a process for each. It appears\nin `list-clients` and counts toward `session_attached`, which is why opening one\nis explicit:\n\n<!-- docs:control-pool -->\n\n```go\nconnection, err := session.OpenControl(ctx, tmux.ConnectionOptions{})\nif err != nil {\n\treturn fmt.Errorf(\"open control connection: %w\", err)\n}\ndefer func() { _ = connection.Close() }()\nconnected := connection.Session()\n```\n\n<!-- docs:end -->\n\nOnce established, `connection.Server()` and `connection.Session()` are bound\nto that exact daemon. Values derived from them retain that owner. The binding\nis terminal: closing the connection makes later operations return\n`ErrControlClosed`, and an operation that needs a separate process returns\n`ErrConnectionRequiresProcess`. It never falls back or rebinds. The original\nsession remains on its frozen subprocess binding.\n\n`Server.NewSessionConnection` creates a session and retains its creating\ncontrol process as the first lane. It returns the ordinary created session and\nan owned connection; use `connection.Session()` for connected operations.\n\nA plan records commands instead of running them, sends the ones needing no\nanswer together, and hands back a reference to what a step *will* create — so a\nbuild is written in one pass:\n\n<!-- docs:planning -->\n\n```go\nplan := tmux.NewPlan()\nplan.SelectLayout(window.Ref(), tmux.SelectLayoutRequest{Layout: \"tiled\"})\neditor := plan.SplitPane(window.Ref(), tmux.SplitPaneRequest{Attach: true})\nplan.SetPaneTitle(editor, \"editor\")\nplan.SendKeys(editor, tmux.SendKeysRequest{Command: tmux.Ptr(\"echo built\")})\nplan.DisplayMessage(editor, \"#{pane_title}\")\n```\n\n<!-- docs:end -->\n\nRunnable: [`examples/fast-path`](examples/fast-path) and\n[`examples/planned-build`](examples/planned-build).\n[`BENCHMARKS.md`](BENCHMARKS.md) is what each path costs, measured on every\nsupported tmux.\n\n## Watching tmux\n\n`Session.OpenNotifications` and `Server.OpenNotifications` return owned\nstreams. Zero options retain tmux changes but suppress pane output; set\n`IncludePaneOutput` when watching pane content. tmux pushes each change when it\nhappens rather than making a poll guess how often to ask. Before tmux 3.6,\ndestroying the attached session follows its `detach-on-destroy` policy and may\nend the stream:\n\n<!-- docs:watching -->\n\n```go\nstream, err := session.OpenNotifications(ctx, tmux.NotificationOptions{})\nif err != nil {\n\treturn fmt.Errorf(\"open notification stream: %w\", err)\n}\ndefer func() { err = errors.Join(err, stream.Close()) }()\n\n// Rename after subscribing; notifications do not include earlier changes.\nif _, err := session.Rename(ctx, \"control-example\"); err != nil {\n\treturn fmt.Errorf(\"rename session: %w\", err)\n}\n\nfor {\n\tnotification, err := stream.Next(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read notification: %w\", err)\n\t}\n\tfmt.Printf(\"notification: %s\\n\", notification.Kind())\n\tif notification.Kind() == tmux.ControlNotificationSessionRenamed {\n\t\tfmt.Println(\"heard the rename\")\n\t\treturn nil\n\t}\n}\n```\n\n<!-- docs:end -->\n\nRunnable: [`examples/control-mode-subscribe`](examples/control-mode-subscribe).\n\n## Packages\n\n| Package | Source | Reference | What it is |\n| --- | --- | --- | --- |\n| `tmux` | [`tmux/`](tmux/) | [pkg.go.dev](https://pkg.go.dev/github.com/libtmux/libtmux-go/tmux) | The library. Sessions, windows, panes, options, hooks, formats, filters, snapshots, plans. |\n| `tmuxtest` | [`tmux/tmuxtest/`](tmux/tmuxtest/) | [pkg.go.dev](https://pkg.go.dev/github.com/libtmux/libtmux-go/tmux/tmuxtest) | Run your program in a real tmux and assert on what it drew. |\n| `tmuxq` | [`tmuxq/`](tmuxq/) | [pkg.go.dev](https://pkg.go.dev/github.com/libtmux/libtmux-go/tmuxq) | Model-free generic helpers for slices and `iter.Seq`. |\n\nThree more ship as **separate modules**, so `go get` on the library pulls in\nnone of them:\n\n| Module | Source | Reference | What it is |\n| --- | --- | --- | --- |\n| `mcp` | [`mcp/`](mcp/) | [pkg.go.dev](https://pkg.go.dev/github.com/libtmux/libtmux-go/mcp) | **A tmux server for AI agents** over the Model Context Protocol. Install it as a binary. |\n| `workspace` | [`workspace/`](workspace/) | [pkg.go.dev](https://pkg.go.dev/github.com/libtmux/libtmux-go/workspace) | Loads tmuxp-style YAML workspaces and builds them. |\n| `benchmarks` | [`benchmarks/`](benchmarks/) | — | Prints what each way of reaching tmux costs. |\n\n### For agents\n\n[`mcp/`](mcp/) is a standalone Model Context Protocol server that gives an agent\none tmux server: create panes, send keys, read output, wait for text.\n\n```console\n$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest\n```\n\nSee [`mcp/README.md`](mcp/README.md) for client configuration, and\n[`mcp/TOOLS.md`](mcp/TOOLS.md) for the tool reference.\n\n## Testing your own code\n\n[`tmux/tmuxtest`](tmux/tmuxtest/) runs your program inside a real tmux and lets\na test assert on what it drew, with no sleeps. Run it, wait for what it draws,\ntype at it:\n\n<!-- docs:tmuxtest-quickstart -->\n\n```go\npane := tmuxtest.RunInPane(ctx, t, \"printf 'ready\\\\n'; cat\")\n\ntmuxtest.WaitForText(ctx, t, pane, \"ready\")\ntmuxtest.Type(ctx, t, pane, \"a line for the program\")\ntmuxtest.WaitForLine(ctx, t, pane, \"a line for the program\")\n```\n\n<!-- docs:end -->\n\nA wait that runs out fails with the screen the pane last held, rather than\nsending you back to add a print statement:\n\n```\ntmuxtest: pane %1 never showed a line containing \"ready\"\nthe pane showed 3 line(s):\n    | tmuxtest$ ./mytui --watch\n    | loading widgets\n    | connecting\n```\n\nIt works for a test whose subject is tmux itself too, giving a server on its own\nsocket that is killed when the test ends:\n\n```go\nfunc TestSomething(t *testing.T) {\n\tctx := context.Background()\n\tserver := tmuxtest.NewServer(ctx, t)\n\n\tsession, err := server.NewSession(ctx, tmux.NewSessionRequest{Name: \"under-test\"})\n\t// ...\n}\n```\n\n`NewServer` snapshots its effective environment and working directory, resolves\none absolute executable, and returns an error before starting tmux when\nconfiguration or resolution fails. Later environment and directory changes do\nnot retarget the handle, and the zero `Server` is invalid. Tests of process\nbehavior can point `ServerOptions.Binary` at an executable fixture;\nconstruction still resolves and freezes it. Use `tmuxtest` when the behavior\nbelongs to a real tmux daemon.\n\n## Documentation\n\nThe package documentation is the reference, written to be read start to finish\nrather than searched:\n\n```console\n$ go doc github.com/libtmux/libtmux-go/tmux\n```\n\nIt opens with a task index, then the rule mapping a tmux command to its Go\nmethod — `kill-pane` is `Pane.Kill`, `rename-session` is `Session.Rename` — so a\ncommand usually leads to its method without a lookup.\n\n| | |\n| --- | --- |\n| [`DESIGN.md`](DESIGN.md) | The conventions this package holds itself to, and the bakeoffs behind them |\n| [`PARITY.md`](PARITY.md) | How the surface is checked against the Python libtmux |\n| [`BENCHMARKS.md`](BENCHMARKS.md) | What each way of reaching tmux costs |\n| [`CHANGELOG.md`](CHANGELOG.md) | What each release changed |\n| [`CONTRIBUTING.md`](.github/CONTRIBUTING.md) | The gates a change has to pass |\n| [`WRITING.md`](.github/WRITING.md) | How this repository writes: docs, the changelog, commits |\n| [`SECURITY.md`](SECURITY.md) | What this software executes, and how to report a hole in it |\n| [`AGENTS.md`](AGENTS.md) | Which of the above applies to what you are changing |\n| [`examples/`](examples/) | Runnable programs for each of the above |\n\n## License\n\nMIT. See [`LICENSE`](LICENSE).\n\n[tmux]: https://github.com/tmux/tmux\n",
  "bytes": 12509,
  "sha": "23284809238bb6e009300cf23e55c05b3731b117ae64cae068af926e812d388c",
  "repo_slug": "libtmux/libtmux-go",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_libtmux_tmux_mcp_go_0f064d7c/readme"
}