diff --git a/src/terminal.go b/src/terminal.go index be97a686..1ef745f9 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -5615,7 +5615,7 @@ func (t *Terminal) Loop() error { select { case <-ctx.Done(): 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: event = tui.Invalid.AsEvent() actions = append(actions, &action{t: actAsync}) diff --git a/src/tui/dummy.go b/src/tui/dummy.go index cb0ce0da..32afb04e 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -34,11 +34,11 @@ func (r *FullscreenRenderer) ShowCursor() {} func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Size() TermSize { return TermSize{} } - -func (r *FullscreenRenderer) GetChar() Event { return Event{} } -func (r *FullscreenRenderer) Top() int { return 0 } -func (r *FullscreenRenderer) MaxX() int { return 0 } -func (r *FullscreenRenderer) MaxY() int { return 0 } +func (r *FullscreenRenderer) Top() int { return 0 } +func (r *FullscreenRenderer) MaxX() int { return 0 } +func (r *FullscreenRenderer) MaxY() int { return 0 } +func (r *FullscreenRenderer) GetChar(bool) Event { return Event{} } +func (r *FullscreenRenderer) CancelGetChar() {} func (r *FullscreenRenderer) RefreshWindows(windows []Window) {} diff --git a/src/tui/light.go b/src/tui/light.go index 133759fb..ed537322 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -26,6 +26,7 @@ const ( escPollInterval = 5 offsetPollTries = 10 maxInputBuffer = 1024 * 1024 + maxSelectTries = 100 ) const DefaultTtyDevice string = "/dev/tty" @@ -49,6 +50,18 @@ const DIM string = "\x1b[2m" const CR 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) { bytes := []byte(str) runes := []rune{} @@ -104,6 +117,7 @@ type LightRenderer struct { clicks [][2]int ttyin *os.File ttyout *os.File + cancel func() buffer []byte origState *term.State width int @@ -118,9 +132,9 @@ type LightRenderer struct { x int maxHeightFunc func(int) int showCursor bool + mutex sync.Mutex // Windows only - mutex sync.Mutex ttyinChannel chan byte inHandle uintptr outHandle uintptr @@ -262,16 +276,18 @@ func getEnv(name string, defaultValue int) int { return atoi(env, defaultValue) } -func (r *LightRenderer) getBytes() ([]byte, error) { - bytes, err := r.getBytesInternal(r.buffer, false) - return bytes, err +func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) { + return r.getBytesInternal(cancellable, r.buffer, false) } -func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) { - c, ok := r.getch(nonblock) - if !nonblock && !ok { +func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblock bool) ([]byte, getCharResult, error) { + c, result := r.getch(cancellable, nonblock) + if result == getCharCancelled { + return buffer, getCharCancelled, nil + } + if !nonblock && !result.ok() { r.Close() - return nil, errors.New("failed to read " + DefaultTtyDevice) + return nil, getCharError, errors.New("failed to read " + DefaultTtyDevice) } retries := 0 @@ -282,8 +298,8 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, pc := c for { - c, ok = r.getch(true) - if !ok { + c, result = r.getch(false, true) + if !result.ok() { if retries > 0 { retries-- time.Sleep(escPollInterval * time.Millisecond) @@ -302,20 +318,24 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, // so terminate fzf immediately. if len(buffer) > maxInputBuffer { 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 result getCharResult if len(r.buffer) == 0 { - r.buffer, err = r.getBytes() + r.buffer, result, err = r.getBytes(cancellable) if err != nil { return Event{Fatal, 0, nil} } + if result == getCharCancelled { + return Event{Invalid, 0, nil} + } } if len(r.buffer) == 0 { return Event{Fatal, 0, nil} @@ -351,9 +371,14 @@ func (r *LightRenderer) GetChar() Event { ev := r.escSequence(&sz) // Second chance 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} } + if result == getCharCancelled { + return Event{Invalid, 0, nil} + } + ev = r.escSequence(&sz) } return ev @@ -371,6 +396,21 @@ func (r *LightRenderer) GetChar() Event { 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 { if len(r.buffer) < 2 { return Event{Esc, 0, nil} diff --git a/src/tui/light_test.go b/src/tui/light_test.go index 717a01d0..72e1a5c5 100644 --- a/src/tui/light_test.go +++ b/src/tui/light_test.go @@ -15,10 +15,27 @@ func TestLightRenderer(t *testing.T) { 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) { bytes := []byte(sequence) light_renderer.buffer = bytes - event := light_renderer.GetChar() + event := light_renderer.GetChar(true) if event.KeyName() != name { t.Errorf( "sequence: %q | %v | '%s' (%s) != %s", diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go index 8cec30c6..f38aa64e 100644 --- a/src/tui/light_unix.go +++ b/src/tui/light_unix.go @@ -99,7 +99,7 @@ func (r *LightRenderer) findOffset() (row int, col int) { var err error bytes := []byte{} for tries := range offsetPollTries { - bytes, err = r.getBytesInternal(bytes, tries > 0) + bytes, _, err = r.getBytesInternal(false, bytes, tries > 0) if err != nil { return -1, -1 } @@ -114,15 +114,62 @@ func (r *LightRenderer) findOffset() (row int, col int) { return -1, -1 } -func (r *LightRenderer) getch(nonblock bool) (int, bool) { - b := make([]byte, 1) +func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) { fd := r.fd() - util.SetNonblock(r.ttyin, nonblock) - _, err := util.Read(fd, b) - if err != nil { - return 0, false + getter := func() (int, getCharResult) { + b := make([]byte, 1) + util.SetNonblock(r.ttyin, nonblock) + _, 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 { diff --git a/src/tui/light_windows.go b/src/tui/light_windows.go index fd5cc142..d1779795 100644 --- a/src/tui/light_windows.go +++ b/src/tui/light_windows.go @@ -151,16 +151,33 @@ func (r *LightRenderer) findOffset() (row int, col int) { return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X) } -func (r *LightRenderer) getch(nonblock bool) (int, bool) { - if nonblock { - select { - case bc := <-r.ttyinChannel: - return int(bc), true - case <-time.After(timeoutInterval * time.Millisecond): - return 0, false - } - } else { +func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) { + if !nonblock && !cancellable { 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 } } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index b027cad9..1ea4c8f5 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -246,7 +246,7 @@ func (r *FullscreenRenderer) Size() TermSize { return TermSize{lines, cols, 0, 0} } -func (r *FullscreenRenderer) GetChar() Event { +func (r *FullscreenRenderer) GetChar(cancellable bool) Event { ev := _screen.PollEvent() switch ev := ev.(type) { case *tcell.EventPaste: @@ -703,6 +703,10 @@ func (r *FullscreenRenderer) GetChar() Event { return Event{Invalid, 0, nil} } +func (r *FullscreenRenderer) CancelGetChar() { + // TODO +} + func (r *FullscreenRenderer) Pause(clear bool) { if clear { _screen.Suspend() diff --git a/src/tui/tui.go b/src/tui/tui.go index c29c4181..5eb8356b 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -749,7 +749,8 @@ type Renderer interface { HideCursor() ShowCursor() - GetChar() Event + GetChar(cancellable bool) Event + CancelGetChar() Top() int MaxX() int