From 1448d631a7c72905f62dbb343a8f231a1c3cc52c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Jan 2017 01:30:31 +0900 Subject: [PATCH 01/28] Add --height option --- .travis.yml | 2 +- CHANGELOG.md | 6 + README.md | 36 +- bin/fzf-tmux | 3 + man/man1/fzf.1 | 9 +- plugin/fzf.vim | 26 +- shell/completion.bash | 57 +-- shell/completion.zsh | 14 +- shell/key-bindings.bash | 33 +- shell/key-bindings.fish | 13 +- shell/key-bindings.zsh | 10 +- src/options.go | 18 + src/result.go | 2 +- src/result_test.go | 15 +- src/terminal.go | 225 +++++++----- src/tui/light.go | 764 +++++++++++++++++++++++++++++++++++++++ src/tui/ncurses.go | 248 ++++++------- src/tui/tcell.go | 275 +++++++------- src/tui/tui.go | 170 +++++++-- src/tui/tui_test.go | 14 - src/util/util.go | 16 + src/util/util_unix.go | 6 + src/util/util_windows.go | 6 + test/test_go.rb | 264 +++++++------- 24 files changed, 1624 insertions(+), 608 deletions(-) create mode 100644 src/tui/light.go delete mode 100644 src/tui/tui_test.go diff --git a/.travis.yml b/.travis.yml index a1a6497..3f2a67f 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 a37b74c..f78270c 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 diff --git a/README.md b/README.md index 6381279..550ea77 100644 --- a/README.md +++ b/README.md @@ -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 dd335d2..f201199 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 4e19ce9..fac2aab 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 06af648..c8c6c91 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 392aee2..d6d7238 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 d3faef8..fb2c16a 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 33d8bc8..382302d 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 dd75fec..fc61844 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 fed0153..7e24d92 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 6fd3f6c..0c6661f 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 e2d7c75..3d79176 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 15b1bdb..0e91fc8 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 5b482b0..8002923 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], width, @@ -530,7 +544,7 @@ func (t *Terminal) resizeWindows() { func (t *Terminal) move(y int, x int, clear bool) { if !t.reverse { - y = t.window.Height - y - 1 + y = t.window.Height() - y - 1 } if clear { @@ -541,7 +555,7 @@ func (t *Terminal) move(y int, x int, clear bool) { } func (t *Terminal) placeCursor() { - t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input[:t.cx]), false) + t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input[:t.cx]), false) } func (t *Terminal) printPrompt() { @@ -552,7 +566,7 @@ func (t *Terminal) printPrompt() { func (t *Terminal) printInfo() { if t.inlineInfo { - t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) + t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input)+1, true) if t.reading { t.window.CPrint(tui.ColSpinner, t.strong, " < ") } else { @@ -589,7 +603,7 @@ func (t *Terminal) printHeader() { if len(t.header) == 0 { return } - max := t.window.Height + max := t.window.Height() var state *ansiState for idx, lineStr := range t.header { line := idx + 2 @@ -616,19 +630,25 @@ func (t *Terminal) printList() { maxy := t.maxItems() count := t.merger.Length() - t.offset - for i := 0; i < maxy; i++ { + for j := 0; j < maxy; j++ { + i := j + if !t.reverse { + i = maxy - 1 - j + } line := i + 2 + len(t.header) if t.inlineInfo { line-- } - t.move(line, 0, true) if i < count { - t.printItem(t.merger.Get(i+t.offset), i, i == t.cy-t.offset) + t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset) + } else if t.prevLines[i] != emptyLine { + t.prevLines[i] = emptyLine + t.move(line, 0, true) } } } -func (t *Terminal) printItem(result *Result, i int, current bool) { +func (t *Terminal) printItem(result *Result, line int, i int, current bool) { item := result.item _, selected := t.selected[item.Index()] label := " " @@ -641,6 +661,15 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } else if current { label = ">" } + + // Avoid unnecessary redraw + newLine := itemLine{current, label, *result} + if t.prevLines[i] == newLine { + return + } + t.prevLines[i] = newLine + + t.move(line, 0, true) t.window.CPrint(tui.ColCursor, t.strong, label) if current { if selected { @@ -659,11 +688,11 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } } -func trimRight(runes []rune, width int) ([]rune, int) { +func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) { // We start from the beginning to handle tab characters l := 0 for idx, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) if l > width { return runes[:idx], len(runes) - idx } @@ -671,10 +700,10 @@ func trimRight(runes []rune, width int) ([]rune, int) { return runes, 0 } -func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { +func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { l := 0 for _, r := range runes { - l += runeWidth(r, l+prefixWidth) + l += util.RuneWidth(r, l+prefixWidth, t.tabstop) if l > limit { // Early exit return l @@ -683,27 +712,27 @@ func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { return l } -func trimLeft(runes []rune, width int) ([]rune, int32) { +func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) { if len(runes) > maxDisplayWidthCalc && len(runes) > width { trimmed := len(runes) - width return runes[trimmed:], int32(trimmed) } - currentWidth := displayWidth(runes) + currentWidth := t.displayWidth(runes) var trimmed int32 for currentWidth > width && len(runes) > 0 { runes = runes[1:] trimmed++ - currentWidth = displayWidthWithLimit(runes, 2, width) + currentWidth = t.displayWidthWithLimit(runes, 2, width) } return runes, trimmed } -func overflow(runes []rune, max int) bool { +func (t *Terminal) overflow(runes []rune, max int) bool { l := 0 for _, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) if l > max { return true } @@ -737,22 +766,22 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo } offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) - maxWidth := t.window.Width - 3 + maxWidth := t.window.Width() - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) - if overflow(text, maxWidth) { + if t.overflow(text, maxWidth) { if t.hscroll { // Stri.. - if !overflow(text[:maxe], maxWidth-2) { - text, _ = trimRight(text, maxWidth-2) + if !t.overflow(text[:maxe], maxWidth-2) { + text, _ = t.trimRight(text, maxWidth-2) text = append(text, []rune("..")...) } else { // Stri.. - if overflow(text[maxe:], 2) { + if t.overflow(text[maxe:], 2) { text = append(text[:maxe], []rune("..")...) } // ..ri.. var diff int32 - text, diff = trimLeft(text, maxWidth-2) + text, diff = t.trimLeft(text, maxWidth-2) // Transform offsets for idx, offset := range offsets { @@ -766,7 +795,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo text = append([]rune(".."), text...) } } else { - text, _ = trimRight(text, maxWidth-2) + text, _ = t.trimRight(text, maxWidth-2) text = append(text, []rune("..")...) for idx, offset := range offsets { @@ -784,11 +813,11 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo b := util.Constrain32(offset.offset[0], index, maxOffset) e := util.Constrain32(offset.offset[1], index, maxOffset) - substr, prefixWidth = processTabs(text[index:b], prefixWidth) + substr, prefixWidth = t.processTabs(text[index:b], prefixWidth) t.window.CPrint(col1, attr, substr) if b < e { - substr, prefixWidth = processTabs(text[b:e], prefixWidth) + substr, prefixWidth = t.processTabs(text[b:e], prefixWidth) t.window.CPrint(offset.color, offset.attr, substr) } @@ -798,7 +827,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo } } if index < maxOffset { - substr, _ = processTabs(text[index:], prefixWidth) + substr, _ = t.processTabs(text[index:], prefixWidth) t.window.CPrint(col1, attr, substr) } } @@ -835,38 +864,44 @@ func (t *Terminal) printPreview() { return true } } - if !t.preview.wrap { - lines := strings.Split(str, "\n") - for i, line := range lines { - limit := t.pwindow.Width - if tui.DoesAutoWrap() { - limit -= 1 - } - if i == 0 { - limit -= t.pwindow.X() - } - trimmed, _ := trimRight([]rune(line), limit) - lines[i], _ = processTabs(trimmed, 0) + lines := strings.Split(str, "\n") + for i, line := range lines { + limit := t.pwindow.Width() + if t.tui.DoesAutoWrap() { + limit -= 1 } + if i == 0 { + limit -= t.pwindow.X() + } + trimmed := []rune(line) + if !t.preview.wrap { + trimmed, _ = t.trimRight(trimmed, limit) + } + lines[i], _ = t.processTabs(trimmed, 0) str = strings.Join(lines, "\n") } if ansi != nil && ansi.colored() { - return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr) + return t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) } return t.pwindow.Fill(str) }) - if t.previewer.lines > t.pwindow.Height { + t.pwindow.FinishFill() + if t.previewer.lines > t.pwindow.Height() { offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) - t.pwindow.Move(0, t.pwindow.Width-len(offset)) + pos := t.pwindow.Width() - len(offset) + if t.tui.DoesAutoWrap() { + pos -= 1 + } + t.pwindow.Move(0, pos) t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset) } } -func processTabs(runes []rune, prefixWidth int) (string, int) { +func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { var strbuf bytes.Buffer l := prefixWidth for _, r := range runes { - w := runeWidth(r, l) + w := util.RuneWidth(r, l, t.tabstop) l += w if r == '\t' { strbuf.WriteString(strings.Repeat(" ", w)) @@ -889,9 +924,9 @@ func (t *Terminal) printAll() { func (t *Terminal) refresh() { if !t.suppress { if t.isPreviewEnabled() { - tui.RefreshWindows([]*tui.Window{t.bwindow, t.pwindow, t.window}) + t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window}) } else { - tui.RefreshWindows([]*tui.Window{t.window}) + t.tui.RefreshWindows([]tui.Window{t.window}) } } } @@ -1013,9 +1048,9 @@ func (t *Terminal) executeCommand(template string, items []*Item) { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - tui.Pause() + t.tui.Pause() cmd.Run() - if tui.Resume() { + if t.tui.Resume() { t.printAll() } t.refresh() @@ -1162,11 +1197,11 @@ func (t *Terminal) Loop() { case reqRefresh: t.suppress = false case reqRedraw: - tui.Clear() - tui.Refresh() + t.tui.Clear() + t.tui.Refresh() t.printAll() case reqClose: - tui.Close() + t.tui.Close() if t.output() { exit(exitOk) } @@ -1179,11 +1214,11 @@ func (t *Terminal) Loop() { case reqPreviewRefresh: t.printPreview() case reqPrintQuery: - tui.Close() + t.tui.Close() t.printer(string(t.input)) exit(exitOk) case reqQuit: - tui.Close() + t.tui.Close() exit(exitInterrupt) } } @@ -1196,7 +1231,7 @@ func (t *Terminal) Loop() { looping := true for looping { - event := tui.GetChar() + event := t.tui.GetChar() t.mutex.Lock() previousInput := t.input @@ -1288,11 +1323,11 @@ func (t *Terminal) Loop() { } case actPreviewPageUp: if t.isPreviewEnabled() { - scrollPreview(-t.pwindow.Height) + scrollPreview(-t.pwindow.Height()) } case actPreviewPageDown: if t.isPreviewEnabled() { - scrollPreview(t.pwindow.Height) + scrollPreview(t.pwindow.Height()) } case actBeginningOfLine: t.cx = 0 @@ -1466,11 +1501,11 @@ func (t *Terminal) Loop() { scrollPreview(-me.S) } } else if t.window.Enclose(my, mx) { - mx -= t.window.Left - my -= t.window.Top - mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input)) + mx -= t.window.Left() + my -= t.window.Top() + mx = util.Constrain(mx-t.displayWidth([]rune(t.prompt)), 0, len(t.input)) if !t.reverse { - my = t.window.Height - my - 1 + my = t.window.Height() - my - 1 } min := 2 + len(t.header) if t.inlineInfo { @@ -1582,7 +1617,7 @@ func (t *Terminal) vset(o int) bool { } func (t *Terminal) maxItems() int { - max := t.window.Height - 2 - len(t.header) + max := t.window.Height() - 2 - len(t.header) if t.inlineInfo { max++ } diff --git a/src/tui/light.go b/src/tui/light.go new file mode 100644 index 0000000..1273c8f --- /dev/null +++ b/src/tui/light.go @@ -0,0 +1,764 @@ +package tui + +import ( + "fmt" + "os" + "strconv" + "strings" + "syscall" + "time" + "unicode/utf8" + + "github.com/junegunn/fzf/src/util" +) + +const ( + defaultWidth = 80 + defaultHeight = 24 + + escPollInterval = 5 +) + +func openTtyIn() *os.File { + in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + if err != nil { + panic("Failed to open /dev/tty") + } + return in +} + +// FIXME: Need better handling of non-displayable characters +func (r *LightRenderer) stderr(str string) { + bytes := []byte(str) + runes := []rune{} + for len(bytes) > 0 { + r, sz := utf8.DecodeRune(bytes) + if r == utf8.RuneError || r != '\x1b' && r != '\n' && r < 32 { + runes = append(runes, '?') + } else { + runes = append(runes, r) + } + bytes = bytes[sz:] + } + r.queued += string(runes) +} + +func (r *LightRenderer) csi(code string) { + r.stderr("\x1b[" + code) +} + +func (r *LightRenderer) flush() { + if len(r.queued) > 0 { + fmt.Fprint(os.Stderr, r.queued) + r.queued = "" + } +} + +// Light renderer +type LightRenderer struct { + theme *ColorTheme + mouse bool + forceBlack bool + prevDownTime time.Time + clickY []int + ttyin *os.File + buffer []byte + ostty string + width int + height int + yoffset int + tabstop int + escDelay int + upOneLine bool + queued string + maxHeightFunc func(int) int +} + +type LightWindow struct { + renderer *LightRenderer + colored bool + border bool + top int + left int + width int + height int + posx int + posy int + tabstop int + bg Color +} + +func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer { + r := LightRenderer{ + theme: theme, + forceBlack: forceBlack, + mouse: mouse, + ttyin: openTtyIn(), + yoffset: -1, + tabstop: tabstop, + upOneLine: false, + maxHeightFunc: maxHeightFunc} + return &r +} + +func (r *LightRenderer) defaultTheme() *ColorTheme { + colors, err := util.ExecCommand("tput colors").Output() + if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { + return Dark256 + } + return Default16 +} + +func stty(cmd string) string { + out, err := util.ExecCommand("stty " + cmd + " < /dev/tty").Output() + if err != nil { + // Not sure how to handle this + panic("stty " + cmd + ": " + err.Error()) + } + return strings.TrimSpace(string(out)) +} + +func (r *LightRenderer) findOffset() (row int, col int) { + r.csi("6n") + r.flush() + bytes := r.getBytesInternal([]byte{}) + + // ^[[*;*R + if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' { + nums := strings.Split(string(bytes[2:len(bytes)-1]), ";") + if len(nums) == 2 { + return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1 + } + return -1, -1 + } + + // No idea + return -1, -1 +} + +func repeat(s string, times int) string { + if times > 0 { + return strings.Repeat(s, times) + } + return "" +} + +func atoi(s string, defaultValue int) int { + value, err := strconv.Atoi(s) + if err != nil { + return defaultValue + } + return value +} + +func (r *LightRenderer) Init() { + delay := 100 + delayEnv := os.Getenv("ESCDELAY") + if len(delayEnv) > 0 { + num, err := strconv.Atoi(delayEnv) + if err == nil && num >= 0 { + delay = num + } + } + r.escDelay = delay + + r.ostty = stty("-g") + stty("raw") + r.updateTerminalSize() + initTheme(r.theme, r.defaultTheme(), r.forceBlack) + + _, x := r.findOffset() + if x > 0 { + r.upOneLine = true + r.stderr("\n") + } + for i := 1; i < r.MaxY(); i++ { + r.stderr("\n") + r.csi("G") + } + + if r.mouse { + r.csi("?1000h") + } + r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) + r.csi("G") + r.csi("s") + r.yoffset, _ = r.findOffset() +} + +func (r *LightRenderer) updateTerminalSize() { + sizes := strings.Split(stty("size"), " ") + if len(sizes) < 2 { + r.width = defaultWidth + r.height = r.maxHeightFunc(defaultHeight) + } else { + r.width = atoi(sizes[1], defaultWidth) + r.height = r.maxHeightFunc(atoi(sizes[0], defaultHeight)) + } +} + +func (r *LightRenderer) getch(nonblock bool) int { + b := make([]byte, 1) + util.SetNonblock(r.ttyin, nonblock) + _, err := r.ttyin.Read(b) + if err != nil { + return -1 + } + return int(b[0]) +} + +func (r *LightRenderer) getBytes() []byte { + return r.getBytesInternal(r.buffer) +} + +func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { + c := r.getch(false) + + retries := 0 + if c == ESC { + retries = r.escDelay / escPollInterval + } + buffer = append(buffer, byte(c)) + + for { + c = r.getch(true) + if c == -1 { + if retries > 0 { + retries-- + time.Sleep(escPollInterval * time.Millisecond) + continue + } + break + } + retries = 0 + buffer = append(buffer, byte(c)) + } + + return buffer +} + +func (r *LightRenderer) GetChar() Event { + if len(r.buffer) == 0 { + r.buffer = r.getBytes() + } + if len(r.buffer) == 0 { + panic("Empty buffer") + } + + sz := 1 + defer func() { + r.buffer = r.buffer[sz:] + }() + + switch r.buffer[0] { + case CtrlC: + return Event{CtrlC, 0, nil} + case CtrlG: + return Event{CtrlG, 0, nil} + case CtrlQ: + return Event{CtrlQ, 0, nil} + case 127: + return Event{BSpace, 0, nil} + case ESC: + ev := r.escSequence(&sz) + // Second chance + if ev.Type == Invalid { + r.buffer = r.getBytes() + ev = r.escSequence(&sz) + } + return ev + } + + // CTRL-A ~ CTRL-Z + if r.buffer[0] <= CtrlZ { + return Event{int(r.buffer[0]), 0, nil} + } + char, rsz := utf8.DecodeRune(r.buffer) + if char == utf8.RuneError { + return Event{ESC, 0, nil} + } + sz = rsz + return Event{Rune, char, nil} +} + +func (r *LightRenderer) escSequence(sz *int) Event { + if len(r.buffer) < 2 { + return Event{ESC, 0, nil} + } + *sz = 2 + switch r.buffer[1] { + case 13: + return Event{AltEnter, 0, nil} + case 32: + return Event{AltSpace, 0, nil} + case 47: + return Event{AltSlash, 0, nil} + case 98: + return Event{AltB, 0, nil} + case 100: + return Event{AltD, 0, nil} + case 102: + return Event{AltF, 0, nil} + case 127: + return Event{AltBS, 0, nil} + case 91, 79: + if len(r.buffer) < 3 { + return Event{Invalid, 0, nil} + } + *sz = 3 + switch r.buffer[2] { + case 68: + return Event{Left, 0, nil} + case 67: + return Event{Right, 0, nil} + case 66: + return Event{Down, 0, nil} + case 65: + return Event{Up, 0, nil} + case 90: + return Event{BTab, 0, nil} + case 72: + return Event{Home, 0, nil} + case 70: + return Event{End, 0, nil} + case 77: + return r.mouseSequence(sz) + case 80: + return Event{F1, 0, nil} + case 81: + return Event{F2, 0, nil} + case 82: + return Event{F3, 0, nil} + case 83: + return Event{F4, 0, nil} + case 49, 50, 51, 52, 53, 54: + if len(r.buffer) < 4 { + return Event{Invalid, 0, nil} + } + *sz = 4 + switch r.buffer[2] { + case 50: + if len(r.buffer) == 5 && r.buffer[4] == 126 { + *sz = 5 + switch r.buffer[3] { + case 48: + return Event{F9, 0, nil} + case 49: + return Event{F10, 0, nil} + case 51: + return Event{F11, 0, nil} + case 52: + return Event{F12, 0, nil} + } + } + // Bracketed paste mode \e[200~ / \e[201 + if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 { + *sz = 6 + return Event{Invalid, 0, nil} + } + return Event{Invalid, 0, nil} // INS + case 51: + return Event{Del, 0, nil} + case 52: + return Event{End, 0, nil} + case 53: + return Event{PgUp, 0, nil} + case 54: + return Event{PgDn, 0, nil} + case 49: + switch r.buffer[3] { + case 126: + return Event{Home, 0, nil} + case 53, 55, 56, 57: + if len(r.buffer) == 5 && r.buffer[4] == 126 { + *sz = 5 + switch r.buffer[3] { + case 53: + return Event{F5, 0, nil} + case 55: + return Event{F6, 0, nil} + case 56: + return Event{F7, 0, nil} + case 57: + return Event{F8, 0, nil} + } + } + return Event{Invalid, 0, nil} + case 59: + if len(r.buffer) != 6 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch r.buffer[4] { + case 50: + switch r.buffer[5] { + case 68: + return Event{Home, 0, nil} + case 67: + return Event{End, 0, nil} + } + case 53: + switch r.buffer[5] { + case 68: + return Event{SLeft, 0, nil} + case 67: + return Event{SRight, 0, nil} + } + } // r.buffer[4] + } // r.buffer[3] + } // r.buffer[2] + } // r.buffer[2] + } // r.buffer[1] + if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' { + return Event{AltA + int(r.buffer[1]) - 'a', 0, nil} + } + return Event{Invalid, 0, nil} +} + +func (r *LightRenderer) mouseSequence(sz *int) Event { + if len(r.buffer) < 6 || r.yoffset < 0 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch r.buffer[3] { + case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl + 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl + mod := r.buffer[3] >= 36 + down := r.buffer[3]%2 == 0 + x := int(r.buffer[4] - 33) + y := int(r.buffer[5]-33) - r.yoffset + double := false + if down { + now := time.Now() + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, y) + } else { + r.clickY = []int{y} + } + r.prevDownTime = now + } else { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { + double = true + } + } + + return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} + case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl + 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl + mod := r.buffer[3] >= 100 + s := 1 - int(r.buffer[3]%2)*2 + x := int(r.buffer[4] - 33) + y := int(r.buffer[5]-33) - r.yoffset + return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} + } + return Event{Invalid, 0, nil} +} + +func (r *LightRenderer) Pause() { + stty(fmt.Sprintf("%q", r.ostty)) + r.csi("?1049h") + r.flush() +} + +func (r *LightRenderer) Resume() bool { + stty("raw") + r.csi("?1049l") + r.flush() + // Should redraw + return true +} + +func (r *LightRenderer) Clear() { + r.csi("u") + r.csi("J") + r.flush() +} + +func (r *LightRenderer) RefreshWindows(windows []Window) { + r.flush() +} + +func (r *LightRenderer) Refresh() { + r.updateTerminalSize() +} + +func (r *LightRenderer) Close() { + r.csi("u") + r.csi("J") + if r.mouse { + r.csi("?1000l") + } + if r.upOneLine { + r.csi("A") + } + r.flush() + stty(fmt.Sprintf("%q", r.ostty)) +} + +func (r *LightRenderer) MaxX() int { + return r.width +} + +func (r *LightRenderer) MaxY() int { + return r.height +} + +func (r *LightRenderer) DoesAutoWrap() bool { + return true +} + +func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { + w := &LightWindow{ + renderer: r, + colored: r.theme != nil, + border: border, + top: top, + left: left, + width: width, + height: height, + tabstop: r.tabstop, + bg: colDefault} + if r.theme != nil { + w.bg = r.theme.Bg + } + if w.border { + w.drawBorder() + } + return w +} + +func (w *LightWindow) drawBorder() { + w.Move(0, 0) + w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐") + for y := 1; y < w.height-1; y++ { + w.Move(y, 0) + w.CPrint(ColBorder, AttrRegular, "│") + w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2)) + w.CPrint(ColBorder, AttrRegular, "│") + } + w.Move(w.height-1, 0) + w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘") +} + +func (w *LightWindow) csi(code string) { + w.renderer.csi(code) +} + +func (w *LightWindow) stderr(str string) { + w.renderer.stderr(str) +} + +func (w *LightWindow) Top() int { + return w.top +} + +func (w *LightWindow) Left() int { + return w.left +} + +func (w *LightWindow) Width() int { + return w.width +} + +func (w *LightWindow) Height() int { + return w.height +} + +func (w *LightWindow) Refresh() { +} + +func (w *LightWindow) Close() { +} + +func (w *LightWindow) X() int { + return w.posx +} + +func (w *LightWindow) Enclose(y int, x int) bool { + return x >= w.left && x < (w.left+w.width) && + y >= w.top && y < (w.top+w.height) +} + +func (w *LightWindow) Move(y int, x int) { + w.posx = x + w.posy = y + + w.csi("u") + y += w.Top() + if y > 0 { + w.csi(fmt.Sprintf("%dB", y)) + } + x += w.Left() + if x > 0 { + w.csi(fmt.Sprintf("%dC", x)) + } +} + +func (w *LightWindow) MoveAndClear(y int, x int) { + w.Move(y, x) + // We should not delete preview window on the right + // csi("K") + w.Print(repeat(" ", w.width-x)) + w.Move(y, x) +} + +func attrCodes(attr Attr) []string { + codes := []string{} + if (attr & Bold) > 0 { + codes = append(codes, "1") + } + if (attr & Dim) > 0 { + codes = append(codes, "2") + } + if (attr & Italic) > 0 { + codes = append(codes, "3") + } + if (attr & Underline) > 0 { + codes = append(codes, "4") + } + if (attr & Blink) > 0 { + codes = append(codes, "5") + } + if (attr & Reverse) > 0 { + codes = append(codes, "7") + } + return codes +} + +func colorCodes(fg Color, bg Color) []string { + codes := []string{} + appendCode := func(c Color, offset int) { + if c == colDefault { + return + } + if c.is24() { + r := (c >> 16) & 0xff + g := (c >> 8) & 0xff + b := (c) & 0xff + codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b)) + } else if c >= colBlack && c <= colWhite { + codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset)) + } else if c > colWhite && c < 16 { + codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8)) + } else if c >= 16 && c < 256 { + codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c)) + } + } + appendCode(fg, 0) + appendCode(bg, 10) + return codes +} + +func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool { + codes := append(attrCodes(attr), colorCodes(fg, bg)...) + w.csi(";" + strings.Join(codes, ";") + "m") + return len(codes) > 0 +} + +func (w *LightWindow) Print(text string) { + w.cprint2(colDefault, w.bg, AttrRegular, text) +} + +func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { + if !w.colored { + w.csiColor(colDefault, colDefault, attrFor(pair, attr)) + } else { + w.csiColor(pair.Fg(), pair.Bg(), attr) + } + w.stderr(text) + w.csi("m") +} + +func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { + if w.csiColor(fg, bg, attr) { + defer w.csi("m") + } + w.stderr(text) +} + +type wrappedLine struct { + text string + displayWidth int +} + +func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine { + lines := []wrappedLine{} + width := 0 + line := "" + for _, r := range input { + w := util.Max(util.RuneWidth(r, prefixLength+width, 8), 1) + width += w + str := string(r) + if r == '\t' { + str = repeat(" ", w) + } + if prefixLength+width <= max { + line += str + } else { + lines = append(lines, wrappedLine{string(line), width - w}) + line = str + prefixLength = 0 + width = util.RuneWidth(r, prefixLength, 8) + } + } + lines = append(lines, wrappedLine{string(line), width}) + return lines +} + +func (w *LightWindow) fill(str string, onMove func()) bool { + allLines := strings.Split(str, "\n") + for i, line := range allLines { + lines := wrapLine(line, w.posx, w.width, w.tabstop) + for j, wl := range lines { + w.stderr(wl.text) + w.posx += wl.displayWidth + if j < len(lines)-1 || i < len(allLines)-1 { + if w.posy+1 >= w.height { + return false + } + w.MoveAndClear(w.posy+1, 0) + onMove() + } + } + } + return true +} + +func (w *LightWindow) setBg() { + if w.bg != colDefault { + w.csiColor(colDefault, w.bg, AttrRegular) + } +} + +func (w *LightWindow) Fill(text string) bool { + w.MoveAndClear(w.posy, w.posx) + w.setBg() + return w.fill(text, w.setBg) +} + +func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) bool { + w.MoveAndClear(w.posy, w.posx) + if bg == colDefault { + bg = w.bg + } + if w.csiColor(fg, bg, attr) { + return w.fill(text, func() { w.csiColor(fg, bg, attr) }) + defer w.csi("m") + } + return w.fill(text, w.setBg) +} + +func (w *LightWindow) FinishFill() { + for y := w.posy + 1; y < w.height; y++ { + w.MoveAndClear(y, 0) + } +} + +func (w *LightWindow) Erase() { + if w.border { + w.drawBorder() + } + // We don't erase the window here to avoid flickering during scroll + w.Move(0, 0) +} diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 7f515b2..b160692 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -33,9 +33,39 @@ import ( "unicode/utf8" ) -type ColorPair int16 type Attr C.uint -type WindowImpl C.WINDOW + +type CursesWindow struct { + impl *C.WINDOW + top int + left int + width int + height int +} + +func (w *CursesWindow) Top() int { + return w.top +} + +func (w *CursesWindow) Left() int { + return w.left +} + +func (w *CursesWindow) Width() int { + return w.width +} + +func (w *CursesWindow) Height() int { + return w.height +} + +func (w *CursesWindow) Refresh() { + C.wnoutrefresh(w.impl) +} + +func (w *CursesWindow) FinishFill() { + // NO-OP +} const ( Bold Attr = C.A_BOLD @@ -51,31 +81,14 @@ const ( AttrRegular Attr = 0 ) -// Pallete -const ( - ColDefault ColorPair = iota - ColNormal - ColPrompt - ColMatch - ColCurrent - ColCurrentMatch - ColSpinner - ColInfo - ColCursor - ColSelected - ColHeader - ColBorder - ColUser // Should be the last entry -) - var ( _screen *C.SCREEN - _colorMap map[int]ColorPair + _colorMap map[int]int16 _colorFn func(ColorPair, Attr) (C.short, C.int) ) func init() { - _colorMap = make(map[int]ColorPair) + _colorMap = make(map[int]int16) if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") { Italic = C.A_NORMAL } @@ -85,14 +98,14 @@ func (a Attr) Merge(b Attr) Attr { return a | b } -func DefaultTheme() *ColorTheme { +func (r *FullscreenRenderer) defaultTheme() *ColorTheme { if C.tigetnum(C.CString("colors")) >= 256 { return Dark256 } return Default16 } -func Init(theme *ColorTheme, black bool, mouse bool) { +func (r *FullscreenRenderer) Init() { C.setlocale(C.LC_ALL, C.CString("")) tty := C.c_tty() if tty == nil { @@ -105,7 +118,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) { os.Exit(2) } C.set_term(_screen) - if mouse { + if r.mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mouseinterval(0) } @@ -124,14 +137,14 @@ func Init(theme *ColorTheme, black bool, mouse bool) { } C.set_escdelay(C.int(delay)) - _color = theme != nil - if _color { + if r.theme != nil { C.start_color() - InitTheme(theme, black) - initPairs(theme) - C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) + initTheme(r.theme, r.defaultTheme(), r.forceBlack) + initPairs(r.theme) + C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) _colorFn = attrColored } else { + initTheme(r.theme, nil, r.forceBlack) _colorFn = attrMono } @@ -145,39 +158,39 @@ func Init(theme *ColorTheme, black bool, mouse bool) { func initPairs(theme *ColorTheme) { C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) - initPair := func(group ColorPair, fg Color, bg Color) { - C.init_pair(C.short(group), C.short(fg), C.short(bg)) + for _, pair := range []ColorPair{ + ColNormal, + ColPrompt, + ColMatch, + ColCurrent, + ColCurrentMatch, + ColSpinner, + ColInfo, + ColCursor, + ColSelected, + ColHeader, + ColBorder} { + C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg())) } - initPair(ColNormal, theme.Fg, theme.Bg) - initPair(ColPrompt, theme.Prompt, theme.Bg) - initPair(ColMatch, theme.Match, theme.Bg) - initPair(ColCurrent, theme.Current, theme.DarkBg) - initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg) - initPair(ColSpinner, theme.Spinner, theme.Bg) - initPair(ColInfo, theme.Info, theme.Bg) - initPair(ColCursor, theme.Cursor, theme.DarkBg) - initPair(ColSelected, theme.Selected, theme.DarkBg) - initPair(ColHeader, theme.Header, theme.Bg) - initPair(ColBorder, theme.Border, theme.Bg) } -func Pause() { +func (r *FullscreenRenderer) Pause() { C.endwin() } -func Resume() bool { +func (r *FullscreenRenderer) Resume() bool { return false } -func Close() { +func (r *FullscreenRenderer) Close() { C.endwin() C.delscreen(_screen) } -func NewWindow(top int, left int, width int, height int, border bool) *Window { +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) - if _color { - C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) + if r.theme != nil { + C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) } if border { pair, attr := _colorFn(ColBorder, 0) @@ -188,66 +201,50 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window { C.wcolor_set(win, 0, nil) } - return &Window{ - impl: (*WindowImpl)(win), - Top: top, - Left: left, - Width: width, - Height: height, + return &CursesWindow{ + impl: win, + top: top, + left: left, + width: width, + height: height, } } -func attrColored(pair ColorPair, a Attr) (C.short, C.int) { - return C.short(pair), C.int(a) +func attrColored(color ColorPair, a Attr) (C.short, C.int) { + return C.short(color.index()), C.int(a) } -func attrMono(pair ColorPair, a Attr) (C.short, C.int) { - var attr C.int - switch pair { - case ColCurrent: - attr = C.A_REVERSE - case ColMatch: - attr = C.A_UNDERLINE - case ColCurrentMatch: - attr = C.A_UNDERLINE | C.A_REVERSE - } - if C.int(a)&C.A_BOLD == C.A_BOLD { - attr = attr | C.A_BOLD - } - return 0, attr +func attrMono(color ColorPair, a Attr) (C.short, C.int) { + return 0, C.int(attrFor(color, a)) } -func MaxX() int { +func (r *FullscreenRenderer) MaxX() int { return int(C.COLS) } -func MaxY() int { +func (r *FullscreenRenderer) MaxY() int { return int(C.LINES) } -func (w *Window) win() *C.WINDOW { - return (*C.WINDOW)(w.impl) +func (w *CursesWindow) Close() { + C.delwin(w.impl) } -func (w *Window) Close() { - C.delwin(w.win()) +func (w *CursesWindow) Enclose(y int, x int) bool { + return bool(C.wenclose(w.impl, C.int(y), C.int(x))) } -func (w *Window) Enclose(y int, x int) bool { - return bool(C.wenclose(w.win(), C.int(y), C.int(x))) +func (w *CursesWindow) Move(y int, x int) { + C.wmove(w.impl, C.int(y), C.int(x)) } -func (w *Window) Move(y int, x int) { - C.wmove(w.win(), C.int(y), C.int(x)) -} - -func (w *Window) MoveAndClear(y int, x int) { +func (w *CursesWindow) MoveAndClear(y int, x int) { w.Move(y, x) - C.wclrtoeol(w.win()) + C.wclrtoeol(w.impl) } -func (w *Window) Print(text string) { - C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune { +func (w *CursesWindow) Print(text string) { + C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune { if r < 32 { return -1 } @@ -255,69 +252,74 @@ func (w *Window) Print(text string) { }, text))) } -func (w *Window) CPrint(pair ColorPair, attr Attr, text string) { - p, a := _colorFn(pair, attr) - C.wcolor_set(w.win(), p, nil) - C.wattron(w.win(), a) +func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) { + p, a := _colorFn(color, attr) + C.wcolor_set(w.impl, p, nil) + C.wattron(w.impl, a) w.Print(text) - C.wattroff(w.win(), a) - C.wcolor_set(w.win(), 0, nil) + C.wattroff(w.impl, a) + C.wcolor_set(w.impl, 0, nil) } -func Clear() { +func (r *FullscreenRenderer) Clear() { C.clear() C.endwin() } -func Refresh() { +func (r *FullscreenRenderer) Refresh() { C.refresh() } -func (w *Window) Erase() { - C.werase(w.win()) +func (w *CursesWindow) Erase() { + C.werase(w.impl) } -func (w *Window) X() int { - return int(C.c_getcurx(w.win())) +func (w *CursesWindow) X() int { + return int(C.c_getcurx(w.impl)) } -func DoesAutoWrap() bool { +func (r *FullscreenRenderer) DoesAutoWrap() bool { return true } -func (w *Window) Fill(str string) bool { - return C.waddstr(w.win(), C.CString(str)) == C.OK +func (w *CursesWindow) Fill(str string) bool { + return C.waddstr(w.impl, C.CString(str)) == C.OK } -func (w *Window) CFill(str string, fg Color, bg Color, attr Attr) bool { - pair := PairFor(fg, bg) - C.wcolor_set(w.win(), C.short(pair), nil) - C.wattron(w.win(), C.int(attr)) +func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) bool { + index := ColorPair{fg, bg, -1}.index() + C.wcolor_set(w.impl, C.short(index), nil) + C.wattron(w.impl, C.int(attr)) ret := w.Fill(str) - C.wattroff(w.win(), C.int(attr)) - C.wcolor_set(w.win(), 0, nil) + C.wattroff(w.impl, C.int(attr)) + C.wcolor_set(w.impl, 0, nil) return ret } -func RefreshWindows(windows []*Window) { +func (r *FullscreenRenderer) RefreshWindows(windows []Window) { for _, w := range windows { - C.wnoutrefresh(w.win()) + w.Refresh() } C.doupdate() } -func PairFor(fg Color, bg Color) ColorPair { - // ncurses does not support 24-bit colors - if fg.is24() || bg.is24() { - return ColDefault +func (p ColorPair) index() int16 { + if p.id >= 0 { + return p.id } - key := (int(fg) << 8) + int(bg) + + // ncurses does not support 24-bit colors + if p.is24() { + return ColDefault.index() + } + + key := p.key() if found, prs := _colorMap[key]; prs { return found } - id := ColorPair(len(_colorMap) + int(ColUser)) - C.init_pair(C.short(id), C.short(fg), C.short(bg)) + id := int16(len(_colorMap)) + ColUser.id + C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg())) _colorMap[key] = id return id } @@ -369,7 +371,7 @@ func escSequence() Event { return Event{Invalid, 0, nil} } -func GetChar() Event { +func (r *FullscreenRenderer) GetChar() Event { c := C.getch() switch c { case C.ERR: @@ -435,17 +437,17 @@ func GetChar() Event { /* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */ if (me.bstate & C.BUTTON1_PRESSED) > 0 { now := time.Now() - if now.Sub(_prevDownTime) < doubleClickDuration { - _clickY = append(_clickY, y) + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, y) } else { - _clickY = []int{y} - _prevDownTime = now + r.clickY = []int{y} + r.prevDownTime = now } return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}} } else if (me.bstate & C.BUTTON1_RELEASED) > 0 { double := false - if len(_clickY) > 1 && _clickY[0] == _clickY[1] && - time.Now().Sub(_prevDownTime) < doubleClickDuration { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { double = true } return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}} diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 1793836..460bfd5 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -18,30 +18,56 @@ import ( "github.com/junegunn/go-runewidth" ) -type ColorPair [2]Color - -func (p ColorPair) fg() Color { - return p[0] -} - -func (p ColorPair) bg() Color { - return p[1] -} - func (p ColorPair) style() tcell.Style { style := tcell.StyleDefault - return style.Foreground(tcell.Color(p.fg())).Background(tcell.Color(p.bg())) + return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) } type Attr tcell.Style -type WindowTcell struct { - LastX int - LastY int - MoveCursor bool - Border bool +type TcellWindow struct { + color bool + top int + left int + width int + height int + lastX int + lastY int + moveCursor bool + border bool +} + +func (w *TcellWindow) Top() int { + return w.top +} + +func (w *TcellWindow) Left() int { + return w.left +} + +func (w *TcellWindow) Width() int { + return w.width +} + +func (w *TcellWindow) Height() int { + return w.height +} + +func (w *TcellWindow) Refresh() { + if w.moveCursor { + _screen.ShowCursor(w.left+w.lastX, w.top+w.lastY) + w.moveCursor = false + } + w.lastX = 0 + w.lastY = 0 + if w.border { + w.drawBorder() + } +} + +func (w *TcellWindow) FinishFill() { + // NO-OP } -type WindowImpl WindowTcell const ( Bold Attr = Attr(tcell.AttrBold) @@ -56,33 +82,13 @@ const ( AttrRegular Attr = 0 ) -var ( - ColDefault = ColorPair{colDefault, colDefault} - ColNormal ColorPair - ColPrompt ColorPair - ColMatch ColorPair - ColCurrent ColorPair - ColCurrentMatch ColorPair - ColSpinner ColorPair - ColInfo ColorPair - ColCursor ColorPair - ColSelected ColorPair - ColHeader ColorPair - ColBorder ColorPair - ColUser ColorPair -) - -func DefaultTheme() *ColorTheme { +func (r *FullscreenRenderer) defaultTheme() *ColorTheme { if _screen.Colors() >= 256 { return Dark256 } return Default16 } -func PairFor(fg Color, bg Color) ColorPair { - return [2]Color{fg, bg} -} - var ( _colorToAttribute = []tcell.Color{ tcell.ColorBlack, @@ -112,10 +118,9 @@ func (a Attr) Merge(b Attr) Attr { var ( _screen tcell.Screen - _mouse bool ) -func initScreen() { +func (r *FullscreenRenderer) initScreen() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) @@ -125,7 +130,7 @@ func initScreen() { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(2) } - if _mouse { + if r.mouse { s.EnableMouse() } else { s.DisableMouse() @@ -133,63 +138,41 @@ func initScreen() { _screen = s } -func Init(theme *ColorTheme, black bool, mouse bool) { +func (r *FullscreenRenderer) Init() { encoding.Register() - _mouse = mouse - initScreen() - - _color = theme != nil - if _color { - InitTheme(theme, black) - } else { - theme = DefaultTheme() - } - ColNormal = ColorPair{theme.Fg, theme.Bg} - ColPrompt = ColorPair{theme.Prompt, theme.Bg} - ColMatch = ColorPair{theme.Match, theme.Bg} - ColCurrent = ColorPair{theme.Current, theme.DarkBg} - ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg} - ColSpinner = ColorPair{theme.Spinner, theme.Bg} - ColInfo = ColorPair{theme.Info, theme.Bg} - ColCursor = ColorPair{theme.Cursor, theme.DarkBg} - ColSelected = ColorPair{theme.Selected, theme.DarkBg} - ColHeader = ColorPair{theme.Header, theme.Bg} - ColBorder = ColorPair{theme.Border, theme.Bg} + r.initScreen() + initTheme(r.theme, r.defaultTheme(), r.forceBlack) } -func MaxX() int { +func (r *FullscreenRenderer) MaxX() int { ncols, _ := _screen.Size() return int(ncols) } -func MaxY() int { +func (r *FullscreenRenderer) MaxY() int { _, nlines := _screen.Size() return int(nlines) } -func (w *Window) win() *WindowTcell { - return (*WindowTcell)(w.impl) +func (w *TcellWindow) X() int { + return w.lastX } -func (w *Window) X() int { - return w.impl.LastX -} - -func DoesAutoWrap() bool { +func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } -func Clear() { +func (r *FullscreenRenderer) Clear() { _screen.Sync() _screen.Clear() } -func Refresh() { +func (r *FullscreenRenderer) Refresh() { // noop } -func GetChar() Event { +func (r *FullscreenRenderer) GetChar() Event { ev := _screen.PollEvent() switch ev := ev.(type) { case *tcell.EventResize: @@ -213,15 +196,15 @@ func GetChar() Event { double := false if down { now := time.Now() - if now.Sub(_prevDownTime) < doubleClickDuration { - _clickY = append(_clickY, x) + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, x) } else { - _clickY = []int{x} - _prevDownTime = now + r.clickY = []int{x} + r.prevDownTime = now } } else { - if len(_clickY) > 1 && _clickY[0] == _clickY[1] && - time.Now().Sub(_prevDownTime) < doubleClickDuration { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { double = true } } @@ -368,49 +351,39 @@ func GetChar() Event { return Event{Invalid, 0, nil} } -func Pause() { +func (r *FullscreenRenderer) Pause() { _screen.Fini() } -func Resume() bool { - initScreen() +func (r *FullscreenRenderer) Resume() bool { + r.initScreen() return true } -func Close() { +func (r *FullscreenRenderer) Close() { _screen.Fini() } -func RefreshWindows(windows []*Window) { +func (r *FullscreenRenderer) RefreshWindows(windows []Window) { // TODO for _, w := range windows { - if w.win().MoveCursor { - _screen.ShowCursor(w.Left+w.win().LastX, w.Top+w.win().LastY) - w.win().MoveCursor = false - } - w.win().LastX = 0 - w.win().LastY = 0 - if w.win().Border { - w.DrawBorder() - } + w.Refresh() } _screen.Show() } -func NewWindow(top int, left int, width int, height int, border bool) *Window { +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { // TODO - win := new(WindowTcell) - win.Border = border - return &Window{ - impl: (*WindowImpl)(win), - Top: top, - Left: left, - Width: width, - Height: height, - } + return &TcellWindow{ + color: r.theme != nil, + top: top, + left: left, + width: width, + height: height, + border: border} } -func (w *Window) Close() { +func (w *TcellWindow) Close() { // TODO } @@ -422,40 +395,40 @@ func fill(x, y, w, h int, r rune) { } } -func (w *Window) Erase() { +func (w *TcellWindow) Erase() { // TODO - fill(w.Left, w.Top, w.Width, w.Height, ' ') + fill(w.left, w.top, w.width, w.height, ' ') } -func (w *Window) Enclose(y int, x int) bool { - return x >= w.Left && x <= (w.Left+w.Width) && - y >= w.Top && y <= (w.Top+w.Height) +func (w *TcellWindow) Enclose(y int, x int) bool { + return x >= w.left && x < (w.left+w.width) && + y >= w.top && y < (w.top+w.height) } -func (w *Window) Move(y int, x int) { - w.win().LastX = x - w.win().LastY = y - w.win().MoveCursor = true +func (w *TcellWindow) Move(y int, x int) { + w.lastX = x + w.lastY = y + w.moveCursor = true } -func (w *Window) MoveAndClear(y int, x int) { +func (w *TcellWindow) MoveAndClear(y int, x int) { w.Move(y, x) - for i := w.win().LastX; i < w.Width; i++ { - _screen.SetContent(i+w.Left, w.win().LastY+w.Top, rune(' '), nil, ColDefault.style()) + for i := w.lastX; i < w.width; i++ { + _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style()) } - w.win().LastX = x + w.lastX = x } -func (w *Window) Print(text string) { - w.PrintString(text, ColDefault, 0) +func (w *TcellWindow) Print(text string) { + w.printString(text, ColDefault, 0) } -func (w *Window) PrintString(text string, pair ColorPair, a Attr) { +func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { t := text lx := 0 var style tcell.Style - if _color { + if w.color { style = pair.style(). Reverse(a&Attr(tcell.AttrReverse) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0) @@ -481,7 +454,7 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) { } if r == '\n' { - w.win().LastY++ + w.lastY++ lx = 0 } else { @@ -489,26 +462,26 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) { continue } - var xPos = w.Left + w.win().LastX + lx - var yPos = w.Top + w.win().LastY - if xPos < (w.Left+w.Width) && yPos < (w.Top+w.Height) { + var xPos = w.left + w.lastX + lx + var yPos = w.top + w.lastY + if xPos < (w.left+w.width) && yPos < (w.top+w.height) { _screen.SetContent(xPos, yPos, r, nil, style) } lx += runewidth.RuneWidth(r) } } - w.win().LastX += lx + w.lastX += lx } -func (w *Window) CPrint(pair ColorPair, a Attr, text string) { - w.PrintString(text, pair, a) +func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) { + w.printString(text, pair, attr) } -func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { +func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { lx := 0 var style tcell.Style - if _color { + if w.color { style = pair.style() } else { style = ColDefault.style() @@ -522,22 +495,22 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { for _, r := range text { if r == '\n' { - w.win().LastY++ - w.win().LastX = 0 + w.lastY++ + w.lastX = 0 lx = 0 } else { - var xPos = w.Left + w.win().LastX + lx + var xPos = w.left + w.lastX + lx // word wrap: - if xPos >= (w.Left + w.Width) { - w.win().LastY++ - w.win().LastX = 0 + if xPos >= (w.left + w.width) { + w.lastY++ + w.lastX = 0 lx = 0 - xPos = w.Left + xPos = w.left } - var yPos = w.Top + w.win().LastY + var yPos = w.top + w.lastY - if yPos >= (w.Top + w.Height) { + if yPos >= (w.top + w.height) { return false } @@ -545,27 +518,27 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { lx += runewidth.RuneWidth(r) } } - w.win().LastX += lx + w.lastX += lx return true } -func (w *Window) Fill(str string) bool { - return w.FillString(str, ColDefault, 0) +func (w *TcellWindow) Fill(str string) bool { + return w.fillString(str, ColDefault, 0) } -func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { - return w.FillString(str, ColorPair{fg, bg}, a) +func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) bool { + return w.fillString(str, ColorPair{fg, bg, -1}, a) } -func (w *Window) DrawBorder() { - left := w.Left - right := left + w.Width - top := w.Top - bot := top + w.Height +func (w *TcellWindow) drawBorder() { + left := w.left + right := left + w.width + top := w.top + bot := top + w.height var style tcell.Style - if _color { + if w.color { style = ColBorder.style() } else { style = ColDefault.style() diff --git a/src/tui/tui.go b/src/tui/tui.go index 125611c..859eed7 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -115,6 +115,32 @@ const ( colWhite ) +type ColorPair struct { + fg Color + bg Color + id int16 +} + +func NewColorPair(fg Color, bg Color) ColorPair { + return ColorPair{fg, bg, -1} +} + +func (p ColorPair) Fg() Color { + return p.fg +} + +func (p ColorPair) Bg() Color { + return p.bg +} + +func (p ColorPair) key() int { + return (int(p.Fg()) << 8) + int(p.Bg()) +} + +func (p ColorPair) is24() bool { + return p.Fg().is24() || p.Bg().is24() +} + type ColorTheme struct { Fg Color Bg Color @@ -146,23 +172,84 @@ type MouseEvent struct { Mod bool } -var ( - _color bool - _prevDownTime time.Time - _clickY []int - Default16 *ColorTheme - Dark256 *ColorTheme - Light256 *ColorTheme -) +type Renderer interface { + Init() + Pause() + Resume() bool + Clear() + RefreshWindows(windows []Window) + Refresh() + Close() -type Window struct { - impl *WindowImpl - Top int - Left int - Width int - Height int + GetChar() Event + + MaxX() int + MaxY() int + DoesAutoWrap() bool + + NewWindow(top int, left int, width int, height int, border bool) Window } +type Window interface { + Top() int + Left() int + Width() int + Height() int + + Refresh() + FinishFill() + Close() + + X() int + Enclose(y int, x int) bool + + Move(y int, x int) + MoveAndClear(y int, x int) + Print(text string) + CPrint(color ColorPair, attr Attr, text string) + Fill(text string) bool + CFill(fg Color, bg Color, attr Attr, text string) bool + Erase() +} + +type FullscreenRenderer struct { + theme *ColorTheme + mouse bool + forceBlack bool + prevDownTime time.Time + clickY []int +} + +func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { + r := &FullscreenRenderer{ + theme: theme, + mouse: mouse, + forceBlack: forceBlack, + prevDownTime: time.Unix(0, 0), + clickY: []int{}} + return r +} + +var ( + Default16 *ColorTheme + Dark256 *ColorTheme + Light256 *ColorTheme + + ColDefault ColorPair + ColNormal ColorPair + ColPrompt ColorPair + ColMatch ColorPair + ColCurrent ColorPair + ColCurrentMatch ColorPair + ColSpinner ColorPair + ColInfo ColorPair + ColCursor ColorPair + ColSelected ColorPair + ColHeader ColorPair + ColBorder ColorPair + ColUser ColorPair +) + func EmptyTheme() *ColorTheme { return &ColorTheme{ Fg: colUndefined, @@ -181,8 +268,6 @@ func EmptyTheme() *ColorTheme { } func init() { - _prevDownTime = time.Unix(0, 0) - _clickY = []int{} Default16 = &ColorTheme{ Fg: colDefault, Bg: colDefault, @@ -227,14 +312,13 @@ func init() { Border: 145} } -func InitTheme(theme *ColorTheme, black bool) { - _color = theme != nil - if !_color { +func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { + if theme == nil { + initPalette(theme) return } - baseTheme := DefaultTheme() - if black { + if forceBlack { theme.Bg = colBlack } @@ -257,4 +341,48 @@ func InitTheme(theme *ColorTheme, black bool) { theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Header = o(baseTheme.Header, theme.Header) theme.Border = o(baseTheme.Border, theme.Border) + + initPalette(theme) +} + +func initPalette(theme *ColorTheme) { + ColDefault = ColorPair{colDefault, colDefault, 0} + if theme != nil { + ColNormal = ColorPair{theme.Fg, theme.Bg, 1} + ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2} + ColMatch = ColorPair{theme.Match, theme.Bg, 3} + ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4} + ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5} + ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6} + ColInfo = ColorPair{theme.Info, theme.Bg, 7} + ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8} + ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9} + ColHeader = ColorPair{theme.Header, theme.Bg, 10} + ColBorder = ColorPair{theme.Border, theme.Bg, 11} + } else { + ColNormal = ColorPair{colDefault, colDefault, 1} + ColPrompt = ColorPair{colDefault, colDefault, 2} + ColMatch = ColorPair{colDefault, colDefault, 3} + ColCurrent = ColorPair{colDefault, colDefault, 4} + ColCurrentMatch = ColorPair{colDefault, colDefault, 5} + ColSpinner = ColorPair{colDefault, colDefault, 6} + ColInfo = ColorPair{colDefault, colDefault, 7} + ColCursor = ColorPair{colDefault, colDefault, 8} + ColSelected = ColorPair{colDefault, colDefault, 9} + ColHeader = ColorPair{colDefault, colDefault, 10} + ColBorder = ColorPair{colDefault, colDefault, 11} + } + ColUser = ColorPair{colDefault, colDefault, 12} +} + +func attrFor(color ColorPair, attr Attr) Attr { + switch color { + case ColCurrent: + return attr | Reverse + case ColMatch: + return attr | Underline + case ColCurrentMatch: + return attr | Underline | Reverse + } + return attr } diff --git a/src/tui/tui_test.go b/src/tui/tui_test.go deleted file mode 100644 index 4a2fee9..0000000 --- a/src/tui/tui_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package tui - -import ( - "testing" -) - -func TestPairFor(t *testing.T) { - if PairFor(30, 50) != PairFor(30, 50) { - t.Fail() - } - if PairFor(-1, 10) != PairFor(-1, 10) { - t.Fail() - } -} diff --git a/src/util/util.go b/src/util/util.go index 2a1607c..29e8017 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -6,8 +6,24 @@ import ( "time" "github.com/junegunn/go-isatty" + "github.com/junegunn/go-runewidth" ) +var _runeWidths = make(map[rune]int) + +// RuneWidth returns rune width +func RuneWidth(r rune, prefixWidth int, tabstop 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 + } +} + // Max returns the largest integer func Max(first int, second int) int { if first >= second { diff --git a/src/util/util_unix.go b/src/util/util_unix.go index 29e0d30..bc1b7b5 100644 --- a/src/util/util_unix.go +++ b/src/util/util_unix.go @@ -5,6 +5,7 @@ package util import ( "os" "os/exec" + "syscall" ) // ExecCommand executes the given command with $SHELL @@ -20,3 +21,8 @@ func ExecCommand(command string) *exec.Cmd { func IsWindows() bool { return false } + +// SetNonBlock executes syscall.SetNonblock on file descriptor +func SetNonblock(file *os.File, nonblock bool) { + syscall.SetNonblock(int(file.Fd()), nonblock) +} diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 3aa8660..9ba4f79 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -5,6 +5,7 @@ package util import ( "os" "os/exec" + "syscall" "github.com/junegunn/go-shellwords" ) @@ -26,3 +27,8 @@ func ExecCommand(command string) *exec.Cmd { func IsWindows() bool { return true } + +// SetNonBlock executes syscall.SetNonblock on file descriptor +func SetNonblock(file *os.File, nonblock bool) { + syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock) +} diff --git a/test/test_go.rb b/test/test_go.rb index 4cd4a4f..55422aa 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -117,8 +117,28 @@ class Tmux wait do lines = capture(pane) class << lines + def counts + self.lazy + .map { |l| l.scan /^. ([0-9]+)\/([0-9]+)( \(([0-9]+)\))?/ } + .reject(&:empty?) + .first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0] + end + + def match_count + counts[0] + end + def item_count - self[-2] ? self[-2].strip.split('/').last.to_i : 0 + counts[1] + end + + def select_count + counts[2] + end + + def any_include? val + method = val.is_a?(Regexp) ? :match : :include? + self.select { |line| line.send method, val }.first end end yield lines @@ -163,6 +183,11 @@ class TestBase < Minitest::Test @temp_suffix].join '-' end + def writelines path, lines + File.unlink path while File.exists? path + File.open(path, 'w') { |f| f << lines.join($/) + $/ } + end + def readonce wait { File.exists?(tempname) } File.read(tempname) @@ -1190,12 +1215,6 @@ class TestGoFZF < TestBase tmux.send_keys '?' tmux.until { |lines| lines[-1] == '> 555' } end - -private - def writelines path, lines - File.unlink path while File.exists? path - File.open(path, 'w') { |f| f << lines.join($/) + $/ } - end end module TestShell @@ -1213,79 +1232,60 @@ module TestShell tmux.prepare end - def test_ctrl_t + def unset_var name + tmux.prepare + tmux.send_keys "unset #{name}", :Enter tmux.prepare - tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(1) { |lines| lines.item_count > 1 } - expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') - tmux.send_keys :BTab, :BTab, pane: 1 - tmux.until(1) { |lines| lines[-2].include?('(2)') } - tmux.send_keys :Enter, pane: 1 - tmux.until(0) { |lines| lines[-1].include? expected } - tmux.send_keys 'C-c' - - # FZF_TMUX=0 - new_shell - tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(0) { |lines| lines.item_count > 1 } - expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') - tmux.send_keys :BTab, :BTab, pane: 0 - tmux.until(0) { |lines| lines[-2].include?('(2)') } - tmux.send_keys :Enter, pane: 0 - tmux.until(0) { |lines| lines[-1].include? expected } - tmux.send_keys 'C-c', 'C-d' end - def test_ctrl_t_command + def test_ctrl_t set_var "FZF_CTRL_T_COMMAND", "seq 100" - tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(1) { |lines| lines.item_count == 100 } - tmux.send_keys :BTab, :BTab, :BTab, pane: 1 - tmux.until(1) { |lines| lines[-2].include?('(3)') } - tmux.send_keys :Enter, pane: 1 - tmux.until(0) { |lines| lines[-1].include? '1 2 3' } + + tmux.prepare + tmux.send_keys 'C-t' + lines = tmux.until { |lines| lines.item_count == 100 } + tmux.send_keys :Tab, :Tab, :Tab + tmux.until { |lines| lines.any_include? ' (3)' } + tmux.send_keys :Enter + tmux.until { |lines| lines.any_include? '1 2 3' } + tmux.send_keys 'C-c' end def test_ctrl_t_unicode - FileUtils.mkdir_p '/tmp/fzf-test' - tmux.paste 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"' + writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2'] + set_var "FZF_CTRL_T_COMMAND", "cat #{tempname}" + tmux.prepare - tmux.send_keys 'cat ', 'C-t', pane: 0 - tmux.until(1) { |lines| lines.item_count >= 1 } - tmux.send_keys 'fzf-unicode', pane: 1 - redraw = ->() { tmux.send_keys 'C-l', pane: 1 } - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } + tmux.send_keys 'echo ', 'C-t' + tmux.until { |lines| lines.item_count == 2 } + tmux.send_keys 'fzf-unicode' + tmux.until { |lines| lines.match_count == 2 } - tmux.send_keys '1', pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } - tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' } + tmux.send_keys '1' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Tab + tmux.until { |lines| lines.select_count == 1 } - tmux.send_keys :BSpace, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } + tmux.send_keys :BSpace + tmux.until { |lines| lines.match_count == 2 } - tmux.send_keys '2', pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } - tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' } + tmux.send_keys '2' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Tab + tmux.until { |lines| lines.select_count == 2 } - tmux.send_keys :Enter, pane: 1 - tmux.until do |lines| - tmux.send_keys 'C-l' - [-1, -2].map { |offset| lines[offset] }.any? do |line| - line.start_with?('cat') && line.include?('fzf-unicode') - end - end tmux.send_keys :Enter - tmux.until { |lines| lines[-1].include? 'test1test2' } + tmux.until { |lines| lines.any_include? /echo.*fzf-unicode.*1.*fzf-unicode.*2/ } + tmux.send_keys :Enter + tmux.until { |lines| lines.any_include? /^fzf-unicode.*1.*fzf-unicode.*2/ } end def test_alt_c tmux.prepare - tmux.send_keys :Escape, :c, pane: 0 - lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] } - expected = lines[-3][2..-1] - tmux.send_keys :Enter, pane: 1 + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| lines.item_count > 0 } + expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1] + tmux.send_keys :Enter tmux.prepare tmux.send_keys :pwd, :Enter tmux.until { |lines| lines[-1].end_with?(expected) } @@ -1298,9 +1298,9 @@ module TestShell tmux.send_keys 'cd /', :Enter tmux.prepare - tmux.send_keys :Escape, :c, pane: 0 - lines = tmux.until(1) { |lines| lines.item_count == 1 } - tmux.send_keys :Enter, pane: 1 + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| lines.item_count == 1 } + tmux.send_keys :Enter tmux.prepare tmux.send_keys :pwd, :Enter @@ -1314,11 +1314,11 @@ module TestShell tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter; tmux.prepare - tmux.send_keys 'C-r', pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys '3d', pane: 1 - tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort - tmux.send_keys :Enter, pane: 1 + tmux.send_keys 'C-r' + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys '3d' + tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } + tmux.send_keys :Enter tmux.until { |lines| lines[-1] == 'echo 3rd' } tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '3rd' } @@ -1334,12 +1334,12 @@ module CompletionTest FileUtils.touch File.expand_path(f) end tmux.prepare - tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab + tmux.until { |lines| lines.item_count > 0 } tmux.send_keys ' !d' - tmux.until(1) { |lines| lines[-2].include?(' 2/') } - tmux.send_keys :BTab, :BTab - tmux.until(1) { |lines| lines[-2].include?('(2)') } + tmux.until { |lines| lines.match_count == 2 } + tmux.send_keys :Tab, :Tab + tmux.until { |lines| lines.select_count == 2 } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1349,10 +1349,10 @@ module CompletionTest # ~USERNAME** tmux.send_keys 'C-u' - tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys '.fzf-home' - tmux.until(1) { |lines| lines[-3].end_with? '.fzf-home' } + tmux.send_keys "cat ~#{ENV['USER']}**", :Tab + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys "'.fzf-home" + tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1361,8 +1361,8 @@ module CompletionTest # ~INVALID_USERNAME** tmux.send_keys 'C-u' - tmux.send_keys "cat ~such**", :Tab, pane: 0 - tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' } + tmux.send_keys "cat ~such**", :Tab + tmux.until { |lines| lines.any_include? 'no~such~user' } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1371,9 +1371,11 @@ module CompletionTest # /tmp/fzf\ test** tmux.send_keys 'C-u' - tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys 'C-K', :Enter + tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys 'foobar$' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' lines[-1].end_with?('/tmp/fzf\ test/foobar') @@ -1382,11 +1384,10 @@ module CompletionTest # Should include hidden files (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } tmux.send_keys 'C-u' - tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab, pane: 0 - tmux.until(1) do |lines| + tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab + tmux.until do |lines| tmux.send_keys 'C-L' - lines[-2].include?('100/') && - lines[-3].include?('/tmp/fzf-test/.hidden-') + lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') end tmux.send_keys :Enter ensure @@ -1396,19 +1397,22 @@ module CompletionTest end def test_file_completion_root - tmux.send_keys 'ls /**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys 'ls /**', :Tab + tmux.until { |lines| lines.item_count > 0 } tmux.send_keys :Enter end def test_dir_completion - tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter + (1..100).each do |idx| + FileUtils.mkdir_p "/tmp/fzf-test/d#{idx}" + end + FileUtils.touch '/tmp/fzf-test/d55/xxx' tmux.prepare - tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys :BTab, :BTab # BTab does not work here + tmux.send_keys 'cd /tmp/fzf-test/**', :Tab + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys :Tab, :Tab # Tab does not work here tmux.send_keys 55 - tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1435,14 +1439,15 @@ module CompletionTest lines = tmux.until { |lines| lines[-1].start_with? '[1]' } pid = lines[-1].split.last tmux.prepare - tmux.send_keys 'kill ', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys 'C-L' + tmux.send_keys 'kill ', :Tab + tmux.until { |lines| lines.item_count > 0 } tmux.send_keys 'sleep12345' - tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' } + tmux.until { |lines| lines.any_include? 'sleep 12345' } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' - lines[-1] == "kill #{pid}" + lines[-1].include? "kill #{pid}" end ensure Process.kill 'KILL', pid.to_i rescue nil if pid @@ -1451,10 +1456,10 @@ module CompletionTest def test_custom_completion tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter tmux.prepare - tmux.send_keys 'ls /tmp/**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count == 11 } - tmux.send_keys :BTab, :BTab, :BTab - tmux.until(1) { |lines| lines[-2].include? '(3)' } + tmux.send_keys 'ls /tmp/**', :Tab + tmux.until { |lines| lines.item_count == 11 } + tmux.send_keys :Tab, :Tab, :Tab + tmux.until { |lines| lines.select_count == 3 } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1463,49 +1468,48 @@ module CompletionTest end def test_unset_completion - tmux.send_keys 'export FOO=BAR', :Enter + tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter tmux.prepare # Using tmux - tmux.send_keys 'unset FOO**', :Tab, pane: 0 - tmux.until(1) { |lines| lines[-2].include? ' 1/' } + tmux.send_keys 'unset FZFFOO**', :Tab + tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter - tmux.until { |lines| lines[-1] == 'unset FOO' } + tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } tmux.send_keys 'C-c' - # FZF_TMUX=0 + # FZF_TMUX=1 new_shell - tmux.send_keys 'unset FOO**', :Tab - tmux.until { |lines| lines[-2].include? ' 1/' } + tmux.send_keys 'unset FZFFO**', :Tab, pane: 0 + tmux.until(1) { |lines| lines.match_count == 1 } tmux.send_keys :Enter - tmux.until { |lines| lines[-1] == 'unset FOO' } + tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } end def test_file_completion_unicode FileUtils.mkdir_p '/tmp/fzf-test' tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"' tmux.prepare - tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 - redraw = ->() { tmux.send_keys 'C-l', pane: 1 } - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } + tmux.send_keys 'cat fzf-unicode**', :Tab + tmux.until { |lines| lines.match_count == 2 } - tmux.send_keys '1', pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } - tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' } + tmux.send_keys '1' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Tab + tmux.until { |lines| lines.select_count == 1 } - tmux.send_keys :BSpace, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } + tmux.send_keys :BSpace + tmux.until { |lines| lines.match_count == 2 } - tmux.send_keys '2', pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } - tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' } + tmux.send_keys '2' + tmux.until { |lines| lines.select_count == 1 } + tmux.send_keys :Tab + tmux.until { |lines| lines.select_count == 2 } - tmux.send_keys :Enter, pane: 1 + tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-l' - lines[-1].include?('cat') || lines[-2].include?('cat') + lines.any_include? 'cat' end tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test3test4' } @@ -1518,7 +1522,7 @@ class TestBash < TestBase def new_shell tmux.prepare - tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter + tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter tmux.prepare end @@ -1533,7 +1537,7 @@ class TestZsh < TestBase include CompletionTest def new_shell - tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter + tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter tmux.prepare end @@ -1547,7 +1551,7 @@ class TestFish < TestBase include TestShell def new_shell - tmux.send_keys 'env FZF_TMUX=0 fish', :Enter + tmux.send_keys 'env FZF_TMUX=1 fish', :Enter tmux.send_keys 'function fish_prompt; end; clear', :Enter tmux.until { |lines| lines.empty? } end From 1a50f1eca1ce81da30cdf47291a485aea1a6431a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Jan 2017 02:06:39 +0900 Subject: [PATCH 02/28] [vim] Use --height instead of fzf-tmux --- plugin/fzf.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index c8c6c91..9c9c6db 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -298,7 +298,8 @@ try endif let use_height = has_key(dict, 'down') && - \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) + \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) && + \ executable('tput') && filereadable('/dev/tty') 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 From a30999a78528b3c773b86ca5bd21a0220116c9de Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Jan 2017 02:09:31 +0900 Subject: [PATCH 03/28] Prepare for 0.16.0 release --- README.md | 11 ++++------- install | 13 +++++++------ man/man1/fzf-tmux.1 | 4 ++-- man/man1/fzf.1 | 4 ++-- src/constants.go | 2 +- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 550ea77..de69d01 100644 --- a/README.md +++ b/README.md @@ -496,12 +496,9 @@ valid directory. Example: set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" ``` -License -------- +[License](LICENSE) +------------------ -[MIT](LICENSE) +The MIT License (MIT) -Author ------- - -Junegunn Choi +Copyright (c) 2017 Junegunn Choi diff --git a/install b/install index 58a42e3..7a2481f 100755 --- a/install +++ b/install @@ -2,9 +2,7 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.9 pre=1 || - version=0.15.9 pre=0 - +version=0.16.0-alpha auto_completion= key_bindings= update_config=2 @@ -48,7 +46,7 @@ for opt in "$@"; do --no-update-rc) update_config=0 ;; --32) binary_arch=386 ;; --64) binary_arch=amd64 ;; - --bin|--pre) ;; + --bin) ;; *) echo "unknown option: $opt" help @@ -121,7 +119,7 @@ try_wget() { download() { echo "Downloading bin/fzf ..." - if [ $pre = 0 ]; then + if [[ ! "$version" =~ alpha ]]; then if [ -x "$fzf_base"/bin/fzf ]; then echo " - Already exists" check_binary && return @@ -137,7 +135,10 @@ download() { return fi - local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz + local url + [[ "$version" =~ alpha ]] && + url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1}.tgz || + url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz set -o pipefail if ! (try_curl $url || try_wget $url); then set +o pipefail diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 6f21139..6ccdb66 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -1,7 +1,7 @@ .ig The MIT License (MIT) -Copyright (c) 2016 Junegunn Choi +Copyright (c) 2017 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -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 "Nov 2016" "fzf 0.15.9" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.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 fac2aab..b6540e1 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1,7 +1,7 @@ .ig The MIT License (MIT) -Copyright (c) 2016 Junegunn Choi +Copyright (c) 2017 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -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 "Nov 2016" "fzf 0.15.9" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jan 2017" "fzf 0.16.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index b9def74..9640dbe 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.9" + version = "0.16.0-alpha" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 9d545f9578b7cf154543bb645cb6da0f01801e5d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Jan 2017 02:29:31 +0900 Subject: [PATCH 04/28] Fix update of multi-select pointer --- src/terminal.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 8002923..c653327 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -41,9 +41,10 @@ type previewer struct { } type itemLine struct { - current bool - label string - result Result + current bool + selected bool + label string + result Result } var emptyLine = itemLine{} @@ -663,7 +664,7 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } // Avoid unnecessary redraw - newLine := itemLine{current, label, *result} + newLine := itemLine{current, selected, label, *result} if t.prevLines[i] == newLine { return } From 45793d75c2d168d51be3578b8d785159ec81ff92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 03:12:23 +0900 Subject: [PATCH 05/28] Add --normalize option to normalize latin script characters Close #790 --- CHANGELOG.md | 2 + man/man1/fzf.1 | 4 + src/algo/algo.go | 119 ++++++++++-- src/algo/algo_test.go | 21 ++- src/algo/normalize.go | 408 ++++++++++++++++++++++++++++++++++++++++++ src/core.go | 2 +- src/options.go | 7 + src/pattern.go | 14 +- src/pattern_test.go | 24 +-- 9 files changed, 563 insertions(+), 38 deletions(-) create mode 100644 src/algo/normalize.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f78270c..fbc1e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ CHANGELOG - Added `--height HEIGHT[%]` option - Preview window will truncate long lines by default. Line wrap can be enabled by `:wrap` flag in `--preview-window`. +- Added `--normalize` option to normalize latin script letters before + matching. e.g. `sodanco` can match `Só Danço Samba`. 0.15.9 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index b6540e1..1bcfb63 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -48,6 +48,10 @@ Case-insensitive match (default: smart-case match) .B "+i" Case-sensitive match .TP +.B "--normalize" +Normalize latin script letters before matching. This is not enabled by default +to avoid performance overhead. +.TP .BI "--algo=" TYPE Fuzzy matching algorithm (default: v2) diff --git a/src/algo/algo.go b/src/algo/algo.go index 1b85594..2a3bc9d 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -234,9 +234,36 @@ func bonusAt(input util.Chars, idx int) int16 { return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) } -type Algo func(caseSensitive bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) +func normalizeRune(r rune) rune { + if r < 0x00C0 || r > 0x2184 { + return r + } -func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + n := normalized[r] + if n > 0 { + return n + } + return r +} + +func normalizeRunes(runes []rune) []rune { + ret := make([]rune, len(runes)) + copy(ret, runes) + for idx, r := range runes { + if r < 0x00C0 || r > 0x2184 { + continue + } + n := normalized[r] + if n > 0 { + ret[idx] = normalized[r] + } + } + return ret +} + +type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) + +func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { // Assume that pattern is given in lowercase if case-insensitive. // First check if there's a match and calculate bonus for each position. // If the input string is too long, consider finding the matching chars in @@ -247,13 +274,17 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] case 0: return Result{0, 0, 0}, posArray(withPos, M) case 1: - return ExactMatchNaive(caseSensitive, forward, input, pattern[0:1], withPos, slab) + return ExactMatchNaive(caseSensitive, normalize, forward, input, pattern[0:1], withPos, slab) } // Since O(nm) algorithm can be prohibitively expensive for large input, // we fall back to the greedy algorithm. if slab != nil && N*M > cap(slab.I16) { - return FuzzyMatchV1(caseSensitive, forward, input, pattern, withPos, slab) + return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) + } + + if normalize { + pattern = normalizeRunes(pattern) } // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages @@ -285,6 +316,10 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] } } + if normalize { + char = normalizeRune(char) + } + T[idx] = char B[idx] = bonusFor(prevClass, class) prevClass = class @@ -432,7 +467,7 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] } // Implement the same sorting criteria as V2 -func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { +func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) pos := posArray(withPos, len(pattern)) prevClass := charNonWord @@ -449,6 +484,10 @@ func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx in char = unicode.To(unicode.LowerCase, char) } } + // pattern is already normalized + if normalize { + char = normalizeRune(char) + } if char == pattern[pidx] { if withPos { *pos = append(*pos, idx) @@ -488,7 +527,7 @@ func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx in } // FuzzyMatchV1 performs fuzzy-match -func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } @@ -500,6 +539,10 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r lenRunes := text.Length() lenPattern := len(pattern) + if normalize { + pattern = normalizeRunes(pattern) + } + for index := 0; index < lenRunes; index++ { char := text.Get(indexAt(index, lenRunes, forward)) // This is considerably faster than blindly applying strings.ToLower to the @@ -514,6 +557,9 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r char = unicode.To(unicode.LowerCase, char) } } + if normalize { + char = normalizeRune(char) + } pchar := pattern[indexAt(pidx, lenPattern, forward)] if char == pchar { if sidx < 0 { @@ -553,7 +599,7 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r sidx, eidx = lenRunes-eidx, lenRunes-sidx } - score, pos := calculateScore(caseSensitive, text, pattern, sidx, eidx, withPos) + score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos) return Result{sidx, eidx, score}, pos } return Result{-1, -1, 0}, nil @@ -568,7 +614,7 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r // bonus point, instead of stopping immediately after finding the first match. // The solution is much cheaper since there is only one possible alignment of // the pattern. -func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } @@ -580,6 +626,10 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern return Result{-1, -1, 0}, nil } + if normalize { + pattern = normalizeRunes(pattern) + } + // For simplicity, only look at the bonus at the first character position pidx := 0 bestPos, bonus, bestBonus := -1, int16(0), int16(-1) @@ -593,6 +643,9 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern char = unicode.To(unicode.LowerCase, char) } } + if normalize { + char = normalizeRune(char) + } pidx_ := indexAt(pidx, lenPattern, forward) pchar := pattern[pidx_] if pchar == char { @@ -624,14 +677,14 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern sidx = lenRunes - (bestPos + 1) eidx = lenRunes - (bestPos - lenPattern + 1) } - score, _ := calculateScore(caseSensitive, text, pattern, sidx, eidx, false) + score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false) return Result{sidx, eidx, score}, nil } return Result{-1, -1, 0}, nil } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } @@ -640,22 +693,29 @@ func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru return Result{-1, -1, 0}, nil } + if normalize { + pattern = normalizeRunes(pattern) + } + for index, r := range pattern { char := text.Get(index) if !caseSensitive { char = unicode.ToLower(char) } + if normalize { + char = normalizeRune(char) + } if char != r { return Result{-1, -1, 0}, nil } } lenPattern := len(pattern) - score, _ := calculateScore(caseSensitive, text, pattern, 0, lenPattern, false) + score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false) return Result{0, lenPattern, score}, nil } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { lenRunes := text.Length() trimmedLen := lenRunes - text.TrailingWhitespaces() if len(pattern) == 0 { @@ -666,11 +726,18 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru return Result{-1, -1, 0}, nil } + if normalize { + pattern = normalizeRunes(pattern) + } + for index, r := range pattern { char := text.Get(index + diff) if !caseSensitive { char = unicode.ToLower(char) } + if normalize { + char = normalizeRune(char) + } if char != r { return Result{-1, -1, 0}, nil } @@ -678,21 +745,37 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru lenPattern := len(pattern) sidx := trimmedLen - lenPattern eidx := trimmedLen - score, _ := calculateScore(caseSensitive, text, pattern, sidx, eidx, false) + score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false) return Result{sidx, eidx, score}, nil } // EqualMatch performs equal-match -func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func EqualMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { lenPattern := len(pattern) if text.Length() != lenPattern { return Result{-1, -1, 0}, nil } - runesStr := text.ToString() - if !caseSensitive { - runesStr = strings.ToLower(runesStr) + match := true + if normalize { + runes := text.ToRunes() + for idx, pchar := range pattern { + char := runes[idx] + if !caseSensitive { + char = unicode.To(unicode.LowerCase, char) + } + if normalizeRune(pchar) != normalizeRune(char) { + match = false + break + } + } + } else { + runesStr := text.ToString() + if !caseSensitive { + runesStr = strings.ToLower(runesStr) + } + match = runesStr == string(pattern) } - if runesStr == string(pattern) { + if match { return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern + (bonusFirstCharMultiplier-1)*bonusBoundary}, nil } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index fc24f6d..df8b227 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -10,10 +10,14 @@ import ( ) func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { + assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score) +} + +func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) { if !caseSensitive { pattern = strings.ToLower(pattern) } - res, pos := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil) + res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil) var start, end int if pos == nil || len(*pos) == 0 { start = res.Start @@ -156,6 +160,21 @@ func TestEmptyPattern(t *testing.T) { } } +func TestNormalize(t *testing.T) { + caseSensitive := false + normalize := true + forward := true + test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) { + for _, fun := range funs { + assertMatch2(t, fun, caseSensitive, normalize, forward, + input, pattern, sidx, eidx, score) + } + } + test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive) + test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2) + test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch) +} + func TestLongString(t *testing.T) { bytes := make([]byte, math.MaxUint16*2) for i := range bytes { diff --git a/src/algo/normalize.go b/src/algo/normalize.go new file mode 100644 index 0000000..1168a64 --- /dev/null +++ b/src/algo/normalize.go @@ -0,0 +1,408 @@ +// Normalization of latin script letters +// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt + +package algo + +var normalized map[rune]rune = map[rune]rune{ + 0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER + 0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER + 0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER + 0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER + 0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER + 0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER + 0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER + 0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER + 0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER + 0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER + 0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER + 0x0363: 'a', // , COMBINING LATIN SMALL LETTER + 0x0250: 'a', // , LATIN SMALL LETTER TURNED + 0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER + 0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER + 0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER + 0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER + 0x010D: 'c', // WITH CARON, LATIN SMALL LETTER + 0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER + 0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0255: 'c', // WITH CURL, LATIN SMALL LETTER + 0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER + 0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER + 0x0368: 'c', // , COMBINING LATIN SMALL LETTER + 0x0297: 'c', // , LATIN LETTER STRETCHED + 0x2184: 'c', // , LATIN SMALL LETTER REVERSED + 0x010F: 'd', // WITH CARON, LATIN SMALL LETTER + 0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0221: 'd', // WITH CURL, LATIN SMALL LETTER + 0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER + 0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER + 0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER + 0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER + 0x0369: 'd', // , COMBINING LATIN SMALL LETTER + 0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER + 0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER + 0x011B: 'e', // WITH CARON, LATIN SMALL LETTER + 0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER + 0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER + 0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN + 0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER + 0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER + 0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER + 0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER + 0x0364: 'e', // , COMBINING LATIN SMALL LETTER + 0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN + 0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN + 0x025B: 'e', // , LATIN SMALL LETTER OPEN + 0x0258: 'e', // , LATIN SMALL LETTER REVERSED + 0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN + 0x01DD: 'e', // , LATIN SMALL LETTER TURNED + 0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN + 0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER + 0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER + 0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER + 0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER + 0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER + 0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER + 0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER + 0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER + 0x0261: 'g', // , LATIN SMALL LETTER SCRIPT + 0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER + 0x021F: 'h', // WITH CARON, LATIN SMALL LETTER + 0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER + 0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER + 0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED + 0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER + 0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER + 0x036A: 'h', // , COMBINING LATIN SMALL LETTER + 0x0265: 'h', // , LATIN SMALL LETTER TURNED + 0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER + 0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER + 0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER + 0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER + 0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER + 0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER + 0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER + 0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER + 0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER + 0x0365: 'i', // , COMBINING LATIN SMALL LETTER + 0x0131: 'i', // , LATIN SMALL LETTER DOTLESS + 0x1D09: 'i', // , LATIN SMALL LETTER TURNED + 0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER + 0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER + 0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER + 0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER + 0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER + 0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS + 0x0237: 'j', // , LATIN SMALL LETTER DOTLESS + 0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER + 0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER + 0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER + 0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER + 0x029E: 'k', // , LATIN SMALL LETTER TURNED + 0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER + 0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER + 0x019A: 'l', // WITH BAR, LATIN SMALL LETTER + 0x026C: 'l', // WITH BELT, LATIN SMALL LETTER + 0x013E: 'l', // WITH CARON, LATIN SMALL LETTER + 0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0234: 'l', // WITH CURL, LATIN SMALL LETTER + 0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER + 0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER + 0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER + 0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER + 0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER + 0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED + 0x036B: 'm', // , COMBINING LATIN SMALL LETTER + 0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED + 0x026F: 'm', // , LATIN SMALL LETTER TURNED + 0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER + 0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER + 0x0148: 'n', // WITH CARON, LATIN SMALL LETTER + 0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0235: 'n', // WITH CURL, LATIN SMALL LETTER + 0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER + 0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER + 0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER + 0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER + 0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER + 0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER + 0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER + 0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER + 0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER + 0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER + 0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER + 0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER + 0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER + 0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER + 0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER + 0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER + 0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER + 0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS + 0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER + 0x0366: 'o', // , COMBINING LATIN SMALL LETTER + 0x0275: 'o', // , LATIN SMALL LETTER BARRED + 0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF + 0x0254: 'o', // , LATIN SMALL LETTER OPEN + 0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS + 0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN + 0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF + 0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER + 0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER + 0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER + 0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER + 0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER + 0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER + 0x0159: 'r', // WITH CARON, LATIN SMALL LETTER + 0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER + 0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED + 0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED + 0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER + 0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER + 0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED + 0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER + 0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER + 0x036C: 'r', // , COMBINING LATIN SMALL LETTER + 0x0279: 'r', // , LATIN SMALL LETTER TURNED + 0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER + 0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER + 0x0161: 's', // WITH CARON, LATIN SMALL LETTER + 0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER + 0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER + 0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG + 0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0282: 's', // WITH HOOK, LATIN SMALL LETTER + 0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER + 0x017F: 's', // , LATIN SMALL LETTER LONG + 0x00DF: 's', // , LATIN SMALL LETTER SHARP + 0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER + 0x0165: 't', // WITH CARON, LATIN SMALL LETTER + 0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER + 0x0236: 't', // WITH CURL, LATIN SMALL LETTER + 0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER + 0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER + 0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER + 0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER + 0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x0167: 't', // WITH STROKE, LATIN SMALL LETTER + 0x036D: 't', // , COMBINING LATIN SMALL LETTER + 0x0287: 't', // , LATIN SMALL LETTER TURNED + 0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER + 0x0289: 'u', // BAR, LATIN SMALL LETTER + 0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER + 0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER + 0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER + 0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER + 0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER + 0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER + 0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER + 0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER + 0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER + 0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER + 0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER + 0x0367: 'u', // , COMBINING LATIN SMALL LETTER + 0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS + 0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED + 0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER + 0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER + 0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER + 0x036E: 'v', // , COMBINING LATIN SMALL LETTER + 0x028C: 'v', // , LATIN SMALL LETTER TURNED + 0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER + 0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER + 0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER + 0x028D: 'w', // , LATIN SMALL LETTER TURNED + 0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x036F: 'x', // , COMBINING LATIN SMALL LETTER + 0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER + 0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER + 0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER + 0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER + 0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER + 0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER + 0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER + 0x028E: 'y', // , LATIN SMALL LETTER TURNED + 0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER + 0x017E: 'z', // WITH CARON, LATIN SMALL LETTER + 0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0291: 'z', // WITH CURL, LATIN SMALL LETTER + 0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER + 0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER + 0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER + 0x0251: 'a', // , latin small letter script + 0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER + 0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER + 0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER + 0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER + 0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL + 0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER + 0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER + 0x0299: 'B', // , LATIN LETTER SMALL CAPITAL + 0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED + 0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER + 0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER + 0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL + 0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER + 0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN + 0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL + 0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER + 0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER + 0x0190: 'E', // , LATIN CAPITAL LETTER OPEN + 0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED + 0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL + 0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER + 0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL + 0x0262: 'G', // , LATIN LETTER SMALL CAPITAL + 0x029C: 'H', // , LATIN LETTER SMALL CAPITAL + 0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER + 0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER + 0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER + 0x026A: 'I', // , LATIN LETTER SMALL CAPITAL + 0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER + 0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL + 0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL + 0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER + 0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL + 0x029F: 'L', // , LATIN LETTER SMALL CAPITAL + 0x019C: 'M', // , LATIN CAPITAL LETTER TURNED + 0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL + 0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER + 0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER + 0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER + 0x0274: 'N', // , LATIN LETTER SMALL CAPITAL + 0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED + 0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER + 0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER + 0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER + 0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER + 0x0186: 'O', // , LATIN CAPITAL LETTER OPEN + 0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL + 0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN + 0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL + 0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL + 0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER + 0x0280: 'R', // , LATIN LETTER SMALL CAPITAL + 0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED + 0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED + 0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED + 0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER + 0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER + 0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL + 0x0244: 'U', // BAR, LATIN CAPITAL LETTER + 0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER + 0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL + 0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER + 0x0245: 'V', // , LATIN CAPITAL LETTER TURNED + 0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL + 0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL + 0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER + 0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER + 0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL + 0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL +} diff --git a/src/core.go b/src/core.go index f7dae1a..3e27ed7 100644 --- a/src/core.go +++ b/src/core.go @@ -143,7 +143,7 @@ func Run(opts *Options) { } patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, forward, + opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, opts.Filter == nil, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/options.go b/src/options.go index 0c6661f..f34ee84 100644 --- a/src/options.go +++ b/src/options.go @@ -24,6 +24,7 @@ const usage = `usage: fzf [options] --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2) -i Case-insensitive match (default: smart-case match) +i Case-sensitive match + --normalize Normalize latin script letters before matching -n, --nth=N[,..] Comma-separated list of field index expressions for limiting search scope. Each can be a non-zero integer or a range expression ([BEGIN]..[END]). @@ -138,6 +139,7 @@ type Options struct { FuzzyAlgo algo.Algo Extended bool Case Case + Normalize bool Nth []Range WithNth []Range Delimiter Delimiter @@ -185,6 +187,7 @@ func defaultOptions() *Options { FuzzyAlgo: algo.FuzzyMatchV2, Extended: true, Case: CaseSmart, + Normalize: false, Nth: make([]Range, 0), WithNth: make([]Range, 0), Delimiter: Delimiter{}, @@ -887,6 +890,10 @@ func parseOptions(opts *Options, allArgs []string) { case "-f", "--filter": filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter + case "--normalize": + opts.Normalize = true + case "--no-normalize": + opts.Normalize = false case "--algo": opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) case "--expect": diff --git a/src/pattern.go b/src/pattern.go index 82272af..8f1d9bc 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -43,6 +43,7 @@ type Pattern struct { fuzzyAlgo algo.Algo extended bool caseSensitive bool + normalize bool forward bool text []rune termSets []termSet @@ -75,7 +76,7 @@ func clearChunkCache() { } // BuildPattern builds Pattern object from the given arguments -func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, forward bool, +func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string @@ -120,6 +121,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, fuzzyAlgo: fuzzyAlgo, extended: extended, caseSensitive: caseSensitive, + normalize: normalize, forward: forward, text: []rune(asString), termSets: termSets, @@ -309,9 +311,9 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { input := p.prepareInput(item) if p.fuzzy { - return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.forward, p.text, withPos, slab) + return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) } - return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text, withPos, slab) + return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) } func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, int, *[]int) { @@ -330,7 +332,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of matched := false for _, term := range termSet { pfun := p.procFun[term.typ] - off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.forward, term.text, withPos, slab) + off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab) if sidx := off[0]; sidx >= 0 { if term.inv { continue @@ -378,9 +380,9 @@ func (p *Pattern) prepareInput(item *Item) []Token { return ret } -func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { +func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { for _, part := range tokens { - if res, pos := pfun(caseSensitive, forward, *part.text, pattern, withPos, slab); res.Start >= 0 { + if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength eidx := int32(res.End) + part.prefixLength if pos != nil { diff --git a/src/pattern_test.go b/src/pattern_test.go index 9b6d394..66c0041 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -75,10 +75,10 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("'abc")) res, pos := algo.ExactMatchNaive( - pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil) + pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil) if res.Start != 7 || res.End != 10 { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -90,11 +90,11 @@ func TestExact(t *testing.T) { func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { res, pos := algo.EqualMatch( - pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil) + pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil) if res.Start != sidxExpected || res.End != eidxExpected { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -109,17 +109,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -132,7 +132,7 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg")) + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) @@ -167,7 +167,7 @@ func TestOrigTextAndTransformed(t *testing.T) { func TestCacheKey(t *testing.T) { test := func(extended bool, patStr string, expected string, cacheable bool) { - pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr)) + pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr)) if pat.CacheKey() != expected { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } From a16d8f66a99ae365a57d114d5ee7d0a7ebc4cf5f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 09:52:17 +0900 Subject: [PATCH 06/28] Normalize pattern string before passing it to Algo function --- src/algo/algo.go | 38 +++----------------------------------- src/algo/normalize.go | 16 ++++++++++++++++ src/pattern.go | 10 +++++++--- src/pattern_test.go | 6 +++--- 4 files changed, 29 insertions(+), 41 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 2a3bc9d..ac6c66d 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -246,21 +246,9 @@ func normalizeRune(r rune) rune { return r } -func normalizeRunes(runes []rune) []rune { - ret := make([]rune, len(runes)) - copy(ret, runes) - for idx, r := range runes { - if r < 0x00C0 || r > 0x2184 { - continue - } - n := normalized[r] - if n > 0 { - ret[idx] = normalized[r] - } - } - return ret -} - +// Algo functions make two assumptions +// 1. "pattern" is given in lowercase if "caseSensitive" is false +// 2. "pattern" is already normalized if "normalize" is true type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { @@ -283,10 +271,6 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) } - if normalize { - pattern = normalizeRunes(pattern) - } - // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages offset16 := 0 offset32 := 0 @@ -539,10 +523,6 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Ch lenRunes := text.Length() lenPattern := len(pattern) - if normalize { - pattern = normalizeRunes(pattern) - } - for index := 0; index < lenRunes; index++ { char := text.Get(indexAt(index, lenRunes, forward)) // This is considerably faster than blindly applying strings.ToLower to the @@ -626,10 +606,6 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util return Result{-1, -1, 0}, nil } - if normalize { - pattern = normalizeRunes(pattern) - } - // For simplicity, only look at the bonus at the first character position pidx := 0 bestPos, bonus, bestBonus := -1, int16(0), int16(-1) @@ -693,10 +669,6 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha return Result{-1, -1, 0}, nil } - if normalize { - pattern = normalizeRunes(pattern) - } - for index, r := range pattern { char := text.Get(index) if !caseSensitive { @@ -726,10 +698,6 @@ func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha return Result{-1, -1, 0}, nil } - if normalize { - pattern = normalizeRunes(pattern) - } - for index, r := range pattern { char := text.Get(index + diff) if !caseSensitive { diff --git a/src/algo/normalize.go b/src/algo/normalize.go index 1168a64..7a49644 100644 --- a/src/algo/normalize.go +++ b/src/algo/normalize.go @@ -406,3 +406,19 @@ var normalized map[rune]rune = map[rune]rune{ 0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL 0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL } + +// NormalizeRunes normalizes latin script letters +func NormalizeRunes(runes []rune) []rune { + ret := make([]rune, len(runes)) + copy(ret, runes) + for idx, r := range runes { + if r < 0x00C0 || r > 0x2184 { + continue + } + n := normalized[r] + if n > 0 { + ret[idx] = normalized[r] + } + } + return ret +} diff --git a/src/pattern.go b/src/pattern.go index 8f1d9bc..731104f 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -95,7 +95,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, termSets := []termSet{} if extended { - termSets = parseTerms(fuzzy, caseMode, asString) + termSets = parseTerms(fuzzy, caseMode, normalize, asString) Loop: for _, termSet := range termSets { for idx, term := range termSet { @@ -140,7 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, return ptr } -func parseTerms(fuzzy bool, caseMode Case, str string) []termSet { +func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { tokens := _splitRegex.Split(str, -1) sets := []termSet{} set := termSet{} @@ -196,10 +196,14 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet { sets = append(sets, set) set = termSet{} } + textRunes := []rune(text) + if normalize { + textRunes = algo.NormalizeRunes(textRunes) + } set = append(set, term{ typ: typ, inv: inv, - text: []rune(text), + text: textRunes, caseSensitive: caseSensitive, origText: origText}) switchSet = true diff --git a/src/pattern_test.go b/src/pattern_test.go index 66c0041..ea0082f 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -15,7 +15,7 @@ func init() { } func TestParseTermsExtended(t *testing.T) { - terms := parseTerms(true, CaseSmart, + terms := parseTerms(true, CaseSmart, false, "| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |") if len(terms) != 9 || terms[0][0].typ != termFuzzy || terms[0][0].inv || @@ -50,7 +50,7 @@ func TestParseTermsExtended(t *testing.T) { } func TestParseTermsExtendedExact(t *testing.T) { - terms := parseTerms(false, CaseSmart, + terms := parseTerms(false, CaseSmart, false, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 || @@ -66,7 +66,7 @@ func TestParseTermsExtendedExact(t *testing.T) { } func TestParseTermsEmpty(t *testing.T) { - terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$") + terms := parseTerms(true, CaseSmart, false, "' $ ^ !' !^ !$") if len(terms) != 0 { t.Errorf("%s", terms) } From 6c6c0a47782328d21cc2899ac9c9f0e6edb1fb40 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 10:45:05 +0900 Subject: [PATCH 07/28] Make util.RuneWidth return 1 for non-displayable characters Fix line wrapping in preview window --- src/util/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/util.go b/src/util/util.go index 29e8017..15ec0ad 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -18,7 +18,7 @@ func RuneWidth(r rune, prefixWidth int, tabstop int) int { } else if w, found := _runeWidths[r]; found { return w } else { - w := runewidth.RuneWidth(r) + w := Max(runewidth.RuneWidth(r), 1) _runeWidths[r] = w return w } From d18b8e0d2c9987322e3a17b72a1db6c497cee947 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 13:22:24 +0900 Subject: [PATCH 08/28] Retry flaky test cases --- test/test_go.rb | 51 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 55422aa..494d3cd 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1241,9 +1241,11 @@ module TestShell def test_ctrl_t set_var "FZF_CTRL_T_COMMAND", "seq 100" - tmux.prepare - tmux.send_keys 'C-t' - lines = tmux.until { |lines| lines.item_count == 100 } + lines = retries do + tmux.prepare + tmux.send_keys 'C-t' + tmux.until { |lines| lines.item_count == 100 } + end tmux.send_keys :Tab, :Tab, :Tab tmux.until { |lines| lines.any_include? ' (3)' } tmux.send_keys :Enter @@ -1255,9 +1257,11 @@ module TestShell writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2'] set_var "FZF_CTRL_T_COMMAND", "cat #{tempname}" - tmux.prepare - tmux.send_keys 'echo ', 'C-t' - tmux.until { |lines| lines.item_count == 2 } + retries do + tmux.prepare + tmux.send_keys 'echo ', 'C-t' + tmux.until { |lines| lines.item_count == 2 } + end tmux.send_keys 'fzf-unicode' tmux.until { |lines| lines.match_count == 2 } @@ -1281,9 +1285,11 @@ module TestShell end def test_alt_c - tmux.prepare - tmux.send_keys :Escape, :c - lines = tmux.until { |lines| lines.item_count > 0 } + lines = retries do + tmux.prepare + tmux.send_keys :Escape, :c + tmux.until { |lines| lines.item_count > 0 } + end expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1] tmux.send_keys :Enter tmux.prepare @@ -1297,9 +1303,11 @@ module TestShell tmux.prepare tmux.send_keys 'cd /', :Enter - tmux.prepare - tmux.send_keys :Escape, :c - lines = tmux.until { |lines| lines.item_count == 1 } + retries do + tmux.prepare + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| lines.item_count == 1 } + end tmux.send_keys :Enter tmux.prepare @@ -1313,9 +1321,12 @@ module TestShell tmux.send_keys 'echo 2nd', :Enter; tmux.prepare tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare - tmux.send_keys 'echo 4th', :Enter; tmux.prepare - tmux.send_keys 'C-r' - tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys 'echo 4th', :Enter + retries do + tmux.prepare + tmux.send_keys 'C-r' + tmux.until { |lines| lines.item_count > 0 } + end tmux.send_keys '3d' tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } tmux.send_keys :Enter @@ -1323,6 +1334,16 @@ module TestShell tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '3rd' } end + + def retries times = 3, &block + (times - 1).times do |t| + begin + return block.call + rescue RuntimeError + end + end + block.call + end end module CompletionTest From 78a3f81972312d13d50e5b24dcabaa40284566b5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 19:09:30 +0900 Subject: [PATCH 09/28] Do not use \e[s and \e[u Excerpt from http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html: > - Save cursor position: > \033[s > - Restore cursor position: > \033[u > > The latter two codes are NOT honoured by many terminal emulators. The > only ones that I'm aware of that do are xterm and nxterm - even though > the majority of terminal emulators are based on xterm code. As far as > I can tell, rxvt, kvt, xiterm, and Eterm do not support them. They are > supported on the console. They are also unsupported by Neovim terminal. --- src/tui/light.go | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 1273c8f..379680c 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -33,7 +33,7 @@ func (r *LightRenderer) stderr(str string) { runes := []rune{} for len(bytes) > 0 { r, sz := utf8.DecodeRune(bytes) - if r == utf8.RuneError || r != '\x1b' && r != '\n' && r < 32 { + if r == utf8.RuneError || r != '\x1b' && r != '\n' && r != '\r' && r < 32 { runes = append(runes, '?') } else { runes = append(runes, r) @@ -71,6 +71,8 @@ type LightRenderer struct { escDelay int upOneLine bool queued string + y int + x int maxHeightFunc func(int) int } @@ -182,10 +184,29 @@ func (r *LightRenderer) Init() { } r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi("G") - r.csi("s") + // r.csi("s") r.yoffset, _ = r.findOffset() } +func (r *LightRenderer) move(y int, x int) { + // w.csi("u") + if r.y < y { + r.csi(fmt.Sprintf("%dB", y-r.y)) + } else if r.y > y { + r.csi(fmt.Sprintf("%dA", r.y-y)) + } + r.stderr("\r") + if x > 0 { + r.csi(fmt.Sprintf("%dC", x)) + } + r.y = y + r.x = x +} + +func (r *LightRenderer) origin() { + r.move(0, 0) +} + func (r *LightRenderer) updateTerminalSize() { sizes := strings.Split(stty("size"), " ") if len(sizes) < 2 { @@ -470,7 +491,8 @@ func (r *LightRenderer) Resume() bool { } func (r *LightRenderer) Clear() { - r.csi("u") + // r.csi("u") + r.origin() r.csi("J") r.flush() } @@ -484,7 +506,8 @@ func (r *LightRenderer) Refresh() { } func (r *LightRenderer) Close() { - r.csi("u") + // r.csi("u") + r.origin() r.csi("J") if r.mouse { r.csi("?1000l") @@ -584,15 +607,7 @@ func (w *LightWindow) Move(y int, x int) { w.posx = x w.posy = y - w.csi("u") - y += w.Top() - if y > 0 { - w.csi(fmt.Sprintf("%dB", y)) - } - x += w.Left() - if x > 0 { - w.csi(fmt.Sprintf("%dC", x)) - } + w.renderer.move(w.Top()+y, w.Left()+x) } func (w *LightWindow) MoveAndClear(y int, x int) { From 340af463cd9c52c0a3a7ea5688035bba1cd29f6a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Jan 2017 01:04:36 +0900 Subject: [PATCH 10/28] Add --min-height option for percent --height --- man/man1/fzf.1 | 4 ++++ src/options.go | 10 +++++++++- src/terminal.go | 9 +++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 1bcfb63..98c1c79 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -138,6 +138,10 @@ Label characters for \fBjump\fR and \fBjump-accept\fR Display fzf window below the cursor with the given height instead of using fullscreen. .TP +.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 .B "--reverse" Reverse orientation .TP diff --git a/src/options.go b/src/options.go index f34ee84..cbb0155 100644 --- a/src/options.go +++ b/src/options.go @@ -50,6 +50,8 @@ const usage = `usage: fzf [options] Layout --height=HEIGHT[%] Display fzf window below the cursor with the given height instead of using fullscreen + --min-height=HEIGHT Minimum height when --height is given in percent + (default: 10) --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 @@ -153,6 +155,7 @@ type Options struct { Black bool Bold bool Height sizeSpec + MinHeight int Reverse bool Cycle bool Hscroll bool @@ -200,6 +203,7 @@ func defaultOptions() *Options { Theme: tui.EmptyTheme(), Black: false, Bold: true, + MinHeight: 10, Reverse: false, Cycle: false, Hscroll: true, @@ -1023,7 +1027,9 @@ func parseOptions(opts *Options, allArgs []string) { 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[%]]")) + opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) + case "--min-height": + opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT") case "--no-height": opts.Height = sizeSpec{} case "--no-margin": @@ -1054,6 +1060,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Sort = 1 // Don't care } else if match, value := optString(arg, "--height="); match { opts.Height = parseHeight(value) + } else if match, value := optString(arg, "--min-height="); match { + opts.MinHeight = atoi(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/terminal.go b/src/terminal.go index c653327..ce63adf 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -275,14 +275,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { maxHeightFunc := func(termHeight int) int { var maxHeight int if opts.Height.percent { - maxHeight = int(opts.Height.size * float64(termHeight) / 100.0) + maxHeight = util.Min(termHeight, + util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)) } else { - maxHeight = util.Min(int(opts.Height.size), termHeight) + maxHeight = util.Min(termHeight, int(opts.Height.size)) } if opts.InlineInfo { - return util.Max(maxHeight, 3) + return util.Max(maxHeight, minHeight-1) } - return util.Max(maxHeight, 4) + return util.Max(maxHeight, minHeight) } renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) } else { From ae274158de38181bca27f2ce54c8b4fc0b688eff Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Jan 2017 02:16:12 +0900 Subject: [PATCH 11/28] Add experimental support for 24-bit colors --- man/man1/fzf.1 | 4 +++- src/options.go | 15 +++++++++++---- src/tui/tui.go | 8 ++++++++ src/tui/tui_test.go | 20 ++++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/tui/tui_test.go diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 98c1c79..2ef3cdd 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -197,7 +197,9 @@ Number of spaces for a tab character (default: 8) .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" Color configuration. The name of the base color scheme is followed by custom color mappings. Ansi color code of -1 denotes terminal default -foreground/background color. +foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR +format, but the support for 24-bit colors is experimental and only works when +\fB--height\fR option is used. .RS e.g. \fBfzf --color=bg+:24\fR diff --git a/src/options.go b/src/options.go index cbb0155..bcd2458 100644 --- a/src/options.go +++ b/src/options.go @@ -493,6 +493,7 @@ func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { theme := dupeTheme(defaultTheme) + rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$") for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { case "dark": @@ -516,11 +517,17 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { if len(pair) != 2 { fail() } - ansi32, err := strconv.Atoi(pair[1]) - if err != nil || ansi32 < -1 || ansi32 > 255 { - fail() + + var ansi tui.Color + if rrggbb.MatchString(pair[1]) { + ansi = tui.HexToColor(pair[1]) + } else { + ansi32, err := strconv.Atoi(pair[1]) + if err != nil || ansi32 < -1 || ansi32 > 255 { + fail() + } + ansi = tui.Color(ansi32) } - ansi := tui.Color(ansi32) switch pair[0] { case "fg": theme.Fg = ansi diff --git a/src/tui/tui.go b/src/tui/tui.go index 859eed7..11ac1e7 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -1,6 +1,7 @@ package tui import ( + "strconv" "time" ) @@ -121,6 +122,13 @@ type ColorPair struct { id int16 } +func HexToColor(rrggbb string) Color { + r, _ := strconv.ParseInt(rrggbb[1:3], 16, 0) + g, _ := strconv.ParseInt(rrggbb[3:5], 16, 0) + b, _ := strconv.ParseInt(rrggbb[5:7], 16, 0) + return Color((1 << 24) + (r << 16) + (g << 8) + b) +} + func NewColorPair(fg Color, bg Color) ColorPair { return ColorPair{fg, bg, -1} } diff --git a/src/tui/tui_test.go b/src/tui/tui_test.go new file mode 100644 index 0000000..3ba9bf3 --- /dev/null +++ b/src/tui/tui_test.go @@ -0,0 +1,20 @@ +package tui + +import "testing" + +func TestHexToColor(t *testing.T) { + assert := func(expr string, r, g, b int) { + color := HexToColor(expr) + if !color.is24() || + int((color>>16)&0xff) != r || + int((color>>8)&0xff) != g || + int((color)&0xff) != b { + t.Fail() + } + } + + assert("#ff0000", 255, 0, 0) + assert("#010203", 1, 2, 3) + assert("#102030", 16, 32, 48) + assert("#ffffff", 255, 255, 255) +} From 0c127cfdc1b62a3b5ca1730fd1e60a406b9ee54b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Jan 2017 22:55:55 +0900 Subject: [PATCH 12/28] No need to query row position of the cursor if mouse is disabled --- src/tui/light.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tui/light.go b/src/tui/light.go index 379680c..574d161 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -185,7 +185,9 @@ func (r *LightRenderer) Init() { r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi("G") // r.csi("s") - r.yoffset, _ = r.findOffset() + if r.mouse { + r.yoffset, _ = r.findOffset() + } } func (r *LightRenderer) move(y int, x int) { From 996dcb14a3fae6a640bff072149352cd82fd8392 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 02:12:32 +0900 Subject: [PATCH 13/28] Make fzf immediately quit when failed to read /dev/tty Close #798 --- src/tui/light.go | 34 ++++++++++++++++++++-------------- src/tui/ncurses.go | 8 +++++--- src/tui/tcell.go | 4 ++-- src/tui/tui.go | 5 +++++ 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 574d161..b9e90d6 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -111,8 +111,10 @@ func (r *LightRenderer) defaultTheme() *ColorTheme { return Default16 } -func stty(cmd string) string { - out, err := util.ExecCommand("stty " + cmd + " < /dev/tty").Output() +func (r *LightRenderer) stty(cmd string) string { + proc := util.ExecCommand("stty " + cmd) + proc.Stdin = r.ttyin + out, err := proc.Output() if err != nil { // Not sure how to handle this panic("stty " + cmd + ": " + err.Error()) @@ -164,8 +166,8 @@ func (r *LightRenderer) Init() { } r.escDelay = delay - r.ostty = stty("-g") - stty("raw") + r.ostty = r.stty("-g") + r.stty("raw") r.updateTerminalSize() initTheme(r.theme, r.defaultTheme(), r.forceBlack) @@ -210,7 +212,7 @@ func (r *LightRenderer) origin() { } func (r *LightRenderer) updateTerminalSize() { - sizes := strings.Split(stty("size"), " ") + sizes := strings.Split(r.stty("size"), " ") if len(sizes) < 2 { r.width = defaultWidth r.height = r.maxHeightFunc(defaultHeight) @@ -220,14 +222,14 @@ func (r *LightRenderer) updateTerminalSize() { } } -func (r *LightRenderer) getch(nonblock bool) int { +func (r *LightRenderer) getch(nonblock bool) (int, bool) { b := make([]byte, 1) util.SetNonblock(r.ttyin, nonblock) _, err := r.ttyin.Read(b) if err != nil { - return -1 + return 0, false } - return int(b[0]) + return int(b[0]), true } func (r *LightRenderer) getBytes() []byte { @@ -235,7 +237,11 @@ func (r *LightRenderer) getBytes() []byte { } func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { - c := r.getch(false) + c, ok := r.getch(false) + if !ok { + r.Close() + errorExit() + } retries := 0 if c == ESC { @@ -244,8 +250,8 @@ func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { buffer = append(buffer, byte(c)) for { - c = r.getch(true) - if c == -1 { + c, ok = r.getch(true) + if !ok { if retries > 0 { retries-- time.Sleep(escPollInterval * time.Millisecond) @@ -479,13 +485,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event { } func (r *LightRenderer) Pause() { - stty(fmt.Sprintf("%q", r.ostty)) + r.stty(fmt.Sprintf("%q", r.ostty)) r.csi("?1049h") r.flush() } func (r *LightRenderer) Resume() bool { - stty("raw") + r.stty("raw") r.csi("?1049l") r.flush() // Should redraw @@ -518,7 +524,7 @@ func (r *LightRenderer) Close() { r.csi("A") } r.flush() - stty(fmt.Sprintf("%q", r.ostty)) + r.stty(fmt.Sprintf("%q", r.ostty)) } func (r *LightRenderer) MaxX() int { diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index b160692..f6feefc 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -110,12 +110,12 @@ func (r *FullscreenRenderer) Init() { tty := C.c_tty() if tty == nil { fmt.Println("Failed to open /dev/tty") - os.Exit(2) + errorExit() } _screen = C.c_newterm(tty) if _screen == nil { fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) - os.Exit(2) + errorExit() } C.set_term(_screen) if r.mouse { @@ -375,7 +375,9 @@ func (r *FullscreenRenderer) GetChar() Event { c := C.getch() switch c { case C.ERR: - return Event{Invalid, 0, nil} + // Unexpected error from blocking read + r.Close() + errorExit() case C.KEY_UP: return Event{Up, 0, nil} case C.KEY_DOWN: diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 460bfd5..b6f0819 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -124,11 +124,11 @@ func (r *FullscreenRenderer) initScreen() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) - os.Exit(2) + errorExit() } if e = s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) - os.Exit(2) + errorExit() } if r.mouse { s.EnableMouse() diff --git a/src/tui/tui.go b/src/tui/tui.go index 11ac1e7..eb504f8 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -1,6 +1,7 @@ package tui import ( + "os" "strconv" "time" ) @@ -275,6 +276,10 @@ func EmptyTheme() *ColorTheme { Border: colUndefined} } +func errorExit() { + os.Exit(2) +} + func init() { Default16 = &ColorTheme{ Fg: colDefault, From f8082bc53a5bc736558d5952a34b527f12815878 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 21:48:36 +0900 Subject: [PATCH 14/28] No need to use /bin/sh to execute stty and tput --- src/tui/light.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index b9e90d6..4725ef5 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -3,6 +3,7 @@ package tui import ( "fmt" "os" + "os/exec" "strconv" "strings" "syscall" @@ -104,7 +105,7 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in } func (r *LightRenderer) defaultTheme() *ColorTheme { - colors, err := util.ExecCommand("tput colors").Output() + colors, err := exec.Command("tput", "colors").Output() if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { return Dark256 } @@ -112,7 +113,7 @@ func (r *LightRenderer) defaultTheme() *ColorTheme { } func (r *LightRenderer) stty(cmd string) string { - proc := util.ExecCommand("stty " + cmd) + proc := exec.Command("stty", cmd) proc.Stdin = r.ttyin out, err := proc.Output() if err != nil { @@ -485,7 +486,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event { } func (r *LightRenderer) Pause() { - r.stty(fmt.Sprintf("%q", r.ostty)) + r.stty(r.ostty) r.csi("?1049h") r.flush() } @@ -524,7 +525,7 @@ func (r *LightRenderer) Close() { r.csi("A") } r.flush() - r.stty(fmt.Sprintf("%q", r.ostty)) + r.stty(r.ostty) } func (r *LightRenderer) MaxX() int { From 9977a3e9fcdc2d2feda2f8cd2990a89c21804e56 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 22:13:40 +0900 Subject: [PATCH 15/28] Make preview renderer suspend early on line wrap --- src/terminal.go | 72 ++++++++++++++++++++++++++-------------------- src/tui/light.go | 16 +++++++---- src/tui/ncurses.go | 9 ++++-- src/tui/tcell.go | 10 +++---- src/tui/tui.go | 12 ++++++-- 5 files changed, 73 insertions(+), 46 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index ce63adf..0353157 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1,8 +1,10 @@ package fzf import ( + "bufio" "bytes" "fmt" + "io" "os" "os/signal" "regexp" @@ -852,41 +854,49 @@ func (t *Terminal) printPreview() { return } t.pwindow.Erase() - skip := t.previewer.offset - extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool { - if skip > 0 { - newlines := numLinesMax(str, skip) - if skip <= newlines { - for i := 0; i < skip; i++ { - str = str[strings.Index(str, "\n")+1:] + + maxWidth := t.pwindow.Width() + if t.tui.DoesAutoWrap() { + maxWidth -= 1 + } + reader := bufio.NewReader(strings.NewReader(t.previewer.text)) + lineNo := -t.previewer.offset + for { + line, err := reader.ReadString('\n') + eof := err == io.EOF + if !eof { + line = line[:len(line)-1] + } + lineNo++ + if lineNo > t.pwindow.Height() { + break + } else if lineNo > 0 { + var fillRet tui.FillReturn + extractColor(line, nil, func(str string, ansi *ansiState) bool { + trimmed := []rune(str) + if !t.preview.wrap { + trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) } - skip = 0 - } else { - skip -= newlines - return true + str, _ = t.processTabs(trimmed, 0) + if ansi != nil && ansi.colored() { + fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) + } else { + fillRet = t.pwindow.Fill(str) + } + return fillRet == tui.FillContinue + }) + switch fillRet { + case tui.FillNextLine: + continue + case tui.FillSuspend: + break } + t.pwindow.Fill("\n") } - lines := strings.Split(str, "\n") - for i, line := range lines { - limit := t.pwindow.Width() - if t.tui.DoesAutoWrap() { - limit -= 1 - } - if i == 0 { - limit -= t.pwindow.X() - } - trimmed := []rune(line) - if !t.preview.wrap { - trimmed, _ = t.trimRight(trimmed, limit) - } - lines[i], _ = t.processTabs(trimmed, 0) - str = strings.Join(lines, "\n") + if eof { + break } - if ansi != nil && ansi.colored() { - return t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) - } - return t.pwindow.Fill(str) - }) + } t.pwindow.FinishFill() if t.previewer.lines > t.pwindow.Height() { offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) diff --git a/src/tui/light.go b/src/tui/light.go index 4725ef5..248477c 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -730,23 +730,29 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin return lines } -func (w *LightWindow) fill(str string, onMove func()) bool { +func (w *LightWindow) fill(str string, onMove func()) FillReturn { allLines := strings.Split(str, "\n") for i, line := range allLines { lines := wrapLine(line, w.posx, w.width, w.tabstop) for j, wl := range lines { + if w.posx >= w.Width()-1 && wl.displayWidth == 0 { + if w.posy < w.height-1 { + w.MoveAndClear(w.posy+1, 0) + } + return FillNextLine + } w.stderr(wl.text) w.posx += wl.displayWidth if j < len(lines)-1 || i < len(allLines)-1 { if w.posy+1 >= w.height { - return false + return FillSuspend } w.MoveAndClear(w.posy+1, 0) onMove() } } } - return true + return FillContinue } func (w *LightWindow) setBg() { @@ -755,13 +761,13 @@ func (w *LightWindow) setBg() { } } -func (w *LightWindow) Fill(text string) bool { +func (w *LightWindow) Fill(text string) FillReturn { w.MoveAndClear(w.posy, w.posx) w.setBg() return w.fill(text, w.setBg) } -func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) bool { +func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { w.MoveAndClear(w.posy, w.posx) if bg == colDefault { bg = w.bg diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index f6feefc..3e636ba 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -282,11 +282,14 @@ func (r *FullscreenRenderer) DoesAutoWrap() bool { return true } -func (w *CursesWindow) Fill(str string) bool { - return C.waddstr(w.impl, C.CString(str)) == C.OK +func (w *CursesWindow) Fill(str string) FillReturn { + if C.waddstr(w.impl, C.CString(str)) == C.OK { + return FillContinue + } + return FillSuspend } -func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) bool { +func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn { index := ColorPair{fg, bg, -1}.index() C.wcolor_set(w.impl, C.short(index), nil) C.wattron(w.impl, C.int(attr)) diff --git a/src/tui/tcell.go b/src/tui/tcell.go index b6f0819..3399d32 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -477,7 +477,7 @@ func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) { w.printString(text, pair, attr) } -func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { +func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn { lx := 0 var style tcell.Style @@ -511,7 +511,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { var yPos = w.top + w.lastY if yPos >= (w.top + w.height) { - return false + return FillSuspend } _screen.SetContent(xPos, yPos, r, nil, style) @@ -520,14 +520,14 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { } w.lastX += lx - return true + return FillContinue } -func (w *TcellWindow) Fill(str string) bool { +func (w *TcellWindow) Fill(str string) FillReturn { return w.fillString(str, ColDefault, 0) } -func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) bool { +func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { return w.fillString(str, ColorPair{fg, bg, -1}, a) } diff --git a/src/tui/tui.go b/src/tui/tui.go index eb504f8..33358e8 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -117,6 +117,14 @@ const ( colWhite ) +type FillReturn int + +const ( + FillContinue FillReturn = iota + FillNextLine + FillSuspend +) + type ColorPair struct { fg Color bg Color @@ -216,8 +224,8 @@ type Window interface { MoveAndClear(y int, x int) Print(text string) CPrint(color ColorPair, attr Attr, text string) - Fill(text string) bool - CFill(fg Color, bg Color, attr Attr, text string) bool + Fill(text string) FillReturn + CFill(fg Color, bg Color, attr Attr, text string) FillReturn Erase() } From 2aa739be81c7786eb9fd53606e2cfb4525344e8e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 22:47:26 +0900 Subject: [PATCH 16/28] Fix bug where occurrence of the pattern in header lines are highlighted --- src/terminal.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 0353157..3a89bb4 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -625,7 +625,7 @@ func (t *Terminal) printHeader() { t.move(line, 2, true) t.printHighlighted(&Result{item: item}, - tui.AttrRegular, tui.ColHeader, tui.ColDefault, false) + tui.AttrRegular, tui.ColHeader, tui.ColDefault, false, false) } } @@ -681,14 +681,14 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } else { t.window.CPrint(tui.ColCurrent, t.strong, " ") } - t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true) + t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true) } else { if selected { t.window.CPrint(tui.ColSelected, t.strong, ">") } else { t.window.Print(" ") } - t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false) + t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) } } @@ -744,7 +744,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool { return false } -func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool) { +func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) { item := result.item // Overflow @@ -752,7 +752,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo copy(text, item.text.ToRunes()) matchOffsets := []Offset{} var pos *[]int - if t.merger.pattern != nil { + if match && t.merger.pattern != nil { _, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab) } charOffsets := matchOffsets From d64828ce6d974c016050ec6f3b39c7e6108c39ae Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 23:01:56 +0900 Subject: [PATCH 17/28] Print error message to stderr on unexpected exit --- src/tui/light.go | 2 +- src/tui/ncurses.go | 9 +++------ src/tui/tcell.go | 9 ++------- src/tui/tui.go | 4 +++- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 248477c..0bfc57f 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -241,7 +241,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { c, ok := r.getch(false) if !ok { r.Close() - errorExit() + errorExit("Failed to read /dev/tty") } retries := 0 diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 3e636ba..db7cd83 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -25,7 +25,6 @@ int c_getcurx(WINDOW* win) { import "C" import ( - "fmt" "os" "strconv" "strings" @@ -109,13 +108,11 @@ func (r *FullscreenRenderer) Init() { C.setlocale(C.LC_ALL, C.CString("")) tty := C.c_tty() if tty == nil { - fmt.Println("Failed to open /dev/tty") - errorExit() + errorExit("Failed to open /dev/tty") } _screen = C.c_newterm(tty) if _screen == nil { - fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) - errorExit() + errorExit("Invalid $TERM: " + os.Getenv("TERM")) } C.set_term(_screen) if r.mouse { @@ -380,7 +377,7 @@ func (r *FullscreenRenderer) GetChar() Event { case C.ERR: // Unexpected error from blocking read r.Close() - errorExit() + errorExit("Failed to read /dev/tty") case C.KEY_UP: return Event{Up, 0, nil} case C.KEY_DOWN: diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 3399d32..8de6fe3 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -6,9 +6,6 @@ import ( "time" "unicode/utf8" - "fmt" - "os" - "runtime" // https://github.com/gdamore/tcell/pull/135 @@ -123,12 +120,10 @@ var ( func (r *FullscreenRenderer) initScreen() { s, e := tcell.NewScreen() if e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) - errorExit() + errorExit(e.Error()) } if e = s.Init(); e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) - errorExit() + errorExit(e.Error()) } if r.mouse { s.EnableMouse() diff --git a/src/tui/tui.go b/src/tui/tui.go index 33358e8..4760a38 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -1,6 +1,7 @@ package tui import ( + "fmt" "os" "strconv" "time" @@ -284,7 +285,8 @@ func EmptyTheme() *ColorTheme { Border: colUndefined} } -func errorExit() { +func errorExit(message string) { + fmt.Fprintln(os.Stderr, message) os.Exit(2) } From 48863ac55cc25f8e940cf21f5c32943de90f50c9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 14 Jan 2017 01:04:03 +0900 Subject: [PATCH 18/28] Update invalid $TERM test case --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 494d3cd..e41201f 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1081,7 +1081,7 @@ class TestGoFZF < TestBase end def test_invalid_term - lines = `TERM=xxx #{FZF}` + lines = `TERM=xxx #{FZF} 2>&1` assert_equal 2, $?.exitstatus assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found') end From 5b68027bee72cf32d61795694a07750d6477ee92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 14 Jan 2017 01:10:34 +0900 Subject: [PATCH 19/28] Fix $FZF_COMPLETION_OPTS evaluation Close #799 --- shell/completion.bash | 6 +++--- shell/completion.zsh | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index d6d7238..9c4cdb1 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -148,7 +148,7 @@ __fzf_generic_path_completion() { leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" - matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do + matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) matches=${matches% } @@ -183,7 +183,7 @@ _fzf_complete() { if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} - selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ') + selected=$(cat | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" printf '\e[5n' @@ -215,7 +215,7 @@ _fzf_complete_kill() { local selected fzf fzf="$(__fzfcmd_complete)" - selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') + selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') printf '\e[5n' if [ -n "$selected" ]; then diff --git a/shell/completion.zsh b/shell/completion.zsh index fb2c16a..d32eff1 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -56,7 +56,7 @@ __fzf_generic_path_completion() { [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" dir=${~dir} - matches=$(eval "$compgen $(printf %q "$dir")" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do + matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do echo -n "${(q)item}$suffix " done) matches=${matches% } @@ -99,7 +99,7 @@ _fzf_complete() { fzf="$(__fzfcmd_complete)" _fzf_feed_fifo "$fifo" - matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') + matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$lbuf$matches" fi @@ -164,7 +164,7 @@ fzf-completion() { # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then fzf="$(__fzfcmd_complete)" - matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') + matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" fi From 2720816266013793da99f75df4ab90fb3f4013c2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 04:32:39 +0900 Subject: [PATCH 20/28] [vim] Use /dev/tty as STDIN when using --height w/o explicit source --- plugin/fzf.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 9c9c6db..d1a81fc 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -428,7 +428,8 @@ function! s:execute(dict, command, use_height, temps) abort let command = escaped endif if a:use_height - call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s 2> /dev/tty', &lines, command)) + let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' + call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) else execute 'silent !'.command endif From 03f5ef08c8276f034269dbb8a6e6fd9decf58439 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 13:10:59 +0900 Subject: [PATCH 21/28] Use crypto/ssh/terminal instead of external stty command --- src/tui/light.go | 65 +++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 0bfc57f..2e99aa3 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -11,6 +11,8 @@ import ( "unicode/utf8" "github.com/junegunn/fzf/src/util" + + "golang.org/x/crypto/ssh/terminal" ) const ( @@ -20,10 +22,12 @@ const ( escPollInterval = 5 ) +const consoleDevice string = "/dev/tty" + func openTtyIn() *os.File { - in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) if err != nil { - panic("Failed to open /dev/tty") + panic("Failed to open " + consoleDevice) } return in } @@ -64,7 +68,7 @@ type LightRenderer struct { clickY []int ttyin *os.File buffer []byte - ostty string + origState *terminal.State width int height int yoffset int @@ -104,7 +108,14 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in return &r } +func (r *LightRenderer) fd() int { + return int(r.ttyin.Fd()) +} + func (r *LightRenderer) defaultTheme() *ColorTheme { + if strings.Contains(os.Getenv("TERM"), "256") { + return Dark256 + } colors, err := exec.Command("tput", "colors").Output() if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { return Dark256 @@ -112,17 +123,6 @@ func (r *LightRenderer) defaultTheme() *ColorTheme { return Default16 } -func (r *LightRenderer) stty(cmd string) string { - proc := exec.Command("stty", cmd) - proc.Stdin = r.ttyin - out, err := proc.Output() - if err != nil { - // Not sure how to handle this - panic("stty " + cmd + ": " + err.Error()) - } - return strings.TrimSpace(string(out)) -} - func (r *LightRenderer) findOffset() (row int, col int) { r.csi("6n") r.flush() @@ -167,8 +167,13 @@ func (r *LightRenderer) Init() { } r.escDelay = delay - r.ostty = r.stty("-g") - r.stty("raw") + fd := r.fd() + origState, err := terminal.GetState(fd) + if err != nil { + errorExit(err.Error()) + } + r.origState = origState + terminal.MakeRaw(fd) r.updateTerminalSize() initTheme(r.theme, r.defaultTheme(), r.forceBlack) @@ -212,14 +217,22 @@ func (r *LightRenderer) origin() { r.move(0, 0) } +func getEnv(name string, defaultValue int) int { + env := os.Getenv(name) + if len(env) == 0 { + return defaultValue + } + return atoi(env, defaultValue) +} + func (r *LightRenderer) updateTerminalSize() { - sizes := strings.Split(r.stty("size"), " ") - if len(sizes) < 2 { - r.width = defaultWidth - r.height = r.maxHeightFunc(defaultHeight) + width, height, err := terminal.GetSize(r.fd()) + if err == nil { + r.width = width + r.height = r.maxHeightFunc(height) } else { - r.width = atoi(sizes[1], defaultWidth) - r.height = r.maxHeightFunc(atoi(sizes[0], defaultHeight)) + r.width = getEnv("COLUMNS", defaultWidth) + r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) } } @@ -241,7 +254,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { c, ok := r.getch(false) if !ok { r.Close() - errorExit("Failed to read /dev/tty") + errorExit("Failed to read " + consoleDevice) } retries := 0 @@ -486,13 +499,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event { } func (r *LightRenderer) Pause() { - r.stty(r.ostty) + terminal.Restore(r.fd(), r.origState) r.csi("?1049h") r.flush() } func (r *LightRenderer) Resume() bool { - r.stty("raw") + terminal.MakeRaw(r.fd()) r.csi("?1049l") r.flush() // Should redraw @@ -525,7 +538,7 @@ func (r *LightRenderer) Close() { r.csi("A") } r.flush() - r.stty(r.ostty) + terminal.Restore(r.fd(), r.origState) } func (r *LightRenderer) MaxX() int { From 4ecb7f3a162186fd7758f3bdedb3a0b4bb32f9d0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 13:22:09 +0900 Subject: [PATCH 22/28] Replace --normalize with --literal and enable normalization by default Ref #790 --- CHANGELOG.md | 5 +++-- man/man1/fzf.1 | 5 ++--- shell/completion.bash | 1 + src/options.go | 10 +++++----- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc1e41..22a5b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ CHANGELOG - Added `--height HEIGHT[%]` option - Preview window will truncate long lines by default. Line wrap can be enabled by `:wrap` flag in `--preview-window`. -- Added `--normalize` option to normalize latin script letters before - matching. e.g. `sodanco` can match `Só Danço Samba`. +- Latin script letters will be normalized before matching so that it's easier + to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`. + - Normalization can be disabled via `--literal` 0.15.9 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 2ef3cdd..8980316 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -48,9 +48,8 @@ Case-insensitive match (default: smart-case match) .B "+i" Case-sensitive match .TP -.B "--normalize" -Normalize latin script letters before matching. This is not enabled by default -to avoid performance overhead. +.B "--literal" +Do not normalize latin script letters for matching. .TP .BI "--algo=" TYPE Fuzzy matching algorithm (default: v2) diff --git a/shell/completion.bash b/shell/completion.bash index 9c4cdb1..d17bb19 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -67,6 +67,7 @@ _fzf_opts_completion() { --no-hscroll --jump-labels --height + --literal --reverse --margin --inline-info diff --git a/src/options.go b/src/options.go index bcd2458..a0653d4 100644 --- a/src/options.go +++ b/src/options.go @@ -24,7 +24,7 @@ const usage = `usage: fzf [options] --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2) -i Case-insensitive match (default: smart-case match) +i Case-sensitive match - --normalize Normalize latin script letters before matching + --literal Do not normalize latin script letters before matching -n, --nth=N[,..] Comma-separated list of field index expressions for limiting search scope. Each can be a non-zero integer or a range expression ([BEGIN]..[END]). @@ -190,7 +190,7 @@ func defaultOptions() *Options { FuzzyAlgo: algo.FuzzyMatchV2, Extended: true, Case: CaseSmart, - Normalize: false, + Normalize: true, Nth: make([]Range, 0), WithNth: make([]Range, 0), Delimiter: Delimiter{}, @@ -901,10 +901,10 @@ func parseOptions(opts *Options, allArgs []string) { case "-f", "--filter": filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter - case "--normalize": - opts.Normalize = true - case "--no-normalize": + case "--literal": opts.Normalize = false + case "--no-literal": + opts.Normalize = true case "--algo": opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) case "--expect": From dc3957ce79cdcbcd4314700db11ef0fc32d3363c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 15:06:37 +0900 Subject: [PATCH 23/28] [completion] Add preview window to kill completion --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index d17bb19..86f8e15 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -216,7 +216,7 @@ _fzf_complete_kill() { local selected fzf fzf="$(__fzfcmd_complete)" - selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') + selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap --min-height 15 $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') printf '\e[5n' if [ -n "$selected" ]; then diff --git a/shell/completion.zsh b/shell/completion.zsh index d32eff1..9a33a14 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -164,7 +164,7 @@ fzf-completion() { # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then fzf="$(__fzfcmd_complete)" - matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') + matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap --min-height 15 $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" fi From 208d4f2173c1630408b836312271b3c675ddc5eb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 16:15:51 +0900 Subject: [PATCH 24/28] [shell] Make layout configurable via $FZF_DEFAULT_OPTS and $FZF_{KEY}_OPTS --- shell/completion.bash | 7 +++---- shell/completion.zsh | 7 +++---- shell/key-bindings.bash | 11 +++++------ shell/key-bindings.fish | 24 ++++++++++++++++++------ shell/key-bindings.zsh | 10 +++++----- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 86f8e15..ad778d1 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -35,8 +35,7 @@ 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" + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } _fzf_orig_completion_filter() { @@ -149,7 +148,7 @@ __fzf_generic_path_completion() { leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" - matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do + matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) matches=${matches% } @@ -184,7 +183,7 @@ _fzf_complete() { if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} - selected=$(cat | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ') + selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" printf '\e[5n' diff --git a/shell/completion.zsh b/shell/completion.zsh index 9a33a14..850aab6 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -32,8 +32,7 @@ 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" + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } __fzf_generic_path_completion() { @@ -56,7 +55,7 @@ __fzf_generic_path_completion() { [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" dir=${~dir} - matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do + matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do echo -n "${(q)item}$suffix " done) matches=${matches% } @@ -99,7 +98,7 @@ _fzf_complete() { fzf="$(__fzfcmd_complete)" _fzf_feed_fifo "$fifo" - matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') + matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$lbuf$matches" fi diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 382302d..06e9656 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 --height ${FZF_TMUX_HEIGHT:-40%} --reverse -m $@ $FZF_CTRL_T_OPTS" | while read -r item; do + 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 printf '%q ' "$item" done echo @@ -19,8 +19,7 @@ __fzf_use_tmux__() { __fzfcmd() { __fzf_use_tmux__ && - echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || - echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } __fzf_select_tmux__() { @@ -39,7 +38,7 @@ fzf-file-widget() { if __fzf_use_tmux__; then __fzf_select_tmux__ else - local selected="$(__fzf_select__ --height ${FZF_TMUX_HEIGHT:-40%} --reverse)" + local selected="$(__fzf_select__)" READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" READLINE_POINT=$(( READLINE_POINT + ${#selected} )) fi @@ -49,7 +48,7 @@ __fzf_cd__() { local cmd dir cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" - dir=$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir" + dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir" } __fzf_history__() ( @@ -57,7 +56,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index fc61844..3fb7da8 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -21,7 +21,11 @@ function fzf_key_bindings -o -type d -print \ -o -type l -print 2> /dev/null | sed 's#^\./##'" - eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m $FZF_CTRL_T_OPTS" | while read -l r; set result $result $r; end + set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" + eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end + end if [ -z "$result" ] commandline -f repaint return @@ -39,8 +43,12 @@ function fzf_key_bindings end function fzf-history-widget -d "Show command history" - history | eval (__fzfcmd) +s +m --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result - and commandline -- $result + set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS +m" + history | eval (__fzfcmd) -q '(commandline)' | read -l result + and commandline -- $result + end commandline -f repaint end @@ -48,8 +56,12 @@ function fzf_key_bindings set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-" - eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS" | read -l result - [ "$result" ]; and cd $result + set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" + eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result + [ "$result" ]; and cd $result + end commandline -f repaint end @@ -59,7 +71,7 @@ function fzf_key_bindings if [ $FZF_TMUX -eq 1 ] echo "fzf-tmux -d$FZF_TMUX_HEIGHT" else - echo "fzf --height $FZF_TMUX_HEIGHT --reverse" + echo "fzf" end end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 7e24d92..c8705ad 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -9,7 +9,7 @@ __fsel() { -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" setopt localoptions pipefail 2> /dev/null - eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do + eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do echo -n "${(q)item} " done local ret=$? @@ -23,8 +23,7 @@ __fzf_use_tmux__() { __fzfcmd() { __fzf_use_tmux__ && - echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || - echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } fzf-file-widget() { @@ -42,7 +41,7 @@ fzf-cd-widget() { local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" setopt localoptions pipefail 2> /dev/null - cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}" + cd "${$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m):-.}" local ret=$? zle reset-prompt typeset -f zle-line-init >/dev/null && zle zle-line-init @@ -55,7 +54,8 @@ 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 --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) + selected=( $(fc -l 1 | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] From e0036b5ad208f71d02447c233a621e67185b0fff Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 19:42:28 +0900 Subject: [PATCH 25/28] Add --filepath-word option Close #802 --- CHANGELOG.md | 2 ++ man/man1/fzf.1 | 12 ++++++++++++ src/options.go | 7 +++++++ src/terminal.go | 19 +++++++++++++++---- test/test_go.rb | 13 +++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a5b75..d5802c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ CHANGELOG - Latin script letters will be normalized before matching so that it's easier to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`. - Normalization can be disabled via `--literal` +- Added `--filepath-word` to make word-wise movements/actions (`alt-b`, + `alt-f`, `alt-bs`, `alt-d`) respect path separators 0.15.9 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8980316..accf3f2 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -129,6 +129,18 @@ Number of screen columns to keep to the right of the highlighted substring (default: 10). Setting it to a large value will cause the text to be positioned on the center of the screen. .TP +.B "--filepath-word" +Make word-wise movements and actions respect path separators. The following +actions are affected: + +\fBbackward-kill-word\fR +.br +\fBbackward-word\fR +.br +\fBforward-word\fR +.br +\fBkill-word\fR +.TP .BI "--jump-labels=" "CHARS" Label characters for \fBjump\fR and \fBjump-accept\fR .SS Layout diff --git a/src/options.go b/src/options.go index a0653d4..7885325 100644 --- a/src/options.go +++ b/src/options.go @@ -45,6 +45,7 @@ const usage = `usage: fzf [options] --no-hscroll Disable horizontal scroll --hscroll-off=COL Number of screen columns to keep to the right of the highlighted substring (default: 10) + --filepath-word Make word-wise movements respect path separators --jump-labels=CHARS Label characters for jump and jump-accept Layout @@ -160,6 +161,7 @@ type Options struct { Cycle bool Hscroll bool HscrollOff int + FileWord bool InlineInfo bool JumpLabels string Prompt string @@ -208,6 +210,7 @@ func defaultOptions() *Options { Cycle: false, Hscroll: true, HscrollOff: 10, + FileWord: false, InlineInfo: false, JumpLabels: defaultJumpLabels, Prompt: "> ", @@ -976,6 +979,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Hscroll = false case "--hscroll-off": opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") + case "--filepath-word": + opts.FileWord = true + case "--no-filepath-word": + opts.FileWord = false case "--inline-info": opts.InlineInfo = true case "--no-inline-info": diff --git a/src/terminal.go b/src/terminal.go index 3a89bb4..69735d9 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -59,6 +59,8 @@ type Terminal struct { reverse bool hscroll bool hscrollOff int + wordRubout string + wordNext string cx int cy int offset int @@ -291,6 +293,13 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } else { renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) } + wordRubout := "[^[:alnum:]][[:alnum:]]" + wordNext := "[[:alnum:]][^[:alnum:]]|(.$)" + if opts.FileWord { + sep := regexp.QuoteMeta(string(os.PathSeparator)) + wordRubout = fmt.Sprintf("%s[^%s]", sep, sep) + wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) + } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, @@ -298,6 +307,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { reverse: opts.Reverse, hscroll: opts.Hscroll, hscrollOff: opts.HscrollOff, + wordRubout: wordRubout, + wordNext: wordNext, cx: len(input), cy: 0, offset: 0, @@ -1448,7 +1459,7 @@ func (t *Terminal) Loop() { } case actBackwardKillWord: if t.cx > 0 { - t.rubout("[^[:alnum:]][[:alnum:]]") + t.rubout(t.wordRubout) } case actYank: suffix := copySlice(t.input[t.cx:]) @@ -1467,12 +1478,12 @@ func (t *Terminal) Loop() { t.jumping = jumpAcceptEnabled req(reqJump) case actBackwardWord: - t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 + t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1 case actForwardWord: - t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1 case actKillWord: ncx := t.cx + - findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1 if ncx > t.cx { t.yanked = copySlice(t.input[t.cx:ncx]) t.input = append(t.input[:t.cx], t.input[ncx:]...) diff --git a/test/test_go.rb b/test/test_go.rb index e41201f..6a8e3bf 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -322,6 +322,19 @@ class TestGoFZF < TestBase tmux.until { |lines| lines.last !~ /^>/ } end + def test_file_word + tmux.send_keys "#{FZF} -q '--/foo bar/foo-bar/baz' --filepath-word", :Enter + tmux.until { |lines| lines.last =~ /^>/ } + + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :d + tmux.send_keys :Escape, :f + tmux.send_keys :Escape, :BSpace + tmux.until { |lines| lines.last == '> --///baz' } + end + def test_multi_order tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter tmux.until { |lines| lines.last =~ /^>/ } From 44d3faa048df85355ab59ea8b9b9d52dd288532a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 22:02:04 +0900 Subject: [PATCH 26/28] [completion] Restore --height option for kill completion --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index ad778d1..6ef0386 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -215,7 +215,7 @@ _fzf_complete_kill() { local selected fzf fzf="$(__fzfcmd_complete)" - selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap --min-height 15 $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') + selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') printf '\e[5n' if [ -n "$selected" ]; then diff --git a/shell/completion.zsh b/shell/completion.zsh index 850aab6..9b9dba3 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -163,7 +163,7 @@ fzf-completion() { # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then fzf="$(__fzfcmd_complete)" - matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap --min-height 15 $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') + matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" fi From ede7bfb90105b7df5bf115a837e307ac3f2540fe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Jan 2017 02:26:36 +0900 Subject: [PATCH 27/28] Optimize LightRenderer for slow terminals --- src/terminal.go | 39 +++++++++++++++++++++++---------------- src/tui/light.go | 23 ++++++++++++++++++----- src/tui/ncurses.go | 4 ++++ src/tui/tcell.go | 4 ++++ src/tui/tui.go | 1 + 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 69735d9..02c8f1f 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -46,6 +46,7 @@ type itemLine struct { current bool selected bool label string + width int result Result } @@ -678,13 +679,17 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } // Avoid unnecessary redraw - newLine := itemLine{current, selected, label, *result} - if t.prevLines[i] == newLine { + newLine := itemLine{current: current, selected: selected, label: label, result: *result, width: 0} + prevLine := t.prevLines[i] + if prevLine.current == newLine.current && + prevLine.selected == newLine.selected && + prevLine.label == newLine.label && + prevLine.result == newLine.result { return } - t.prevLines[i] = newLine - t.move(line, 0, true) + // Optimized renderer can simply erase to the end of the window + t.move(line, 0, t.tui.IsOptimized()) t.window.CPrint(tui.ColCursor, t.strong, label) if current { if selected { @@ -692,15 +697,22 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } else { t.window.CPrint(tui.ColCurrent, t.strong, " ") } - t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true) + newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true) } else { if selected { t.window.CPrint(tui.ColSelected, t.strong, ">") } else { t.window.Print(" ") } - t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) + newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) } + if !t.tui.IsOptimized() { + fillSpaces := prevLine.width - newLine.width + if fillSpaces > 0 { + t.window.Print(strings.Repeat(" ", fillSpaces)) + } + } + t.prevLines[i] = newLine } func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) { @@ -745,17 +757,10 @@ func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) { } func (t *Terminal) overflow(runes []rune, max int) bool { - l := 0 - for _, r := range runes { - l += util.RuneWidth(r, l, t.tabstop) - if l > max { - return true - } - } - return false + return t.displayWidthWithLimit(runes, 0, max) > max } -func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) { +func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int { item := result.item // Overflow @@ -783,7 +788,8 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) maxWidth := t.window.Width() - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) - if t.overflow(text, maxWidth) { + displayWidth := t.displayWidthWithLimit(text, 0, maxWidth) + if displayWidth > maxWidth { if t.hscroll { // Stri.. if !t.overflow(text[:maxe], maxWidth-2) { @@ -845,6 +851,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo substr, _ = t.processTabs(text[index:], prefixWidth) t.window.CPrint(col1, attr, substr) } + return displayWidth } func numLinesMax(str string, max int) int { diff --git a/src/tui/light.go b/src/tui/light.go index 2e99aa3..075fd6b 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -32,13 +32,18 @@ func openTtyIn() *os.File { return in } -// FIXME: Need better handling of non-displayable characters func (r *LightRenderer) stderr(str string) { + r.stderrInternal(str, true) +} + +// FIXME: Need better handling of non-displayable characters +func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { bytes := []byte(str) runes := []rune{} for len(bytes) > 0 { r, sz := utf8.DecodeRune(bytes) - if r == utf8.RuneError || r != '\x1b' && r != '\n' && r != '\r' && r < 32 { + if r == utf8.RuneError || r < 32 && + r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') { runes = append(runes, '?') } else { runes = append(runes, r) @@ -553,6 +558,10 @@ func (r *LightRenderer) DoesAutoWrap() bool { return true } +func (r *LightRenderer) IsOptimized() bool { + return false +} + func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { w := &LightWindow{ renderer: r, @@ -594,6 +603,10 @@ func (w *LightWindow) stderr(str string) { w.renderer.stderr(str) } +func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { + w.renderer.stderrInternal(str, allowNLCR) +} + func (w *LightWindow) Top() int { return w.top } @@ -703,7 +716,7 @@ func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { } else { w.csiColor(pair.Fg(), pair.Bg(), attr) } - w.stderr(text) + w.stderrInternal(text, false) w.csi("m") } @@ -711,7 +724,7 @@ func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { if w.csiColor(fg, bg, attr) { defer w.csi("m") } - w.stderr(text) + w.stderrInternal(text, false) } type wrappedLine struct { @@ -754,7 +767,7 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn { } return FillNextLine } - w.stderr(wl.text) + w.stderrInternal(wl.text, false) w.posx += wl.displayWidth if j < len(lines)-1 || i < len(allLines)-1 { if w.posy+1 >= w.height { diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index db7cd83..0978ea8 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -279,6 +279,10 @@ func (r *FullscreenRenderer) DoesAutoWrap() bool { return true } +func (r *FullscreenRenderer) IsOptimized() bool { + return true +} + func (w *CursesWindow) Fill(str string) FillReturn { if C.waddstr(w.impl, C.CString(str)) == C.OK { return FillContinue diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 8de6fe3..c898a38 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -158,6 +158,10 @@ func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } +func (r *FullscreenRenderer) IsOptimized() bool { + return false +} + func (r *FullscreenRenderer) Clear() { _screen.Sync() _screen.Clear() diff --git a/src/tui/tui.go b/src/tui/tui.go index 4760a38..fd4a21e 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -204,6 +204,7 @@ type Renderer interface { MaxX() int MaxY() int DoesAutoWrap() bool + IsOptimized() bool NewWindow(top int, left int, width int, height int, border bool) Window } From 4bece042077dbf2a37e4f6a9e03e34e2cd20014c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Jan 2017 02:39:37 +0900 Subject: [PATCH 28/28] 0.16.0 --- CHANGELOG.md | 3 ++- install | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5802c8..d46df32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ CHANGELOG 0.16.0 ------ -- Added `--height HEIGHT[%]` option +- *Added `--height HEIGHT[%]` option* + - fzf can now display finder without occupying the full screen - Preview window will truncate long lines by default. Line wrap can be enabled by `:wrap` flag in `--preview-window`. - Latin script letters will be normalized before matching so that it's easier diff --git a/install b/install index 7a2481f..3c338fb 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.0-alpha +version=0.16.0 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index accf3f2..a7abf84 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -147,7 +147,7 @@ Label characters for \fBjump\fR and \fBjump-accept\fR .TP .BI "--height=" "HEIGHT[%]" Display fzf window below the cursor with the given height instead of using -fullscreen. +the full screen. .TP .BI "--min-height=" "HEIGHT" Minimum height when \fB--height\fR is given in percent (default: 10). diff --git a/src/constants.go b/src/constants.go index 9640dbe..1abc9b9 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.0-alpha" + version = "0.16.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond