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 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 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. 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. e.g.
\fB# Prepend the current cursor position in yellow \fB# Prepend the current cursor position in yellow

View File

@ -15,6 +15,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@ -58,6 +59,10 @@ const clearCode string = "\x1b[2J"
// Number of maximum focus events to process synchronously // Number of maximum focus events to process synchronously
const maxFocusEvents = 10000 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() { func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`) placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`) whiteSuffix = regexp.MustCompile(`\s*$`)
@ -1792,20 +1797,8 @@ func (t *Terminal) printInfo() {
t.window.Print(strings.Repeat(" ", fillLength+1)) t.window.Print(strings.Repeat(" ", fillLength+1))
} }
} }
switch t.infoStyle {
case infoDefault: if t.infoStyle == infoHidden {
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.separatorLen > 0 { if t.separatorLen > 0 {
move(line+1, 0, false) move(line+1, 0, false)
printSeparator(t.window.Width()-1, false) printSeparator(t.window.Width()-1, false)
@ -1849,6 +1842,21 @@ func (t *Terminal) printInfo() {
outputPrinter, outputLen = t.ansiLabelPrinter(output, &tui.ColInfo, false) 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 { if t.infoStyle == infoRight {
maxWidth := t.window.Width() maxWidth := t.window.Width()
if t.reading { if t.reading {
@ -3055,6 +3063,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Run() cmd.Run()
t.tui.Resume(true, false) t.tui.Resume(true, false)
t.mutex.Lock() t.mutex.Lock()
// NOTE: Using t.reqBox.Set(reqFullRedraw...) instead can cause a deadlock
t.fullRedraw() t.fullRedraw()
t.flush() t.flush()
} else { } else {
@ -3062,6 +3071,18 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
if len(info) == 0 { if len(info) == 0 {
t.uiMutex.Lock() 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 { if capture {
out, _ := cmd.StdoutPipe() out, _ := cmd.StdoutPipe()
reader := bufio.NewReader(out) reader := bufio.NewReader(out)
@ -3077,7 +3098,20 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
} else { } else {
cmd.Run() cmd.Run()
} }
cancel()
if paused.CompareAndSwap(1, 2) {
t.tui.Resume(false, false)
}
t.mutex.Lock() 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 { if len(info) == 0 {
t.uiMutex.Unlock() t.uiMutex.Unlock()
@ -3300,11 +3334,11 @@ func (t *Terminal) Loop() error {
t.termSize = t.tui.Size() t.termSize = t.tui.Size()
t.resizeWindows(false) t.resizeWindows(false)
t.window.Erase() t.window.Erase()
t.printPrompt()
t.printInfo()
t.printHeader()
t.flush()
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqPrompt, nil)
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqHeader, nil)
if t.initDelay > 0 { if t.initDelay > 0 {
go func() { go func() {
timer := time.NewTimer(t.initDelay) timer := time.NewTimer(t.initDelay)
@ -3530,6 +3564,14 @@ func (t *Terminal) Loop() error {
t.reqBox.Wait(func(events *util.Events) { t.reqBox.Wait(func(events *util.Events) {
defer events.Clear() 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 // 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 // will 1. unlock t.mutex to allow GET endpoint and 2. lock t.uiMutex
// to block rendering during the execution. // to block rendering during the execution.
@ -3547,33 +3589,36 @@ func (t *Terminal) Loop() error {
// U t.uiMutex | // U t.uiMutex |
t.uiMutex.Lock() t.uiMutex.Lock()
t.mutex.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 { switch req {
case reqPrompt: case reqPrompt:
t.printPrompt() t.printPrompt()
if t.infoStyle == infoInline || t.infoStyle == infoInlineRight { if t.infoStyle == infoInline || t.infoStyle == infoInlineRight {
t.printInfo() printInfo()
} }
case reqInfo: case reqInfo:
t.printInfo() printInfo()
case reqList: case reqList:
t.printList() t.printList()
currentIndex := t.currentIndex() currentIndex := t.currentIndex()
focusChanged := focusedIndex != currentIndex focusChanged := focusedIndex != currentIndex
printInfo := false info := false
if focusChanged && t.track == trackCurrent { if focusChanged && t.track == trackCurrent {
t.track = trackDisabled t.track = trackDisabled
printInfo = true info = true
} }
if (t.hasFocusActions || t.infoCommand != "") && focusChanged && currentIndex != t.lastFocus { if (t.hasFocusActions || t.infoCommand != "") && focusChanged && currentIndex != t.lastFocus {
t.lastFocus = currentIndex t.lastFocus = currentIndex
t.eventChan <- tui.Focus.AsEvent() t.eventChan <- tui.Focus.AsEvent()
if t.infoCommand != "" { if t.infoCommand != "" {
printInfo = true info = true
} }
} }
if printInfo { if info {
t.printInfo() printInfo()
} }
if focusChanged || version != t.version { if focusChanged || version != t.version {
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) 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 // Once returns a function that returns the specified boolean value only once
func Once(nextResponse bool) func() bool { func Once(nextResponse bool) func() bool {
state := nextResponse state := nextResponse
return func() bool { return func() bool {
prevState := state prevState := state
state = false state = !nextResponse
return prevState return prevState
} }
} }

View File

@ -137,8 +137,11 @@ func TestOnce(t *testing.T) {
if o() { if o() {
t.Error("Expected: false") t.Error("Expected: false")
} }
if o() { if !o() {
t.Error("Expected: false") t.Error("Expected: true")
}
if !o() {
t.Error("Expected: true")
} }
o = Once(true) o = Once(true)
@ -148,6 +151,9 @@ func TestOnce(t *testing.T) {
if o() { if o() {
t.Error("Expected: false") t.Error("Expected: false")
} }
if o() {
t.Error("Expected: false")
}
} }
func TestRunesWidth(t *testing.T) { func TestRunesWidth(t *testing.T) {