agentCreate the session.
Result
{ "sessionId": "$0", "sessionName": "checks"}Choose documentation 3
No matching documentation.
No matching versions.
Current version
latestNo matching languages.
Early release libtmux is released, and its APIs may still change between releases. This site's structure and URLs may change too.
Go-based tmux client
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
// Command quickstart lays out a tmux session, then walks it as objects.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 }
// Lay out the session: name its window, split it, add a second window. editor, err := session.ResolveActiveWindow(ctx) if err != nil { return err } editor, err = editor.Rename(ctx, "editor") if err != nil { return err } left, err := editor.ResolveActivePane(ctx) if err != nil { return err } right, err := left.Split(ctx, tmux.SplitPaneRequest{ Direction: tmux.PaneDirectionRight, }) if err != nil { return err } name := "logs" logs, err := session.NewWindow(ctx, tmux.NewWindowRequest{ Name: &name, }) if err != nil { return err }
// Type a command into the new pane and wait for what it prints. command := "printf 'hello from %s\\n' tmux" err = right.SendKeys(ctx, tmux.SendKeysRequest{Command: &command}) if err != nil { return err } _, err = right.WaitForText(ctx, tmux.WaitForTextRequest{ Text: "hello from tmux", }) if err != nil { return err }
// Move the focus an attached client sees: the pane, then the window. _, err = right.Select(ctx, tmux.PaneSelectRequest{}) if err != nil { return err } _, err = logs.Select(ctx) if err != nil { return err }
// Walk the objects: down from the server, then up from one pane. snapshot, err := server.Snapshot(ctx) if err != nil { return err } for _, each := range snapshot.Sessions() { sessionName, _ := each.Name() fmt.Println("session", sessionName) windows, _ := each.Windows() for _, window := range windows { name, _ := window.Name() fmt.Printf(" window %d:%s:", window.Index(), name) panes, _ := window.Panes() for _, pane := range panes { fmt.Print(" ", pane.ID()) } fmt.Println() } } pane, _ := snapshot.PaneByID(right.ID()) window, _ := pane.Window() owner, _ := window.Session() windowName, _ := window.Name() sessionName, _ := owner.Name() fmt.Println(pane.ID(), "is in window", windowName, "of session", sessionName) return nil}// Command quickstart lays out a tmux session, then walks it as objects.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) }
// Lay out the session: name its window, split it, add a second window. editor, err := session.ResolveActiveWindow(ctx) if err != nil { return fmt.Errorf("find window: %w", err) } editor, err = editor.Rename(ctx, "editor") if err != nil { return fmt.Errorf("rename window: %w", err) } left, err := editor.ResolveActivePane(ctx) if err != nil { return fmt.Errorf("find pane: %w", err) } right, err := left.Split(ctx, tmux.SplitPaneRequest{ Direction: tmux.PaneDirectionRight, }) if err != nil { return fmt.Errorf("split pane: %w", err) } name := "logs" logs, err := session.NewWindow(ctx, tmux.NewWindowRequest{ Name: &name, }) if err != nil { return fmt.Errorf("add window: %w", err) }
// Type a command into the new pane and wait for what it prints. command := "printf 'hello from %s\\n' tmux" err = right.SendKeys(ctx, tmux.SendKeysRequest{Command: &command}) if err != nil { return fmt.Errorf("send keys: %w", err) } _, err = right.WaitForText(ctx, tmux.WaitForTextRequest{ Text: "hello from tmux", }) if err != nil { return fmt.Errorf("wait for output: %w", err) }
// Move the focus an attached client sees: the pane, then the window. _, err = right.Select(ctx, tmux.PaneSelectRequest{}) if err != nil { return fmt.Errorf("select pane: %w", err) } _, err = logs.Select(ctx) if err != nil { return fmt.Errorf("select window: %w", err) }
// Walk the objects: down from the server, then up from one pane. snapshot, err := server.Snapshot(ctx) if err != nil { return fmt.Errorf("read tmux: %w", err) } for _, each := range snapshot.Sessions() { sessionName, _ := each.Name() fmt.Println("session", sessionName) windows, _ := each.Windows() for _, window := range windows { name, _ := window.Name() fmt.Printf(" window %d:%s:", window.Index(), name) panes, _ := window.Panes() for _, pane := range panes { fmt.Print(" ", pane.ID()) } fmt.Println() } } pane, _ := snapshot.PaneByID(right.ID()) window, _ := pane.Window() owner, _ := window.Session() windowName, _ := window.Name() sessionName, _ := owner.Name() fmt.Println(pane.ID(), "is in window", windowName, "of session", sessionName) return nil}// Command quickstart lays out a tmux session, then walks it as objects.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))) }()
// Lay out the session: name its window, split it, add a second window. editor, err := session.ResolveActiveWindow(ctx) if err != nil { return err } editor, err = editor.Rename(ctx, "editor") if err != nil { return err } left, err := editor.ResolveActivePane(ctx) if err != nil { return err } right, err := left.Split(ctx, tmux.SplitPaneRequest{ Direction: tmux.PaneDirectionRight, }) if err != nil { return err } name := "logs" logs, err := session.NewWindow(ctx, tmux.NewWindowRequest{ Name: &name, }) if err != nil { return err }
// Type a command into the new pane and wait for what it prints. command := "printf 'hello from %s\\n' tmux" err = right.SendKeys(ctx, tmux.SendKeysRequest{Command: &command}) if err != nil { return err } _, err = right.WaitForText(ctx, tmux.WaitForTextRequest{ Text: "hello from tmux", }) if err != nil { return err }
// Move the focus an attached client sees: the pane, then the window. _, err = right.Select(ctx, tmux.PaneSelectRequest{}) if err != nil { return err } _, err = logs.Select(ctx) if err != nil { return err }
// Walk the objects: down from the server, then up from one pane. snapshot, err := server.Snapshot(ctx) if err != nil { return err } for _, each := range snapshot.Sessions() { sessionName, _ := each.Name() fmt.Println("session", sessionName) windows, _ := each.Windows() for _, window := range windows { name, _ := window.Name() fmt.Printf(" window %d:%s:", window.Index(), name) panes, _ := window.Panes() for _, pane := range panes { fmt.Print(" ", pane.ID()) } fmt.Println() } } pane, _ := snapshot.PaneByID(right.ID()) window, _ := pane.Window() owner, _ := window.Session() windowName, _ := window.Name() sessionName, _ := owner.Name() fmt.Println(pane.ID(), "is in window", windowName, "of session", sessionName) return nil}// Command quickstart lays out a tmux session, then walks it as objects.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))) }()
// Lay out the session: name its window, split it, add a second window. editor, err := session.ResolveActiveWindow(ctx) if err != nil { return fmt.Errorf("find window: %w", err) } editor, err = editor.Rename(ctx, "editor") if err != nil { return fmt.Errorf("rename window: %w", err) } left, err := editor.ResolveActivePane(ctx) if err != nil { return fmt.Errorf("find pane: %w", err) } right, err := left.Split(ctx, tmux.SplitPaneRequest{ Direction: tmux.PaneDirectionRight, }) if err != nil { return fmt.Errorf("split pane: %w", err) } name := "logs" logs, err := session.NewWindow(ctx, tmux.NewWindowRequest{ Name: &name, }) if err != nil { return fmt.Errorf("add window: %w", err) }
// Type a command into the new pane and wait for what it prints. command := "printf 'hello from %s\\n' tmux" err = right.SendKeys(ctx, tmux.SendKeysRequest{Command: &command}) if err != nil { return fmt.Errorf("send keys: %w", err) } _, err = right.WaitForText(ctx, tmux.WaitForTextRequest{ Text: "hello from tmux", }) if err != nil { return fmt.Errorf("wait for output: %w", err) }
// Move the focus an attached client sees: the pane, then the window. _, err = right.Select(ctx, tmux.PaneSelectRequest{}) if err != nil { return fmt.Errorf("select pane: %w", err) } _, err = logs.Select(ctx) if err != nil { return fmt.Errorf("select window: %w", err) }
// Walk the objects: down from the server, then up from one pane. snapshot, err := server.Snapshot(ctx) if err != nil { return fmt.Errorf("read tmux: %w", err) } for _, each := range snapshot.Sessions() { sessionName, _ := each.Name() fmt.Println("session", sessionName) windows, _ := each.Windows() for _, window := range windows { name, _ := window.Name() fmt.Printf(" window %d:%s:", window.Index(), name) panes, _ := window.Panes() for _, pane := range panes { fmt.Print(" ", pane.ID()) } fmt.Println() } } pane, _ := snapshot.PaneByID(right.ID()) window, _ := pane.Window() owner, _ := window.Session() windowName, _ := window.Name() sessionName, _ := owner.Name() fmt.Println(pane.ID(), "is in window", windowName, "of session", sessionName) return nil}tmux before the first call: no server is running.
tmux after tmux.NewServer: no server is running; the call sent no command.
tmux after Server.NewSession: added session demo.
tmux after Session.ResolveActiveWindow: unchanged.
tmux after Window.Rename: renamed window 0:sh to 0:editor.
tmux after Window.ResolveActivePane: unchanged.
tmux after Pane.Split: added pane %1 to window 0:editor.
tmux after Session.NewWindow: added window 1:logs.
tmux after Pane.SendKeys: sent keys to pane %1.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Pane.WaitForText: read pane %1.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Pane.Select: selected pane %1 in window 0:editor.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Window.Select: selected window 1:logs.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Server.Snapshot: unchanged.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Snapshot.PaneByID: unchanged, the call sent no command.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Pane.Window: unchanged, the call sent no command.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Window.Session: unchanged, the call sent no command.
tmux after Server.Kill: the server has exited.
No tmux server is running yet.
tmux.NewServer returned tmux.Server { "configFile": "/dev/null", "executable": "/usr/bin/tmux", "socketPath": "/tmp/tmux-1000/libtmux-quickstart-12345"}The call sent no tmux command.
Server.NewSession returned tmux.Session { "id": "$0", "name": "demo"}list-sessions -F '#{session_id} #{session_name}' error connecting to /tmp/tmux-1000/libtmux-quickstart-12345 (No such file or directory) new-session -P -F#{session_id} -sdemo -d $0 list-sessions -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|0|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2… Before these, the library asked tmux about itself 7 times (its version, its daemon, how a control client reads escapes).
Session.ResolveActiveWindow returned tmux.Window { "id": "@0", "index": 0, "name": "sh"}list-sessions -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|0|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2… list-windows -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_coll… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|1|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2… list-panes -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|1|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|1|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2… list-clients -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:client_activity}|#{q:client_cell_height}|#{q:client_cell_width}|#{q:client_control_mode}|#{q:client_created}|#{q:client_discarded}|#{q:client_flags}|#{q:client_height}|#{q:client_key_table}|#{q:client_last_session}|#{q:client_mode_format}|#{q:c… no output Window.Rename returned tmux.Window { "id": "@0", "index": 0, "name": "editor"}rename-window -t @0 -- editor no output list-windows -t @0 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_c… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|1|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2… Window.ResolveActivePane returned tmux.Pane { "id": "%0"}list-panes -t @0 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_col… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|1|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|1|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2… Pane.Split returned tmux.Pane { "id": "%1"}split-window -t $0:.%0 -h -P -F#{pane_id} -d %1 list-panes -t $0:.%1 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|0|2|0|0|0|0|0|0|\$1|0|1|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|1|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0… Session.NewWindow returned tmux.Window { "id": "@1", "index": 1, "name": "logs"}new-window -t $0 -d -P -F#{window_id} -n logs @1 list-sessions -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|0|0|0|0|0|0|0|\$1|0|1|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0… list-windows -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_coll… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|1|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0… list-panes -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|1|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|1|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0… list-clients -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:client_activity}|#{q:client_cell_height}|#{q:client_cell_width}|#{q:client_control_mode}|#{q:client_created}|#{q:client_discarded}|#{q:client_flags}|#{q:client_height}|#{q:client_key_table}|#{q:client_last_session}|#{q:client_mode_format}|#{q:c… no output Pane.SendKeys returned error nullsend-keys -t $0:.%1 -- 'printf '\''hello from %s\n'\'' tmux' no output send-keys -t $0:.%1 -- Enter no output Pane.WaitForText returned error nullcapture-pane -p -J -t %1 ; run-shell -C -t %1 libtmux-text-wait-fcb5574e00cc8b43-#{q:pane_id}-#{q:pane_dead}| $ printf 'hello from %s\n' tmux
hello from tmux
$ Pane.Select returned error nullselect-pane -t $0:.%1 no output list-panes -t $0:.%1 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|0|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|-|0|1|24|\%0|0|0|0|VT10x|1|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0|… Window.Select returned error nullselect-window -t @1 no output list-windows -t @1 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_c… 1|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|2|0|24,960,67,335,0,0|1295|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|1|1|0|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%1|0|1|0|VT10x|0|41|0|0|0||4||0|hidden|2848987|0||7,25|79||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/16|0|24|39|39|41|0|1… Server.Snapshot returned tmux.Snapshot { "panes": 3, "sessions": 1, "windows": 2}list-sessions -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 1|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|1|0|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%2|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2849087|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/17|0|2… list-windows -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_coll… 1|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|2|0|24,960,67,335,0,0|1295|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|1|1|0|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%1|0|1|0|VT10x|0|41|0|0|0||4||0|hidden|2848987|0||7,25|79||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/16|0|24|39|39|41|0|1… list-panes -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 1|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|0|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|-|0|1|24|\%0|0|0|0|VT10x|1|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0|… list-clients -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:client_activity}|#{q:client_cell_height}|#{q:client_cell_width}|#{q:client_control_mode}|#{q:client_created}|#{q:client_discarded}|#{q:client_flags}|#{q:client_height}|#{q:client_key_table}|#{q:client_last_session}|#{q:client_mode_format}|#{q:c… no output Before these, the library asked tmux about itself 2 times (its version, its daemon, how a control client reads escapes).
Snapshot.PaneByID returned tmux.Pane { "id": "%1"}The call sent no tmux command.
Pane.Window returned tmux.Window { "id": "@0", "index": 0, "name": "editor"}The call sent no tmux command.
Window.Session returned tmux.Session { "id": "$0", "name": "demo"}The call sent no tmux command.
Server.Kill returned error nullkill-server no output Before these, the library asked tmux about itself 1 time (its version, its daemon, how a control client reads escapes).
libtmux/libtmux-go
at 449267f7c on a private tmux 3.8a server.
A person matched each call to its lines; the values, commands and tmux states are what the run produced.
Commands appear without the library's if-shell guard, and the socket name and paths are set to fixed values.
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.
module example.com/libtmux-home
go 1.26.0
require github.com/libtmux/libtmux-go v0.1.2go mod tidygo build -p 2 -o quickstart quickstart.gounset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh./quickstartsession demo window 0:editor: %0 %1 window 1:logs: %2%1 is in window editor of session demoLibrary revision: 449267f7cd1d.
// 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}APIs used
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}APIs used
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.
github.com/libtmux/libtmux-go/tmux$ go get github.com/libtmux/libtmux-go/tmux@latesttmux before the first call.
tmux after create_session.
tmux after list_panes: unchanged, the call only reads.
tmux after split_window.
tmux after list_panes: unchanged, the call only reads.
$ pytest -q .. [100%] 2 passed in 0.01s
tmux after run_shell_command.
$ pytest -q .. [100%] 2 passed in 0.01s
$ cat checks.log 2026-01-01 checks started 2026-01-01 checks finished
tmux after run_shell_command.
tmux after kill_session.
Create a tmux session called checks, split its window into two panes, run pytest -q in one and show checks.log in the other, show me the results, then remove the session.
agentCreate the session.
{ "sessionId": "$0", "sessionName": "checks"}agentList the panes, each with the session it belongs to.
{ "panes": [ { "active": true, "currentCommand": "sh", "geometry": { "height": 24, "left": 0, "top": 0, "width": 80 }, "id": "%0", "index": 0, "isCaller": null, "session": "checks", "window": "sh", "windowId": "@0" } ], "total": 1}agentSplit the pane the list returned (%0) in two.
{ "paneId": "%1"}agentList the panes again; each now carries its position and size.
{ "panes": [ { "active": true, "currentCommand": "sh", "geometry": { "height": 24, "left": 0, "top": 0, "width": 40 }, "id": "%0", "index": 0, "isCaller": null, "session": "checks", "window": "sh", "windowId": "@0" }, { "active": false, "currentCommand": "sh", "geometry": { "height": 24, "left": 41, "top": 0, "width": 39 }, "id": "%1", "index": 1, "isCaller": null, "session": "checks", "window": "sh", "windowId": "@0" } ], "total": 2}agentRun the tests in the pane the list returned (%0).
{ "effective_timeout_seconds": 120, "exit_status": 0, "output": [ ".. [100%]", "2 passed in 0.01s" ], "pane_id": "%0", "resolved_pane_ids": [ "%0" ], "timed_out": false}agentShow the log in the new pane the split returned (%1).
{ "effective_timeout_seconds": 120, "exit_status": 0, "output": [ "2026-01-01 checks started", "2026-01-01 checks finished" ], "pane_id": "%1", "resolved_pane_ids": [ "%1" ], "timed_out": false}agentRemove the session by the id the create result returned ($0).
{ "killed": "checks"}agent in %0 it printed 2 passed in 0.01s
; in %1 it printed 2026-01-01 checks finished
.
Calls and results are recorded against libtmux/libtmux-go
at f59af95c2, on a private tmux 3.8a server, running pytest 9.2.0.
The preview takes its layout and panes from the recording's own tmux server after each call, and a pane's output from the results of the calls.
The agent's narration is illustrative.
This page documents d8ea6f77e, a different revision. The calls have the same input and output schemas there, but the results at that revision may differ from the recorded ones. The sandbox's project directory is shown as /home/user/project.
github.com/libtmux/libtmux-go/mcpChoose your client and configuration scope, then run the command or paste the configuration.
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ claude mcp add tmux -- libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ~/.claude.json (this project)
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ claude mcp add tmux --scope user -- libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ~/.claude.json (all projects)
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ claude mcp add tmux --scope project -- libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: .mcp.json (in repo, version-controlled)
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen use this config:
That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: claude_desktop_config.json
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ codex mcp add tmux -- libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ~/.codex/config.toml
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.
That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: .codex/config.toml (in repo)
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ gemini mcp add --scope user tmux libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ~/.gemini/settings.json
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ gemini mcp add --scope project tmux libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: .gemini/settings.json (in repo)
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen use this config:
That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: .cursor/mcp.json (in repo)
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen use this config:
That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ~/.cursor/mcp.json
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ grok mcp add --scope user tmux -- libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ~/.grok/config.toml
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ grok mcp add --scope project tmux -- libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ./.grok/config.toml (in repo)
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen use this config:
That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ~/.gemini/config/mcp_config.json
Build or install the server first:
$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latestThen register:
$ opencode mcp add tmux -- libtmux-mcpThat puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.
Config file: ~/.config/opencode/opencode.jsonc
func connect(ctx context.Context, target tmux.Server) (*sdk.ClientSession, func(), error) { clientTransport, serverTransport := sdk.NewInMemoryTransports() instance, err := tmuxmcp.NewServer(target) if err != nil { return nil, nil, fmt.Errorf("construct the server: %w", err) } serverSession, err := instance.Connect( ctx, tmuxmcp.AssumeResponseCommit(serverTransport), nil, ) if err != nil { _ = instance.Close() return nil, nil, fmt.Errorf("start the server: %w", err) } client := sdk.NewClient(&sdk.Implementation{Name: "agent-workflow", Version: "1"}, nil) session, err := client.Connect(ctx, clientTransport, nil) if err != nil { _ = serverSession.Close() _ = instance.Close() return nil, nil, fmt.Errorf("connect a client: %w", err) } return session, func() { _ = session.Close() _ = serverSession.Close() _ = instance.Close() }, nil}
Excerpt of mcp/examples/agent-workflow/main.go,
which go test -race -count=1 runs in the repository's CI
(.github/workflows/tests.yml).
environment sets variables for a session, a window or a pane.
session_name: nativestart_directory: .environment: {SESSION_VALUE: session}windows:- window_name: development window_index: 3 layout: even-horizontal environment: {WINDOW_VALUE: window} panes: - start_directory: one shell_command: 'printf "%s:%s:%s" "$SESSION_VALUE" "$WINDOW_VALUE" "$PANE_VALUE" > result' - start_directory: two environment: {PANE_VALUE: pane} shell_command: 'printf "%s:%s:%s" "$SESSION_VALUE" "$WINDOW_VALUE" "$PANE_VALUE" > result'- window_name: second focus: true panes: [blank]{ "session_name": "native", "start_directory": ".", "environment": { "SESSION_VALUE": "session" }, "windows": [ { "window_name": "development", "window_index": 3, "layout": "even-horizontal", "environment": { "WINDOW_VALUE": "window" }, "panes": [ { "start_directory": "one", "shell_command": "printf \"%s:%s:%s\" \"$SESSION_VALUE\" \"$WINDOW_VALUE\" \"$PANE_VALUE\" > result" }, { "start_directory": "two", "environment": { "PANE_VALUE": "pane" }, "shell_command": "printf \"%s:%s:%s\" \"$SESSION_VALUE\" \"$WINDOW_VALUE\" \"$PANE_VALUE\" > result" } ] }, { "window_name": "second", "focus": true, "panes": [ "blank" ] } ]}Converted from the YAML; the tests load the YAML.
Save the file as workspace.yaml and load it with:
tmux-workspace load workspace.yaml
Workspace file: workspace/internal/integration/cli_test.go.
Tested by TestLoadFreezeReloadAppendAndEnvironment
(workspace/internal/integration/cli_test.go; a test runs the command).
github.com/libtmux/libtmux-go/workspaceThe workspace package is separate from the core library.
$ go install github.com/libtmux/libtmux-go/workspace/cmd/tmux-workspace@latestNeeds Go 1.26 or newer. Puts tmux-workspace in $(go env GOBIN), or in $(go env GOPATH)/bin when GOBIN is unset; rerunning it upgrades.
// document is a workspace file. A program of your own would read one from// disk with os.ReadFile.const document = `session_name: projectwindows: - window_name: editor layout: main-vertical panes: - echo editing - echo testing - window_name: logs panes: - echo api - echo worker - echo database`
// Parse refuses a field it does not know rather than dropping it, so a// workspace that loads is one that was understood.parsed, err := workspace.Parse([]byte(document))if err != nil { return fmt.Errorf("parse workspace: %w", err)}
// 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-build-workspace", ConfigFile: os.DevNull,})if err != nil { return fmt.Errorf("configure tmux: %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)))}()
session, err := workspace.Build(ctx, server, parsed)
Excerpt of workspace/examples/build-workspace/main.go,
which go test -race -count=1 runs in the repository's CI
(.github/workflows/tests.yml).
Subject-by-subject, with Go in every code block.
Task guides for Go.
Complete programs, sourced from the code each port tests.
The domain model every port shares.
Reference preview