Skip to content

Commit ab86c88

Browse files
committed
Implemented fuzzy search and made exact search optional
1 parent 863e3f0 commit ab86c88

File tree

6 files changed

+117
-37
lines changed

6 files changed

+117
-37
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,9 @@ Account aliases for AWS accounts. Key is the account ID, and value is the accoun
170170
Default value is `["Instance Type", "Private IP", "Public IP", "Name"]`.
171171

172172
Specify "Instance Type", "Private IP", or "Public IP" to display when selecting an instance. All other values are extracted from tags.
173+
174+
### `filter_strategy`
175+
176+
Default value is `"fuzzy"`.
177+
178+
You can specify `"fuzzy"` for fuzzy search. All other values will be treated as `"exact"`.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ require (
2727
github.com/hashicorp/hcl v1.0.0 // indirect
2828
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2929
github.com/jmespath/go-jmespath v0.4.0 // indirect
30+
github.com/lithammer/fuzzysearch v1.1.8 // indirect
3031
github.com/magiconair/properties v1.8.7 // indirect
3132
github.com/mattn/go-runewidth v0.0.15 // indirect
3233
github.com/mitchellh/mapstructure v1.5.0 // indirect

go.sum

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
6868
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
6969
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
7070
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
71+
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
72+
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
7173
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
7274
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
7375
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -131,28 +133,56 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
131133
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
132134
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
133135
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
136+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
134137
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
135138
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
136139
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
137140
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
141+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
142+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
138143
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
139144
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
145+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
146+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
147+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
148+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
149+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
150+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
151+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
152+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
153+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
154+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
140155
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
141156
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
142157
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
143158
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
144159
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
145160
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
161+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
162+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
146163
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
164+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
147165
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
148166
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
167+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
149168
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
150169
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
151170
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
171+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
152172
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
153173
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
174+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
175+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
176+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
177+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
178+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
154179
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
155180
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
181+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
182+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
183+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
184+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
185+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
156186
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
157187
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
158188
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,14 @@ func setupConfigFile() {
175175
viper.AddConfigPath("$HOME/.aws/knox")
176176
viper.SetDefault("default_connect_uid", uint32(0))
177177
viper.SetDefault("select_cached_first", false)
178+
viper.SetDefault("filter_strategy", "fuzzy")
178179
viper.SetDefault("max_items_to_show", 10)
179180
viper.SetDefault("account_aliases", map[string]string{})
180181
viper.SetDefault("instance_col_tags", []string{"Instance Type", "Private IP", "Public IP", "Name"})
181182
viper.SafeWriteConfig()
182183
viper.ReadInConfig()
183184
tui.MaxItemsToShow = viper.GetInt("max_items_to_show")
185+
tui.FilterStrategy = viper.GetString("filter_strategy")
184186
selectCachedFirst = viper.GetBool("select_cached_first")
185187
connectUid = viper.GetUint32("default_connect_uid")
186188
accountAliases = viper.GetStringMapString("account_aliases")

sdk/picker/picker.go

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
package picker
22

33
import (
4+
"sort"
45
"strings"
56

67
"atomicgo.dev/keyboard"
78
"atomicgo.dev/keyboard/keys"
9+
"github.com/lithammer/fuzzysearch/fuzzy"
810
"github.com/null93/aws-knox/pkg/ansi"
911
"github.com/null93/aws-knox/pkg/color"
1012
. "github.com/null93/aws-knox/sdk/style"
1113
)
1214

1315
type picker struct {
14-
actions []action
15-
options []option
16-
filtered []*option
17-
selectedIndex int
18-
term string
19-
title string
20-
longestCols []int
21-
emptyMessage string
22-
maxHeight int
23-
windowStart int
24-
windowEnd int
25-
headers []string
16+
actions []action
17+
options []option
18+
filtered []*option
19+
selectedIndex int
20+
term string
21+
filterStrategy string
22+
title string
23+
longestCols []int
24+
emptyMessage string
25+
maxHeight int
26+
windowStart int
27+
windowEnd int
28+
headers []string
2629
}
2730

2831
type option struct {
@@ -38,22 +41,27 @@ type action struct {
3841

3942
func NewPicker() *picker {
4043
p := picker{
41-
actions: []action{},
42-
options: []option{},
43-
filtered: []*option{},
44-
selectedIndex: 0,
45-
title: "Please Pick One",
46-
term: "",
47-
longestCols: []int{},
48-
emptyMessage: "Nothing Found",
49-
maxHeight: 5,
50-
windowStart: 0,
51-
windowEnd: 5,
52-
headers: []string{},
44+
actions: []action{},
45+
options: []option{},
46+
filtered: []*option{},
47+
selectedIndex: 0,
48+
title: "Please Pick One",
49+
term: "",
50+
filterStrategy: "fuzzy",
51+
longestCols: []int{},
52+
emptyMessage: "Nothing Found",
53+
maxHeight: 5,
54+
windowStart: 0,
55+
windowEnd: 5,
56+
headers: []string{},
5357
}
5458
return &p
5559
}
5660

61+
func (p *picker) WithFilterStrategy(strategy string) {
62+
p.filterStrategy = strategy
63+
}
64+
5765
func (p *picker) WithMaxHeight(maxHeight int) {
5866
p.maxHeight = maxHeight
5967
p.windowStart = 0
@@ -108,18 +116,41 @@ func (p *picker) filter() {
108116
p.selectedIndex = 0
109117
p.windowStart = 0
110118
p.windowEnd = p.maxHeight
111-
for i, option := range p.options {
112-
if p.term == "" {
113-
p.filtered = append(p.filtered, &p.options[i])
114-
continue
119+
120+
if p.filterStrategy == "fuzzy" {
121+
optionsMap := map[string]*option{}
122+
fullValues := []string{}
123+
for i, option := range p.options {
124+
if p.term == "" {
125+
p.filtered = append(p.filtered, &p.options[i])
126+
continue
127+
}
128+
fullValue := strings.Join(option.Columns, " ")
129+
fullValues = append(fullValues, fullValue)
130+
optionsMap[fullValue] = &p.options[i]
115131
}
116-
for _, col := range option.Columns {
117-
if strings.Contains(strings.ToLower(col), strings.ToLower(p.term)) {
132+
if p.term != "" {
133+
ranks := fuzzy.RankFindFold(p.term, fullValues)
134+
sort.Sort(ranks)
135+
for _, rank := range ranks {
136+
p.filtered = append(p.filtered, optionsMap[rank.Target])
137+
}
138+
}
139+
} else {
140+
for i, option := range p.options {
141+
if p.term == "" {
118142
p.filtered = append(p.filtered, &p.options[i])
119-
break
143+
continue
144+
}
145+
for _, col := range option.Columns {
146+
if strings.Contains(strings.ToLower(col), strings.ToLower(p.term)) {
147+
p.filtered = append(p.filtered, &p.options[i])
148+
break
149+
}
120150
}
121151
}
122152
}
153+
123154
if len(p.filtered) < 1 {
124155
p.selectedIndex = -1
125156
}
@@ -225,6 +256,9 @@ func (p *picker) Pick(initialFilter string) (*option, *keys.KeyCode) {
225256
}
226257
}
227258
if key.Code == keys.RuneKey || key.Code == keys.Space {
259+
if p.term == "" && key.Code == keys.Space {
260+
return false, nil
261+
}
228262
p.term += string(key.Runes)
229263
p.filter()
230264
p.render()

sdk/tui/tui.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import (
1515
)
1616

1717
var (
18-
MaxItemsToShow int = 10
19-
ErrNotPickedSession = fmt.Errorf("no sso session picked")
20-
ErrNotPickedAccount = fmt.Errorf("no account picked")
21-
ErrNotPickedRole = fmt.Errorf("no role picked")
22-
ErrNotPickedInstance = fmt.Errorf("no instance picked")
23-
ErrNotPickedRoleCredentials = fmt.Errorf("no role credentials picked")
18+
MaxItemsToShow int = 10
19+
FilterStrategy string = "fuzzy"
20+
ErrNotPickedSession error = fmt.Errorf("no sso session picked")
21+
ErrNotPickedAccount error = fmt.Errorf("no account picked")
22+
ErrNotPickedRole error = fmt.Errorf("no role picked")
23+
ErrNotPickedInstance error = fmt.Errorf("no instance picked")
24+
ErrNotPickedRoleCredentials error = fmt.Errorf("no role credentials picked")
2425
)
2526

2627
func ClientLogin(session *credentials.Session) error {
@@ -73,6 +74,7 @@ func SelectSession(sessions credentials.Sessions) (string, string, error) {
7374
now := time.Now()
7475
p := picker.NewPicker()
7576
p.WithMaxHeight(MaxItemsToShow)
77+
p.WithFilterStrategy(FilterStrategy)
7678
p.WithEmptyMessage("No SSO Sessions Found")
7779
p.WithTitle("Pick SSO Session")
7880
p.WithHeaders("SSO Session", "Region", "SSO Start URL", "Expires In")
@@ -101,6 +103,7 @@ func SelectAccount(session *credentials.Session, accountAliases map[string]strin
101103
}
102104
p := picker.NewPicker()
103105
p.WithMaxHeight(MaxItemsToShow)
106+
p.WithFilterStrategy(FilterStrategy)
104107
p.WithEmptyMessage("No Accounts Found")
105108
p.WithTitle("Pick Account")
106109
p.WithHeaders("Account ID", "Alias/Name", "Email")
@@ -128,6 +131,7 @@ func SelectRole(roles credentials.Roles) (string, string, error) {
128131
now := time.Now()
129132
p := picker.NewPicker()
130133
p.WithMaxHeight(MaxItemsToShow)
134+
p.WithFilterStrategy(FilterStrategy)
131135
p.WithEmptyMessage("No Roles Found")
132136
p.WithTitle("Pick Role")
133137
p.WithHeaders("Role Name", "Expires In")
@@ -175,6 +179,7 @@ func SelectInstance(role *credentials.Role, region, initialFilter string, instan
175179
}
176180
p := picker.NewPicker()
177181
p.WithMaxHeight(MaxItemsToShow)
182+
p.WithFilterStrategy(FilterStrategy)
178183
p.WithEmptyMessage("No Instances Found")
179184
p.WithTitle(fmt.Sprintf("Pick EC2 Instance (%s)", region))
180185
p.WithHeaders(cols...)
@@ -242,6 +247,7 @@ func SelectRegion(initialFilter string) (string, string, error) {
242247
}
243248
p := picker.NewPicker()
244249
p.WithMaxHeight(MaxItemsToShow)
250+
p.WithFilterStrategy(FilterStrategy)
245251
p.WithEmptyMessage("No Regions Found")
246252
p.WithTitle("Pick Region")
247253
p.WithHeaders("Region", "Name")
@@ -267,6 +273,7 @@ func SelectRolesCredentials(accountAliases map[string]string) (*credentials.Role
267273
}
268274
p := picker.NewPicker()
269275
p.WithMaxHeight(MaxItemsToShow)
276+
p.WithFilterStrategy(FilterStrategy)
270277
p.WithEmptyMessage("No Role Credentials Found")
271278
p.WithTitle("Pick Role Credentials")
272279
p.WithHeaders("SSO Session", "Region", "Account ID", "Alias", "Role Name", "Expires In")

0 commit comments

Comments
 (0)