Improved --sync behavior

When --sync is provided, fzf will not render the interface until the
initial filtering and associated actions (bound to any of 'start',
'load', or 'result') are complete.
This commit is contained in:
Junegunn Choi 2024-06-17 00:07:27 +09:00
parent b8c01af0fc
commit e0ddb97ab4
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
4 changed files with 130 additions and 75 deletions

View File

@ -3,16 +3,20 @@ CHANGELOG
0.53.1 0.53.1
------ ------
- Bug fixes and minor improvements - Better cache management and improved rendering for `--tail`
- Better cache management and improved rendering for `--tail` - Improved `--sync` behavior
- Fixed crash when using `--tiebreak=end` with very long items - When `--sync` is provided, fzf will not render the interface until the initial filtering and associated actions (bound to any of `start`, `load`, or `result`) are complete.
- Fixed mouse support on Windows
- zsh 5.0 compatibility (thanks to @LangLangBart)
- Fixed `--walker-skip` to also skip symlinks to directories
- GET endpoint is now available from `execute` and `transform` actions (it used to timeout due to lock conflict)
```sh ```sh
fzf --listen --bind 'focus:transform-header:curl -s localhost:$FZF_PORT?limit=0 | jq .' (sleep 1; seq 1000000; sleep 1) | fzf --sync --query 5 --listen --bind start:up,load:up,result:up
``` ```
- GET endpoint is now available from `execute` and `transform` actions (it used to timeout due to lock conflict)
```sh
fzf --listen --bind 'focus:transform-header:curl -s localhost:$FZF_PORT?limit=0 | jq .'
```
- Fixed crash when using `--tiebreak=end` with very long items
- Fixed mouse support on Windows
- zsh 5.0 compatibility (thanks to @LangLangBart)
- Fixed `--walker-skip` to also skip symlinks to directories
0.53.0 0.53.0
------ ------

View File

@ -339,9 +339,6 @@ func Run(opts *Options) (int, error) {
} }
total = count total = count
terminal.UpdateCount(total, !reading, value.(*string)) terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
}
if heightUnknown && !deferred { if heightUnknown && !deferred {
determine(!reading) determine(!reading)
} }
@ -429,7 +426,7 @@ func Run(opts *Options) (int, error) {
determine(val.final) determine(val.final)
} }
} }
terminal.UpdateList(val, true) terminal.UpdateList(val)
} }
} }
} }

View File

