Cancel key reading when 'execute' triggered via a server request (#4653)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled

Fix #4524
Close #4648
This commit is contained in:
Junegunn Choi
2026-01-09 00:29:40 +09:00
committed by GitHub
parent 3c7cbc9d47
commit 3f94bcb5bf
8 changed files with 175 additions and 42 deletions

View File

@@ -5615,7 +5615,7 @@ func (t *Terminal) Loop() error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case t.keyChan <- t.tui.GetChar(): case t.keyChan <- t.tui.GetChar(t.listenAddr != nil):
} }
} }
}() }()
@@ -5702,6 +5702,13 @@ func (t *Terminal) Loop() error {
} }
} }
} }
for _, action := range actions {
if action.t == actExecute {
t.tui.CancelGetChar()
break
}
}
case callback := <-t.callbackChan: case callback := <-t.callbackChan:
event = tui.Invalid.AsEvent() event = tui.Invalid.AsEvent()
actions = append(actions, &action{t: actAsync}) actions = append(actions, &action{t: actAsync})

View File

@@ -34,11 +34,11 @@ func (r *FullscreenRenderer) ShowCursor() {}
func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} } func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
func (r *FullscreenRenderer) Top() int { return 0 }
func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) Top() int { return 0 } func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) GetChar(bool) Event { return Event{} }
func (r *FullscreenRenderer) MaxY() int { return 0 } func (r *FullscreenRenderer) CancelGetChar() {}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {} func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}

View File

