Skip to content

Commit

Permalink
Add suggestions in filter textfield (#95)
Browse files Browse the repository at this point in the history
* suggestions in filter

* Renamed fields

* Refactored string splitting

* Added help text on suggestions

* Updated README.md

---------

Co-authored-by: Kalle Fagerberg <kalle.fagerberg@riskident.com>
  • Loading branch information
semihbkgr and applejag authored May 10, 2024
1 parent 14dbb40 commit cd392da
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 41 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,12 @@ kubectl klock pods -W
There's also some hotkeys available:

```text
→/l/pgdn next page d show/hide deleted ctrl+c quit
←/h/pgup prev page f toggle fullscreen ?/esc close help
g/home go to start / filter by text
G/end go to end enter close the filter input field
esc clear the applied filter
→/l/pgdn next page / filter by text ctrl+c quit
←/h/pgup prev page enter close the filter input field ?/esc close help
g/home go to start esc clear the applied filter d show/hide deleted
G/end go to end ↓/ctrl+n show next suggestion f toggle fullscreen
↑/ctrl+p show previous suggestion
tab accept a suggestion
```

## Features
Expand Down
43 changes: 43 additions & 0 deletions internal/util/strings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Kalle Fagerberg
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the
// Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.

package util

import "strings"

func SplitsFromStart(s string, sep byte) []string {
if s == "" {
return nil
}

var indexFromStart int
var result []string

for {
index := strings.IndexByte(s[indexFromStart:], sep)
if index == -1 {
break
}

indexFromStart += index
result = append(result, s[:indexFromStart])
indexFromStart++ // skip over the separator
}

result = append(result, s)
return result
}
62 changes: 62 additions & 0 deletions internal/util/strings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2024 Kalle Fagerberg
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the
// Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.

package util

import (
"slices"
"testing"
)

func TestSplitsFromStart(t *testing.T) {
const sep = '-'
tests := []struct {
name string
s string
want []string
}{
{
name: "empty",
s: "",
want: nil,
},
{
name: "1 split",
s: "foo",
want: []string{"foo"},
},
{
name: "2 splits",
s: "foo-bar",
want: []string{"foo", "foo-bar"},
},
{
name: "deployment pod name",
s: "thing-operator-675ffd4bbb-jfsfn",
want: []string{"thing", "thing-operator", "thing-operator-675ffd4bbb", "thing-operator-675ffd4bbb-jfsfn"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := SplitsFromStart(tc.s, sep)
if !slices.Equal(tc.want, got) {
t.Errorf("wrong result\ns: %q\nsep: %q\nwant: %v\ngot: %v", tc.s, sep, tc.want, got)
}
})
}
}
11 changes: 6 additions & 5 deletions pkg/klock/klock.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,17 +411,18 @@ func (p *Printer) addObjectToTable(objTable *metav1.Table, eventType watch.Event
return nil, fmt.Errorf("metadata.creationTimestamp: %w", err)
}
tableRow := table.Row{
ID: uid,
Fields: make([]any, 0, len(p.colDefs)),
SortField: name,
ID: uid,
Fields: make([]any, 0, len(p.colDefs)),
SortKey: name,
Suggestion: name,
}
if p.apiVersion == "v1" && p.kind == "Event" {
tableRow.SortField = creationTimestamp
tableRow.SortKey = creationTimestamp
}
if p.printNamespace {
namespace := metadata["namespace"]
tableRow.Fields = append(tableRow.Fields, namespace)
tableRow.SortField = fmt.Sprintf("%s/%s", namespace, tableRow.SortField)
tableRow.SortKey = fmt.Sprintf("%s/%s", namespace, tableRow.SortKey)
}
for i, cell := range row.Cells {
if i >= len(p.colDefs) {
Expand Down
56 changes: 39 additions & 17 deletions pkg/table/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@ type KeyMap struct {
GoToEnd key.Binding

// Keybindings for view settings
Filter key.Binding
ToggleDeleted key.Binding
ToggleFullscreen key.Binding

// Keybindings used while the text-filter is enabled.
CloseFilter key.Binding
ClearFilter key.Binding
Filter key.Binding
CloseFilter key.Binding
ClearFilter key.Binding
NextSuggestion key.Binding
PrevSuggestion key.Binding
AcceptSuggestion key.Binding

// Help toggle keybindings.
ShowFullHelp key.Binding
Expand Down Expand Up @@ -76,16 +79,16 @@ var DefaultKeyMap = KeyMap{
key.WithHelp("f", "toggle fullscreen"),
),

Filter: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "filter by text"),
),
ToggleDeleted: key.NewBinding(
key.WithKeys("d"),
key.WithHelp("d", "show/hide deleted"),
),

// Filtering.
Filter: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "filter by text"),
),
CloseFilter: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "close the filter input field"),
Expand All @@ -94,6 +97,18 @@ var DefaultKeyMap = KeyMap{
key.WithKeys("esc"),
key.WithHelp("esc", "clear the applied filter"),
),
NextSuggestion: key.NewBinding(
key.WithKeys("down", "ctrl+n"),
key.WithHelp("↓/ctrl+n", "show next suggestion"),
),
PrevSuggestion: key.NewBinding(
key.WithKeys("up", "ctrl+p"),
key.WithHelp("↑/ctrl+p", "show previous suggestion"),
),
AcceptSuggestion: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "accept a suggestion"),
),

