@ 261102.1

260109.1 · 3f94bcb5bf93b0c04a7fc4087dff8f99e84e4a9b

Cancel key reading when 'execute' triggered via a server request (#4653)

Fix #4524 Close #4648

reading declarations

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

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

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}

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",

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)+	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+	}+	if nonblock || !cancellable {+		return getter()+	}++	rpipe, wpipe, err := os.Pipe() 	if err != nil {-		return 0, false+		// 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 int(b[0]), true+	return 0, getCharError }  func (r *LightRenderer) Size() TermSize {

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

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()

src/tui/tui.go

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