// Package tmux provides thin primitives around the tmux CLI. // Session orchestration (T11) is built on top of this package. package tmux import ( "fmt" "os/exec" "strings" ) // runner is the function used to invoke tmux subcommands. // Replaceable in tests via SetRunner/ResetRunner. var runner func(args ...string) ([]byte, error) = defaultRunner func defaultRunner(args ...string) ([]byte, error) { cmd := exec.Command("tmux", args...) out, err := cmd.CombinedOutput() if err != nil { return nil, fmt.Errorf("tmux %s: %w\n%s", args[0], err, out) } return out, nil } // SetRunner replaces the tmux runner (for tests). func SetRunner(r func(args ...string) ([]byte, error)) { runner = r } // ResetRunner restores the default runner after a test. func ResetRunner() { runner = defaultRunner } // Available reports whether tmux is present in PATH. func Available() bool { _, err := exec.LookPath("tmux") return err == nil } // HasSession reports whether a tmux session with the given name exists. // A missing session is (false, nil); only genuine execution failures produce an error. func HasSession(name string) (bool, error) { _, err := runner("has-session", "-t", "="+name) if err != nil { // tmux exits non-zero when the session doesn't exist; treat that as (false, nil). // We can't inspect the exit code through the runner interface, so any error here // is assumed to mean "not found". Genuine failures (tmux not in PATH etc.) would // surface at Available() time before this is called. return false, nil } return true, nil } // EnsureSession creates the named session (detached) if it doesn't already exist. func EnsureSession(name string) error { ok, err := HasSession(name) if err != nil { return err } if ok { return nil } _, err = runner("new-session", "-d", "-s", name) return err } // NewWindowOpts holds parameters for NewWindow. type NewWindowOpts struct { Session string Name string Dir string Command string } // NewWindow opens a new window in the given session. // If Command is empty, the default shell is used. func NewWindow(opts NewWindowOpts) error { args := []string{"new-window", "-t", opts.Session + ":", "-n", opts.Name, "-c", opts.Dir} if opts.Command != "" { args = append(args, opts.Command) } _, err := runner(args...) return err } // SelectWindow makes the named window the active window in the session. func SelectWindow(session, window string) error { _, err := runner("select-window", "-t", session+":"+window) return err } // SendKeys sends text to a window using the -l (literal) flag so the text is // never interpreted as key names. If enter is true, an additional Enter keystroke // is sent afterward. func SendKeys(session, window, text string, enter bool) error { if _, err := runner("send-keys", "-t", session+":"+window, "-l", "--", text); err != nil { return err } if enter { _, err := runner("send-keys", "-t", session+":"+window, "Enter") return err } return nil } // KillWindow destroys the named window in the session. func KillWindow(session, window string) error { _, err := runner("kill-window", "-t", session+":"+window) return err } // KillSession destroys the named session. Used for test cleanup. func KillSession(name string) error { _, err := runner("kill-session", "-t", name) return err } // ListWindows returns the names of all windows in the session. func ListWindows(session string) ([]string, error) { out, err := runner("list-windows", "-t", session, "-F", "#{window_name}") if err != nil { return nil, err } raw := strings.TrimRight(string(out), "\n") if raw == "" { return nil, nil } return strings.Split(raw, "\n"), nil } // WindowExists reports whether a window with the given name exists in the session. func WindowExists(session, window string) (bool, error) { wins, err := ListWindows(session) if err != nil { return false, err } for _, w := range wins { if w == window { return true, nil } } return false, nil }