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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
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 Runner{}.Available() }
type Runner struct {
lookPath func(string) (string, error)
command func(string, ...string) *exec.Cmd
}
func (r Runner) find(name string) (string, error) {
if r.lookPath != nil {
return r.lookPath(name)
}
return exec.LookPath(name)
}
func (r Runner) cmd(name string, args ...string) *exec.Cmd {
if r.command != nil {
return r.command(name, args...)
}
return exec.Command(name, args...)
}
// HasBinary reports whether the tmux binary is on PATH.
func HasBinary() bool { return Runner{}.HasBinary() }
func (r Runner) HasBinary() bool { _, err := r.find("tmux"); return err == nil }
func (r Runner) Available() bool { return r.HasBinary() && InSession() }
// 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 {
return Runner{}.SplitRun(opts, argv)
}
func (r Runner) 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 := r.cmd("tmux", args...)
return c.Run()
}
// PopupOpts controls how a tmux display-popup is created for running a command.
type PopupOpts struct {
Target string // optional pane target, e.g. ":."
Width string // popup width, e.g. "80%" or "120"; empty means tmux default
Height string // popup height, e.g. "80%" or "40"; empty means tmux default
}
// PopupRun opens a tmux display-popup and runs argv inside it.
// The -E flag makes the popup close automatically when the command exits.
// It returns once the popup has closed (blocking call).
func PopupRun(opts PopupOpts, argv []string) error {
return Runner{}.PopupRun(opts, argv)
}
func (r Runner) PopupRun(opts PopupOpts, argv []string) error {
if len(argv) == 0 {
return nil
}
args := []string{"display-popup", "-E"}
if cwd, err := os.Getwd(); err == nil && cwd != "" {
args = append(args, "-d", cwd)
}
if strings.TrimSpace(opts.Target) != "" {
args = append(args, "-t", opts.Target)
}
if strings.TrimSpace(opts.Width) != "" {
args = append(args, "-w", strings.TrimSpace(opts.Width))
}
if strings.TrimSpace(opts.Height) != "" {
args = append(args, "-h", strings.TrimSpace(opts.Height))
}
cmdStr := shellJoin(argv)
args = append(args, cmdStr)
return r.cmd("tmux", args...).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
}
|