summaryrefslogtreecommitdiff
path: root/internal/tmux/tmux.go
blob: 63b56601cff3f79747e02c06a60b68035d5b867c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package tmux

import (
    "os"
    "os/exec"
    "strconv"
    "strings"
)

// Available reports whether tmux is available and we appear to be in a tmux session.
func Available() bool { return HasBinary() && InSession() }

// HasBinary reports whether the tmux binary is on PATH.
var lookPath = exec.LookPath
var command = exec.Command

func HasBinary() bool { _, err := lookPath("tmux"); return err == nil }

// InSession reports whether we seem to be running inside a tmux session.
func InSession() bool { return strings.TrimSpace(os.Getenv("TMUX")) != "" }

// SplitOpts controls how a new pane is created for running a command.
type SplitOpts struct {
	Target   string // optional pane target, e.g. ":."
	Vertical bool   // true => split vertically (-v); false => horizontally (-h)
	Percent  int    // 1..100; 0 means use tmux default
}

// SplitRun splits the current tmux window and runs argv in the new pane.
// It returns once tmux has launched the child process.
func SplitRun(opts SplitOpts, argv []string) error {
	if len(argv) == 0 {
		return nil
	}
	args := []string{"split-window"}
	if opts.Vertical {
		args = append(args, "-v")
	} else {
		args = append(args, "-h")
	}
	if opts.Percent > 0 && opts.Percent <= 100 {
		args = append(args, "-p", strconv.Itoa(opts.Percent))
	}
	if strings.TrimSpace(opts.Target) != "" {
		args = append(args, "-t", opts.Target)
	}
	// tmux takes a single command string. Use a conservative shell join.
	cmdStr := shellJoin(argv)
	args = append(args, cmdStr)
    c := command("tmux", args...)
    return c.Run()
}

// shellJoin quotes argv elements for safe use in a single shell command string.
// It avoids interpretation by wrapping in single quotes and escaping embedded single quotes.
func shellJoin(argv []string) string {
	out := make([]string, 0, len(argv))
	for _, a := range argv {
		if a == "" {
			out = append(out, "''")
			continue
		}
		if isSafeBare(a) {
			out = append(out, a)
			continue
		}
		// single-quote wrapping with escaped single quotes
		// ' => '\'' (close, escaped quote, reopen)
		esc := strings.ReplaceAll(a, "'", "'\\''")
		out = append(out, "'"+esc+"'")
	}
	return strings.Join(out, " ")
}

// isSafeBare returns true if a contains only safe characters for bare words.
func isSafeBare(s string) bool {
	for i := 0; i < len(s); i++ {
		b := s[i]
		if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '-' || b == '_' || b == '.' || b == '/' || b == ':' {
			continue
		}
		return false
	}
	return true
}