summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-05-10 01:40:56 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2024-05-18 17:08:36 +0900
commit83b603390683d49ff75b72d142b4dba4b5186d73 (patch)
tree103394f2ffab559d08528ed50a342f80c61d392b
parent01e7668915c4e3cf8c9eeca283d41beac924fe1f (diff)
Add --tmux option to replace fzf-tmux script
-rwxr-xr-xbin/fzf-tmux6
-rw-r--r--main.go2
-rw-r--r--man/man1/fzf-tmux.12
-rw-r--r--man/man1/fzf.120
-rw-r--r--plugin/fzf.vim4
-rw-r--r--src/core.go5
-rw-r--r--src/functions.go2
-rw-r--r--src/options.go103
-rw-r--r--src/tmux.go149
-rw-r--r--src/tmux_unix.go9
-rw-r--r--src/tmux_windows.go17
-rw-r--r--src/tui/tui.go3
12 files changed, 313 insertions, 9 deletions
diff --git a/bin/fzf-tmux b/bin/fzf-tmux
index e66dcda5..71369737 100755
--- a/bin/fzf-tmux
+++ b/bin/fzf-tmux
@@ -132,8 +132,10 @@ if [[ -z "$TMUX" ]]; then
exit $?
fi
-# --height option is not allowed. CTRL-Z is also disabled.
-args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
+# * --height option is not allowed
+# * CTRL-Z is also disabled
+# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
+args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
# Handle zoomed tmux pane without popup options by moving it to a temp window
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
diff --git a/main.go b/main.go
index 7c76bef0..1e05345c 100644
--- a/main.go
+++ b/main.go
@@ -35,7 +35,7 @@ func printScript(label string, content []byte) {
}
func exit(code int, err error) {
- if err != nil {
+ if code == fzf.ExitError {
fmt.Fprintln(os.Stderr, err.Error())
}
os.Exit(code)
diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1
index 11161e48..3ab36d44 100644
--- a/man/man1/fzf-tmux.1
+++ b/man/man1/fzf-tmux.1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
-.TH fzf-tmux 1 "May 2024" "fzf 0.52.1" "fzf-tmux - open fzf in tmux split pane"
+.TH fzf-tmux 1 "May 2024" "fzf 0.53.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 0f99739d..09d7e893 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
-.TH fzf 1 "May 2024" "fzf 0.52.1" "fzf - a command-line fuzzy finder"
+.TH fzf 1 "May 2024" "fzf 0.53.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -216,6 +216,24 @@ compatible with a negative height value.
Minimum height when \fB--height\fR is given in percent (default: 10).
Ignored when \fB--height\fR is not specified.
.TP
+.BI "--tmux=" "[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]"
+Start fzf in a tmux popup. Requires tmux 3.3 or later. This option is ignored
+if you are not running fzf inside tmux.
+
+e.g.
+ \fB# Popup in the center with 80% width height
+ fzf --tmux 80%
+
+ # Popup on the left with 40% width and 100% height
+ fzf --tmux right,40%
+
+ # Popup on the bottom with 100% width and 30% height
+ fzf --tmux bottom,30%
+
+ # Popup on the top with 80% width and 40% height
+ fzf --tmux top,80%,40%\fR
+
+.TP
.BI "--layout=" "LAYOUT"
Choose the layout (default: default)
diff --git a/plugin/fzf.vim b/plugin/fzf.vim
index fc7b196a..51ed1388 100644
--- a/plugin/fzf.vim
+++ b/plugin/fzf.vim
@@ -537,10 +537,10 @@ try
let use_term = 0
endif
if use_term
- let optstr .= ' --no-height'
+ let optstr .= ' --no-height --no-tmux'
elseif use_height
let height = s:calc_size(&lines, dict.down, dict)
- let optstr .= ' --height='.height
+ let optstr .= ' --no-tmux --height='.height
endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
diff --git a/src/core.go b/src/core.go
index 2a07b82a..17b7d1df 100644
--- a/src/core.go
+++ b/src/core.go
@@ -2,6 +2,7 @@
package fzf
import (
+ "os"
"sync"
"time"
@@ -19,6 +20,10 @@ 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)
+ }
+
if err := postProcessOptions(opts); err != nil {
return ExitError, err
}
diff --git a/src/functions.go b/src/functions.go
index f16371a2..8d29db74 100644
--- a/src/functions.go
+++ b/src/functions.go
@@ -7,7 +7,7 @@ import (
)
func writeTemporaryFile(data []string, printSep string) string {
- f, err := os.CreateTemp("", "fzf-preview-*")
+ f, err := os.CreateTemp("", "fzf-temp-*")
if err != nil {
// Unable to create temporary file
// FIXME: Should we terminate the program?
diff --git a/src/options.go b/src/options.go
index 0265693a..1024038d 100644
--- a/src/options.go
+++ b/src/options.go
@@ -63,6 +63,8 @@ const Usage = `usage: fzf [options]
according to the input size.
--min-height=HEIGHT Minimum height when --height is given in percent
(default: 10)
+ --tmux=OPTS Start fzf in a tmux popup (requires tmux 3.3+)
+ [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border[=STYLE] Draw border around the finder
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
@@ -180,6 +182,13 @@ type sizeSpec struct {
percent bool
}
+func (s sizeSpec) String() string {
+ if s.percent {
+ return fmt.Sprintf("%d%%", int(s.size))
+ }
+ return fmt.Sprintf("%d", int(s.size))
+}
+
func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{}
}
@@ -199,8 +208,15 @@ const (
posDown
posLeft
posRight
+ posCenter
)
+type tmuxOptions struct {
+ width sizeSpec
+ height sizeSpec
+ position windowPosition
+}
+
type layoutType int
const (
@@ -248,6 +264,74 @@ func (o *previewOpts) Toggle() {
o.hidden = !o.hidden
}
+func parseTmuxOptions(arg string) (*tmuxOptions, error) {
+ var err error
+ opts := tmuxOptions{}
+ tokens := splitRegexp.Split(arg, -1)
+ if len(tokens) == 0 || len(tokens) > 3 {
+ return nil, errors.New("invalid tmux option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]])")
+ }
+
+ // Defaults to 'center'
+ switch tokens[0] {
+ case "top", "up":
+ opts.position = posUp
+ opts.width = sizeSpec{100, true}
+ case "bottom", "down":
+ opts.position = posDown
+ opts.width = sizeSpec{100, true}
+ case "left":
+ opts.position = posLeft
+ opts.height = sizeSpec{100, true}
+ case "right":
+ opts.position = posRight
+ opts.height = sizeSpec{100, true}
+ case "center":
+ opts.position = posCenter
+ opts.width = sizeSpec{50, true}
+ opts.height = sizeSpec{50, true}
+ default:
+ opts.position = posCenter
+ opts.width = sizeSpec{50, true}
+ opts.height = sizeSpec{50, true}
+ tokens = append([]string{"center"}, tokens...)
+ }
+
+ // One size given
+ var size1 sizeSpec
+ if len(tokens) > 1 {
+ if size1, err = parseSize(tokens[1], 100, "size"); err != nil {
+ return nil, err
+ }
+ }
+
+ // Two sizes given
+ var size2 sizeSpec
+ if len(tokens) == 3 {
+ if size2, err = parseSize(tokens[2], 100, "size"); err != nil {
+ return nil, err
+ }
+ opts.width = size1
+ opts.height = size2
+ } else if len(tokens) == 2 {
+ switch tokens[0] {
+ case "top", "up":
+ opts.height = size1
+ case "bottom", "down":
+ opts.height = size1
+ case "left":
+ opts.width = size1
+ case "right":
+ opts.width = size1
+ case "center":
+ opts.width = size1
+ opts.height = size1
+ }
+ }
+
+ return &opts, nil
+}
+
func parseLabelPosition(opts *labelOpts, arg string) error {
opts.column = 0
opts.bottom = false
@@ -296,6 +380,7 @@ type walkerOpts struct {
type Options struct {
Input chan string
Output chan string
+ Tmux *tmuxOptions
Bash bool
Zsh bool
Fish bool
@@ -1787,6 +1872,16 @@ func parseOptions(opts *Options, allArgs []string) error {
case "--version":
clearExitingOpts()
opts.Version = true
+ case "--tmux":
+ str, err := nextString(allArgs, &i, "tmux options required")
+ if err != nil {
+ return err
+ }
+ if opts.Tmux, err = parseTmuxOptions(str); err != nil {
+ return err
+ }
+ case "--no-tmux":
+ opts.Tmux = nil
case "-x", "--extended":
opts.Extended = true
case "-e", "--exact":
@@ -2264,6 +2359,10 @@ func parseOptions(opts *Options, allArgs []string) error {
if opts.FuzzyAlgo, err = parseAlgo(value); err != nil {
return err
}
+ } else if match, value := optString(arg, "--tmux="); match {
+ if opts.Tmux, err = parseTmuxOptions(value); err != nil {
+ return err
+ }
} else if match, value := optString(arg, "--scheme="); match {
opts.Scheme = strings.ToLower(value)
} else if match, value := optString(arg, "-q", "--query="); match {
@@ -2478,6 +2577,10 @@ func postProcessOptions(opts *Options) error {
uniseg.EastAsianAmbiguousWidth = 2
}
+ if opts.BorderShape == tui.BorderUndefined {
+ opts.BorderShape = tui.BorderNone
+ }
+
if err := validateSign(opts.Pointer, "pointer"); err != nil {
return err
}
diff --git a/src/tmux.go b/src/tmux.go
new file mode 100644
index 00000000..ea1816a5
--- /dev/null
+++ b/src/tmux.go
@@ -0,0 +1,149 @@
+package fzf
+
+import (
+ "bufio"
+ "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...)
+ if opts.BorderShape == tui.BorderUndefined {
+ args = append(args, "--border")
+ }
+ args = append(args, "--no-height")
+ args = append(args, "--no-tmux")
+ argStr := ""
+ for _, arg := range args {
+ // %q formatting escapes $'foo\nbar' to "foo\nbar"
+ argStr += " " + escapeSingleQuote(arg)
+ }
+
+ // 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)
+ }
+
+ // Get current directory
+ dir, err := os.Getwd()
+ if err != nil {
+ dir = "."
+ }
+
+ // Set tmux options for popup placement
+ // C Both The centre of the terminal
+ // R -x The right side of the terminal
+ // P Both The bottom left of the pane
+ // M Both The mouse position
+ // W Both The window position on the status line
+ // S -y The line above or below the status line
+ tmuxArgs := []string{"display-popup", "-E", "-B", "-d", dir}
+ switch opts.Tmux.position {
+ case posUp:
+ tmuxArgs = append(tmuxArgs, "-xC", "-y0")
+ case posDown:
+ tmuxArgs = append(tmuxArgs, "-xC", "-yS")
+ case posLeft:
+ tmuxArgs = append(tmuxArgs, "-x0", "-yC")
+ case posRight:
+ tmuxArgs = append(tmuxArgs, "-xR", "-yC")
+ case posCenter:
+ tmuxArgs = append(tmuxArgs, "-xC", "-yC")
+ }
+ 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 {
+ return exitError.ExitCode(), err
+ }
+ }
+
+ return ExitOk, nil
+}
diff --git a/src/tmux_unix.go b/src/tmux_unix.go
new file mode 100644
index 00000000..f6ddd58b
--- /dev/null
+++ b/src/tmux_unix.go
@@ -0,0 +1,9 @@
+//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
new file mode 100644
index 00000000..bd356364
--- /dev/null
+++ b/src/tmux_windows.go
@@ -0,0 +1,17 @@
+//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/tui/tui.go b/src/tui/tui.go
index aed41a9d..ee497740 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -356,7 +356,8 @@ type MouseEvent struct {
type BorderShape int
const (
- BorderNone BorderShape = iota
+ BorderUndefined BorderShape = iota
+ BorderNone
BorderRounded
BorderSharp
BorderBold