Skip to content

Commit

Permalink
Prevent a race allowing the submission of an invalid filter
Browse files Browse the repository at this point in the history
Prior to this change, it's possible to type an invalid filter expression
and submit it - using enter - in the brief interval before termshark
determines it's invalid and sets the filter field red.

The fix is to delay the submission of a filter on enter until the filter
expression is determined valid. If the filter changes in the meantime,
then the enter is forgotten. If enter is still pending when the
expression becomes green, then it's submitted.
  • Loading branch information
gcla committed Jul 4, 2020
1 parent 0865bc4 commit df5ad6b
Showing 1 changed file with 48 additions and 10 deletions.
58 changes: 48 additions & 10 deletions widgets/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type Widget struct {
quitchan chan struct{}
readytorunchan chan struct{}
temporarilyDisabled *bool // set to true right after submitting a new filter, so the menu disappears
enterPending bool // set to true if the user has hit enter; process if the filter goes to valid before another change. For slow validity processing.
*gowid.Callbacks
gowid.IsSelectable
}
Expand All @@ -86,7 +87,14 @@ type Options struct {
}

func New(opt Options) *Widget {
res := &Widget{}

ed := edit.New()
ed.OnTextSet(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) {
// every time the filter changes, drop any pending enter - we don't want to
// apply a filter to a stale value
res.enterPending = false
}})

fixed := gowid.RenderFixed{}
filterList := list.New(list.NewSimpleListWalker([]gowid.IWidget{}))
Expand Down Expand Up @@ -121,8 +129,7 @@ func New(opt Options) *Widget {

cb := gowid.NewCallbacks()

temporarilyDisabled := false
onelineEd := appkeys.New(ed, handleEnter(cb, &temporarilyDisabled), appkeys.Options{
onelineEd := appkeys.New(ed, handleEnter(cb, res), appkeys.Options{
ApplyBefore: true,
})

Expand All @@ -148,7 +155,7 @@ func New(opt Options) *Widget {
readytorunchan := make(chan struct{})
filterchangedchan := make(chan *filtStruct)

res := &Widget{
*res = Widget{
wrapped: wrapped,
opts: opt,
ed: ed,
Expand All @@ -166,7 +173,7 @@ func New(opt Options) *Widget {
runthisfilterchan: runthisfilterchan,
quitchan: quitchan,
readytorunchan: readytorunchan,
temporarilyDisabled: &temporarilyDisabled,
temporarilyDisabled: new(bool),
Callbacks: cb,
}

Expand All @@ -175,6 +182,14 @@ func New(opt Options) *Widget {
app.Run(gowid.RunFunction(func(app gowid.IApp) {
res.validitySite.SetSubWidget(res.valid, app)
gowid.RunWidgetCallbacks(res.Callbacks, ValidCB{}, app, res)

if res.enterPending {
var dummy gowid.IWidget
gowid.RunWidgetCallbacks(cb, SubmitCB{}, app, dummy)
*res.temporarilyDisabled = true
res.enterPending = false
}

}))
},
}
Expand All @@ -184,6 +199,7 @@ func New(opt Options) *Widget {
app.Run(gowid.RunFunction(func(app gowid.IApp) {
res.validitySite.SetSubWidget(res.invalid, app)
gowid.RunWidgetCallbacks(res.Callbacks, InvalidCB{}, app, res)
res.enterPending = false
}))
},
}
Expand All @@ -193,6 +209,7 @@ func New(opt Options) *Widget {
app.Run(gowid.RunFunction(func(app gowid.IApp) {
res.validitySite.SetSubWidget(res.intermediate, app)
gowid.RunWidgetCallbacks(res.Callbacks, IntermediateCB{}, app, res)
res.enterPending = false
}))
},
}
Expand Down Expand Up @@ -337,18 +354,26 @@ func (f *Validator) Validate(filter string) {
}
}

type iFilterEnter interface {
setDisabled()
setEnterPending()
isValid() bool
}

// if the filter is valid when enter is pressed, submit the SubmitCB callback. Those
// registered will be able to respond e.g. start handling the valid filter value.
func handleEnter(cb *gowid.Callbacks, temporarilyDisabled *bool) appkeys.KeyInputFn {
func handleEnter(cb *gowid.Callbacks, fe iFilterEnter) appkeys.KeyInputFn {
return func(evk *tcell.EventKey, app gowid.IApp) bool {
handled := false
switch evk.Key() {
case tcell.KeyEnter:

var dummy gowid.IWidget
gowid.RunWidgetCallbacks(cb, SubmitCB{}, app, dummy)
*temporarilyDisabled = true

if fe.isValid() {
var dummy gowid.IWidget
gowid.RunWidgetCallbacks(cb, SubmitCB{}, app, dummy)
fe.setDisabled()
} else {
fe.setEnterPending() // remember in case the filter goes valid shortly
}
handled = true
}
return handled
Expand Down Expand Up @@ -451,6 +476,19 @@ func makeCompletions(comp termshark.IPrefixCompleter, txt string, max int, app g
comp.Completions(txt, cb)
}

func (w *Widget) setDisabled() {
*w.temporarilyDisabled = true
}

func (w *Widget) setEnterPending() {
w.enterPending = true
}

// isCurrentlyValid returns true if the current state of the filter is valid (green)
func (w *Widget) isValid() bool {
return w.validitySite.SubWidget() == w.valid
}

// Start an asynchronous routine to update the drop-down menu with completion
// options. Runs on a small delay so it can be cancelled and restarted if the
// user is typing quickly.
Expand Down

0 comments on commit df5ad6b

Please sign in to comment.