// Package tmux provides a wrapper for tmux session operations via subprocess. package tmux import ( "bytes" "errors" "fmt" "os/exec" "strings" ) // Common errors var ( ErrNoServer = errors.New("no tmux server running") ErrSessionExists = errors.New("session already exists") ErrSessionNotFound = errors.New("session not found") ) // Tmux wraps tmux operations. type Tmux struct{} // NewTmux creates a new Tmux wrapper. func NewTmux() *Tmux { return &Tmux{} } // run executes a tmux command and returns stdout. func (t *Tmux) run(args ...string) (string, error) { cmd := exec.Command("tmux", args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() if err != nil { return "", t.wrapError(err, stderr.String(), args) } return strings.TrimSpace(stdout.String()), nil } // wrapError wraps tmux errors with context. func (t *Tmux) wrapError(err error, stderr string, args []string) error { stderr = strings.TrimSpace(stderr) // Detect specific error types if strings.Contains(stderr, "no server running") || strings.Contains(stderr, "error connecting to") { return ErrNoServer } if strings.Contains(stderr, "duplicate session") { return ErrSessionExists } if strings.Contains(stderr, "session not found") || strings.Contains(stderr, "can't find session") { return ErrSessionNotFound } if stderr != "" { return fmt.Errorf("tmux %s: %s", args[0], stderr) } return fmt.Errorf("tmux %s: %w", args[0], err) } // NewSession creates a new detached tmux session. func (t *Tmux) NewSession(name, workDir string) error { args := []string{"new-session", "-d", "-s", name} if workDir != "" { args = append(args, "-c", workDir) } _, err := t.run(args...) return err } // KillSession terminates a tmux session. func (t *Tmux) KillSession(name string) error { _, err := t.run("kill-session", "-t", name) return err } // HasSession checks if a session exists. func (t *Tmux) HasSession(name string) (bool, error) { _, err := t.run("has-session", "-t", name) if err != nil { if errors.Is(err, ErrSessionNotFound) || errors.Is(err, ErrNoServer) { return false, nil } return false, err } return true, nil } // ListSessions returns all session names. func (t *Tmux) ListSessions() ([]string, error) { out, err := t.run("list-sessions", "-F", "#{session_name}") if err != nil { if errors.Is(err, ErrNoServer) { return nil, nil // No server = no sessions } return nil, err } if out == "" { return nil, nil } return strings.Split(out, "\n"), nil } // SendKeys sends keystrokes to a session. func (t *Tmux) SendKeys(session, keys string) error { _, err := t.run("send-keys", "-t", session, keys, "Enter") return err } // SendKeysRaw sends keystrokes without adding Enter. func (t *Tmux) SendKeysRaw(session, keys string) error { _, err := t.run("send-keys", "-t", session, keys) return err } // CapturePane captures the visible content of a pane. func (t *Tmux) CapturePane(session string, lines int) (string, error) { return t.run("capture-pane", "-p", "-t", session, "-S", fmt.Sprintf("-%d", lines)) } // CapturePaneAll captures all scrollback history. func (t *Tmux) CapturePaneAll(session string) (string, error) { return t.run("capture-pane", "-p", "-t", session, "-S", "-") } // AttachSession attaches to an existing session. // Note: This replaces the current process with tmux attach. func (t *Tmux) AttachSession(session string) error { _, err := t.run("attach-session", "-t", session) return err } // SelectWindow selects a window by index. func (t *Tmux) SelectWindow(session string, index int) error { _, err := t.run("select-window", "-t", fmt.Sprintf("%s:%d", session, index)) return err } // SetEnvironment sets an environment variable in the session. func (t *Tmux) SetEnvironment(session, key, value string) error { _, err := t.run("set-environment", "-t", session, key, value) return err } // GetEnvironment gets an environment variable from the session. func (t *Tmux) GetEnvironment(session, key string) (string, error) { out, err := t.run("show-environment", "-t", session, key) if err != nil { return "", err } // Output format: KEY=value parts := strings.SplitN(out, "=", 2) if len(parts) != 2 { return "", nil } return parts[1], nil } // RenameSession renames a session. func (t *Tmux) RenameSession(oldName, newName string) error { _, err := t.run("rename-session", "-t", oldName, newName) return err } // SessionInfo contains information about a tmux session. type SessionInfo struct { Name string Windows int Created string Attached bool } // GetSessionInfo returns detailed information about a session. func (t *Tmux) GetSessionInfo(name string) (*SessionInfo, error) { format := "#{session_name}|#{session_windows}|#{session_created_string}|#{session_attached}" out, err := t.run("list-sessions", "-F", format, "-f", fmt.Sprintf("#{==:#{session_name},%s}", name)) if err != nil { return nil, err } if out == "" { return nil, ErrSessionNotFound } parts := strings.Split(out, "|") if len(parts) != 4 { return nil, fmt.Errorf("unexpected session info format: %s", out) } windows := 0 fmt.Sscanf(parts[1], "%d", &windows) return &SessionInfo{ Name: parts[0], Windows: windows, Created: parts[2], Attached: parts[3] == "1", }, nil }