Use winpty to launch fzf in Git bash (mintty)

Close #3806

Known limitation:
* --height cannot be used
This commit is contained in:
Junegunn Choi 2024-05-20 17:06:44 +09:00
parent aee417c46a
commit 573df524fe
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
12 changed files with 315 additions and 157 deletions

View File

@ -21,7 +21,11 @@ Matcher -> EvtHeader -> Terminal (update header)
// Run starts fzf
func Run(opts *Options) (int, error) {
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 {
return runTmux(os.Args[1:], opts)
return runTmux(os.Args, opts)
}
if os.Getenv("TERM_PROGRAM") == "mintty" && !opts.NoWinpty {
return runWinpty(os.Args, opts)
}
if err := postProcessOptions(opts); err != nil {

View File

@ -381,8 +381,9 @@ type walkerOpts struct {
type Options struct {
Input chan string
Output chan string
NoWinpty bool
Tmux *tmuxOptions
TmuxScript string
ProxyScript string
Bash bool
Zsh bool
Fish bool
@ -1883,6 +1884,8 @@ func parseOptions(opts *Options, allArgs []string) error {
case "--version":
clearExitingOpts()
opts.Version = true
case "--no-winpty":
opts.NoWinpty = true
case "--tmux":
str, err := nextString(allArgs, &i, "tmux options required")
if err != nil {
@ -1893,8 +1896,8 @@ func parseOptions(opts *Options, allArgs []string) error {
}
case "--no-tmux":
opts.Tmux = nil
case "--tmux-script":
if opts.TmuxScript, err = nextString(allArgs, &i, ""); err != nil {
case "--proxy-script":
if opts.ProxyScript, err = nextString(allArgs, &i, ""); err != nil {
return err
}
case "-x", "--extended":

132
src/proxy.go Normal file
View File

@ -0,0 +1,132 @@
package fzf
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
const becomeSuffix = ".become"
func escapeSingleQuote(str string) string {
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
}
func fifo(name string) (string, error) {
ns := time.Now().UnixNano()
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
output, err := mkfifo(output, 0600)
if err != nil {
return output, err
}
return output, nil
}
func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts *Options, withExports bool) (int, error) {
output, err := fifo("proxy-output")
if err != nil {
return ExitError, err
}
defer os.Remove(output)
// Take the output
go func() {
withOutputPipe(output, func(outputFile io.ReadCloser) {
if opts.Output == nil {
io.Copy(os.Stdout, outputFile)
} else {
reader := bufio.NewReader(outputFile)
sep := opts.PrintSep[0]
for {
item, err := reader.ReadString(sep)
if err != nil {
break
}
opts.Output <- item
}
}
})
}()
var command string
commandPrefix += ` --proxy-script "$0"`
if opts.Input == nil && util.IsTty() {
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
} else {
input, err := fifo("proxy-input")
if err != nil {
return ExitError, err
}
defer os.Remove(input)
go func() {
withInputPipe(input, func(inputFile io.WriteCloser) {
if opts.Input == nil {
io.Copy(inputFile, os.Stdin)
} else {
for item := range opts.Input {
fmt.Fprint(inputFile, item+opts.PrintSep)
}
}
})
}()
if withExports {
command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
} else {
// For mintty: cannot directly read named pipe from Go code
command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
}
}
// To ensure that the options are processed by a POSIX-compliant shell,
// we need to write the command to a temporary file and execute it with sh.
var exports []string
if withExports {
exports = os.Environ()
for idx, pairStr := range exports {
pair := strings.SplitN(pairStr, "=", 2)
exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
}
}
temp := writeTemporaryFile(append(exports, command), "\n")
defer os.Remove(temp)
cmd := cmdBuilder(temp)
if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
code := exitError.ExitCode()
if code == ExitBecome {
becomeFile := temp + becomeSuffix
data, err := os.ReadFile(becomeFile)
os.Remove(becomeFile)
if err != nil {
return ExitError, err
}
elems := strings.Split(string(data), "\x00")
if len(elems) < 1 {
return ExitError, errors.New("invalid become command")
}
command := elems[0]
env := []string{}
if len(elems) > 1 {
env = elems[1:]
}
executor := util.NewExecutor(opts.WithShell)
executor.Become(tui.TtyIn(), env, command)
}
return code, err
}
}
return ExitOk, nil
}

38
src/proxy_unix.go Normal file
View File

@ -0,0 +1,38 @@
//go:build !windows
package fzf
import (
"io"
"os"
"golang.org/x/sys/unix"
)
func sh() (string, error) {
return "sh", nil
}
func mkfifo(path string, mode uint32) (string, error) {
return path, unix.Mkfifo(path, mode)
}
func withOutputPipe(output string, task func(io.ReadCloser)) error {
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
if err != nil {
return err
}
task(outputFile)
outputFile.Close()
return nil
}
func withInputPipe(input string, task func(io.WriteCloser)) error {
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
if err != nil {
return err
}
task(inputFile)
inputFile.Close()
return nil
}

81
src/proxy_windows.go Normal file
View File

@ -0,0 +1,81 @@
//go:build windows
package fzf
import (
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"sync/atomic"
)
var shPath atomic.Value
func sh() (string, error) {
if cached := shPath.Load(); cached != nil {
return cached.(string), nil
}
cmd := exec.Command("cygpath", "-w", "/usr/bin/sh")
bytes, err := cmd.Output()
if err != nil {
return "", err
}
sh := strings.TrimSpace(string(bytes))
shPath.Store(sh)
return sh, nil
}
func mkfifo(path string, mode uint32) (string, error) {
m := strconv.FormatUint(uint64(mode), 8)
sh, err := sh()
if err != nil {
return path, err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command mkfifo -m %s %q`, m, path))
if err := cmd.Run(); err != nil {
return path, err
}
return path + ".lnk", nil
}
func withOutputPipe(output string, task func(io.ReadCloser)) error {
sh, err := sh()
if err != nil {
return err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat %q`, output))
outputFile, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
task(outputFile)
cmd.Wait()
return nil
}
func withInputPipe(input string, task func(io.WriteCloser)) error {
sh, err := sh()
if err != nil {
return err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat - > %q`, input))
inputFile, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
task(inputFile)
inputFile.Close()
cmd.Wait()
return nil
}

View File

@ -322,7 +322,7 @@ type Terminal struct {
forcePreview bool
clickHeaderLine int
clickHeaderColumn int
tmuxScript string
proxyScript string
}
type selectedItem struct {
@ -795,7 +795,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
printsep: opts.PrintSep,
tmuxScript: opts.TmuxScript,
proxyScript: opts.ProxyScript,
merger: EmptyMerger(0),
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
@ -3608,9 +3608,9 @@ func (t *Terminal) Loop() error {
t.history.append(string(t.input))
}
if len(t.tmuxScript) > 0 {
if len(t.proxyScript) > 0 {
data := strings.Join(append([]string{command}, t.environ()...), "\x00")
os.WriteFile(t.tmuxScript, []byte(data), 0600)
os.WriteFile(t.proxyScript+becomeSuffix, []byte(data), 0600)
req(reqBecome)
} else {
t.executor.Become(t.ttyin, t.environ(), command)

View File

@ -1,81 +1,24 @@
package fzf
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
func escapeSingleQuote(str string) string {
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
}
func runTmux(args []string, opts *Options) (int, error) {
ns := time.Now().UnixNano()
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-tmux-output-%d", ns))
if err := mkfifo(output, 0666); err != nil {
return ExitError, err
}
defer os.Remove(output)
// Find fzf executable
fzf := "fzf"
if found, err := os.Executable(); err == nil {
fzf = found
}
// Prepare arguments
args = append([]string{"--bind=ctrl-z:ignore"}, args...)
fzf := args[0]
args = append([]string{"--bind=ctrl-z:ignore"}, args[1:]...)
if opts.BorderShape == tui.BorderUndefined {
args = append(args, "--border")
}
args = append(args, "--no-height")
args = append(args, "--no-tmux")
argStr := ""
argStr := escapeSingleQuote(fzf)
for _, arg := range args {
// %q formatting escapes $'foo\nbar' to "foo\nbar"
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --tmux-script "$0"`
// Build command
var command string
if opts.Input == nil && util.IsTty() {
command = fmt.Sprintf(`%q%s > %q`, fzf, argStr, output)
} else {
input := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-tmux-input-%d", ns))
if err := mkfifo(input, 0644); err != nil {
return ExitError, err
}
defer os.Remove(input)
go func() {
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
if err != nil {
return
}
if opts.Input == nil {
io.Copy(inputFile, os.Stdin)
} else {
for item := range opts.Input {
fmt.Fprint(inputFile, item+opts.PrintSep)
}
}
inputFile.Close()
}()
command = fmt.Sprintf(`%q%s < %q > %q`, fzf, argStr, input, output)
}
argStr += ` --no-tmux --no-height`
// Get current directory
dir, err := os.Getwd()
@ -106,65 +49,9 @@ func runTmux(args []string, opts *Options) (int, error) {
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
// To ensure that the options are processed by a POSIX-compliant shell,
// we need to write the command to a temporary file and execute it with sh.
exports := os.Environ()
for idx, pairStr := range exports {
pair := strings.SplitN(pairStr, "=", 2)
exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
}
temp := writeTemporaryFile(append(exports, command), "\n")
defer os.Remove(temp)
tmuxArgs = append(tmuxArgs, "sh", temp)
// Take the output
go func() {
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
if err != nil {
return
}
if opts.Output == nil {
io.Copy(os.Stdout, outputFile)
} else {
reader := bufio.NewReader(outputFile)
sep := opts.PrintSep[0]
for {
item, err := reader.ReadString(sep)
if err != nil {
break
}
opts.Output <- item
}
}
outputFile.Close()
}()
cmd := exec.Command("tmux", tmuxArgs...)
if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
code := exitError.ExitCode()
if code == ExitBecome {
data, err := os.ReadFile(temp)
if err != nil {
return ExitError, err
}
elems := strings.Split(string(data), "\x00")
if len(elems) < 1 {
return ExitError, errors.New("invalid become command")
}
command := elems[0]
env := []string{}
if len(elems) > 1 {
env = elems[1:]
}
os.Remove(temp)
executor := util.NewExecutor(opts.WithShell)
executor.Become(tui.TtyIn(), env, command)
}
return code, err
}
}
return ExitOk, nil
return runProxy(argStr, func(temp string) *exec.Cmd {
sh, _ := sh()
tmuxArgs = append(tmuxArgs, sh, temp)
return exec.Command("tmux", tmuxArgs...)
}, opts, true)
}

View File

@ -1,9 +0,0 @@
//go:build !windows
package fzf
import "golang.org/x/sys/unix"
func mkfifo(path string, mode uint32) error {
return unix.Mkfifo(path, mode)
}

View File

@ -1,17 +0,0 @@
//go:build windows
package fzf
import (
"os/exec"
"strconv"
)
func mkfifo(path string, mode uint32) error {
m := strconv.FormatUint(uint64(mode), 8)
cmd := exec.Command("mkfifo", "-m", m, path)
if err := cmd.Run(); err != nil {
return err
}
return nil
}

View File

@ -139,7 +139,8 @@ func DurWithin(
// IsTty returns true if stdin is a terminal
func IsTty() bool {
return isatty.IsTerminal(os.Stdin.Fd())
fd := os.Stdin.Fd()
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
}
// ToTty returns true if stdout is a terminal

9
src/winpty.go Normal file
View File

@ -0,0 +1,9 @@
//go:build !windows
package fzf
import "errors"
func runWinpty(_ []string, _ *Options) (int, error) {
return ExitError, errors.New("Not supported")
}

29
src/winpty_windows.go Normal file
View File

@ -0,0 +1,29 @@
//go:build windows
package fzf
import (
"fmt"
"os"
"os/exec"
)
func runWinpty(args []string, opts *Options) (int, error) {
sh, err := sh()
if err != nil {
return ExitError, err
}
argStr := escapeSingleQuote(args[0])
for _, arg := range args[1:] {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-winpty --no-height`
return runProxy(argStr, func(temp string) *exec.Cmd {
cmd := exec.Command(sh, "-c", fmt.Sprintf(`winpty < /dev/tty > /dev/tty -- sh %q`, temp))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}, opts, false)
}