From a40ab82ecedbd95ace447630e72f31efee4d16d2 Mon Sep 17 00:00:00 2001 From: kjvjobin Date: Thu, 6 Nov 2025 21:55:11 +0530 Subject: [PATCH 1/2] fix(tui): allow filter & select; Fixes #4 --- cmd/kubeutil.go | 4 +- cmd/root.go | 2 +- cmd/tui_picker.go | 121 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 95 insertions(+), 32 deletions(-) diff --git a/cmd/kubeutil.go b/cmd/kubeutil.go index d68f56a..3592961 100644 --- a/cmd/kubeutil.go +++ b/cmd/kubeutil.go @@ -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 { diff --git a/cmd/root.go b/cmd/root.go index af05e1d..f313fd0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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() diff --git a/cmd/tui_picker.go b/cmd/tui_picker.go index a472ed7..0c24b40 100644 --- a/cmd/tui_picker.go +++ b/cmd/tui_picker.go @@ -14,9 +14,12 @@ 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 @@ -24,19 +27,20 @@ type pickModel struct { 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}, @@ -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) @@ -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() { @@ -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 @@ -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}, @@ -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() } From 628735f197a2bc374775477a27b5069cf01f702c Mon Sep 17 00:00:00 2001 From: kjvjobin Date: Thu, 6 Nov 2025 21:56:10 +0530 Subject: [PATCH 2/2] docs: add v0.1.1 notes for filter/select (fix #4) --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..14a7540 --- /dev/null +++ b/CHANGELOG.md @@ -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.