From fce0b58640f3f03bf6212cdb65b7c8606539d486 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 07:52:12 +0000 Subject: [PATCH 1/3] feat: open floating system terminal for exec instead of suspending TUI Pressing 'x' now launches kubectl exec in the default system terminal emulator (gnome-terminal, xterm, etc.) as a separate floating window, keeping the TUI alive so the user can type freely without losing context. https://claude.ai/code/session_01U4tvdcoQpWc17WQT3agZxh --- internal/tui/app.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/tui/app.go b/internal/tui/app.go index 4fc0bcd..7d7939c 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -1230,7 +1230,8 @@ func makeKubectlEditCmd(kubectlBin, resource, name, namespace string) *exec.Cmd return cmd } -// execIntoPod starts an exec session in the pod +// execIntoPod opens a floating system terminal window with kubectl exec. +// The TUI stays alive while the user types in the new window. func (m *Model) execIntoPod() tea.Cmd { names, namespace := m.getSelectedResourceInfo() if len(names) == 0 { @@ -1243,13 +1244,9 @@ func (m *Model) execIntoPod() tea.Cmd { return nil } - // Return a command that suspends the TUI - return tea.ExecProcess( - makeKubectlExecCmd(m.kubectl.BinaryPath(), names[0], namespace, ""), - func(err error) tea.Msg { - return nil // Don't show error, just return to TUI - }, - ) + cmd := makeKubectlExecCmd(m.kubectl.BinaryPath(), names[0], namespace, "") + launchInNewShell(cmd.Args) + return nil } // makeKubectlExecCmd creates an exec.Cmd for kubectl exec From 13ad30438c6793f75fe57c47b855d57c1042dee3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 08:50:11 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20open=20plain=20system=20terminal=20o?= =?UTF-8?q?n=20'x'=20=E2=80=94=20no=20pre-loaded=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the kubectl exec pre-load so pressing 'x' simply launches the default system terminal emulator (gnome-terminal, xterm, etc.) with a plain interactive shell where the user can type any command freely. https://claude.ai/code/session_01U4tvdcoQpWc17WQT3agZxh --- internal/tui/app.go | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/internal/tui/app.go b/internal/tui/app.go index 7d7939c..f01746f 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -1230,22 +1230,10 @@ func makeKubectlEditCmd(kubectlBin, resource, name, namespace string) *exec.Cmd return cmd } -// execIntoPod opens a floating system terminal window with kubectl exec. -// The TUI stays alive while the user types in the new window. +// execIntoPod opens the system's default terminal emulator as a floating window. +// The TUI stays alive; the user gets a plain interactive shell to type any command. func (m *Model) execIntoPod() tea.Cmd { - names, namespace := m.getSelectedResourceInfo() - if len(names) == 0 { - return nil - } - - resourceType := m.getCurrentResourceType() - // Exec only works for pods - if !isPodResource(resourceType) { - return nil - } - - cmd := makeKubectlExecCmd(m.kubectl.BinaryPath(), names[0], namespace, "") - launchInNewShell(cmd.Args) + launchTerminal() return nil } @@ -1296,6 +1284,33 @@ func launchInNewShell(args []string) { } } +// launchTerminal opens the system's default terminal emulator with a plain +// interactive shell — no pre-loaded command. The caller's TUI is unaffected. +func launchTerminal() { + terminals := []struct { + bin string + args []string + }{ + {"x-terminal-emulator", nil}, + {"gnome-terminal", nil}, + {"konsole", nil}, + {"xfce4-terminal", nil}, + {"xterm", nil}, + {"open", []string{"-a", "Terminal"}}, // macOS + } + + for _, t := range terminals { + if path, err := exec.LookPath(t.bin); err == nil { + cmd := exec.Command(path, t.args...) + cmd.Stdout = nil + cmd.Stderr = nil + cmd.Stdin = nil + _ = cmd.Start() + return + } + } +} + // makeKubectlLogsFollowCmd creates an exec.Cmd for kubectl logs -f func makeKubectlLogsFollowCmd(kubectlBin, podName, namespace, container string) *exec.Cmd { args := []string{"logs", "-f", podName} From ac7983d498cc89933eb24ea3a62e2b4a9249a22a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 10:21:00 +0000 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20rename=20exec=E2=86=92terminal,?= =?UTF-8?q?=20key=20x=E2=86=92T,=20clean=20up=20dead=20exec=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Config field: Exec → Terminal, yaml tag exec → terminal, default x → T - KeyMap: Exec → Terminal binding with help text "terminal" - Case handler: "x" → m.config.Keybindings.Terminal, calls openTerminal() - Footer hints: [x]exec → [T]erminal in both normal and filtered views - Removed dead code: makeKubectlExecCmd, execWithContainer, container-exec selector case, ExecRequestMsg (none were reachable after the rework) - README: updated keybinding table and config example https://claude.ai/code/session_01U4tvdcoQpWc17WQT3agZxh --- README.md | 4 +-- internal/config/config.go | 2 +- internal/config/defaults.go | 2 +- internal/tui/app.go | 55 +++++-------------------------------- internal/tui/keys.go | 10 +++---- internal/tui/messages.go | 7 ----- 6 files changed, 16 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 79302f2..864d28b 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Press `/` to activate fuzzy filter mode. The filter uses fuzzy matching to filte | `L` | Follow logs (pods only) | | `D` | Delete resource (with confirmation) | | `e` | Edit resource | -| `x` | Exec into pod | +| `T` | Open system terminal | | `R` | Rollout restart (deployments) | ### Selection @@ -206,7 +206,7 @@ keybindings: logs_follow: "L" delete: "D" edit: "e" - exec: "x" + terminal: "T" yaml_view: "Y" json_view: "J" search: "/" diff --git a/internal/config/config.go b/internal/config/config.go index 60af463..493e168 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -55,7 +55,7 @@ type Keybindings struct { Logs string `yaml:"logs"` Delete string `yaml:"delete"` Edit string `yaml:"edit"` - Exec string `yaml:"exec"` + Terminal string `yaml:"terminal"` PortForward string `yaml:"port_forward"` Scale string `yaml:"scale"` RolloutRestart string `yaml:"rollout_restart"` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 364613a..92e997f 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -18,7 +18,7 @@ func DefaultConfig() *Config { Logs: "L", Delete: "D", Edit: "e", - Exec: "x", + Terminal: "T", PortForward: "p", Scale: "s", RolloutRestart: "R", diff --git a/internal/tui/app.go b/internal/tui/app.go index f01746f..1be3ba7 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -626,9 +626,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "e": // Edit resource return m, m.editResource() - case "x": - // Exec into pod - return m, m.execIntoPod() + case m.config.Keybindings.Terminal: + // Open system terminal + return m, m.openTerminal() case "R": // Rollout restart return m, m.startLoading(m.rolloutRestart()) @@ -838,11 +838,11 @@ func (m *Model) View() string { } b.WriteString(hint) } else if m.search.IsFiltered() { - b.WriteString(wrapAtWidth(fmt.Sprintf("[Esc] clear filter [/] modify filter [d]escribe [L]ogs [D]elete [e]dit [x]exec [+]new tab [%s]delete tab", deleteTabKey), m.width)) + b.WriteString(wrapAtWidth(fmt.Sprintf("[Esc] clear filter [/] modify filter [d]escribe [L]ogs [D]elete [e]dit [T]erminal [+]new tab [%s]delete tab", deleteTabKey), m.width)) } else if m.currentTab == SearchTabIndex { b.WriteString(wrapAtWidth(fmt.Sprintf("[Enter] enter command [/]filter results [r]efresh [q]uit [+]new tab [%s]delete tab", deleteTabKey), m.width)) } else { - b.WriteString(wrapAtWidth(fmt.Sprintf("[d]escribe [L]ogs [Y]aml [D]elete [e]dit [x]exec [R]estart [c]ontext [n]amespace [s]ort [/]search [r]efresh [?]help [+]new tab [%s]delete tab", deleteTabKey), m.width)) + b.WriteString(wrapAtWidth(fmt.Sprintf("[d]escribe [L]ogs [Y]aml [D]elete [e]dit [T]erminal [R]estart [c]ontext [n]amespace [s]ort [/]search [r]efresh [?]help [+]new tab [%s]delete tab", deleteTabKey), m.width)) } return b.String() @@ -1230,30 +1230,13 @@ func makeKubectlEditCmd(kubectlBin, resource, name, namespace string) *exec.Cmd return cmd } -// execIntoPod opens the system's default terminal emulator as a floating window. +// openTerminal opens the system's default terminal emulator as a floating window. // The TUI stays alive; the user gets a plain interactive shell to type any command. -func (m *Model) execIntoPod() tea.Cmd { +func (m *Model) openTerminal() tea.Cmd { launchTerminal() return nil } -// makeKubectlExecCmd creates an exec.Cmd for kubectl exec -func makeKubectlExecCmd(kubectlBin, podName, namespace, container string) *exec.Cmd { - args := []string{"exec", "-it", podName} - if namespace != "" { - args = append(args, "-n", namespace) - } - if container != "" { - args = append(args, "-c", container) - } - args = append(args, "--", "/bin/sh") - cmd := exec.Command(kubectlBin, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd -} - // launchInNewShell opens a command in a new terminal window. // It tries common terminal emulators in order of preference. func launchInNewShell(args []string) { @@ -1479,9 +1462,6 @@ func (m *Model) handleSelectorResult() tea.Cmd { container := strings.TrimPrefix(selected, "log ") container = strings.TrimSuffix(container, " --follow") return m.viewLogsWithContainer(container, follow) - case "container-exec": - // Use selected container for exec - return m.execWithContainer(selected) case "context": // Switch context return m.switchContext(selected) @@ -1536,27 +1516,6 @@ func (m *Model) viewLogsWithContainer(container string, follow bool) tea.Cmd { } } -// execWithContainer starts exec with a specific container -func (m *Model) execWithContainer(container string) tea.Cmd { - if len(m.pendingNames) == 0 { - return nil - } - - podName := m.pendingNames[0] - namespace := m.pendingNs - kubectlBin := m.kubectl.BinaryPath() - - m.pendingNames = nil - m.pendingNs = "" - - return tea.ExecProcess( - makeKubectlExecCmd(kubectlBin, podName, namespace, container), - func(err error) tea.Msg { - return nil - }, - ) -} - // switchContext switches to a different kubectl context func (m *Model) switchContext(contextName string) tea.Cmd { return func() tea.Msg { diff --git a/internal/tui/keys.go b/internal/tui/keys.go index fb9a901..88dbcb4 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -22,7 +22,7 @@ type KeyMap struct { Logs key.Binding Delete key.Binding Edit key.Binding - Exec key.Binding + Terminal key.Binding PortForward key.Binding Scale key.Binding RolloutRestart key.Binding @@ -96,9 +96,9 @@ func NewKeyMap(cfg config.Keybindings) *KeyMap { key.WithKeys(cfg.Edit), key.WithHelp(cfg.Edit, "edit"), ), - Exec: key.NewBinding( - key.WithKeys(cfg.Exec), - key.WithHelp(cfg.Exec, "exec"), + Terminal: key.NewBinding( + key.WithKeys(cfg.Terminal), + key.WithHelp(cfg.Terminal, "terminal"), ), PortForward: key.NewBinding( key.WithKeys(cfg.PortForward), @@ -151,7 +151,7 @@ func (k KeyMap) FullHelp() [][]key.Binding { {k.Up, k.Down, k.Enter, k.Escape}, {k.TabNext, k.TabPrev, k.Search, k.Refresh}, {k.Describe, k.Logs, k.Delete, k.Edit}, - {k.Exec, k.PortForward, k.Scale, k.RolloutRestart}, + {k.Terminal, k.PortForward, k.Scale, k.RolloutRestart}, {k.YAMLView, k.JSONView, k.SwitchNamespace, k.SwitchContext}, {k.Help, k.Quit}, } diff --git a/internal/tui/messages.go b/internal/tui/messages.go index ec6a6fe..b73260e 100644 --- a/internal/tui/messages.go +++ b/internal/tui/messages.go @@ -64,13 +64,6 @@ type LogsFollowMsg struct { NewShell bool // if true, open in a new terminal shell instead of suspending the TUI } -// ExecRequestMsg is sent to request exec into a container -type ExecRequestMsg struct { - PodName string - Namespace string - Container string -} - // TabAddedMsg is sent when a new tab has been validated and saved to config type TabAddedMsg struct { Name string