2026-04-14 18:02:25 +00:00
|
|
|
// 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
|
2026-04-15 20:49:59 +00:00
|
|
|
// 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.
|
2026-04-14 18:02:25 +00:00
|
|
|
SendKeys(session, keys string) error
|
2026-04-15 20:49:59 +00:00
|
|
|
// SendEnter sends a lone Enter keypress. Used to submit a multi-line
|
|
|
|
|
// paste in Claude Code after SendKeys.
|
|
|
|
|
SendEnter(session string) error
|
2026-04-14 18:02:25 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 20:49:59 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 18:02:25 +00:00
|
|
|
// 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
|
|
|
|
|
}
|