diff options
author | Junegunn Choi <junegunn.c@gmail.com> | 2024-05-20 17:06:44 +0900 |
---|---|---|
committer | Junegunn Choi <junegunn.c@gmail.com> | 2024-05-20 18:24:14 +0900 |
commit | 573df524fed1c493ce7d8ea893f06ab90f2ca18a (patch) | |
tree | 0bd1185bf827de5860aaf6a4944c2fd1c8ed69ef /src | |
parent | aee417c46a2f6d2aa87ea3fcc799fdc7bc830dfe (diff) |
Use winpty to launch fzf in Git bash (mintty)
Close #3806
Known limitation:
* --height cannot be used
Diffstat (limited to 'src')
-rw-r--r-- | src/core.go | 6 | ||||
-rw-r--r-- | src/options.go | 9 | ||||
-rw-r--r-- | src/proxy.go | 132 | ||||
-rw-r--r-- | src/proxy_unix.go | 38 | ||||
-rw-r--r-- | src/proxy_windows.go | 81 | ||||
-rw-r--r-- | src/terminal.go | 8 | ||||
-rw-r--r-- | src/tmux.go | 131 | ||||
-rw-r--r-- | src/tmux_unix.go | 9 | ||||
-rw-r--r-- | src/tmux_windows.go | 17 | ||||
-rw-r--r-- | src/util/util.go | 3 | ||||
-rw-r--r-- | src/winpty.go | 9 | ||||
-rw-r--r-- | src/winpty_windows.go | 29 |
12 files changed, 315 insertions, 157 deletions
diff --git a/src/core.go b/src/core.go index 17b7d1df..9c5ac033 100644 --- a/src/core.go +++ b/src/core.go @@ -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 { diff --git a/src/options.go b/src/options.go index 669feef4..ac7e76f0 100644 --- a/src/options.go +++ b/src/options.go @@ -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": diff --git a/src/proxy.go b/src/proxy.go new file mode 100644 index 00000000..bbac0292 --- /dev/null +++ b/src/proxy.go @@ -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 +} diff --git a/src/proxy_unix.go b/src/proxy_unix.go new file mode 100644 index 00000000..189d0e56 --- /dev/null +++ b/src/proxy_unix.go @@ -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 +} diff --git a/src/proxy_windows.go b/src/proxy_windows.go new file mode 100644 index 00000000..a957da8a --- /dev/null +++ b/src/proxy_windows.go @@ -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 +} diff --git a/src/terminal.go b/src/terminal.go index c396fa20..f50d7698 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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) diff --git a/src/tmux.go b/src/tmux.go index 5cc970fa..3be95661 100644 --- a/src/tmux.go +++ b/src/tmux.go @@ -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) } diff --git a/src/tmux_unix.go b/src/tmux_unix.go deleted file mode 100644 index f6ddd58b..00000000 --- a/src/tmux_unix.go +++ /dev/null @@ -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) -} diff --git a/src/tmux_windows.go b/src/tmux_windows.go deleted file mode 100644 index bd356364..00000000 --- a/src/tmux_windows.go +++ /dev/null @@ -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 -} diff --git a/src/util/util.go b/src/util/util.go index f6e00e9c..b5b27f28 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -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 diff --git a/src/winpty.go b/src/winpty.go new file mode 100644 index 00000000..46f9400f --- /dev/null +++ b/src/winpty.go @@ -0,0 +1,9 @@ +//go:build !windows + +package fzf + +import "errors" + +func runWinpty(_ []string, _ *Options) (int, error) { + return ExitError, errors.New("Not supported") +} diff --git a/src/winpty_windows.go b/src/winpty_windows.go new file mode 100644 index 00000000..83802a2b --- /dev/null +++ b/src/winpty_windows.go @@ -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) +} |