Compare commits

...
6 Commits
Author SHA1 Message Date
stefankoelle 2d382441a1 release v0.3.4
Improved Windows Terminal layout and update error handling.

- Split ratios: panel 25%, k9sA 37.5%, k9sB 37.5% (was 50/25/25) — k9s panes now get more space
- Update failures now exit with error code 1 instead of continuing silently with the old version
2026-09-16 22:21:56 +02:00
stefankoelle ad7d8534ab feat: improve Windows Terminal split ratios and update error handling
- Split ratios: panel 25%, k9sA 37.5%, k9sB 37.5% (was 50/25/25)
- Update failures now exit with error code 1 instead of continuing with old version
2026-09-16 22:20:42 +02:00
stefankoelle a0b937e882 release v0.3.3
Fix Windows Terminal mode issues.

- Fix split direction: use -H for top/bottom layout (opposite of tmux)
- Fix command quoting: use cmd /c to avoid Windows file-not-found errors
- Remove confusing tmux session closed message in wt mode
2026-09-16 21:44:37 +02:00
stefankoelle d32ccd5952 fix: avoid confusing tmux message in wt mode
Use c.Start() instead of tea.ExecProcess in startWtSession() to prevent
tmux session closed message from appearing in the main window.
2026-09-16 21:44:23 +02:00
stefankoelle 3729a0301d release v0.3.2
Windows Terminal native split-pane option.

