From bf65e8cd12b0477a916ed16fa6d9d3ff9a78c824 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 31 Mar 2020 22:18:09 +0900 Subject: [PATCH] [fzf-tmux] Add option to start fzf in tmux popup window Requires latest tmux built from source (e.g. brew install tmux --HEAD) Examples: # 50%/50% width and height on the center of the screen fzf-tmux -p # 80%/80% fzf-tmux -p80% # 80%/40% fzf-tmux -p80%,40% # Separate -w and -h fzf-tmux -w80% -h40% # 80%/40% at position (0, 0) fzf-tmux -w80% -h40% -x0 -y0 You can configure key bindings and fuzzy completion to open in tmux popup window like so: FZF_TMUX_OPTS='-p 80%' --- README.md | 14 +++++---- bin/fzf-tmux | 66 +++++++++++++++++++++++++++++++---------- man/man1/fzf-tmux.1 | 30 ++++++++++++++----- shell/completion.bash | 8 ++--- shell/completion.zsh | 8 ++--- shell/key-bindings.bash | 51 +++++++++++++------------------ shell/key-bindings.fish | 19 ++++++++++-- shell/key-bindings.zsh | 21 +++++++++---- test/test_go.rb | 28 ++++++++--------- 9 files changed, 153 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index bfd8580..02fcbc1 100644 --- a/README.md +++ b/README.md @@ -281,8 +281,10 @@ own as well. [fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane. ```sh -# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] -# (-[udlr]: up/down/left/right) +# usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] + +# See available options +fzf-tmux --help # select git branches in horizontal split below (15 lines) git branch | fzf-tmux -d 15 @@ -291,7 +293,7 @@ git branch | fzf-tmux -d 15 cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse ``` -It will still work even when you're not on tmux, silently ignoring `-[udlr]` +It will still work even when you're not on tmux, silently ignoring `-[pudlr]` options, so you can invariably use `fzf-tmux` in your scripts. Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in @@ -318,9 +320,9 @@ fish. - Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_OPTS` to pass additional options -If you're on a tmux session, you can start fzf in a split pane by setting -`FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT` -(e.g. `20`, `50%`). +If you're on a tmux session, you can start fzf in a tmux split pane or in +a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`). +See `fzf-tmux --help` for available options. More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings). diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 3233bd5..53dfd9b 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -1,6 +1,6 @@ #!/usr/bin/env bash # fzf-tmux: starts fzf in a tmux pane -# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] +# usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] fail() { >&2 echo "$1" @@ -10,6 +10,7 @@ fail() { fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" [[ -x "$fzf" ]] || fail 'fzf executable not found' +tmux_args=() args=() opt="" skip="" @@ -20,15 +21,23 @@ term="" [[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}") help() { - >&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] + >&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] - Layout - -u [HEIGHT[%]] Split above (up) - -d [HEIGHT[%]] Split below (down) - -l [WIDTH[%]] Split left - -r [WIDTH[%]] Split right + LAYOUT OPTIONS: + (default layout: -d 50%) - (default: -d 50%) + Popup window (requires tmux 3.2 or above): + -p [WIDTH[%][,HEIGHT[%]]] (default: 50%) + -w WIDTH[%] + -h HEIGHT[%] + -x COL + -y ROW + + Split pane: + -u [HEIGHT[%]] Split above (up) + -d [HEIGHT[%]] Split below (down) + -l [WIDTH[%]] Split left + -r [WIDTH[%]] Split right ' exit } @@ -47,8 +56,10 @@ while [[ $# -gt 0 ]]; do echo "fzf-tmux (with fzf $("$fzf" --version))" exit ;; - -w*|-h*|-d*|-u*|-r*|-l*) - if [[ "$arg" =~ ^.[lrw] ]]; then + -p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*) + if [[ "$arg" =~ ^-[pwhxy] ]]; then + [[ "$opt" =~ "-K -E" ]] || opt="-K -E" + elif [[ "$arg" =~ ^.[lr] ]]; then opt="-h" if [[ "$arg" =~ ^.l ]]; then opt="$opt -d" @@ -66,7 +77,7 @@ while [[ $# -gt 0 ]]; do if [[ ${#arg} -gt 2 ]]; then size="${arg:2}" else - if [[ "$1" =~ ^[0-9]+%?$ ]]; then + if [[ "$1" =~ ^[0-9%,C]+$ ]]; then size="$1" shift else @@ -74,7 +85,15 @@ while [[ $# -gt 0 ]]; do fi fi - if [[ "$size" =~ %$ ]]; then + if [[ "$arg" =~ ^-p ]]; then + if [[ -n "$size" ]]; then + w=${size%%,*} + h=${size##*,} + opt="$opt -w$w -h$h" + fi + elif [[ "$arg" =~ ^-[whxy] ]]; then + opt="$opt ${arg:0:2}$size" + elif [[ "$size" =~ %$ ]]; then size=${size:0:((${#size}-1))} if [[ -n "$swap" ]]; then opt="$opt -p $(( 100 - size ))" @@ -100,6 +119,8 @@ while [[ $# -gt 0 ]]; do # "--" can be used to separate fzf-tmux options from fzf options to # avoid conflicts skip=1 + tmux_args=("${args[@]}") + args=() continue ;; *) @@ -115,7 +136,7 @@ if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$l fi # --height option is not allowed -args+=("--no-height") +args=("--no-height" "${args[@]}") # Handle zoomed tmux pane by moving it to a temp window if tmux list-panes -F '#F' | grep -q Z; then @@ -181,21 +202,34 @@ close="; trap - EXIT SIGINT SIGTERM $close" tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') ) +if [[ "$opt" =~ "-K -E" ]]; then + cat $fifo2 & + if [[ -n "$term" ]] || [[ -t 0 ]]; then + cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf + TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux popup -d '#{pane_current_path}' "${tmux_args[@]}" $opt -R "$envs bash $argsf" > /dev/null 2>&1 + else + mkfifo $fifo1 + cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf + cat <&0 > $fifo1 & + TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux popup -d '#{pane_current_path}' "${tmux_args[@]}" $opt -R "$envs bash $argsf" > /dev/null 2>&1 + fi + exit $? +fi + if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "$envs bash -c 'cd $(printf %q "$PWD"); exec -a fzf bash $argsf'" $swap \ + split-window $opt "${tmux_args[@]}" "$envs bash -c 'cd $(printf %q "$PWD"); exec -a fzf bash $argsf'" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "$envs bash -c 'exec -a fzf bash $argsf'" $swap \ + split-window $opt "${tmux_args[@]}" "$envs bash -c 'exec -a fzf bash $argsf'" $swap \ > /dev/null 2>&1 cat <&0 > $fifo1 & fi cat $fifo2 exit "$(cat $fifo3)" - diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 1f47134..b559d79 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -27,19 +27,33 @@ THE SOFTWARE. fzf-tmux - open fzf in tmux split pane .SH SYNOPSIS -.B fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] +.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] .SH DESCRIPTION -fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane. It is -designed to work just like fzf except that it does not take up the whole -screen. You can safely use fzf-tmux instead of fzf in your scripts as the extra -options will be silently ignored if you're not on tmux. +fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in +a tmux popup window. It is designed to work just like fzf except that it does +not take up the whole screen. You can safely use fzf-tmux instead of fzf in +your scripts as the extra options will be silently ignored if you're not on +tmux. -.SH OPTIONS -.SS Layout +.SH LAYOUT OPTIONS -(default: \fB-d 50%\fR) +(default layout: \fB-d 50%\fR) +.SS Popup window +(requires tmux 3.2 or above) +.TP +.B "-p [WIDTH[%][,HEIGHT[%]]]" +.TP +.B "-w WIDTH[%]" +.TP +.B "-h WIDTH[%]" +.TP +.B "-x COL" +.TP +.B "-y ROW" + +.SS Split pane .TP .B "-u [height[%]]" Split above (up) diff --git a/shell/completion.bash b/shell/completion.bash index b1f7ac9..377afe5 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -2,10 +2,10 @@ # / __/___ / __/ # / /_/_ / / /_ # / __/ / /_/ __/ -# /_/ /___/_/-completion.bash +# /_/ /___/_/ completion.bash # # - $FZF_TMUX (default: 0) -# - $FZF_TMUX_HEIGHT (default: '40%') +# - $FZF_TMUX_OPTS (default: empty) # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) @@ -37,9 +37,9 @@ bind '"\e[0n": redraw-current-line' __fzf_comprun() { if [ "$(type -t _fzf_comprun 2>&1)" = function ]; then _fzf_comprun "$@" - elif [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]; then + elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then shift - fzf-tmux -d "${FZF_TMUX_HEIGHT:-40%}" "$@" + fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@" else shift fzf "$@" diff --git a/shell/completion.zsh b/shell/completion.zsh index e828a70..e791b3a 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -2,10 +2,10 @@ # / __/___ / __/ # / /_/_ / / /_ # / __/ / /_/ __/ -# /_/ /___/_/-completion.zsh +# /_/ /___/_/ completion.zsh # # - $FZF_TMUX (default: 0) -# - $FZF_TMUX_HEIGHT (default: '40%') +# - $FZF_TMUX_OPTS (default: '-d 40%') # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) @@ -99,9 +99,9 @@ fi __fzf_comprun() { if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then _fzf_comprun "$@" - elif [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]; then + elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then shift - fzf-tmux -d "${FZF_TMUX_HEIGHT:-40%}" "$@" + fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}}} -- "$@" else shift fzf "$@" diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 16e1ec5..a0b9eae 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -1,3 +1,16 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ key-bindings.bash +# +# - $FZF_TMUX_OPTS +# - $FZF_CTRL_T_COMMAND +# - $FZF_CTRL_T_OPTS +# - $FZF_CTRL_R_OPTS +# - $FZF_ALT_C_COMMAND +# - $FZF_ALT_C_OPTS + # Key bindings # ------------ __fzf_select__() { @@ -5,7 +18,7 @@ __fzf_select__() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" - eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do + eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read -r item; do printf '%q ' "$item" done echo @@ -13,35 +26,15 @@ __fzf_select__() { if [[ $- =~ i ]]; then -__fzf_use_tmux__() { - [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] -} - __fzfcmd() { - __fzf_use_tmux__ && - echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" -} - -__fzf_select_tmux__() { - local height - height=${FZF_TMUX_HEIGHT:-40%} - if [[ $height =~ %$ ]]; then - height="-p ${height%\%}" - else - height="-l $height" - fi - - tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'" + [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] || [ -n "$FZF_TMUX_OPTS" ]; } && + echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" } fzf-file-widget() { - if __fzf_use_tmux__; then - __fzf_select_tmux__ - else - local selected="$(__fzf_select__)" - READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" - READLINE_POINT=$(( READLINE_POINT + ${#selected} )) - fi + local selected="$(__fzf_select__)" + READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" + READLINE_POINT=$(( READLINE_POINT + ${#selected} )) } __fzf_cd__() { @@ -75,11 +68,7 @@ bind -m emacs-standard '"\C-z": vi-editing-mode' if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then # CTRL-T - Paste the selected file path into the command line - if __fzf_use_tmux__; then - bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select_tmux__`\e\C-e\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"' - else - bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"' - fi + bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"' bind -m vi-command '"\C-t": "\C-z\C-t\C-z"' bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"' diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index c4bad83..73a0733 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -1,3 +1,16 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ key-bindings.fish +# +# - $FZF_TMUX_OPTS +# - $FZF_CTRL_T_COMMAND +# - $FZF_CTRL_T_OPTS +# - $FZF_CTRL_R_OPTS +# - $FZF_ALT_C_COMMAND +# - $FZF_ALT_C_OPTS + # Key bindings # ------------ function fzf_key_bindings @@ -84,8 +97,10 @@ function fzf_key_bindings function __fzfcmd test -n "$FZF_TMUX"; or set FZF_TMUX 0 test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% - if [ $FZF_TMUX -eq 1 ] - echo "fzf-tmux -d$FZF_TMUX_HEIGHT" + if [ -n "$FZF_TMUX_OPTS" ] + echo "fzf-tmux $FZF_TMUX_OPTS -- " + else if [ $FZF_TMUX -eq 1 ] + echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- " else echo "fzf" end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index fa8dbe7..f8cb782 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -1,3 +1,16 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ key-bindings.zsh +# +# - $FZF_TMUX_OPTS +# - $FZF_CTRL_T_COMMAND +# - $FZF_CTRL_T_OPTS +# - $FZF_CTRL_R_OPTS +# - $FZF_ALT_C_COMMAND +# - $FZF_ALT_C_OPTS + # Key bindings # ------------ @@ -40,13 +53,9 @@ __fsel() { return $ret } -__fzf_use_tmux__() { - [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] -} - __fzfcmd() { - __fzf_use_tmux__ && - echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" + [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] || [ -n "$FZF_TMUX_OPTS" ]; } && + echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" } fzf-file-widget() { diff --git a/test/test_go.rb b/test/test_go.rb index a1ea33a..108b6eb 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -99,31 +99,27 @@ class Tmux go(%W[kill-window -t #{win}]) end + def focus + go(%W[select-window -t #{win}]) + end + def send_keys(*args) - target = - if args.last.is_a?(Hash) - hash = args.pop - go(%W[select-window -t #{win}]) - "#{win}.#{hash[:pane]}" - else - win - end - go(%W[send-keys -t #{target}] + args.map(&:to_s)) + go(%W[send-keys -t #{win}] + args.map(&:to_s)) end def paste(str) system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter') end - def capture(pane = 0) - go(%W[capture-pane -p -t #{win}.#{pane}]).reverse.drop_while(&:empty?).reverse + def capture + go(%W[capture-pane -p -t #{win}]).reverse.drop_while(&:empty?).reverse end - def until(refresh = false, pane = 0) + def until(refresh = false) lines = nil begin wait do - lines = capture(pane) + lines = capture class << lines def counts lazy @@ -2040,9 +2036,11 @@ module CompletionTest tmux.send_keys 'C-c' # FZF_TMUX=1 + skip 'screen size too small' if `tput lines`.to_i < 15 new_shell - tmux.send_keys 'unset FZFFOOBR**', :Tab, pane: 0 - tmux.until(false, 1) { |lines| lines.match_count == 1 } + tmux.focus + tmux.send_keys 'unset FZFFOOBR**', :Tab + tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } end