summaryrefslogtreecommitdiff
path: root/internal/tmux
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
committerPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
commit4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b (patch)
tree2dc708cbb95975d34084eb5afd376871caa13e27 /internal/tmux
parentece7dcfd232b780f5650326c8ac2379ca70387d4 (diff)
ik0 replace test seams with dependency injection
Diffstat (limited to 'internal/tmux')
-rw-r--r--internal/tmux/status.go9
-rw-r--r--internal/tmux/status_coverage_test.go8
-rw-r--r--internal/tmux/tmux.go42
-rw-r--r--internal/tmux/tmux_test.go28
4 files changed, 54 insertions, 33 deletions
diff --git a/internal/tmux/status.go b/internal/tmux/status.go
index 2f8cff6..2657495 100644
--- a/internal/tmux/status.go
+++ b/internal/tmux/status.go
@@ -3,7 +3,6 @@ package tmux
import (
"fmt"
"os"
- "os/exec"
"strconv"
"strings"
"time"
@@ -33,7 +32,11 @@ func Enabled() bool {
// SetUserOption sets a global tmux user option like @hexai_status to value.
func SetUserOption(key, value string) error {
- if !Enabled() || !HasBinary() || !InSession() {
+ return Runner{}.SetUserOption(key, value)
+}
+
+func (r Runner) SetUserOption(key, value string) error {
+ if !Enabled() || !r.HasBinary() || !InSession() {
return nil
}
k := strings.TrimPrefix(strings.TrimSpace(key), "@")
@@ -41,7 +44,7 @@ func SetUserOption(key, value string) error {
return nil
}
// Use set-option -g so it appears for all windows
- return exec.Command("tmux", "set-option", "-g", "@"+k, value).Run()
+ return r.cmd("tmux", "set-option", "-g", "@"+k, value).Run()
}
// SetStatus is a convenience for setting @hexai_status.
diff --git a/internal/tmux/status_coverage_test.go b/internal/tmux/status_coverage_test.go
index 0c76ee6..732a89e 100644
--- a/internal/tmux/status_coverage_test.go
+++ b/internal/tmux/status_coverage_test.go
@@ -50,14 +50,12 @@ func TestSetUserOption_DisabledByEnv(t *testing.T) {
func TestSetUserOption_EmptyKey(t *testing.T) {
t.Setenv("HEXAI_TMUX_STATUS", "1")
t.Setenv("TMUX", "/tmp/tmux-1,1,1")
- old := lookPath
- t.Cleanup(func() { lookPath = old })
- lookPath = func(string) (string, error) { return "/bin/tmux", nil }
+ r := Runner{lookPath: func(string) (string, error) { return "/bin/tmux", nil }}
// Empty key after trimming should return nil
- if err := SetUserOption(" @ ", "test"); err != nil {
+ if err := r.SetUserOption(" @ ", "test"); err != nil {
t.Fatalf("expected nil for empty key, got %v", err)
}
- if err := SetUserOption(" ", "test"); err != nil {
+ if err := r.SetUserOption(" ", "test"); err != nil {
t.Fatalf("expected nil for blank key, got %v", err)
}
}
diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go
index 9a4f5ee..7e5c7c7 100644
--- a/internal/tmux/tmux.go
+++ b/internal/tmux/tmux.go
@@ -8,15 +8,33 @@ import (
)
// Available reports whether tmux is available and we appear to be in a tmux session.
-func Available() bool { return HasBinary() && InSession() }
+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.
-var (
- lookPath = exec.LookPath
- command = exec.Command
-)
+func HasBinary() bool { return Runner{}.HasBinary() }
-func HasBinary() bool { _, err := lookPath("tmux"); return err == nil }
+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")) != "" }
@@ -31,6 +49,10 @@ type SplitOpts struct {
// 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
}
@@ -49,7 +71,7 @@ func SplitRun(opts SplitOpts, argv []string) error {
// tmux takes a single command string. Use a conservative shell join.
cmdStr := shellJoin(argv)
args = append(args, cmdStr)
- c := command("tmux", args...)
+ c := r.cmd("tmux", args...)
return c.Run()
}
@@ -64,6 +86,10 @@ type PopupOpts struct {
// 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
}
@@ -82,7 +108,7 @@ func PopupRun(opts PopupOpts, argv []string) error {
}
cmdStr := shellJoin(argv)
args = append(args, cmdStr)
- return command("tmux", args...).Run()
+ return r.cmd("tmux", args...).Run()
}
// shellJoin quotes argv elements for safe use in a single shell command string.
diff --git a/internal/tmux/tmux_test.go b/internal/tmux/tmux_test.go
index a8d1574..0946b01 100644
--- a/internal/tmux/tmux_test.go
+++ b/internal/tmux/tmux_test.go
@@ -19,14 +19,12 @@ func TestInSession(t *testing.T) {
}
func TestHasBinary_UsesLookPath(t *testing.T) {
- old := lookPath
- t.Cleanup(func() { lookPath = old })
- lookPath = func(file string) (string, error) { return "/bin/tmux", nil }
- if !HasBinary() {
+ r := Runner{lookPath: func(file string) (string, error) { return "/bin/tmux", nil }}
+ if !r.HasBinary() {
t.Fatal("expected HasBinary true when lookPath succeeds")
}
- lookPath = func(file string) (string, error) { return "", errors.New("nope") }
- if HasBinary() {
+ r.lookPath = func(file string) (string, error) { return "", errors.New("nope") }
+ if r.HasBinary() {
t.Fatal("expected HasBinary false when lookPath fails")
}
}
@@ -237,17 +235,15 @@ func TestSplitRun_AssemblesArgs(t *testing.T) {
name string
args []string
}{}
- oldCmd := command
- t.Cleanup(func() { command = oldCmd })
- command = func(name string, args ...string) *exec.Cmd {
+ r := Runner{command: func(name string, args ...string) *exec.Cmd {
captured.name = name
captured.args = append([]string(nil), args...)
// Use a benign command that exits 0
return exec.Command("true")
- }
+ }}
opts := SplitOpts{Target: ":.", Vertical: true, Percent: 40}
argv := []string{"/path/to/bin", "-flag", "value with spaces", "and'quote"}
- if err := SplitRun(opts, argv); err != nil {
+ if err := r.SplitRun(opts, argv); err != nil {
t.Fatalf("SplitRun error: %v", err)
}
if captured.name != "tmux" {
@@ -270,17 +266,15 @@ func TestSplitRun_AssemblesArgs(t *testing.T) {
}
func TestAvailable(t *testing.T) {
- oldLook := lookPath
- t.Cleanup(func() { lookPath = oldLook })
// Present binary + TMUX set -> available
- lookPath = func(file string) (string, error) { return "/bin/tmux", nil }
+ r := Runner{lookPath: func(file string) (string, error) { return "/bin/tmux", nil }}
t.Setenv("TMUX", "/tmp/tmux-1,1,1")
- if !Available() {
+ if !r.Available() {
t.Fatal("expected Available true with TMUX + binary")
}
// No binary -> not available
- lookPath = func(file string) (string, error) { return "", errors.New("nope") }
- if Available() {
+ r.lookPath = func(file string) (string, error) { return "", errors.New("nope") }
+ if r.Available() {
t.Fatal("expected Available false without binary")
}
}