m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 06:43:47 -05:00

Compare commits

...

40 Commits

Author SHA1 Message Date
Junegunn Choi
d85a69a709 0.16.3 2017-01-30 01:53:17 +09:00
Junegunn Choi
a425e96fb2 [vim] g:fzf_prefer_tmux for choosing fzf-tmux over --height
https://github.com/junegunn/fzf.vim/issues/296
2017-01-30 01:47:30 +09:00
Junegunn Choi
7763fdf6ba Update man pages 2017-01-30 01:27:12 +09:00
Junegunn Choi
dd156b59fc Fix display issues with execute action
- Move cursor to the top-left corner when starting a command in
  alternate screen
- Fix cursor position when returning to alternate screen when fzf is
  running in full screen mode
2017-01-30 01:08:07 +09:00
Junegunn Choi
36dceecd58 Add support for ctrl-space key
Close #825
2017-01-28 02:54:47 +09:00
Junegunn Choi
421b9b271a Add execute-silent action
Close #823
2017-01-27 18:56:41 +09:00
Junegunn Choi
ed57dcb924 Extend placeholder expression for multiple selections
Close #788
2017-01-27 16:38:42 +09:00
Junegunn Choi
95c77bfb98 Use --bind instead of --toggle-sort
Related #822
2017-01-26 11:54:08 +09:00
Junegunn Choi
2e3e721344 Merge branch 'devel' 2017-01-26 11:52:24 +09:00
Junegunn Choi
da2c28d5c2 Add --read0 and --print0 to --help output
Close #822
2017-01-26 11:41:20 +09:00
Junegunn Choi
dbddee9de9 [fish] Add toggle-sort back to CTRL-R (#759) 2017-01-25 10:21:14 +09:00
Junegunn Choi
8731d75607 Recalculate the width of trimmed line
Close #821
2017-01-25 02:39:49 +09:00
Junegunn Choi
f2ce233a6d 0.16.2 2017-01-24 00:37:47 +09:00
Junegunn Choi
6a75e30941 Allow invisible preview window (--preview-window 0)
Close #820
2017-01-24 00:23:16 +09:00
Junegunn Choi
a3244c4892 Delete every line below the cursor 2017-01-23 22:07:18 +09:00
Junegunn Choi
a5ad8fd3bd Minor refactoring 2017-01-23 12:55:13 +09:00
Junegunn Choi
deccdb1ec5 Cursor postition response can be preceded by user key strokes 2017-01-23 12:55:11 +09:00
Junegunn Choi
12a43b5e62 Disable mouse if failed to query cursor position 2017-01-23 12:55:04 +09:00
Junegunn Choi
e1291aa6d2 Fix make deps to see the right git dir 2017-01-23 12:10:43 +09:00
Junegunn Choi
bb26f32ac7 Allow build on OpenBSD/FreeBSD/Android
Close #497
2017-01-22 18:51:04 +09:00
Junegunn Choi
4d928001b8 Update release script to upload assets in parallel 2017-01-22 18:33:30 +09:00
Junegunn Choi
c4baa6a10c Update man page: 24-bit color 2017-01-22 18:33:03 +09:00
Junegunn Choi
71dec3dc5e Fix bug where screen is not properly cleared on toggle-preview 2017-01-22 17:43:27 +09:00
Junegunn Choi
e5017c0431 Remove unnecesasry test case 2017-01-22 17:41:47 +09:00
Junegunn Choi
cbb5134874 [vim] Use 24-bit colors if termguicolors is set 2017-01-22 14:40:30 +09:00
Junegunn Choi
ff248d566d Drop ncurses dependency
Close #818
2017-01-22 14:13:37 +09:00
Junegunn Choi
6ccc12c332 Use alternate screen if --height needs the entire screen
- Remove unnecessary scrolling
- Allow us to use `--height 100%` under Neovim terminal for 24-bit colors

Related:
- #789
- https://github.com/neovim/neovim/issues/4151
2017-01-22 05:26:38 +09:00
Junegunn Choi
2a669e9a17 Clear lines even when background color is not set
Also revert the workaround in Vim plugin introduced in fa7c897.

Related: #814
2017-01-22 03:19:50 +09:00
Junegunn Choi
5130abe76f Merge branch 'master' into devel 2017-01-22 03:10:06 +09:00
Junegunn Choi
fa7c8977a8 [vim] tput el to clear the last line
Close #814

Not grouping commands to avoid errors on non-standard shells.
2017-01-22 03:03:26 +09:00
Junegunn Choi
24fa183297 make deps 2017-01-22 02:54:19 +09:00
Junegunn Choi
131aa5dd15 Composable actions in --bind
Close #816
2017-01-22 02:32:49 +09:00
Junegunn Choi
a06ccc928f Fix flakies 2017-01-21 04:17:51 +09:00
Junegunn Choi
d09ad13208 [zsh] Workaround trailing esacped space bug in go-shellwords
https://github.com/mattn/go-shellwords/issues/3

Close #812
2017-01-21 03:59:36 +09:00
Junegunn Choi
8ac37d5927 [shell] Do not override --reverse in CTRL-R
Close #807
2017-01-17 18:09:29 +09:00
Junegunn Choi
7ef0e50507 [bash/zsh] Remove unused --reverse in CTRL-R binding
Related #807
2017-01-17 11:58:25 +09:00
Junegunn Choi
62ab8ece5e 0.16.1 2017-01-16 12:27:40 +09:00
Junegunn Choi
8e2e63f9b9 Propertly fill window with background color
Close #805
2017-01-16 12:27:32 +09:00
Junegunn Choi
f96173cbe4 Add -L flag to the default find command
Close #781
2017-01-16 12:01:58 +09:00
Amos Bird
11015df52f Add half-page-{up,down} actions (#784) 2017-01-16 11:58:13 +09:00
26 changed files with 900 additions and 473 deletions

View File

@@ -1,6 +1,38 @@
CHANGELOG CHANGELOG
========= =========
0.16.3
------
- Fixed a bug where fzf incorrectly display the lines when straddling tab
characters are trimmed
- Placeholder expression used in `--preview` and `execute` action can
optionally take `+` flag to be used with multiple selections
- e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'`
- Added `execute-silent` action for executing a command silently without
switching to the alternate screen. This is useful when the process is
short-lived and you're not interested in its output.
- e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'`
- `ctrl-space` is allowed in `--bind`
0.16.2
------
- Dropped ncurses dependency
- Binaries for freebsd, openbsd, arm5, arm6, arm7, and arm8
- Official 24-bit color support
- Added support for composite actions in `--bind`. Multiple actions can be
chained using `+` separator.
- e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'`
- `--preview-window` with size 0 is allowed. This is used to make fzf execute
preview command in the background without displaying the result.
- Minor bug fixes and improvements
0.16.1
------
- Fixed `--height` option to properly fill the window with the background
color
- Added `half-page-up` and `half-page-down` actions
- Added `-L` flag to the default find command
0.16.0 0.16.0
------ ------
- *Added `--height HEIGHT[%]` option* - *Added `--height HEIGHT[%]` option*

18
install
View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.16.0 version=0.16.3
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -160,10 +160,18 @@ archi=$(uname -sm)
binary_available=1 binary_available=1
binary_error="" binary_error=""
case "$archi" in case "$archi" in
Darwin\ x86_64) download fzf-$version-darwin_${binary_arch:-amd64} ;; Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64} ;;
Darwin\ i*86) download fzf-$version-darwin_${binary_arch:-386} ;; Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386} ;;
Linux\ x86_64) download fzf-$version-linux_${binary_arch:-amd64} ;; Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64} ;;
Linux\ i*86) download fzf-$version-linux_${binary_arch:-386} ;; Linux\ *86) download fzf-$version-linux_${binary_arch:-386} ;;
Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5} ;;
Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6} ;;
Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7} ;;
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8} ;;
FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64} ;;
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386} ;;
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64} ;;
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386} ;;
*) binary_available=0 binary_error=1 ;; *) binary_available=0 binary_error=1 ;;
esac esac

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-tmux 1 "Jan 2017" "fzf 0.16.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Jan 2017" "fzf 0.16.3" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -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 "Jan 2017" "fzf 0.16.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jan 2017" "fzf 0.16.3" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -209,8 +209,7 @@ Number of spaces for a tab character (default: 8)
Color configuration. The name of the base color scheme is followed by custom Color configuration. The name of the base color scheme is followed by custom
color mappings. Ansi color code of -1 denotes terminal default color mappings. Ansi color code of -1 denotes terminal default
foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
format, but the support for 24-bit colors is experimental and only works when format.
\fB--height\fR option is used.
.RS .RS
e.g. \fBfzf --color=bg+:24\fR e.g. \fBfzf --color=bg+:24\fR
@@ -263,13 +262,21 @@ Execute the given command for the current line and display the result on the
preview window. \fB{}\fR in the command is the placeholder that is replaced to preview window. \fB{}\fR in the command is the placeholder that is replaced to
the single-quoted string of the current line. To transform the replacement the single-quoted string of the current line. To transform the replacement
string, specify field index expressions between the braces (See \fBFIELD INDEX string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current EXPRESSION\fR for the details).
query string.
.RS .RS
e.g. \fBfzf --preview="head -$LINES {}"\fR e.g. \fBfzf --preview="head -$LINES {}"\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR \fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection
was made) individually quoted.
e.g. \fBfzf --multi --preview="head -10 {+}"\fR
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
Also, \fB{q}\fR is replaced to the current query string.
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
.RE .RE
.TP .TP
@@ -279,6 +286,9 @@ Determine the layout of the preview window. If the argument ends with
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default. \fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
Line wrap can be enabled with \fB:wrap\fR flag. Line wrap can be enabled with \fB:wrap\fR flag.
If size is given as 0, preview window will not be visible, but fzf will still
execute the command in the background.
.RS .RS
.B POSITION: (default: right) .B POSITION: (default: right)
\fBup \fBup
@@ -321,10 +331,10 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE .RE
.TP .TP
.B "--read0" .B "--read0"
Read input delimited by ASCII NUL character instead of newline character Read input delimited by ASCII NUL characters instead of newline characters
.TP .TP
.B "--print0" .B "--print0"
Print output delimited by ASCII NUL character instead of newline character Print output delimited by ASCII NUL characters instead of newline characters
.TP .TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
@@ -416,6 +426,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.B AVAILABLE KEYS: (SYNONYMS) .B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
\fIctrl-space\fR
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
\fIalt-[0-9]\fR \fIalt-[0-9]\fR
\fIf[1-12]\fR \fIf[1-12]\fR
@@ -459,7 +470,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\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
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute-multi(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details)
\fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
@@ -470,6 +482,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR \fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
\fBpreview-down\fR \fBpreview-down\fR
\fBpreview-up\fR \fBpreview-up\fR
\fBpreview-page-down\fR \fBpreview-page-down\fR
@@ -479,17 +493,21 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBselect-all\fR \fBselect-all\fR
\fBtoggle\fR \fBtoggle\fR
\fBtoggle-all\fR \fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR) \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR) \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) \fBtoggle-sort\fR
\fBtoggle-up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR \fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR \fByank\fR \fIctrl-y\fR
Multiple actions can be chained using \fB+\fR separator.
\fBfzf --bind 'ctrl-a:select-all+accept'\fR
With \fBexecute(...)\fR action, you can execute arbitrary commands without With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows. binding \fBenter\fR key to \fBless\fR command like follows.
@@ -522,10 +540,12 @@ the closing character. The catch is that it should be the last one in the
comma-separated list of key-action pairs. comma-separated list of key-action pairs.
.RE .RE
\fBexecute-multi(...)\fR is an alternative action that executes the command fzf switches to the alternate screen when executing a command. However, if the
with the selected entries when multi-select is enabled (\fB--multi\fR). With command is expected to complete quickly, and you are not interested in its
this action, \fB{}\fR is replaced with the quoted strings of the selected output, you might want to use \fBexecute-silent\fR instead, which silently
entries separated by spaces. executes the command without the switching. Note that fzf will not be
responsible until the command is complete. For asynchronous execution, start
your command as a background process (i.e. appending \fB&\fR).
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2016 Junegunn Choi " Copyright (c) 2017 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -167,9 +167,12 @@ function! s:common_sink(action, lines) abort
endfunction endfunction
function! s:get_color(attr, ...) function! s:get_color(attr, ...)
let gui = has('termguicolors') && &termguicolors
let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000 for group in a:000
let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm') let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam)
if code =~ '^[0-9]\+$' if code =~? pat
return code return code
endif endif
endfor endfor
@@ -298,23 +301,28 @@ try
let prefix = '' let prefix = ''
endif endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
let use_height = has_key(dict, 'down') && let use_height = has_key(dict, 'down') &&
\ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) && \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) &&
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) let use_term = has('nvim')
let term = has('nvim') && !tmux let use_tmux = (!use_height && !use_term || prefer_tmux) && s:tmux_enabled() && s:splittable(dict)
if prefer_tmux && use_tmux
let use_height = 0
let use_term = 0
endif
if use_height if use_height
let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict)
elseif term elseif use_term
let optstr .= ' --no-height' let optstr .= ' --no-height'
endif endif
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if term if use_term
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
let lines = tmux ? s:execute_tmux(dict, command, temps) let lines = use_tmux ? s:execute_tmux(dict, command, temps)
\ : s:execute(dict, command, use_height, temps) \ : s:execute(dict, command, use_height, temps)
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines

