Compare commits

...

34 Commits

Author SHA1 Message Date
Junegunn Choi
909ea1a698 0.10.1 2015-07-27 00:09:07 +09:00
Junegunn Choi
7231acd442 Fix mouse scroll when --margin is set 2015-07-27 00:06:44 +09:00
Junegunn Choi
7814371a9a Revert "0.10.1"
This reverts commit 6166e2dd80.
2015-07-27 00:03:14 +09:00
Junegunn Choi
6166e2dd80 0.10.1 2015-07-26 23:57:26 +09:00
Junegunn Choi
ee0c8a2635 Add --margin option
Close #299
2015-07-26 23:02:04 +09:00
Junegunn Choi
2bebddefc0 Do not print the entire --help on invalid option 2015-07-26 13:39:34 +09:00
Junegunn Choi
fdbf3d3fec Replace eof action with cancel (#289) 2015-07-23 21:05:33 +09:00
Junegunn Choi
f9136cffe6 Update man page 2015-07-23 10:45:01 +09:00
Junegunn Choi
51d84b1869 [bash] Update fzf option completion 2015-07-23 00:58:20 +09:00
Junegunn Choi
13e040baee Bind CTRL-D to the new delete-char/eof action
- CTRL-D - delete-char/eof
- DEL - delete-char
2015-07-23 00:56:03 +09:00
Junegunn Choi
cc0d5539ba Add "eof" action which closes the finder only when input is empty
Close #289
2015-07-22 22:57:48 +09:00
Junegunn Choi
b53f61fc59 Remove cbreak before raw 2015-07-22 22:36:39 +09:00
Junegunn Choi
4e0e03403e Fix --header-lines unaffected by --with-nth 2015-07-22 21:24:02 +09:00
Junegunn Choi
928fccc15b Fix header not shown when the lines go beyond the screen limit 2015-07-22 21:22:59 +09:00
Junegunn Choi
bbaa3ab8bd Update CHANGELOG 2015-07-22 14:19:55 +09:00
Junegunn Choi
5e3cb3a4ea Fix ANSI processor to handle multi-line regions 2015-07-22 14:19:45 +09:00
Junegunn Choi
f71ea5f3ea Add test cases for header and fix corner cases 2015-07-22 13:45:38 +09:00
Junegunn Choi
f469c25730 Add --header-lines option 2015-07-22 03:21:20 +09:00
Junegunn Choi
18469b6954 Adjust header color for dark color scheme 2015-07-22 03:07:27 +09:00
Junegunn Choi
d01db4862b Update documentation 2015-07-22 01:12:50 +09:00
Junegunn Choi
8b2adba8d6 Redraw of header on resize 2015-07-22 00:47:14 +09:00
Junegunn Choi
d459e9abce Add --header-file option 2015-07-22 00:38:38 +09:00
Junegunn Choi
c9abe1b1ff Show more specific error message on invalid binding 2015-07-18 02:31:35 +09:00
Junegunn Choi
a0e6147bb5 Fix #292 - Allow binding of colon and comma 2015-07-16 21:14:08 +09:00
Junegunn Choi
b0f491d3c3 Fix travis CI build
- Fix test failures on new fish 2.2.0
- Make timeout-based test cases more robust
2015-07-13 19:24:22 +09:00
Junegunn Choi
392da53f53 [bash] Make CTRL-R work when histexpand is unset (#286)
Note that it still can't handle properly multi-line commands.
Thanks to @jpcirrus for the bug report and the fix.
2015-07-13 00:22:13 +09:00
Junegunn Choi
ae72b0fb70 Merge pull request #285 from evverx/possible-retry-loop
[bash-completion] Fix g++: possible retry loop
2015-07-04 11:24:08 +09:00
Evgeny Vereshchagin
a79d080ea8 Fix g++: possible retry loop
See http://unix.stackexchange.com/q/213432/120177
2015-07-04 01:20:36 +00:00
Junegunn Choi
ec85fd552d Update README - how to use ag with CTRL-T 2015-06-30 13:17:48 +09:00
Junegunn Choi
11db046fc7 [neovim] Fix #281 - Properly close window with winnr 1 2015-06-27 14:23:51 +09:00
Junegunn Choi
938151a834 [shell] Add FZF_CTRL_T_COMMAND for CTRL-T
Close #40
2015-06-26 01:02:44 +09:00
Junegunn Choi
14e3b84073 [zsh] No need to define __fsel in non-interactive shell
Since we now use fzf-tmux instead of tmux split-window
2015-06-26 00:14:36 +09:00
Junegunn Choi
56100f0fa7 [bash] Use command \find for ALT-C
ALT-C can fail with the following aliases as pointed out in #272

    alias find='noglob find'
    alias command='command '
2015-06-25 23:54:05 +09:00
Junegunn Choi
5254ee2e2a Update documentation (#277) 2015-06-22 01:35:36 +09:00
21 changed files with 743 additions and 200 deletions

View File

@@ -1,6 +1,25 @@
CHANGELOG CHANGELOG
========= =========
0.10.1
------
### New features
- Added `--margin` option
- Added options for sticky header
- `--header-file`
- `--header-lines`
- Added `cancel` action which clears the input or closes the finder when the
input is already empty
- e.g. `export FZF_DEFAULT_OPTS="--bind esc:cancel"`
- Added `delete-char/eof` action to differentiate `CTRL-D` and `DEL`
### Minor improvements/fixes
- Fixed to allow binding colon and comma keys
- Fixed ANSI processor to handle color regions spanning multiple lines
0.10.0 0.10.0
------ ------

View File

@@ -127,6 +127,13 @@ such as: `^music .mp3$ sbtrkt !rmx`
If you don't need fuzzy matching and do not wish to "quote" every word, start If you don't need fuzzy matching and do not wish to "quote" every word, start
fzf with `-e` or `--extended-exact` option. fzf with `-e` or `--extended-exact` option.
#### Environment variables
- `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty
- `FZF_DEFAULT_OPTS`
- Default options. e.g. `export FZF_DEFAULT_OPTS="--extended --cycle"`
Examples Examples
-------- --------
@@ -140,8 +147,9 @@ Key bindings for command line
The install script will setup the following key bindings for bash, zsh, and The install script will setup the following key bindings for bash, zsh, and
fish. fish.
- `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-T` - Paste the selected files and directories onto the command line
- `CTRL-R` - Paste the selected command from history into the command line - Set `FZF_CTRL_T_COMMAND` to override the default command
- `CTRL-R` - Paste the selected command from history onto the command line
- Sort is disabled by default to respect chronological ordering - Sort is disabled by default to respect chronological ordering
- Press `CTRL-R` again to toggle sort - Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
@@ -390,6 +398,9 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""'
# Now fzf (w/o pipe) will use ag instead of find # Now fzf (w/o pipe) will use ag instead of find
fzf fzf
# To apply the command to CTRL-T as well
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
``` ```
#### `git ls-tree` for fast traversal #### `git ls-tree` for fast traversal

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.10.0 version=0.10.1
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "June 2015" "fzf 0.10.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "July 2015" "fzf 0.10.1" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -122,6 +122,7 @@ e.g. \fBfzf --color=bg+:24\fR
\fBpointer \fRPointer to the current line \fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker \fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator \fBspinner \fRStreaming input indicator
\fBheader \fRHeader
.RE .RE
.TP .TP
.B "--black" .B "--black"
@@ -130,6 +131,31 @@ Use black background
.B "--reverse" .B "--reverse"
Reverse orientation Reverse orientation
.TP .TP
.BI "--margin=" MARGIN
Comma-separated expression for margins around the finder.
.br
.R ""
.br
.RS
.BR TRBL " Same margin for top, right, bottom, and left"
.br
.BR TB,RL " Vertical, horizontal margin"
.br
.BR T,RL,B " Top, horizontal, bottom margin"
.br
.BR T,R,B,L " Top, right, bottom, left margin"
.br
.R ""
.br
Each part can be given in absolute number or in percentage relative to the
terminal size with \fB%\fR suffix.
.br
.R ""
.br
e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR
.RE
.TP
.B "--cycle" .B "--cycle"
Enable cyclic scroll Enable cyclic scroll
.TP .TP
@@ -187,8 +213,10 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR
\fBclear-screen\fR \fIctrl-l\fR \fBclear-screen\fR \fIctrl-l\fR
\fBdelete-char\fR \fIctrl-d del\fR \fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR
\fBdeselect-all\fR \fBdeselect-all\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
@@ -256,6 +284,17 @@ When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
.BI "--history-size=" "N" .BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is Maximum number of entries in the history file (default: 1000). The file is
automatically truncated when the number of the lines exceeds the value. automatically truncated when the number of the lines exceeds the value.
.TP
.BI "--header-file=" "FILE"
The content of the file will be printed as the sticky header. The lines in the
file are displayed in order from top to bottom regardless of \fB--reverse\fR,
and are not affected by \fB--with-nth\fR. ANSI color codes are processed even
when \fB--ansi\fR is not set.
.TP
.BI "--header-lines=" "N"
The first N lines of the input are treated as the sticky header. When
\fB--with-nth\fR is set, the lines are transformed just like the other
lines that follow.
.SS Scripting .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"
@@ -297,7 +336,7 @@ e.g. \fBfzf --multi | fzf --sync\fR
Default command to use when input is tty Default command to use when input is tty
.TP .TP
.B FZF_DEFAULT_OPTS .B FZF_DEFAULT_OPTS
Default options. e.g. \fB--extended --ansi\fR Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
.SH EXIT STATUS .SH EXIT STATUS
.BR 0 " Normal exit" .BR 0 " Normal exit"

View File

@@ -249,7 +249,7 @@ function! s:calc_size(max, val)
endfunction endfunction
function! s:getpos() function! s:getpos()
return {'tab': tabpagenr(), 'win': winnr()} return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$')}
endfunction endfunction
function! s:split(dict) function! s:split(dict)

View File

@@ -48,7 +48,10 @@ _fzf_opts_completion() {
--sync --sync
--cycle --cycle
--history --history
--history-size" --history-size
--header-file
--header-lines
--margin"
case "${prev}" in case "${prev}" in
--tiebreak) --tiebreak)
@@ -59,7 +62,7 @@ _fzf_opts_completion() {
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0 return 0
;; ;;
--history) --history|--header-file)
COMPREPLY=() COMPREPLY=()
return 0 return 0
;; ;;
@@ -74,9 +77,10 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig ret local cmd orig ret orig_cmd
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1"
orig=$(eval "echo \$_fzf_orig_completion_$cmd") orig=$(eval "echo \$_fzf_orig_completion_$cmd")
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
@@ -84,7 +88,7 @@ _fzf_handle_dynamic_completion() {
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
_completion_loader "$@" _completion_loader "$@"
ret=$? ret=$?
eval $(complete | \grep "\-F.* $cmd$" | _fzf_orig_completion_filter) eval $(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)
source $BASH_SOURCE source $BASH_SOURCE
return $ret return $ret
fi fi

View File

@@ -1,10 +1,11 @@
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command \\find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
eval "$cmd" | fzf -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -24,22 +25,28 @@ __fzf_select_tmux__() {
else else
height="-l $height" height="-l $height"
fi fi
tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" tmux split-window $height "cd $(printf %q "$PWD"); FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'"
} }
__fzf_cd__() { __fzf_cd__() {
local dir local dir
dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ dir=$(command \find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir" -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir"
} }
__fzf_history__() { __fzf_history__() (
local line local line
shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r |
\grep '^ *[0-9]') && sed 's/ *\([0-9]*\)\** .*/!\1/' <<< "$line" \grep '^ *[0-9]') &&
} if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
else
sed 's/^ *\([0-9]*\)\** *//' <<< "$line"
fi
)
__use_tmux=0 __use_tmux=0
[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1

View File

@@ -14,10 +14,12 @@ function fzf_key_bindings
end end
function __fzf_ctrl_t function __fzf_ctrl_t
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) -m > $TMPDIR/fzf.result -o -type l -print 2> /dev/null | sed 1d | cut -b3-"
eval $FZF_CTRL_T_COMMAND | eval (__fzfcmd) -m > $TMPDIR/fzf.result
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result

View File

@@ -1,11 +1,14 @@
# Key bindings # Key bindings
# ------------ # ------------
if [[ $- =~ i ]]; then
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command \\find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
eval "$cmd" | $(__fzfcmd) -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -15,8 +18,6 @@ __fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
} }
if [[ $- =~ i ]]; then
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fsel)"
zle redisplay zle redisplay

View File

@@ -36,11 +36,13 @@ func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
} }
func extractColor(str *string) (*string, []ansiOffset) { func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiState) {
var offsets []ansiOffset var offsets []ansiOffset
var output bytes.Buffer var output bytes.Buffer
var state *ansiState
if state != nil {
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
}
idx := 0 idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) { for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) {
@@ -76,7 +78,7 @@ func extractColor(str *string) (*string, []ansiOffset) {
} }
} }
outputStr := output.String() outputStr := output.String()
return &outputStr, offsets return &outputStr, offsets, state
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func interpretCode(ansiCode string, prevState *ansiState) *ansiState {

View File

@@ -14,79 +14,89 @@ func TestExtractColor(t *testing.T) {
} }
src := "hello world" src := "hello world"
var state *ansiState
clean := "\x1b[0m" clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset)) { check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) {
output, ansiOffsets := extractColor(&src) output, ansiOffsets, newState := extractColor(&src, state)
state = newState
if *output != "hello world" { if *output != "hello world" {
t.Errorf("Invalid output: {}", output) t.Errorf("Invalid output: {}", output)
} }
fmt.Println(src, ansiOffsets, clean) fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets) assertion(ansiOffsets, state)
} }
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) > 0 { if len(offsets) > 0 {
t.Fail() t.Fail()
} }
}) })
state = nil
src = "\x1b[0mhello world" src = "\x1b[0mhello world"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) > 0 { if len(offsets) > 0 {
t.Fail() t.Fail()
} }
}) })
state = nil
src = "\x1b[1mhello world" src = "\x1b[1mhello world"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, -1, -1, true) assert(offsets[0], 0, 11, -1, -1, true)
}) })
state = nil
src = "\x1b[1mhello \x1b[mworld" src = "\x1b[1mhello \x1b[mworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 6, -1, -1, true) assert(offsets[0], 0, 6, -1, -1, true)
}) })
state = nil
src = "\x1b[1mhello \x1b[Kworld" src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, -1, -1, true) assert(offsets[0], 0, 11, -1, -1, true)
}) })
state = nil
src = "hello \x1b[34;45;1mworld" src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 4, 5, true) assert(offsets[0], 6, 11, 4, 5, true)
}) })
state = nil
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 4, 5, true) assert(offsets[0], 6, 11, 4, 5, true)
}) })
state = nil
src = "hello \x1b[34;45;1mwor\x1b[0mld" src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 4, 5, true) assert(offsets[0], 6, 9, 4, 5, true)
}) })
state = nil
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 3 { if len(offsets) != 3 {
t.Fail() t.Fail()
} }
@@ -96,12 +106,47 @@ func TestExtractColor(t *testing.T) {
}) })
// {38,48};5;{38,48} // {38,48};5;{38,48}
state = nil
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 2 { if len(offsets) != 2 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 38, 48, true) assert(offsets[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true) assert(offsets[1], 9, 10, 48, 38, true)
}) })
src = "hello \x1b[32;1mworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
t.Fail()
}
if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail()
}
assert(offsets[0], 6, 11, 2, -1, true)
})
src = "hello world"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
t.Fail()
}
if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail()
}
assert(offsets[0], 0, 11, 2, -1, true)
})
src = "hello \x1b[0;38;5;200;48;5;100mworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 2 {
t.Fail()
}
if state.fg != 200 || state.bg != 100 || state.bold {
t.Fail()
}
assert(offsets[0], 0, 6, 2, -1, true)
assert(offsets[1], 6, 11, 200, 100, false)
})
} }

View File

@@ -26,8 +26,13 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
trans: trans} trans: trans}
} }
func (c *Chunk) push(trans ItemBuilder, data *string, index int) { func (c *Chunk) push(trans ItemBuilder, data *string, index int) bool {
*c = append(*c, trans(data, index)) item := trans(data, index)
if item != nil {
*c = append(*c, item)
return true
}
return false
} }
// IsFull returns true if the Chunk is full // IsFull returns true if the Chunk is full
@@ -48,7 +53,7 @@ func CountItems(cs []*Chunk) int {
} }
// Push adds the item to the list // Push adds the item to the list
func (cl *ChunkList) Push(data string) { func (cl *ChunkList) Push(data string) bool {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock() defer cl.mutex.Unlock()
@@ -57,8 +62,11 @@ func (cl *ChunkList) Push(data string) {
cl.chunks = append(cl.chunks, &newChunk) cl.chunks = append(cl.chunks, &newChunk)
} }
cl.lastChunk().push(cl.trans, &data, cl.count) if cl.lastChunk().push(cl.trans, &data, cl.count) {
cl.count++ cl.count++
return true
}
return false
} }
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
Version = "0.10.0" Version = "0.10.1"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -44,5 +44,6 @@ const (
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtHeader
EvtClose EvtClose
) )

View File

@@ -44,6 +44,7 @@ Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew:bool -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list) Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/ */
// Run starts fzf // Run starts fzf
@@ -68,14 +69,17 @@ func Run(opts *Options) {
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
var state *ansiState
ansiProcessor = func(data *string) (*string, []ansiOffset) { ansiProcessor = func(data *string) (*string, []ansiOffset) {
return extractColor(data) trimmed, offsets, newState := extractColor(data, state)
state = newState
return trimmed, offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data *string) (*string, []ansiOffset) { ansiProcessor = func(data *string) (*string, []ansiOffset) {
trimmed, _ := extractColor(data) trimmed, _, _ := extractColor(data, nil)
return trimmed, nil return trimmed, nil
} }
} }
@@ -83,8 +87,14 @@ func Run(opts *Options) {
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data *string, index int) *Item {
if len(header) < opts.HeaderLines {
header = append(header, *data)
eventBox.Set(EvtHeader, header)
return nil
}
data, colors := ansiProcessor(data) data, colors := ansiProcessor(data)
return &Item{ return &Item{
text: data, text: data,
@@ -96,6 +106,11 @@ func Run(opts *Options) {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data *string, index int) *Item {
tokens := Tokenize(data, opts.Delimiter) tokens := Tokenize(data, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines {
header = append(header, *joinTokens(trans))
eventBox.Set(EvtHeader, header)
return nil
}
item := Item{ item := Item{
text: joinTokens(trans), text: joinTokens(trans),
origText: data, origText: data,
@@ -113,7 +128,9 @@ func Run(opts *Options) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox, opts.ReadZero} reader := Reader{func(str string) bool {
return chunkList.Push(str)
}, eventBox, opts.ReadZero}
go reader.ReadSource() go reader.ReadSource()
} }
@@ -134,11 +151,12 @@ func Run(opts *Options) {
if streamingFilter { if streamingFilter {
reader := Reader{ reader := Reader{
func(str string) { func(str string) bool {
item := chunkList.trans(&str, 0) item := chunkList.trans(&str, 0)
if pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) {
fmt.Println(*item.text) fmt.Println(*item.text)
} }
return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
reader.ReadSource() reader.ReadSource()
} else { } else {
@@ -206,6 +224,9 @@ func Run(opts *Options) {
terminal.UpdateProgress(val) terminal.UpdateProgress(val)
} }
case EvtHeader:
terminal.UpdateHeader(value.([]string), opts.HeaderLines)
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
case *Merger: case *Merger:

View File

@@ -94,6 +94,7 @@ const (
ColInfo ColInfo
ColCursor ColCursor
ColSelected ColSelected
ColHeader
ColUser ColUser
) )
@@ -114,6 +115,7 @@ type ColorTheme struct {
Info int16 Info int16
Cursor int16 Cursor int16
Selected int16 Selected int16
Header int16
} }
type Event struct { type Event struct {
@@ -164,7 +166,8 @@ func init() {
Spinner: C.COLOR_GREEN, Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE, Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED, Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA} Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@@ -177,7 +180,8 @@ func init() {
Spinner: 148, Spinner: 148,
Info: 144, Info: 144,
Cursor: 161, Cursor: 161,
Selected: 168} Selected: 168,
Header: 109}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@@ -190,7 +194,8 @@ func init() {
Spinner: 65, Spinner: 65,
Info: 101, Info: 101,
Cursor: 161, Cursor: 161,
Selected: 168} Selected: 168,
Header: 31}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@@ -257,7 +262,6 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
if mouse { if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mousemask(C.ALL_MOUSE_EVENTS, nil)
} }
C.cbreak()
C.noecho() C.noecho()
C.raw() // stty dsusp undef C.raw() // stty dsusp undef
@@ -308,6 +312,7 @@ func initPairs(theme *ColorTheme, black bool) {
C.init_pair(ColInfo, C.short(theme.Info), bg) C.init_pair(ColInfo, C.short(theme.Info), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG) C.init_pair(ColCursor, C.short(theme.Cursor), darkBG)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG) C.init_pair(ColSelected, C.short(theme.Selected), darkBG)
C.init_pair(ColHeader, C.short(theme.Header), bg)
} }
func Close() { func Close() {

View File

@@ -2,6 +2,7 @@ package fzf
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -27,7 +28,7 @@ const usage = `usage: fzf [options]
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--tiebreak=CRI Sort criterion when the scores are tied; --tiebreak=CRITERION Sort criterion when the scores are tied;
[length|begin|end|index] (default: length) [length|begin|end|index] (default: length)
Interface Interface
@@ -37,6 +38,7 @@ const usage = `usage: fzf [options]
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--black Use black background --black Use black background
--reverse Reverse orientation --reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
@@ -44,6 +46,8 @@ const usage = `usage: fzf [options]
--bind=KEYBINDS Custom key bindings. Refer to the man page. --bind=KEYBINDS Custom key bindings. Refer to the man page.
--history=FILE History file --history=FILE History file
--history-size=N Maximum number of history entries (default: 1000) --history-size=N Maximum number of history entries (default: 1000)
--header-file=FILE The file whose content to be printed as header
--header-lines=N The first N lines of the input are treated as header
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@@ -90,6 +94,10 @@ const (
byIndex byIndex
) )
func defaultMargin() [4]string {
return [4]string{"0", "0", "0", "0"}
}
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Mode Mode Mode Mode
@@ -122,6 +130,9 @@ type Options struct {
ReadZero bool ReadZero bool
Sync bool Sync bool
History *History History *History
Header []string
HeaderLines int
Margin [4]string
Version bool Version bool
} }
@@ -164,6 +175,9 @@ func defaultOptions() *Options {
ReadZero: false, ReadZero: false,
Sync: false, Sync: false,
History: nil, History: nil,
Header: make([]string, 0),
HeaderLines: 0,
Margin: defaultMargin(),
Version: false} Version: false}
} }
@@ -174,7 +188,7 @@ func help(ok int) {
func errorExit(msg string) { func errorExit(msg string) {
os.Stderr.WriteString(msg + "\n") os.Stderr.WriteString(msg + "\n")
help(1) os.Exit(1)
} }
func optString(arg string, prefixes ...string) (bool, string) { func optString(arg string, prefixes ...string) (bool, string) {
@@ -211,6 +225,14 @@ func atoi(str string) int {
return num return num
} }
func atof(str string) float64 {
num, err := strconv.ParseFloat(str, 64)
if err != nil {
errorExit("not a valid number: " + str)
}
return num
}
func nextInt(args []string, i *int, message string) int { func nextInt(args []string, i *int, message string) int {
if len(args) > *i+1 { if len(args) > *i+1 {
*i++ *i++
@@ -413,6 +435,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
theme.Cursor = ansi theme.Cursor = ansi
case "marker": case "marker":
theme.Selected = ansi theme.Selected = ansi
case "header":
theme.Header = ansi
default: default:
fail() fail()
} }
@@ -430,6 +454,11 @@ func firstKey(keymap map[int]string) int {
return 0 return 0
} }
const (
escapedColon = 0
escapedComma = 1
)
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
if executeRegexp == nil { if executeRegexp == nil {
// Backreferences are not supported. // Backreferences are not supported.
@@ -440,26 +469,31 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
}) })
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
idx := 0 idx := 0
for _, pairStr := range strings.Split(masked, ",") { for _, pairStr := range strings.Split(masked, ",") {
pairStr = str[idx : idx+len(pairStr)] origPairStr := str[idx : idx+len(pairStr)]
idx += len(pairStr) + 1 idx += len(pairStr) + 1
fail := func() {
errorExit("invalid key binding: " + pairStr)
}
pair := strings.SplitN(pairStr, ":", 2) pair := strings.SplitN(pairStr, ":", 2)
if len(pair) != 2 { if len(pair) < 2 {
fail() errorExit("bind action not specified: " + origPairStr)
} }
var key int
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = ':' + curses.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + curses.AltZ
} else {
keys := parseKeyChords(pair[0], "key name required") keys := parseKeyChords(pair[0], "key name required")
if len(keys) != 1 { key = firstKey(keys)
fail()
} }
key := firstKey(keys)
act := strings.ToLower(pair[1]) act := origPairStr[len(pair[0])+1 : len(origPairStr)]
switch act { actLower := strings.ToLower(act)
switch actLower {
case "ignore": case "ignore":
keymap[key] = actIgnore keymap[key] = actIgnore
case "beginning-of-line": case "beginning-of-line":
@@ -478,8 +512,12 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
keymap[key] = actClearScreen keymap[key] = actClearScreen
case "delete-char": case "delete-char":
keymap[key] = actDeleteChar keymap[key] = actDeleteChar
case "delete-char/eof":
keymap[key] = actDeleteCharEof
case "end-of-line": case "end-of-line":
keymap[key] = actEndOfLine keymap[key] = actEndOfLine
case "cancel":
keymap[key] = actCancel
case "forward-char": case "forward-char":
keymap[key] = actForwardChar keymap[key] = actForwardChar
case "forward-word": case "forward-word":
@@ -524,12 +562,12 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
keymap[key] = actToggleSort keymap[key] = actToggleSort
toggleSort = true toggleSort = true
default: default:
if isExecuteAction(act) { if isExecuteAction(actLower) {
keymap[key] = actExecute keymap[key] = actExecute
if pair[1][7] == ':' { if act[7] == ':' {
execmap[key] = pair[1][8:] execmap[key] = act[8:]
} else { } else {
execmap[key] = pair[1][8 : len(act)-1] execmap[key] = act[8 : len(act)-1]
} }
} else { } else {
errorExit("unknown action: " + act) errorExit("unknown action: " + act)
@@ -561,6 +599,56 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
return keymap return keymap
} }
func readHeaderFile(filename string) []string {
content, err := ioutil.ReadFile(filename)
if err != nil {
errorExit("failed to read header file: " + filename)
}
return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
}
func parseMargin(margin string) [4]string {
margins := strings.Split(margin, ",")
checked := func(str string) string {
if strings.HasSuffix(str, "%") {
val := atof(str[:len(str)-1])
if val < 0 {
errorExit("margin must be non-negative")
}
if val > 100 {
errorExit("margin too large")
}
} else {
val := atoi(str)
if val < 0 {
errorExit("margin must be non-negative")
}
}
return str
}
switch len(margins) {
case 1:
m := checked(margins[0])
return [4]string{m, m, m, m}
case 2:
tb := checked(margins[0])
rl := checked(margins[1])
return [4]string{tb, rl, tb, rl}
case 3:
t := checked(margins[0])
rl := checked(margins[1])
b := checked(margins[2])
return [4]string{t, rl, b, rl}
case 4:
return [4]string{
checked(margins[0]), checked(margins[1]),
checked(margins[2]), checked(margins[3])}
default:
errorExit("invalid margin: " + margin)
}
return defaultMargin()
}
func parseOptions(opts *Options, allArgs []string) { func parseOptions(opts *Options, allArgs []string) {
keymap := make(map[int]actionType) keymap := make(map[int]actionType)
var historyMax int var historyMax int
@@ -700,6 +788,23 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(nextString(allArgs, &i, "history file path required")) setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size": case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required")) setHistoryMax(nextInt(allArgs, &i, "history max size required"))
case "--no-header-file":
opts.Header = []string{}
case "--no-header-lines":
opts.HeaderLines = 0
case "--header-file":
opts.Header = readHeaderFile(
nextString(allArgs, &i, "header file name required"))
opts.HeaderLines = 0
case "--header-lines":
opts.Header = []string{}
opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required"))
case "--no-margin":
opts.Margin = defaultMargin()
case "--margin":
opts.Margin = parseMargin(
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--version": case "--version":
opts.Version = true opts.Version = true
default: default:
@@ -733,12 +838,24 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(value) setHistory(value)
} else if match, value := optString(arg, "--history-size="); match { } else if match, value := optString(arg, "--history-size="); match {
setHistoryMax(atoi(value)) setHistoryMax(atoi(value))
} else if match, value := optString(arg, "--header-file="); match {
opts.Header = readHeaderFile(value)
opts.HeaderLines = 0
} else if match, value := optString(arg, "--header-lines="); match {
opts.Header = []string{}
opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }
} }
} }
if opts.HeaderLines < 0 {
errorExit("header lines must be a non-negative integer")
}
// Change default actions for CTRL-N / CTRL-P when --history is used // Change default actions for CTRL-N / CTRL-P when --history is used
if opts.History != nil { if opts.History != nil {
if _, prs := keymap[curses.CtrlP]; !prs { if _, prs := keymap[curses.CtrlP]; !prs {

View File

@@ -188,13 +188,15 @@ func TestBind(t *testing.T) {
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
",X:execute:\nfoobar,Y:execute(baz)") ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
if !toggleSort { if !toggleSort {
t.Errorf("toggleSort not set") t.Errorf("toggleSort not set")
} }
check(actKillLine, keymap[curses.CtrlA]) check(actKillLine, keymap[curses.CtrlA])
check(actToggleSort, keymap[curses.CtrlB]) check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c']) check(actPageUp, keymap[curses.AltZ+'c'])
check(actAbort, keymap[curses.AltZ+','])
check(actAccept, keymap[curses.AltZ+':'])
check(actPageDown, keymap[curses.AltZ]) check(actPageDown, keymap[curses.AltZ])
check(actExecute, keymap[curses.F1]) check(actExecute, keymap[curses.F1])
check(actExecute, keymap[curses.F2]) check(actExecute, keymap[curses.F2])

View File

@@ -11,7 +11,7 @@ import (
// Reader reads from command or standard input // Reader reads from command or standard input
type Reader struct { type Reader struct {
pusher func(string) pusher func(string) bool
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
} }
@@ -43,9 +43,10 @@ func (r *Reader) feed(src io.Reader) {
if err == nil { if err == nil {
line = line[:len(line)-1] line = line[:len(line)-1]
} }
r.pusher(line) if r.pusher(line) {
r.eventBox.Set(EvtReadNew, nil) r.eventBox.Set(EvtReadNew, nil)
} }
}
if err != nil { if err != nil {
break break
} }

View File

@@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) {
strs := []string{} strs := []string{}
eb := util.NewEventBox() eb := util.NewEventBox()
reader := Reader{ reader := Reader{
pusher: func(s string) { strs = append(strs, s) }, pusher: func(s string) bool { strs = append(strs, s); return true },
eventBox: eb} eventBox: eb}
// Check EventBox // Check EventBox

View File

@@ -8,6 +8,7 @@ import (
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -40,6 +41,9 @@ type Terminal struct {
printQuery bool printQuery bool
history *History history *History
cycle bool cycle bool
header []string
margin [4]string
marginInt [4]int
count int count int
progress int progress int
reading bool reading bool
@@ -78,6 +82,7 @@ var _runeWidths = make(map[rune]int)
const ( const (
reqPrompt util.EventType = iota reqPrompt util.EventType = iota
reqInfo reqInfo
reqHeader
reqList reqList
reqRefresh reqRefresh
reqRedraw reqRedraw
@@ -98,8 +103,10 @@ const (
actBackwardChar actBackwardChar
actBackwardDeleteChar actBackwardDeleteChar
actBackwardWord actBackwardWord
actCancel
actClearScreen actClearScreen
actDeleteChar actDeleteChar
actDeleteCharEof
actEndOfLine actEndOfLine
actForwardChar actForwardChar
actForwardWord actForwardWord
@@ -134,7 +141,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.CtrlG] = actAbort keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort keymap[C.ESC] = actAbort
keymap[C.CtrlD] = actDeleteChar keymap[C.CtrlD] = actDeleteCharEof
keymap[C.CtrlE] = actEndOfLine keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar keymap[C.CtrlH] = actBackwardDeleteChar
@@ -165,7 +172,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.Home] = actBeginningOfLine keymap[C.Home] = actBeginningOfLine
keymap[C.End] = actEndOfLine keymap[C.End] = actEndOfLine
keymap[C.Del] = actDeleteChar // FIXME Del vs. CTRL-D keymap[C.Del] = actDeleteChar
keymap[C.PgUp] = actPageUp keymap[C.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown keymap[C.PgDn] = actPageDown
@@ -196,7 +203,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
margin: opts.Margin,
marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle, cycle: opts.Cycle,
header: opts.Header,
reading: true, reading: true,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[uint32]selectedItem), selected: make(map[uint32]selectedItem),
@@ -229,6 +239,22 @@ func (t *Terminal) UpdateCount(cnt int, final bool) {
} }
} }
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string, lines int) {
t.mutex.Lock()
t.header = make([]string, lines)
copy(t.header, header)
if !t.reverse {
reversed := make([]string, lines)
for idx, str := range t.header {
reversed[lines-idx-1] = str
}
t.header = reversed
}
t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil)
}
// UpdateProgress updates the search progress // UpdateProgress updates the search progress
func (t *Terminal) UpdateProgress(progress float32) { func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock() t.mutex.Lock()
@@ -296,10 +322,50 @@ func displayWidth(runes []rune) int {
return l return l
} }
const minWidth = 16
const minHeight = 4
func (t *Terminal) calculateMargins() {
screenWidth := C.MaxX()
screenHeight := C.MaxY()
for idx, str := range t.margin {
if str == "0" {
t.marginInt[idx] = 0
} else if strings.HasSuffix(str, "%") {
num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
var val float64
if idx%2 == 0 {
val = float64(screenHeight)
} else {
val = float64(screenWidth)
}
t.marginInt[idx] = int(val * num * 0.01)
} else {
num, _ := strconv.Atoi(str)
t.marginInt[idx] = num
}
}
adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min {
margin := t.marginInt[idx1] + t.marginInt[idx2]
if max-margin < min {
desired := max - min
t.marginInt[idx1] = desired * t.marginInt[idx1] / margin
t.marginInt[idx2] = desired * t.marginInt[idx2] / margin
}
}
}
adjust(1, 3, screenWidth, minWidth)
adjust(0, 2, screenHeight, minHeight)
}
func (t *Terminal) move(y int, x int, clear bool) { func (t *Terminal) move(y int, x int, clear bool) {
x += t.marginInt[3]
maxy := C.MaxY() maxy := C.MaxY()
if !t.reverse { if !t.reverse {
y = maxy - y - 1 y = maxy - y - 1 - t.marginInt[2]
} else {
y += t.marginInt[0]
} }
if clear { if clear {
@@ -354,17 +420,49 @@ func (t *Terminal) printInfo() {
C.CPrint(C.ColInfo, false, output) C.CPrint(C.ColInfo, false, output)
} }
func (t *Terminal) maxHeight() int {
return C.MaxY() - t.marginInt[0] - t.marginInt[2]
}
func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
max := t.maxHeight()
var state *ansiState
for idx, lineStr := range t.header {
if !t.reverse {
idx = len(t.header) - idx - 1
}
line := idx + 2
if t.inlineInfo {
line -= 1
}
if line >= max {
continue
}
trimmed, colors, newState := extractColor(&lineStr, state)
state = newState
item := &Item{
text: trimmed,
index: 0,
colors: colors,
rank: Rank{0, 0, 0}}
t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false)
}
}
func (t *Terminal) printList() { func (t *Terminal) printList() {
t.constrain() t.constrain()
maxy := t.maxItems() maxy := t.maxItems()
count := t.merger.Length() - t.offset count := t.merger.Length() - t.offset
for i := 0; i < maxy; i++ { for i := 0; i < maxy; i++ {
var line int line := i + 2 + len(t.header)
if t.inlineInfo { if t.inlineInfo {
line = i + 1 line -= 1
} else {
line = i + 2
} }
t.move(line, 0, true) t.move(line, 0, true)
if i < count { if i < count {
@@ -441,7 +539,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
// Overflow // Overflow
text := []rune(*item.text) text := []rune(*item.text)
offsets := item.colorOffsets(col2, bold, current) offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
fullWidth := displayWidth(text) fullWidth := displayWidth(text)
if fullWidth > maxWidth { if fullWidth > maxWidth {
if t.hscroll { if t.hscroll {
@@ -524,9 +622,11 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
} }
func (t *Terminal) printAll() { func (t *Terminal) printAll() {
t.calculateMargins()
t.printList() t.printList()
t.printPrompt() t.printPrompt()
t.printInfo() t.printInfo()
t.printHeader()
} }
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
@@ -602,10 +702,12 @@ func (t *Terminal) Loop() {
{ // Late initialization { // Late initialization
t.mutex.Lock() t.mutex.Lock()
t.initFunc() t.initFunc()
t.calculateMargins()
t.printPrompt() t.printPrompt()
t.placeCursor() t.placeCursor()
C.Refresh() C.Refresh()
t.printInfo() t.printInfo()
t.printHeader()
t.mutex.Unlock() t.mutex.Unlock()
go func() { go func() {
timer := time.NewTimer(initialDelay) timer := time.NewTimer(initialDelay)
@@ -660,6 +762,8 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
case reqHeader:
t.printHeader()
case reqRefresh: case reqRefresh:
t.suppress = false t.suppress = false
case reqRedraw: case reqRedraw:
@@ -757,11 +861,21 @@ func (t *Terminal) Loop() {
case actAbort: case actAbort:
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
t.delChar()
case actDeleteCharEof:
if !t.delChar() && t.cx == 0 { if !t.delChar() && t.cx == 0 {
req(reqQuit) req(reqQuit)
} }
case actEndOfLine: case actEndOfLine:
t.cx = len(t.input) t.cx = len(t.input)
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actForwardChar: case actForwardChar:
if t.cx < len(t.input) { if t.cx < len(t.input) {
t.cx++ t.cx++
@@ -879,14 +993,7 @@ func (t *Terminal) Loop() {
} }
case actMouse: case actMouse:
me := event.MouseEvent me := event.MouseEvent
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y mx, my := me.X, me.Y
if !t.reverse {
my = C.MaxY() - my - 1
}
min := 2
if t.inlineInfo {
min = 1
}
if me.S != 0 { if me.S != 0 {
// Scroll // Scroll
if t.merger.Length() > 0 { if t.merger.Length() > 0 {
@@ -896,7 +1003,19 @@ func (t *Terminal) Loop() {
t.vmove(me.S) t.vmove(me.S)
req(reqList) req(reqList)
} }
} else if me.Double { } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3]
my -= t.marginInt[0]
mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
if !t.reverse {
my = t.maxHeight() - my - 1
}
min := 2 + len(t.header)
if t.inlineInfo {
min -= 1
}
if me.Double {
// Double-click // Double-click
if my >= min { if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
@@ -916,6 +1035,7 @@ func (t *Terminal) Loop() {
} }
} }
} }
}
changed := string(previousInput) != string(t.input) changed := string(previousInput) != string(t.input)
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
@@ -948,6 +1068,7 @@ func (t *Terminal) constrain() {
t.offset = util.Max(0, count-height) t.offset = util.Max(0, count-height)
t.cy = util.Constrain(t.offset+diffpos, 0, count-1) t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
} }
t.offset = util.Max(0, t.offset)
} }
func (t *Terminal) vmove(o int) { func (t *Terminal) vmove(o int) {
@@ -976,8 +1097,9 @@ func (t *Terminal) vset(o int) bool {
} }
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.maxHeight() - 2 - len(t.header)
if t.inlineInfo { if t.inlineInfo {
return C.MaxY() - 1 max += 1
} }
return C.MaxY() - 2 return util.Max(max, 0)
} }

View File

@@ -148,6 +148,7 @@ class TestBase < Minitest::Test
def setup def setup
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_CTRL_T_COMMAND'
ENV.delete 'FZF_DEFAULT_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND'
end end
@@ -311,7 +312,9 @@ class TestGoFZF < TestBase
# However, the output must not be transformed # However, the output must not be transformed
if multi if multi
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/) assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
else else
tmux.send_keys '^', '3' tmux.send_keys '^', '3'
@@ -347,7 +350,9 @@ class TestGoFZF < TestBase
[:'0', :'1', [:'1', :'0']].each do |opt| [:'0', :'1', [:'1', :'0']].each do |opt|
tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
tmux.until { |lines| lines.last =~ /^> 5/ } tmux.until { |lines| lines.last =~ /^> 5/ }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal ['5', '5', '15', '25'], readonce.split($/) assert_equal ['5', '5', '15', '25'], readonce.split($/)
end end
end end
@@ -364,7 +369,9 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-1] == '>' } tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 9 tmux.send_keys 9
tmux.until { |lines| lines[-2] == ' 19/100' } tmux.until { |lines| lines[-2] == ' 19/100' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '>' } tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 'C-K', :Enter tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/) assert_equal ['1919'], readonce.split($/)
@@ -373,7 +380,9 @@ class TestGoFZF < TestBase
def test_tac def test_tac
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[1000 999 998], readonce.split($/) assert_equal %w[1000 999 998], readonce.split($/)
end end
@@ -381,7 +390,9 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '99' tmux.send_keys '99'
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[99 999 998], readonce.split($/) assert_equal %w[99 999 998], readonce.split($/)
end end
@@ -390,7 +401,9 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '00' tmux.send_keys '00'
tmux.until { |lines| lines[-2].include? '10/1000' } tmux.until { |lines| lines[-2].include? '10/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[1000 900 800], readonce.split($/) assert_equal %w[1000 900 800], readonce.split($/)
end end
@@ -635,6 +648,101 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-10].start_with? '>' } tmux.until { |lines| lines[-10].start_with? '>' }
end end
def test_header_lines
tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5'}", :Enter
2.times do
tmux.until do |lines|
lines[-2].include?('/90') &&
lines[-3] == ' 1' &&
lines[-4] == ' 2' &&
lines[-13] == '> 15'
end
tmux.send_keys :Down
end
tmux.send_keys :Enter
assert_equal '15', readonce.chomp
end
def test_header_lines_reverse
tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --reverse'}", :Enter
2.times do
tmux.until do |lines|
lines[1].include?('/90') &&
lines[2] == ' 1' &&
lines[3] == ' 2' &&
lines[12] == '> 15'
end
tmux.send_keys :Up
end
tmux.send_keys :Enter
assert_equal '15', readonce.chomp
end
def test_header_lines_overflow
tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter
tmux.until do |lines|
lines[-2].include?('0/0') &&
lines[-3].include?(' 1')
end
tmux.send_keys :Enter
assert_equal '', readonce.chomp
end
def test_header_lines_with_nth
tmux.send_keys "seq 100 | #{fzf "--header-lines 5 --with-nth 1,1,1,1,1"}", :Enter
tmux.until do |lines|
lines[-2].include?('95/95') &&
lines[-3] == ' 11111' &&
lines[-7] == ' 55555' &&
lines[-8] == '> 66666'
end
tmux.send_keys :Enter
assert_equal '6', readonce.chomp
end
def test_header_file
tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[-2].include?('100/100') &&
lines[-7..-3].map(&:strip) == header
end
end
def test_header_file_reverse
tmux.send_keys "seq 100 | #{fzf "--header-file=<(head -5 #{__FILE__}) --reverse"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[1].include?('100/100') &&
lines[2..6].map(&:strip) == header
end
end
def test_canel
tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
tmux.until { |lines| lines[-2].include?('10/10') }
tmux.send_keys '123'
tmux.until { |lines| lines[-1] == '> 3' && lines[-2].include?('1/10') }
tmux.send_keys 'C-y', 'C-y'
tmux.until { |lines| lines[-1] == '> 311' }
tmux.send_keys 2
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 2
tmux.prepare
end
def test_margin
tmux.send_keys "yes | head -1000 | #{fzf "--margin 5,3"}", :Enter
tmux.until { |lines| lines[4] == '' && lines[5] == ' y' }
tmux.send_keys :Enter
end
def test_margin_reverse
tmux.send_keys "seq 1000 | #{fzf "--margin 7,5 --reverse"}", :Enter
tmux.until { |lines| lines[1 + 7] == ' 1000/1000' }
tmux.send_keys :Enter
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path
@@ -651,12 +759,20 @@ module TestShell
@tmux.kill @tmux.kill
end end
def set_var name, val
tmux.prepare
tmux.send_keys "export #{name}='#{val}'", :Enter
tmux.prepare
end
def test_ctrl_t def test_ctrl_t
tmux.prepare tmux.prepare
tmux.send_keys 'C-t', pane: 0 tmux.send_keys 'C-t', pane: 0
lines = tmux.until(1) { |lines| lines.item_count > 1 } lines = tmux.until(1) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 1 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.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
@@ -665,11 +781,23 @@ module TestShell
tmux.send_keys 'C-t', pane: 0 tmux.send_keys 'C-t', pane: 0
lines = tmux.until(0) { |lines| lines.item_count > 1 } lines = tmux.until(0) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 0 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.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c', 'C-d' tmux.send_keys 'C-c', 'C-d'
end end
def test_ctrl_t_command
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' }
end
def test_alt_c def test_alt_c
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c, pane: 0 tmux.send_keys :Escape, :c, pane: 0
@@ -710,7 +838,9 @@ module CompletionTest
tmux.prepare tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab
tmux.until(1) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/10') &&
@@ -842,6 +972,12 @@ class TestFish < TestBase
tmux.until { |lines| lines.empty? } tmux.until { |lines| lines.empty? }
end end
def set_var name, val
tmux.prepare
tmux.send_keys "set -g #{name} '#{val}'", :Enter
tmux.prepare
end
def setup def setup
super super
@tmux = Tmux.new :fish @tmux = Tmux.new :fish