// Toggle help.
ShowFullHelp: key.NewBinding(
Expand All @@ -115,7 +130,7 @@ var DefaultKeyMap = KeyMap{
// FullHelp returns bindings to show the full help view. It's part of the
// help.KeyMap interface.
func (m Model) FullHelp() [][]key.Binding {
kb := [][]key.Binding{{
browsingBindings := [][]key.Binding{{
m.KeyMap.NextPage,
m.KeyMap.PrevPage,
m.KeyMap.GoToStart,
Expand All @@ -132,18 +147,25 @@ func (m Model) FullHelp() [][]key.Binding {
// }
//}

listLevelBindings := []key.Binding{
m.KeyMap.ToggleDeleted,
m.KeyMap.ToggleFullscreen,
filterBindings := []key.Binding{
m.KeyMap.Filter,
m.KeyMap.CloseFilter,
m.KeyMap.ClearFilter,
m.KeyMap.NextSuggestion,
m.KeyMap.PrevSuggestion,
m.KeyMap.AcceptSuggestion,
}

actionsBindings := []key.Binding{
m.KeyMap.ForceQuit,
m.KeyMap.CloseFullHelp,
m.KeyMap.ToggleDeleted,
m.KeyMap.ToggleFullscreen,
}

return append(kb,
listLevelBindings,
[]key.Binding{
m.KeyMap.ForceQuit,
m.KeyMap.CloseFullHelp,
})
return append(
browsingBindings,
filterBindings,
actionsBindings,
)
}
13 changes: 7 additions & 6 deletions pkg/table/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ func (c AgoColumn) String() string {
}

type Row struct {
ID string
Fields []any
Status Status
SortField string
ID string
Fields []any
Status Status
SortKey string
Suggestion string

renderedFields []string
}
Expand All @@ -78,8 +79,8 @@ const (

// SortValue value is the value we use when sorting the list.
func (r Row) SortValue() string {
if r.SortField != "" {
return r.SortField
if r.SortKey != "" {
return r.SortKey
}
if len(r.Fields) == 0 {
return ""
Expand Down
42 changes: 34 additions & 8 deletions pkg/table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strings"
"time"

"github.com/applejag/kubectl-klock/internal/util"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/paginator"
Expand Down Expand Up @@ -100,14 +101,15 @@ type Model struct {
filterInput textinput.Model
showSpinner bool

err error
headers []string
maxHeight int
rows []Row
filteredRows []Row
columnWidths []int
fullscreenOverride bool
quitting bool
err error
headers []string
maxHeight int
rows []Row
filteredRows []Row
columnWidths []int
fullscreenOverride bool
quitting bool
prevSuggestionCount int

filterInputEnabled bool
}
Expand Down Expand Up @@ -167,6 +169,7 @@ func (m *Model) SetRows(rows []Row) tea.Cmd {

func (m *Model) updateRows() {
m.updateFilteredRows()
m.updateFilterSuggestions()
m.updatePagination()
m.updateColumnWidths()
}
Expand Down Expand Up @@ -194,6 +197,26 @@ func rowMatchesText(row Row, needle string) bool {
return false
}

func (m *Model) updateFilterSuggestions() {
m.filterInput.ShowSuggestions = true
suggestionsMap := make(map[string]struct{}, m.prevSuggestionCount)
suggestions := make([]string, 0, m.prevSuggestionCount)

for _, row := range m.filteredRows {
for _, split := range util.SplitsFromStart(row.Suggestion, '-') {
_, isDuplicate := suggestionsMap[split]
if isDuplicate {
continue
}
suggestionsMap[split] = struct{}{}
suggestions = append(suggestions, split)
}
}

m.prevSuggestionCount = len(suggestions)
m.filterInput.SetSuggestions(suggestions)
}

func (m *Model) SetError(err error) {
m.err = err
}
Expand Down Expand Up @@ -278,6 +301,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch {
case m.filterInputEnabled && !m.KeyMap.EscapeFilterText(msg):
m.filterInput.KeyMap.NextSuggestion = m.KeyMap.NextSuggestion
m.filterInput.KeyMap.PrevSuggestion = m.KeyMap.PrevSuggestion
m.filterInput.KeyMap.AcceptSuggestion = m.KeyMap.AcceptSuggestion
i, cmd := m.filterInput.Update(msg)
m.filterInput = i
m.updateRows()
Expand Down
25 changes: 25 additions & 0 deletions pkg/table/table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Kalle Fagerberg
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the
// Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.

package table

import (
"testing"
)

func Test(t *testing.T) {
}

0 comments on commit cd392da

Please sign in to comment.