summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2017-01-08 01:30:31 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2017-01-08 02:09:56 +0900
commit1448d631a7c72905f62dbb343a8f231a1c3cc52c (patch)
tree05abfedd2a0777c2640c8259267d3ad855879dfe
parentfd137a9e875ba1fd9feed4903e102951f8098c33 (diff)
downloadfzf-1448d631a7c72905f62dbb343a8f231a1c3cc52c.tar.gz
Add --height option
-rw-r--r--.travis.yml2
-rw-r--r--CHANGELOG.md6
-rw-r--r--README.md36
-rwxr-xr-xbin/fzf-tmux3
-rw-r--r--man/man1/fzf.19
-rw-r--r--plugin/fzf.vim26
-rw-r--r--shell/completion.bash57
-rw-r--r--shell/completion.zsh14
-rw-r--r--shell/key-bindings.bash33
-rw-r--r--shell/key-bindings.fish13
-rw-r--r--shell/key-bindings.zsh10
-rw-r--r--src/options.go18
-rw-r--r--src/result.go2
-rw-r--r--src/result_test.go15
-rw-r--r--src/terminal.go225
-rw-r--r--src/tui/light.go764
-rw-r--r--src/tui/ncurses.go250
-rw-r--r--src/tui/tcell.go273
-rw-r--r--src/tui/tui.go170
-rw-r--r--src/tui/tui_test.go14
-rw-r--r--src/util/util.go16
-rw-r--r--src/util/util_unix.go6
-rw-r--r--src/util/util_windows.go6
-rw-r--r--test/test_go.rb272
24 files changed, 1628 insertions, 612 deletions
diff --git a/.travis.yml b/.travis.yml
index a1a6497d..3f2a67f7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,7 @@ language: ruby
matrix:
include:
- env: TAGS=
- rvm: 2.2.0
+ rvm: 2.3.3
# - env: TAGS=tcell
# rvm: 2.2.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a37b74c5..f78270c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
CHANGELOG
=========
+0.16.0
+------
+- Added `--height HEIGHT[%]` option
+- Preview window will truncate long lines by default. Line wrap can be enabled
+ by `:wrap` flag in `--preview-window`.
+
0.15.9
------
- Fixed rendering glitches introduced in 0.15.8
diff --git a/README.md b/README.md
index 63812794..550ea777 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,29 @@ vim $(fzf)
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode
+#### Layout
+
+fzf by default starts in fullscreen mode, but you can make it start below the
+cursor with `--height` option.
+
+```sh
+vim $(fzf --height 40%)
+```
+
+Also check out `--reverse` option if you prefer "top-down" layout instead of
+the default "bottom-up" layout.
+
+```sh
+vim $(fzf --height 40% --reverse)
+```
+
+You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
+default.
+
+```sh
+export FZF_DEFAULT_OPTS='--height 40% --reverse'
+```
+
#### Search syntax
Unless otherwise specified, fzf starts in "extended-search mode" where you can
@@ -189,6 +212,13 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
It will still work even when you're not on tmux, silently ignoring `-[udlr]`
options, so you can invariably use `fzf-tmux` in your scripts.
+Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in
+fullscreen mode.
+
+```sh
+fzf --height 40%
+```
+
Key bindings for command line
-----------------------------
@@ -206,9 +236,9 @@ fish.
- Set `FZF_ALT_C_COMMAND` to override the default command
- Set `FZF_ALT_C_OPTS` to pass additional options
-If you're on a tmux session, fzf will start in a split pane. You may disable
-this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
-pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
+If you're on a tmux session, you can start fzf in a split pane by setting
+`FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT`
+(e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
diff --git a/bin/fzf-tmux b/bin/fzf-tmux
index dd335d2b..f2011998 100755
--- a/bin/fzf-tmux
+++ b/bin/fzf-tmux
@@ -114,6 +114,9 @@ if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$l
exit $?
fi
+# --height option is not allowed
+args+=("--no-height")
+
# Handle zoomed tmux pane by moving it to a temp window
if tmux list-panes -F '#F' | grep -q Z; then
zoomed=1
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 4e19ce92..fac2aab5 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -130,6 +130,10 @@ on the center of the screen.
Label characters for \fBjump\fR and \fBjump-accept\fR
.SS Layout
.TP
+.BI "--height=" "HEIGHT[%]"
+Display fzf window below the cursor with the given height instead of using
+fullscreen.
+.TP
.B "--reverse"
Reverse orientation
.TP
@@ -248,10 +252,11 @@ e.g. \fBfzf --preview="head -$LINES {}"\fR
Note that you can escape a placeholder pattern by prepending a backslash.
.RE
.TP
-.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]"
+.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]"
Determine the layout of the preview window. If the argument ends with
\fB:hidden\fR, the preview window will be hidden by default until
-\fBtoggle-preview\fR action is triggered.
+\fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
+Line wrap can be enabled with \fB:wrap\fR flag.
.RS
.B POSITION: (default: right)
diff --git a/plugin/fzf.vim b/plugin/fzf.vim
index 06af648e..c8c6c91f 100644
--- a/plugin/fzf.vim
+++ b/plugin/fzf.vim
@@ -296,14 +296,24 @@ try
else
let prefix = ''
endif
- let tmux = (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
+
+ let use_height = has_key(dict, 'down') &&
+ \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right'))
+ let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
+ let term = has('nvim') && !tmux
+ if use_height
+ let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict)
+ elseif term
+ let optstr .= ' --no-height'
+ endif
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
- if has('nvim') && !tmux
+ if term
return s:execute_term(dict, command, temps)
endif
- let lines = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps)
+ let lines = tmux ? s:execute_tmux(dict, command, temps)
+ \ : s:execute(dict, command, use_height, temps)
call s:callback(dict, lines)
return lines
finally
@@ -400,9 +410,9 @@ function! s:exit_handler(code, command, ...)
return 1
endfunction
-function! s:execute(dict, command, temps) abort
+function! s:execute(dict, command, use_height, temps) abort
call s:pushd(a:dict)
- if has('unix')
+ if has('unix') && !a:use_height
silent! !clear 2> /dev/null
endif
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
@@ -416,7 +426,11 @@ function! s:execute(dict, command, temps) abort
else
let command = escaped
endif
- execute 'silent !'.command
+ if a:use_height
+ call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s 2> /dev/tty', &lines, command))
+ else
+ execute 'silent !'.command
+ endif
let exit_status = v:shell_error
redraw!
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
diff --git a/shell/completion.bash b/shell/completion.bash
index 392aee2d..d6d7238d 100644
--- a/shell/completion.bash
+++ b/shell/completion.bash
@@ -5,7 +5,7 @@
# / __/ / /_/ __/
# /_/ /___/_/-completion.bash
#
-# - $FZF_TMUX (default: 1)
+# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
@@ -30,6 +30,15 @@ fi
###########################################################
+# To redraw line after fzf closes (printf '\e[5n')
+bind '"\e[0n": redraw-current-line'
+
+__fzfcmd_complete() {
+ [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
+ echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" ||
+ echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse"
+}
+
_fzf_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
@@ -43,35 +52,42 @@ _fzf_opts_completion() {
opts="
-x --extended
-e --exact
+ --algo
-i +i
-n --nth
+ --with-nth
-d --delimiter
+s --no-sort
--tac
--tiebreak
- --bind
-m --multi
--no-mouse
- --color
- --black
- --reverse
+ --bind
+ --cycle
--no-hscroll
+ --jump-labels
+ --height
+ --reverse
+ --margin
--inline-info
--prompt
+ --header
+ --header-lines
+ --ansi
+ --tabstop
+ --color
+ --no-bold
+ --history
+ --history-size
+ --preview
+ --preview-window
-q --query
-1 --select-1
-0 --exit-0
-f --filter
--print-query
--expect
- --toggle-sort
- --sync
- --cycle
- --history
- --history-size
- --header
- --header-lines
- --margin"
+ --sync"
case "${prev}" in
--tiebreak)
@@ -116,7 +132,7 @@ _fzf_handle_dynamic_completion() {
__fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf
- [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
+ fzf="$(__fzfcmd_complete)"
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'}
@@ -132,7 +148,6 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
- tput sc
matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do
printf "%q$3 " "$item"
done)
@@ -142,7 +157,7 @@ __fzf_generic_path_completion() {
else
COMPREPLY=( "$cur" )
fi
- tput rc
+ printf '\e[5n'
return 0
fi
dir=$(dirname "$dir")
@@ -160,7 +175,7 @@ _fzf_complete() {
local cur selected trigger cmd fzf post
post="$(caller 0 | awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post=cat
- [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
+ fzf="$(__fzfcmd_complete)"
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'}
@@ -168,10 +183,9 @@ _fzf_complete() {
if [[ "$cur" == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}}
- tput sc
selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
- tput rc
+ printf '\e[5n'
if [ -n "$selected" ]; then
COMPREPLY=("$selected")
@@ -200,10 +214,9 @@ _fzf_complete_kill() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected fzf
- [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
- tput sc
+ fzf="$(__fzfcmd_complete)"
selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
- tput rc
+ printf '\e[5n'
if [ -n "$selected" ]; then
COMPREPLY=( "$selected" )
diff --git a/shell/completion.zsh b/shell/completion.zsh
index d3faef80..fb2c16a1 100644
--- a/shell/completion.zsh
+++ b/shell/completion.zsh
@@ -5,7 +5,7 @@
# / __/ / /_/ __/
# /_/ /___/_/-completion.zsh
#
-# - $FZF_TMUX (default: 1)
+# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
@@ -30,6 +30,12 @@ fi
###########################################################
+__fzfcmd_complete() {
+ [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
+ echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" ||
+ echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse"
+}
+
__fzf_generic_path_completion() {
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
@@ -39,7 +45,7 @@ __fzf_generic_path_completion() {
fzf_opts=$4
suffix=$5
tail=$6
- [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
+ fzf="$(__fzfcmd_complete)"
setopt localoptions nonomatch
dir="$base"
@@ -90,7 +96,7 @@ _fzf_complete() {
post="${funcstack[2]}_post"
type $post > /dev/null 2>&1 || post=cat
- [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
+ fzf="$(__fzfcmd_complete)"
_fzf_feed_fifo "$fifo"
matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ')
@@ -157,7 +163,7 @@ fzf-completion() {
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence)
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
- [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
+ fzf="$(__fzfcmd_complete)"
matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$LBUFFER$matches"
diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash
index 33d8bc8d..382302d4 100644
--- a/shell/key-bindings.bash
+++ b/shell/key-bindings.bash
@@ -5,7 +5,7 @@ __fzf_select__() {
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}"
- eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do
+ eval "$cmd | fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse -m $@ $FZF_CTRL_T_OPTS" | while read -r item; do
printf '%q ' "$item"
done
echo
@@ -13,8 +13,14 @@ __fzf_select__() {
if [[ $- =~ i ]]; then
+__fzf_use_tmux__() {
+ [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
+}
+
__fzfcmd() {
- [ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
+ __fzf_use_tmux__ &&
+ echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" ||
+ echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse"
}
__fzf_select_tmux__() {
@@ -26,14 +32,14 @@ __fzf_select_tmux__() {
height="-l $height"
fi
- tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
+ tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
}
fzf-file-widget() {
if __fzf_use_tmux__; then
__fzf_select_tmux__
else
- local selected="$(__fzf_select__)"
+ local selected="$(__fzf_select__ --height ${FZF_TMUX_HEIGHT:-40%} --reverse)"
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
fi
@@ -51,7 +57,7 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch
line=$(
HISTTIMEFORMAT= history |
- eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" |
+ eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" |
command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
@@ -60,22 +66,15 @@ __fzf_history__() (
fi
)
-__fzf_use_tmux__() {
- [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ]
-}
-
-[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0
-__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0
-
if [[ ! -o vi ]]; then
# Required to refresh the prompt after fzf
bind '"\er": redraw-current-line'
bind '"\e^": history-expand-line'
# CTRL-T - Paste the selected file path into the command line
- if [ $__use_bind_x -eq 1 ]; then
+ if [ $BASH_VERSINFO -gt 3 ]; then
bind -x '"\C-t": "fzf-file-widget"'
- elif [ $__use_tmux -eq 1 ]; then
+ elif __fzf_use_tmux__; then
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
@@ -102,9 +101,9 @@ else
# CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position
- if [ $__use_bind_x -eq 1 ]; then
+ if [ $BASH_VERSINFO -gt 3 ]; then
bind -x '"\C-t": "fzf-file-widget"'
- elif [ $__use_tmux -eq 1 ]; then
+ elif __fzf_use_tmux__; then
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"'
else
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
@@ -120,6 +119,4 @@ else
bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
fi
-unset -v __use_tmux __use_bind_x
-
fi
diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish
index dd75fecf..fc618448 100644
--- a/shell/key-bindings.fish
+++ b/shell/key-bindings.fish
@@ -39,7 +39,7 @@ function fzf_key_bindings
end
function fzf-history-widget -d "Show command history"
- history | eval (__fzfcmd) +s +m --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result
+ history | eval (__fzfcmd) +s +m --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result
and commandline -- $result
commandline -f repaint
end
@@ -54,15 +54,12 @@ function fzf_key_bindings
end
function __fzfcmd
- set -q FZF_TMUX; or set FZF_TMUX 1
+ set -q FZF_TMUX; or set FZF_TMUX 0
+ set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
if [ $FZF_TMUX -eq 1 ]
- if set -q FZF_TMUX_HEIGHT
- echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
- else
- echo "fzf-tmux -d40%"
- end
+ echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
else
- echo "fzf"
+ echo "fzf --height $FZF_TMUX_HEIGHT --reverse"
end
end
diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh
index fed01532..7e24d92a 100644
--- a/shell/key-bindings.zsh
+++ b/shell/key-bindings.zsh
@@ -17,8 +17,14 @@ __fsel() {
return $ret
}
+__fzf_use_tmux__() {
+ [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
+}
+
__fzfcmd() {
- [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
+ __fzf_use_tmux__ &&
+ echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" ||
+ echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse"
}
fzf-file-widget() {
@@ -49,7 +55,7 @@ bindkey '\ec' fzf-cd-widget
fzf-history-widget() {
local selected num
setopt localoptions noglobsubst pipefail 2> /dev/null
- selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") )
+ selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") )
local ret=$?
if [ -n "$selected" ]; then
num=$selected[1]
diff --git a/src/options.go b/src/options.go
index 6fd3f6c6..0c6661f3 100644
--- a/src/options.go
+++ b/src/options.go
@@ -10,6 +10,7 @@ import (
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui"
+ "github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-shellwords"
)
@@ -46,6 +47,8 @@ const usage = `usage: fzf [options]
--jump-labels=CHARS Label characters for jump and jump-accept
Layout
+ --height=HEIGHT[%] Display fzf window below the cursor with the given
+ height instead of using fullscreen
--reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query
@@ -147,6 +150,7 @@ type Options struct {
Theme *tui.ColorTheme
Black bool
Bold bool
+ Height sizeSpec
Reverse bool
Cycle bool
Hscroll bool
@@ -760,6 +764,14 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
return sizeSpec{val, percent}
}
+func parseHeight(str string) sizeSpec {
+ if util.IsWindows() {
+ errorExit("--height options is currently not supported on Windows")
+ }
+ size := parseSize(str, 100, "height")
+ return size
+}
+
func parsePreviewWindow(opts *previewOpts, input string) {
// Default
opts.position = posRight
@@ -1003,6 +1015,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--preview-window":
parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]"))
+ case "--height":
+ opts.Height = parseHeight(nextString(allArgs, &i, "height required: [HEIGHT[%]]"))
+ case "--no-height":
+ opts.Height = sizeSpec{}
case "--no-margin":
opts.Margin = defaultMargin()
case "--margin":
@@ -1029,6 +1045,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s", "--sort="); match {
opts.Sort = 1 // Don't care
+ } else if match, value := optString(arg, "--height="); match {
+ opts.Height = parseHeight(value)
} else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match {
diff --git a/src/result.go b/src/result.go
index e2d7c755..3d79176f 100644
--- a/src/result.go
+++ b/src/result.go
@@ -166,7 +166,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
- color: tui.PairFor(fg, bg),
+ color: tui.NewColorPair(fg, bg),
attr: ansi.color.attr.Merge(attr)})
}
}
diff --git a/src/result_test.go b/src/result_test.go
index 15b1bdbb..0e91fc87 100644
--- a/src/result_test.go
+++ b/src/result_test.go
@@ -105,7 +105,8 @@ func TestColorOffset(t *testing.T) {
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
- colors := item.colorOffsets(offsets, tui.Dark256, 99, 0, true)
+ pair := tui.NewColorPair(99, 199)
+ colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true)
assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) {
var attr tui.Attr
if bold {
@@ -116,10 +117,10 @@ func TestColorOffset(t *testing.T) {
t.Error(o)
}
}
- assert(0, 0, 5, tui.ColUser, false)
- assert(1, 5, 15, 99, false)
- assert(2, 15, 20, tui.ColUser, false)
- assert(3, 22, 25, tui.ColUser+1, true)
- assert(4, 25, 35, 99, false)
- assert(5, 35, 40, tui.ColUser+2, true)
+ assert(0, 0, 5, tui.NewColorPair(1, 5), false)
+ assert(1, 5, 15, pair, false)
+ assert(2, 15, 20, tui.NewColorPair(1, 5), false)
+ assert(3, 22, 25, tui.NewColorPair(2, 6), true)
+ assert(4, 25, 35, pair, false)
+ assert(5, 35, 40, tui.NewColorPair(4, 8), true)
}
diff --git a/src/terminal.go b/src/terminal.go
index 5b482b06..80029236 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -15,8 +15,6 @@ import (
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
-
- "github.com/junegunn/go-runewidth"
)
// import "github.com/pkg/profile"
@@ -42,6 +40,14 @@ type previewer struct {
enabled bool
}
+type itemLine struct {
+ current bool
+ label string
+ result Result
+}
+
+var emptyLine = itemLine{}
+
// Terminal represents terminal input/output
type Terminal struct {
initDelay time.Duration
@@ -69,11 +75,12 @@ type Terminal struct {
header []string
header0 []string
ansi bool
+ tabstop int
margin [4]sizeSpec
strong tui.Attr
- window *tui.Window
- bwindow *tui.Window
- pwindow *tui.Window
+ window tui.Window
+ bwindow tui.Window
+ pwindow tui.Window
count int
progress int
reading bool
@@ -89,10 +96,12 @@ type Terminal struct {
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
+ prevLines []itemLine
suppress bool
startChan chan bool
slab *util.Slab
theme *tui.ColorTheme
+ tui tui.Renderer
}
type selectedItem struct {
@@ -115,7 +124,6 @@ func (a byTimeOrder) Less(i, j int) bool {
}
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
-var _runeWidths = make(map[rune]int)
var _tabStop int
const (
@@ -247,7 +255,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} else {
header = reverseStringArray(opts.Header)
}
- _tabStop = opts.Tabstop
var delay time.Duration
if opts.Tac {
delay = initialDelayTac
@@ -262,6 +269,24 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
if !opts.Bold {
strongAttr = tui.AttrRegular
}
+ var renderer tui.Renderer
+ if opts.Height.size > 0 {
+ maxHeightFunc := func(termHeight int) int {
+ var maxHeight int
+ if opts.Height.percent {
+ maxHeight = int(opts.Height.size * float64(termHeight) / 100.0)
+ } else {
+ maxHeight = util.Min(int(opts.Height.size), termHeight)
+ }
+ if opts.InlineInfo {
+ return util.Max(maxHeight, 3)
+ }
+ return util.Max(maxHeight, 4)
+ }
+ renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc)
+ } else {
+ renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
+ }
return &Terminal{
initDelay: delay,
inlineInfo: opts.InlineInfo,
@@ -290,6 +315,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
header: header,
header0: header,
ansi: opts.Ansi,
+ tabstop: opts.Tabstop,
reading: true,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
@@ -306,9 +332,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
- initFunc: func() {
- tui.Init(opts.Theme, opts.Black, opts.Mouse)
- }}
+ tui: renderer,
+ initFunc: func() { renderer.Init() }}
}
// Input returns current query string
@@ -401,22 +426,10 @@ func (t *Terminal) sortSelected() []selectedItem {
return sels
}
-func runeWidth(r rune, prefixWidth int) int {
- if r == '\t' {
- return _tabStop - prefixWidth%_tabStop
- } else if w, found := _runeWidths[r]; found {
- return w
- } else {
- w := runewidth.RuneWidth(r)
- _runeWidths[r] = w
- return w
- }
-}
-
-func displayWidth(runes []rune) int {
+func (t *Terminal) displayWidth(runes []rune) int {
l := 0
for _, r := range runes {
- l += runeWidth(r, l)
+ l += util.RuneWidth(r, l, t.tabstop)
}
return l
}
@@ -437,9 +450,10 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
}
func (t *Terminal) resizeWindows() {
- screenWidth := tui.MaxX()
- screenHeight := tui.MaxY()
+ screenWidth := t.tui.MaxX()
+ screenHeight := t.tui.MaxY()
marginInt := [4]int{}
+ t.prevLines = make([]itemLine, screenHeight)
for idx, sizeSpec := range t.margin {
if sizeSpec.percent {
var max float64
@@ -487,40 +501,40 @@ func (t *Terminal) resizeWindows() {
height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() {
createPreviewWindow := func(y int, x int, w int, h int) {
- t.bwindow = tui.NewWindow(y, x, w, h, true)
+ t.bwindow = t.tui.NewWindow(y, x, w, h, true)
pwidth := w - 4
// ncurses auto-wraps the line when the cursor reaches the right-end of
// the window. To prevent unintended line-wraps, we use the width one
// column larger than the desired value.
- if !t.preview.wrap && tui.DoesAutoWrap() {
+ if !t.preview.wrap && t.tui.DoesAutoWrap() {
pwidth += 1
}
- t.pwindow = tui.NewWindow(y+1, x+2, pwidth, h-2, false)
+ t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, false)
}
switch t.preview.position {
case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
- t.window = tui.NewWindow(
+ t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
- t.window = tui.NewWindow(
+ t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
- t.window = tui.NewWindow(
+ t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
- t.window = tui.NewWindow(
+ t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
}
} else {
- t.window = tui.NewWindow(
+ t.window = t.tui.NewWindow(
marginInt[0],
marginInt[3],
width,
@@ -530,7 +544,7 @@ func (t *Terminal) resizeWindows() {
func (t *Terminal) move(y int, x int, clear bool) {
if !t.reverse {
- y = t.window.Height - y - 1
+ y = t.window.Height() - y - 1
}
if clear {
@@ -541,7 +555,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
}
func (t *Terminal) placeCursor() {
- t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input[:t.cx]), false)
+ t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input[:t.cx]), false)
}
func (t *Terminal) printPrompt() {
@@ -552,7 +566,7 @@ func (t *Terminal) printPrompt() {
func (t *Terminal) printInfo() {
if t.inlineInfo {
- t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
+ t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input)+1, true)
if t.reading {
t.window.CPrint(tui.ColSpinner, t.strong, " < ")
} else {
@@ -589,7 +603,7 @@ func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
- max := t.window.Height
+ max := t.window.Height()
var state *ansiState
for idx, lineStr := range t.header {
line := idx + 2
@@ -616,19 +630,25 @@ func (t *Terminal) printList() {
maxy := t.maxItems()
count := t.merger.Length() - t.offset
- for i := 0; i < maxy; i++ {
+ for j := 0; j < maxy; j++ {
+ i := j
+ if !t.reverse {
+ i = maxy - 1 - j
+ }
line := i + 2 + len(t.header)
if t.inlineInfo {
line--
}
- t.move(line, 0, true)
if i < count {
- t.printItem(t.merger.Get(i+t.offset), i, i == t.cy-t.offset)
+ t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset)
+ } else if t.prevLines[i] != emptyLine {
+ t.prevLines[i] = emptyLine
+ t.move(line, 0, true)
}
}
}
-func (t *Terminal) printItem(result *Result, i int, current bool) {
+func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
item := result.item
_, selected := t.selected[item.Index()]
label := " "
@@ -641,6 +661,15 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
} else if current {
label = ">"
}
+
+ // Avoid unnecessary redraw
+ newLine := itemLine{current, label, *result}
+ if t.prevLines[i] == newLine {
+ return
+ }
+ t.prevLines[i] = newLine
+
+ t.move(line, 0, true)
t.window.CPrint(tui.ColCursor, t.strong, label)
if current {
if selected {
@@ -659,11 +688,11 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
}
}
-func trimRight(runes []rune, width int) ([]rune, int) {
+func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) {
// We start from the beginning to handle tab characters
l := 0
for idx, r := range runes {
- l += runeWidth(r, l)
+ l += util.RuneWidth(r, l, t.tabstop)
if l > width {
return runes[:idx], len(runes) - idx
}
@@ -671,10 +700,10 @@ func trimRight(runes []rune, width int) ([]rune, int) {
return runes, 0
}
-func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
+func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
l := 0
for _, r := range runes {
- l += runeWidth(r, l+prefixWidth)
+ l += util.RuneWidth(r, l+prefixWidth, t.tabstop)
if l > limit {
// Early exit
return l
@@ -683,27 +712,27 @@ func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
return l
}
-func trimLeft(runes []rune, width int) ([]rune, int32) {
+func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
if len(runes) > maxDisplayWidthCalc && len(runes) > width {
trimmed := len(runes) - width
return runes[trimmed:], int32(trimmed)
}
- currentWidth := displayWidth(runes)
+ currentWidth := t.displayWidth(runes)
var trimmed int32
for currentWidth > width && len(runes) > 0 {
runes = runes[1:]
trimmed++
- currentWidth = displayWidthWithLimit(runes, 2, width)
+ currentWidth = t.displayWidthWithLimit(runes, 2, width)
}
return runes, trimmed
}
-func overflow(runes []rune, max int) bool {
+func (t *Terminal) overflow(runes []rune, max int) bool {
l := 0
for _, r := range runes {
- l += runeWidth(r, l)
+ l += util.RuneWidth(r, l, t.tabstop)
if l > max {
return true
}
@@ -737,22 +766,22 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
}
offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current)
- maxWidth := t.window.Width - 3
+ maxWidth := t.window.Width() - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
- if overflow(text, maxWidth) {
+ if t.overflow(text, maxWidth) {
if t.hscroll {
// Stri..
- if !overflow(text[:maxe], maxWidth-2) {
- text, _ = trimRight(text, maxWidth-2)
+ if !t.overflow(text[:maxe], maxWidth-2) {
+ text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
- if overflow(text[maxe:], 2) {
+ if t.overflow(text[maxe:], 2) {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
var diff int32
- text, diff = trimLeft(text, maxWidth-2)
+ text, diff = t.trimLeft(text, maxWidth-2)
// Transform offsets
for idx, offset := range offsets {
@@ -766,7 +795,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
text = append([]rune(".."), text...)
}
} else {
- text, _ = trimRight(text, maxWidth-2)
+ text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
for idx, offset := range offsets {
@@ -784,11 +813,11 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Constrain32(offset.offset[1], index, maxOffset)
- substr, prefixWidth = processTabs(text[index:b], prefixWidth)
+ substr, prefixWidth = t.processTabs(text[index:b], prefixWidth)
t.window.CPrint(col1, attr, substr)
if b < e {
- substr, prefixWidth = processTabs(text[b:e], prefixWidth)
+ substr, prefixWidth = t.processTabs(text[b:e], prefixWidth)
t.window.CPrint(offset.color, offset.attr, substr)
}
@@ -798,7 +827,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
}
}
if index < maxOffset {
- substr, _ = processTabs(text[index:], prefixWidth)
+ substr, _ = t.processTabs(text[index:], prefixWidth)
t.window.CPrint(col1, attr, substr)
}
}
@@ -835,38 +864,44 @@ func (t *Terminal) printPreview() {
return true
}
}
- if !t.preview.wrap {
- lines := strings.Split(str, "\n")
- for i, line := range lines {
- limit := t.pwindow.Width
- if tui.DoesAutoWrap() {
- limit -= 1
- }
- if i == 0 {
- limit -= t.pwindow.X()
- }
- trimmed, _ := trimRight([]rune(line), limit)
- lines[i], _ = processTabs(trimmed, 0)
+ lines := strings.Split(str, "\n")
+ for i, line := range lines {
+ limit := t.pwindow.Width()
+ if t.tui.DoesAutoWrap() {
+ limit -= 1
}
+ if i == 0 {
+ limit -= t.pwindow.X()
+ }
+ trimmed := []rune(line)
+ if !t.preview.wrap {
+ trimmed, _ = t.trimRight(trimmed, limit)
+ }
+ lines[i], _ = t.processTabs(trimmed, 0)
str = strings.Join(lines, "\n")
}
if ansi != nil && ansi.colored() {
- return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr)
+ return t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
}
return t.pwindow.Fill(str)
})
- if t.previewer.lines > t.pwindow.Height {
+ t.pwindow.FinishFill()
+ if t.previewer.lines > t.pwindow.Height() {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
- t.pwindow.Move(0, t.pwindow.Width-len(offset))
+ pos := t.pwindow.Width() - len(offset)
+ if t.tui.DoesAutoWrap() {
+ pos -= 1
+ }
+ t.pwindow.Move(0, pos)
t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset)
}
}
-func processTabs(runes []rune, prefixWidth int) (string, int) {
+func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer
l := prefixWidth
for _, r := range runes {
- w := runeWidth(r, l)
+ w := util.RuneWidth(r, l, t.tabstop)
l += w
if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w))
@@ -889,9 +924,9 @@ func (t *Terminal) printAll() {
func (t *Terminal) refresh() {
if !t.suppress {
if t.isPreviewEnabled() {
- tui.RefreshWindows([]*tui.Window{t.bwindow, t.pwindow, t.window})
+ t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window})
} else {
- tui.RefreshWindows([]*tui.Window{t.window})
+ t.tui.RefreshWindows([]tui.Window{t.window})
}
}
}
@@ -1013,9 +1048,9 @@ func (t *Terminal) executeCommand(template string, items []*Item) {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
- tui.Pause()
+ t.tui.Pause()
cmd.Run()
- if tui.Resume() {
+ if t.tui.Resume() {
t.printAll()
}
t.refresh()
@@ -1162,11 +1197,11 @@ func (t *Terminal) Loop() {
case reqRefresh:
t.suppress = false
case reqRedraw:
- tui.Clear()
- tui.Refresh()
+ t.tui.Clear()
+ t.tui.Refresh()
t.printAll()
case reqClose:
- tui.Close()
+ t.tui.Close()
if t.output() {
exit(exitOk)
}
@@ -1179,11 +1214,11 @@ func (t *Terminal) Loop() {
case reqPreviewRefresh:
t.printPreview()
case reqPrintQuery:
- tui.Close()
+ t.tui.Close()
t.printer(string(t.input))
exit(exitOk)
case reqQuit:
- tui.Close()
+ t.tui.Close()
exit(exitInterrupt)
}
}
@@ -1196,7 +1231,7 @@ func (t *Terminal) Loop() {
looping := true
for looping {
- event := tui.GetChar()
+ event := t.tui.GetChar()
t.mutex.Lock()
previousInput := t.input
@@ -1288,11 +1323,11 @@ func (t *Terminal) Loop() {
}
case actPreviewPageUp:
if t.isPreviewEnabled() {
- scrollPreview(-t.pwindow.Height)
+ scrollPreview(-t.pwindow.Height())
}
case actPreviewPageDown:
if t.isPreviewEnabled() {
- scrollPreview(t.pwindow.Height)
+ scrollPreview(t.pwindow.Height())
}
case actBeginningOfLine:
t.cx = 0
@@ -1466,11 +1501,11 @@ func (t *Terminal) Loop() {
scrollPreview(-me.S)
}
} else if t.window.Enclose(my, mx) {
- mx -= t.window.Left
- my -= t.window.Top
- mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input))
+ mx -= t.window.Left()
+ my -= t.window.Top()
+ mx = util.Constrain(mx-t.displayWidth([]rune(t.prompt)), 0, len(t.input))
if !t.reverse {
- my = t.window.Height - my - 1
+ my = t.window.Height() - my - 1
}
min := 2 + len(t.header)
if t.inlineInfo {
@@ -1582,7 +1617,7 @@ func (t *Terminal) vset(o int) bool {
}
func (t *Terminal) maxItems() int {
- max := t.window.Height - 2 - len(t.header)
+ max := t.window.Height() - 2 - len(t.header)
if t.inlineInfo {
max++
}
diff --git a/src/tui/light.go b/src/tui/light.go
new file mode 100644
index 00000000..1273c8fb
--- /dev/null
+++ b/src/tui/light.go
@@ -0,0 +1,764 @@
+package tui
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+ "unicode/utf8"
+
+ "github.com/junegunn/fzf/src/util"
+)
+
+const (
+ defaultWidth = 80
+ defaultHeight = 24
+
+ escPollInterval = 5
+)
+
+func openTtyIn() *os.File {
+ in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
+ if err != nil {
+ panic("Failed to open /dev/tty")
+ }
+ return in
+}
+
+// FIXME: Need better handling of non-displayable characters
+func (r *LightRenderer) stderr(str string) {
+ bytes := []byte(str)
+ runes := []rune{}
+ for len(bytes) > 0 {
+ r, sz := utf8.DecodeRune(bytes)
+ if r == utf8.RuneError || r != '\x1b' && r != '\n' && r < 32 {
+ runes = append(runes, '?')
+ } else {
+ runes = append(runes, r)
+ }
+ bytes = bytes[sz:]
+ }
+ r.queued += string(runes)
+}
+
+func (r *LightRenderer) csi(code string) {
+ r.stderr("\x1b[" + code)
+}
+
+func (r *LightRenderer) flush() {
+ if len(r.queued) > 0 {
+ fmt.Fprint(os.Stderr, r.queued)
+ r.queued = ""
+ }
+}
+
+// Light renderer
+type LightRenderer struct {
+ theme *ColorTheme
+ mouse bool
+ forceBlack bool
+ prevDownTime time.Time
+ clickY []int
+ ttyin *os.File
+ buffer []byte
+ ostty string
+ width int
+ height int
+ yoffset int
+ tabstop int
+ escDelay int
+ upOneLine bool
+ queued string
+ maxHeightFunc func(int) int
+}
+
+type LightWindow struct {
+ renderer *LightRenderer
+ colored bool
+ border bool
+ top int
+ left int
+ width int
+ height int
+ posx int
+ posy int
+ tabstop int
+ bg Color
+}
+
+func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer {
+ r := LightRenderer{
+ theme: theme,
+ forceBlack: forceBlack,
+ mouse: mouse,
+ ttyin: openTtyIn(),
+ yoffset: -1,
+ tabstop: tabstop,
+ upOneLine: false,
+ maxHeightFunc: maxHeightFunc}
+ return &r
+}
+
+func (r *LightRenderer) defaultTheme() *ColorTheme {
+ colors, err := util.ExecCommand("tput colors").Output()
+ if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 {
+ return Dark256
+ }
+ return Default16
+}
+
+func stty(cmd string) string {
+ out, err := util.ExecCommand("stty " + cmd + " < /dev/tty").Output()
+ if err != nil {
+ // Not sure how to handle this
+ panic("stty " + cmd + ": " + err.Error())
+ }
+ return strings.TrimSpace(string(out))
+}
+
+func (r *LightRenderer) findOffset() (row int, col int) {
+ r.csi("6n")
+ r.flush()
+ bytes := r.getBytesInternal([]byte{})
+
+ // ^[[*;*R
+ if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' {
+ nums := strings.Split(string(bytes[2:len(bytes)-1]), ";")
+ if len(nums) == 2 {
+ return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1
+ }
+ return -1, -1
+ }
+
+ // No idea
+ return -1, -1
+}
+
+func repeat(s string, times int) string {
+ if times > 0 {
+ return strings.Repeat(s, times)
+ }
+ return ""
+}
+
+func atoi(s string, defaultValue int) int {
+ value, err := strconv.Atoi(s)
+ if err != nil {
+ return defaultValue
+ }
+ return value
+}
+
+func (r *LightRenderer) Init() {
+ delay := 100
+ delayEnv := os.Getenv("ESCDELAY")
+ if len(delayEnv) > 0 {
+ num, err := strconv.Atoi(delayEnv)
+ if err == nil && num >= 0 {
+ delay = num
+ }
+ }
+ r.escDelay = delay
+
+ r.ostty = stty("-g")
+ stty("raw")
+ r.updateTerminalSize()
+ initTheme(r.theme, r.defaultTheme(), r.forceBlack)
+
+ _, x := r.findOffset()
+ if x > 0 {
+ r.upOneLine = true
+ r.stderr("\n")
+ }
+ for i := 1; i < r.MaxY(); i++ {
+ r.stderr("\n")
+ r.csi("G")
+ }
+
+ if r.mouse {
+ r.csi("?1000h")
+ }
+ r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
+ r.csi("G")
+ r.csi("s")
+ r.yoffset, _ = r.findOffset()
+}
+
+func (r *LightRenderer) updateTerminalSize() {
+ sizes := strings.Split(stty("size"), " ")
+ if len(sizes) < 2 {
+ r.width = defaultWidth
+ r.height = r.maxHeightFunc(defaultHeight)
+ } else {
+ r.width = atoi(sizes[1], defaultWidth)
+ r.height = r.maxHeightFunc(atoi(sizes[0], defaultHeight))
+ }
+}
+
+func (r *LightRenderer) getch(nonblock bool) int {
+ b := make([]byte, 1)
+ util.SetNonblock(r.ttyin, nonblock)
+ _, err := r.ttyin.Read(b)
+ if err != nil {
+ return -1
+ }
+ return int(b[0])
+}
+
+func (r *LightRenderer) getBytes() []byte {
+ return r.getBytesInternal(r.buffer)
+}
+
+func (r *LightRenderer) getBytesInternal(buffer []byte) []byte {
+ c := r.getch(false)
+
+ retries := 0
+ if c == ESC {
+ retries = r.escDelay / escPollInterval
+ }
+ buffer = append(buffer, byte(c))
+
+ for {
+ c = r.getch(true)
+ if c == -1 {
+ if retries > 0 {
+ retries--
+ time.Sleep(escPollInterval * time.Millisecond)
+ continue
+ }
+ break
+ }
+ retries = 0
+ buffer = append(buffer, byte(c))
+ }
+
+ return buffer
+}
+
+func (r *LightRenderer) GetChar() Event {
+ if len(r.buffer) == 0 {
+ r.buffer = r.getBytes()
+ }
+ if len(r.buffer) == 0 {
+ panic("Empty buffer")
+ }
+
+ sz := 1
+ defer func() {
+ r.buffer = r.buffer[sz:]
+ }()
+
+ switch r.buffer[0] {
+ case CtrlC:
+ return Event{CtrlC, 0, nil}
+ case CtrlG:
+ return Event{CtrlG, 0, nil}
+ case CtrlQ:
+ return Event{CtrlQ, 0, nil}
+ case 127:
+ return Event{BSpace, 0, nil}
+ case ESC:
+ ev := r.escSequence(&sz)
+ // Second chance
+ if ev.Type == Invalid {
+ r.buffer = r.getBytes()
+ ev = r.escSequence(&sz)
+ }
+ return ev
+ }
+
+ // CTRL-A ~ CTRL-Z
+ if r.buffer[0] <= CtrlZ {
+ return Event{int(r.buffer[0]), 0, nil}
+ }
+ char, rsz := utf8.DecodeRune(r.buffer)
+ if char == utf8.RuneError {
+ return Event{ESC, 0, nil}
+ }
+ sz = rsz
+ return Event{Rune, char, nil}
+}
+
+func (r *LightRenderer) escSequence(sz *int) Event {
+ if len(r.buffer) < 2 {
+ return Event{ESC, 0, nil}
+ }
+ *sz = 2
+ switch r.buffer[1] {
+ case 13:
+ return Event{AltEnter, 0, nil}
+ case 32:
+ return Event{AltSpace, 0, nil}
+ case 47:
+ return Event{AltSlash, 0, nil}
+ case 98:
+ return Event{AltB, 0, nil}
+ case 100:
+ return Event{AltD, 0, nil}
+ case 102:
+ return Event{AltF, 0, nil}
+ case 127:
+ return Event{AltBS, 0, nil}
+ case 91, 79:
+ if len(r.buffer) < 3 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 3
+ switch r.buffer[2] {
+ case 68:
+ return Event{Left, 0, nil}
+ case 67:
+ return Event{Right, 0, nil}
+ case 66:
+ return Event{Down, 0, nil}
+ case 65:
+ return Event{Up, 0, nil}
+ case 90:
+ return Event{BTab, 0, nil}
+ case 72:
+ return Event{Home, 0, nil}
+ case 70:
+ return Event{End, 0, nil}
+ case 77:
+ return r.mouseSequence(sz)
+ case 80:
+ return Event{F1, 0, nil}
+ case 81:
+ return Event{F2, 0, nil}
+ case 82:
+ return Event{F3, 0, nil}
+ case 83:
+ return Event{F4, 0, nil}
+ case 49, 50, 51, 52, 53, 54:
+ if len(r.buffer) < 4 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 4
+ switch r.buffer[2] {
+ case 50:
+ if len(r.buffer) == 5 && r.buffer[4] == 126 {
+ *sz = 5
+ switch r.buffer[3] {
+ case 48:
+ return Event{F9, 0, nil}
+ case 49:
+ return Event{F10, 0, nil}
+ case 51:
+ return Event{F11, 0, nil}
+ case 52:
+ return Event{F12, 0, nil}
+ }
+ }
+ // Bracketed paste mode \e[200~ / \e[201
+ if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 {
+ *sz = 6
+ return Event{Invalid, 0, nil}
+ }
+ return Event{Invalid, 0, nil} // INS
+ case 51:
+ return Event{Del, 0, nil}
+ case 52:
+ return Event{End, 0, nil}
+ case 53:
+ return Event{PgUp, 0, nil}
+ case 54:
+ return Event{PgDn, 0, nil}
+ case 49:
+ switch r.buffer[3] {
+ case 126:
+ return Event{Home, 0, nil}
+ case 53, 55, 56, 57:
+ if len(r.buffer) == 5 && r.buffer[4] == 126 {
+ *sz = 5
+ switch r.buffer[3] {
+ case 53:
+ return Event{F5, 0, nil}
+ case 55:
+ return Event{F6, 0, nil}
+ case 56:
+ return Event{F7, 0, nil}
+ case 57:
+ return Event{F8, 0, nil}
+ }
+ }
+ return Event{Invalid, 0, nil}
+ case 59:
+ if len(r.buffer) != 6 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 6
+ switch r.buffer[4] {
+ case 50:
+ switch r.buffer[5] {
+ case 68:
+ return Event{Home, 0, nil}
+ case 67:
+ return Event{End, 0, nil}
+ }
+ case 53:
+ switch r.buffer[5] {
+ case 68:
+ return Event{SLeft, 0, nil}
+ case 67:
+ return Event{SRight, 0, nil}
+ }
+ } // r.buffer[4]
+ } // r.buffer[3]
+ } // r.buffer[2]
+ } // r.buffer[2]
+ } // r.buffer[1]
+ if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' {
+ return Event{AltA + int(r.buffer[1]) - 'a', 0, nil}
+ }
+ return Event{Invalid, 0, nil}
+}
+
+func (r *LightRenderer) mouseSequence(sz *int) Event {
+ if len(r.buffer) < 6 || r.yoffset < 0 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 6
+ switch r.buffer[3] {
+ case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
+ 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
+ mod := r.buffer[3] >= 36
+ down := r.buffer[3]%2 == 0
+ x := int(r.buffer[4] - 33)
+ y := int(r.buffer[5]-33) - r.yoffset
+ double := false
+ if down {
+ now := time.Now()
+ if now.Sub(r.prevDownTime) < doubleClickDuration {
+ r.clickY = append(r.clickY, y)
+ } else {
+ r.clickY = []int{y}
+ }
+ r.prevDownTime = now
+ } else {
+ if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
+ time.Now().Sub(r.prevDownTime) < doubleClickDuration {
+ double = true
+ }
+ }
+
+ return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
+ case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
+ 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
+ mod := r.buffer[3] >= 100
+ s := 1 - int(r.buffer[3]%2)*2
+ x := int(r.buffer[4] - 33)
+ y := int(r.buffer[5]-33) - r.yoffset
+ return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
+ }
+ return Event{Invalid, 0, nil}
+}
+
+func (r *LightRenderer) Pause() {
+ stty(fmt.Sprintf("%q", r.ostty))
+ r.csi("?1049h")
+ r.flush()
+}
+
+func (r *LightRenderer) Resume() bool {
+ stty("raw")
+ r.csi("?1049l")
+ r.flush()
+ // Should redraw
+ return true
+}
+
+func (r *LightRenderer) Clear() {
+ r.csi("u")
+ r.csi("J")
+ r.flush()
+}
+
+func (r *LightRenderer) RefreshWindows(windows []Window) {
+ r.flush()
+}
+
+func (r *LightRenderer) Refresh() {
+ r.updateTerminalSize()
+}
+
+func (r *LightRenderer) Close() {
+ r.csi("u")
+ r.csi("J")
+ if r.mouse {
+ r.csi("?1000l")
+ }
+ if r.upOneLine {
+ r.csi("A")
+ }
+ r.flush()
+ stty(fmt.Sprintf("%q", r.ostty))
+}
+
+func (r *LightRenderer) MaxX() int {
+ return r.width
+}
+
+func (r *LightRenderer) MaxY() int {
+ return r.height
+}
+
+func (r *LightRenderer) DoesAutoWrap() bool {
+ return true
+}
+
+func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
+ w := &LightWindow{
+ renderer: r,
+ colored: r.theme != nil,
+ border: border,
+ top: top,
+ left: left,
+ width: width,
+ height: height,
+ tabstop: r.tabstop,
+ bg: colDefault}
+ if r.theme != nil {
+ w.bg = r.theme.Bg
+ }
+ if w.border {
+ w.drawBorder()
+ }
+ return w
+}
+
+func (w *LightWindow) drawBorder() {
+ w.Move(0, 0)
+ w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐")
+ for y := 1; y < w.height-1; y++ {
+ w.Move(y, 0)
+ w.CPrint(ColBorder, AttrRegular, "│")
+ w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2))
+ w.CPrint(ColBorder, AttrRegular, "│")
+ }
+ w.Move(w.height-1, 0)
+ w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘")
+}
+
+func (w *LightWindow) csi(code string) {
+ w.renderer.csi(code)
+}
+
+func (w *LightWindow) stderr(str string) {
+ w.renderer.stderr(str)
+}
+
+func (w *LightWindow) Top() int {
+ return w.top
+}
+
+func (w *LightWindow) Left() int {
+ return w.left
+}
+
+func (w *LightWindow) Width() int {
+ return w.width
+}
+
+func (w *LightWindow) Height() int {
+ return w.height
+}
+
+func (w *LightWindow) Refresh() {
+}
+
+func (w *LightWindow) Close() {
+}
+
+func (w *LightWindow) X() int {
+ return w.posx
+}
+
+func (w *LightWindow) Enclose(y int, x int) bool {
+ return x >= w.left && x < (w.left+w.width) &&
+ y >= w.top && y < (w.top+w.height)
+}
+
+func (w *LightWindow) Move(y int, x int) {
+ w.posx = x
+ w.posy = y
+
+ w.csi("u")
+ y += w.Top()
+ if y > 0 {
+ w.csi(fmt.Sprintf("%dB", y))
+ }
+ x += w.Left()
+ if x > 0 {
+ w.csi(fmt.Sprintf("%dC", x))
+ }
+}
+
+func (w *LightWindow) MoveAndClear(y int, x int) {
+ w.Move(y, x)
+ // We should not delete preview window on the right
+ // csi("K")
+ w.Print(repeat(" ", w.width-x))
+ w.Move(y, x)
+}
+
+func attrCodes(attr Attr) []string {
+ codes := []string{}
+ if (attr & Bold) > 0 {
+ codes = append(codes, "1")
+ }
+ if (attr & Dim) > 0 {
+ codes = append(codes, "2")
+ }
+ if (attr & Italic) > 0 {
+ codes = append(codes, "3")
+ }
+ if (attr & Underline) > 0 {
+ codes = append(codes, "4")
+ }
+ if (attr & Blink) > 0 {
+ codes = append(codes, "5")
+ }
+ if (attr & Reverse) > 0 {
+ codes = append(codes, "7")
+ }
+ return codes
+}
+
+func colorCodes(fg Color, bg Color) []string {
+ codes := []string{}
+ appendCode := func(c Color, offset int) {
+ if c == colDefault {
+ return
+ }
+ if c.is24() {
+ r := (c >> 16) & 0xff
+ g := (c >> 8) & 0xff
+ b := (c) & 0xff
+ codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b))
+ } else if c >= colBlack && c <= colWhite {
+ codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset))
+ } else if c > colWhite && c < 16 {
+ codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8))
+ } else if c >= 16 && c < 256 {
+ codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c))
+ }
+ }
+ appendCode(fg, 0)
+ appendCode(bg, 10)
+ return codes
+}
+
+func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
+ codes := append(attrCodes(attr), colorCodes(fg, bg)...)
+ w.csi(";" + strings.Join(codes, ";") + "m")
+ return len(codes) > 0
+}
+
+func (w *LightWindow) Print(text string) {
+ w.cprint2(colDefault, w.bg, AttrRegular, text)
+}
+
+func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) {
+ if !w.colored {
+ w.csiColor(colDefault, colDefault, attrFor(pair, attr))
+ } else {
+ w.csiColor(pair.Fg(), pair.Bg(), attr)
+ }
+ w.stderr(text)
+ w.csi("m")
+}
+
+func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
+ if w.csiColor(fg, bg, attr) {
+ defer w.csi("m")
+ }
+ w.stderr(text)
+}
+
+type wrappedLine struct {
+ text string
+ displayWidth int
+}
+
+func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
+ lines := []wrappedLine{}
+ width := 0
+ line := ""
+ for _, r := range input {
+ w := util.Max(util.RuneWidth(r, prefixLength+width, 8), 1)
+ width += w
+ str := string(r)
+ if r == '\t' {
+ str = repeat(" ", w)
+ }
+ if prefixLength+width <= max {
+ line += str
+ } else {
+ lines = append(lines, wrappedLine{string(line), width - w})
+ line = str
+ prefixLength = 0
+ width = util.RuneWidth(r, prefixLength, 8)
+ }
+ }
+ lines = append(lines, wrappedLine{string(line), width})
+ return lines
+}
+
+func (w *LightWindow) fill(str string, onMove func()) bool {
+ allLines := strings.Split(str, "\n")
+ for i, line := range allLines {
+ lines := wrapLine(line, w.posx, w.width, w.tabstop)
+ for j, wl := range lines {
+ w.stderr(wl.text)
+ w.posx += wl.displayWidth
+ if j < len(lines)-1 || i < len(allLines)-1 {
+ if w.posy+1 >= w.height {
+ return false
+ }
+ w.MoveAndClear(w.posy+1, 0)
+ onMove()
+ }
+ }
+ }
+ return true
+}
+
+func (w *LightWindow) setBg() {
+ if w.bg != colDefault {
+ w.csiColor(colDefault, w.bg, AttrRegular)
+ }
+}
+
+func (w *LightWindow) Fill(text string) bool {
+ w.MoveAndClear(w.posy, w.posx)
+ w.setBg()
+ return w.fill(text, w.setBg)
+}
+
+func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) bool {
+ w.MoveAndClear(w.posy, w.posx)
+ if bg == colDefault {
+ bg = w.bg
+ }
+ if w.csiColor(fg, bg, attr) {
+ return w.fill(text, func() { w.csiColor(fg, bg, attr) })
+ defer w.csi("m")
+ }
+ return w.fill(text, w.setBg)
+}
+
+func (w *LightWindow) FinishFill() {
+ for y := w.posy + 1; y < w.height; y++ {
+ w.MoveAndClear(y, 0)
+ }
+}
+
+func (w *LightWindow) Erase() {
+ if w.border {
+ w.drawBorder()
+ }
+ // We don't erase the window here to avoid flickering during scroll
+ w.Move(0, 0)
+}
diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go
index 7f515b2f..b1606920 100644
--- a/src/tui/ncurses.go
+++ b/src/tui/ncurses.go
@@ -33,9 +33,39 @@ import (
"unicode/utf8"
)
-type ColorPair int16
type Attr C.uint
-type WindowImpl C.WINDOW
+
+type CursesWindow struct {
+ impl *C.WINDOW
+ top int
+ left int
+ width int
+ height int
+}
+
+func (w *CursesWindow) Top() int {
+ return w.top
+}
+
+func (w *CursesWindow) Left() int {
+ return w.left
+}
+
+func (w *CursesWindow) Width() int {
+ return w.width
+}
+
+func (w *CursesWindow) Height() int {
+ return w.height
+}
+
+func (w *CursesWindow) Refresh() {
+ C.wnoutrefresh(w.impl)
+}
+
+func (w *CursesWindow) FinishFill() {
+ // NO-OP
+}
const (
Bold Attr = C.A_BOLD
@@ -51,31 +81,14 @@ const (
AttrRegular Attr = 0
)
-// Pallete
-const (
- ColDefault ColorPair = iota
- ColNormal
- ColPrompt
- ColMatch
- ColCurrent
- ColCurrentMatch
- ColSpinner
- ColInfo
- ColCursor
- ColSelected
- ColHeader
- ColBorder
- ColUser // Should be the last entry
-)
-
var (
_screen *C.SCREEN
- _colorMap map[int]ColorPair
+ _colorMap map[int]int16
_colorFn func(ColorPair, Attr) (C.short, C.int)
)
func init() {
- _colorMap = make(map[int]ColorPair)
+ _colorMap = make(map[int]int16)
if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") {
Italic = C.A_NORMAL
}
@@ -85,14 +98,14 @@ func (a Attr) Merge(b Attr) Attr {
return a | b
}
-func DefaultTheme() *ColorTheme {
+func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if C.tigetnum(C.CString("colors")) >= 256 {
return Dark256
}
return Default16
}
-func Init(theme *ColorTheme, black bool, mouse bool) {
+func (r *FullscreenRenderer) Init() {
C.setlocale(C.LC_ALL, C.CString(""))
tty := C.c_tty()
if tty == nil {
@@ -105,7 +118,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
os.Exit(2)
}
C.set_term(_screen)
- if mouse {
+ if r.mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
C.mouseinterval(0)
}
@@ -124,14 +137,14 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
}
C.set_escdelay(C.int(delay))
- _color = theme != nil
- if _color {
+ if r.theme != nil {
C.start_color()
- InitTheme(theme, black)
- initPairs(theme)
- C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal))))
+ initTheme(r.theme, r.defaultTheme(), r.forceBlack)
+ initPairs(r.theme)
+ C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
_colorFn = attrColored
} else {
+ initTheme(r.theme, nil, r.forceBlack)
_colorFn = attrMono
}
@@ -145,39 +158,39 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
func initPairs(theme *ColorTheme) {
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
- initPair := func(group ColorPair, fg Color, bg Color) {
- C.init_pair(C.short(group), C.short(fg), C.short(bg))
+ for _, pair := range []ColorPair{
+ ColNormal,
+ ColPrompt,
+ ColMatch,
+ ColCurrent,
+ ColCurrentMatch,
+ ColSpinner,
+ ColInfo,
+ ColCursor,
+ ColSelected,
+ ColHeader,
+ ColBorder} {
+ C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg()))
}
- initPair(ColNormal, theme.Fg, theme.Bg)
- initPair(ColPrompt, theme.Prompt, theme.Bg)
- initPair(ColMatch, theme.Match, theme.Bg)
- initPair(ColCurrent, theme.Current, theme.DarkBg)
- initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg)
- initPair(ColSpinner, theme.Spinner, theme.Bg)
- initPair(ColInfo, theme.Info, theme.Bg)
- initPair(ColCursor, theme.Cursor, theme.DarkBg)
- initPair(ColSelected, theme.Selected, theme.DarkBg)
- initPair(ColHeader, theme.Header, theme.Bg)
- initPair(ColBorder, theme.Border, theme.Bg)
-}
-
-func Pause() {
+}
+
+func (r *FullscreenRenderer) Pause() {
C.endwin()
}
-func Resume() bool {
+func (r *FullscreenRenderer) Resume() bool {
return false
}
-func Close() {
+func (r *FullscreenRenderer) Close() {
C.endwin()
C.delscreen(_screen)
}
-func NewWindow(top int, left int, width int, height int, border bool) *Window {
+func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
- if _color {
- C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal))))
+ if r.theme != nil {
+ C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
}
if border {
pair, attr := _colorFn(ColBorder, 0)
@@ -188,66 +201,50 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window {
C.wcolor_set(win, 0, nil)
}
- return &Window{
- impl: (*WindowImpl)(win),
- Top: top,
- Left: left,
- Width: width,
- Height: height,
+ return &CursesWindow{
+ impl: win,
+ top: top,
+ left: left,
+ width: width,
+ height: height,
}
}
-func attrColored(pair ColorPair, a Attr) (C.short, C.int) {
- return C.short(pair), C.int(a)
+func attrColored(color ColorPair, a Attr) (C.short, C.int) {
+ return C.short(color.index()), C.int(a)
}
-func attrMono(pair ColorPair, a Attr) (C.short, C.int) {
- var attr C.int
- switch pair {
- case ColCurrent:
- attr = C.A_REVERSE
- case ColMatch:
- attr = C.A_UNDERLINE
- case ColCurrentMatch:
- attr = C.A_UNDERLINE | C.A_REVERSE
- }
- if C.int(a)&C.A_BOLD == C.A_BOLD {
- attr = attr | C.A_BOLD
- }
- return 0, attr
+func attrMono(color ColorPair, a Attr) (C.short, C.int) {
+ return 0, C.int(attrFor(color, a))
}
-func MaxX() int {
+func (r *FullscreenRenderer) MaxX() int {
return int(C.COLS)
}
-func MaxY() int {
+func (r *FullscreenRenderer) MaxY() int {
return int(C.LINES)
}
-func (w *Window) win() *C.WINDOW {
- return (*C.WINDOW)(w.impl)
-}
-
-func (w *Window) Close() {
- C.delwin(w.win())
+func (w *CursesWindow) Close() {
+ C.delwin(w.impl)
}
-func (w *Window) Enclose(y int, x int) bool {
- return bool(C.wenclose(w.win(), C.int(y), C.int(x)))
+func (w *CursesWindow) Enclose(y int, x int) bool {
+ return bool(C.wenclose(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 *CursesWindow) Move(y int, x int) {
+ C.wmove(w.impl, C.int(y), C.int(x))
}
-func (w *Window) MoveAndClear(y int, x int) {
+func (w *CursesWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
- C.wclrtoeol(w.win())
+ C.wclrtoeol(w.impl)
}
-func (w *Window) Print(text string) {
- C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune {
+func (w *CursesWindow) Print(text string) {
+ C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
@@ -255,69 +252,74 @@ func (w *Window) Print(text string) {
}, text)))
}
-func (w *Window) CPrint(pair ColorPair, attr Attr, text string) {
- p, a := _colorFn(pair, attr)
- C.wcolor_set(w.win(), p, nil)
- C.wattron(w.win(), a)
+func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) {
+ p, a := _colorFn(color, attr)
+ C.wcolor_set(w.impl, p, nil)
+ C.wattron(w.impl, a)
w.Print(text)
- C.wattroff(w.win(), a)
- C.wcolor_set(w.win(), 0, nil)
+ C.wattroff(w.impl, a)
+ C.wcolor_set(w.impl, 0, nil)
}
-func Clear() {
+func (r *FullscreenRenderer) Clear() {
C.clear()
C.endwin()
}
-func Refresh() {
+func (r *FullscreenRenderer) Refresh() {
C.refresh()
}
-func (w *Window) Erase() {
- C.werase(w.win())
+func (w *CursesWindow) Erase() {
+ C.werase(w.impl)
}
-func (w *Window) X() int {
- return int(C.c_getcurx(w.win()))
+func (w *CursesWindow) X() int {
+ return int(C.c_getcurx(w.impl))
}
-func DoesAutoWrap() bool {
+func (r *FullscreenRenderer) DoesAutoWrap() bool {
return true
}
-func (w *Window) Fill(str string) bool {
- return C.waddstr(w.win(), C.CString(str)) == C.OK
+func (w *CursesWindow) Fill(str string) bool {
+ return C.waddstr(w.impl, C.CString(str)) == C.OK
}
-func (w *Window) CFill(str string, fg Color, bg Color, attr Attr) bool {
- pair := PairFor(fg, bg)
- C.wcolor_set(w.win(), C.short(pair), nil)
- C.wattron(w.win(), C.int(attr))
+func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) bool {
+ index := ColorPair{fg, bg, -1}.index()
+ C.wcolor_set(w.impl, C.short(index), nil)
+ C.wattron(w.impl, C.int(attr))
ret := w.Fill(str)
- C.wattroff(w.win(), C.int(attr))
- C.wcolor_set(w.win(), 0, nil)
+ C.wattroff(w.impl, C.int(attr))
+ C.wcolor_set(w.impl, 0, nil)
return ret
}
-func RefreshWindows(windows []*Window) {
+func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
for _, w := range windows {
- C.wnoutrefresh(w.win())
+ w.Refresh()
}
C.doupdate()
}
-func PairFor(fg Color, bg Color) ColorPair {
+func (p ColorPair) index() int16 {
+ if p.id >= 0 {
+ return p.id
+ }
+
// ncurses does not support 24-bit colors
- if fg.is24() || bg.is24() {
- return ColDefault
+ if p.is24() {
+ return ColDefault.index()
}
- key := (int(fg) << 8) + int(bg)
+
+ key := p.key()
if found, prs := _colorMap[key]; prs {
return found
}
- id := ColorPair(len(_colorMap) + int(ColUser))
- C.init_pair(C.short(id), C.short(fg), C.short(bg))
+ id := int16(len(_colorMap)) + ColUser.id
+ C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg()))
_colorMap[key] = id
return id
}
@@ -369,7 +371,7 @@ func escSequence() Event {
return Event{Invalid, 0, nil}
}
-func GetChar() Event {
+func (r *FullscreenRenderer) GetChar() Event {
c := C.getch()
switch c {
case C.ERR:
@@ -435,17 +437,17 @@ func GetChar() Event {
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
if (me.bstate & C.BUTTON1_PRESSED) > 0 {
now := time.Now()
- if now.Sub(_prevDownTime) < doubleClickDuration {
- _clickY = append(_clickY, y)
+ if now.Sub(r.prevDownTime) < doubleClickDuration {
+ r.clickY = append(r.clickY, y)
} else {
- _clickY = []int{y}
- _prevDownTime = now
+ r.clickY = []int{y}
+ r.prevDownTime = now
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
} else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
double := false
- if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
- time.Now().Sub(_prevDownTime) < doubleClickDuration {
+ if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
+ time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}}
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 1793836a..460bfd5b 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -18,30 +18,56 @@ import (
"github.com/junegunn/go-runewidth"
)
-type ColorPair [2]Color
+func (p ColorPair) style() tcell.Style {
+ style := tcell.StyleDefault
+ return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
+}
-func (p ColorPair) fg() Color {
- return p[0]
+type Attr tcell.Style
+
+type TcellWindow struct {
+ color bool
+ top int
+ left int
+ width int
+ height int
+ lastX int
+ lastY int
+ moveCursor bool
+ border bool
}
-func (p ColorPair) bg() Color {
- return p[1]
+func (w *TcellWindow) Top() int {
+ return w.top
}
-func (p ColorPair) style() tcell.Style {
- style := tcell.StyleDefault
- return style.Foreground(tcell.Color(p.fg())).Background(tcell.Color(p.bg()))
+func (w *TcellWindow) Left() int {
+ return w.left
}
-type Attr tcell.Style
+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()
+ }
+}
-type WindowTcell struct {
- LastX int
- LastY int
- MoveCursor bool
- Border bool
+func (w *TcellWindow) FinishFill() {
+ // NO-OP
}
-type WindowImpl WindowTcell
const (
Bold Attr = Attr(tcell.AttrBold)
@@ -56,33 +82,13 @@ const (
AttrRegular Attr = 0
)
-var (
- ColDefault = ColorPair{colDefault, colDefault}
- ColNormal ColorPair
- ColPrompt ColorPair
- ColMatch ColorPair
- ColCurrent ColorPair
- ColCurrentMatch ColorPair
- ColSpinner ColorPair
- ColInfo ColorPair
- ColCursor ColorPair
- ColSelected ColorPair
- ColHeader ColorPair
- ColBorder ColorPair
- ColUser ColorPair
-)
-
-func DefaultTheme() *ColorTheme {
+func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if _screen.Colors() >= 256 {
return Dark256
}
return Default16
}
-func PairFor(fg Color, bg Color) ColorPair {
- return [2]Color{fg, bg}
-}
-
var (
_colorToAttribute = []tcell.Color{
tcell.ColorBlack,
@@ -112,10 +118,9 @@ func (a Attr) Merge(b Attr) Attr {
var (
_screen tcell.Screen
- _mouse bool
)
-func initScreen() {
+func (r *FullscreenRenderer) initScreen() {
s, e := tcell.NewScreen()
if e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
@@ -125,7 +130,7 @@ func initScreen() {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(2)
}
- if _mouse {
+ if r.mouse {
s.EnableMouse()
} else {
s.DisableMouse()
@@ -133,63 +138,41 @@ func initScreen() {
_screen = s
}
-func Init(theme *ColorTheme, black bool, mouse bool) {
+func (r *FullscreenRenderer) Init() {
encoding.Register()
- _mouse = mouse
- initScreen()
+ r.initScreen()
+ initTheme(r.theme, r.defaultTheme(), r.forceBlack)
+}
- _color = theme != nil
- if _color {
- InitTheme(theme, black)
- } else {
- theme = DefaultTheme()
- }
- ColNormal = ColorPair{theme.Fg, theme.Bg}
- ColPrompt = ColorPair{theme.Prompt, theme.Bg}
- ColMatch = ColorPair{theme.Match, theme.Bg}
- ColCurrent = ColorPair{theme.Current, theme.DarkBg}
- ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg}
- ColSpinner = ColorPair{theme.Spinner, theme.Bg}
- ColInfo = ColorPair{theme.Info, theme.Bg}
- ColCursor = ColorPair{theme.Cursor, theme.DarkBg}
- ColSelected = ColorPair{theme.Selected, theme.DarkBg}
- ColHeader = ColorPair{theme.Header, theme.Bg}
- ColBorder = ColorPair{theme.Border, theme.Bg}
-}
-
-func MaxX() int {
+func (r *FullscreenRenderer) MaxX() int {
ncols, _ := _screen.Size()
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 *Window) X() int {
- return w.impl.LastX
+func (w *TcellWindow) X() int {
+ return w.lastX
}
-func DoesAutoWrap() bool {
+func (r *FullscreenRenderer) DoesAutoWrap() bool {
return false
}
-func Clear() {
+func (r *FullscreenRenderer) Clear() {
_screen.Sync()
_screen.Clear()
}
-func Refresh() {
+func (r *FullscreenRenderer) Refresh() {
// noop
}
-func GetChar() Event {
+func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
@@ -213,15 +196,15 @@ func GetChar() Event {
double := false
if down {
now := time.Now()
- if now.Sub(_prevDownTime) < doubleClickDuration {
- _clickY = append(_clickY, x)
+ if now.Sub(r.prevDownTime) < doubleClickDuration {
+ r.clickY = append(r.clickY, x)
} else {
- _clickY = []int{x}
- _prevDownTime = now
+ r.clickY = []int{x}
+ r.prevDownTime = now
}
} else {
- if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
- time.Now().Sub(_prevDownTime) < doubleClickDuration {
+ if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
+ time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
}
@@ -368,49 +351,39 @@ func GetChar() Event {
return Event{Invalid, 0, nil}
}
-func Pause() {
+func (r *FullscreenRenderer) Pause() {
_screen.Fini()
}
-func Resume() bool {
- initScreen()
+func (r *FullscreenRenderer) Resume() bool {
+ r.initScreen()
return true
}
-func Close() {
+func (r *FullscreenRenderer) Close() {
_screen.Fini()
}
-func RefreshWindows(windows []*Window) {
+func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
// TODO
for _, w := range windows {
- if w.win().MoveCursor {
- _screen.ShowCursor(w.Left+w.win().LastX, w.Top+w.win().LastY)
- w.win().MoveCursor = false
- }
- w.win().LastX = 0
- w.win().LastY = 0
- if w.win().Border {
- w.DrawBorder()
- }
+ w.Refresh()
}
_screen.Show()
}
-func NewWindow(top int, left int, width int, height int, border bool) *Window {
+func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
// TODO
- win := new(WindowTcell)
- win.Border = border
- return &Window{
- impl: (*WindowImpl)(win),
- Top: top,
- Left: left,
- Width: width,
- Height: height,
- }
+ return &TcellWindow{
+ color: r.theme != nil,
+ top: top,
+ left: left,
+ width: width,
+ height: height,
+ border: border}
}
-func (w *Window) Close() {
+func (w *TcellWindow) Close() {
// TODO
}
@@ -422,40 +395,40 @@ func fill(x, y, w, h int, r rune) {
}
}
-func (w *Window) Erase() {
+func (w *TcellWindow) Erase() {
// TODO
- fill(w.Left, w.Top, w.Width, w.Height, ' ')
+ fill(w.left, w.top, w.width, w.height, ' ')
}
-func (w *Window) Enclose(y int, x int) bool {
- return x >= w.Left && x <= (w.Left+w.Width) &&
- y >= w.Top && y <= (w.Top+w.Height)
+func (w *TcellWindow) Enclose(y int, x int) bool {
+ return x >= w.left && x < (w.left+w.width) &&
+ y >= w.top && y < (w.top+w.height)
}
-func (w *Window) Move(y int, x int) {
- w.win().LastX = x
- w.win().LastY = y
- w.win().MoveCursor = true
+func (w *TcellWindow) Move(y int, x int) {
+ w.lastX = x
+ w.lastY = y
+ w.moveCursor = true
}
-func (w *Window) MoveAndClear(y int, x int) {
+func (w *TcellWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
- for i := w.win().LastX; i < w.Width; i++ {
- _screen.SetContent(i+w.Left, w.win().LastY+w.Top, rune(' '), nil, ColDefault.style())
+ for i := w.lastX; i < w.width; i++ {
+ _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style())
}
- w.win().LastX = x
+ w.lastX = x
}
-func (w *Window) Print(text string) {
- w.PrintString(text, ColDefault, 0)
+func (w *TcellWindow) Print(text string) {
+ w.printString(text, ColDefault, 0)
}
-func (w *Window) PrintString(text string, pair ColorPair, a Attr) {
+func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
t := text
lx := 0
var style tcell.Style
- if _color {
+ if w.color {
style = pair.style().
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
@@ -481,7 +454,7 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) {
}
if r == '\n' {
- w.win().LastY++
+ w.lastY++
lx = 0
} else {
@@ -489,26 +462,26 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) {
continue
}
- var xPos = w.Left + w.win().LastX + lx
- var yPos = w.Top + w.win().LastY
- if xPos < (w.Left+w.Width) && yPos < (w.Top+w.Height) {
+ var xPos = w.left + w.lastX + lx
+ var yPos = w.top + w.lastY
+ if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, r, nil, style)
}
lx += runewidth.RuneWidth(r)
}
}
- w.win().LastX += lx
+ w.lastX += lx
}
-func (w *Window) CPrint(pair ColorPair, a Attr, text string) {
- w.PrintString(text, pair, a)
+func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) {
+ w.printString(text, pair, attr)
}
-func (w *Window) FillString(text string, pair ColorPair, a Attr) bool {
+func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool {
lx := 0
var style tcell.Style
- if _color {
+ if w.color {
style = pair.style()
} else {
style = ColDefault.style()
@@ -522,22 +495,22 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool {
for _, r := range text {
if r == '\n' {
- w.win().LastY++
- w.win().LastX = 0
+ w.lastY++
+ w.lastX = 0
lx = 0
} else {
- var xPos = w.Left + w.win().LastX + lx
+ var xPos = w.left + w.lastX + lx
// word wrap:
- if xPos >= (w.Left + w.Width) {
- w.win().LastY++
- w.win().LastX = 0
+ if xPos >= (w.left + w.width) {
+ w.lastY++
+ w.lastX = 0
lx = 0
- xPos = w.Left
+ xPos = w.left
}
- var yPos = w.Top + w.win().LastY
+ var yPos = w.top + w.lastY
- if yPos >= (w.Top + w.Height) {
+ if yPos >= (w.top + w.height) {
return false
}
@@ -545,27 +518,27 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool {
lx += runewidth.RuneWidth(r)
}
}
- w.win().LastX += lx
+ w.lastX += lx
return true
}
-func (w *Window) Fill(str string) bool {
- return w.FillString(str, ColDefault, 0)
+func (w *TcellWindow) Fill(str string) bool {
+ return w.fillString(str, ColDefault, 0)
}
-func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool {
- return w.FillString(str, ColorPair{fg, bg}, a)
+func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) bool {
+ return w.fillString(str, ColorPair{fg, bg, -1}, a)
}
-func (w *Window) DrawBorder() {
- left := w.Left
- right := left + w.Width
- top := w.Top
- bot := top + w.Height
+func (w *TcellWindow) drawBorder() {
+ left := w.left
+ right := left + w.width
+ top := w.top
+ bot := top + w.height
var style tcell.Style
- if _color {
+ if w.color {
style = ColBorder.style()
} else {
style = ColDefault.style()
diff --git a/src/tui/tui.go b/src/tui/tui.go
index 125611cf..859eed7a 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -115,6 +115,32 @@ const (
colWhite
)
+type ColorPair struct {
+ fg Color
+ bg Color
+ id int16
+}
+
+func NewColorPair(fg Color, bg Color) ColorPair {
+ return ColorPair{fg, bg, -1}
+}
+
+func (p ColorPair) Fg() Color {
+ return p.fg
+}
+
+func (p ColorPair) Bg() Color {
+ return p.bg
+}
+
+func (p ColorPair) key() int {
+ return (int(p.Fg()) << 8) + int(p.Bg())
+}
+
+func (p ColorPair) is24() bool {
+ return p.Fg().is24() || p.Bg().is24()
+}
+
type ColorTheme struct {
Fg Color
Bg Color
@@ -146,23 +172,84 @@ type MouseEvent struct {
Mod bool
}
-var (
- _color bool
- _prevDownTime time.Time
- _clickY []int
- Default16 *ColorTheme
- Dark256 *ColorTheme
- Light256 *ColorTheme
-)
+type Renderer interface {
+ Init()
+ Pause()
+ Resume() bool
+ Clear()
+ RefreshWindows(windows []Window)
+ Refresh()
+ Close()
+
+ GetChar() Event
+
+ MaxX() int
+ MaxY() int
+ DoesAutoWrap() bool
+
+ NewWindow(top int, left int, width int, height int, border bool) Window
+}
-type Window struct {
- impl *WindowImpl
- Top int
- Left int
- Width int
- Height int
+type Window interface {
+ Top() int
+ Left() int
+ Width() int
+ Height() int
+
+ Refresh()
+ FinishFill()
+ Close()
+
+ X() int
+ Enclose(y int, x int) bool
+
+ Move(y int, x int)
+ MoveAndClear(y int, x int)
+ Print(text string)
+ CPrint(color ColorPair, attr Attr, text string)
+ Fill(text string) bool
+ CFill(fg Color, bg Color, attr Attr, text string) bool
+ Erase()
+}
+
+type FullscreenRenderer struct {
+ theme *ColorTheme
+ mouse bool
+ forceBlack bool
+ prevDownTime time.Time
+ clickY []int
}
+func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
+ r := &FullscreenRenderer{
+ theme: theme,
+ mouse: mouse,
+ forceBlack: forceBlack,
+ prevDownTime: time.Unix(0, 0),
+ clickY: []int{}}
+ return r
+}
+
+var (
+ Default16 *ColorTheme
+ Dark256 *ColorTheme
+ Light256 *ColorTheme
+
+ ColDefault ColorPair
+ ColNormal ColorPair
+ ColPrompt ColorPair
+ ColMatch ColorPair
+ ColCurrent ColorPair
+ ColCurrentMatch ColorPair
+ ColSpinner ColorPair
+ ColInfo ColorPair
+ ColCursor ColorPair
+ ColSelected ColorPair
+ ColHeader ColorPair
+ ColBorder ColorPair
+ ColUser ColorPair
+)
+
func EmptyTheme() *ColorTheme {
return &ColorTheme{
Fg: colUndefined,
@@ -181,8 +268,6 @@ func EmptyTheme() *ColorTheme {
}
func init() {
- _prevDownTime = time.Unix(0, 0)
- _clickY = []int{}
Default16 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
@@ -227,14 +312,13 @@ func init() {
Border: 145}
}
-func InitTheme(theme *ColorTheme, black bool) {
- _color = theme != nil
- if !_color {
+func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
+ if theme == nil {
+ initPalette(theme)
return
}
- baseTheme := DefaultTheme()
- if black {
+ if forceBlack {
theme.Bg = colBlack
}
@@ -257,4 +341,48 @@ func InitTheme(theme *ColorTheme, black bool) {
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
+
+ initPalette(theme)
+}
+
+func initPalette(theme *ColorTheme) {
+ ColDefault = ColorPair{colDefault, colDefault, 0}
+ if theme != nil {
+ ColNormal = ColorPair{theme.Fg, theme.Bg, 1}
+ ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2}
+ ColMatch = ColorPair{theme.Match, theme.Bg, 3}
+ ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4}
+ ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5}
+ ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6}
+ ColInfo = ColorPair{theme.Info, theme.Bg, 7}
+ ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8}
+ ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9}
+ ColHeader = ColorPair{theme.Header, theme.Bg, 10}
+ ColBorder = ColorPair{theme.Border, theme.Bg, 11}
+ } else {
+ ColNormal = ColorPair{colDefault, colDefault, 1}
+ ColPrompt = ColorPair{colDefault, colDefault, 2}
+ ColMatch = ColorPair{colDefault, colDefault, 3}
+ ColCurrent = ColorPair{colDefault, colDefault, 4}
+ ColCurrentMatch = ColorPair{colDefault, colDefault, 5}
+ ColSpinner = ColorPair{colDefault, colDefault, 6}
+ ColInfo = ColorPair{colDefault, colDefault, 7}
+ ColCursor = ColorPair{colDefault, colDefault, 8}
+ ColSelected = ColorPair{colDefault, colDefault, 9}
+ ColHeader = ColorPair{colDefault, colDefault, 10}
+ ColBorder = ColorPair{colDefault, colDefault, 11}
+ }
+ ColUser = ColorPair{colDefault, colDefault, 12}
+}
+
+func attrFor(color ColorPair, attr Attr) Attr {
+ switch color {
+ case ColCurrent:
+ return attr | Reverse
+ case ColMatch:
+ return attr | Underline
+ case ColCurrentMatch:
+ return attr | Underline | Reverse
+ }
+ return attr
}
diff --git a/src/tui/tui_test.go b/src/tui/tui_test.go
deleted file mode 100644
index 4a2fee91..00000000
--- a/src/tui/tui_test.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package tui
-
-import (
- "testing"
-)
-
-func TestPairFor(t *testing.T) {
- if PairFor(30, 50) != PairFor(30, 50) {
- t.Fail()
- }
- if PairFor(-1, 10) != PairFor(-1, 10) {
- t.Fail()
- }
-}
diff --git a/src/util/util.go b/src/util/util.go
index 2a1607ce..29e80176 100644
--- a/src/util/util.go
+++ b/src/util/util.go
@@ -6,8 +6,24 @@ import (
"time"
"github.com/junegunn/go-isatty"
+ "github.com/junegunn/go-runewidth"
)
+var _runeWidths = make(map[rune]int)
+
+// RuneWidth returns rune width
+func RuneWidth(r rune, prefixWidth int, tabstop int) int {
+ if r == '\t' {
+ return tabstop - prefixWidth%tabstop
+ } else if w, found := _runeWidths[r]; found {
+ return w
+ } else {
+ w := runewidth.RuneWidth(r)
+ _runeWidths[r] = w
+ return w
+ }
+}
+
// Max returns the largest integer
func Max(first int, second int) int {
if first >= second {
diff --git a/src/util/util_unix.go b/src/util/util_unix.go
index 29e0d30d..bc1b7b52 100644
--- a/src/util/util_unix.go
+++ b/src/util/util_unix.go
@@ -5,6 +5,7 @@ package util
import (
"os"
"os/exec"
+ "syscall"
)
// ExecCommand executes the given command with $SHELL
@@ -20,3 +21,8 @@ func ExecCommand(command string) *exec.Cmd {
func IsWindows() bool {
return false
}
+
+// SetNonBlock executes syscall.SetNonblock on file descriptor
+func SetNonblock(file *os.File, nonblock bool) {
+ syscall.SetNonblock(int(file.Fd()), nonblock)
+}
diff --git a/src/util/util_windows.go b/src/util/util_windows.go
index 3aa86606..9ba4f79e 100644
--- a/src/util/util_windows.go
+++ b/src/util/util_windows.go
@@ -5,6 +5,7 @@ package util
import (
"os"
"os/exec"
+ "syscall"
"github.com/junegunn/go-shellwords"
)
@@ -26,3 +27,8 @@ func ExecCommand(command string) *exec.Cmd {
func IsWindows() bool {
return true
}
+
+// SetNonBlock executes syscall.SetNonblock on file descriptor
+func SetNonblock(file *os.File, nonblock bool) {
+ syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
+}
diff --git a/test/test_go.rb b/test/test_go.rb
index 4cd4a4f7..55422aa0 100644
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -117,8 +117,28 @@ class Tmux
wait do
lines = capture(pane)
class << lines
+ def counts
+ self.lazy
+ .map { |l| l.scan /^. ([0-9]+)\/([0-9]+)( \(([0-9]+)\))?/ }
+ .reject(&:empty?)
+ .first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
+ end
+
+ def match_count
+ counts[0]
+ end
+
def item_count
- self[-2] ? self[-2].strip.split('/').last.to_i : 0
+ counts[1]
+ end
+
+ def select_count
+ counts[2]
+ end
+
+ def any_include? val
+ method = val.is_a?(Regexp) ? :match : :include?
+ self.select { |line| line.send method, val }.first
end
end
yield lines
@@ -163,6 +183,11 @@ class TestBase < Minitest::Test
@temp_suffix].join '-'
end
+ def writelines path, lines
+ File.unlink path while File.exists? path
+ File.open(path, 'w') { |f| f << lines.join($/) + $/ }
+ end
+
def readonce
wait { File.exists?(tempname) }
File.read(tempname)
@@ -1190,12 +1215,6 @@ class TestGoFZF < TestBase
tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' }
end
-
-private
- def writelines path, lines
- File.unlink path while File.exists? path
- File.open(path, 'w') { |f| f << lines.join($/) + $/ }
- end
end
module TestShell
@@ -1213,79 +1232,60 @@ module TestShell
tmux.prepare
end
- def test_ctrl_t
+ def unset_var name
+ tmux.prepare
+ tmux.send_keys "unset #{name}", :Enter
tmux.prepare
- tmux.send_keys 'C-t', pane: 0
- lines = tmux.until(1) { |lines| lines.item_count > 1 }
- expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
- tmux.send_keys :BTab, :BTab, pane: 1
- tmux.until(1) { |lines| lines[-2].include?('(2)') }
- tmux.send_keys :Enter, pane: 1
- tmux.until(0) { |lines| lines[-1].include? expected }
- tmux.send_keys 'C-c'
-
- # FZF_TMUX=0
- new_shell
- tmux.send_keys 'C-t', pane: 0
- lines = tmux.until(0) { |lines| lines.item_count > 1 }
- expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
- tmux.send_keys :BTab, :BTab, pane: 0
- tmux.until(0) { |lines| lines[-2].include?('(2)') }
- tmux.send_keys :Enter, pane: 0
- tmux.until(0) { |lines| lines[-1].include? expected }
- tmux.send_keys 'C-c', 'C-d'
end
- def test_ctrl_t_command
+ def test_ctrl_t
set_var "FZF_CTRL_T_COMMAND", "seq 100"
- tmux.send_keys 'C-t', pane: 0
- lines = tmux.until(1) { |lines| lines.item_count == 100 }
- tmux.send_keys :BTab, :BTab, :BTab, pane: 1
- tmux.until(1) { |lines| lines[-2].include?('(3)') }
- tmux.send_keys :Enter, pane: 1
- tmux.until(0) { |lines| lines[-1].include? '1 2 3' }
+
+ tmux.prepare
+ tmux.send_keys 'C-t'
+ lines = tmux.until { |lines| lines.item_count == 100 }
+ tmux.send_keys :Tab, :Tab, :Tab
+ tmux.until { |lines| lines.any_include? ' (3)' }
+ tmux.send_keys :Enter
+ tmux.until { |lines| lines.any_include? '1 2 3' }
+ tmux.send_keys 'C-c'
end
def test_ctrl_t_unicode
- FileUtils.mkdir_p '/tmp/fzf-test'
- tmux.paste 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"'
+ writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2']
+ set_var "FZF_CTRL_T_COMMAND", "cat #{tempname}"
+
tmux.prepare
- tmux.send_keys 'cat ', 'C-t', pane: 0
- tmux.until(1) { |lines| lines.item_count >= 1 }
- tmux.send_keys 'fzf-unicode', pane: 1
- redraw = ->() { tmux.send_keys 'C-l', pane: 1 }
- tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
-
- tmux.send_keys '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
- end
+ tmux.send_keys 'echo ', 'C-t'
+ tmux.until { |lines| lines.item_count == 2 }
+ tmux.send_keys 'fzf-unicode'
+ tmux.until { |lines| lines.match_count == 2 }
+
+ tmux.send_keys '1'
+ 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.any_include? /echo.*fzf-unicode.*1.*fzf-unicode.*2/ }
tmux.send_keys :Enter
- tmux.until { |lines| lines[-1].include? 'test1test2' }
+ tmux.until { |lines| lines.any_include? /^fzf-unicode.*1.*fzf-unicode.*2/ }
end
def test_alt_c
tmux.prepare
- tmux.send_keys :Escape, :c, pane: 0
- lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] }
- expected = lines[-3][2..-1]
- tmux.send_keys :Enter, pane: 1
+ tmux.send_keys :Escape, :c
+ lines = tmux.until { |lines| lines.item_count > 0 }
+ expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1]
+ tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| lines[-1].end_with?(expected) }
@@ -1298,9 +1298,9 @@ module TestShell
tmux.send_keys 'cd /', :Enter
tmux.prepare
- tmux.send_keys :Escape, :c, pane: 0
- lines = tmux.until(1) { |lines| lines.item_count == 1 }
- tmux.send_keys :Enter, pane: 1
+ tmux.send_keys :Escape, :c
+ lines = tmux.until { |lines| lines.item_count == 1 }
+ tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
@@ -1314,11 +1314,11 @@ module TestShell
tmux.send_keys 'echo 3d', :Enter; tmux.prepare
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
tmux.send_keys 'echo 4th', :Enter; tmux.prepare
- tmux.send_keys 'C-r', pane: 0
- tmux.until(1) { |lines| lines.item_count > 0 }
- tmux.send_keys '3d', pane: 1
- tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
- tmux.send_keys :Enter, pane: 1
+ tmux.send_keys 'C-r'
+ tmux.until { |lines| lines.item_count > 0 }
+ tmux.send_keys '3d'
+ tmux.until { |lines| lines[-3].end_with? 'echo 3rd' }
+ tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' }
@@ -1334,12 +1334,12 @@ module CompletionTest
FileUtils.touch File.expand_path(f)
end
tmux.prepare
- tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
- tmux.until(1) { |lines| lines.item_count > 0 }
+ tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
+ tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys ' !d'
- tmux.until(1) { |lines| lines[-2].include?(' 2/') }
- tmux.send_keys :BTab, :BTab
- tmux.until(1) { |lines| lines[-2].include?('(2)') }
+ tmux.until { |lines| lines.match_count == 2 }
+ tmux.send_keys :Tab, :Tab
+ tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
@@ -1349,10 +1349,10 @@ module CompletionTest
# ~USERNAME**<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 +1361,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 +1371,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 +1384,10 @@ module CompletionTest
# Should include hidden files
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" }
tmux.send_keys 'C-u'
- tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab, pane: 0
- tmux.until(1) do |lines|
+ tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
+ tmux.until do |lines|
tmux.send_keys 'C-L'
- lines[-2].include?('100/') &&
- lines[-3].include?('/tmp/fzf-test/.hidden-')
+ lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-')
end
tmux.send_keys :Enter
ensure
@@ -1396,19 +1397,22 @@ module CompletionTest
end
def test_file_completion_root
- tmux.send_keys 'ls /**', :Tab, pane: 0
- tmux.until(1) { |lines| lines.item_count > 0 }
+ tmux.send_keys 'ls /**', :Tab
+ tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :Enter
end
def test_dir_completion
- tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter
+ (1..100).each do |idx|
+ FileUtils.mkdir_p "/tmp/fzf-test/d#{idx}"
+ end
+ FileUtils.touch '/tmp/fzf-test/d55/xxx'
tmux.prepare
- tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0
- tmux.until(1) { |lines| lines.item_count > 0 }
- tmux.send_keys :BTab, :BTab # BTab does not work here
+ tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
+ tmux.until { |lines| lines.item_count > 0 }
+ tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55
- tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
+ tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
@@ -1435,14 +1439,15 @@ module CompletionTest
lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
pid = lines[-1].split.last
tmux.prepare
- tmux.send_keys 'kill ', :Tab, pane: 0
- tmux.until(1) { |lines| lines.item_count > 0 }
+ tmux.send_keys 'C-L'
+ tmux.send_keys 'kill ', :Tab
+ tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys 'sleep12345'
- tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' }
+ tmux.until { |lines| lines.any_include? 'sleep 12345' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
- lines[-1] == "kill #{pid}"
+ lines[-1].include? "kill #{pid}"
end
ensure
Process.kill 'KILL', pid.to_i rescue nil if pid
@@ -1451,10 +1456,10 @@ module CompletionTest
def test_custom_completion
tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
tmux.prepare
- tmux.send_keys 'ls /tmp/**', :Tab, pane: 0
- tmux.until(1) { |lines| lines.item_count == 11 }
- tmux.send_keys :BTab, :BTab, :BTab
- tmux.until(1) { |lines| lines[-2].include? '(3)' }
+ tmux.send_keys 'ls /tmp/**', :Tab
+ tmux.until { |lines| lines.item_count == 11 }
+ tmux.send_keys :Tab, :Tab, :Tab
+ tmux.until { |lines| lines.select_count == 3 }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
@@ -1463,49 +1468,48 @@ module CompletionTest
end
def test_unset_completion
- tmux.send_keys 'export FOO=BAR', :Enter
+ tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
# Using tmux
- tmux.send_keys 'unset FOO**', :Tab, pane: 0
- tmux.until(1) { |lines| lines[-2].include? ' 1/' }
+ tmux.send_keys 'unset FZFFOO**', :Tab
+ tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter
- tmux.until { |lines| lines[-1] == 'unset FOO' }
+ tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
tmux.send_keys 'C-c'
- # FZF_TMUX=0
+ # FZF_TMUX=1
new_shell
- tmux.send_keys 'unset FOO**', :Tab
- tmux.until { |lines| lines[-2].include? ' 1/' }
+ tmux.send_keys 'unset FZFFO**', :Tab, pane: 0
+ tmux.until(1) { |lines| lines.match_count == 1 }
tmux.send_keys :Enter
- tmux.until { |lines| lines[-1] == 'unset FOO' }
+ tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
end
def test_file_completion_unicode
FileUtils.mkdir_p '/tmp/fzf-test'
tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"'
tmux.prepare
- tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0
- redraw = ->() { tmux.send_keys 'C-l', pane: 1 }
- tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
+ tmux.send_keys 'cat fzf-unicode**', :Tab
+ tmux.until { |lines| lines.match_count == 2 }
- tmux.send_keys '1', pane: 1
- tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
- tmux.send_keys :BTab, pane: 1
- tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' }
+ tmux.send_keys '1'
+ tmux.until { |lines| lines.match_count == 1 }
+ tmux.send_keys :Tab
+ tmux.until { |lines| lines.select_count == 1 }
- tmux.send_keys :BSpace, pane: 1
- tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
+ tmux.send_keys :BSpace
+ tmux.until { |lines| lines.match_count == 2 }
- tmux.send_keys '2', pane: 1
- tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
- tmux.send_keys :BTab, pane: 1
- tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' }
+ tmux.send_keys '2'
+ tmux.until { |lines| lines.select_count == 1 }
+ tmux.send_keys :Tab
+ tmux.until { |lines| lines.select_count == 2 }
- tmux.send_keys :Enter, pane: 1
+ tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-l'
- lines[-1].include?('cat') || lines[-2].include?('cat')
+ lines.any_include? 'cat'
end
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' }
@@ -1518,7 +1522,7 @@ class TestBash < TestBase
def new_shell
tmux.prepare
- tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
+ tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
tmux.prepare
end
@@ -1533,7 +1537,7 @@ class TestZsh < TestBase
include CompletionTest
def new_shell
- tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter
+ tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
tmux.prepare
end
@@ -1547,7 +1551,7 @@ class TestFish < TestBase
include TestShell
def new_shell
- tmux.send_keys 'env FZF_TMUX=0 fish', :Enter
+ tmux.send_keys 'env FZF_TMUX=1 fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| lines.empty? }
end