View File

@@ -56,7 +56,7 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
command grep '^ *[0-9]') && command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"

View File

@@ -45,7 +45,7 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS +m" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
history | eval (__fzfcmd) -q '(commandline)' | read -l result history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result and commandline -- $result
end end

View File

@@ -55,7 +55,7 @@ fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst pipefail 2> /dev/null setopt localoptions noglobsubst pipefail 2> /dev/null
selected=( $(fc -l 1 | selected=( $(fc -l 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) )
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -15,18 +15,35 @@ SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src
DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM7 := fzf-$(GOOS)_arm7
BINARYARM8 := fzf-$(GOOS)_arm8
VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ") VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ")
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386 RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5
RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8
export GOPATH export GOPATH
# https://en.wikipedia.org/wiki/Uname
UNAME_M := $(shell uname -m) UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),x86_64) ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i686) else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32) BINARY := $(BINARY32)
else ifeq ($(UNAME_M),i386)
BINARY := $(BINARY32)
else ifeq ($(UNAME_M),armv5l)
BINARY := $(BINARYARM5)
else ifeq ($(UNAME_M),armv6l)
BINARY := $(BINARYARM6)
else ifeq ($(UNAME_M),armv7l)
BINARY := $(BINARYARM7)
else else
$(error "Build on $(UNAME_M) is not supported, yet.") $(error "Build on $(UNAME_M) is not supported, yet.")
endif endif
@@ -35,22 +52,39 @@ all: fzf/$(BINARY)
ifeq ($(GOOS),windows) ifeq ($(GOOS),windows)
release: fzf/$(BINARY32) fzf/$(BINARY64) release: fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe
cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe && \ cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe
rm -f $(RELEASE32).exe $(RELEASE64).exe cd fzf && rm -f $(RELEASE32).exe $(RELEASE64).exe
else ifeq ($(GOOS),linux)
release: fzf/$(BINARY32) fzf/$(BINARY64) fzf/$(BINARYARM5) fzf/$(BINARYARM6) fzf/$(BINARYARM7) fzf/$(BINARYARM8)
cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64)
cd fzf && cp $(BINARYARM5) $(RELEASEARM5) && tar -czf $(RELEASEARM5).tgz $(RELEASEARM5)
cd fzf && cp $(BINARYARM6) $(RELEASEARM6) && tar -czf $(RELEASEARM6).tgz $(RELEASEARM6)
cd fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7)
cd fzf && cp $(BINARYARM8) $(RELEASEARM8) && tar -czf $(RELEASEARM8).tgz $(RELEASEARM8)
cd fzf && rm -f $(RELEASE32) $(RELEASE64) $(RELEASEARM5) $(RELEASEARM6) $(RELEASEARM7) $(RELEASEARM8)
else else
release: test fzf/$(BINARY32) fzf/$(BINARY64) release: fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64)
rm -f $(RELEASE32) $(RELEASE64) cd fzf && rm -f $(RELEASE32) $(RELEASE64)
endif endif
release-all: clean test
GOOS=darwin make release
GOOS=linux make release
GOOS=freebsd make release
GOOS=openbsd make release
GOOS=windows make release
$(SRCDIR): $(SRCDIR):
mkdir -p $(shell dirname $(SRCDIR)) mkdir -p $(shell dirname $(SRCDIR))
ln -s $(ROOTDIR) $(SRCDIR) ln -s $(ROOTDIR) $(SRCDIR)
deps: $(SRCDIR) $(SOURCES) deps: $(SRCDIR) $(SOURCES)
cd $(SRCDIR) && go get -tags "$(TAGS)" cd $(SRCDIR) && go get -tags "$(TAGS)"
./deps
android-build: $(SRCDIR) android-build: $(SRCDIR)
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
@@ -59,7 +93,7 @@ android-build: $(SRCDIR)
rm -f $(RELEASEARM7) rm -f $(RELEASEARM7)
test: deps test: deps
SHELL=/bin/sh GOOS=$(GOOS) go test -v -tags "$(TAGS)" ./... SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" ./...
install: $(BINDIR)/fzf install: $(BINDIR)/fzf
@@ -70,10 +104,23 @@ clean:
cd fzf && rm -f fzf-* cd fzf && rm -f fzf-*
fzf/$(BINARY32): deps fzf/$(BINARY32): deps
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32) cd fzf && GOARCH=386 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32)
fzf/$(BINARY64): deps fzf/$(BINARY64): deps
cd fzf && go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64) cd fzf && GOARCH=amd64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64)
# https://github.com/golang/go/wiki/GoArm
fzf/$(BINARYARM5): deps
cd fzf && GOARCH=arm GOARM=5 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM5)
fzf/$(BINARYARM6): deps
cd fzf && GOARCH=arm GOARM=6 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM6)
fzf/$(BINARYARM7): deps
cd fzf && GOARCH=arm GOARM=7 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM7)
fzf/$(BINARYARM8): deps
cd fzf && GOARCH=arm64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM8)
$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
cp -f fzf/$(BINARY) $(BINDIR) cp -f fzf/$(BINARY) $(BINDIR)

