m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-18 00:03:39 -05:00

Add --tmux option to replace fzf-tmux script

This commit is contained in:
Junegunn Choi
2024-05-10 01:40:56 +09:00
parent 01e7668915
commit 83b6033906
12 changed files with 313 additions and 9 deletions

View File

@@ -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
}

View File

@@ -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?

View File

@@ -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
}

149
src/tmux.go Normal file
View File

@@ -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
}

9
src/tmux_unix.go Normal file
View File

@@ -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)
}

17
src/tmux_windows.go Normal file
View File

@@ -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
}

View File

@@ -356,7 +356,8 @@ type MouseEvent struct {
type BorderShape int
const (
BorderNone BorderShape = iota
BorderUndefined BorderShape = iota
BorderNone
BorderRounded
BorderSharp
BorderBold