@@ -26,6 +26,7 @@ const (
escPollInterval = 5 escPollInterval = 5
offsetPollTries = 10 offsetPollTries = 10
maxInputBuffer = 1024 * 1024 maxInputBuffer = 1024 * 1024
maxSelectTries = 100
) )
const DefaultTtyDevice string = "/dev/tty" const DefaultTtyDevice string = "/dev/tty"
@@ -49,6 +50,18 @@ const DIM string = "\x1b[2m"
const CR string = DIM + "␍" const CR string = DIM + "␍"
const LF string = DIM + "␊" const LF string = DIM + "␊"
type getCharResult int
const (
getCharSuccess getCharResult = iota
getCharError
getCharCancelled
)
func (r getCharResult) ok() bool {
return r == getCharSuccess
}
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) { func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str) bytes := []byte(str)
runes := []rune{} runes := []rune{}
@@ -104,6 +117,7 @@ type LightRenderer struct {
clicks [][2]int clicks [][2]int
ttyin *os.File ttyin *os.File
ttyout *os.File ttyout *os.File
cancel func()
buffer []byte buffer []byte
origState *term.State origState *term.State
width int width int
@@ -118,9 +132,9 @@ type LightRenderer struct {
x int x int
maxHeightFunc func(int) int maxHeightFunc func(int) int
showCursor bool showCursor bool
mutex sync.Mutex
// Windows only // Windows only
mutex sync.Mutex
ttyinChannel chan byte ttyinChannel chan byte
inHandle uintptr inHandle uintptr
outHandle uintptr outHandle uintptr
@@ -262,16 +276,18 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue) return atoi(env, defaultValue)
} }
func (r *LightRenderer) getBytes() ([]byte, error) { func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) {
bytes, err := r.getBytesInternal(r.buffer, false) return r.getBytesInternal(cancellable, r.buffer, false)
return bytes, err
} }
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) { func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblock bool) ([]byte, getCharResult, error) {
c, ok := r.getch(nonblock) c, result := r.getch(cancellable, nonblock)
if !nonblock && !ok { if result == getCharCancelled {
return buffer, getCharCancelled, nil
}
if !nonblock && !result.ok() {
r.Close() r.Close()
return nil, errors.New("failed to read " + DefaultTtyDevice) return nil, getCharError, errors.New("failed to read " + DefaultTtyDevice)
} }
retries := 0 retries := 0
@@ -282,8 +298,8 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
pc := c pc := c
for { for {
c, ok = r.getch(true) c, result = r.getch(false, true)
if !ok { if !result.ok() {
if retries > 0 { if retries > 0 {
retries-- retries--
time.Sleep(escPollInterval * time.Millisecond) time.Sleep(escPollInterval * time.Millisecond)
@@ -302,20 +318,24 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
// so terminate fzf immediately. // so terminate fzf immediately.
if len(buffer) > maxInputBuffer { if len(buffer) > maxInputBuffer {
r.Close() r.Close()
return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer) return nil, getCharError, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
} }
} }
return buffer, nil return buffer, getCharSuccess, nil
} }
func (r *LightRenderer) GetChar() Event { func (r *LightRenderer) GetChar(cancellable bool) Event {
var err error var err error
var result getCharResult
if len(r.buffer) == 0 { if len(r.buffer) == 0 {
r.buffer, err = r.getBytes() r.buffer, result, err = r.getBytes(cancellable)
if err != nil { if err != nil {
return Event{Fatal, 0, nil} return Event{Fatal, 0, nil}
} }
if result == getCharCancelled {
return Event{Invalid, 0, nil}
}
} }
if len(r.buffer) == 0 { if len(r.buffer) == 0 {
return Event{Fatal, 0, nil} return Event{Fatal, 0, nil}
@@ -351,9 +371,14 @@ func (r *LightRenderer) GetChar() Event {
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
if ev.Type == Invalid { if ev.Type == Invalid {
if r.buffer, err = r.getBytes(); err != nil { r.buffer, result, err = r.getBytes(true)
if err != nil {
return Event{Fatal, 0, nil} return Event{Fatal, 0, nil}
} }
if result == getCharCancelled {
return Event{Invalid, 0, nil}
}
ev = r.escSequence(&sz) ev = r.escSequence(&sz)
} }
return ev return ev
@@ -371,6 +396,21 @@ func (r *LightRenderer) GetChar() Event {
return Event{Rune, char, nil} return Event{Rune, char, nil}
} }
func (r *LightRenderer) CancelGetChar() {
r.mutex.Lock()
if r.cancel != nil {
r.cancel()
r.cancel = nil
}
r.mutex.Unlock()
}
func (r *LightRenderer) setCancel(f func()) {
r.mutex.Lock()
r.cancel = f
r.mutex.Unlock()
}
func (r *LightRenderer) escSequence(sz *int) Event { func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 { if len(r.buffer) < 2 {
return Event{Esc, 0, nil} return Event{Esc, 0, nil}

View File

@@ -15,10 +15,27 @@ func TestLightRenderer(t *testing.T) {
light_renderer := renderer.(*LightRenderer) light_renderer := renderer.(*LightRenderer)
go func() {
for {
light_renderer.mutex.Lock()
ready := light_renderer.cancel != nil
light_renderer.mutex.Unlock()
if ready {
light_renderer.CancelGetChar()
break
}
}
}()
event := light_renderer.GetChar(true)
if event.Type != Invalid {
t.Error("Not cancelled")
}
assertCharSequence := func(sequence string, name string) { assertCharSequence := func(sequence string, name string) {
bytes := []byte(sequence) bytes := []byte(sequence)
light_renderer.buffer = bytes light_renderer.buffer = bytes
event := light_renderer.GetChar() event := light_renderer.GetChar(true)
if event.KeyName() != name { if event.KeyName() != name {
t.Errorf( t.Errorf(
"sequence: %q | %v | '%s' (%s) != %s", "sequence: %q | %v | '%s' (%s) != %s",

View File

@@ -99,7 +99,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
var err error var err error
bytes := []byte{} bytes := []byte{}
for tries := range offsetPollTries { for tries := range offsetPollTries {
bytes, err = r.getBytesInternal(bytes, tries > 0) bytes, _, err = r.getBytesInternal(false, bytes, tries > 0)
if err != nil { if err != nil {
return -1, -1 return -1, -1
} }
@@ -114,15 +114,62 @@ func (r *LightRenderer) findOffset() (row int, col int) {
return -1, -1 return -1, -1
} }
func (r *LightRenderer) getch(nonblock bool) (int, bool) { func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
b := make([]byte, 1)
fd := r.fd() fd := r.fd()
util.SetNonblock(r.ttyin, nonblock) getter := func() (int, getCharResult) {
_, err := util.Read(fd, b) b := make([]byte, 1)
if err != nil { util.SetNonblock(r.ttyin, nonblock)
return 0, false _, err := util.Read(fd, b)
if err != nil {
return 0, getCharError
}
return int(b[0]), getCharSuccess
} }
return int(b[0]), true if nonblock || !cancellable {
return getter()
}
rpipe, wpipe, err := os.Pipe()
if err != nil {
// Fallback to blocking read without cancellation
return getter()
}
r.setCancel(func() {
wpipe.Write([]byte{0})
})
defer func() {
r.setCancel(nil)
rpipe.Close()
wpipe.Close()
}()
cancelFd := int(rpipe.Fd())
for range maxSelectTries {
var rfds unix.FdSet
limit := len(rfds.Bits) * unix.NFDBITS
if fd >= limit || cancelFd >= limit {
return getter()
}
rfds.Set(fd)
rfds.Set(cancelFd)
_, err := unix.Select(max(fd, cancelFd)+1, &rfds, nil, nil, nil)
if err != nil {
if err == syscall.EINTR {
continue
}
return 0, getCharError
}
if rfds.IsSet(cancelFd) {
return 0, getCharCancelled
}
if rfds.IsSet(fd) {
return getter()
}
}
return 0, getCharError
} }
func (r *LightRenderer) Size() TermSize { func (r *LightRenderer) Size() TermSize {

View File

@@ -151,16 +151,33 @@ func (r *LightRenderer) findOffset() (row int, col int) {
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X) return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
} }
func (r *LightRenderer) getch(nonblock bool) (int, bool) { func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
if nonblock { if !nonblock && !cancellable {
select {
case bc := <-r.ttyinChannel:
return int(bc), true
case <-time.After(timeoutInterval * time.Millisecond):
return 0, false
}
} else {
bc := <-r.ttyinChannel bc := <-r.ttyinChannel
return int(bc), true return int(bc), getCharSuccess
}
var timeout <-chan time.Time
if nonblock {
timeout = time.After(timeoutInterval * time.Millisecond)
}
var cancel chan struct{}
if cancellable {
cancel = make(chan struct{})
r.setCancel(func() {
close(cancel)
})
defer r.setCancel(nil)
}
select {
case bc := <-r.ttyinChannel:
return int(bc), getCharSuccess
case <-cancel:
return 0, getCharCancelled
case <-timeout:
// NOTE: not really an error
return 0, getCharError
} }
} }

View File

@@ -246,7 +246,7 @@ func (r *FullscreenRenderer) Size() TermSize {
return TermSize{lines, cols, 0, 0} return TermSize{lines, cols, 0, 0}
} }
func (r *FullscreenRenderer) GetChar() Event { func (r *FullscreenRenderer) GetChar(cancellable bool) Event {
ev := _screen.PollEvent() ev := _screen.PollEvent()
switch ev := ev.(type) { switch ev := ev.(type) {
case *tcell.EventPaste: case *tcell.EventPaste:
@@ -703,6 +703,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
func (r *FullscreenRenderer) CancelGetChar() {
// TODO
}
func (r *FullscreenRenderer) Pause(clear bool) { func (r *FullscreenRenderer) Pause(clear bool) {
if clear { if clear {
_screen.Suspend() _screen.Suspend()

View File

@@ -749,7 +749,8 @@ type Renderer interface {
HideCursor() HideCursor()
ShowCursor() ShowCursor()
GetChar() Event GetChar(cancellable bool) Event
CancelGetChar()
Top() int Top() int
MaxX() int MaxX() int