mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-17 20:10:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3729a0301d | ||
|
|
a1760b7b01 | ||
|
|
bd23403e6c | ||
|
|
6a0405640d |
+65
-19
@@ -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"
|
||||||
@@ -47,7 +48,7 @@ type fullModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newFullModel() *fullModel {
|
func newFullModel() *fullModel {
|
||||||
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
l := list.New(nil, newCompactDelegate(), 0, 0)
|
||||||
l.Title = "kctl-tui"
|
l.Title = "kctl-tui"
|
||||||
l.SetShowStatusBar(false)
|
l.SetShowStatusBar(false)
|
||||||
l.SetFilteringEnabled(false)
|
l.SetFilteringEnabled(false)
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -210,23 +215,21 @@ func (m *fullModel) handleSelect() (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// bootstrapContext resolves a kubectl context purely to discover
|
|
||||||
// namespaces/labels for the team/namespace screens. The first configured
|
|
||||||
// env is used as a stable default for this discovery step, since
|
|
||||||
// namespace names are assumed to be identical across envs.
|
|
||||||
func (m *fullModel) bootstrapContext() string {
|
|
||||||
if len(m.cfg.Envs) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return m.cfg.ResolveContext(m.cfg.Envs[0], m.selectedContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fullModel) loadTeams() tea.Msg {
|
func (m *fullModel) loadTeams() tea.Msg {
|
||||||
namespaces, err := kubeexec.GetNamespacesWithLabels(m.bootstrapContext())
|
merged := map[string]map[string]string{}
|
||||||
if err != nil {
|
for _, env := range m.cfg.Envs {
|
||||||
return errMsg{err}
|
ctx := m.cfg.ResolveContext(env, m.selectedContext)
|
||||||
|
namespaces, err := kubeexec.GetNamespacesWithLabels(ctx)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for ns, labels := range namespaces {
|
||||||
|
if _, exists := merged[ns]; !exists {
|
||||||
|
merged[ns] = labels
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return *toTeamsLoadedMsg(namespaces, m.cfg.TeamLabelKey)
|
return *toTeamsLoadedMsg(merged, m.cfg.TeamLabelKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fullModel) loadTeamsFor(namespaces map[string]map[string]string) tea.Cmd {
|
func (m *fullModel) loadTeamsFor(namespaces map[string]map[string]string) tea.Cmd {
|
||||||
@@ -264,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
|
||||||
@@ -321,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 {
|
func (m *fullModel) View() string {
|
||||||
view := m.list.View()
|
view := m.list.View()
|
||||||
if m.statusMessage != "" {
|
if m.statusMessage != "" {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/bubbles/list"
|
||||||
|
|
||||||
// simpleItem is a minimal implementation of list.Item used for all
|
// simpleItem is a minimal implementation of list.Item used for all
|
||||||
// selection screens (contexts, teams, namespaces, menu actions).
|
// selection screens (contexts, teams, namespaces, menu actions).
|
||||||
type simpleItem struct {
|
type simpleItem struct {
|
||||||
@@ -13,3 +15,13 @@ type simpleItem struct {
|
|||||||
func (i simpleItem) Title() string { return i.label }
|
func (i simpleItem) Title() string { return i.label }
|
||||||
func (i simpleItem) Description() string { return "" }
|
func (i simpleItem) Description() string { return "" }
|
||||||
func (i simpleItem) FilterValue() string { return i.label }
|
func (i simpleItem) FilterValue() string { return i.label }
|
||||||
|
|
||||||
|
// newCompactDelegate returns a list delegate that renders each item as a
|
||||||
|
// single line with no extra spacing, maximizing the number of visible
|
||||||
|
// entries in the terminal.
|
||||||
|
func newCompactDelegate() list.DefaultDelegate {
|
||||||
|
d := list.NewDefaultDelegate()
|
||||||
|
d.ShowDescription = false
|
||||||
|
d.SetSpacing(0)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func newPanelModel(context, ns, team string) *panelModel {
|
|||||||
cfgPath, _ := config.DefaultPath()
|
cfgPath, _ := config.DefaultPath()
|
||||||
cfg, loadErr := config.Load(cfgPath)
|
cfg, loadErr := config.Load(cfgPath)
|
||||||
|
|
||||||
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
l := list.New(nil, newCompactDelegate(), 0, 0)
|
||||||
l.SetShowStatusBar(false)
|
l.SetShowStatusBar(false)
|
||||||
l.SetFilteringEnabled(false)
|
l.SetFilteringEnabled(false)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user