View File

@@ -59,20 +59,31 @@ Unit tests can be run with `make test`. Integration tests are written in Ruby
script that should be run on tmux. script that should be run on tmux.
```sh ```sh
cd src
# Unit tests # Unit tests
make test make test
# Integration tests
ruby ../test/test_go.rb
# Build binary for the platform
make
# Install the executable to ../bin directory # Install the executable to ../bin directory
make install make install
# Integration tests # Make release archives
ruby ../test/test_go.rb make release
# Make release archives for all supported platforms
make release-all
``` ```
Third-party libraries used Third-party libraries used
-------------------------- --------------------------
- [ncurses][ncurses] - ~[ncurses][ncurses]~
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords)

View File

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

View File

@@ -4,5 +4,5 @@ package fzf
const ( const (
// Reader // Reader
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` defaultCommand = `find -L . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
) )

View File

@@ -3,7 +3,7 @@ Package fzf implements fzf, a command-line fuzzy finder.
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

18
src/deps Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
if [ -z "$GOPATH" ]; then
echo '$GOPATH not defined'
exit 1
fi
reset() (
cd "$GOPATH/src/$1"
export GIT_DIR="$(pwd)/.git"
[ "$(git rev-parse HEAD)" = "$2" ] ||
(git fetch && git reset --hard "$2")
)
reset github.com/junegunn/go-isatty 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
reset github.com/junegunn/go-runewidth 63c378b851290989b19ca955468386485f118c65
reset github.com/junegunn/go-shellwords 33bd8f1ebe16d6e5eb688cc885749a63059e9167
reset golang.org/x/crypto abc5fa7ad02123a41f02bf1391c9760f7586e608

View File

