From 6bc67e97f4ab96dd0cd884e3c07f8559cbaea5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cm=C3=BCt=20=C3=96zalp?= <54961032+uozalp@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:49:29 +0100 Subject: [PATCH] Sort all columns (#3650) * feat: enhance table column selection and sorting functionality * fix: update key binding for sorting selected column to Ctrl+O * remove unused setSelectedColIdx method and sortIndicator function * test: update expected hints count in various view tests * test: update expected hints count in TestServiceNew * fix: update keyboard handling for column selection to use Shift key * refactor: streamline column selection logic and initialize selected column based on current sort * fix: change key binding for sorting selected column from Ctrl+O to Shift+S * test: update expected hints count in help, namespace, pod, and PVC tests --- internal/ui/table.go | 186 +++++++++++++++++++++++++--- internal/ui/table_helper.go | 23 ++-- internal/view/alias_test.go | 2 +- internal/view/cm_test.go | 2 +- internal/view/container_test.go | 2 +- internal/view/context_test.go | 2 +- internal/view/dir_test.go | 2 +- internal/view/dp_test.go | 2 +- internal/view/ds_test.go | 2 +- internal/view/pf_test.go | 2 +- internal/view/priorityclass_test.go | 2 +- internal/view/rbac_test.go | 2 +- internal/view/reference_test.go | 2 +- internal/view/screen_dump_test.go | 2 +- internal/view/secret_test.go | 2 +- internal/view/sts_test.go | 2 +- internal/view/svc_test.go | 2 +- internal/view/table.go | 19 +++ 18 files changed, 216 insertions(+), 42 deletions(-) diff --git a/internal/ui/table.go b/internal/ui/table.go index e2932225..96efb939 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -37,25 +37,26 @@ type ( // Table represents tabular data. type Table struct { *SelectTable - gvr *client.GVR - sortCol model1.SortColumn - manualSort bool - Path string - Extras string - actions *KeyActions - cmdBuff *model.FishBuff - styles *config.Styles - viewSetting *config.ViewSetting - colorerFn model1.ColorerFunc - decorateFn DecorateFunc - wide bool - toast bool - hasMetrics bool - ctx context.Context - mx sync.RWMutex - readOnly bool - noIcon bool - fullGVR bool + gvr *client.GVR + sortCol model1.SortColumn + selectedColIdx int + manualSort bool + Path string + Extras string + actions *KeyActions + cmdBuff *model.FishBuff + styles *config.Styles + viewSetting *config.ViewSetting + colorerFn model1.ColorerFunc + decorateFn DecorateFunc + wide bool + toast bool + hasMetrics bool + ctx context.Context + mx sync.RWMutex + readOnly bool + noIcon bool + fullGVR bool } // NewTable returns a new table view. @@ -133,6 +134,137 @@ func (t *Table) getMSort() bool { return t.manualSort } +func (t *Table) getSelectedColIdx() int { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.selectedColIdx +} + +// initSelectedColumn initializes the selected column index based on current sort column. +func (t *Table) initSelectedColumn() { + data := t.GetFilteredData() + if data == nil || data.HeaderCount() == 0 { + return + } + + sc := t.getSortCol() + if sc.Name == "" { + t.mx.Lock() + t.selectedColIdx = 0 + t.mx.Unlock() + return + } + + // Find the visual column index for the current sort column + header := data.Header() + visibleCol := 0 + for _, h := range header { + if t.shouldExcludeColumn(h) { + continue + } + if h.Name == sc.Name { + t.mx.Lock() + t.selectedColIdx = visibleCol + t.mx.Unlock() + return + } + visibleCol++ + } + + // If sort column not found in visible columns, default to 0 + t.mx.Lock() + t.selectedColIdx = 0 + t.mx.Unlock() +} + +// moveSelectedColumn moves the column selection by delta (-1 for left, +1 for right). +func (t *Table) moveSelectedColumn(delta int) { + data := t.GetFilteredData() + if data == nil || data.HeaderCount() == 0 { + return + } + + // Count visible columns + visibleCount := 0 + for _, h := range data.Header() { + if !t.shouldExcludeColumn(h) { + visibleCount++ + } + } + + if visibleCount == 0 { + return + } + + t.mx.Lock() + t.selectedColIdx += delta + // Wrap around + if t.selectedColIdx >= visibleCount { + t.selectedColIdx = 0 + } else if t.selectedColIdx < 0 { + t.selectedColIdx = visibleCount - 1 + } + t.mx.Unlock() + + t.Refresh() +} + +// SelectNextColumn moves the column selection to the right. +func (t *Table) SelectNextColumn() { + t.moveSelectedColumn(1) +} + +// SelectPrevColumn moves the column selection to the left. +func (t *Table) SelectPrevColumn() { + t.moveSelectedColumn(-1) +} + +// SortSelectedColumn sorts by the currently selected column. +func (t *Table) SortSelectedColumn() { + data := t.GetFilteredData() + if data == nil || data.HeaderCount() == 0 { + return + } + + idx := t.getSelectedColIdx() + if idx < 0 { + return + } + + // Map visual column index to actual header column name + // (accounting for hidden columns) + header := data.Header() + visibleCol := 0 + var colName string + for _, h := range header { + if t.shouldExcludeColumn(h) { + continue + } + if visibleCol == idx { + colName = h.Name + break + } + visibleCol++ + } + + if colName == "" { + return + } + + sc := t.getSortCol() + + // Toggle direction if same column, otherwise default to ascending + asc := true + if sc.Name == colName { + asc = !sc.ASC + } + + t.SetSortCol(colName, asc) + t.setMSort(true) + t.Refresh() +} + // SetViewSetting sets custom view config is present. func (t *Table) SetViewSetting(vs *config.ViewSetting) bool { t.mx.Lock() @@ -315,8 +447,17 @@ func (t *Table) doUpdate(data *model1.TableData) *model1.TableData { } else { t.actions.Delete(KeyShiftP) } + + oldSortCol := t.getSortCol() t.setSortCol(data.ComputeSortCol(t.GetViewSetting(), t.getSortCol(), t.getMSort())) + // Initialize selected column index to match the current sort column + // This ensures the highlight starts at the sorted column + newSortCol := t.getSortCol() + if oldSortCol.Name != newSortCol.Name { + t.initSelectedColumn() + } + return data } @@ -429,6 +570,10 @@ func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tce sc.Name = name t.setSortCol(sc) t.setMSort(true) + + // Sync selected column index with the new sort column + t.initSelectedColumn() + t.Refresh() return nil } @@ -486,8 +631,9 @@ func (t *Table) NameColIndex() int { func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) { sc := t.getSortCol() sortCol := h.Name == sc.Name + selectedCol := col == t.getSelectedColIdx() styles := t.styles.Table() - c := tview.NewTableCell(sortIndicator(sortCol, sc.ASC, &styles, h.Name)) + c := tview.NewTableCell(columnIndicator(sortCol, selectedCol, sc.ASC, &styles, h.Name)) c.SetExpansion(1) c.SetSelectable(false) c.SetAlign(h.Align) diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 8722b75a..18a30d16 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -84,16 +84,25 @@ func SkinTitle(fmat string, style *config.Frame) string { return fmat } -func sortIndicator(sort, asc bool, style *config.Table, name string) string { - if !sort { - return name +func columnIndicator(sort, selected, asc bool, style *config.Table, name string) string { + // Build the column name with selection indicator + displayName := name + if selected { + // Make selected column bold + displayName = fmt.Sprintf("[::b]%s[::-]", name) } - order := descIndicator - if asc { - order = ascIndicator + // Add sort indicator if this column is sorted + suffix := "" + if sort { + order := descIndicator + if asc { + order = ascIndicator + } + suffix = fmt.Sprintf("[%s::b]%s[::]", style.Header.SorterColor, order) } - return fmt.Sprintf("%s[%s::b]%s[::]", name, style.Header.SorterColor, order) + + return displayName + suffix } func formatCell(field string, padding int) string { diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index 04d145ae..7fae7f31 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -30,7 +30,7 @@ func TestAliasNew(t *testing.T) { require.NoError(t, v.Init(makeContext(t))) assert.Equal(t, "Aliases", v.Name()) - assert.Len(t, v.Hints(), 6) + assert.Len(t, v.Hints(), 7) } func TestAliasSearch(t *testing.T) { diff --git a/internal/view/cm_test.go b/internal/view/cm_test.go index c7c8bf80..993246b4 100644 --- a/internal/view/cm_test.go +++ b/internal/view/cm_test.go @@ -17,5 +17,5 @@ func TestConfigMapNew(t *testing.T) { require.NoError(t, s.Init(makeCtx(t))) assert.Equal(t, "ConfigMaps", s.Name()) - assert.Len(t, s.Hints(), 7) + assert.Len(t, s.Hints(), 8) } diff --git a/internal/view/container_test.go b/internal/view/container_test.go index d46da494..981b9d0c 100644 --- a/internal/view/container_test.go +++ b/internal/view/container_test.go @@ -17,5 +17,5 @@ func TestContainerNew(t *testing.T) { require.NoError(t, c.Init(makeCtx(t))) assert.Equal(t, "Containers", c.Name()) - assert.Len(t, c.Hints(), 19) + assert.Len(t, c.Hints(), 20) } diff --git a/internal/view/context_test.go b/internal/view/context_test.go index 12df2044..d253f6b5 100644 --- a/internal/view/context_test.go +++ b/internal/view/context_test.go @@ -17,5 +17,5 @@ func TestContext(t *testing.T) { require.NoError(t, ctx.Init(makeCtx(t))) assert.Equal(t, "Contexts", ctx.Name()) - assert.Len(t, ctx.Hints(), 6) + assert.Len(t, ctx.Hints(), 7) } diff --git a/internal/view/dir_test.go b/internal/view/dir_test.go index 53818dfa..429526ef 100644 --- a/internal/view/dir_test.go +++ b/internal/view/dir_test.go @@ -16,5 +16,5 @@ func TestDir(t *testing.T) { require.NoError(t, v.Init(makeCtx(t))) assert.Equal(t, "Directory", v.Name()) - assert.Len(t, v.Hints(), 7) + assert.Len(t, v.Hints(), 8) } diff --git a/internal/view/dp_test.go b/internal/view/dp_test.go index fe7b9700..685a95c3 100644 --- a/internal/view/dp_test.go +++ b/internal/view/dp_test.go @@ -17,5 +17,5 @@ func TestDeploy(t *testing.T) { require.NoError(t, v.Init(makeCtx(t))) assert.Equal(t, "Deployments", v.Name()) - assert.Len(t, v.Hints(), 17) + assert.Len(t, v.Hints(), 18) } diff --git a/internal/view/ds_test.go b/internal/view/ds_test.go index 9f2787cf..f5db0013 100644 --- a/internal/view/ds_test.go +++ b/internal/view/ds_test.go @@ -17,5 +17,5 @@ func TestDaemonSet(t *testing.T) { require.NoError(t, v.Init(makeCtx(t))) assert.Equal(t, "DaemonSets", v.Name()) - assert.Len(t, v.Hints(), 17) + assert.Len(t, v.Hints(), 18) } diff --git a/internal/view/pf_test.go b/internal/view/pf_test.go index ebc030ce..90f86e2b 100644 --- a/internal/view/pf_test.go +++ b/internal/view/pf_test.go @@ -17,5 +17,5 @@ func TestPortForwardNew(t *testing.T) { require.NoError(t, pf.Init(makeCtx(t))) assert.Equal(t, "PortForwards", pf.Name()) - assert.Len(t, pf.Hints(), 10) + assert.Len(t, pf.Hints(), 11) } diff --git a/internal/view/priorityclass_test.go b/internal/view/priorityclass_test.go index b3e4f0ea..e2a38e9c 100644 --- a/internal/view/priorityclass_test.go +++ b/internal/view/priorityclass_test.go @@ -17,5 +17,5 @@ func TestPriorityClassNew(t *testing.T) { require.NoError(t, s.Init(makeCtx(t))) assert.Equal(t, "PriorityClass", s.Name()) - assert.Len(t, s.Hints(), 6) + assert.Len(t, s.Hints(), 7) } diff --git a/internal/view/rbac_test.go b/internal/view/rbac_test.go index 2ff06571..d3a83651 100644 --- a/internal/view/rbac_test.go +++ b/internal/view/rbac_test.go @@ -17,5 +17,5 @@ func TestRbacNew(t *testing.T) { require.NoError(t, v.Init(makeCtx(t))) assert.Equal(t, "Rbac", v.Name()) - assert.Len(t, v.Hints(), 5) + assert.Len(t, v.Hints(), 6) } diff --git a/internal/view/reference_test.go b/internal/view/reference_test.go index 63188f73..3bd4f3df 100644 --- a/internal/view/reference_test.go +++ b/internal/view/reference_test.go @@ -17,5 +17,5 @@ func TestReferenceNew(t *testing.T) { require.NoError(t, s.Init(makeCtx(t))) assert.Equal(t, "References", s.Name()) - assert.Len(t, s.Hints(), 4) + assert.Len(t, s.Hints(), 5) } diff --git a/internal/view/screen_dump_test.go b/internal/view/screen_dump_test.go index 06740fc6..33e5694e 100644 --- a/internal/view/screen_dump_test.go +++ b/internal/view/screen_dump_test.go @@ -17,5 +17,5 @@ func TestScreenDumpNew(t *testing.T) { require.NoError(t, po.Init(makeCtx(t))) assert.Equal(t, "ScreenDumps", po.Name()) - assert.Len(t, po.Hints(), 5) + assert.Len(t, po.Hints(), 6) } diff --git a/internal/view/secret_test.go b/internal/view/secret_test.go index 0b76e597..e75449e3 100644 --- a/internal/view/secret_test.go +++ b/internal/view/secret_test.go @@ -17,5 +17,5 @@ func TestSecretNew(t *testing.T) { require.NoError(t, s.Init(makeCtx(t))) assert.Equal(t, "Secrets", s.Name()) - assert.Len(t, s.Hints(), 8) + assert.Len(t, s.Hints(), 9) } diff --git a/internal/view/sts_test.go b/internal/view/sts_test.go index 0ff5d12d..d67f57b8 100644 --- a/internal/view/sts_test.go +++ b/internal/view/sts_test.go @@ -17,5 +17,5 @@ func TestStatefulSetNew(t *testing.T) { require.NoError(t, s.Init(makeCtx(t))) assert.Equal(t, "StatefulSets", s.Name()) - assert.Len(t, s.Hints(), 14) + assert.Len(t, s.Hints(), 15) } diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go index 3682fc56..b347bef8 100644 --- a/internal/view/svc_test.go +++ b/internal/view/svc_test.go @@ -174,5 +174,5 @@ func TestServiceNew(t *testing.T) { require.NoError(t, s.Init(makeCtx(t))) assert.Equal(t, "Services", s.Name()) - assert.Len(t, s.Hints(), 12) + assert.Len(t, s.Hints(), 13) } diff --git a/internal/view/table.go b/internal/view/table.go index f05c4780..799f9280 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -95,6 +95,19 @@ func (t *Table) SendKey(evt *tcell.EventKey) { func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { key := evt.Key() + + // Handle Shift+Left/Right for column selection + if evt.Modifiers()&tcell.ModShift != 0 { + if key == tcell.KeyLeft { + t.Table.SelectPrevColumn() + return nil + } + if key == tcell.KeyRight { + t.Table.SelectNextColumn() + return nil + } + } + if key == tcell.KeyUp || key == tcell.KeyDown { return evt } @@ -214,6 +227,7 @@ func (t *Table) bindKeys() { tcell.KeyCtrlW: ui.NewKeyAction("Toggle Wide", t.toggleWideCmd, false), ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(nameCol, true), false), ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(ageCol, true), false), + ui.KeyShiftS: ui.NewKeyAction("Sort Selected Column", t.sortSelectedColumnCmd, false), }) } @@ -227,6 +241,11 @@ func (t *Table) toggleWideCmd(*tcell.EventKey) *tcell.EventKey { return nil } +func (t *Table) sortSelectedColumnCmd(*tcell.EventKey) *tcell.EventKey { + t.Table.SortSelectedColumn() + return nil +} + func (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey { path := t.GetSelectedItem() if path == "" {