Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -206,7 +206,7 @@ keybindings:
logs_follow: "L"
delete: "D"
edit: "e"
exec: "x"
terminal: "T"
yaml_view: "Y"
json_view: "J"
search: "/"
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
2 changes: 1 addition & 1 deletion internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func DefaultConfig() *Config {
Logs: "L",
Delete: "D",
Edit: "e",
Exec: "x",
Terminal: "T",
PortForward: "p",
Scale: "s",
RolloutRestart: "R",
Expand Down
103 changes: 37 additions & 66 deletions internal/tui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
detail *detail.Model
confirm *dialog.ConfirmModel
selector *dialog.SelectorModel
input *dialog.InputModel

Check failure on line 153 in internal/tui/app.go

View workflow job for this annotation

GitHub Actions / Lint

field `input` is unused (unused)

// Pending action state
pendingAction string
Expand All @@ -162,7 +162,7 @@
currentContext string
currentNamespace string
resources *kubectl.TableData
selectedIndex int

Check failure on line 165 in internal/tui/app.go

View workflow job for this annotation

GitHub Actions / Lint

field `selectedIndex` is unused (unused)

// Search tab state
searchInput *dialog.InputModel
Expand Down Expand Up @@ -626,9 +626,9 @@
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())
Expand Down Expand Up @@ -838,11 +838,11 @@
}
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()
Expand Down Expand Up @@ -946,7 +946,7 @@
func (m *Model) loadResourceDetail(format detail.Format) tea.Cmd {
return func() tea.Msg {
selected := m.list.SelectedItem()
if selected == nil || len(selected) == 0 {

Check failure on line 949 in internal/tui/app.go

View workflow job for this annotation

GitHub Actions / Lint

S1009: should omit nil check; len() for nil slices is defined as zero (gosimple)
return ErrorMsg{Err: nil}
}

Expand Down Expand Up @@ -1230,43 +1230,11 @@
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.
Expand Down Expand Up @@ -1299,6 +1267,33 @@
}
}

// 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}
Expand Down Expand Up @@ -1467,9 +1462,6 @@
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)
Expand Down Expand Up @@ -1524,27 +1516,6 @@
}
}

// 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 {
Expand Down
10 changes: 5 additions & 5 deletions internal/tui/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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},
}
Expand Down
7 changes: 0 additions & 7 deletions internal/tui/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading