From a1760b7b01a7ff48d5afe33457a62e78a9bc75eb Mon Sep 17 00:00:00 2001 From: Stefan Koelle Date: Wed, 16 Sep 2026 21:10:36 +0200 Subject: [PATCH] feat: add Windows Terminal native split-pane option Add multiplexer config option to switch between tmux/psmux (default) and Windows Terminal's native split-pane on Windows. This avoids the psmux focus-freeze issue when switching away from the terminal window. - Add Multiplexer field to config with MultiplexerBackend() getter - Add startWtSession() using wt.exe split-pane command - Dispatch startTmuxSession() based on OS and config - Document option in config.example.yaml --- cmd/kctl-tui/full.go | 54 ++++++++++++++++++++++++++++++++++++--- config.example.yaml | 5 ++++ internal/config/config.go | 14 ++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/cmd/kctl-tui/full.go b/cmd/kctl-tui/full.go index a28500a..a5e2cbb 100644 --- a/cmd/kctl-tui/full.go +++ b/cmd/kctl-tui/full.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "os/exec" + "runtime" "sort" tea "github.com/charmbracelet/bubbletea" @@ -197,7 +198,11 @@ func (m *fullModel) handleSelect() (tea.Model, tea.Cmd) { case screenNamespace: m.selectedNamespace = item.value - if err := kubeexec.CheckTool("tmux"); err != nil { + tool := "tmux" + if runtime.GOOS == "windows" && m.cfg.MultiplexerBackend() == "wt" { + tool = "wt" + } + if err := kubeexec.CheckTool(tool); err != nil { m.err = err return m, nil } @@ -262,12 +267,21 @@ func (m *fullModel) loadNamespacesFor(teamValue string) tea.Cmd { } } -// startTmuxSession builds the 3-pane tmux command: the control pane runs +// startTmuxSession builds the 3-pane session: the control pane runs // this binary in "panel" mode (letting the user pick an env and an // action), and the two status panes run k9s against the first two // configured envs, resolved via the context template, so both are -// visible side by side. +// visible side by side. On Windows with multiplexer: "wt", this uses +// Windows Terminal's native split-pane instead of tmux/psmux. func (m *fullModel) startTmuxSession() tea.Cmd { + if runtime.GOOS == "windows" && m.cfg.MultiplexerBackend() == "wt" { + return m.startWtSession() + } + return m.startTmuxSessionTmux() +} + +// startTmuxSessionTmux creates the session using tmux/psmux. +func (m *fullModel) startTmuxSessionTmux() tea.Cmd { selfPath, err := os.Executable() if err != nil { selfPath = "kctl-tui" // fallback to PATH lookup @@ -319,6 +333,40 @@ func (m *fullModel) startTmuxSession() tea.Cmd { }) } +// startWtSession creates the session using Windows Terminal's native +// split-pane feature. This avoids the psmux focus-freeze issue on Windows. +func (m *fullModel) startWtSession() tea.Cmd { + selfPath, err := os.Executable() + if err != nil { + selfPath = "kctl-tui" + } + panelCmd := fmt.Sprintf("%s panel --context=%s --ns=%s --team=%s", + selfPath, m.selectedContext, m.selectedNamespace, m.selectedTeam) + + envA := m.cfg.Envs[0] + ctxA := m.cfg.ResolveContext(envA, m.selectedContext) + k9sCmdA := fmt.Sprintf("k9s --context %s --namespace %s --command pods", ctxA, m.selectedNamespace) + + kubeexec.VerboseLog("[debug] selfPath=%s\n", selfPath) + kubeexec.VerboseLog("[debug] panelCmd=%s\n", panelCmd) + kubeexec.VerboseLog("[debug] k9sCmdA=%s\n", k9sCmdA) + + args := []string{"new-tab", panelCmd, ";", "split-pane", "-V", 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 --namespace %s --command pods", ctxB, m.selectedNamespace) + kubeexec.VerboseLog("[debug] k9sCmdB=%s\n", k9sCmdB) + args = append(args, ";", "split-pane", "-V", k9sCmdB) + } + + c := exec.Command("wt", args...) + return tea.ExecProcess(c, func(err error) tea.Msg { + return tmuxDoneMsg{err: err} + }) +} + func (m *fullModel) View() string { view := m.list.View() if m.statusMessage != "" { diff --git a/config.example.yaml b/config.example.yaml index 855e4d2..ac383eb 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -72,3 +72,8 @@ aws_sso_login_command: "aws sso login" # Check for updates on startup and prompt to update if a newer version is # available. Set to false to disable. Defaults to true if omitted. # auto_update_check: true + +# Terminal multiplexer backend. "tmux" (default) uses tmux/psmux. +# "wt" uses Windows Terminal's native split-pane — avoids the psmux +# focus-freeze issue on Windows. Only effective on Windows. +# multiplexer: "tmux" diff --git a/internal/config/config.go b/internal/config/config.go index a769455..bdbcda0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -73,6 +73,11 @@ type Config struct { // AutoUpdateCheck controls whether kctl-tui checks for updates on // startup. Defaults to true when omitted. AutoUpdateCheck *bool `yaml:"auto_update_check"` + + // Multiplexer selects the terminal multiplexer for the session. + // "tmux" (default) uses tmux/psmux. "wt" uses Windows Terminal's + // native split-pane (only effective on Windows). + Multiplexer string `yaml:"multiplexer"` } // IsAutoUpdateCheckEnabled returns true unless the user has explicitly set @@ -84,6 +89,15 @@ func (c Config) IsAutoUpdateCheckEnabled() bool { return *c.AutoUpdateCheck } +// MultiplexerBackend returns the configured multiplexer backend, +// falling back to "tmux" if not set. +func (c Config) MultiplexerBackend() string { + if c.Multiplexer == "" { + return "tmux" + } + return c.Multiplexer +} + // LoginCommand returns the configured AWS SSO login command, falling back // to DefaultAWSSSOLoginCommand if none is set. func (c Config) LoginCommand() string {