Skip to content

Commit aa604fc

Browse files
committed
feat: add substring suggestion mode
Signed-off-by: y-rabie <[email protected]>
1 parent af27aff commit aa604fc

File tree

10 files changed

+245
-68
lines changed

10 files changed

+245
-68
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,11 @@ You can now override the context portForward default address configuration by se
461461
disableAutoscroll: false
462462
# Toggles log line timestamp info. Default false
463463
showTime: false
464+
# Define how suggestions are made.
465+
# "PREFIX": typed text is considered a prefix, sorts suggestions alphabetically. Default mode.
466+
# "LONGEST_PREFIX": typed text is considered a prefix, sorts suggestions by how much of the typed text they match; if equal, it falls back to alphabetical order.
467+
# "LONGEST_PREFIX": typed text is considered a substring, sorts suggestions by how much of the typed text they match; if equal, it falls back to alphabetical order.
468+
suggestionMode: PREFIX
464469
# Provide shell pod customization when nodeShell feature gate is enabled!
465470
shellPod:
466471
# The shell pod image to use.

cmd/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ func initK9sFlags() {
257257
"",
258258
"Sets a path to a dir for a screen dumps",
259259
)
260+
rootCmd.Flags().StringVar(
261+
k9sFlags.SuggestionMode,
262+
"suggestion-mode",
263+
string(config.DefaultSuggestionMode),
264+
"Set autocompletion suggestion mode (PREFIX, LONGEST_PREFIX, LONGEST_SUBSTRING)",
265+
)
260266
rootCmd.Flags()
261267
}
262268

internal/config/flags.go

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,44 @@ const (
1212

1313
// DefaultCommand represents the default command to run.
1414
DefaultCommand = ""
15+
16+
// DefaultSuggestionMode represents the default suggestion mode to use, which is `PREFIX`.
17+
DefaultSuggestionMode = SuggestionModePrefix
1518
)
1619

1720
// Flags represents K9s configuration flags.
1821
type Flags struct {
19-
RefreshRate *float32
20-
LogLevel *string
21-
LogFile *string
22-
Headless *bool
23-
Logoless *bool
24-
Command *string
25-
AllNamespaces *bool
26-
ReadOnly *bool
27-
Write *bool
28-
Crumbsless *bool
29-
Splashless *bool
30-
ScreenDumpDir *string
22+
RefreshRate *float32
23+
LogLevel *string
24+
LogFile *string
25+
Headless *bool
26+
Logoless *bool
27+
Command *string
28+
AllNamespaces *bool
29+
ReadOnly *bool
30+
Write *bool
31+
Crumbsless *bool
32+
Splashless *bool
33+
ScreenDumpDir *string
34+
SuggestionMode *string
3135
}
3236

3337
// NewFlags returns new configuration flags.
3438
func NewFlags() *Flags {
3539
return &Flags{
36-
RefreshRate: float32Ptr(DefaultRefreshRate),
37-
LogLevel: strPtr(DefaultLogLevel),
38-
LogFile: strPtr(AppLogFile),
39-
Headless: boolPtr(false),
40-
Logoless: boolPtr(false),
41-
Command: strPtr(DefaultCommand),
42-
AllNamespaces: boolPtr(false),
43-
ReadOnly: boolPtr(false),
44-
Write: boolPtr(false),
45-
Crumbsless: boolPtr(false),
46-
Splashless: boolPtr(false),
47-
ScreenDumpDir: strPtr(AppDumpsDir),
40+
RefreshRate: float32Ptr(DefaultRefreshRate),
41+
LogLevel: strPtr(DefaultLogLevel),
42+
LogFile: strPtr(AppLogFile),
43+
Headless: boolPtr(false),
44+
Logoless: boolPtr(false),
45+
Command: strPtr(DefaultCommand),
46+
AllNamespaces: boolPtr(false),
47+
ReadOnly: boolPtr(false),
48+
Write: boolPtr(false),
49+
Crumbsless: boolPtr(false),
50+
Splashless: boolPtr(false),
51+
ScreenDumpDir: strPtr(AppDumpsDir),
52+
SuggestionMode: strPtr(string(DefaultSuggestionMode)),
4853
}
4954
}
5055

