Skip to content

Commit

Permalink
Add --tmux option to replace fzf-tmux script
Browse files Browse the repository at this point in the history
  • Loading branch information
junegunn committed May 9, 2024
1 parent 07880ca commit 8522afb
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 8 deletions.
6 changes: 4 additions & 2 deletions bin/fzf-tmux
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion man/man1/fzf-tmux.1
Original file line number Diff line number Diff line change
Expand Up @@ -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.0" "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
Expand Down
20 changes: 19 additions & 1 deletion man/man1/fzf.1
Original file line number Diff line number Diff line change
Expand Up @@ -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.0" "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
Expand Down Expand Up @@ -215,6 +215,24 @@ compatible with a negative height value.
.BI "--min-height=" "HEIGHT"
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)
Expand Down
4 changes: 2 additions & 2 deletions plugin/fzf.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
5 changes: 5 additions & 0 deletions src/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package fzf

import (
"os"
"sync"
"time"

Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion src/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
103 changes: 103 additions & 0 deletions src/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -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{}
}
Expand All @@ -199,8 +208,15 @@ const (
posDown
posLeft
posRight
posCenter
)

type tmuxOptions struct {
width sizeSpec
height sizeSpec
position windowPosition
}

type layoutType int

const (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -296,6 +380,7 @@ type walkerOpts struct {
type Options struct {
Input chan string
Output chan string
Tmux *tmuxOptions
Bash bool
Zsh bool
Fish bool
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
126 changes: 126 additions & 0 deletions src/tmux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package fzf

import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)

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 += " '" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
}

// Build command
var command string
if !util.IsTty() {
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
}
io.Copy(inputFile, os.Stdin)
inputFile.Close()
}()

command = fmt.Sprintf(`%q%s < %q > %q`, fzf, argStr, input, output)
} else {
command = fmt.Sprintf(`%q%s > %q`, fzf, argStr, 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.
temp := writeTemporaryFile([]string{command}, "\n")
defer os.Remove(temp)

tmuxArgs = append(tmuxArgs, "sh", temp)
cmd := exec.Command("tmux", tmuxArgs...)
cmd.Stdin = tui.TtyIn()
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
cmd.Env = os.Environ()
cmd.Start()

// Take the output
go func() {
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
if err != nil {
return
}
io.Copy(os.Stdout, outputFile)
outputFile.Close()
}()

if err := cmd.Wait(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode(), err
}
}

return ExitOk, nil
}
9 changes: 9 additions & 0 deletions src/tmux_unix.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 8522afb

Please sign in to comment.