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:
include:
- env: TAGS=
rvm: 2.2.0
rvm: 2.3.3
# - env: TAGS=tcell
# rvm: 2.2.0

View File

@ -1,6 +1,18 @@
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
------
- 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
multi-select mode
#### Layout
fzf by default starts in fullscreen mode, but you can make it start below the
cursor with `--height` option.
```sh
vim $(fzf --height 40%)
```
Also check out `--reverse` option if you prefer "top-down" layout instead of
the default "bottom-up" layout.
```sh
vim $(fzf --height 40% --reverse)
```
You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
default.
```sh
export FZF_DEFAULT_OPTS='--height 40% --reverse'
```
#### Search syntax
Unless otherwise specified, fzf starts in "extended-search mode" where you can
@ -189,6 +212,13 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
It will still work even when you're not on tmux, silently ignoring `-[udlr]`
options, so you can invariably use `fzf-tmux` in your scripts.
Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in
fullscreen mode.
```sh
fzf --height 40%
```
Key bindings for command line
-----------------------------
@ -206,9 +236,9 @@ fish.
- Set `FZF_ALT_C_COMMAND` to override the default command
- Set `FZF_ALT_C_OPTS` to pass additional options
If you're on a tmux session, fzf will start in a split pane. You may disable
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you're on a tmux session, you can start fzf in a split pane by setting
`FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT`
(e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
@ -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#^\./##'"
```
License
-------
[License](LICENSE)
------------------
[MIT](LICENSE)
The MIT License (MIT)
Author
------
Junegunn Choi
Copyright (c) 2017 Junegunn Choi

View File

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

13
install
View File

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

View File

@ -1,7 +1,7 @@
.ig
The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi
Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf-tmux 1 "Nov 2016" "fzf 0.15.9" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@ -1,7 +1,7 @@
.ig
The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi
Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "Nov 2016" "fzf 0.15.9" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Jan 2017" "fzf 0.16.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@ -48,6 +48,9 @@ Case-insensitive match (default: smart-case match)
.B "+i"
Case-sensitive match
.TP
.B "--literal"
Do not normalize latin script letters for matching.
.TP
.BI "--algo=" TYPE
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
on the center of the screen.
.TP
.B "--filepath-word"
Make word-wise movements and actions respect path separators. The following
actions are affected:
\fBbackward-kill-word\fR
.br
\fBbackward-word\fR
.br
\fBforward-word\fR
.br
\fBkill-word\fR
.TP
.BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR
.SS Layout
.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"
Reverse orientation
.TP
@ -185,7 +208,9 @@ Number of spaces for a tab character (default: 8)
.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color configuration. The name of the base color scheme is followed by custom
color mappings. Ansi color code of -1 denotes terminal default
foreground/background color.
foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
format, but the support for 24-bit colors is experimental and only works when
\fB--height\fR option is used.
.RS
e.g. \fBfzf --color=bg+:24\fR
@ -248,10 +273,11 @@ e.g. \fBfzf --preview="head -$LINES {}"\fR
Note that you can escape a placeholder pattern by prepending a backslash.
.RE
.TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]"
.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]"
Determine the layout of the preview window. If the argument ends with
\fB:hidden\fR, the preview window will be hidden by default until
\fBtoggle-preview\fR action is triggered.
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
Line wrap can be enabled with \fB:wrap\fR flag.
.RS
.B POSITION: (default: right)

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ __fzf_select__() {
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}"
eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do
printf '%q ' "$item"
done
echo
@ -13,8 +13,13 @@ __fzf_select__() {
if [[ $- =~ i ]]; then
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
__fzfcmd() {
[ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
__fzf_use_tmux__ &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
__fzf_select_tmux__() {
@ -26,7 +31,7 @@ __fzf_select_tmux__() {
height="-l $height"
fi
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
}
fzf-file-widget() {
@ -43,7 +48,7 @@ __fzf_cd__() {
local cmd dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
dir=$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
}
__fzf_history__() (
@ -51,7 +56,7 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch
line=$(
HISTTIMEFORMAT= history |
eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
@ -60,22 +65,15 @@ __fzf_history__() (
fi
)
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0
__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0
if [[ ! -o vi ]]; then
# Required to refresh the prompt after fzf
bind '"\er": redraw-current-line'
bind '"\e^": history-expand-line'
# CTRL-T - Paste the selected file path into the command line
if [ $__use_bind_x -eq 1 ]; then
if [ $BASH_VERSINFO -gt 3 ]; then
bind -x '"\C-t": "fzf-file-widget"'
elif [ $__use_tmux -eq 1 ]; then
elif __fzf_use_tmux__; then
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
@ -102,9 +100,9 @@ else
# CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position
if [ $__use_bind_x -eq 1 ]; then
if [ $BASH_VERSINFO -gt 3 ]; then
bind -x '"\C-t": "fzf-file-widget"'
elif [ $__use_tmux -eq 1 ]; then
elif __fzf_use_tmux__; then
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"'
else
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
@ -120,6 +118,4 @@ else
bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
fi
unset -v __use_tmux __use_bind_x
fi

View File

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

View File

@ -9,7 +9,7 @@ __fsel() {
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null
eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
echo -n "${(q)item} "
done
local ret=$?
@ -17,8 +17,13 @@ __fsel() {
return $ret
}
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
__fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
__fzf_use_tmux__ &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
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 \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null
cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}"
cd "${$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m):-.}"
local ret=$?
zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
@ -49,7 +54,8 @@ bindkey '\ec' fzf-cd-widget
fzf-history-widget() {
local selected num
setopt localoptions noglobsubst pipefail 2> /dev/null
selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") )
selected=( $(fc -l 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) )
local ret=$?
if [ -n "$selected" ]; then
num=$selected[1]

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

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) {
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
}
func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) {
if !caseSensitive {
pattern = strings.ToLower(pattern)
}
res, pos := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
var start, end int
if pos == nil || len(*pos) == 0 {
start = res.Start
@ -156,6 +160,21 @@ func TestEmptyPattern(t *testing.T) {
}
}
func TestNormalize(t *testing.T) {
caseSensitive := false
normalize := true
forward := true
test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) {
for _, fun := range funs {
assertMatch2(t, fun, caseSensitive, normalize, forward,
input, pattern, sidx, eidx, score)
}
}
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
}
func TestLongString(t *testing.T) {
bytes := make([]byte, math.MaxUint16*2)
for i := range bytes {

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 (
// Current version
version = "0.15.9"
version = "0.16.0"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@ -143,7 +143,7 @@ func Run(opts *Options) {
}
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, forward,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
}
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -1,6 +1,9 @@
package tui
import (
"fmt"
"os"
"strconv"
"time"
)
@ -115,6 +118,47 @@ const (
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 {
Fg Color
Bg Color
@ -146,23 +190,85 @@ type MouseEvent struct {
Mod bool
}
var (
_color bool
_prevDownTime time.Time
_clickY []int
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
)
type Renderer interface {
Init()
Pause()
Resume() bool
Clear()
RefreshWindows(windows []Window)
Refresh()
Close()
type Window struct {
impl *WindowImpl
Top int
Left int
Width int
Height int
GetChar() Event
MaxX() int
MaxY() int
DoesAutoWrap() bool
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 {
return &ColorTheme{
Fg: colUndefined,
@ -180,9 +286,12 @@ func EmptyTheme() *ColorTheme {
Border: colUndefined}
}
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(2)
}
func init() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
Default16 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
@ -227,14 +336,13 @@ func init() {
Border: 145}
}
func InitTheme(theme *ColorTheme, black bool) {
_color = theme != nil
if !_color {
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
if theme == nil {
initPalette(theme)
return
}
baseTheme := DefaultTheme()
if black {
if forceBlack {
theme.Bg = colBlack
}
@ -257,4 +365,48 @@ func InitTheme(theme *ColorTheme, black bool) {
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
initPalette(theme)
}
func initPalette(theme *ColorTheme) {
ColDefault = ColorPair{colDefault, colDefault, 0}
if theme != nil {
ColNormal = ColorPair{theme.Fg, theme.Bg, 1}
ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2}
ColMatch = ColorPair{theme.Match, theme.Bg, 3}
ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4}
ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5}
ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6}
ColInfo = ColorPair{theme.Info, theme.Bg, 7}
ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8}
ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9}
ColHeader = ColorPair{theme.Header, theme.Bg, 10}
ColBorder = ColorPair{theme.Border, theme.Bg, 11}
} else {
ColNormal = ColorPair{colDefault, colDefault, 1}
ColPrompt = ColorPair{colDefault, colDefault, 2}
ColMatch = ColorPair{colDefault, colDefault, 3}
ColCurrent = ColorPair{colDefault, colDefault, 4}
ColCurrentMatch = ColorPair{colDefault, colDefault, 5}
ColSpinner = ColorPair{colDefault, colDefault, 6}
ColInfo = ColorPair{colDefault, colDefault, 7}
ColCursor = ColorPair{colDefault, colDefault, 8}
ColSelected = ColorPair{colDefault, colDefault, 9}
ColHeader = ColorPair{colDefault, colDefault, 10}
ColBorder = ColorPair{colDefault, colDefault, 11}
}
ColUser = ColorPair{colDefault, colDefault, 12}
}
func attrFor(color ColorPair, attr Attr) Attr {
switch color {
case ColCurrent:
return attr | Reverse
case ColMatch:
return attr | Underline
case ColCurrentMatch:
return attr | Underline | Reverse
}
return attr
}

View File

@ -1,14 +1,20 @@
package tui
import (
"testing"
)
import "testing"
func TestPairFor(t *testing.T) {
if PairFor(30, 50) != PairFor(30, 50) {
t.Fail()
}
if PairFor(-1, 10) != PairFor(-1, 10) {
t.Fail()
func TestHexToColor(t *testing.T) {
assert := func(expr string, r, g, b int) {
color := HexToColor(expr)
if !color.is24() ||
int((color>>16)&0xff) != r ||
int((color>>8)&0xff) != g ||
int((color)&0xff) != b {
t.Fail()
}
}
assert("#ff0000", 255, 0, 0)
assert("#010203", 1, 2, 3)
assert("#102030", 16, 32, 48)
assert("#ffffff", 255, 255, 255)
}

View File

@ -6,8 +6,24 @@ import (
"time"
"github.com/junegunn/go-isatty"
"github.com/junegunn/go-runewidth"
)
var _runeWidths = make(map[rune]int)
// RuneWidth returns rune width
func RuneWidth(r rune, prefixWidth int, tabstop int) int {
if r == '\t' {
return tabstop - prefixWidth%tabstop
} else if w, found := _runeWidths[r]; found {
return w
} else {
w := Max(runewidth.RuneWidth(r), 1)
_runeWidths[r] = w
return w
}
}
// Max returns the largest integer
func Max(first int, second int) int {
if first >= second {

View File

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

View File

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

View File

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