summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-06-20 23:18:28 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2024-06-22 17:24:47 +0900
commit7c2ffd3fef3f9131ee448a5f40d91835c8bd814d (patch)
tree0cf1df14f976fde619d5084310fa972f07eec21d /src
parentdb01e7dab65423cd1d14e15f5b15dfaabe760283 (diff)
downloadfzf-7c2ffd3fef3f9131ee448a5f40d91835c8bd814d.tar.gz
Make transform*, --info-command, and execute-silent cancellable
Users can press CTRL-C after 1 second to terminate the command. Close #3883
Diffstat (limited to 'src')
-rw-r--r--src/terminal.go97
-rw-r--r--src/util/util.go12
-rw-r--r--src/util/util_test.go10
3 files changed, 90 insertions, 29 deletions
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) {