internal/config/k9s.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type K9s struct {
5050
Logger Logger `json:"logger" yaml:"logger"`
5151
Thresholds Threshold `json:"thresholds" yaml:"thresholds"`
5252
DefaultView string `json:"defaultView" yaml:"defaultView"`
53+
SuggestionMode string `json:"suggestionMode" yaml:"suggestionMode,omitempty"`
5354
manualRefreshRate float32
5455
manualReadOnly *bool
5556
manualCommand *string
@@ -77,6 +78,7 @@ func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s {
7778
PortForwardAddress: defaultPFAddress(),
7879
ShellPod: NewShellPod(),
7980
ImageScans: NewImageScans(),
81+
SuggestionMode: string(DefaultSuggestionMode),
8082
dir: data.NewDir(AppContextsDir),
8183
conn: conn,
8284
ks: ks,
@@ -152,6 +154,7 @@ func (k *K9s) Merge(k1 *K9s) {
152154
if k1.Thresholds != nil {
153155
k.Thresholds = k1.Thresholds
154156
}
157+
k.SuggestionMode = k1.SuggestionMode
155158
}
156159

157160
// AppScreenDumpDir fetch screen dumps dir.
@@ -332,6 +335,9 @@ func (k *K9s) Override(k9sFlags *Flags) {
332335
}
333336
k.manualCommand = k9sFlags.Command
334337
k.manualScreenDumpDir = k9sFlags.ScreenDumpDir
338+
if k9sFlags.SuggestionMode != nil {
339+
k.SuggestionMode = *k9sFlags.SuggestionMode
340+
}
335341
}
336342

337343
// IsHeadless returns headless setting.

internal/config/testdata/configs/default.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ k9s:
4646
critical: 90
4747
warn: 70
4848
defaultView: ""
49+
suggestionMode: PREFIX

internal/config/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ const (
1414
MEM = "memory"
1515
)
1616

17+
type SuggestionMode string
18+
19+
const (
20+
SuggestionModePrefix SuggestionMode = "PREFIX"
21+
SuggestionModeLongestPrefix SuggestionMode = "LONGEST_PREFIX"
22+
SuggestionModeLongestSubstring SuggestionMode = "LONGEST_SUBSTRING"
23+
)
24+
1725
// UI tracks ui specific configs.
1826
type UI struct {
1927
// EnableMouse toggles mouse support.

internal/ui/prompt.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package ui
55

66
import (
77
"fmt"
8+
"strings"
89
"sync"
910

1011
"github.com/derailed/k9s/internal/config"
@@ -177,7 +178,14 @@ func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey {
177178

178179
case tcell.KeyTab, tcell.KeyRight, tcell.KeyCtrlF:
179180
if s, ok := m.CurrentSuggestion(); ok {
180-
p.model.SetText(p.model.GetText()+s, "")
181+
// Substitute the last word of the text with the suggestion if text isn't empty.
182+
// Otherwise, just write the suggestion.
183+
if words := strings.Fields(strings.TrimSpace(p.model.GetText())); len(words) != 0 {
184+
text := strings.Join(words[0:len(words)-1], " ") + " " + s
185+
p.model.SetText(strings.TrimSpace(text), "")
186+
} else {
187+
p.model.SetText(s, "")
188+
}
181189
m.ClearSuggestions()
182190
}
183191
}
@@ -231,11 +239,43 @@ func (p *Prompt) write(text, suggest string) {
231239
defer p.mx.Unlock()
232240

233241
p.SetCursorIndex(p.spacer + len(text))
242+
243+
written := text
244+
234245
if suggest != "" {
235-
text += fmt.Sprintf("[%s::-]%s", p.styles.Prompt().SuggestColor, suggest)
246+
words := strings.Fields(strings.TrimSpace(text))
247+
248+
if len(words) != 0 {
249+
// Append all words of text except the last one.
250+
written = strings.Join(words[0:len(words)-1], " ") + " "
251+
lastWord := words[len(words)-1]
252+
253+
// Get indices of where the last word of text lie within the suggestion.
254+
// Could be anywhere if LONGEST_SUBSTRING mode suggestion is used.
255+
// Otherwise at 0 for PREFIX/LONGEST_PREFIX suggestion modes.
256+
startIdx := strings.Index(suggest, lastWord)
257+
endIdx := startIdx + len(lastWord)
258+
259+
if startIdx != -1 {
260+
written += fmt.Sprintf("[%s::-]%s[%s::-]%s[%s::-]%s",
261+
p.styles.Prompt().SuggestColor, suggest[0:startIdx],
262+
p.styles.Prompt().FgColor, suggest[startIdx:endIdx],
263+
p.styles.Prompt().SuggestColor, suggest[endIdx:],
264+
)
265+
} else {
266+
// `lastWord` isn't found within suggest. Just append `lastWord` again.
267+
written += lastWord
268+
}
269+
270+
written = strings.TrimSpace(written)
271+
} else {
272+
// No text, just write suggest in `SuggestColor`.
273+
written = fmt.Sprintf("[%s::-]%s", p.styles.Prompt().SuggestColor, suggest)
274+
}
236275
}
276+
237277
p.StylesChanged(p.styles)
238-
_, _ = fmt.Fprintf(p, defaultPrompt, p.icon, p.prefix, text)
278+
_, _ = fmt.Fprintf(p, defaultPrompt, p.icon, p.prefix, written)
239279
}
240280

241281
// ----------------------------------------------------------------------------

internal/view/app.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,22 +198,38 @@ func (a *App) suggestCommand() model.SuggestionFunc {
198198
return a.cmdHistory.List()
199199
}
200200

201+
entriesMap := make(map[string]float64)
202+
201203
ls := strings.ToLower(s)
202204
for alias := range maps.Keys(a.command.alias.Alias) {
203-
if suggest, ok := cmd.ShouldAddSuggest(ls, alias); ok {
204-
entries = append(entries, suggest)
205+
if weight := cmd.ShouldAddSuggest(a.Config.K9s.SuggestionMode, ls, alias); weight != -1 {
206+
entriesMap[alias] = weight
205207
}
206208
}
207209

208210
namespaceNames, err := a.factory.Client().ValidNamespaceNames()
209211
if err != nil {
210212
slog.Error("Failed to obtain list of namespaces", slogs.Error, err)
211213
}
212-
entries = append(entries, cmd.SuggestSubCommand(s, namespaceNames, contextNames)...)
213-
if len(entries) == 0 {
214+
maps.Copy(entriesMap, cmd.SuggestSubCommand(a.Config.K9s.SuggestionMode, s, namespaceNames, contextNames))
215+
if len(entriesMap) == 0 {
214216
return nil
215217
}
216-
entries.Sort()
218+
219+
entries = make([]string, 0, len(entriesMap))
220+
for k := range entriesMap {
221+
entries = append(entries, k)
222+
}
223+
224+
// PREFIX suggestion mode:, all weights are set to 0, sort strings normally.
225+
// LONGEST_PREFIX,LONGEST_SUBSTRING suggestion modes: sort based on weights and fallback to string sorting for equal weights.
226+
sort.SliceStable(entries, func(i, j int) bool {
227+
if entriesMap[entries[i]] == entriesMap[entries[j]] {
228+
return entries[i] < entries[j]
229+
}
230+
return entriesMap[entries[i]] > entriesMap[entries[j]]
231+
})
232+
217233
return
218234
}
219235
}

internal/view/cmd/helpers.go

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package cmd
55

66
import (
7-
"slices"
87
"strings"
98

109
"github.com/derailed/k9s/internal/client"
@@ -30,18 +29,32 @@ func ToLabels(s string) map[string]string {
3029
}
3130

3231
// ShouldAddSuggest checks if a suggestion match the given command.
33-
func ShouldAddSuggest(command, suggest string) (string, bool) {
34-
if command != suggest && strings.HasPrefix(suggest, command) {
35-
return strings.TrimPrefix(suggest, command), true
32+
func ShouldAddSuggest(suggestionMode, command, suggest string) float64 {
33+
var searchCondition bool
34+
var weight float64 = 0
35+
36+
switch suggestionMode {
37+
case "LONGEST_SUBSTRING":
38+
searchCondition = strings.Contains(suggest, command)
39+
weight = float64(len(command)) / float64(len(suggest))
40+
case "LONGEST_PREFIX":
41+
searchCondition = strings.HasPrefix(suggest, command)
42+
weight = float64(len(command)) / float64(len(suggest))
43+
default:
44+
searchCondition = strings.HasPrefix(suggest, command)
45+
}
46+
47+
if command != suggest && searchCondition {
48+
return weight
3649
}
3750

38-
return "", false
51+
return -1
3952
}
4053

4154
// SuggestSubCommand suggests namespaces or contexts based on current command.
42-
func SuggestSubCommand(command string, namespaces client.NamespaceNames, contexts []string) []string {
55+
func SuggestSubCommand(suggestionMode, command string, namespaces client.NamespaceNames, contexts []string) map[string]float64 {
4356
p := NewInterpreter(command)
44-
var suggests []string
57+
var suggests map[string]float64
4558
switch {
4659
case p.IsCowCmd(), p.IsHelpCmd(), p.IsAliasCmd(), p.IsBailCmd(), p.IsDirCmd():
4760
return nil
@@ -51,18 +64,18 @@ func SuggestSubCommand(command string, namespaces client.NamespaceNames, context
5164
if !ok || ns == "" {
5265
return nil
5366
}
54-
suggests = completeNS(ns, namespaces)
67+
suggests = completeNS(suggestionMode, ns, namespaces)
5568

5669
case p.IsContextCmd():
5770
n, ok := p.ContextArg()
5871
if !ok {
5972
return nil
6073
}
61-
suggests = completeCtx(command, n, contexts)
74+
suggests = completeCtx(suggestionMode, command, n, contexts)
6275

6376
case p.HasNS():
6477
if n, ok := p.HasContext(); ok {
65-
suggests = completeCtx(command, n, contexts)
78+
suggests = completeCtx(suggestionMode, command, n, contexts)
6679
}
6780
if len(suggests) > 0 {
6881
break
@@ -72,44 +85,51 @@ func SuggestSubCommand(command string, namespaces client.NamespaceNames, context
7285
if !ok {
7386
return nil
7487
}
75-
suggests = completeNS(ns, namespaces)
88+
suggests = completeNS(suggestionMode, ns, namespaces)
7689

7790
default:
7891
if n, ok := p.HasContext(); ok {
79-
suggests = completeCtx(command, n, contexts)
92+
suggests = completeCtx(suggestionMode, command, n, contexts)
8093
}
8194
}
82-
slices.Sort(suggests)
8395

8496
return suggests
8597
}
8698

87-
func completeNS(s string, nn client.NamespaceNames) []string {
99+
func completeNS(suggestionMode, s string, nn client.NamespaceNames) map[string]float64 {
88100
s = strings.ToLower(s)
89-
var suggests []string
90-
if suggest, ok := ShouldAddSuggest(s, client.NamespaceAll); ok {
91-
suggests = append(suggests, suggest)
101+
suggests := make(map[string]float64)
102+
if weight := ShouldAddSuggest(suggestionMode, s, client.NamespaceAll); weight != -1 {
103+
suggests[client.NamespaceAll] = weight
92104
}
93105
for ns := range nn {
94-
if suggest, ok := ShouldAddSuggest(s, ns); ok {
95-
suggests = append(suggests, suggest)
106+
if weight := ShouldAddSuggest(suggestionMode, s, ns); weight != -1 {
107+
suggests[ns] = weight
96108
}
97109
}
98110

111+
if len(suggests) == 0 {
112+
return nil
113+
}
114+
99115
return suggests
100116
}
101117

102-
func completeCtx(command, s string, contexts []string) []string {
103-
var suggests []string
118+
func completeCtx(suggestionMode, command, s string, contexts []string) map[string]float64 {
119+
suggests := make(map[string]float64)
104120
for _, ctxName := range contexts {
105-
if suggest, ok := ShouldAddSuggest(s, ctxName); ok {
121+
if weight := ShouldAddSuggest(suggestionMode, s, ctxName); weight != -1 {
106122
if s == "" && !strings.HasSuffix(command, " ") {
107-
suggests = append(suggests, " "+suggest)
123+
suggests[" "+ctxName] = weight
108124
continue
109125
}
110-
suggests = append(suggests, suggest)
126+
suggests[ctxName] = weight
111127
}
112128
}
113129

130+
if len(suggests) == 0 {
131+
return nil
132+
}
133+
114134
return suggests
115135
}

0 commit comments

Comments
 (0)