From df5ad6bc369b31dad9bf575f4ec690796b9a5458 Mon Sep 17 00:00:00 2001 From: Graham Clark Date: Sat, 4 Jul 2020 14:00:16 -0400 Subject: [PATCH] Prevent a race allowing the submission of an invalid filter 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. --- widgets/filter/filter.go | 58 +++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/widgets/filter/filter.go b/widgets/filter/filter.go index d367e74..1f49adc 100644 --- a/widgets/filter/filter.go +++ b/widgets/filter/filter.go @@ -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 } @@ -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{})) @@ -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, }) @@ -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, @@ -166,7 +173,7 @@ func New(opt Options) *Widget { runthisfilterchan: runthisfilterchan, quitchan: quitchan, readytorunchan: readytorunchan, - temporarilyDisabled: &temporarilyDisabled, + temporarilyDisabled: new(bool), Callbacks: cb, } @@ -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 + } + })) }, } @@ -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 })) }, } @@ -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 })) }, } @@ -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 @@ -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.