mirror of
https://github.com/junegunn/fzf.git
synced 2026-10-13 08:29:04 +08:00
Restore bracketed paste mode instead of forcing it off
Ask the terminal whether the mode is already on (DECRQM) and put it back that way on exit. Forcing it off broke pasting in shells that run fzf from a line editor widget, which enable the mode only when the editor starts. Terminals that do not answer fall back to disabling it. - Startup queries go out in one write, cursor position last. Every terminal answers DSR, so its reply bounds the wait: a paste reply still missing by then means the terminal does not know the query. - Bound the first read with select(2). Terminals that never answer escape sequences, such as FreeBSD virtual terminals, blocked startup until a key was pressed, and that keystroke was then discarded. Fix #4887 Fix #2860 Fix #976
This commit is contained in:
1 parent
2885df8395
commit
dab626bd9e
4 files changed
+168
-19
No files matched your search
+103
-14
@@ -8,6 +8,7 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -93,25 +94,113 @@ func (r *LightRenderer) updateTerminalSize() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
r.csi("6n")
|
||||
r.flush()
|
||||
var err error
|
||||
bytes := []byte{}
|
||||
for tries := range offsetPollTries {
|
||||
bytes, _, err = r.getBytesInternal(false, bytes, tries > 0)
|
||||
// waitReadable reports whether the tty has something to read before the
|
||||
// deadline. A terminal that does not recognize a query answers nothing at all,
|
||||
// so the read that follows must be able to stop waiting, or fzf would wait for
|
||||
// the user to press a key instead of drawing itself. The timeout is generous
|
||||
// because a terminal that does answer exceeds it only when the link is slow
|
||||
// enough to be unusable anyway.
|
||||
func (r *LightRenderer) waitReadable(timeout time.Duration) bool {
|
||||
fd := r.fd()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
return false
|
||||
}
|
||||
var rfds unix.FdSet
|
||||
if fd >= len(rfds.Bits)*unix.NFDBITS {
|
||||
return false
|
||||
}
|
||||
rfds.Set(fd)
|
||||
// Recomputed each time round: Linux select rewrites the timeout with
|
||||
// the time left, other systems leave it alone
|
||||
tv := unix.NsecToTimeval(int64(remaining))
|
||||
n, err := unix.Select(fd+1, &rfds, nil, nil, &tv)
|
||||
if err == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return -1, -1
|
||||
// Nothing was confirmed readable, and the read that follows
|
||||
// reports the failure if the fd is really broken
|
||||
return false
|
||||
}
|
||||
return n > 0
|
||||
}
|
||||
}
|
||||
|
||||
// queryTerminal sends every query in a single write and reads until the last
|
||||
// one is answered. Returns the submatches of each reply, nil for a query the
|
||||
// terminal ignored. Replies are cut out of what we read as they are recognized,
|
||||
// so whatever is left over is input the user typed during the round trip.
|
||||
func (r *LightRenderer) queryTerminal(queries []termQuery) [][][]byte {
|
||||
for _, query := range queries {
|
||||
r.csi(query.seq)
|
||||
}
|
||||
r.flush()
|
||||
|
||||
replies := make([][][]byte, len(queries))
|
||||
buffer := []byte{}
|
||||
for tries := range offsetPollTries {
|
||||
// Only the first read blocks, so that is the one to put a bound on
|
||||
if tries == 0 && !r.waitReadable(queryTimeout) {
|
||||
return replies
|
||||
}
|
||||
|
||||
offsets := offsetRegexp.FindSubmatch(bytes)
|
||||
if len(offsets) > 3 {
|
||||
// Add anything we skipped over to the input buffer
|
||||
r.buffer = append(r.buffer, offsets[1]...)
|
||||
return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1
|
||||
var err error
|
||||
buffer, _, err = r.getBytesInternal(false, buffer, tries > 0)
|
||||
if err != nil {
|
||||
return replies
|
||||
}
|
||||
|
||||
for idx, query := range queries {
|
||||
if replies[idx] != nil {
|
||||
continue
|
||||
}
|
||||
loc := query.reply.FindSubmatchIndex(buffer)
|
||||
if loc == nil {
|
||||
continue
|
||||
}
|
||||
groups := make([][]byte, len(loc)/2)
|
||||
for group := range groups {
|
||||
if loc[group*2] >= 0 {
|
||||
groups[group] = buffer[loc[group*2]:loc[group*2+1]]
|
||||
}
|
||||
}
|
||||
replies[idx] = groups
|
||||
// Capping the prefix makes append copy, leaving groups valid
|
||||
buffer = append(buffer[:loc[0]:loc[0]], buffer[loc[1]:]...)
|
||||
}
|
||||
|
||||
if replies[len(queries)-1] != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
|
||||
r.buffer = append(r.buffer, buffer...)
|
||||
return replies
|
||||
}
|
||||
|
||||
func (r *LightRenderer) queryStartup() (row int, col int, pasteWasSet *bool) {
|
||||
replies := r.queryTerminal(startupQueries)
|
||||
if paste := replies[0]; paste != nil && paste[1][0] != '0' {
|
||||
// 1 = set, 3 = permanently set
|
||||
set := paste[1][0] == '1' || paste[1][0] == '3'
|
||||
pasteWasSet = &set
|
||||
}
|
||||
row, col = parseOffset(replies[1])
|
||||
return
|
||||
}
|
||||
|
||||
func parseOffset(reply [][]byte) (row int, col int) {
|
||||
if reply == nil {
|
||||
return -1, -1
|
||||
}
|
||||
return atoi(string(reply[1]), 0) - 1, atoi(string(reply[2]), 0) - 1
|
||||
}
|
||||
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
return parseOffset(r.queryTerminal(startupQueries[1:])[0])
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
|
||||
|
||||
Reference in new issue
Block a user