// Package tmux provides an interface and implementation for controlling tmux sessions. package tmux import ( "bytes" "fmt" "os/exec" "strings" ) // Client defines the operations the daemon needs on tmux sessions. type Client interface { // HasSession returns true if the named session exists. HasSession(name string) bool // CreateSession creates a new detached session with an optional working directory. CreateSession(name, workdir string) error // KillSession destroys the named session. KillSession(name string) error // SendKeys sends key strokes to the first window of a session, terminated // by a single Enter. Note: when keys contains newlines, Claude Code's // TUI treats the whole block as a paste and the trailing Enter is // consumed as part of it — call SendEnter afterwards to submit. SendKeys(session, keys string) error // SendEnter sends a lone Enter keypress. Used to submit a multi-line // paste in Claude Code after SendKeys. SendEnter(session string) error // CapturePaneTail returns the last n lines from the session's active pane. CapturePaneTail(session string, lines int) (string, error) } // ExecClient implements Client by shelling out to the tmux binary. type ExecClient struct{} // NewExecClient returns a ready-to-use ExecClient. func NewExecClient() *ExecClient { return &ExecClient{} } // run executes a tmux subcommand and returns combined output. func run(args ...string) (string, error) { cmd := exec.Command("tmux", args...) var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out err := cmd.Run() return strings.TrimSpace(out.String()), err } // HasSession returns true when `tmux has-session` exits 0. func (c *ExecClient) HasSession(name string) bool { _, err := run("has-session", "-t", name) return err == nil } // CreateSession creates a new detached tmux session. // If workdir is non-empty, the session starts in that directory. func (c *ExecClient) CreateSession(name, workdir string) error { args := []string{"new-session", "-d", "-s", name} if workdir != "" { args = append(args, "-c", workdir) } out, err := run(args...) if err != nil { return fmt.Errorf("tmux new-session %q: %w (%s)", name, err, out) } return nil } // KillSession destroys the named session. func (c *ExecClient) KillSession(name string) error { out, err := run("kill-session", "-t", name) if err != nil { return fmt.Errorf("tmux kill-session %q: %w (%s)", name, err, out) } return nil } // SendKeys sends key strokes to the first pane of the session. func (c *ExecClient) SendKeys(session, keys string) error { out, err := run("send-keys", "-t", session, keys, "Enter") if err != nil { return fmt.Errorf("tmux send-keys %q: %w (%s)", session, err, out) } return nil } // SendEnter sends a lone Enter keypress to the session's first pane. func (c *ExecClient) SendEnter(session string) error { out, err := run("send-keys", "-t", session, "Enter") if err != nil { return fmt.Errorf("tmux send-keys Enter %q: %w (%s)", session, err, out) } return nil } // CapturePaneTail captures the last n lines from the session's active pane. func (c *ExecClient) CapturePaneTail(session string, lines int) (string, error) { out, err := run("capture-pane", "-p", "-t", session, "-S", fmt.Sprintf("-%d", lines)) if err != nil { return "", fmt.Errorf("tmux capture-pane %q: %w (%s)", session, err, out) } return out, nil }