summaryrefslogtreecommitdiff
path: root/internal/tmux/tmux.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tmux/tmux.go')
-rw-r--r--internal/tmux/tmux.go85
1 files changed, 85 insertions, 0 deletions
diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go
new file mode 100644
index 0000000..63b5660
--- /dev/null
+++ b/internal/tmux/tmux.go
@@ -0,0 +1,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
+}