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
100 changes: 100 additions & 0 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Auto tag from PR labels

on:
pull_request:
types: [closed]
branches: [main]

permissions:
contents: write

jobs:
tag:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # need full history & tags

- name: Fetch tags
run: git fetch --tags --force

- name: Determine bump from PR labels
id: bump
shell: bash
env:
PR_LABELS: ${{ toJson(github.event.pull_request.labels) }}
run: |
# Collect label names (lowercase)
labels=$(echo "$PR_LABELS" | jq -r '.[].name' | tr '[:upper:]' '[:lower:]')
echo "PR labels: $labels"

level="patch" # default if none present
if echo "$labels" | grep -q '\bmajor\b'; then
level="major"
elif echo "$labels" | grep -q '\bminor\b'; then
level="minor"
elif echo "$labels" | grep -q '\bpatch\b'; then
level="patch"
fi

echo "level=$level" >> "$GITHUB_OUTPUT"
echo "Selected bump: $level"

- name: Compute next tag
id: next
shell: bash
env:
LEVEL: ${{ steps.bump.outputs.level }}
run: |
# Latest semver tag (vX.Y.Z). If none, seed at v0.1.0
latest="$(git tag --list 'v*.*.*' | sort -V | tail -n1)"
if [[ -z "$latest" ]]; then
latest="v0.1.0"
echo "No previous tag; starting at $latest"
fi
ver="${latest#v}"
IFS='.' read -r MA MI PA <<<"$ver"

case "$LEVEL" in
major) MA=$((MA+1)); MI=0; PA=0 ;;
minor) MI=$((MI+1)); PA=0 ;;
patch) PA=$((PA+1)) ;;
*) PA=$((PA+1)) ;;
esac

next="v${MA}.${MI}.${PA}"
echo "latest=$latest"
echo "next=$next"
echo "next=$next" >> "$GITHUB_OUTPUT"

- name: Tag and push
env:
NEXT: ${{ steps.next.outputs.next }}
run: |
if git rev-parse -q --verify "refs/tags/${NEXT}" >/dev/null; then
echo "Tag ${NEXT} already exists; nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$NEXT" -m "chore(release): $NEXT"
git push origin "$NEXT"

# Optional: comment back on the PR with the new version
- name: Comment with result
if: ${{ always() }}
uses: actions/github-script@v7
with:
script: |
const next = core.getInput('next', { required: false }) || '${{ steps.next.outputs.next }}';
const level = '${{ steps.bump.outputs.level }}';
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: `🔖 Released **${next}** (bump: \`${level}\`).`
});
result-encoding: string
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
Loading