Merge pull request #794 from junegunn/devel

0.16.0
This commit is contained in:
Junegunn Choi 2017-01-16 02:43:56 +09:00 committed by GitHub
commit 05ed57a9f0
33 changed files with 2487 additions and 736 deletions

View File

@ -2,7 +2,7 @@ language: ruby
matrix: matrix:
include: include:
- env: TAGS= - env: TAGS=
rvm: 2.2.0 rvm: 2.3.3
# - env: TAGS=tcell # - env: TAGS=tcell
# rvm: 2.2.0 # rvm: 2.2.0

View File

@ -1,6 +1,18 @@
CHANGELOG CHANGELOG
========= =========
0.16.0
------
- *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
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 0.15.9
------ ------
- Fixed rendering glitches introduced in 0.15.8 - Fixed rendering glitches introduced in 0.15.8

View File

@ -123,6 +123,29 @@ vim $(fzf)
- Mouse: scroll, click, double-click; shift-click and shift-scroll on - Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode 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 #### Search syntax
Unless otherwise specified, fzf starts in "extended-search mode" where you can 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]` 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. 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 Key bindings for command line
----------------------------- -----------------------------
@ -206,9 +236,9 @@ fish.
- Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_COMMAND` to override the default command
- Set `FZF_ALT_C_OPTS` to pass additional options - 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 If you're on a tmux session, you can start fzf in a split pane by setting
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the `FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT`
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). (e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source 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 ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
@ -466,12 +496,9 @@ valid directory. Example:
set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" 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 Copyright (c) 2017 Junegunn Choi
------
Junegunn Choi

View File

@ -114,6 +114,9 @@ if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$l
exit $? exit $?
fi fi
# --height option is not allowed
args+=("--no-height")
# Handle zoomed tmux pane by moving it to a temp window # Handle zoomed tmux pane by moving it to a temp window
if tmux list-panes -F '#F' | grep -q Z; then if tmux list-panes -F '#F' | grep -q Z; then
zoomed=1 zoomed=1

13
install
View File

@ -2,9 +2,7 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.15.9 pre=1 || version=0.16.0
version=0.15.9 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@ -48,7 +46,7 @@ for opt in "$@"; do
--no-update-rc) update_config=0 ;; --no-update-rc) update_config=0 ;;
--32) binary_arch=386 ;; --32) binary_arch=386 ;;
--64) binary_arch=amd64 ;; --64) binary_arch=amd64 ;;
--bin|--pre) ;; --bin) ;;
*) *)
echo "unknown option: $opt" echo "unknown option: $opt"
help help
@ -121,7 +119,7 @@ try_wget() {
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [ $pre = 0 ]; then if [[ ! "$version" =~ alpha ]]; then
if [ -x "$fzf_base"/bin/fzf ]; then if [ -x "$fzf_base"/bin/fzf ]; then
echo " - Already exists" echo " - Already exists"
check_binary && return check_binary && return
@ -137,7 +135,10 @@ download() {
return return
fi 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 set -o pipefail
if ! (try_curl $url || try_wget $url); then if ! (try_curl $url || try_wget $url); then
set +o pipefail set +o pipefail

View File

@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. 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 .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. 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 .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@ -48,6 +48,9 @@ Case-insensitive match (default: smart-case match)
.B "+i" .B "+i"
Case-sensitive match Case-sensitive match
.TP .TP
.B "--literal"
Do not normalize latin script letters for matching.
.TP
.BI "--algo=" TYPE .BI "--algo=" TYPE
Fuzzy matching algorithm (default: v2) Fuzzy matching algorithm (default: v2)
@ -126,10 +129,30 @@ 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 (default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen. on the center of the screen.
.TP .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" .BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR Label characters for \fBjump\fR and \fBjump-accept\fR
.SS Layout .SS Layout
.TP .TP
.BI "--height=" "HEIGHT[%]"
Display fzf window below the cursor with the given height instead of using
the full screen.
.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" .B "--reverse"
Reverse orientation Reverse orientation
.TP .TP
@ -185,7 +208,9 @@ Number of spaces for a tab character (default: 8)
.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color configuration. The name of the base color scheme is followed by custom Color configuration. The name of the base color scheme is followed by custom
color mappings. Ansi color code of -1 denotes terminal default 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 .RS
e.g. \fBfzf --color=bg+:24\fR e.g. \fBfzf --color=bg+:24\fR
@ -248,10 +273,11 @@ e.g. \fBfzf --preview="head -$LINES {}"\fR
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
.RE .RE
.TP .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 Determine the layout of the preview window. If the argument ends with
\fB:hidden\fR, the preview window will be hidden by default until \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 .RS
.B POSITION: (default: right) .B POSITION: (default: right)

View File

@ -297,14 +297,25 @@ try
else else
let prefix = '' let prefix = ''
endif 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')) &&
\ 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
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 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) return s:execute_term(dict, command, temps)
endif 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) call s:callback(dict, lines)
return lines return lines
finally finally
@ -401,9 +412,9 @@ function! s:exit_handler(code, command, ...)
return 1 return 1
endfunction endfunction
function! s:execute(dict, command, temps) abort function! s:execute(dict, command, use_height, temps) abort
call s:pushd(a:dict) call s:pushd(a:dict)
if has('unix') if has('unix') && !a:use_height
silent! !clear 2> /dev/null silent! !clear 2> /dev/null
endif endif
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
@ -417,7 +428,12 @@ function! s:execute(dict, command, temps) abort
else else
let command = escaped let command = escaped
endif endif
execute 'silent !'.command if a:use_height
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
let exit_status = v:shell_error let exit_status = v:shell_error
redraw! redraw!
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : [] return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []

View File

@ -5,7 +5,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/-completion.bash # /_/ /___/_/-completion.bash
# #
# - $FZF_TMUX (default: 1) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
@ -30,6 +30,14 @@ 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"
}
_fzf_orig_completion_filter() { _fzf_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}' awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
@ -43,35 +51,43 @@ _fzf_opts_completion() {
opts=" opts="
-x --extended -x --extended
-e --exact -e --exact
--algo
-i +i -i +i
-n --nth -n --nth
--with-nth
-d --delimiter -d --delimiter
+s --no-sort +s --no-sort
--tac --tac
--tiebreak --tiebreak
--bind
-m --multi -m --multi
--no-mouse --no-mouse
--color --bind
--black --cycle
--reverse
--no-hscroll --no-hscroll
--jump-labels
--height
--literal
--reverse
--margin
--inline-info --inline-info
--prompt --prompt
--header
--header-lines
--ansi
--tabstop
--color
--no-bold
--history
--history-size
--preview
--preview-window
-q --query -q --query
-1 --select-1 -1 --select-1
-0 --exit-0 -0 --exit-0
-f --filter -f --filter
--print-query --print-query
--expect --expect
--toggle-sort --sync"
--sync
--cycle
--history
--history-size
--header
--header-lines
--margin"
case "${prev}" in case "${prev}" in
--tiebreak) --tiebreak)
@ -116,7 +132,7 @@ _fzf_handle_dynamic_completion() {
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf 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_=]/_}" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
@ -132,8 +148,7 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
tput sc 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
matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do
printf "%q$3 " "$item" printf "%q$3 " "$item"
done) done)
matches=${matches% } matches=${matches% }
@ -142,7 +157,7 @@ __fzf_generic_path_completion() {
else else
COMPREPLY=( "$cur" ) COMPREPLY=( "$cur" )
fi fi
tput rc printf '\e[5n'
return 0 return 0
fi fi
dir=$(dirname "$dir") dir=$(dirname "$dir")
@ -160,7 +175,7 @@ _fzf_complete() {
local cur selected trigger cmd fzf post local cur selected trigger cmd fzf post
post="$(caller 0 | awk '{print $2}')_post" post="$(caller 0 | awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post=cat 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_=]/_}" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
@ -168,10 +183,9 @@ _fzf_complete() {
if [[ "$cur" == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
tput sc 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=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
tput rc printf '\e[5n'
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
@ -200,10 +214,9 @@ _fzf_complete_kill() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected fzf local selected fzf
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
tput sc 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' ' ')
selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') printf '\e[5n'
tput rc
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
COMPREPLY=( "$selected" ) COMPREPLY=( "$selected" )

View File

@ -5,7 +5,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/-completion.zsh # /_/ /___/_/-completion.zsh
# #
# - $FZF_TMUX (default: 1) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
@ -30,6 +30,11 @@ fi
########################################################### ###########################################################
__fzfcmd_complete() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar" # (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
@ -39,7 +44,7 @@ __fzf_generic_path_completion() {
fzf_opts=$4 fzf_opts=$4
suffix=$5 suffix=$5
tail=$6 tail=$6
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
setopt localoptions nonomatch setopt localoptions nonomatch
dir="$base" dir="$base"
@ -50,7 +55,7 @@ __fzf_generic_path_completion() {
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$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="--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 " echo -n "${(q)item}$suffix "
done) done)
matches=${matches% } matches=${matches% }
@ -90,10 +95,10 @@ _fzf_complete() {
post="${funcstack[2]}_post" post="${funcstack[2]}_post"
type $post > /dev/null 2>&1 || post=cat 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" _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="--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 if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
@ -157,8 +162,8 @@ fzf-completion() {
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence) # Kill completion (do not require trigger sequence)
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then 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' ' ') 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 if [ -n "$matches" ]; then
LBUFFER="$LBUFFER$matches" LBUFFER="$LBUFFER$matches"
fi fi

View File

@ -5,7 +5,7 @@ __fzf_select__() {
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}" -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_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" printf '%q ' "$item"
done done
echo echo
@ -13,8 +13,13 @@ __fzf_select__() {
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
__fzfcmd() { __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"
} }
__fzf_select_tmux__() { __fzf_select_tmux__() {
@ -26,7 +31,7 @@ __fzf_select_tmux__() {
height="-l $height" height="-l $height"
fi 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() { fzf-file-widget() {
@ -43,7 +48,7 @@ __fzf_cd__() {
local cmd dir local cmd dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ 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-"}" -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__() ( __fzf_history__() (
@ -51,7 +56,7 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
eval "$(__fzfcmd) +s --tac +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]') && command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
@ -60,22 +65,15 @@ __fzf_history__() (
fi 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 if [[ ! -o vi ]]; then
# Required to refresh the prompt after fzf # Required to refresh the prompt after fzf
bind '"\er": redraw-current-line' bind '"\er": redraw-current-line'
bind '"\e^": history-expand-line' bind '"\e^": history-expand-line'
# CTRL-T - Paste the selected file path into the command 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"' 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"' 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 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"' 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 +100,9 @@ else
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position # - 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"' 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"' 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 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 "' 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 +118,4 @@ else
bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"' bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
fi fi
unset -v __use_tmux __use_bind_x
fi fi

View File

@ -21,7 +21,11 @@ function fzf_key_bindings
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 's#^\./##'" -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" ] if [ -z "$result" ]
commandline -f repaint commandline -f repaint
return return
@ -39,8 +43,12 @@ function fzf_key_bindings
end end
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
history | eval (__fzfcmd) +s +m --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
and commandline -- $result 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 commandline -f repaint
end end
@ -48,19 +56,20 @@ function fzf_key_bindings
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-" -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 set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
[ "$result" ]; and cd $result 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 commandline -f repaint
end end
function __fzfcmd 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 [ $FZF_TMUX -eq 1 ]
if set -q FZF_TMUX_HEIGHT echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
else
echo "fzf-tmux -d40%"
end
else else
echo "fzf" echo "fzf"
end end

View File

@ -9,7 +9,7 @@ __fsel() {
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}" -o -type l -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null 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} " echo -n "${(q)item} "
done done
local ret=$? local ret=$?
@ -17,8 +17,13 @@ __fsel() {
return $ret return $ret
} }
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
__fzfcmd() { __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"
} }
fzf-file-widget() { fzf-file-widget() {
@ -36,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 \ 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-"}" -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null 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=$? local ret=$?
zle reset-prompt zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init typeset -f zle-line-init >/dev/null && zle zle-line-init
@ -49,7 +54,8 @@ bindkey '\ec' fzf-cd-widget
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst pipefail 2> /dev/null 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 |
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=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]

View File

@ -234,9 +234,24 @@ func bonusAt(input util.Chars, idx int) int16 {
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) 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
}
// 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) {
// Assume that pattern is given in lowercase if case-insensitive. // Assume that pattern is given in lowercase if case-insensitive.
// First check if there's a match and calculate bonus for each position. // 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 // If the input string is too long, consider finding the matching chars in
@ -247,13 +262,13 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
case 0: case 0:
return Result{0, 0, 0}, posArray(withPos, M) return Result{0, 0, 0}, posArray(withPos, M)
case 1: 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, // Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm. // we fall back to the greedy algorithm.
if slab != nil && N*M > cap(slab.I16) { 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)
} }
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
@ -285,6 +300,10 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
} }
} }
if normalize {
char = normalizeRune(char)
}
T[idx] = char T[idx] = char
B[idx] = bonusFor(prevClass, class) B[idx] = bonusFor(prevClass, class)
prevClass = class prevClass = class
@ -432,7 +451,7 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
} }
// Implement the same sorting criteria as V2 // 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) pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern)) pos := posArray(withPos, len(pattern))
prevClass := charNonWord prevClass := charNonWord
@ -449,6 +468,10 @@ func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx in
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
// pattern is already normalized
if normalize {
char = normalizeRune(char)
}
if char == pattern[pidx] { if char == pattern[pidx] {
if withPos { if withPos {
*pos = append(*pos, idx) *pos = append(*pos, idx)
@ -488,7 +511,7 @@ func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx in
} }
// FuzzyMatchV1 performs fuzzy-match // 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 { if len(pattern) == 0 {
return Result{0, 0, 0}, nil return Result{0, 0, 0}, nil
} }
@ -514,6 +537,9 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if normalize {
char = normalizeRune(char)
}
pchar := pattern[indexAt(pidx, lenPattern, forward)] pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar { if char == pchar {
if sidx < 0 { if sidx < 0 {
@ -553,7 +579,7 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r
sidx, eidx = lenRunes-eidx, lenRunes-sidx 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{sidx, eidx, score}, pos
} }
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
@ -568,7 +594,7 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r
// bonus point, instead of stopping immediately after finding the first match. // 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 solution is much cheaper since there is only one possible alignment of
// the pattern. // 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 { if len(pattern) == 0 {
return Result{0, 0, 0}, nil return Result{0, 0, 0}, nil
} }
@ -593,6 +619,9 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if normalize {
char = normalizeRune(char)
}
pidx_ := indexAt(pidx, lenPattern, forward) pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_] pchar := pattern[pidx_]
if pchar == char { if pchar == char {
@ -624,14 +653,14 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern
sidx = lenRunes - (bestPos + 1) sidx = lenRunes - (bestPos + 1)
eidx = lenRunes - (bestPos - lenPattern + 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{sidx, eidx, score}, nil
} }
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
// PrefixMatch performs prefix-match // 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 { if len(pattern) == 0 {
return Result{0, 0, 0}, nil return Result{0, 0, 0}, nil
} }
@ -645,17 +674,20 @@ func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
if normalize {
char = normalizeRune(char)
}
if char != r { if char != r {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
} }
lenPattern := len(pattern) 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 return Result{0, lenPattern, score}, nil
} }
// SuffixMatch performs suffix-match // 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() lenRunes := text.Length()
trimmedLen := lenRunes - text.TrailingWhitespaces() trimmedLen := lenRunes - text.TrailingWhitespaces()
if len(pattern) == 0 { if len(pattern) == 0 {
@ -671,6 +703,9 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
if normalize {
char = normalizeRune(char)
}
if char != r { if char != r {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
@ -678,21 +713,37 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru
lenPattern := len(pattern) lenPattern := len(pattern)
sidx := trimmedLen - lenPattern sidx := trimmedLen - lenPattern
eidx := trimmedLen 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 return Result{sidx, eidx, score}, nil
} }
// EqualMatch performs equal-match // 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) lenPattern := len(pattern)
if text.Length() != lenPattern { if text.Length() != lenPattern {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
runesStr := text.ToString() match := true
if !caseSensitive { if normalize {
runesStr = strings.ToLower(runesStr) 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 + return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil (bonusFirstCharMultiplier-1)*bonusBoundary}, nil
} }

View File

@ -10,10 +10,14 @@ import (
) )
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { 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 { if !caseSensitive {
pattern = strings.ToLower(pattern) 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 var start, end int
if pos == nil || len(*pos) == 0 { if pos == nil || len(*pos) == 0 {
start = res.Start 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) { func TestLongString(t *testing.T) {
bytes := make([]byte, math.MaxUint16*2) bytes := make([]byte, math.MaxUint16*2)
for i := range bytes { for i := range bytes {

424
src/algo/normalize.go Normal file
View File

@ -0,0 +1,424 @@
// 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
}
// 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
}

View File

@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
version = "0.15.9" version = "0.16.0"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@ -143,7 +143,7 @@ func Run(opts *Options) {
} }
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( 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) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)

View File

@ -10,6 +10,7 @@ import (
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-shellwords" "github.com/junegunn/go-shellwords"
) )
@ -23,6 +24,7 @@ const usage = `usage: fzf [options]
--algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2) --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2)
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i Case-sensitive match
--literal Do not normalize latin script letters before matching
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]). integer or a range expression ([BEGIN]..[END]).
@ -43,9 +45,14 @@ const usage = `usage: fzf [options]
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--hscroll-off=COL Number of screen columns to keep to the right of the --hscroll-off=COL Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump and jump-accept --jump-labels=CHARS Label characters for jump and jump-accept
Layout 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 --reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
@ -135,6 +142,7 @@ type Options struct {
FuzzyAlgo algo.Algo FuzzyAlgo algo.Algo
Extended bool Extended bool
Case Case Case Case
Normalize bool
Nth []Range Nth []Range
WithNth []Range WithNth []Range
Delimiter Delimiter Delimiter Delimiter
@ -147,10 +155,13 @@ type Options struct {
Theme *tui.ColorTheme Theme *tui.ColorTheme
Black bool Black bool
Bold bool Bold bool
Height sizeSpec
MinHeight int
Reverse bool Reverse bool
Cycle bool Cycle bool
Hscroll bool Hscroll bool
HscrollOff int HscrollOff int
FileWord bool
InlineInfo bool InlineInfo bool
JumpLabels string JumpLabels string
Prompt string Prompt string
@ -181,6 +192,7 @@ func defaultOptions() *Options {
FuzzyAlgo: algo.FuzzyMatchV2, FuzzyAlgo: algo.FuzzyMatchV2,
Extended: true, Extended: true,
Case: CaseSmart, Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: Delimiter{}, Delimiter: Delimiter{},
@ -193,10 +205,12 @@ func defaultOptions() *Options {
Theme: tui.EmptyTheme(), Theme: tui.EmptyTheme(),
Black: false, Black: false,
Bold: true, Bold: true,
MinHeight: 10,
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
FileWord: false,
InlineInfo: false, InlineInfo: false,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
@ -482,6 +496,7 @@ func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
theme := dupeTheme(defaultTheme) theme := dupeTheme(defaultTheme)
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
for _, str := range strings.Split(strings.ToLower(str), ",") { for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str { switch str {
case "dark": case "dark":
@ -505,11 +520,17 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
if len(pair) != 2 { if len(pair) != 2 {
fail() fail()
} }
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 { var ansi tui.Color
fail() 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] { switch pair[0] {
case "fg": case "fg":
theme.Fg = ansi theme.Fg = ansi
@ -760,6 +781,14 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
return sizeSpec{val, percent} 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) { func parsePreviewWindow(opts *previewOpts, input string) {
// Default // Default
opts.position = posRight opts.position = posRight
@ -875,6 +904,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "-f", "--filter": case "-f", "--filter":
filter := nextString(allArgs, &i, "query string required") filter := nextString(allArgs, &i, "query string required")
opts.Filter = &filter opts.Filter = &filter
case "--literal":
opts.Normalize = false
case "--no-literal":
opts.Normalize = true
case "--algo": case "--algo":
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
case "--expect": case "--expect":
@ -946,6 +979,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Hscroll = false opts.Hscroll = false
case "--hscroll-off": case "--hscroll-off":
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
case "--filepath-word":
opts.FileWord = true
case "--no-filepath-word":
opts.FileWord = false
case "--inline-info": case "--inline-info":
opts.InlineInfo = true opts.InlineInfo = true
case "--no-inline-info": case "--no-inline-info":
@ -1003,6 +1040,12 @@ func parseOptions(opts *Options, allArgs []string) {
case "--preview-window": case "--preview-window":
parsePreviewWindow(&opts.Preview, parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) 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 "--min-height":
opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT")
case "--no-height":
opts.Height = sizeSpec{}
case "--no-margin": case "--no-margin":
opts.Margin = defaultMargin() opts.Margin = defaultMargin()
case "--margin": case "--margin":
@ -1029,6 +1072,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value) opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s", "--sort="); match { } else if match, _ := optString(arg, "-s", "--sort="); match {
opts.Sort = 1 // Don't care 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 { } else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value) parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {

View File

@ -43,6 +43,7 @@ type Pattern struct {
fuzzyAlgo algo.Algo fuzzyAlgo algo.Algo
extended bool extended bool
caseSensitive bool caseSensitive bool
normalize bool
forward bool forward bool
text []rune text []rune
termSets []termSet termSets []termSet
@ -75,7 +76,7 @@ func clearChunkCache() {
} }
// BuildPattern builds Pattern object from the given arguments // 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 { cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
@ -94,7 +95,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
termSets := []termSet{} termSets := []termSet{}
if extended { if extended {
termSets = parseTerms(fuzzy, caseMode, asString) termSets = parseTerms(fuzzy, caseMode, normalize, asString)
Loop: Loop:
for _, termSet := range termSets { for _, termSet := range termSets {
for idx, term := range termSet { for idx, term := range termSet {
@ -120,6 +121,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
fuzzyAlgo: fuzzyAlgo, fuzzyAlgo: fuzzyAlgo,
extended: extended, extended: extended,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
normalize: normalize,
forward: forward, forward: forward,
text: []rune(asString), text: []rune(asString),
termSets: termSets, termSets: termSets,
@ -138,7 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
return ptr 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) tokens := _splitRegex.Split(str, -1)
sets := []termSet{} sets := []termSet{}
set := termSet{} set := termSet{}
@ -194,10 +196,14 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
sets = append(sets, set) sets = append(sets, set)
set = termSet{} set = termSet{}
} }
textRunes := []rune(text)
if normalize {
textRunes = algo.NormalizeRunes(textRunes)
}
set = append(set, term{ set = append(set, term{
typ: typ, typ: typ,
inv: inv, inv: inv,
text: []rune(text), text: textRunes,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
origText: origText}) origText: origText})
switchSet = true switchSet = true
@ -309,9 +315,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) { func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) {
input := p.prepareInput(item) input := p.prepareInput(item)
if p.fuzzy { 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) { func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, int, *[]int) {
@ -330,7 +336,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
matched := false matched := false
for _, term := range termSet { for _, term := range termSet {
pfun := p.procFun[term.typ] 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 sidx := off[0]; sidx >= 0 {
if term.inv { if term.inv {
continue continue
@ -378,9 +384,9 @@ func (p *Pattern) prepareInput(item *Item) []Token {
return ret 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 { 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 sidx := int32(res.Start) + part.prefixLength
eidx := int32(res.End) + part.prefixLength eidx := int32(res.End) + part.prefixLength
if pos != nil { if pos != nil {

View File

@ -15,7 +15,7 @@ func init() {
} }
func TestParseTermsExtended(t *testing.T) { 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 |") "| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |")
if len(terms) != 9 || if len(terms) != 9 ||
terms[0][0].typ != termFuzzy || terms[0][0].inv || terms[0][0].typ != termFuzzy || terms[0][0].inv ||
@ -50,7 +50,7 @@ func TestParseTermsExtended(t *testing.T) {
} }
func TestParseTermsExtendedExact(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$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 || if len(terms) != 8 ||
terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 || 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) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$") terms := parseTerms(true, CaseSmart, false, "' $ ^ !' !^ !$")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
@ -75,10 +75,10 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
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")) []Range{}, Delimiter{}, []rune("'abc"))
res, pos := algo.ExactMatchNaive( 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 { if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) 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) { func TestEqual(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
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) { match := func(str string, sidxExpected int, eidxExpected int) {
res, pos := algo.EqualMatch( 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 { if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) 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) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
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() 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() 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() 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() 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() 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 || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@ -132,7 +132,7 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(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{}) tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
@ -167,7 +167,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
func TestCacheKey(t *testing.T) { func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) { 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 { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }

View File

@ -166,7 +166,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
} }
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: tui.PairFor(fg, bg), color: tui.NewColorPair(fg, bg),
attr: ansi.color.attr.Merge(attr)}) attr: ansi.color.attr.Merge(attr)})
} }
} }

View File

@ -105,7 +105,8 @@ func TestColorOffset(t *testing.T) {
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}} 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}] // [{[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) { assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) {
var attr tui.Attr var attr tui.Attr
if bold { if bold {
@ -116,10 +117,10 @@ func TestColorOffset(t *testing.T) {
t.Error(o) t.Error(o)
} }
} }
assert(0, 0, 5, tui.ColUser, false) assert(0, 0, 5, tui.NewColorPair(1, 5), false)
assert(1, 5, 15, 99, false) assert(1, 5, 15, pair, false)
assert(2, 15, 20, tui.ColUser, false) assert(2, 15, 20, tui.NewColorPair(1, 5), false)
assert(3, 22, 25, tui.ColUser+1, true) assert(3, 22, 25, tui.NewColorPair(2, 6), true)
assert(4, 25, 35, 99, false) assert(4, 25, 35, pair, false)
assert(5, 35, 40, tui.ColUser+2, true) assert(5, 35, 40, tui.NewColorPair(4, 8), true)
} }

View File

@ -1,8 +1,10 @@
package fzf package fzf
import ( import (
"bufio"
"bytes" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
@ -15,8 +17,6 @@ import (
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-runewidth"
) )
// import "github.com/pkg/profile" // import "github.com/pkg/profile"
@ -42,6 +42,16 @@ type previewer struct {
enabled bool enabled bool
} }
type itemLine struct {
current bool
selected bool
label string
width int
result Result
}
var emptyLine = itemLine{}
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
@ -50,6 +60,8 @@ type Terminal struct {
reverse bool reverse bool
hscroll bool hscroll bool
hscrollOff int hscrollOff int
wordRubout string
wordNext string
cx int cx int
cy int cy int
offset int offset int
@ -69,11 +81,12 @@ type Terminal struct {
header []string header []string
header0 []string header0 []string
ansi bool ansi bool
tabstop int
margin [4]sizeSpec margin [4]sizeSpec
strong tui.Attr strong tui.Attr
window *tui.Window window tui.Window
bwindow *tui.Window bwindow tui.Window
pwindow *tui.Window pwindow tui.Window
count int count int
progress int progress int
reading bool reading bool
@ -89,10 +102,12 @@ type Terminal struct {
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
initFunc func() initFunc func()
prevLines []itemLine
suppress bool suppress bool
startChan chan bool startChan chan bool
slab *util.Slab slab *util.Slab
theme *tui.ColorTheme theme *tui.ColorTheme
tui tui.Renderer
} }
type selectedItem struct { type selectedItem struct {
@ -115,7 +130,6 @@ func (a byTimeOrder) Less(i, j int) bool {
} }
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int)
var _tabStop int var _tabStop int
const ( const (
@ -247,7 +261,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} else { } else {
header = reverseStringArray(opts.Header) header = reverseStringArray(opts.Header)
} }
_tabStop = opts.Tabstop
var delay time.Duration var delay time.Duration
if opts.Tac { if opts.Tac {
delay = initialDelayTac delay = initialDelayTac
@ -262,6 +275,32 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
if !opts.Bold { if !opts.Bold {
strongAttr = tui.AttrRegular 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 = util.Min(termHeight,
util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight))
} else {
maxHeight = util.Min(termHeight, int(opts.Height.size))
}
if opts.InlineInfo {
return util.Max(maxHeight, minHeight-1)
}
return util.Max(maxHeight, minHeight)
}
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc)
} 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{ return &Terminal{
initDelay: delay, initDelay: delay,
inlineInfo: opts.InlineInfo, inlineInfo: opts.InlineInfo,
@ -269,6 +308,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
reverse: opts.Reverse, reverse: opts.Reverse,
hscroll: opts.Hscroll, hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff, hscrollOff: opts.HscrollOff,
wordRubout: wordRubout,
wordNext: wordNext,
cx: len(input), cx: len(input),
cy: 0, cy: 0,
offset: 0, offset: 0,
@ -290,6 +331,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
header: header, header: header,
header0: header, header0: header,
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true, reading: true,
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
@ -306,9 +348,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
slab: util.MakeSlab(slab16Size, slab32Size), slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme, theme: opts.Theme,
startChan: make(chan bool, 1), startChan: make(chan bool, 1),
initFunc: func() { tui: renderer,
tui.Init(opts.Theme, opts.Black, opts.Mouse) initFunc: func() { renderer.Init() }}
}}
} }
// Input returns current query string // Input returns current query string
@ -401,22 +442,10 @@ func (t *Terminal) sortSelected() []selectedItem {
return sels return sels
} }
func runeWidth(r rune, prefixWidth int) int { func (t *Terminal) displayWidth(runes []rune) 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 {
l := 0 l := 0
for _, r := range runes { for _, r := range runes {
l += runeWidth(r, l) l += util.RuneWidth(r, l, t.tabstop)
} }
return l return l
} }
@ -437,9 +466,10 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
} }
func (t *Terminal) resizeWindows() { func (t *Terminal) resizeWindows() {
screenWidth := tui.MaxX() screenWidth := t.tui.MaxX()
screenHeight := tui.MaxY() screenHeight := t.tui.MaxY()
marginInt := [4]int{} marginInt := [4]int{}
t.prevLines = make([]itemLine, screenHeight)
for idx, sizeSpec := range t.margin { for idx, sizeSpec := range t.margin {
if sizeSpec.percent { if sizeSpec.percent {
var max float64 var max float64
@ -487,40 +517,40 @@ func (t *Terminal) resizeWindows() {
height := screenHeight - marginInt[0] - marginInt[2] height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
createPreviewWindow := func(y int, x int, w int, h int) { 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 pwidth := w - 4
// ncurses auto-wraps the line when the cursor reaches the right-end of // 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 // the window. To prevent unintended line-wraps, we use the width one
// column larger than the desired value. // column larger than the desired value.
if !t.preview.wrap && tui.DoesAutoWrap() { if !t.preview.wrap && t.tui.DoesAutoWrap() {
pwidth += 1 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 { switch t.preview.position {
case posUp: case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3) 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) marginInt[0]+pheight, marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight) createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown: case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3) 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) marginInt[0], marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft: case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5) 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) marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight: case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5) 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) marginInt[0], marginInt[3], width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
} }
} else { } else {
t.window = tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[0],
marginInt[3], marginInt[3],
width, width,
@ -530,7 +560,7 @@ func (t *Terminal) resizeWindows() {
func (t *Terminal) move(y int, x int, clear bool) { func (t *Terminal) move(y int, x int, clear bool) {
if !t.reverse { if !t.reverse {
y = t.window.Height - y - 1 y = t.window.Height() - y - 1
} }
if clear { if clear {
@ -541,7 +571,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
} }
func (t *Terminal) placeCursor() { 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() { func (t *Terminal) printPrompt() {
@ -552,7 +582,7 @@ func (t *Terminal) printPrompt() {
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
if t.inlineInfo { 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 { if t.reading {
t.window.CPrint(tui.ColSpinner, t.strong, " < ") t.window.CPrint(tui.ColSpinner, t.strong, " < ")
} else { } else {
@ -589,7 +619,7 @@ func (t *Terminal) printHeader() {
if len(t.header) == 0 { if len(t.header) == 0 {
return return
} }
max := t.window.Height max := t.window.Height()
var state *ansiState var state *ansiState
for idx, lineStr := range t.header { for idx, lineStr := range t.header {
line := idx + 2 line := idx + 2
@ -607,7 +637,7 @@ func (t *Terminal) printHeader() {
t.move(line, 2, true) t.move(line, 2, true)
t.printHighlighted(&Result{item: item}, t.printHighlighted(&Result{item: item},
tui.AttrRegular, tui.ColHeader, tui.ColDefault, false) tui.AttrRegular, tui.ColHeader, tui.ColDefault, false, false)
} }
} }
@ -616,19 +646,25 @@ func (t *Terminal) printList() {
maxy := t.maxItems() maxy := t.maxItems()
count := t.merger.Length() - t.offset 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) line := i + 2 + len(t.header)
if t.inlineInfo { if t.inlineInfo {
line-- line--
} }
t.move(line, 0, true)
if i < count { 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 item := result.item
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := " " label := " "
@ -641,6 +677,19 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
} else if current { } else if current {
label = ">" label = ">"
} }
// Avoid unnecessary redraw
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
}
// 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) t.window.CPrint(tui.ColCursor, t.strong, label)
if current { if current {
if selected { if selected {
@ -648,22 +697,29 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
} else { } else {
t.window.CPrint(tui.ColCurrent, t.strong, " ") t.window.CPrint(tui.ColCurrent, t.strong, " ")
} }
t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true) newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true)
} else { } else {
if selected { if selected {
t.window.CPrint(tui.ColSelected, t.strong, ">") t.window.CPrint(tui.ColSelected, t.strong, ">")
} else { } else {
t.window.Print(" ") t.window.Print(" ")
} }
t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false) 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 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 // We start from the beginning to handle tab characters
l := 0 l := 0
for idx, r := range runes { for idx, r := range runes {
l += runeWidth(r, l) l += util.RuneWidth(r, l, t.tabstop)
if l > width { if l > width {
return runes[:idx], len(runes) - idx return runes[:idx], len(runes) - idx
} }
@ -671,10 +727,10 @@ func trimRight(runes []rune, width int) ([]rune, int) {
return runes, 0 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 l := 0
for _, r := range runes { for _, r := range runes {
l += runeWidth(r, l+prefixWidth) l += util.RuneWidth(r, l+prefixWidth, t.tabstop)
if l > limit { if l > limit {
// Early exit // Early exit
return l return l
@ -683,35 +739,28 @@ func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
return l 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 { if len(runes) > maxDisplayWidthCalc && len(runes) > width {
trimmed := len(runes) - width trimmed := len(runes) - width
return runes[trimmed:], int32(trimmed) return runes[trimmed:], int32(trimmed)
} }
currentWidth := displayWidth(runes) currentWidth := t.displayWidth(runes)
var trimmed int32 var trimmed int32
for currentWidth > width && len(runes) > 0 { for currentWidth > width && len(runes) > 0 {
runes = runes[1:] runes = runes[1:]
trimmed++ trimmed++
currentWidth = displayWidthWithLimit(runes, 2, width) currentWidth = t.displayWidthWithLimit(runes, 2, width)
} }
return runes, trimmed return runes, trimmed
} }
func overflow(runes []rune, max int) bool { func (t *Terminal) overflow(runes []rune, max int) bool {
l := 0 return t.displayWidthWithLimit(runes, 0, max) > max
for _, r := range runes {
l += runeWidth(r, l)
if l > max {
return true
}
}
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) int {
item := result.item item := result.item
// Overflow // Overflow
@ -719,7 +768,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
copy(text, item.text.ToRunes()) copy(text, item.text.ToRunes())
matchOffsets := []Offset{} matchOffsets := []Offset{}
var pos *[]int var pos *[]int
if t.merger.pattern != nil { if match && t.merger.pattern != nil {
_, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab) _, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab)
} }
charOffsets := matchOffsets charOffsets := matchOffsets
@ -737,22 +786,23 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
} }
offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) 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)) maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
if overflow(text, maxWidth) { displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
if displayWidth > maxWidth {
if t.hscroll { if t.hscroll {
// Stri.. // Stri..
if !overflow(text[:maxe], maxWidth-2) { if !t.overflow(text[:maxe], maxWidth-2) {
text, _ = trimRight(text, maxWidth-2) text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune("..")...) text = append(text, []rune("..")...)
} else { } else {
// Stri.. // Stri..
if overflow(text[maxe:], 2) { if t.overflow(text[maxe:], 2) {
text = append(text[:maxe], []rune("..")...) text = append(text[:maxe], []rune("..")...)
} }
// ..ri.. // ..ri..
var diff int32 var diff int32
text, diff = trimLeft(text, maxWidth-2) text, diff = t.trimLeft(text, maxWidth-2)
// Transform offsets // Transform offsets
for idx, offset := range offsets { for idx, offset := range offsets {
@ -766,7 +816,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
text = append([]rune(".."), text...) text = append([]rune(".."), text...)
} }
} else { } else {
text, _ = trimRight(text, maxWidth-2) text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune("..")...) text = append(text, []rune("..")...)
for idx, offset := range offsets { for idx, offset := range offsets {
@ -784,11 +834,11 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
b := util.Constrain32(offset.offset[0], index, maxOffset) b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Constrain32(offset.offset[1], 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) t.window.CPrint(col1, attr, substr)
if b < e { 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) t.window.CPrint(offset.color, offset.attr, substr)
} }
@ -798,9 +848,10 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
} }
} }
if index < maxOffset { if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth) substr, _ = t.processTabs(text[index:], prefixWidth)
t.window.CPrint(col1, attr, substr) t.window.CPrint(col1, attr, substr)
} }
return displayWidth
} }
func numLinesMax(str string, max int) int { func numLinesMax(str string, max int) int {
@ -821,52 +872,66 @@ func (t *Terminal) printPreview() {
return return
} }
t.pwindow.Erase() t.pwindow.Erase()
skip := t.previewer.offset
extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool { maxWidth := t.pwindow.Width()
if skip > 0 { if t.tui.DoesAutoWrap() {
newlines := numLinesMax(str, skip) maxWidth -= 1
if skip <= newlines { }
for i := 0; i < skip; i++ { reader := bufio.NewReader(strings.NewReader(t.previewer.text))
str = str[strings.Index(str, "\n")+1:] 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 str, _ = t.processTabs(trimmed, 0)
} else { if ansi != nil && ansi.colored() {
skip -= newlines fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
return true } else {
fillRet = t.pwindow.Fill(str)
}
return fillRet == tui.FillContinue
})
switch fillRet {
case tui.FillNextLine:
continue
case tui.FillSuspend:
break
} }
t.pwindow.Fill("\n")
} }
if !t.preview.wrap { if eof {
lines := strings.Split(str, "\n") break
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)
}
str = strings.Join(lines, "\n")
} }
if ansi != nil && ansi.colored() { }
return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr) t.pwindow.FinishFill()
} if t.previewer.lines > t.pwindow.Height() {
return t.pwindow.Fill(str)
})
if t.previewer.lines > t.pwindow.Height {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) 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) 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 var strbuf bytes.Buffer
l := prefixWidth l := prefixWidth
for _, r := range runes { for _, r := range runes {
w := runeWidth(r, l) w := util.RuneWidth(r, l, t.tabstop)
l += w l += w
if r == '\t' { if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w)) strbuf.WriteString(strings.Repeat(" ", w))
@ -889,9 +954,9 @@ func (t *Terminal) printAll() {
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
if !t.suppress { if !t.suppress {
if t.isPreviewEnabled() { 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 { } else {
tui.RefreshWindows([]*tui.Window{t.window}) t.tui.RefreshWindows([]tui.Window{t.window})
} }
} }
} }
@ -1013,9 +1078,9 @@ func (t *Terminal) executeCommand(template string, items []*Item) {
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
tui.Pause() t.tui.Pause()
cmd.Run() cmd.Run()
if tui.Resume() { if t.tui.Resume() {
t.printAll() t.printAll()
} }
t.refresh() t.refresh()
@ -1162,11 +1227,11 @@ func (t *Terminal) Loop() {
case reqRefresh: case reqRefresh:
t.suppress = false t.suppress = false
case reqRedraw: case reqRedraw:
tui.Clear() t.tui.Clear()
tui.Refresh() t.tui.Refresh()
t.printAll() t.printAll()
case reqClose: case reqClose:
tui.Close() t.tui.Close()
if t.output() { if t.output() {
exit(exitOk) exit(exitOk)
} }
@ -1179,11 +1244,11 @@ func (t *Terminal) Loop() {
case reqPreviewRefresh: case reqPreviewRefresh:
t.printPreview() t.printPreview()
case reqPrintQuery: case reqPrintQuery:
tui.Close() t.tui.Close()
t.printer(string(t.input)) t.printer(string(t.input))
exit(exitOk) exit(exitOk)
case reqQuit: case reqQuit:
tui.Close() t.tui.Close()
exit(exitInterrupt) exit(exitInterrupt)
} }
} }
@ -1196,7 +1261,7 @@ func (t *Terminal) Loop() {
looping := true looping := true
for looping { for looping {
event := tui.GetChar() event := t.tui.GetChar()
t.mutex.Lock() t.mutex.Lock()
previousInput := t.input previousInput := t.input
@ -1288,11 +1353,11 @@ func (t *Terminal) Loop() {
} }
case actPreviewPageUp: case actPreviewPageUp:
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
scrollPreview(-t.pwindow.Height) scrollPreview(-t.pwindow.Height())
} }
case actPreviewPageDown: case actPreviewPageDown:
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
scrollPreview(t.pwindow.Height) scrollPreview(t.pwindow.Height())
} }
case actBeginningOfLine: case actBeginningOfLine:
t.cx = 0 t.cx = 0
@ -1401,7 +1466,7 @@ func (t *Terminal) Loop() {
} }
case actBackwardKillWord: case actBackwardKillWord:
if t.cx > 0 { if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]") t.rubout(t.wordRubout)
} }
case actYank: case actYank:
suffix := copySlice(t.input[t.cx:]) suffix := copySlice(t.input[t.cx:])
@ -1420,12 +1485,12 @@ func (t *Terminal) Loop() {
t.jumping = jumpAcceptEnabled t.jumping = jumpAcceptEnabled
req(reqJump) req(reqJump)
case actBackwardWord: 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: 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: case actKillWord:
ncx := t.cx + 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 { if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx]) t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...) t.input = append(t.input[:t.cx], t.input[ncx:]...)
@ -1466,11 +1531,11 @@ func (t *Terminal) Loop() {
scrollPreview(-me.S) scrollPreview(-me.S)
} }
} else if t.window.Enclose(my, mx) { } else if t.window.Enclose(my, mx) {
mx -= t.window.Left mx -= t.window.Left()
my -= t.window.Top my -= t.window.Top()
mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input)) mx = util.Constrain(mx-t.displayWidth([]rune(t.prompt)), 0, len(t.input))
if !t.reverse { if !t.reverse {
my = t.window.Height - my - 1 my = t.window.Height() - my - 1
} }
min := 2 + len(t.header) min := 2 + len(t.header)
if t.inlineInfo { if t.inlineInfo {
@ -1582,7 +1647,7 @@ func (t *Terminal) vset(o int) bool {
} }
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.window.Height - 2 - len(t.header) max := t.window.Height() - 2 - len(t.header)
if t.inlineInfo { if t.inlineInfo {
max++ max++
} }

820
src/tui/light.go Normal file
View File

@ -0,0 +1,820 @@
package tui
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"unicode/utf8"
"github.com/junegunn/fzf/src/util"
"golang.org/x/crypto/ssh/terminal"
)
const (
defaultWidth = 80
defaultHeight = 24
escPollInterval = 5
)
const consoleDevice string = "/dev/tty"
func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil {
panic("Failed to open " + consoleDevice)
}
return in
}
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 < 32 &&
r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') {
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
origState *terminal.State
width int
height int
yoffset int
tabstop int
escDelay int
upOneLine bool
queued string
y int
x int
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) 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
}
return Default16
}
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
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)
_, 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")
if r.mouse {
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 getEnv(name string, defaultValue int) int {
env := os.Getenv(name)
if len(env) == 0 {
return defaultValue
}
return atoi(env, defaultValue)
}
func (r *LightRenderer) updateTerminalSize() {
width, height, err := terminal.GetSize(r.fd())
if err == nil {
r.width = width
r.height = r.maxHeightFunc(height)
} else {
r.width = getEnv("COLUMNS", defaultWidth)
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
}
}
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 0, false
}
return int(b[0]), true
}
func (r *LightRenderer) getBytes() []byte {
return r.getBytesInternal(r.buffer)
}
func (r *LightRenderer) getBytesInternal(buffer []byte) []byte {
c, ok := r.getch(false)
if !ok {
r.Close()
errorExit("Failed to read " + consoleDevice)
}
retries := 0
if c == ESC {
retries = r.escDelay / escPollInterval
}
buffer = append(buffer, byte(c))
for {
c, ok = r.getch(true)
if !ok {
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() {
terminal.Restore(r.fd(), r.origState)
r.csi("?1049h")
r.flush()
}
func (r *LightRenderer) Resume() bool {
terminal.MakeRaw(r.fd())
r.csi("?1049l")
r.flush()
// Should redraw
return true
}
func (r *LightRenderer) Clear() {
// r.csi("u")
r.origin()
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.origin()
r.csi("J")
if r.mouse {
r.csi("?1000l")
}
if r.upOneLine {
r.csi("A")
}
r.flush()
terminal.Restore(r.fd(), r.origState)
}
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) IsOptimized() bool {
return false
}
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) stderrInternal(str string, allowNLCR bool) {
w.renderer.stderrInternal(str, allowNLCR)
}
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.renderer.move(w.Top()+y, w.Left()+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.stderrInternal(text, false)
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.stderrInternal(text, false)
}
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()) 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.stderrInternal(wl.text, false)
w.posx += wl.displayWidth
if j < len(lines)-1 || i < len(allLines)-1 {
if w.posy+1 >= w.height {
return FillSuspend
}
w.MoveAndClear(w.posy+1, 0)
onMove()
}
}
}
return FillContinue
}
func (w *LightWindow) setBg() {
if w.bg != colDefault {
w.csiColor(colDefault, w.bg, AttrRegular)
}
}
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) FillReturn {
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)
}

