mirror of
https://github.com/junegunn/fzf
synced 2026-08-10 20:01:42 +00:00
dab626bd9e
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
271 lines
6.4 KiB
Go
271 lines
6.4 KiB
Go
//go:build !windows
|
|
|
|
package tui
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/junegunn/fzf/src/util"
|
|
"golang.org/x/sys/unix"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
func IsLightRendererSupported() bool {
|
|
return true
|
|
}
|
|
|
|
func (r *LightRenderer) DefaultTheme() *ColorTheme {
|
|
if strings.Contains(os.Getenv("TERM"), "256") {
|
|
return Dark256
|
|
}
|
|
colors, err := exec.Command("tput", "colors").Output()
|
|
if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 {
|
|
return Dark256
|
|
}
|
|
return Default16
|
|
}
|
|
|
|
func (r *LightRenderer) fd() int {
|
|
return int(r.ttyin.Fd())
|
|
}
|
|
|
|
func (r *LightRenderer) initPlatform() (err error) {
|
|
r.origState, err = term.MakeRaw(r.fd())
|
|
return err
|
|
}
|
|
|
|
func (r *LightRenderer) closePlatform() {
|
|
r.ttyout.Close()
|
|
}
|
|
|
|
func openTty(ttyDefault string, mode int) (*os.File, error) {
|
|
var in *os.File
|
|
var err error
|
|
if len(ttyDefault) > 0 {
|
|
in, err = os.OpenFile(ttyDefault, mode, 0)
|
|
}
|
|
if in == nil || err != nil || ttyDefault != DefaultTtyDevice && !util.IsTty(in) {
|
|
tty := ttyname()
|
|
if len(tty) > 0 {
|
|
if in, err := os.OpenFile(tty, mode, 0); err == nil {
|
|
return in, nil
|
|
}
|
|
}
|
|
if ttyDefault != DefaultTtyDevice {
|
|
if in, err = os.OpenFile(DefaultTtyDevice, mode, 0); err == nil {
|
|
return in, nil
|
|
}
|
|
}
|
|
return nil, errors.New("failed to open " + DefaultTtyDevice)
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
func openTtyIn(ttyDefault string) (*os.File, error) {
|
|
return openTty(ttyDefault, syscall.O_RDONLY)
|
|
}
|
|
|
|
func openTtyOut(ttyDefault string) (*os.File, error) {
|
|
return openTty(ttyDefault, syscall.O_WRONLY)
|
|
}
|
|
|
|
func (r *LightRenderer) setupTerminal() {
|
|
term.MakeRaw(r.fd())
|
|
}
|
|
|
|
func (r *LightRenderer) restoreTerminal() {
|
|
term.Restore(r.fd(), r.origState)
|
|
}
|
|
|
|
func (r *LightRenderer) updateTerminalSize() {
|
|
width, height, err := term.GetSize(r.fd())
|
|
|
|
if err == nil {
|
|
r.width = width
|
|
r.height = r.maxHeightFunc(height)
|
|
} else {
|
|
r.width = getEnv("COLUMNS", defaultWidth)
|
|
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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) {
|
|
fd := r.fd()
|
|
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 {
|
|
// 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 {
|
|
ws, err := unix.IoctlGetWinsize(int(r.ttyin.Fd()), unix.TIOCGWINSZ)
|
|
if err != nil {
|
|
return TermSize{}
|
|
}
|
|
return TermSize{int(ws.Row), int(ws.Col), int(ws.Xpixel), int(ws.Ypixel)}
|
|
}
|