diff options
author | Junegunn Choi <junegunn.c@gmail.com> | 2017-01-08 01:30:31 +0900 |
---|---|---|
committer | Junegunn Choi <junegunn.c@gmail.com> | 2017-01-08 02:09:56 +0900 |
commit | 1448d631a7c72905f62dbb343a8f231a1c3cc52c (patch) | |
tree | 05abfedd2a0777c2640c8259267d3ad855879dfe | |
parent | fd137a9e875ba1fd9feed4903e102951f8098c33 (diff) |
Add --height option
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | CHANGELOG.md | 6 | ||||
-rw-r--r-- | README.md | 36 | ||||
-rwxr-xr-x | bin/fzf-tmux | 3 | ||||
-rw-r--r-- | man/man1/fzf.1 | 9 | ||||
-rw-r--r-- | plugin/fzf.vim | 26 | ||||
-rw-r--r-- | shell/completion.bash | 57 | ||||
-rw-r--r-- | shell/completion.zsh | 14 | ||||
-rw-r--r-- | shell/key-bindings.bash | 33 | ||||
-rw-r--r-- | shell/key-bindings.fish | 13 | ||||
-rw-r--r-- | shell/key-bindings.zsh | 10 | ||||
-rw-r--r-- | src/options.go | 18 | ||||
-rw-r--r-- | src/result.go | 2 | ||||
-rw-r--r-- | src/result_test.go | 15 | ||||
-rw-r--r-- | src/terminal.go | 225 | ||||
-rw-r--r-- | src/tui/light.go | 764 | ||||
-rw-r--r-- | src/tui/ncurses.go | 250 | ||||
-rw-r--r-- | src/tui/tcell.go | 273 | ||||
-rw-r--r-- | src/tui/tui.go | 170 | ||||
-rw-r--r-- | src/tui/tui_test.go | 14 | ||||
-rw-r--r-- | src/util/util.go | 16 | ||||
-rw-r--r-- | src/util/util_unix.go | 6 | ||||
-rw-r--r-- | src/util/util_windows.go | 6 | ||||
-rw-r--r-- | test/test_go.rb | 272 |
24 files changed, 1628 insertions, 612 deletions
diff --git a/.travis.yml b/.travis.yml index a1a6497d..3f2a67f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: ruby matrix: include: - env: TAGS= - rvm: 2.2.0 + rvm: 2.3.3 # - env: TAGS=tcell # rvm: 2.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a37b74c5..f78270c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +0.16.0 +------ +- Added `--height HEIGHT[%]` option +- Preview window will truncate long lines by default. Line wrap can be enabled + by `:wrap` flag in `--preview-window`. + 0.15.9 ------ - Fixed rendering glitches introduced in 0.15.8 @@ -123,6 +123,29 @@ vim $(fzf) - Mouse: scroll, click, double-click; shift-click and shift-scroll on multi-select mode +#### Layout + +fzf by default starts in fullscreen mode, but you can make it start below the +cursor with `--height` option. + +```sh +vim $(fzf --height 40%) +``` + +Also check out `--reverse` option if you prefer "top-down" layout instead of +the default "bottom-up" layout. + +```sh +vim $(fzf --height 40% --reverse) +``` + +You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by +default. + +```sh +export FZF_DEFAULT_OPTS='--height 40% --reverse' +``` + #### Search syntax Unless otherwise specified, fzf starts in "extended-search mode" where you can @@ -189,6 +212,13 @@ 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]` options, so you can invariably use `fzf-tmux` in your scripts. +Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in +fullscreen mode. + +```sh +fzf --height 40% +``` + Key bindings for command line ----------------------------- @@ -206,9 +236,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, fzf will start in a split pane. You may disable -this tmux integration by setting `FZF_TMUX` to 0, or 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 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 use vi mode on bash, you need to add `set -o vi` *before* `source ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi diff --git a/bin/fzf-tmux b/bin/fzf-tmux index dd335d2b..f2011998 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -114,6 +114,9 @@ if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$l exit $? fi +# --height option is not allowed +args+=("--no-height") + # Handle zoomed tmux pane by moving it to a temp window if tmux list-panes -F '#F' | grep -q Z; then zoomed=1 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 4e19ce92..fac2aab5 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -130,6 +130,10 @@ on the center of the screen. Label characters for \fBjump\fR and \fBjump-accept\fR .SS Layout .TP +.BI "--height=" "HEIGHT[%]" +Display fzf window below the cursor with the given height instead of using +fullscreen. +.TP .B "--reverse" Reverse orientation .TP @@ -248,10 +252,11 @@ e.g. \fBfzf --preview="head -$LINES {}"\fR Note that you can escape a placeholder pattern by prepending a backslash. .RE .TP -.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]" +.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]" Determine the layout of the preview window. If the argument ends with \fB:hidden\fR, the preview window will be hidden by default until -\fBtoggle-preview\fR action is triggered. +\fBtoggle-preview\fR action is triggered. Long lines are truncated by default. +Line wrap can be enabled with \fB:wrap\fR flag. .RS .B POSITION: (default: right) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 06af648e..c8c6c91f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -296,14 +296,24 @@ try else let prefix = '' endif - let tmux = (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) + + let use_height = has_key(dict, 'down') && + \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) + let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) + let term = has('nvim') && !tmux + if use_height + let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) + elseif term + let optstr .= ' --no-height' + endif let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result - if has('nvim') && !tmux + if term return s:execute_term(dict, command, temps) endif - let lines = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps) + let lines = tmux ? s:execute_tmux(dict, command, temps) + \ : s:execute(dict, command, use_height, temps) call s:callback(dict, lines) return lines finally @@ -400,9 +410,9 @@ function! s:exit_handler(code, command, ...) return 1 endfunction -function! s:execute(dict, command, temps) abort +function! s:execute(dict, command, use_height, temps) abort call s:pushd(a:dict) - if has('unix') + if has('unix') && !a:use_height silent! !clear 2> /dev/null endif let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') @@ -416,7 +426,11 @@ function! s:execute(dict, command, temps) abort else let command = escaped endif - execute 'silent !'.command + if a:use_height + call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s 2> /dev/tty', &lines, command)) + else + execute 'silent !'.command + endif let exit_status = v:shell_error redraw! return s:exit_handler(exit_status, command) ? s:collect(a:temps) : [] diff --git a/shell/completion.bash b/shell/completion.bash index 392aee2d..d6d7238d 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -5,7 +5,7 @@ # / __/ / /_/ __/ # /_/ /___/_/-completion.bash # -# - $FZF_TMUX (default: 1) +# - $FZF_TMUX (default: 0) # - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) @@ -30,6 +30,15 @@ fi ########################################################### +# To redraw line after fzf closes (printf '\e[5n') +bind '"\e[0n": redraw-current-line' + +__fzfcmd_complete() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] && + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || + echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" +} + _fzf_orig_completion_filter() { sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}' @@ -43,35 +52,42 @@ _fzf_opts_completion() { opts=" -x --extended -e --exact + --algo -i +i -n --nth + --with-nth -d --delimiter +s --no-sort --tac --tiebreak - --bind -m --multi --no-mouse - --color - --black - --reverse + --bind + --cycle --no-hscroll + --jump-labels + --height + --reverse + --margin --inline-info --prompt + --header + --header-lines + --ansi + --tabstop + --color + --no-bold + --history + --history-size + --preview + --preview-window -q --query -1 --select-1 -0 --exit-0 -f --filter --print-query --expect - --toggle-sort - --sync - --cycle - --history - --history-size - --header - --header-lines - --margin" + --sync" case "${prev}" in --tiebreak) @@ -116,7 +132,7 @@ _fzf_handle_dynamic_completion() { __fzf_generic_path_completion() { local cur base dir leftover matches trigger cmd fzf - [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" COMPREPLY=() trigger=${FZF_COMPLETION_TRIGGER-'**'} @@ -132,7 +148,6 @@ __fzf_generic_path_completion() { leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" - tput sc matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) @@ -142,7 +157,7 @@ __fzf_generic_path_completion() { else COMPREPLY=( "$cur" ) fi - tput rc + printf '\e[5n' return 0 fi dir=$(dirname "$dir") @@ -160,7 +175,7 @@ _fzf_complete() { local cur selected trigger cmd fzf post post="$(caller 0 | awk '{print $2}')_post" type -t "$post" > /dev/null 2>&1 || post=cat - [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" trigger=${FZF_COMPLETION_TRIGGER-'**'} @@ -168,10 +183,9 @@ _fzf_complete() { if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} - tput sc selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" - tput rc + printf '\e[5n' if [ -n "$selected" ]; then COMPREPLY=("$selected") @@ -200,10 +214,9 @@ _fzf_complete_kill() { [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 local selected fzf - [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - tput sc + fzf="$(__fzfcmd_complete)" selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') - tput rc + printf '\e[5n' if [ -n "$selected" ]; then COMPREPLY=( "$selected" ) diff --git a/shell/completion.zsh b/shell/completion.zsh index d3faef80..fb2c16a1 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -5,7 +5,7 @@ # / __/ / /_/ __/ # /_/ /___/_/-completion.zsh # -# - $FZF_TMUX (default: 1) +# - $FZF_TMUX (default: 0) # - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) @@ -30,6 +30,12 @@ fi ########################################################### +__fzfcmd_complete() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] && + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || + echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" +} + __fzf_generic_path_completion() { local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches # (Q) flag removes a quoting level: "foo\ bar" => "foo bar" @@ -39,7 +45,7 @@ __fzf_generic_path_completion() { fzf_opts=$4 suffix=$5 tail=$6 - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" setopt localoptions nonomatch dir="$base" @@ -90,7 +96,7 @@ _fzf_complete() { post="${funcstack[2]}_post" type $post > /dev/null 2>&1 || post=cat - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" _fzf_feed_fifo "$fifo" matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') @@ -157,7 +163,7 @@ fzf-completion() { tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 33d8bc8d..382302d4 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -5,7 +5,7 @@ __fzf_select__() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" - eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do + eval "$cmd | fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse -m $@ $FZF_CTRL_T_OPTS" | while read -r item; do printf '%q ' "$item" done echo @@ -13,8 +13,14 @@ __fzf_select__() { if [[ $- =~ i ]]; then +__fzf_use_tmux__() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] +} + __fzfcmd() { - [ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" + __fzf_use_tmux__ && + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || + echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" } __fzf_select_tmux__() { @@ -26,14 +32,14 @@ __fzf_select_tmux__() { 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__)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'" + 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\"'" } fzf-file-widget() { if __fzf_use_tmux__; then __fzf_select_tmux__ else - local selected="$(__fzf_select__)" + local selected="$(__fzf_select__ --height ${FZF_TMUX_HEIGHT:-40%} --reverse)" READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" READLINE_POINT=$(( READLINE_POINT + ${#selected} )) fi @@ -51,7 +57,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | + eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" @@ -60,22 +66,15 @@ __fzf_history__() ( fi ) -__fzf_use_tmux__() { - [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ] -} - -[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0 -__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0 - if [[ ! -o vi ]]; then # Required to refresh the prompt after fzf bind '"\er": redraw-current-line' bind '"\e^": history-expand-line' # CTRL-T - Paste the selected file path into the command line - if [ $__use_bind_x -eq 1 ]; then + if [ $BASH_VERSINFO -gt 3 ]; then bind -x '"\C-t": "fzf-file-widget"' - elif [ $__use_tmux -eq 1 ]; then + elif __fzf_use_tmux__; then bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' else bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' @@ -102,9 +101,9 @@ else # CTRL-T - Paste the selected file path into the command line # - FIXME: Selected items are attached to the end regardless of cursor position - if [ $__use_bind_x -eq 1 ]; then + if [ $BASH_VERSINFO -gt 3 ]; then bind -x '"\C-t": "fzf-file-widget"' - elif [ $__use_tmux -eq 1 ]; then + elif __fzf_use_tmux__; then bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"' else bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "' @@ -120,6 +119,4 @@ else bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"' fi -unset -v __use_tmux __use_bind_x - fi diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index dd75fecf..fc618448 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -39,7 +39,7 @@ function fzf_key_bindings end function fzf-history-widget -d "Show command history" - history | eval (__fzfcmd) +s +m --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result + history | eval (__fzfcmd) +s +m --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result and commandline -- $result commandline -f repaint end @@ -54,15 +54,12 @@ function fzf_key_bindings end function __fzfcmd - set -q FZF_TMUX; or set FZF_TMUX 1 + set -q FZF_TMUX; or set FZF_TMUX 0 + set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% if [ $FZF_TMUX -eq 1 ] - if set -q FZF_TMUX_HEIGHT - echo "fzf-tmux -d$FZF_TMUX_HEIGHT" - else - echo "fzf-tmux -d40%" - end + echo "fzf-tmux -d$FZF_TMUX_HEIGHT" else - echo "fzf" + echo "fzf --height $FZF_TMUX_HEIGHT --reverse" end end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index fed01532..7e24d92a 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -17,8 +17,14 @@ __fsel() { return $ret } +__fzf_use_tmux__() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] +} + __fzfcmd() { - [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" + __fzf_use_tmux__ && + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || + echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" } fzf-file-widget() { @@ -49,7 +55,7 @@ bindkey '\ec' fzf-cd-widget fzf-history-widget() { local selected num setopt localoptions noglobsubst pipefail 2> /dev/null - selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) + selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] diff --git a/src/options.go b/src/options.go index 6fd3f6c6..0c6661f3 100644 --- a/src/options.go +++ b/src/options.go @@ -10,6 +10,7 @@ import ( "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" + "github.com/junegunn/fzf/src/util" "github.com/junegunn/go-shellwords" ) @@ -46,6 +47,8 @@ const usage = `usage: fzf [options] --jump-labels=CHARS Label characters for jump and jump-accept Layout + --height=HEIGHT[%] Display fzf window below the cursor with the given + height instead of using fullscreen --reverse Reverse orientation --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --inline-info Display finder info inline with the query @@ -147,6 +150,7 @@ type Options struct { Theme *tui.ColorTheme Black bool Bold bool + Height sizeSpec Reverse bool Cycle bool Hscroll bool @@ -760,6 +764,14 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec { return sizeSpec{val, percent} } +func parseHeight(str string) sizeSpec { + if util.IsWindows() { + errorExit("--height options is currently not supported on Windows") + } + size := parseSize(str, 100, "height") + return size +} + func parsePreviewWindow(opts *previewOpts, input string) { // Default opts.position = posRight @@ -1003,6 +1015,10 @@ func parseOptions(opts *Options, allArgs []string) { case "--preview-window": parsePreviewWindow(&opts.Preview, nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) + case "--height": + opts.Height = parseHeight(nextString(allArgs, &i, "height required: [HEIGHT[%]]")) + case "--no-height": + opts.Height = sizeSpec{} case "--no-margin": opts.Margin = defaultMargin() case "--margin": @@ -1029,6 +1045,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.WithNth = splitNth(value) } else if match, _ := optString(arg, "-s", "--sort="); match { opts.Sort = 1 // Don't care + } else if match, value := optString(arg, "--height="); match { + opts.Height = parseHeight(value) } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { diff --git a/src/result.go b/src/result.go index e2d7c755..3d79176f 100644 --- a/src/result.go +++ b/src/result.go @@ -166,7 +166,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, - color: tui.PairFor(fg, bg), + color: tui.NewColorPair(fg, bg), attr: ansi.color.attr.Merge(attr)}) } } diff --git a/src/result_test.go b/src/result_test.go index 15b1bdbb..0e91fc87 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -105,7 +105,8 @@ func TestColorOffset(t *testing.T) { ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}} // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - colors := item.colorOffsets(offsets, tui.Dark256, 99, 0, true) + pair := tui.NewColorPair(99, 199) + colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true) assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) { var attr tui.Attr if bold { @@ -116,10 +117,10 @@ func TestColorOffset(t *testing.T) { t.Error(o) } } - assert(0, 0, 5, tui.ColUser, false) - assert(1, 5, 15, 99, false) - assert(2, 15, 20, tui.ColUser, false) - assert(3, 22, 25, tui.ColUser+1, true) - assert(4, 25, 35, 99, false) - assert(5, 35, 40, tui.ColUser+2, true) + assert(0, 0, 5, tui.NewColorPair(1, 5), false) + assert(1, 5, 15, pair, false) + assert(2, 15, 20, tui.NewColorPair(1, 5), false) + assert(3, 22, 25, tui.NewColorPair(2, 6), true) + assert(4, 25, 35, pair, false) + assert(5, 35, 40, tui.NewColorPair(4, 8), true) } diff --git a/src/terminal.go b/src/terminal.go index 5b482b06..80029236 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -15,8 +15,6 @@ import ( "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" - - "github.com/junegunn/go-runewidth" ) // import "github.com/pkg/profile" @@ -42,6 +40,14 @@ type previewer struct { enabled bool } +type itemLine struct { + current bool + label string + result Result +} + +var emptyLine = itemLine{} + // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration @@ -69,11 +75,12 @@ type Terminal struct { header []string header0 []string ansi bool + tabstop int margin [4]sizeSpec strong tui.Attr - window *tui.Window - bwindow *tui.Window - pwindow *tui.Window + window tui.Window + bwindow tui.Window + pwindow tui.Window count int progress int reading bool @@ -89,10 +96,12 @@ type Terminal struct { eventBox *util.EventBox mutex sync.Mutex initFunc func() + prevLines []itemLine suppress bool startChan chan bool slab *util.Slab theme *tui.ColorTheme + tui tui.Renderer } type selectedItem struct { @@ -115,7 +124,6 @@ func (a byTimeOrder) Less(i, j int) bool { } var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} -var _runeWidths = make(map[rune]int) var _tabStop int const ( @@ -247,7 +255,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } else { header = reverseStringArray(opts.Header) } - _tabStop = opts.Tabstop var delay time.Duration if opts.Tac { delay = initialDelayTac @@ -262,6 +269,24 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { if !opts.Bold { strongAttr = tui.AttrRegular } + var renderer tui.Renderer + if opts.Height.size > 0 { + maxHeightFunc := func(termHeight int) int { + var maxHeight int + if opts.Height.percent { + maxHeight = int(opts.Height.size * float64(termHeight) / 100.0) + } else { + maxHeight = util.Min(int(opts.Height.size), termHeight) + } + if opts.InlineInfo { + return util.Max(maxHeight, 3) + } + return util.Max(maxHeight, 4) + } + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) + } else { + renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) + } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, @@ -290,6 +315,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { header: header, header0: header, ansi: opts.Ansi, + tabstop: opts.Tabstop, reading: true, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, @@ -306,9 +332,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { slab: util.MakeSlab(slab16Size, slab32Size), theme: opts.Theme, startChan: make(chan bool, 1), - initFunc: func() { - tui.Init(opts.Theme, opts.Black, opts.Mouse) - }} + tui: renderer, + initFunc: func() { renderer.Init() }} } // Input returns current query string @@ -401,22 +426,10 @@ func (t *Terminal) sortSelected() []selectedItem { return sels } -func runeWidth(r rune, prefixWidth int) int { - if r == '\t' { - return _tabStop - prefixWidth%_tabStop - } else if w, found := _runeWidths[r]; found { - return w - } else { - w := runewidth.RuneWidth(r) - _runeWidths[r] = w - return w - } -} - -func displayWidth(runes []rune) int { +func (t *Terminal) displayWidth(runes []rune) int { l := 0 for _, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) } return l } @@ -437,9 +450,10 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int { } func (t *Terminal) resizeWindows() { - screenWidth := tui.MaxX() - screenHeight := tui.MaxY() + screenWidth := t.tui.MaxX() + screenHeight := t.tui.MaxY() marginInt := [4]int{} + t.prevLines = make([]itemLine, screenHeight) for idx, sizeSpec := range t.margin { if sizeSpec.percent { var max float64 @@ -487,40 +501,40 @@ func (t *Terminal) resizeWindows() { height := screenHeight - marginInt[0] - marginInt[2] if t.isPreviewEnabled() { createPreviewWindow := func(y int, x int, w int, h int) { - t.bwindow = tui.NewWindow(y, x, w, h, true) + t.bwindow = t.tui.NewWindow(y, x, w, h, true) pwidth := w - 4 // ncurses auto-wraps the line when the cursor reaches the right-end of // the window. To prevent unintended line-wraps, we use the width one // column larger than the desired value. - if !t.preview.wrap && tui.DoesAutoWrap() { + if !t.preview.wrap && t.tui.DoesAutoWrap() { pwidth += 1 } - t.pwindow = tui.NewWindow(y+1, x+2, pwidth, h-2, false) + t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, false) } switch t.preview.position { case posUp: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0]+pheight, marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) } } else { - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], |