Fix Windows: run tmux setup commands individually, attach via ExecProcess

psmux on Windows doesn't handle ; separators when args are passed
individually via exec.Command. Instead of chaining commands with ;,
run each setup command (new-session, set-option, split-window, etc.)
as individual exec.Command calls. Only tmux attach uses ExecProcess
so it properly takes over the terminal.
This commit is contained in:
2026-08-09 21:34:46 +02:00
parent 9e6acdedc2
commit 37be0707ad
+22 -37
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"os"
"os/exec"
"runtime"
"sort"
tea "github.com/charmbracelet/bubbletea"
@@ -281,54 +280,40 @@ func (m *fullModel) startTmuxSession() tea.Cmd {
ctxA := m.cfg.ResolveContext(envA, m.selectedContext)
k9sCmdA := fmt.Sprintf("k9s --context %s -n %s", ctxA, m.selectedNamespace)
// Kill stale session first (ignore error if none exists).
killErr := exec.Command("tmux", "kill-session", "-t", "kctl").Run()
fmt.Fprintf(os.Stderr, "[debug] selfPath=%s\n", selfPath)
fmt.Fprintf(os.Stderr, "[debug] panelCmd=%s\n", panelCmd)
fmt.Fprintf(os.Stderr, "[debug] kill-session err=%v\n", killErr)
var c *exec.Cmd
if runtime.GOOS == "windows" {
// On Windows, psmux doesn't handle ; separators when args are
// passed individually via exec.Command. Build the full command
// string and run it through cmd.exe.
tmuxScript := fmt.Sprintf(
"tmux new-session -d -s kctl %q ; set-option -t kctl remain-on-exit on ; split-window -v -t kctl:0.0 %q",
panelCmd, k9sCmdA,
)
if len(m.cfg.Envs) > 1 {
envB := m.cfg.Envs[1]
ctxB := m.cfg.ResolveContext(envB, m.selectedContext)
k9sCmdB := fmt.Sprintf("k9s --context %s -n %s", ctxB, m.selectedNamespace)
tmuxScript += fmt.Sprintf(" ; split-window -v -t kctl:0.1 %q", k9sCmdB)
}
tmuxScript += " ; select-layout -t kctl even-vertical ; select-pane -t kctl:0.0 ; attach -t kctl"
fmt.Fprintf(os.Stderr, "[debug] windows cmd: %s\n", tmuxScript)
c = exec.Command("cmd.exe", "/c", tmuxScript)
} else {
args := []string{
"new-session", "-d", "-s", "kctl",
panelCmd, ";",
"set-option", "-t", "kctl", "remain-on-exit", "on", ";",
"split-window", "-v", "-t", "kctl:0.0", k9sCmdA, ";",
// Kill stale session first (ignore error if none exists).
exec.Command("tmux", "kill-session", "-t", "kctl").Run()
// Run setup commands individually — this avoids the ; separator
// issue on Windows where psmux doesn't handle chained args.
setup := [][]string{
{"new-session", "-d", "-s", "kctl", panelCmd},
{"set-option", "-t", "kctl", "remain-on-exit", "on"},
{"split-window", "-v", "-t", "kctl:0.0", k9sCmdA},
}
if len(m.cfg.Envs) > 1 {
envB := m.cfg.Envs[1]
ctxB := m.cfg.ResolveContext(envB, m.selectedContext)
k9sCmdB := fmt.Sprintf("k9s --context %s -n %s", ctxB, m.selectedNamespace)
args = append(args,
"split-window", "-v", "-t", "kctl:0.1", k9sCmdB, ";",
)
setup = append(setup, []string{"split-window", "-v", "-t", "kctl:0.1", k9sCmdB})
}
args = append(args,
"select-layout", "-t", "kctl", "even-vertical", ";",
"select-pane", "-t", "kctl:0.0", ";",
"attach", "-t", "kctl",
setup = append(setup,
[]string{"select-layout", "-t", "kctl", "even-vertical"},
[]string{"select-pane", "-t", "kctl:0.0"},
)
fmt.Fprintf(os.Stderr, "[debug] unix args: %v\n", args)
c = exec.Command("tmux", args...)
for _, args := range setup {
if out, err := exec.Command("tmux", args...).CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "[debug] tmux %s failed: %v\n%s\n", args[0], err, out)
return func() tea.Msg { return tmuxDoneMsg{err: fmt.Errorf("tmux %s: %w", args[0], err)} }
}
}
// Only attach uses tea.ExecProcess so it takes over the terminal.
fmt.Fprintf(os.Stderr, "[debug] tmux attach -t kctl\n")
c := exec.Command("tmux", "attach", "-t", "kctl")
return tea.ExecProcess(c, func(err error) tea.Msg {
return tmuxDoneMsg{err: err}
})