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 4fc0bcd..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,43 +1230,11 @@ func makeKubectlEditCmd(kubectlBin, resource, name, namespace string) *exec.Cmd return cmd } -// execIntoPod starts an exec session in the pod -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 - } - - // 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 - }, - ) -} - -// 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 +// 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) openTerminal() tea.Cmd { + launchTerminal() + return nil } // launchInNewShell opens a command in a new terminal window. @@ -1299,6 +1267,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} @@ -1467,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) @@ -1524,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