summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-05-20 17:06:44 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2024-05-20 18:24:14 +0900
commit573df524fed1c493ce7d8ea893f06ab90f2ca18a (patch)
tree0bd1185bf827de5860aaf6a4944c2fd1c8ed69ef /src
parentaee417c46a2f6d2aa87ea3fcc799fdc7bc830dfe (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.go6
-rw-r--r--src/options.go9
-rw-r--r--src/proxy.go132
-rw-r--r--src/proxy_unix.go38
-rw-r--r--src/proxy_windows.go81
-rw-r--r--src/terminal.go8
-rw-r--r--src/tmux.go131
-rw-r--r--src/tmux_unix.go9
-rw-r--r--src/tmux_windows.go17
-rw-r--r--src/util/util.go3
-rw-r--r--src/winpty.go9
-rw-r--r--src/winpty_windows.go29
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)
+}