mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-17 20:10:24 +00:00
Compare commits
8
Commits
bd23403e6c
...
v0.3.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb4c1be12b | ||
|
|
9e3fefb71a | ||
|
|
2d382441a1 | ||
|
|
ad7d8534ab | ||
|
|
a0b937e882 | ||
|
|
d32ccd5952 | ||
|
|
3729a0301d | ||
|
|
a1760b7b01 |
+54
-3
@@ -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,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 {
|
||||
view := m.list.View()
|
||||
if m.statusMessage != "" {
|
||||
|
||||
@@ -94,7 +94,12 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: failed to load config: %v\n", cfgErr)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-2
@@ -8,6 +8,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -61,6 +62,11 @@ type panelModel struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// isWtMode returns true when running on Windows with multiplexer: "wt".
|
||||
func (m *panelModel) isWtMode() bool {
|
||||
return runtime.GOOS == "windows" && m.cfg.MultiplexerBackend() == "wt"
|
||||
}
|
||||
|
||||
func runPanel(args []string) error {
|
||||
fs := flag.NewFlagSet("panel", flag.ContinueOnError)
|
||||
context := fs.String("context", "", "context (e.g. internal/external)")
|
||||
@@ -99,7 +105,9 @@ func newPanelModel(context, ns, team string) *panelModel {
|
||||
|
||||
func (m *panelModel) showEnvMenu() {
|
||||
items := make([]list.Item, 0, len(m.cfg.Envs)+1)
|
||||
items = append(items, simpleItem{label: "Quit (closes this tmux session)", value: "quit"})
|
||||
if !m.isWtMode() {
|
||||
items = append(items, simpleItem{label: "Quit (closes this tmux session)", value: "quit"})
|
||||
}
|
||||
for _, env := range m.cfg.Envs {
|
||||
items = append(items, simpleItem{label: env, value: env})
|
||||
}
|
||||
@@ -179,10 +187,14 @@ func (m *panelModel) usesTextInput() bool {
|
||||
// handleEsc navigates one level up: action menu -> env menu, most
|
||||
// sub-steps -> action menu. From the top-level env menu it closes the
|
||||
// whole tmux session (all panes, including the two k9s status panes)
|
||||
// before quitting this program, per SPEC.md 3.6.
|
||||
// before quitting this program, per SPEC.md 3.6. In wt mode, Esc on
|
||||
// the env menu does nothing — the user closes the wt window manually.
|
||||
func (m *panelModel) handleEsc() (tea.Model, tea.Cmd) {
|
||||
switch m.step {
|
||||
case stepEnvMenu:
|
||||
if m.isWtMode() {
|
||||
return m, nil
|
||||
}
|
||||
exec.Command("tmux", "kill-session", "-t", "kctl").Run()
|
||||
return m, tea.Quit
|
||||
case stepActionMenu:
|
||||
|
||||
+12
-12
@@ -83,14 +83,15 @@ func runUpdate(verbose bool) error {
|
||||
}
|
||||
|
||||
// checkForUpdateInteractive checks for a new version and prompts the user to update.
|
||||
// Returns true if an update was applied.
|
||||
func checkForUpdateInteractive(verbose bool) bool {
|
||||
// Returns (true, nil) if an update was applied successfully, (false, nil) if no
|
||||
// update was needed or the user declined, and (false, err) if the update failed.
|
||||
func checkForUpdateInteractive(verbose bool) (bool, error) {
|
||||
if version == "dev" {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
updater, err := initUpdater(verbose)
|
||||
@@ -98,7 +99,7 @@ func checkForUpdateInteractive(verbose bool) bool {
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err)
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), updateTimeout)
|
||||
@@ -110,10 +111,10 @@ func checkForUpdateInteractive(verbose bool) bool {
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err)
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
current, _ := semver.NewVersion(version)
|
||||
@@ -121,7 +122,7 @@ func checkForUpdateInteractive(verbose bool) bool {
|
||||
newVer, _ := semver.NewVersion(newVersion)
|
||||
|
||||
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)
|
||||
@@ -131,17 +132,16 @@ func checkForUpdateInteractive(verbose bool) bool {
|
||||
answer = strings.TrimSpace(strings.ToLower(answer))
|
||||
|
||||
if answer != "y" && answer != "yes" {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
fmt.Println("Updating...")
|
||||
if err := updater.UpdateTo(ctx, rel, ""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Update failed: %v\n", err)
|
||||
return false
|
||||
return false, fmt.Errorf("update failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated to %s. Please restart kctl-tui.\n", newVersion)
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type verboseLogger struct{}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user