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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## v0.1.1 — TUI filter/select fix
- Fix: After filtering pods, pressing Enter now selects correctly.
- Enter applies filter; if one result, auto-selects; otherwise returns focus to table.
- Tab toggles focus; Esc cancels filter.
- Improves overall UX without changing keybindings for table navigation.
4 changes: 2 additions & 2 deletions cmd/kubeutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package cmd

import (
"fmt"
"os"
"path/filepath"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"os"
"path/filepath"
)

func kubeconfigPath() string {
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var rootCmd = &cobra.Command{
`,
RunE: func(cmd *cobra.Command, args []string) error {
if flagVersion {
fmt.Println("kdebug v0.1.0")
fmt.Println("kdebug v0.1.1")
return nil
}
return runInteractive()
Expand Down
121 changes: 92 additions & 29 deletions cmd/tui_picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,33 @@ import (
"k8s.io/client-go/kubernetes"
)

type podRow struct {
ns, name, ready, status, restarts, age string
}
type focusArea int

const (
focusTable focusArea = iota
focusFilter
)

type pickModel struct {
allPods []corev1.Pod
table table.Model
filter textinput.Model
selected *corev1.Pod
termWidth, termHt int
focus focusArea
}

func newPickModel(pods []corev1.Pod) pickModel {
m := pickModel{allPods: pods}
m := pickModel{allPods: pods, focus: focusTable}

// ASCII prompt avoids width quirks in some fonts
// Filter input (ASCII prompt avoids width quirks)
ti := textinput.New()
ti.Placeholder = "Press / to filter… (Enter to select, Esc to cancel)"
ti.Placeholder = "Press / to filter… (Enter applies; Enter again selects; Esc cancels)"
ti.Prompt = "> "
ti.Blur()
m.filter = ti

// Seed; real sizing happens on first WindowSizeMsg
// Seed columns; will be resized on first WindowSizeMsg
cols := []table.Column{
{Title: "NAMESPACE", Width: 12},
{Title: "NAME", Width: 24},
Expand All @@ -53,13 +57,12 @@ func newPickModel(pods []corev1.Pod) pickModel {
table.WithHeight(10),
)

// ZERO padding/margins/borders so headers align perfectly
// Compact styles (no vertical padding so rows are tight)
st := table.Styles{
Header: lipgloss.NewStyle().Bold(true),
Cell: lipgloss.NewStyle(),
// Selected row style—no extra padding
Selected: lipgloss.NewStyle().
Background(lipgloss.Color("57")). // tweak if you like
Background(lipgloss.Color("57")). // purple
Foreground(lipgloss.Color("230")),
}
t.SetStyles(st)
Expand Down Expand Up @@ -116,46 +119,81 @@ func (m pickModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch k.String() {
case "/":
m.focus = focusFilter
m.filter.Focus()
return m, nil

case "tab":
// Toggle focus
if m.focus == focusFilter {
m.filter.Blur()
m.focus = focusTable
} else {
m.focus = focusFilter
m.filter.Focus()
}
return m, nil

case "esc":
if m.filter.Focused() {
// If filtering, blur (and keep current filtered rows)
if m.focus == focusFilter {
m.filter.Blur()
m.filter.SetValue("")
m.applyFilter("")
m.focus = focusTable
return m, nil
}
return m, tea.Quit

case "enter", "ctrl+j":
if m.filter.Focused() {
if m.focus == focusFilter {
// Apply filter first
m.applyFilter(m.filter.Value())

// If exactly one row remains, auto-select it and quit
if len(m.table.Rows()) == 1 {
r := m.table.Rows()[0]
if pod, ok := m.findPod(r); ok {
m.selected = &pod
return m, tea.Quit
}
}

// Otherwise blur filter and return focus to table;
// user can press Enter again to select the highlighted row.
m.filter.Blur()
m.focus = focusTable
return m, nil
}

// Table-focused: select highlighted row
if len(m.table.Rows()) == 0 {
return m, nil
}
r := m.table.SelectedRow()
ns, name := r[0], r[1]
for i := range m.allPods {
if m.allPods[i].Namespace == ns && m.allPods[i].Name == name {
m.selected = &m.allPods[i]
break
}
if pod, ok := m.findPod(r); ok {
m.selected = &pod
}
return m, tea.Quit
}
}

var cmd tea.Cmd
m.table, cmd = m.table.Update(msg)
// Default updates
var cmds []tea.Cmd

// Update table always (so arrows work even when filter focused off)
var tcmd tea.Cmd
m.table, tcmd = m.table.Update(msg)
cmds = append(cmds, tcmd)

if m.filter.Focused() {
// Update filter only when focused
if m.focus == focusFilter {
var fcmd tea.Cmd
m.filter, fcmd = m.filter.Update(msg)
// Live filtering: update rows as user types
m.applyFilter(m.filter.Value())
return m, tea.Batch(cmd, fcmd)
cmds = append(cmds, fcmd)
}
return m, cmd

return m, tea.Batch(cmds...)
}

func (m *pickModel) resize() {
Expand All @@ -170,7 +208,7 @@ func (m *pickModel) resize() {
m.table.SetHeight(tableHeight)
m.table.SetWidth(m.termWidth)

// Fixed columns; give remainder to NAME.
// Column widths: fixed for most, NAME grows/shrinks
wNS := 16
wReady := 5
wStatus := 18
Expand All @@ -182,6 +220,7 @@ func (m *pickModel) resize() {
if wName < 10 {
wName = 10
}

m.table.SetColumns([]table.Column{
{Title: "NAMESPACE", Width: wNS},
{Title: "NAME", Width: wName},
Expand All @@ -196,23 +235,47 @@ func (m *pickModel) applyFilter(q string) {
q = strings.TrimSpace(strings.ToLower(q))
if q == "" {
m.table.SetRows(rowsFromPods(m.allPods))
// Keep cursor in-bounds
if len(m.table.Rows()) > 0 && m.table.Cursor() >= len(m.table.Rows()) {
m.table.SetCursor(len(m.table.Rows()) - 1)
}
return
}
filtered := make([]corev1.Pod, 0, len(m.allPods))
for _, p := range m.allPods {
s := strings.ToLower(strings.Join([]string{
p.Namespace, p.Name, podStatus(&p), fmt.Sprintf("%v", p.Labels),
p.Namespace,
p.Name,
podStatus(&p),
fmt.Sprintf("%v", p.Labels),
}, " "))
if strings.Contains(s, q) {
filtered = append(filtered, p)
}
}
m.table.SetRows(rowsFromPods(filtered))
// Reset cursor to first result when the list changes
if len(m.table.Rows()) > 0 {
m.table.SetCursor(0)
}
}

func (m pickModel) findPod(r table.Row) (corev1.Pod, bool) {
if len(r) < 2 {
return corev1.Pod{}, false
}
ns, name := r[0], r[1]
for i := range m.allPods {
if m.allPods[i].Namespace == ns && m.allPods[i].Name == name {
return m.allPods[i], true
}
}
return corev1.Pod{}, false
}

func (m pickModel) View() string {
// No extra blank lines; keeps header tight to first row
title := lipgloss.NewStyle().Bold(true).Render("Select a pod (↑/↓ or j/k to move, / to filter, Enter to select, Esc to cancel)")
title := lipgloss.NewStyle().Bold(true).
Render("Select a pod (↑/↓ or j/k move, / filter, Enter selects, Tab switch, Esc cancel)")
return title + "\n" + m.table.View() + "\n" + m.filter.View()
}

Expand Down