@ -286,6 +286,7 @@ type Terminal struct {
borderWidth int borderWidth int
count int count int
progress int progress int
hasStartActions bool
hasResultActions bool hasResultActions bool
hasFocusActions bool hasFocusActions bool
hasLoadActions bool hasLoadActions bool
@ -311,6 +312,7 @@ type Terminal struct {
previewBox *util.EventBox previewBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
uiMutex sync.Mutex
initFunc func() error initFunc func() error
prevLines []itemLine prevLines []itemLine
suppress bool suppress bool
@ -318,6 +320,7 @@ type Terminal struct {
startChan chan fitpad startChan chan fitpad
killChan chan bool killChan chan bool
serverInputChan chan []*action serverInputChan chan []*action
keyChan chan tui.Event
eventChan chan tui.Event eventChan chan tui.Event
slab *util.Slab slab *util.Slab
theme *tui.ColorTheme theme *tui.ColorTheme
@ -361,7 +364,7 @@ const (
reqHeader reqHeader
reqList reqList
reqJump reqJump
reqRefresh reqActivate
reqReinit reqReinit
reqFullRedraw reqFullRedraw
reqResize reqResize
@ -684,7 +687,9 @@ func evaluateHeight(opts *Options, termHeight int) int {
func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) (*Terminal, error) { func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) (*Terminal, error) {
input := trimQuery(opts.Query) input := trimQuery(opts.Query)
var delay time.Duration var delay time.Duration
if opts.Tac { if opts.Sync {
delay = 0
} else if opts.Tac {
delay = initialDelayTac delay = initialDelayTac
} else { } else {
delay = initialDelay delay = initialDelay
@ -808,6 +813,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
ellipsis: opts.Ellipsis, ellipsis: opts.Ellipsis,
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
hasStartActions: false,
hasResultActions: false, hasResultActions: false,
hasFocusActions: false, hasFocusActions: false,
hasLoadActions: false, hasLoadActions: false,
@ -830,6 +836,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
previewBox: previewBox, previewBox: previewBox,
eventBox: eventBox, eventBox: eventBox,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
uiMutex: sync.Mutex{},
suppress: true, suppress: true,
sigstop: false, sigstop: false,
slab: util.MakeSlab(slab16Size, slab32Size), slab: util.MakeSlab(slab16Size, slab32Size),
@ -837,7 +844,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
startChan: make(chan fitpad, 1), startChan: make(chan fitpad, 1),
killChan: make(chan bool), killChan: make(chan bool),
serverInputChan: make(chan []*action, 100), serverInputChan: make(chan []*action, 100),
eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar) keyChan: make(chan tui.Event),
eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize)
tui: renderer, tui: renderer,
ttyin: ttyin, ttyin: ttyin,
initFunc: func() error { return renderer.Init() }, initFunc: func() error { return renderer.Init() },
@ -885,6 +893,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
if t.tui.ShouldEmitResizeEvent() { if t.tui.ShouldEmitResizeEvent() {
t.keymap[tui.Resize.AsEvent()] = append(toActions(actClearScreen), resizeActions...) t.keymap[tui.Resize.AsEvent()] = append(toActions(actClearScreen), resizeActions...)
} }
_, t.hasStartActions = t.keymap[tui.Start.AsEvent()]
_, t.hasResultActions = t.keymap[tui.Result.AsEvent()] _, t.hasResultActions = t.keymap[tui.Result.AsEvent()]
_, t.hasFocusActions = t.keymap[tui.Focus.AsEvent()] _, t.hasFocusActions = t.keymap[tui.Focus.AsEvent()]
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()] _, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
@ -898,9 +907,17 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
t.listenPort = &port t.listenPort = &port
} }
if t.hasStartActions {
t.eventChan <- tui.Start.AsEvent()
}
return &t, nil return &t, nil
} }
func (t *Terminal) deferActivation() bool {
return t.initDelay == 0 && (t.hasStartActions || t.hasLoadActions || t.hasResultActions)
}
func (t *Terminal) environ() []string { func (t *Terminal) environ() []string {
env := os.Environ() env := os.Environ()
if t.listenPort != nil { if t.listenPort != nil {
@ -1122,10 +1139,14 @@ func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
} }
t.reading = !final t.reading = !final
t.failed = failedCommand t.failed = failedCommand
suppressed := t.suppress
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqInfo, nil)
if final {
t.reqBox.Set(reqRefresh, nil) // We want to defer activating the interface when --sync is used and any of
// start, load, or result events are bound
if suppressed && final && !t.deferActivation() {
t.reqBox.Set(reqActivate, nil)
} }
} }
@ -1158,7 +1179,7 @@ func (t *Terminal) UpdateProgress(progress float32) {
} }
// UpdateList updates Merger to display the list // UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger, triggerResultEvent bool) { func (t *Terminal) UpdateList(merger *Merger) {
t.mutex.Lock() t.mutex.Lock()
prevIndex := minItem.Index() prevIndex := minItem.Index()
newRevision := merger.Revision() newRevision := merger.Revision()
@ -1229,7 +1250,7 @@ func (t *Terminal) UpdateList(merger *Merger, triggerResultEvent bool) {
t.eventChan <- one t.eventChan <- one
} }
} }
if triggerResultEvent && t.hasResultActions { if t.hasResultActions {
t.eventChan <- tui.Result.AsEvent() t.eventChan <- tui.Result.AsEvent()
} }
} }
@ -2645,7 +2666,7 @@ func (t *Terminal) printAll() {
t.printPreview() t.printPreview()
} }
func (t *Terminal) refresh() { func (t *Terminal) flush() {
t.placeCursor() t.placeCursor()
if !t.suppress { if !t.suppress {
windows := make([]tui.Window, 0, 4) windows := make([]tui.Window, 0, 4)
@ -2949,7 +2970,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
return replaced, tempFiles return replaced, tempFiles
} }
func (t *Terminal) redraw() { func (t *Terminal) fullRedraw() {
t.tui.Clear() t.tui.Clear()
t.tui.Refresh() t.tui.Refresh()
t.printAll() t.printAll()
@ -2992,15 +3013,18 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
} }
} }
t.tui.Pause(true)
t.mutex.Unlock() t.mutex.Unlock()
t.uiMutex.Lock()
t.tui.Pause(true)
cmd.Run() cmd.Run()
t.mutex.Lock()
t.tui.Resume(true, false) t.tui.Resume(true, false)
t.redraw() t.mutex.Lock()
t.refresh() t.fullRedraw()
t.flush()
t.uiMutex.Unlock()
} else { } else {
t.mutex.Unlock() t.mutex.Unlock()
t.uiMutex.Lock()
if capture { if capture {
out, _ := cmd.StdoutPipe() out, _ := cmd.StdoutPipe()
reader := bufio.NewReader(out) reader := bufio.NewReader(out)
@ -3017,6 +3041,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Run() cmd.Run()
} }
t.mutex.Lock() t.mutex.Lock()
t.uiMutex.Unlock()
} }
t.executing.Set(false) t.executing.Set(false)
removeFiles(tempFiles) removeFiles(tempFiles)
@ -3239,13 +3264,15 @@ func (t *Terminal) Loop() error {
t.printPrompt() t.printPrompt()
t.printInfo() t.printInfo()
t.printHeader() t.printHeader()
t.refresh() t.flush()
t.mutex.Unlock() t.mutex.Unlock()
go func() { if t.initDelay > 0 {
timer := time.NewTimer(t.initDelay) go func() {
<-timer.C timer := time.NewTimer(t.initDelay)
t.reqBox.Set(reqRefresh, nil) <-timer.C
}() t.reqBox.Set(reqActivate, nil)
}()
}
// Keep the spinner spinning // Keep the spinner spinning
go func() { go func() {
@ -3439,7 +3466,7 @@ func (t *Terminal) Loop() error {
} }
} }
go func() { go func() { // Render loop
var focusedIndex = minItem.Index() var focusedIndex = minItem.Index()
var version int64 = -1 var version int64 = -1
running := true running := true
@ -3463,6 +3490,23 @@ func (t *Terminal) Loop() error {
for running { for running {
t.reqBox.Wait(func(events *util.Events) { t.reqBox.Wait(func(events *util.Events) {
defer events.Clear() defer events.Clear()
// t.uiMutex must be locked first to avoid deadlock. Execute actions
// will 1. unlock t.mutex to allow GET endpoint and 2. lock t.uiMutex
// to block rendering during the execution.
//
// T1 T2 (good) | T1 T2 (bad)
// L t.uiMutex |
// L t.mutex | L t.mutex
// U t.mutex | U t.mutex
// L t.mutex | L t.mutex
// U t.mutex | L t.uiMutex
// U t.uiMutex | L t.uiMutex!!
// L t.uiMutex |
// | L t.mutex!!
// L t.mutex | U t.uiMutex
// U t.uiMutex |
t.uiMutex.Lock()
t.mutex.Lock() t.mutex.Lock()
for req, value := range *events { for req, value := range *events {
switch req { switch req {
@ -3497,7 +3541,7 @@ func (t *Terminal) Loop() error {
t.printList() t.printList()
case reqHeader: case reqHeader:
t.printHeader() t.printHeader()
case reqRefresh: case reqActivate:
t.suppress = false t.suppress = false
case reqRedrawBorderLabel: case reqRedrawBorderLabel:
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true) t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
@ -3505,13 +3549,13 @@ func (t *Terminal) Loop() error {
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, true) t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, true)
case reqReinit: case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop) t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw() t.fullRedraw()
case reqResize, reqFullRedraw: case reqResize, reqFullRedraw:
if req == reqResize { if req == reqResize {
t.termSize = t.tui.Size() t.termSize = t.tui.Size()
} }
wasHidden := t.pwindow == nil wasHidden := t.pwindow == nil
t.redraw() t.fullRedraw()
if wasHidden && t.hasPreviewWindow() { if wasHidden && t.hasPreviewWindow() {
refreshPreview(t.previewOpts.command) refreshPreview(t.previewOpts.command)
} }
@ -3565,8 +3609,9 @@ func (t *Terminal) Loop() error {
return return
} }
} }
t.refresh() t.flush()
t.mutex.Unlock() t.mutex.Unlock()
t.uiMutex.Unlock()
}) })
} }
@ -3577,9 +3622,6 @@ func (t *Terminal) Loop() error {
}() }()
looping := true looping := true
_, startEvent := t.keymap[tui.Start.AsEvent()]
needBarrier := true
barrier := make(chan bool) barrier := make(chan bool)
go func() { go func() {
for { for {
@ -3591,7 +3633,7 @@ func (t *Terminal) Loop() error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case t.eventChan <- t.tui.GetChar(): case t.keyChan <- t.tui.GetChar():
} }
} }
}() }()
@ -3599,42 +3641,63 @@ func (t *Terminal) Loop() error {
barDragging := false barDragging := false
pbarDragging := false pbarDragging := false
wasDown := false wasDown := false
for looping { needBarrier := true
// If an action is bound to 'start', we're going to process it before reading
// user input.
if !t.hasStartActions {
barrier <- true
needBarrier = false
}
for loopIndex := int64(0); looping; loopIndex++ {
var newCommand *commandSpec var newCommand *commandSpec
var reloadSync bool var reloadSync bool
changed := false changed := false
beof := false beof := false
queryChanged := false queryChanged := false
// Special handling of --sync. Activate the interface on the second tick.
if loopIndex == 1 && t.deferActivation() {
t.reqBox.Set(reqActivate, nil)
}
if loopIndex > 0 && needBarrier {
barrier <- true
needBarrier = false
}
var event tui.Event var event tui.Event
actions := []*action{} actions := []*action{}
if startEvent { select {
event = tui.Start.AsEvent() case event = <-t.keyChan:
startEvent = false needBarrier = true
} else { case event = <-t.eventChan:
if needBarrier { // Drain channel to process all queued events at once without rendering
barrier <- true // the intermediate states
} Drain:
select { for {
case event = <-t.eventChan: if eventActions, prs := t.keymap[event]; prs {
if t.tui.ShouldEmitResizeEvent() { actions = append(actions, eventActions...)
needBarrier = !event.Is(tui.Load, tui.Result, tui.Focus, tui.One, tui.Zero)
} else {
needBarrier = !event.Is(tui.Load, tui.Result, tui.Focus, tui.One, tui.Zero, tui.Resize)
} }
case serverActions := <-t.serverInputChan: for {
event = tui.Invalid.AsEvent() select {
if t.listenAddr == nil || t.listenAddr.IsLocal() || t.listenUnsafe { case event = <-t.eventChan:
actions = serverActions continue Drain
} else { default:
for _, action := range serverActions { break Drain
if !processExecution(action.t) { }
actions = append(actions, action) }
} }
case serverActions := <-t.serverInputChan:
event = tui.Invalid.AsEvent()
if t.listenAddr == nil || t.listenAddr.IsLocal() || t.listenUnsafe {
actions = serverActions
} else {
for _, action := range serverActions {
if !processExecution(action.t) {
actions = append(actions, action)
} }
} }
needBarrier = false
} }
} }
@ -3642,8 +3705,8 @@ func (t *Terminal) Loop() error {
for key, ret := range t.expect { for key, ret := range t.expect {
if keyMatch(key, event) { if keyMatch(key, event) {
t.pressed = ret t.pressed = ret
t.reqBox.Set(reqClose, nil)
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqClose, nil)
return nil return nil
} }
} }

View File

@ -334,15 +334,6 @@ type Event struct {
MouseEvent *MouseEvent MouseEvent *MouseEvent
} }
func (e Event) Is(types ...EventType) bool {
for _, t := range types {
if e.Type == t {
return true
}
}
return false
}
type MouseEvent struct { type MouseEvent struct {
Y int Y int
X int X int