View File

@ -25,7 +25,6 @@ int c_getcurx(WINDOW* win) {
import "C" import "C"
import ( import (
"fmt"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -33,9 +32,39 @@ import (
"unicode/utf8" "unicode/utf8"
) )
type ColorPair int16
type Attr C.uint 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 ( const (
Bold Attr = C.A_BOLD Bold Attr = C.A_BOLD
@ -51,31 +80,14 @@ const (
AttrRegular Attr = 0 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 ( var (
_screen *C.SCREEN _screen *C.SCREEN
_colorMap map[int]ColorPair _colorMap map[int]int16
_colorFn func(ColorPair, Attr) (C.short, C.int) _colorFn func(ColorPair, Attr) (C.short, C.int)
) )
func init() { func init() {
_colorMap = make(map[int]ColorPair) _colorMap = make(map[int]int16)
if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") { if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") {
Italic = C.A_NORMAL Italic = C.A_NORMAL
} }
@ -85,27 +97,25 @@ func (a Attr) Merge(b Attr) Attr {
return a | b return a | b
} }
func DefaultTheme() *ColorTheme { func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if C.tigetnum(C.CString("colors")) >= 256 { if C.tigetnum(C.CString("colors")) >= 256 {
return Dark256 return Dark256
} }
return Default16 return Default16
} }
func Init(theme *ColorTheme, black bool, mouse bool) { func (r *FullscreenRenderer) Init() {
C.setlocale(C.LC_ALL, C.CString("")) C.setlocale(C.LC_ALL, C.CString(""))
tty := C.c_tty() tty := C.c_tty()
if tty == nil { if tty == nil {
fmt.Println("Failed to open /dev/tty") errorExit("Failed to open /dev/tty")
os.Exit(2)
} }
_screen = C.c_newterm(tty) _screen = C.c_newterm(tty)
if _screen == nil { if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) errorExit("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(2)
} }
C.set_term(_screen) C.set_term(_screen)
if mouse { if r.mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mousemask(C.ALL_MOUSE_EVENTS, nil)
C.mouseinterval(0) C.mouseinterval(0)
} }
@ -124,14 +134,14 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
} }
C.set_escdelay(C.int(delay)) C.set_escdelay(C.int(delay))
_color = theme != nil if r.theme != nil {
if _color {
C.start_color() C.start_color()
InitTheme(theme, black) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
initPairs(theme) initPairs(r.theme)
C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
_colorFn = attrColored _colorFn = attrColored
} else { } else {
initTheme(r.theme, nil, r.forceBlack)
_colorFn = attrMono _colorFn = attrMono
} }
@ -145,39 +155,39 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
func initPairs(theme *ColorTheme) { func initPairs(theme *ColorTheme) {
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
initPair := func(group ColorPair, fg Color, bg Color) { for _, pair := range []ColorPair{
C.init_pair(C.short(group), C.short(fg), C.short(bg)) 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() C.endwin()
} }
func Resume() bool { func (r *FullscreenRenderer) Resume() bool {
return false return false
} }
func Close() { func (r *FullscreenRenderer) Close() {
C.endwin() C.endwin()
C.delscreen(_screen) 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)) win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if _color { if r.theme != nil {
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
} }
if border { if border {
pair, attr := _colorFn(ColBorder, 0) pair, attr := _colorFn(ColBorder, 0)
@ -188,66 +198,50 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window {
C.wcolor_set(win, 0, nil) C.wcolor_set(win, 0, nil)
} }
return &Window{ return &CursesWindow{
impl: (*WindowImpl)(win), impl: win,
Top: top, top: top,
Left: left, left: left,
Width: width, width: width,
Height: height, height: height,
} }
} }
func attrColored(pair ColorPair, a Attr) (C.short, C.int) { func attrColored(color ColorPair, a Attr) (C.short, C.int) {
return C.short(pair), C.int(a) return C.short(color.index()), C.int(a)
} }
func attrMono(pair ColorPair, a Attr) (C.short, C.int) { func attrMono(color ColorPair, a Attr) (C.short, C.int) {
var attr C.int return 0, C.int(attrFor(color, a))
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 MaxX() int { func (r *FullscreenRenderer) MaxX() int {
return int(C.COLS) return int(C.COLS)
} }
func MaxY() int { func (r *FullscreenRenderer) MaxY() int {
return int(C.LINES) return int(C.LINES)
} }
func (w *Window) win() *C.WINDOW { func (w *CursesWindow) Close() {
return (*C.WINDOW)(w.impl) C.delwin(w.impl)
} }
func (w *Window) Close() { func (w *CursesWindow) Enclose(y int, x int) bool {
C.delwin(w.win()) return bool(C.wenclose(w.impl, C.int(y), C.int(x)))
} }
func (w *Window) Enclose(y int, x int) bool { func (w *CursesWindow) Move(y int, x int) {
return bool(C.wenclose(w.win(), C.int(y), C.int(x))) C.wmove(w.impl, C.int(y), C.int(x))
} }
func (w *Window) Move(y int, x int) { func (w *CursesWindow) MoveAndClear(y int, x int) {
C.wmove(w.win(), C.int(y), C.int(x))
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x) w.Move(y, x)
C.wclrtoeol(w.win()) C.wclrtoeol(w.impl)
} }
func (w *Window) Print(text string) { func (w *CursesWindow) Print(text string) {
C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune { C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune {
if r < 32 { if r < 32 {
return -1 return -1
} }
@ -255,69 +249,81 @@ func (w *Window) Print(text string) {
}, text))) }, text)))
} }
func (w *Window) CPrint(pair ColorPair, attr Attr, text string) { func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) {
p, a := _colorFn(pair, attr) p, a := _colorFn(color, attr)
C.wcolor_set(w.win(), p, nil) C.wcolor_set(w.impl, p, nil)
C.wattron(w.win(), a) C.wattron(w.impl, a)
w.Print(text) w.Print(text)
C.wattroff(w.win(), a) C.wattroff(w.impl, a)
C.wcolor_set(w.win(), 0, nil) C.wcolor_set(w.impl, 0, nil)
} }
func Clear() { func (r *FullscreenRenderer) Clear() {
C.clear() C.clear()
C.endwin() C.endwin()
} }
func Refresh() { func (r *FullscreenRenderer) Refresh() {
C.refresh() C.refresh()
} }
func (w *Window) Erase() { func (w *CursesWindow) Erase() {
C.werase(w.win()) C.werase(w.impl)
} }
func (w *Window) X() int { func (w *CursesWindow) X() int {
return int(C.c_getcurx(w.win())) return int(C.c_getcurx(w.impl))
} }
func DoesAutoWrap() bool { func (r *FullscreenRenderer) DoesAutoWrap() bool {
return true return true
} }
func (w *Window) Fill(str string) bool { func (r *FullscreenRenderer) IsOptimized() bool {
return C.waddstr(w.win(), C.CString(str)) == C.OK return true
} }
func (w *Window) CFill(str string, fg Color, bg Color, attr Attr) bool { func (w *CursesWindow) Fill(str string) FillReturn {
pair := PairFor(fg, bg) if C.waddstr(w.impl, C.CString(str)) == C.OK {
C.wcolor_set(w.win(), C.short(pair), nil) return FillContinue
C.wattron(w.win(), C.int(attr)) }
return FillSuspend
}
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))
ret := w.Fill(str) ret := w.Fill(str)
C.wattroff(w.win(), C.int(attr)) C.wattroff(w.impl, C.int(attr))
C.wcolor_set(w.win(), 0, nil) C.wcolor_set(w.impl, 0, nil)
return ret return ret
} }
func RefreshWindows(windows []*Window) { func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
for _, w := range windows { for _, w := range windows {
C.wnoutrefresh(w.win()) w.Refresh()
} }
C.doupdate() C.doupdate()
} }
func PairFor(fg Color, bg Color) ColorPair { func (p ColorPair) index() int16 {
// ncurses does not support 24-bit colors if p.id >= 0 {
if fg.is24() || bg.is24() { return p.id
return ColDefault
} }
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 { if found, prs := _colorMap[key]; prs {
return found return found
} }
id := ColorPair(len(_colorMap) + int(ColUser)) id := int16(len(_colorMap)) + ColUser.id
C.init_pair(C.short(id), C.short(fg), C.short(bg)) C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg()))
_colorMap[key] = id _colorMap[key] = id
return id return id
} }
@ -369,11 +375,13 @@ func escSequence() Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
func GetChar() Event { func (r *FullscreenRenderer) GetChar() Event {
c := C.getch() c := C.getch()
switch c { switch c {
case C.ERR: case C.ERR:
return Event{Invalid, 0, nil} // Unexpected error from blocking read
r.Close()
errorExit("Failed to read /dev/tty")
case C.KEY_UP: case C.KEY_UP:
return Event{Up, 0, nil} return Event{Up, 0, nil}
case C.KEY_DOWN: case C.KEY_DOWN:
@ -435,17 +443,17 @@ func GetChar() Event {
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */ /* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
if (me.bstate & C.BUTTON1_PRESSED) > 0 { if (me.bstate & C.BUTTON1_PRESSED) > 0 {
now := time.Now() now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration { if now.Sub(r.prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, y) r.clickY = append(r.clickY, y)
} else { } else {
_clickY = []int{y} r.clickY = []int{y}
_prevDownTime = now r.prevDownTime = now
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
} else if (me.bstate & C.BUTTON1_RELEASED) > 0 { } else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
double := false double := false
if len(_clickY) > 1 && _clickY[0] == _clickY[1] && if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration { time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true double = true
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}}

View File

@ -6,9 +6,6 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"fmt"
"os"
"runtime" "runtime"
// https://github.com/gdamore/tcell/pull/135 // https://github.com/gdamore/tcell/pull/135
@ -18,30 +15,56 @@ import (
"github.com/junegunn/go-runewidth" "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 { func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault 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 Attr tcell.Style
type WindowTcell struct { type TcellWindow struct {
LastX int color bool
LastY int top int
MoveCursor bool left int
Border bool 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 ( const (
Bold Attr = Attr(tcell.AttrBold) Bold Attr = Attr(tcell.AttrBold)
@ -56,33 +79,13 @@ const (
AttrRegular Attr = 0 AttrRegular Attr = 0
) )
var ( func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
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 {
if _screen.Colors() >= 256 { if _screen.Colors() >= 256 {
return Dark256 return Dark256
} }
return Default16 return Default16
} }
func PairFor(fg Color, bg Color) ColorPair {
return [2]Color{fg, bg}
}
var ( var (
_colorToAttribute = []tcell.Color{ _colorToAttribute = []tcell.Color{
tcell.ColorBlack, tcell.ColorBlack,
@ -112,20 +115,17 @@ func (a Attr) Merge(b Attr) Attr {
var ( var (
_screen tcell.Screen _screen tcell.Screen
_mouse bool
) )
func initScreen() { func (r *FullscreenRenderer) initScreen() {
s, e := tcell.NewScreen() s, e := tcell.NewScreen()
if e != nil { if e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e) errorExit(e.Error())
os.Exit(2)
} }
if e = s.Init(); e != nil { if e = s.Init(); e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e) errorExit(e.Error())
os.Exit(2)
} }
if _mouse { if r.mouse {
s.EnableMouse() s.EnableMouse()
} else { } else {
s.DisableMouse() s.DisableMouse()
@ -133,63 +133,45 @@ func initScreen() {
_screen = s _screen = s
} }
func Init(theme *ColorTheme, black bool, mouse bool) { func (r *FullscreenRenderer) Init() {
encoding.Register() encoding.Register()
_mouse = mouse r.initScreen()
initScreen() initTheme(r.theme, r.defaultTheme(), r.forceBlack)
_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}
} }
func MaxX() int { func (r *FullscreenRenderer) MaxX() int {
ncols, _ := _screen.Size() ncols, _ := _screen.Size()
return int(ncols) return int(ncols)
} }
func MaxY() int { func (r *FullscreenRenderer) MaxY() int {
_, nlines := _screen.Size() _, nlines := _screen.Size()
return int(nlines) return int(nlines)
} }
func (w *Window) win() *WindowTcell { func (w *TcellWindow) X() int {
return (*WindowTcell)(w.impl) return w.lastX
} }
func (w *Window) X() int { func (r *FullscreenRenderer) DoesAutoWrap() bool {
return w.impl.LastX
}
func DoesAutoWrap() bool {
return false return false
} }
func Clear() { func (r *FullscreenRenderer) IsOptimized() bool {
return false
}
func (r *FullscreenRenderer) Clear() {
_screen.Sync() _screen.Sync()
_screen.Clear() _screen.Clear()
} }
func Refresh() { func (r *FullscreenRenderer) Refresh() {
// noop // noop
} }
func GetChar() Event { func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent() ev := _screen.PollEvent()
switch ev := ev.(type) { switch ev := ev.(type) {
case *tcell.EventResize: case *tcell.EventResize:
@ -213,15 +195,15 @@ func GetChar() Event {
double := false double := false
if down { if down {
now := time.Now() now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration { if now.Sub(r.prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, x) r.clickY = append(r.clickY, x)
} else { } else {
_clickY = []int{x} r.clickY = []int{x}
_prevDownTime = now r.prevDownTime = now
} }
} else { } else {
if len(_clickY) > 1 && _clickY[0] == _clickY[1] && if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration { time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true double = true
} }
} }
@ -368,49 +350,39 @@ func GetChar() Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
func Pause() { func (r *FullscreenRenderer) Pause() {
_screen.Fini() _screen.Fini()
} }
func Resume() bool { func (r *FullscreenRenderer) Resume() bool {
initScreen() r.initScreen()
return true return true
} }
func Close() { func (r *FullscreenRenderer) Close() {
_screen.Fini() _screen.Fini()
} }
func RefreshWindows(windows []*Window) { func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
// TODO // TODO
for _, w := range windows { for _, w := range windows {
if w.win().MoveCursor { w.Refresh()
_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()
}
} }
_screen.Show() _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 // TODO
win := new(WindowTcell) return &TcellWindow{
win.Border = border color: r.theme != nil,
return &Window{ top: top,
impl: (*WindowImpl)(win), left: left,
Top: top, width: width,
Left: left, height: height,
Width: width, border: border}
Height: height,
}
} }
func (w *Window) Close() { func (w *TcellWindow) Close() {
// TODO // TODO
} }
@ -422,40 +394,40 @@ func fill(x, y, w, h int, r rune) {
} }
} }
func (w *Window) Erase() { func (w *TcellWindow) Erase() {
// TODO // 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 { func (w *TcellWindow) Enclose(y int, x int) bool {
return x >= w.Left && x <= (w.Left+w.Width) && return x >= w.left && x < (w.left+w.width) &&
y >= w.Top && y <= (w.Top+w.Height) y >= w.top && y < (w.top+w.height)
} }
func (w *Window) Move(y int, x int) { func (w *TcellWindow) Move(y int, x int) {
w.win().LastX = x w.lastX = x
w.win().LastY = y w.lastY = y
w.win().MoveCursor = true w.moveCursor = true
} }
func (w *Window) MoveAndClear(y int, x int) { func (w *TcellWindow) MoveAndClear(y int, x int) {
w.Move(y, x) w.Move(y, x)
for i := w.win().LastX; i < w.Width; i++ { for i := w.lastX; i < w.width; i++ {
_screen.SetContent(i+w.Left, w.win().LastY+w.Top, rune(' '), nil, ColDefault.style()) _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) { func (w *TcellWindow) Print(text string) {
w.PrintString(text, ColDefault, 0) 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 t := text
lx := 0 lx := 0
var style tcell.Style var style tcell.Style
if _color { if w.color {
style = pair.style(). style = pair.style().
Reverse(a&Attr(tcell.AttrReverse) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0) Underline(a&Attr(tcell.AttrUnderline) != 0)
@ -481,7 +453,7 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) {
} }
if r == '\n' { if r == '\n' {
w.win().LastY++ w.lastY++
lx = 0 lx = 0
} else { } else {
@ -489,26 +461,26 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) {
continue continue
} }
var xPos = w.Left + w.win().LastX + lx var xPos = w.left + w.lastX + lx
var yPos = w.Top + w.win().LastY var yPos = w.top + w.lastY
if xPos < (w.Left+w.Width) && yPos < (w.Top+w.Height) { if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, r, nil, style) _screen.SetContent(xPos, yPos, r, nil, style)
} }
lx += runewidth.RuneWidth(r) lx += runewidth.RuneWidth(r)
} }
} }
w.win().LastX += lx w.lastX += lx
} }
func (w *Window) CPrint(pair ColorPair, a Attr, text string) { func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) {
w.PrintString(text, pair, a) 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) FillReturn {
lx := 0 lx := 0
var style tcell.Style var style tcell.Style
if _color { if w.color {
style = pair.style() style = pair.style()
} else { } else {
style = ColDefault.style() style = ColDefault.style()
@ -522,50 +494,50 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool {
for _, r := range text { for _, r := range text {
if r == '\n' { if r == '\n' {
w.win().LastY++ w.lastY++
w.win().LastX = 0 w.lastX = 0
lx = 0 lx = 0
} else { } else {
var xPos = w.Left + w.win().LastX + lx var xPos = w.left + w.lastX + lx
// word wrap: // word wrap:
if xPos >= (w.Left + w.Width) { if xPos >= (w.left + w.width) {
w.win().LastY++ w.lastY++
w.win().LastX = 0 w.lastX = 0
lx = 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 return FillSuspend
} }
_screen.SetContent(xPos, yPos, r, nil, style) _screen.SetContent(xPos, yPos, r, nil, style)
lx += runewidth.RuneWidth(r) lx += runewidth.RuneWidth(r)
} }
} }
w.win().LastX += lx w.lastX += lx
return true return FillContinue
} }
func (w *Window) Fill(str string) bool { func (w *TcellWindow) Fill(str string) FillReturn {
return w.FillString(str, ColDefault, 0) return w.fillString(str, ColDefault, 0)
} }
func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.FillString(str, ColorPair{fg, bg}, a) return w.fillString(str, ColorPair{fg, bg, -1}, a)
} }
func (w *Window) DrawBorder() { func (w *TcellWindow) drawBorder() {
left := w.Left left := w.left
right := left + w.Width right := left + w.width
top := w.Top top := w.top
bot := top + w.Height bot := top + w.height
var style tcell.Style var style tcell.Style
if _color { if w.color {
style = ColBorder.style() style = ColBorder.style()
} else { } else {
style = ColDefault.style() style = ColDefault.style()

View File

@ -1,6 +1,9 @@
package tui package tui
import ( import (
"fmt"
"os"
"strconv"
"time" "time"
) )
@ -115,6 +118,47 @@ const (
colWhite colWhite
) )
type FillReturn int
const (
FillContinue FillReturn = iota
FillNextLine
FillSuspend
)
type ColorPair struct {
fg Color
bg Color
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}
}
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 { type ColorTheme struct {
Fg Color Fg Color
Bg Color Bg Color
@ -146,23 +190,85 @@ type MouseEvent struct {
Mod bool Mod bool
} }
var ( type Renderer interface {
_color bool Init()
_prevDownTime time.Time Pause()
_clickY []int Resume() bool
Default16 *ColorTheme Clear()
Dark256 *ColorTheme RefreshWindows(windows []Window)
Light256 *ColorTheme Refresh()
) Close()
type Window struct { GetChar() Event
impl *WindowImpl
Top int MaxX() int
Left int MaxY() int
Width int DoesAutoWrap() bool
Height int IsOptimized() 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) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
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 { func EmptyTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Fg: colUndefined, Fg: colUndefined,
@ -180,9 +286,12 @@ func EmptyTheme() *ColorTheme {
Border: colUndefined} Border: colUndefined}
} }
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(2)
}
func init() { func init() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Fg: colDefault, Fg: colDefault,
Bg: colDefault, Bg: colDefault,
@ -227,14 +336,13 @@ func init() {
Border: 145} Border: 145}
} }
func InitTheme(theme *ColorTheme, black bool) { func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
_color = theme != nil if theme == nil {
if !_color { initPalette(theme)
return return
} }
baseTheme := DefaultTheme() if forceBlack {
if black {
theme.Bg = colBlack theme.Bg = colBlack
} }
@ -257,4 +365,48 @@ func InitTheme(theme *ColorTheme, black bool) {
theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header) theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border) 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
} }

View File

@ -1,14 +1,20 @@
package tui package tui
import ( import "testing"
"testing"
)
func TestPairFor(t *testing.T) { func TestHexToColor(t *testing.T) {
if PairFor(30, 50) != PairFor(30, 50) { assert := func(expr string, r, g, b int) {
t.Fail() color := HexToColor(expr)
} if !color.is24() ||
if PairFor(-1, 10) != PairFor(-1, 10) { int((color>>16)&0xff) != r ||
t.Fail() 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)
} }

View File

@ -6,8 +6,24 @@ import (
"time" "time"
"github.com/junegunn/go-isatty" "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 := Max(runewidth.RuneWidth(r), 1)
_runeWidths[r] = w
return w
}
}
// Max returns the largest integer // Max returns the largest integer
func Max(first int, second int) int { func Max(first int, second int) int {
if first >= second { if first >= second {

View File

@ -5,6 +5,7 @@ package util
import ( import (
"os" "os"
"os/exec" "os/exec"
"syscall"
) )
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
@ -20,3 +21,8 @@ func ExecCommand(command string) *exec.Cmd {
func IsWindows() bool { func IsWindows() bool {
return false return false
} }
// SetNonBlock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) {
syscall.SetNonblock(int(file.Fd()), nonblock)
}

View File

@ -5,6 +5,7 @@ package util
import ( import (
"os" "os"
"os/exec" "os/exec"
"syscall"
"github.com/junegunn/go-shellwords" "github.com/junegunn/go-shellwords"
) )
@ -26,3 +27,8 @@ func ExecCommand(command string) *exec.Cmd {
func IsWindows() bool { func IsWindows() bool {
return true return true
} }
// SetNonBlock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) {
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
}

View File

@ -117,8 +117,28 @@ class Tmux
wait do wait do
lines = capture(pane) lines = capture(pane)
class << lines 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 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
end end
yield lines yield lines
@ -163,6 +183,11 @@ class TestBase < Minitest::Test
@temp_suffix].join '-' @temp_suffix].join '-'
end end
def writelines path, lines
File.unlink path while File.exists? path
File.open(path, 'w') { |f| f << lines.join($/) + $/ }
end
def readonce def readonce
wait { File.exists?(tempname) } wait { File.exists?(tempname) }
File.read(tempname) File.read(tempname)
@ -297,6 +322,19 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines.last !~ /^>/ } tmux.until { |lines| lines.last !~ /^>/ }
end 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 def test_multi_order
tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
@ -1056,7 +1094,7 @@ class TestGoFZF < TestBase
end end
def test_invalid_term def test_invalid_term
lines = `TERM=xxx #{FZF}` lines = `TERM=xxx #{FZF} 2>&1`
assert_equal 2, $?.exitstatus assert_equal 2, $?.exitstatus
assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found') assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found')
end end
@ -1190,12 +1228,6 @@ class TestGoFZF < TestBase
tmux.send_keys '?' tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' } tmux.until { |lines| lines[-1] == '> 555' }
end end
private
def writelines path, lines
File.unlink path while File.exists? path
File.open(path, 'w') { |f| f << lines.join($/) + $/ }
end
end end
module TestShell module TestShell
@ -1213,79 +1245,66 @@ module TestShell
tmux.prepare tmux.prepare
end end
def test_ctrl_t def unset_var name
tmux.prepare
tmux.send_keys "unset #{name}", :Enter
tmux.prepare 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 end
def test_ctrl_t_command def test_ctrl_t
set_var "FZF_CTRL_T_COMMAND", "seq 100" set_var "FZF_CTRL_T_COMMAND", "seq 100"
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(1) { |lines| lines.item_count == 100 } lines = retries do
tmux.send_keys :BTab, :BTab, :BTab, pane: 1 tmux.prepare
tmux.until(1) { |lines| lines[-2].include?('(3)') } tmux.send_keys 'C-t'
tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines.item_count == 100 }
tmux.until(0) { |lines| lines[-1].include? '1 2 3' } end
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 end
def test_ctrl_t_unicode def test_ctrl_t_unicode
FileUtils.mkdir_p '/tmp/fzf-test' writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2']
tmux.paste 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "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 '1', pane: 1 retries do
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } tmux.prepare
tmux.send_keys :BTab, pane: 1 tmux.send_keys 'echo ', 'C-t'
tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' } tmux.until { |lines| lines.item_count == 2 }
tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *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 :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 end
tmux.send_keys 'fzf-unicode'
tmux.until { |lines| lines.match_count == 2 }
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
tmux.until { |lines| lines.match_count == 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 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 end
def test_alt_c def test_alt_c
tmux.prepare lines = retries do
tmux.send_keys :Escape, :c, pane: 0 tmux.prepare
lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] } tmux.send_keys :Escape, :c
expected = lines[-3][2..-1] tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :Enter, pane: 1 end
expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1]
tmux.send_keys :Enter
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
tmux.until { |lines| lines[-1].end_with?(expected) } tmux.until { |lines| lines[-1].end_with?(expected) }
@ -1297,10 +1316,12 @@ module TestShell
tmux.prepare tmux.prepare
tmux.send_keys 'cd /', :Enter tmux.send_keys 'cd /', :Enter
tmux.prepare retries do
tmux.send_keys :Escape, :c, pane: 0 tmux.prepare
lines = tmux.until(1) { |lines| lines.item_count == 1 } tmux.send_keys :Escape, :c
tmux.send_keys :Enter, pane: 1 lines = tmux.until { |lines| lines.item_count == 1 }
end
tmux.send_keys :Enter
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
@ -1313,16 +1334,29 @@ module TestShell
tmux.send_keys 'echo 2nd', :Enter; tmux.prepare tmux.send_keys 'echo 2nd', :Enter; tmux.prepare
tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3d', :Enter; tmux.prepare
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
tmux.send_keys 'echo 4th', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter
tmux.send_keys 'C-r', pane: 0 retries do
tmux.until(1) { |lines| lines.item_count > 0 } tmux.prepare
tmux.send_keys '3d', pane: 1 tmux.send_keys 'C-r'
tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :Enter, pane: 1 end
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.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' } tmux.until { |lines| lines[-1] == '3rd' }
end end
def retries times = 3, &block
(times - 1).times do |t|
begin
return block.call
rescue RuntimeError
end
end
block.call
end
end end
module CompletionTest module CompletionTest
@ -1334,12 +1368,12 @@ module CompletionTest
FileUtils.touch File.expand_path(f) FileUtils.touch File.expand_path(f)
end end
tmux.prepare tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys ' !d' tmux.send_keys ' !d'
tmux.until(1) { |lines| lines[-2].include?(' 2/') } tmux.until { |lines| lines.match_count == 2 }
tmux.send_keys :BTab, :BTab tmux.send_keys :Tab, :Tab
tmux.until(1) { |lines| lines[-2].include?('(2)') } tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@ -1349,10 +1383,10 @@ module CompletionTest
# ~USERNAME**<TAB> # ~USERNAME**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0 tmux.send_keys "cat ~#{ENV['USER']}**", :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys '.fzf-home' tmux.send_keys "'.fzf-home"
tmux.until(1) { |lines| lines[-3].end_with? '.fzf-home' } tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@ -1361,8 +1395,8 @@ module CompletionTest
# ~INVALID_USERNAME**<TAB> # ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys "cat ~such**", :Tab, pane: 0 tmux.send_keys "cat ~such**", :Tab
tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' } tmux.until { |lines| lines.any_include? 'no~such~user' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@ -1371,9 +1405,11 @@ module CompletionTest
# /tmp/fzf\ test**<TAB> # /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys 'C-K', :Enter tmux.send_keys 'foobar$'
tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1].end_with?('/tmp/fzf\ test/foobar') lines[-1].end_with?('/tmp/fzf\ test/foobar')
@ -1382,11 +1418,10 @@ module CompletionTest
# Should include hidden files # Should include hidden files
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" }
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
tmux.until(1) do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-2].include?('100/') && lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-')
lines[-3].include?('/tmp/fzf-test/.hidden-')
end end
tmux.send_keys :Enter tmux.send_keys :Enter
ensure ensure
@ -1396,19 +1431,22 @@ module CompletionTest
end end
def test_file_completion_root def test_file_completion_root
tmux.send_keys 'ls /**', :Tab, pane: 0 tmux.send_keys 'ls /**', :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_dir_completion 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.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab # BTab does not work here tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55 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.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@ -1435,14 +1473,15 @@ module CompletionTest
lines = tmux.until { |lines| lines[-1].start_with? '[1]' } lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
pid = lines[-1].split.last pid = lines[-1].split.last
tmux.prepare tmux.prepare
tmux.send_keys 'kill ', :Tab, pane: 0 tmux.send_keys 'C-L'
tmux.until(1) { |lines| lines.item_count > 0 } tmux.send_keys 'kill ', :Tab
tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys 'sleep12345' 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.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1] == "kill #{pid}" lines[-1].include? "kill #{pid}"
end end
ensure ensure
Process.kill 'KILL', pid.to_i rescue nil if pid Process.kill 'KILL', pid.to_i rescue nil if pid
@ -1451,10 +1490,10 @@ module CompletionTest
def test_custom_completion def test_custom_completion
tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'ls /tmp/**', :Tab, pane: 0 tmux.send_keys 'ls /tmp/**', :Tab
tmux.until(1) { |lines| lines.item_count == 11 } tmux.until { |lines| lines.item_count == 11 }
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :Tab, :Tab, :Tab
tmux.until(1) { |lines| lines[-2].include? '(3)' } tmux.until { |lines| lines.select_count == 3 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@ -1463,49 +1502,48 @@ module CompletionTest
end end
def test_unset_completion def test_unset_completion
tmux.send_keys 'export FOO=BAR', :Enter tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare tmux.prepare
# Using tmux # Using tmux
tmux.send_keys 'unset FOO**', :Tab, pane: 0 tmux.send_keys 'unset FZFFOO**', :Tab
tmux.until(1) { |lines| lines[-2].include? ' 1/' } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'unset FOO' } tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
# FZF_TMUX=0 # FZF_TMUX=1
new_shell new_shell
tmux.send_keys 'unset FOO**', :Tab tmux.send_keys 'unset FZFFO**', :Tab, pane: 0
tmux.until { |lines| lines[-2].include? ' 1/' } tmux.until(1) { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'unset FOO' } tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
end end
def test_file_completion_unicode def test_file_completion_unicode
FileUtils.mkdir_p '/tmp/fzf-test' 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.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"'
tmux.prepare tmux.prepare
tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 tmux.send_keys 'cat fzf-unicode**', :Tab
redraw = ->() { tmux.send_keys 'C-l', pane: 1 } tmux.until { |lines| lines.match_count == 2 }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '1', pane: 1 tmux.send_keys '1'
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :BTab, pane: 1 tmux.send_keys :Tab
tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' } tmux.until { |lines| lines.select_count == 1 }
tmux.send_keys :BSpace, pane: 1 tmux.send_keys :BSpace
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } tmux.until { |lines| lines.match_count == 2 }
tmux.send_keys '2', pane: 1 tmux.send_keys '2'
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } tmux.until { |lines| lines.select_count == 1 }
tmux.send_keys :BTab, pane: 1 tmux.send_keys :Tab
tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' } tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-l' tmux.send_keys 'C-l'
lines[-1].include?('cat') || lines[-2].include?('cat') lines.any_include? 'cat'
end end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' } tmux.until { |lines| lines[-1].include? 'test3test4' }
@ -1518,7 +1556,7 @@ class TestBash < TestBase
def new_shell def new_shell
tmux.prepare tmux.prepare
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
tmux.prepare tmux.prepare
end end
@ -1533,7 +1571,7 @@ class TestZsh < TestBase
include CompletionTest include CompletionTest
def new_shell def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
tmux.prepare tmux.prepare
end end
@ -1547,7 +1585,7 @@ class TestFish < TestBase
include TestShell include TestShell
def new_shell 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.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| lines.empty? } tmux.until { |lines| lines.empty? }
end end