Make transform*, --info-command, and execute-silent cancellable

Users can press CTRL-C after 1 second to terminate the command.

Close #3883
This commit is contained in:
Junegunn Choi 2024-06-20 23:18:28 +09:00
parent db01e7dab6
commit 7c2ffd3fef
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
4 changed files with 92 additions and 29 deletions

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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) {