diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index eb9882e2..136aa1c1 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -457,6 +457,8 @@ Determines the display style of the finder info. (e.g. match counter, loading in Command to generate the finder info line. The command runs synchronously and blocks the UI until completion, so make sure that it's fast. ANSI color codes are supported. \fB$FZF_INFO\fR variable is set to the original info text. +For additional environment variables available to the command, see the section +ENVIRONMENT VARIABLES EXPORTED TO CHILD PROCESSES. e.g. \fB# Prepend the current cursor position in yellow diff --git a/src/terminal.go b/src/terminal.go index f131f1db..a9e2f48e 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -15,6 +15,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "syscall" "time" @@ -58,6 +59,10 @@ const clearCode string = "\x1b[2J" // Number of maximum focus events to process synchronously const maxFocusEvents = 10000 +// execute-silent and transform* actions will block user input for this duration. +// After this duration, users can press CTRL-C to terminate the command. +const blockDuration = 1 * time.Second + func init() { placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`) whiteSuffix = regexp.MustCompile(`\s*$`) @@ -1792,20 +1797,8 @@ func (t *Terminal) printInfo() { t.window.Print(strings.Repeat(" ", fillLength+1)) } } - switch t.infoStyle { - case infoDefault: - move(line+1, 0, t.separatorLen == 0) - printSpinner() - t.window.Print(" ") // Margin - pos = 2 - case infoRight: - move(line+1, 0, false) - case infoInlineRight: - pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 - case infoInline: - pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 - printInfoPrefix() - case infoHidden: + + if t.infoStyle == infoHidden { if t.separatorLen > 0 { move(line+1, 0, false) printSeparator(t.window.Width()-1, false) @@ -1849,6 +1842,21 @@ func (t *Terminal) printInfo() { outputPrinter, outputLen = t.ansiLabelPrinter(output, &tui.ColInfo, false) } + switch t.infoStyle { + case infoDefault: + move(line+1, 0, t.separatorLen == 0) + printSpinner() + t.window.Print(" ") // Margin + pos = 2 + case infoRight: + move(line+1, 0, false) + case infoInlineRight: + pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 + case infoInline: + pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 + printInfoPrefix() + } + if t.infoStyle == infoRight { maxWidth := t.window.Width() if t.reading { @@ -3055,6 +3063,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo cmd.Run() t.tui.Resume(true, false) t.mutex.Lock() + // NOTE: Using t.reqBox.Set(reqFullRedraw...) instead can cause a deadlock t.fullRedraw() t.flush() } else { @@ -3062,6 +3071,18 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo if len(info) == 0 { t.uiMutex.Lock() } + paused := atomic.Int32{} + ctx, cancel := context.WithCancel(context.Background()) + go func() { + select { + case <-ctx.Done(): + return + case <-time.After(blockDuration): + if paused.CompareAndSwap(0, 1) { + t.tui.Pause(false) + } + } + }() if capture { out, _ := cmd.StdoutPipe() reader := bufio.NewReader(out) @@ -3077,7 +3098,20 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo } else { cmd.Run() } + cancel() + if paused.CompareAndSwap(1, 2) { + t.tui.Resume(false, false) + } t.mutex.Lock() + + // Redraw prompt in case the user has typed something after blockDuration + if paused.Load() > 0 { + // NOTE: Using t.reqBox.Set(reqXXX...) instead can cause a deadlock + t.printPrompt() + if t.infoStyle == infoInline || t.infoStyle == infoInlineRight { + t.printInfo() + } + } } if len(info) == 0 { t.uiMutex.Unlock() @@ -3300,11 +3334,11 @@ func (t *Terminal) Loop() error { t.termSize = t.tui.Size() t.resizeWindows(false) t.window.Erase() - t.printPrompt() - t.printInfo() - t.printHeader() - t.flush() t.mutex.Unlock() + + t.reqBox.Set(reqPrompt, nil) + t.reqBox.Set(reqInfo, nil) + t.reqBox.Set(reqHeader, nil) if t.initDelay > 0 { go func() { timer := time.NewTimer(t.initDelay) @@ -3530,6 +3564,14 @@ func (t *Terminal) Loop() error { t.reqBox.Wait(func(events *util.Events) { defer events.Clear() + // Sort events. + // e.g. Make sure that reqPrompt is processed before reqInfo + keys := make([]int, 0, len(*events)) + for key := range *events { + keys = append(keys, int(key)) + } + sort.Ints(keys) + // 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. @@ -3547,33 +3589,36 @@ func (t *Terminal) Loop() error { // U t.uiMutex | t.uiMutex.Lock() t.mutex.Lock() - for req, value := range *events { + printInfo := util.RunOnce(t.printInfo) + for _, key := range keys { + req := util.EventType(key) + value := (*events)[req] switch req { case reqPrompt: t.printPrompt() if t.infoStyle == infoInline || t.infoStyle == infoInlineRight { - t.printInfo() + printInfo() } case reqInfo: - t.printInfo() + printInfo() case reqList: t.printList() currentIndex := t.currentIndex() focusChanged := focusedIndex != currentIndex - printInfo := false + info := false if focusChanged && t.track == trackCurrent { t.track = trackDisabled - printInfo = true + info = true } if (t.hasFocusActions || t.infoCommand != "") && focusChanged && currentIndex != t.lastFocus { t.lastFocus = currentIndex t.eventChan <- tui.Focus.AsEvent() if t.infoCommand != "" { - printInfo = true + info = true } } - if printInfo { - t.printInfo() + if info { + printInfo() } if focusChanged || version != t.version { version = t.version diff --git a/src/util/util.go b/src/util/util.go index ec5a1ea0..c8301363 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -144,12 +144,22 @@ func IsTty(file *os.File) bool { return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) } +// RunOnce runs the given function only once +func RunOnce(f func()) func() { + once := Once(true) + return func() { + if once() { + f() + } + } +} + // Once returns a function that returns the specified boolean value only once func Once(nextResponse bool) func() bool { state := nextResponse return func() bool { prevState := state - state = false + state = !nextResponse return prevState } } diff --git a/src/util/util_test.go b/src/util/util_test.go index 013f3c23..36d71bde 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -137,8 +137,11 @@ func TestOnce(t *testing.T) { if o() { t.Error("Expected: false") } - if o() { - t.Error("Expected: false") + if !o() { + t.Error("Expected: true") + } + if !o() { + t.Error("Expected: true") } o = Once(true) @@ -148,6 +151,9 @@ func TestOnce(t *testing.T) { if o() { t.Error("Expected: false") } + if o() { + t.Error("Expected: false") + } } func TestRunesWidth(t *testing.T) {