- Add multiplexer config option to switch between tmux/psmux (default) and Windows Terminal's native split-pane
- Avoids the psmux focus-freeze issue when switching away from the terminal window on Windows
- Config: multiplexer: "tmux" (default) or "wt" for Windows Terminal
2026-09-16 21:12:26 +02:00
stefankoelle a1760b7b01 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
2026-09-16 21:10:36 +02:00
5 changed files with 91 additions and 16 deletions
+54 -3
View File
@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"runtime"
"sort" "sort"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -197,7 +198,11 @@ func (m *fullModel) handleSelect() (tea.Model, tea.Cmd) {
case screenNamespace: case screenNamespace:
m.selectedNamespace = item.value 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 m.err = err
return m, nil 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 // 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 // action), and the two status panes run k9s against the first two
// configured envs, resolved via the context template, so both are // 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 { 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() selfPath, err := os.Executable()
if err != nil { if err != nil {
selfPath = "kctl-tui" // fallback to PATH lookup selfPath = "kctl-tui" // fallback to PATH lookup
@@ -319,6 +333,43 @@ 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.
// Note: In Windows Terminal, -H (horizontal) stacks panes top/bottom,
// while -V (vertical) places them side by side — opposite of tmux.
// The -s flag controls the split ratio: first split gives k9sA 75%
// (panel keeps 25%), second split divides k9sA equally (37.5% each).
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)
wtCmd := fmt.Sprintf("wt new-tab %s ; split-pane -H -s 0.75 %s", 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 --namespace %s --command pods", ctxB, m.selectedNamespace)
kubeexec.VerboseLog("[debug] k9sCmdB=%s\n", k9sCmdB)
wtCmd += fmt.Sprintf(" ; split-pane -H -s 0.5 %s", k9sCmdB)
}
c := exec.Command("cmd", "/c", wtCmd)
_ = c.Start()
return nil
}
func (m *fullModel) View() string { func (m *fullModel) View() string {
view := m.list.View() view := m.list.View()
if m.statusMessage != "" { if m.statusMessage != "" {
+6 -1
View File
@@ -94,7 +94,12 @@ func main() {
fmt.Fprintf(os.Stderr, "WARNING: failed to load config: %v\n", cfgErr) fmt.Fprintf(os.Stderr, "WARNING: failed to load config: %v\n", cfgErr)
} }
if cfgErr == nil && cfg.IsAutoUpdateCheckEnabled() { if cfgErr == nil && cfg.IsAutoUpdateCheckEnabled() {
if checkForUpdateInteractive(verbose) { updated, err := checkForUpdateInteractive(verbose)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if updated {
os.Exit(0) os.Exit(0)
} }
} }
+12 -12
View File
@@ -83,14 +83,15 @@ func runUpdate(verbose bool) error {
} }
// checkForUpdateInteractive checks for a new version and prompts the user to update. // checkForUpdateInteractive checks for a new version and prompts the user to update.
// Returns true if an update was applied. // Returns (true, nil) if an update was applied successfully, (false, nil) if no
func checkForUpdateInteractive(verbose bool) bool { // update was needed or the user declined, and (false, err) if the update failed.
func checkForUpdateInteractive(verbose bool) (bool, error) {
if version == "dev" { if version == "dev" {
return false return false, nil
} }
if !term.IsTerminal(int(os.Stdin.Fd())) { if !term.IsTerminal(int(os.Stdin.Fd())) {
return false return false, nil
} }
updater, err := initUpdater(verbose) updater, err := initUpdater(verbose)
@@ -98,7 +99,7 @@ func checkForUpdateInteractive(verbose bool) bool {
if verbose { if verbose {
fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err) fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err)
} }
return false return false, nil
} }
ctx, cancel := context.WithTimeout(context.Background(), updateTimeout) ctx, cancel := context.WithTimeout(context.Background(), updateTimeout)
@@ -110,10 +111,10 @@ func checkForUpdateInteractive(verbose bool) bool {
if verbose { if verbose {
fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err) fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err)
} }
return false return false, nil
} }
if !found { if !found {
return false return false, nil
} }
current, _ := semver.NewVersion(version) current, _ := semver.NewVersion(version)
@@ -121,7 +122,7 @@ func checkForUpdateInteractive(verbose bool) bool {
newVer, _ := semver.NewVersion(newVersion) newVer, _ := semver.NewVersion(newVersion)
if current != nil && !current.LessThan(newVer) { if current != nil && !current.LessThan(newVer) {
return false return false, nil
} }
fmt.Printf("New version %s available (current: %s). Update now? [y/N] ", newVersion, version) fmt.Printf("New version %s available (current: %s). Update now? [y/N] ", newVersion, version)
@@ -131,17 +132,16 @@ func checkForUpdateInteractive(verbose bool) bool {
answer = strings.TrimSpace(strings.ToLower(answer)) answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" { if answer != "y" && answer != "yes" {
return false return false, nil
} }
fmt.Println("Updating...") fmt.Println("Updating...")
if err := updater.UpdateTo(ctx, rel, ""); err != nil { if err := updater.UpdateTo(ctx, rel, ""); err != nil {
fmt.Fprintf(os.Stderr, "Update failed: %v\n", err) return false, fmt.Errorf("update failed: %w", err)
return false
} }
fmt.Printf("Updated to %s. Please restart kctl-tui.\n", newVersion) fmt.Printf("Updated to %s. Please restart kctl-tui.\n", newVersion)
return true return true, nil
} }
type verboseLogger struct{} type verboseLogger struct{}
+5
View File
@@ -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 # 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. # available. Set to false to disable. Defaults to true if omitted.
# auto_update_check: true # 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"
+14
View File
@@ -73,6 +73,11 @@ type Config struct {
// AutoUpdateCheck controls whether kctl-tui checks for updates on // AutoUpdateCheck controls whether kctl-tui checks for updates on
// startup. Defaults to true when omitted. // startup. Defaults to true when omitted.
AutoUpdateCheck *bool `yaml:"auto_update_check"` 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 // IsAutoUpdateCheckEnabled returns true unless the user has explicitly set
@@ -84,6 +89,15 @@ func (c Config) IsAutoUpdateCheckEnabled() bool {
return *c.AutoUpdateCheck 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 // LoginCommand returns the configured AWS SSO login command, falling back
// to DefaultAWSSSOLoginCommand if none is set. // to DefaultAWSSSOLoginCommand if none is set.
func (c Config) LoginCommand() string { func (c Config) LoginCommand() string {