@@ -82,6 +82,8 @@ const usage = `usage: fzf [options]
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf --expect=KEYS Comma-separated list of keys to complete fzf
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
Environment variables Environment variables
@@ -171,8 +173,7 @@ type Options struct {
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int][]action
Execmap map[int]string
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
@@ -220,8 +221,7 @@ func defaultOptions() *Options {
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int]actionType), Keymap: make(map[int][]action),
Execmap: make(map[int]string),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
@@ -393,6 +393,8 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.AltZ + int(' ') chord = tui.AltZ + int(' ')
case "bspace", "bs": case "bspace", "bs":
chord = tui.BSpace chord = tui.BSpace
case "ctrl-space":
chord = tui.CtrlSpace
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chord = tui.AltEnter chord = tui.AltEnter
case "alt-space": case "alt-space":
@@ -578,23 +580,32 @@ func firstKey(keymap map[int]string) int {
const ( const (
escapedColon = 0 escapedColon = 0
escapedComma = 1 escapedComma = 1
escapedPlus = 2
) )
func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) { func init() {
if executeRegexp == nil {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
"(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") "(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
} }
func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
if strings.HasPrefix(src, ":execute-multi") { prefix := ":execute"
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
} else {
prefix += "-multi"
} }
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" }
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
}) })
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
idx := 0 idx := 0
for _, pairStr := range strings.Split(masked, ",") { for _, pairStr := range strings.Split(masked, ",") {
@@ -610,147 +621,174 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
key = ':' + tui.AltZ key = ':' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma { } else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + tui.AltZ key = ',' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = '+' + tui.AltZ
} else { } else {
keys := parseKeyChords(pair[0], "key name required") keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys) key = firstKey(keys)
} }
act := origPairStr[len(pair[0])+1 : len(origPairStr)] idx2 := len(pair[0]) + 1
actLower := strings.ToLower(act) specs := strings.Split(pair[1], "+")
switch actLower { actions := make([]action, 0, len(specs))
appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...)
}
prevSpec := ""
for specIndex, maskedSpec := range specs {
spec := origPairStr[idx2 : idx2+len(maskedSpec)]
idx2 += len(maskedSpec) + 1
spec = prevSpec + spec
specLower := strings.ToLower(spec)
switch specLower {
case "ignore": case "ignore":
keymap[key] = actIgnore appendAction(actIgnore)
case "beginning-of-line": case "beginning-of-line":
keymap[key] = actBeginningOfLine appendAction(actBeginningOfLine)
case "abort": case "abort":
keymap[key] = actAbort appendAction(actAbort)
case "accept": case "accept":
keymap[key] = actAccept appendAction(actAccept)
case "print-query": case "print-query":
keymap[key] = actPrintQuery appendAction(actPrintQuery)
case "backward-char": case "backward-char":
keymap[key] = actBackwardChar appendAction(actBackwardChar)
case "backward-delete-char": case "backward-delete-char":
keymap[key] = actBackwardDeleteChar appendAction(actBackwardDeleteChar)
case "backward-word": case "backward-word":
keymap[key] = actBackwardWord appendAction(actBackwardWord)
case "clear-screen": case "clear-screen":
keymap[key] = actClearScreen appendAction(actClearScreen)
case "delete-char": case "delete-char":
keymap[key] = actDeleteChar appendAction(actDeleteChar)
case "delete-char/eof": case "delete-char/eof":
keymap[key] = actDeleteCharEOF appendAction(actDeleteCharEOF)
case "end-of-line": case "end-of-line":
keymap[key] = actEndOfLine appendAction(actEndOfLine)
case "cancel": case "cancel":
keymap[key] = actCancel appendAction(actCancel)
case "forward-char": case "forward-char":
keymap[key] = actForwardChar appendAction(actForwardChar)
case "forward-word": case "forward-word":
keymap[key] = actForwardWord appendAction(actForwardWord)
case "jump": case "jump":
keymap[key] = actJump appendAction(actJump)
case "jump-accept": case "jump-accept":
keymap[key] = actJumpAccept appendAction(actJumpAccept)
case "kill-line": case "kill-line":
keymap[key] = actKillLine appendAction(actKillLine)
case "kill-word": case "kill-word":
keymap[key] = actKillWord appendAction(actKillWord)
case "unix-line-discard", "line-discard": case "unix-line-discard", "line-discard":
keymap[key] = actUnixLineDiscard appendAction(actUnixLineDiscard)
case "unix-word-rubout", "word-rubout": case "unix-word-rubout", "word-rubout":
keymap[key] = actUnixWordRubout appendAction(actUnixWordRubout)
case "yank": case "yank":
keymap[key] = actYank appendAction(actYank)
case "backward-kill-word": case "backward-kill-word":
keymap[key] = actBackwardKillWord appendAction(actBackwardKillWord)
case "toggle-down": case "toggle-down":
keymap[key] = actToggleDown appendAction(actToggle, actDown)
case "toggle-up": case "toggle-up":
keymap[key] = actToggleUp appendAction(actToggle, actUp)
case "toggle-in": case "toggle-in":
keymap[key] = actToggleIn appendAction(actToggleIn)
case "toggle-out": case "toggle-out":
keymap[key] = actToggleOut appendAction(actToggleOut)
case "toggle-all": case "toggle-all":
keymap[key] = actToggleAll appendAction(actToggleAll)
case "select-all": case "select-all":
keymap[key] = actSelectAll appendAction(actSelectAll)
case "deselect-all": case "deselect-all":
keymap[key] = actDeselectAll appendAction(actDeselectAll)
case "toggle": case "toggle":
keymap[key] = actToggle appendAction(actToggle)
case "down": case "down":
keymap[key] = actDown appendAction(actDown)
case "up": case "up":
keymap[key] = actUp appendAction(actUp)
case "page-up": case "page-up":
keymap[key] = actPageUp appendAction(actPageUp)
case "page-down": case "page-down":
keymap[key] = actPageDown appendAction(actPageDown)
case "half-page-up":
appendAction(actHalfPageUp)
case "half-page-down":
appendAction(actHalfPageDown)
case "previous-history": case "previous-history":
keymap[key] = actPreviousHistory appendAction(actPreviousHistory)
case "next-history": case "next-history":
keymap[key] = actNextHistory appendAction(actNextHistory)
case "toggle-preview": case "toggle-preview":
keymap[key] = actTogglePreview appendAction(actTogglePreview)
case "toggle-sort": case "toggle-sort":
keymap[key] = actToggleSort appendAction(actToggleSort)
case "preview-up": case "preview-up":
keymap[key] = actPreviewUp appendAction(actPreviewUp)
case "preview-down": case "preview-down":
keymap[key] = actPreviewDown appendAction(actPreviewDown)
case "preview-page-up": case "preview-page-up":
keymap[key] = actPreviewPageUp appendAction(actPreviewPageUp)
case "preview-page-down": case "preview-page-down":
keymap[key] = actPreviewPageDown appendAction(actPreviewPageDown)
default: default:
if isExecuteAction(actLower) { t := isExecuteAction(specLower)
var offset int if t == actIgnore {
if strings.HasPrefix(actLower, "execute-multi") { errorExit("unknown action: " + spec)
keymap[key] = actExecuteMulti
offset = len("execute-multi")
} else { } else {
keymap[key] = actExecute var offset int
switch t {
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
offset = len("execute-multi")
default:
offset = len("execute") offset = len("execute")
} }
if act[offset] == ':' { if spec[offset] == ':' {
execmap[key] = act[offset+1:] if specIndex == len(specs)-1 {
actions = append(actions, action{t: t, a: spec[offset+1:]})
} else { } else {
execmap[key] = act[offset+1 : len(act)-1] prevSpec = spec + "+"
continue
} }
} else { } else {
errorExit("unknown action: " + act) actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]})
} }
} }
} }
prevSpec = ""
}
keymap[key] = actions
}
} }
func isExecuteAction(str string) bool { func isExecuteAction(str string) actionType {
if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
return false if matches == nil || len(matches) != 1 {
return actIgnore
} }
b := str[len("execute")] prefix := matches[0][1]
if strings.HasPrefix(str, "execute-multi") { if len(prefix) == 0 {
if len(str) < len("execute-multi()") { prefix = matches[0][2]
return false
} }
b = str[len("execute-multi")] switch prefix {
case "execute":
return actExecute
case "execute-silent":
return actExecuteSilent
case "execute-multi":
return actExecuteMulti
} }
e := str[len(str)-1] return actIgnore
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
return true
}
return false
} }
func parseToggleSort(keymap map[int]actionType, str string) { func parseToggleSort(keymap map[int][]action, str string) {
keys := parseKeyChords(str, "key name required") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
} }
keymap[firstKey(keys)] = actToggleSort keymap[firstKey(keys)] = toActions(actToggleSort)
} }
func strLines(str string) []string { func strLines(str string) []string {
@@ -797,7 +835,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.wrap = false opts.wrap = false
tokens := strings.Split(input, ":") tokens := strings.Split(input, ":")
sizeRegex := regexp.MustCompile("^[1-9][0-9]*%?$") sizeRegex := regexp.MustCompile("^[0-9]+%?$")
for _, token := range tokens { for _, token := range tokens {
switch token { switch token {
case "hidden": case "hidden":
@@ -915,7 +953,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tiebreak": case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required")) parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
case "--color": case "--color":
spec := optionalNextString(allArgs, &i) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 { if len(spec) == 0 {
@@ -1085,7 +1123,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match { } else if match, value := optString(arg, "--bind="); match {
parseKeymap(opts.Keymap, opts.Execmap, value) parseKeymap(opts.Keymap, value)
} else if match, value := optString(arg, "--history="); match { } else if match, value := optString(arg, "--history="); match {
setHistory(value) setHistory(value)
} else if match, value := optString(arg, "--history-size="); match { } else if match, value := optString(arg, "--history-size="); match {
@@ -1141,20 +1179,22 @@ func postProcessOptions(opts *Options) {
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[tui.CtrlP]; !prs { if _, prs := opts.Keymap[tui.CtrlP]; !prs {
opts.Keymap[tui.CtrlP] = actPreviousHistory opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory)
} }
if _, prs := opts.Keymap[tui.CtrlN]; !prs { if _, prs := opts.Keymap[tui.CtrlN]; !prs {
opts.Keymap[tui.CtrlN] = actNextHistory opts.Keymap[tui.CtrlN] = toActions(actNextHistory)
} }
} }
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, act := range opts.Keymap { for key, actions := range opts.Keymap {
if act == actToggleSort { for _, act := range actions {
if act.t == actToggleSort {
opts.ToggleSort = true opts.ToggleSort = true
} }
keymap[key] = act }
keymap[key] = actions
} }
opts.Keymap = keymap opts.Keymap = keymap

View File

@@ -225,49 +225,51 @@ func TestParseKeysWithComma(t *testing.T) {
} }
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
check := func(action actionType, expected actionType) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
checkString := func(action string, expected string) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
keymap := defaultKeymap() keymap := defaultKeymap()
execmap := make(map[int]string) check := func(keyName int, arg1 string, types ...actionType) {
check(actBeginningOfLine, keymap[tui.CtrlA]) if len(keymap[keyName]) != len(types) {
parseKeymap(keymap, execmap, t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName]))
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ return
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ }
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ for idx, action := range keymap[keyName] {
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") if types[idx] != action.t {
check(actKillLine, keymap[tui.CtrlA]) t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
check(actToggleSort, keymap[tui.CtrlB]) }
check(actPageUp, keymap[tui.AltZ+'c']) }
check(actAbort, keymap[tui.AltZ+',']) if len(arg1) > 0 && keymap[keyName][0].a != arg1 {
check(actAccept, keymap[tui.AltZ+':']) t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a)
check(actPageDown, keymap[tui.AltZ]) }
check(actExecute, keymap[tui.F1]) }
check(actExecute, keymap[tui.F2]) check(tui.CtrlA, "", actBeginningOfLine)
check(actExecute, keymap[tui.F3]) parseKeymap(keymap,
check(actExecute, keymap[tui.F4]) "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
checkString("ls {}", execmap[tui.F1]) "f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
checkString("echo {}, {}, {}", execmap[tui.F2]) "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
checkString("echo '({})'", execmap[tui.F3]) "x:Execute(foo+bar),X:execute/bar+baz/"+
checkString("less {}", execmap[tui.F4]) ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA]) check(tui.CtrlA, "", actKillLine)
checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB]) check(tui.CtrlB, "", actToggleSort, actUp, actDown)
checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X']) check(tui.AltZ+'c', "", actPageUp)
check(tui.AltZ+',', "", actAbort)
check(tui.AltZ+':', "", actAccept)
check(tui.AltZ, "", actPageDown)
check(tui.F1, "ls {}", actExecute, actAbort)
check(tui.F2, "echo {}, {}, {}", actExecute)
check(tui.F3, "echo '({})'", actExecute)
check(tui.F4, "less {}", actExecute)
check(tui.AltZ+'x', "foo+bar", actExecute)
check(tui.AltZ+'X', "bar+baz", actExecute)
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute)
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, execmap, "f1:abort") parseKeymap(keymap, "f1:abort")
check(actAbort, keymap[tui.F1]) check(tui.F1, "", actAbort)
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
@@ -327,7 +329,7 @@ func TestDefaultCtrlNP(t *testing.T) {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if opts.Keymap[key] != expected { if opts.Keymap[key][0].t != expected {
t.Error() t.Error()
} }
} }

View File

@@ -24,7 +24,7 @@ import (
var placeholder *regexp.Regexp var placeholder *regexp.Regexp
func init() { func init() {
placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})") placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})")
} }
type jumpMode int type jumpMode int
@@ -72,8 +72,7 @@ type Terminal struct {
toggleSort bool toggleSort bool
delimiter Delimiter delimiter Delimiter
expect map[int]string expect map[int]string
keymap map[int]actionType keymap map[int][]action
execmap map[int]string
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
@@ -148,6 +147,11 @@ const (
reqQuit reqQuit
) )
type action struct {
t actionType
a string
}
type actionType int type actionType int
const ( const (
@@ -186,6 +190,8 @@ const (
actUp actUp
actPageUp actPageUp
actPageDown actPageDown
actHalfPageUp
actHalfPageDown
actJump actJump
actJumpAccept actJumpAccept
actPrintQuery actPrintQuery
@@ -198,57 +204,66 @@ const (
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
actExecuteMulti actExecuteSilent
actExecuteMulti // Deprecated
) )
func defaultKeymap() map[int]actionType { func toActions(types ...actionType) []action {
keymap := make(map[int]actionType) actions := make([]action, len(types))
keymap[tui.Invalid] = actInvalid for idx, t := range types {
keymap[tui.Resize] = actClearScreen actions[idx] = action{t: t, a: ""}
keymap[tui.CtrlA] = actBeginningOfLine }
keymap[tui.CtrlB] = actBackwardChar return actions
keymap[tui.CtrlC] = actAbort }
keymap[tui.CtrlG] = actAbort
keymap[tui.CtrlQ] = actAbort
keymap[tui.ESC] = actAbort
keymap[tui.CtrlD] = actDeleteCharEOF
keymap[tui.CtrlE] = actEndOfLine
keymap[tui.CtrlF] = actForwardChar
keymap[tui.CtrlH] = actBackwardDeleteChar
keymap[tui.BSpace] = actBackwardDeleteChar
keymap[tui.Tab] = actToggleDown
keymap[tui.BTab] = actToggleUp
keymap[tui.CtrlJ] = actDown
keymap[tui.CtrlK] = actUp
keymap[tui.CtrlL] = actClearScreen
keymap[tui.CtrlM] = actAccept
keymap[tui.CtrlN] = actDown
keymap[tui.CtrlP] = actUp
keymap[tui.CtrlU] = actUnixLineDiscard
keymap[tui.CtrlW] = actUnixWordRubout
keymap[tui.CtrlY] = actYank
keymap[tui.AltB] = actBackwardWord func defaultKeymap() map[int][]action {
keymap[tui.SLeft] = actBackwardWord keymap := make(map[int][]action)
keymap[tui.AltF] = actForwardWord keymap[tui.Invalid] = toActions(actInvalid)
keymap[tui.SRight] = actForwardWord keymap[tui.Resize] = toActions(actClearScreen)
keymap[tui.AltD] = actKillWord keymap[tui.CtrlA] = toActions(actBeginningOfLine)
keymap[tui.AltBS] = actBackwardKillWord keymap[tui.CtrlB] = toActions(actBackwardChar)
keymap[tui.CtrlC] = toActions(actAbort)
keymap[tui.CtrlG] = toActions(actAbort)
keymap[tui.CtrlQ] = toActions(actAbort)
keymap[tui.ESC] = toActions(actAbort)
keymap[tui.CtrlD] = toActions(actDeleteCharEOF)
keymap[tui.CtrlE] = toActions(actEndOfLine)
keymap[tui.CtrlF] = toActions(actForwardChar)
keymap[tui.CtrlH] = toActions(actBackwardDeleteChar)
keymap[tui.BSpace] = toActions(actBackwardDeleteChar)
keymap[tui.Tab] = toActions(actToggleDown)
keymap[tui.BTab] = toActions(actToggleUp)
keymap[tui.CtrlJ] = toActions(actDown)
keymap[tui.CtrlK] = toActions(actUp)
keymap[tui.CtrlL] = toActions(actClearScreen)
keymap[tui.CtrlM] = toActions(actAccept)
keymap[tui.CtrlN] = toActions(actDown)
keymap[tui.CtrlP] = toActions(actUp)
keymap[tui.CtrlU] = toActions(actUnixLineDiscard)
keymap[tui.CtrlW] = toActions(actUnixWordRubout)
keymap[tui.CtrlY] = toActions(actYank)
keymap[tui.Up] = actUp keymap[tui.AltB] = toActions(actBackwardWord)
keymap[tui.Down] = actDown keymap[tui.SLeft] = toActions(actBackwardWord)
keymap[tui.Left] = actBackwardChar keymap[tui.AltF] = toActions(actForwardWord)
keymap[tui.Right] = actForwardChar keymap[tui.SRight] = toActions(actForwardWord)
keymap[tui.AltD] = toActions(actKillWord)
keymap[tui.AltBS] = toActions(actBackwardKillWord)
keymap[tui.Home] = actBeginningOfLine keymap[tui.Up] = toActions(actUp)
keymap[tui.End] = actEndOfLine keymap[tui.Down] = toActions(actDown)
keymap[tui.Del] = actDeleteChar keymap[tui.Left] = toActions(actBackwardChar)
keymap[tui.PgUp] = actPageUp keymap[tui.Right] = toActions(actForwardChar)
keymap[tui.PgDn] = actPageDown
keymap[tui.Rune] = actRune keymap[tui.Home] = toActions(actBeginningOfLine)
keymap[tui.Mouse] = actMouse keymap[tui.End] = toActions(actEndOfLine)
keymap[tui.DoubleClick] = actAccept keymap[tui.Del] = toActions(actDeleteChar)
keymap[tui.PgUp] = toActions(actPageUp)
keymap[tui.PgDn] = toActions(actPageDown)
keymap[tui.Rune] = toActions(actRune)
keymap[tui.Mouse] = toActions(actMouse)
keymap[tui.DoubleClick] = toActions(actAccept)
return keymap return keymap
} }
@@ -291,8 +306,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
return util.Max(maxHeight, minHeight) return util.Max(maxHeight, minHeight)
} }
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc)
} else { } else if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else {
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop,
func(h int) int { return h })
} }
wordRubout := "[^[:alnum:]][[:alnum:]]" wordRubout := "[^[:alnum:]][[:alnum:]]"
wordNext := "[[:alnum:]][^[:alnum:]]|(.$)" wordNext := "[[:alnum:]][^[:alnum:]]|(.$)"
@@ -321,7 +339,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
execmap: opts.Execmap,
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
@@ -420,9 +437,9 @@ func (t *Terminal) output() bool {
} }
found := len(t.selected) > 0 found := len(t.selected) > 0
if !found { if !found {
cnt := t.merger.Length() current := t.currentItem()
if cnt > 0 && cnt > t.cy { if current != nil {
t.printer(t.current()) t.printer(current.AsString(t.ansi))
found = true found = true
} }
} else { } else {
@@ -493,9 +510,11 @@ func (t *Terminal) resizeWindows() {
} }
} }
} }
previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0
minAreaWidth := minWidth minAreaWidth := minWidth
minAreaHeight := minHeight minAreaHeight := minHeight
if t.isPreviewEnabled() { if previewVisible {
switch t.preview.position { switch t.preview.position {
case posUp, posDown: case posUp, posDown:
minAreaHeight *= 2 minAreaHeight *= 2
@@ -515,7 +534,7 @@ func (t *Terminal) resizeWindows() {
width := screenWidth - marginInt[1] - marginInt[3] width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2] height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() { if previewVisible {
createPreviewWindow := func(y int, x int, w int, h int) { createPreviewWindow := func(y int, x int, w int, h int) {
t.bwindow = t.tui.NewWindow(y, x, w, h, true) t.bwindow = t.tui.NewWindow(y, x, w, h, true)
pwidth := w - 4 pwidth := w - 4
@@ -556,6 +575,11 @@ func (t *Terminal) resizeWindows() {
width, width,
height, false) height, false)
} }
if !t.tui.IsOptimized() && t.theme != nil && t.theme.HasBg() {
for i := 0; i < t.window.Height(); i++ {
t.window.MoveAndClear(i, 0)
}
}
} }
func (t *Terminal) move(y int, x int, clear bool) { func (t *Terminal) move(y int, x int, clear bool) {
@@ -824,6 +848,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
} }
} }
displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
} }
var index int32 var index int32
@@ -868,7 +893,7 @@ func numLinesMax(str string, max int) int {
} }
func (t *Terminal) printPreview() { func (t *Terminal) printPreview() {
if !t.isPreviewEnabled() { if !t.hasPreviewWindow() {
return return
} }
t.pwindow.Erase() t.pwindow.Erase()
@@ -953,7 +978,7 @@ func (t *Terminal) printAll() {
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
if !t.suppress { if !t.suppress {
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window}) t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window})
} else { } else {
t.tui.RefreshWindows([]tui.Window{t.window}) t.tui.RefreshWindows([]tui.Window{t.window})
@@ -1020,7 +1045,27 @@ func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
} }
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string { func hasPlusFlag(template string) bool {
for _, match := range placeholder.FindAllString(template, -1) {
if match[0] == '\\' {
continue
}
if match[1] == '+' {
return true
}
}
return false
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string {
current := allItems[:1]
selected := allItems[1:]
if current[0] == nil {
current = []*Item{}
}
if selected[0] == nil {
selected = []*Item{}
}
return placeholder.ReplaceAllStringFunc(template, func(match string) string { return placeholder.ReplaceAllStringFunc(template, func(match string) string {
// Escaped pattern // Escaped pattern
if match[0] == '\\' { if match[0] == '\\' {
@@ -1032,6 +1077,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
return quoteEntry(query) return quoteEntry(query)
} }
plusFlag := forcePlus
if match[1] == '+' {
match = "{" + match[2:]
plusFlag = true
}
items := current
if plusFlag {
items = selected
}
replacements := make([]string, len(items)) replacements := make([]string, len(items))
if match == "{}" { if match == "{}" {
@@ -1072,34 +1127,60 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
}) })
} }
func (t *Terminal) executeCommand(template string, items []*Item) { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) {
command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items) valid, list := t.buildPlusList(template, forcePlus)
if !valid {
return
}
command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
if !background {
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
t.tui.Pause() t.tui.Pause()
cmd.Run() cmd.Run()
if t.tui.Resume() { if t.tui.Resume() {
t.tui.Clear()
t.printAll() t.printAll()
} }
t.refresh() t.refresh()
} else {
cmd.Run()
}
} }
func (t *Terminal) hasPreviewWindow() bool { func (t *Terminal) hasPreviewer() bool {
return t.previewBox != nil return t.previewBox != nil
} }
func (t *Terminal) isPreviewEnabled() bool { func (t *Terminal) isPreviewEnabled() bool {
return t.previewBox != nil && t.previewer.enabled return t.hasPreviewer() && t.previewer.enabled
}
func (t *Terminal) hasPreviewWindow() bool {
return t.pwindow != nil && t.isPreviewEnabled()
} }
func (t *Terminal) currentItem() *Item { func (t *Terminal) currentItem() *Item {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
return t.merger.Get(t.cy).item return t.merger.Get(t.cy).item
} }
return nil
}
func (t *Terminal) current() string { func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
return t.currentItem().AsString(t.ansi) current := t.currentItem()
if !forcePlus && !hasPlusFlag(template) || len(t.selected) == 0 {
return current != nil, []*Item{current, current}
}
sels := make([]*Item, len(t.selected)+1)
sels[0] = current
for i, sel := range t.sortSelected() {
sels[i+1] = sel.item
}
return true, sels
} }
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
@@ -1153,22 +1234,23 @@ func (t *Terminal) Loop() {
}() }()
} }
if t.hasPreviewWindow() { if t.hasPreviewer() {
go func() { go func() {
for { for {
var request *Item var request []*Item
t.previewBox.Wait(func(events *util.Events) { t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events { for req, value := range *events {
switch req { switch req {
case reqPreviewEnqueue: case reqPreviewEnqueue:
request = value.(*Item) request = value.([]*Item)
} }
} }
events.Clear() events.Clear()
}) })
if request != nil { // We don't display preview window if no match
if request[0] != nil {
command := replacePlaceholder(t.preview.command, command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, string(t.input), []*Item{request}) t.ansi, t.delimiter, false, string(t.input), request)
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput() out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out)) t.reqBox.Set(reqPreviewDisplay, string(out))
@@ -1204,17 +1286,12 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
cnt := t.merger.Length() currentFocus := t.currentItem()
var currentFocus *Item
if cnt > 0 && cnt > t.cy {
currentFocus = t.currentItem()
} else {
currentFocus = nil
}
if currentFocus != focused { if currentFocus != focused {
focused = currentFocus focused = currentFocus
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
t.previewBox.Set(reqPreviewEnqueue, focused) _, list := t.buildPlusList(t.preview.command, false)
t.previewBox.Set(reqPreviewEnqueue, list)
} }
} }
case reqJump: case reqJump:
@@ -1307,34 +1384,35 @@ func (t *Terminal) Loop() {
} }
} }
var doAction func(actionType, int) bool var doAction func(action, int) bool
doAction = func(action actionType, mapkey int) bool { doActions := func(actions []action, mapkey int) bool {
switch action { for _, action := range actions {
if !doAction(action, mapkey) {
return false
}
}
return true
}
doAction = func(a action, mapkey int) bool {
switch a.t {
case actIgnore: case actIgnore:
case actExecute: case actExecute, actExecuteSilent:
if t.cy >= 0 && t.cy < t.merger.Length() { t.executeCommand(a.a, false, a.t == actExecuteSilent)
t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()})
}
case actExecuteMulti: case actExecuteMulti:
if len(t.selected) > 0 { t.executeCommand(a.a, true, false)
sels := make([]*Item, len(t.selected))
for i, sel := range t.sortSelected() {
sels[i] = sel.item
}
t.executeCommand(t.execmap[mapkey], sels)
} else {
return doAction(actExecute, mapkey)
}
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actTogglePreview: case actTogglePreview:
if t.hasPreviewWindow() { if t.hasPreviewer() {
t.previewer.enabled = !t.previewer.enabled t.previewer.enabled = !t.previewer.enabled
t.tui.Clear()
t.resizeWindows() t.resizeWindows()
cnt := t.merger.Length() if t.previewer.enabled {
if t.previewer.enabled && cnt > 0 && cnt > t.cy { valid, list := t.buildPlusList(t.preview.command, false)
t.previewBox.Set(reqPreviewEnqueue, t.currentItem()) if valid {
t.previewBox.Set(reqPreviewEnqueue, list)
}
} }
req(reqList, reqInfo, reqHeader) req(reqList, reqInfo, reqHeader)
} }
@@ -1344,19 +1422,19 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actPreviewUp: case actPreviewUp:
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
scrollPreview(-1) scrollPreview(-1)
} }
case actPreviewDown: case actPreviewDown:
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
scrollPreview(1) scrollPreview(1)
} }
case actPreviewPageUp: case actPreviewPageUp:
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
scrollPreview(-t.pwindow.Height()) scrollPreview(-t.pwindow.Height())
} }
case actPreviewPageDown: case actPreviewPageDown:
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
scrollPreview(t.pwindow.Height()) scrollPreview(t.pwindow.Height())
} }
case actBeginningOfLine: case actBeginningOfLine:
@@ -1424,14 +1502,14 @@ func (t *Terminal) Loop() {
} }
case actToggleIn: case actToggleIn:
if t.reverse { if t.reverse {
return doAction(actToggleUp, mapkey) return doAction(action{t: actToggleUp}, mapkey)
} }
return doAction(actToggleDown, mapkey) return doAction(action{t: actToggleDown}, mapkey)
case actToggleOut: case actToggleOut:
if t.reverse { if t.reverse {
return doAction(actToggleDown, mapkey) return doAction(action{t: actToggleDown}, mapkey)
} }
return doAction(actToggleUp, mapkey) return doAction(action{t: actToggleUp}, mapkey)
case actToggleDown: case actToggleDown:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
@@ -1478,6 +1556,12 @@ func (t *Terminal) Loop() {
case actPageDown: case actPageDown:
t.vmove(-(t.maxItems() - 1)) t.vmove(-(t.maxItems() - 1))
req(reqList) req(reqList)
case actHalfPageUp:
t.vmove(t.maxItems() / 2)
req(reqList)
case actHalfPageDown:
t.vmove(-(t.maxItems() / 2))
req(reqList)
case actJump: case actJump:
t.jumping = jumpEnabled t.jumping = jumpEnabled
req(reqJump) req(reqJump)
@@ -1527,7 +1611,7 @@ func (t *Terminal) Loop() {
} }
t.vmove(me.S) t.vmove(me.S)
req(reqList) req(reqList)
} else if t.isPreviewEnabled() && t.pwindow.Enclose(my, mx) { } else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) {
scrollPreview(-me.S) scrollPreview(-me.S)
} }
} else if t.window.Enclose(my, mx) { } else if t.window.Enclose(my, mx) {
@@ -1545,7 +1629,7 @@ func (t *Terminal) Loop() {
// 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() {
return doAction(t.keymap[tui.DoubleClick], tui.DoubleClick) return doActions(t.keymap[tui.DoubleClick], tui.DoubleClick)
} }
} }
} else if me.Down { } else if me.Down {
@@ -1567,14 +1651,14 @@ func (t *Terminal) Loop() {
changed := false changed := false
mapkey := event.Type mapkey := event.Type
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
action := t.keymap[mapkey] actions := t.keymap[mapkey]
if mapkey == tui.Rune { if mapkey == tui.Rune {
mapkey = int(event.Char) + int(tui.AltZ) mapkey = int(event.Char) + int(tui.AltZ)
if act, prs := t.keymap[mapkey]; prs { if act, prs := t.keymap[mapkey]; prs {
action = act actions = act
} }
} }
if !doAction(action, mapkey) { if !doActions(actions, mapkey) {
continue continue
} }
// Truncate the query if it's too long // Truncate the query if it's too long

View File

@@ -14,8 +14,10 @@ func newItem(str string) *Item {
} }
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")} item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1}
items2 := []*Item{ items2 := []*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
@@ -27,47 +29,65 @@ func TestReplacePlaceholder(t *testing.T) {
} }
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1) result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
// {}, strip ansi // {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz'") check("echo ' foo'\\''bar baz'")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2) result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'") check("echo 'foo'\\''bar baz'")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1) result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
// {..}, strip leading whitespaces, strip ansi // {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar baz'") check("echo 'foo'\\''bar baz'")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz' 'query'") check("echo ' foo'\\''bar baz' 'query'")
// {q}, multiple items // {q}, multiple items
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2) result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items2) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil})
check("echo /")
// No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'")
// String delimiter // String delimiter
delim := "'" delim := "'"
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1) result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
// Regex delimiter // Regex delimiter
regex := regexp.MustCompile("[oa]+") regex := regexp.MustCompile("[oa]+")
// foo'bar baz // foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1) result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
} }

45
src/tui/dummy.go Normal file
View File

@@ -0,0 +1,45 @@
// +build !ncurses
// +build !tcell
// +build !windows
package tui
type Attr int
func HasFullscreenRenderer() bool {
return false
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
const (
AttrRegular Attr = Attr(0)
Bold = Attr(1)
Dim = Attr(1 << 1)
Italic = Attr(1 << 2)
Underline = Attr(1 << 3)
Blink = Attr(1 << 4)
Blink2 = Attr(1 << 5)
Reverse = Attr(1 << 6)
)
func (r *FullscreenRenderer) Init() {}
func (r *FullscreenRenderer) Pause() {}
func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Resume() bool { return false }
func (r *FullscreenRenderer) DoesAutoWrap() bool { return false }
func (r *FullscreenRenderer) IsOptimized() bool { return false }
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"regexp"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -19,11 +20,15 @@ const (
defaultWidth = 80 defaultWidth = 80
defaultHeight = 24 defaultHeight = 24
defaultEscDelay = 100
escPollInterval = 5 escPollInterval = 5
offsetPollTries = 10
) )
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R")
func openTtyIn() *os.File { func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil { if err != nil {
@@ -79,6 +84,7 @@ type LightRenderer struct {
yoffset int yoffset int
tabstop int tabstop int
escDelay int escDelay int
fullscreen bool
upOneLine bool upOneLine bool
queued string queued string
y int y int
@@ -106,8 +112,9 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
ttyin: openTtyIn(), ttyin: openTtyIn(),
yoffset: -1, yoffset: 0,
tabstop: tabstop, tabstop: tabstop,
fullscreen: false,
upOneLine: false, upOneLine: false,
maxHeightFunc: maxHeightFunc} maxHeightFunc: maxHeightFunc}
return &r return &r
@@ -131,18 +138,14 @@ func (r *LightRenderer) defaultTheme() *ColorTheme {
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n") r.csi("6n")
r.flush() r.flush()
bytes := r.getBytesInternal([]byte{}) bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ {
// ^[[*;*R bytes = r.getBytesInternal(bytes, tries > 0)
if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' { offsets := offsetRegexp.FindSubmatch(bytes)
nums := strings.Split(string(bytes[2:len(bytes)-1]), ";") if len(offsets) > 2 {
if len(nums) == 2 { return atoi(string(offsets[1]), 0) - 1, atoi(string(offsets[2]), 0) - 1
return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1
} }
return -1, -1
} }
// No idea
return -1, -1 return -1, -1
} }
@@ -162,15 +165,7 @@ func atoi(s string, defaultValue int) int {
} }
func (r *LightRenderer) Init() { func (r *LightRenderer) Init() {
delay := 100 r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
delayEnv := os.Getenv("ESCDELAY")
if len(delayEnv) > 0 {
num, err := strconv.Atoi(delayEnv)
if err == nil && num >= 0 {
delay = num
}
}
r.escDelay = delay
fd := r.fd() fd := r.fd()
origState, err := terminal.GetState(fd) origState, err := terminal.GetState(fd)
@@ -179,17 +174,26 @@ func (r *LightRenderer) Init() {
} }
r.origState = origState r.origState = origState
terminal.MakeRaw(fd) terminal.MakeRaw(fd)
r.updateTerminalSize() terminalHeight, capHeight := r.updateTerminalSize()
if capHeight == terminalHeight {
r.fullscreen = true
r.height = terminalHeight
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
_, x := r.findOffset() if r.fullscreen {
r.smcup()
} else {
r.csi("J")
y, x := r.findOffset()
r.mouse = r.mouse && y >= 0
if x > 0 { if x > 0 {
r.upOneLine = true r.upOneLine = true
r.stderr("\n") r.makeSpace()
} }
for i := 1; i < r.MaxY(); i++ { for i := 1; i < r.MaxY(); i++ {
r.stderr("\n") r.makeSpace()
r.csi("G") }
} }
if r.mouse { if r.mouse {
@@ -197,12 +201,18 @@ func (r *LightRenderer) Init() {
} }
r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G") r.csi("G")
r.csi("K")
// r.csi("s") // r.csi("s")
if r.mouse { if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset() r.yoffset, _ = r.findOffset()
} }
} }
func (r *LightRenderer) makeSpace() {
r.stderr("\n")
r.csi("G")
}
func (r *LightRenderer) move(y int, x int) { func (r *LightRenderer) move(y int, x int) {
// w.csi("u") // w.csi("u")
if r.y < y { if r.y < y {
@@ -230,15 +240,20 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue) return atoi(env, defaultValue)
} }
func (r *LightRenderer) updateTerminalSize() { func (r *LightRenderer) updateTerminalSize() (int, int) {
width, height, err := terminal.GetSize(r.fd()) width, height, err := terminal.GetSize(r.fd())
if err == nil { if err == nil {
r.width = width r.width = width
if r.fullscreen {
r.height = height
} else {
r.height = r.maxHeightFunc(height) r.height = r.maxHeightFunc(height)
}
} else { } else {
r.width = getEnv("COLUMNS", defaultWidth) r.width = getEnv("COLUMNS", defaultWidth)
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
} }
return height, r.height
} }
func (r *LightRenderer) getch(nonblock bool) (int, bool) { func (r *LightRenderer) getch(nonblock bool) (int, bool) {
@@ -252,18 +267,18 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
} }
func (r *LightRenderer) getBytes() []byte { func (r *LightRenderer) getBytes() []byte {
return r.getBytesInternal(r.buffer) return r.getBytesInternal(r.buffer, false)
} }
func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
c, ok := r.getch(false) c, ok := r.getch(nonblock)
if !ok { if !nonblock && !ok {
r.Close() r.Close()
errorExit("Failed to read " + consoleDevice) errorExit("Failed to read " + consoleDevice)
} }
retries := 0 retries := 0
if c == ESC { if c == ESC || nonblock {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} }
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
@@ -307,6 +322,8 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlQ, 0, nil} return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case ESC: case ESC:
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
@@ -464,7 +481,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
func (r *LightRenderer) mouseSequence(sz *int) Event { func (r *LightRenderer) mouseSequence(sz *int) Event {
if len(r.buffer) < 6 || r.yoffset < 0 { if len(r.buffer) < 6 || !r.mouse {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 6 *sz = 6
@@ -503,15 +520,32 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
func (r *LightRenderer) smcup() {
r.csi("?1049h")
}
func (r *LightRenderer) rmcup() {
r.csi("?1049l")
}
func (r *LightRenderer) Pause() { func (r *LightRenderer) Pause() {
terminal.Restore(r.fd(), r.origState) terminal.Restore(r.fd(), r.origState)
r.csi("?1049h") if r.fullscreen {
r.rmcup()
} else {
r.smcup()
r.csi("H")
}
r.flush() r.flush()
} }
func (r *LightRenderer) Resume() bool { func (r *LightRenderer) Resume() bool {
terminal.MakeRaw(r.fd()) terminal.MakeRaw(r.fd())
r.csi("?1049l") if r.fullscreen {
r.smcup()
} else {
r.rmcup()
}
r.flush() r.flush()
// Should redraw // Should redraw
return true return true
@@ -534,14 +568,18 @@ func (r *LightRenderer) Refresh() {
func (r *LightRenderer) Close() { func (r *LightRenderer) Close() {
// r.csi("u") // r.csi("u")
if r.fullscreen {
r.rmcup()
} else {
r.origin() r.origin()
r.csi("J")
if r.mouse {
r.csi("?1000l")
}
if r.upOneLine { if r.upOneLine {
r.csi("A") r.csi("A")
} }
r.csi("J")
}
if r.mouse {
r.csi("?1000l")
}
r.flush() r.flush()
terminal.Restore(r.fd(), r.origState) terminal.Restore(r.fd(), r.origState)
} }

View File

@@ -1,3 +1,4 @@
// +build ncurses
// +build !windows // +build !windows
// +build !tcell // +build !tcell
@@ -32,6 +33,10 @@ import (
"unicode/utf8" "unicode/utf8"
) )
func HasFullscreenRenderer() bool {
return true
}
type Attr C.uint type Attr C.uint
type CursesWindow struct { type CursesWindow struct {
@@ -470,6 +475,8 @@ func (r *FullscreenRenderer) GetChar() Event {
return escSequence() return escSequence()
case 127: case 127:
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
} }
// CTRL-A ~ CTRL-Z // CTRL-A ~ CTRL-Z
if c >= CtrlA && c <= CtrlZ { if c >= CtrlA && c <= CtrlZ {

View File

@@ -15,6 +15,10 @@ import (
"github.com/junegunn/go-runewidth" "github.com/junegunn/go-runewidth"
) )
func HasFullscreenRenderer() bool {
return true
}
func (p ColorPair) style() tcell.Style { func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault style := tcell.StyleDefault
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
@@ -266,6 +270,8 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlY, 0, nil} return Event{CtrlY, 0, nil}
case tcell.KeyCtrlZ: case tcell.KeyCtrlZ:
return Event{CtrlZ, 0, nil} return Event{CtrlZ, 0, nil}
case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil}
case tcell.KeyBackspace, tcell.KeyBackspace2: case tcell.KeyBackspace, tcell.KeyBackspace2:
if alt { if alt {
return Event{AltBS, 0, nil} return Event{AltBS, 0, nil}

View File

@@ -38,6 +38,7 @@ const (
CtrlY CtrlY
CtrlZ CtrlZ
ESC ESC
CtrlSpace
Invalid Invalid
Resize Resize
@@ -175,6 +176,10 @@ type ColorTheme struct {
Border Color Border Color
} }
func (t *ColorTheme) HasBg() bool {
return t.Bg != colDefault
}
type Event struct { type Event struct {
Type int Type int
Char rune Char rune

View File

@@ -23,7 +23,8 @@ end
# List assets # List assets
assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }] assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }]
files.select { |f| File.exists? f }.each do |file| files.select { |f| File.exists? f }.map do |file|
Thread.new do
name = File.basename file name = File.basename file
if asset_id = assets[name] if asset_id = assets[name]
@@ -41,3 +42,4 @@ files.select { |f| File.exists? f }.each do |file|
:authorization => "token #{token}", :authorization => "token #{token}",
:content_type => "application/octet-stream") :content_type => "application/octet-stream")
end end
end.each(&:join)

View File

@@ -111,7 +111,7 @@ class Tmux
File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end end
def until pane = 0 def until refresh = false, pane = 0
lines = nil lines = nil
begin begin
wait do wait do
@@ -141,7 +141,9 @@ class Tmux
self.select { |line| line.send method, val }.first self.select { |line| line.send method, val }.first
end end
end end
yield lines yield(lines).tap do |ok|
send_keys 'C-l' if refresh && !ok
end
end end
rescue Exception rescue Exception
puts $!.backtrace puts $!.backtrace
@@ -877,7 +879,7 @@ class TestGoFZF < TestBase
def test_execute_multi def test_execute_multi
output = '/tmp/fzf-test-execute-multi' output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"] opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"]
writelines tempname, %w[foo'bar foo"bar foo$bar foobar] writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '4/4' } tmux.until { |lines| lines[-2].include? '4/4' }
@@ -900,6 +902,43 @@ class TestGoFZF < TestBase
File.unlink output rescue nil File.unlink output rescue nil
end end
def test_execute_plus_flag
output = tempname + ".tmp"
File.unlink output rescue nil
writelines tempname, ["foo bar", "123 456"]
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
execute = lambda do
tmux.send_keys 'x', 'y'
tmux.until { |lines| lines[-2].include? '0/2' }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include? '2/2' }
end
tmux.until { |lines| lines[-2].include? '2/2' }
execute.call
tmux.send_keys :Up
tmux.send_keys :Tab
execute.call
tmux.send_keys :Tab
execute.call
tmux.send_keys :Enter
tmux.prepare
readonce
assert_equal [
%[foo bar/foo bar/bar/bar],
%[123 456/foo bar/456/bar],
%[123 456 foo bar/foo bar/456 bar/bar]
], File.readlines(output).map(&:chomp)
rescue
File.unlink output rescue nil
end
def test_execute_shell def test_execute_shell
# Custom script to use as $SHELL # Custom script to use as $SHELL
output = tempname + '.out' output = tempname + '.out'
@@ -1067,7 +1106,7 @@ class TestGoFZF < TestBase
}.each do |ts, exp| }.each do |ts, exp|
tmux.prepare tmux.prepare
tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter
tmux.until { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') } tmux.until(true) { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
end end
@@ -1093,12 +1132,6 @@ class TestGoFZF < TestBase
assert_equal 1, $?.exitstatus assert_equal 1, $?.exitstatus
end end
def test_invalid_term
lines = `TERM=xxx #{FZF} 2>&1`
assert_equal 2, $?.exitstatus
assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found')
end
def test_invalid_option def test_invalid_option
lines = `#{FZF} --foobar 2>&1` lines = `#{FZF} --foobar 2>&1`
assert_equal 2, $?.exitstatus assert_equal 2, $?.exitstatus
@@ -1202,7 +1235,7 @@ class TestGoFZF < TestBase
end end
def test_preview def test_preview
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[1].include?(' {1-1}') } tmux.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| lines[1].include?(' {-}') } tmux.until { |lines| lines[1].include?(' {-}') }
@@ -1216,6 +1249,17 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].start_with? ' 28/1000' } tmux.until { |lines| lines[-2].start_with? ' 28/1000' }
tmux.send_keys 'foobar' tmux.send_keys 'foobar'
tmux.until { |lines| !lines[1].include?('{') } tmux.until { |lines| !lines[1].include?('{') }
tmux.send_keys 'C-u'
tmux.until { |lines| lines.match_count == 1000 }
tmux.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?(' {-1}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?(' {3-1 }') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?(' {4-1 3}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?(' {5-1 3 4}') }
end end
def test_preview_hidden def test_preview_hidden
@@ -1228,6 +1272,19 @@ class TestGoFZF < TestBase
tmux.send_keys '?' tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' } tmux.until { |lines| lines[-1] == '> 555' }
end end
def test_preview_size_0
File.unlink tempname rescue nil
tmux.send_keys %[seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0], :Enter
tmux.until { |lines| lines.item_count == 100 && lines[1] == ' 100/100' && lines[2] == '> 1' }
tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) }
tmux.send_keys :Down
tmux.until { |lines| lines[3] == '> 2' }
tmux.until { |_| %w[1 2] == File.readlines(tempname).map(&:chomp) }
tmux.send_keys :Down
tmux.until { |lines| lines[4] == '> 3' }
tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) }
end
end end
module TestShell module TestShell
@@ -1375,8 +1432,7 @@ module CompletionTest
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines.select_count == 2 } tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) do |lines|
tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100') lines[-1].include?('/tmp/fzf-test/100')
end end
@@ -1388,20 +1444,16 @@ module CompletionTest
tmux.send_keys "'.fzf-home" tmux.send_keys "'.fzf-home"
tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 } tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) do |lines|
tmux.send_keys 'C-L'
lines[-1].end_with?('.fzf-home') lines[-1].end_with?('.fzf-home')
end end
# ~INVALID_USERNAME**<TAB> # ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys "cat ~such**", :Tab tmux.send_keys "cat ~such**", :Tab
tmux.until { |lines| lines.any_include? 'no~such~user' } tmux.until(true) { |lines| lines.any_include? 'no~such~user' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines[-1].end_with?('no~such~user') }
tmux.send_keys 'C-L'
lines[-1].end_with?('no~such~user')
end
# /tmp/fzf\ test**<TAB> # /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
@@ -1410,19 +1462,13 @@ module CompletionTest
tmux.send_keys 'foobar$' tmux.send_keys 'foobar$'
tmux.until { |lines| lines.match_count == 1 } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines[-1].end_with?('/tmp/fzf\ test/foobar') }
tmux.send_keys 'C-L'
lines[-1].end_with?('/tmp/fzf\ test/foobar')
end
# Should include hidden files # Should include hidden files
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" }
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
tmux.until do |lines| tmux.until(true) { |lines| lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') }
tmux.send_keys 'C-L'
lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-')
end
tmux.send_keys :Enter tmux.send_keys :Enter
ensure ensure
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
@@ -1448,10 +1494,7 @@ module CompletionTest
tmux.send_keys 55 tmux.send_keys 55
tmux.until { |lines| lines.match_count == 1 } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
tmux.send_keys 'C-L'
lines[-1] == 'cd /tmp/fzf-test/d55/'
end
tmux.send_keys :xx tmux.send_keys :xx
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
@@ -1479,10 +1522,7 @@ module CompletionTest
tmux.send_keys 'sleep12345' tmux.send_keys 'sleep12345'
tmux.until { |lines| lines.any_include? 'sleep 12345' } tmux.until { |lines| lines.any_include? 'sleep 12345' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines[-1].include? "kill #{pid}" }
tmux.send_keys 'C-L'
lines[-1].include? "kill #{pid}"
end
ensure ensure
Process.kill 'KILL', pid.to_i rescue nil if pid Process.kill 'KILL', pid.to_i rescue nil if pid
end end
@@ -1495,10 +1535,7 @@ module CompletionTest
tmux.send_keys :Tab, :Tab, :Tab tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| lines.select_count == 3 } tmux.until { |lines| lines.select_count == 3 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines[-1] == "ls /tmp 1 2" }
tmux.send_keys 'C-L'
lines[-1] == "ls /tmp 1 2"
end
end end
def test_unset_completion def test_unset_completion
@@ -1515,7 +1552,7 @@ module CompletionTest
# FZF_TMUX=1 # FZF_TMUX=1
new_shell new_shell
tmux.send_keys 'unset FZFFO**', :Tab, pane: 0 tmux.send_keys 'unset FZFFO**', :Tab, pane: 0
tmux.until(1) { |lines| lines.match_count == 1 } tmux.until(false, 1) { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
end end
@@ -1541,10 +1578,7 @@ module CompletionTest
tmux.until { |lines| lines.select_count == 2 } tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines.any_include? 'cat' }
tmux.send_keys 'C-l'
lines.any_include? 'cat'
end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' } tmux.until { |lines| lines[-1].include? 'test3test4' }
end end