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

Compare commits

...

23 Commits

Author SHA1 Message Date
Junegunn Choi
dc975e8974 0.29.0 2021-12-25 01:46:01 +09:00
Junegunn Choi
4311ade535 ADVANCED.md: Add change-preview-window example 2021-12-24 14:41:47 +09:00
Junegunn Choi
cd23401411 Fix rendering of the prompt line when overflow occurs with --info=inline
Fix #2692
2021-12-22 23:23:50 +09:00
Martin Jindra
176ee6910f Update Dockerfile (#2662)
`archlinux/base:latest`cannot be found
2021-12-09 11:05:12 +09:00
Junegunn Choi
13c8f3d3aa [vim] Handle writefile() failure gracefully
Fix #2676
2021-12-07 17:33:26 +09:00
Junegunn Choi
ce9af687bc Remove unused code 2021-12-05 21:17:38 +09:00
Junegunn Choi
43f0d0cacd change-preview-window to take multiple option sets separated by '|'
So you can "rotate" through the different options with a single binding.

  fzf --preview 'cat {}' \
      --bind 'ctrl-/:change-preview-window(70%|down,40%,border-horizontal|hidden|)'

Close #2376
2021-12-05 21:13:10 +09:00
Junegunn Choi
20b4e6953e Implement change-preview and change-preview-window actions
The new actions are named with 'change-' prefix to differentiate from
the pre-existing, one-off 'preview(...)' action.

Fix #2360
Fix #2505
Fix #2666

Related #2435
Related #2376
  - Can set up multiple bindings with different change-preview-window actions
  - Not possible to "rotate" through the options with a single binding
  - Enlarge or shrink not possible
2021-11-30 23:57:46 +09:00
Kai
7da287e3aa README.md: HTTP => HTTPS (#2673) 2021-11-28 22:28:32 +09:00
zsugabubus
205f885d69 [shell] Use cd -- (#2659)
Otherwise directories starting with '-' may treated as options.
2021-11-19 10:36:28 +09:00
Junegunn Choi
3715cd349d Add repology packaging status badge 2021-11-18 15:47:06 +09:00
Junegunn Choi
e4c3ecc57e 0.28.0 2021-11-04 01:05:07 +09:00
Junegunn Choi
673c5d886d Add 'put' action for putting the character to the prompt
fzf --bind 'space:preview(date)+put'

Close #2456
2021-11-04 00:49:05 +09:00
Junegunn Choi
f799b568d1 [bash] Suppress error message from 'bind'
Fix #2618
2021-11-03 23:26:25 +09:00
Junegunn Choi
7bff4661f6 Add --header-first option to display header before prompt line
Close #2422
2021-11-03 21:19:22 +09:00
Junegunn Choi
ffd8bef808 Update CHANGELOG 2021-11-02 21:48:19 +09:00
Junegunn Choi
02cee2234d Implement --scroll-off=LINES
Close #2533
2021-11-02 21:48:19 +09:00
Vlastimil Ovčáčík
e0dd2be3fb Document escaping and expanding of quotes on Windows
Parsers included:
- go parser (well, this is easily dealt with using `` strings)
- win32 (shell-api) parser
- powershell parser (for powershell commands)
- powershell parsing rules for calling native commands
- internal parsers of select regex applications (like grep)
2021-11-02 15:56:20 +09:00
Vlastimil Ovčáčík
a33c011c21 Test escaping of powershell commands on Windows 2021-11-02 15:56:20 +09:00
Rashil Gandhi
7c3f42bbba Fix powershell escaping 2021-11-02 15:56:20 +09:00
Junegunn Choi
edac9820b5 Cache cygpath result
No need to repeatedly run cygpath process because $SHELL never changes.
2021-10-25 18:46:59 +09:00
Rashil Gandhi
84a47f7102 Respect SHELL env var on Windows (#2641)
This makes fzf respect SHELL environment variable on Windows, like it does on *nix, whenever defined.

Close #2638
2021-10-23 01:09:47 +09:00
Junegunn Choi
97ae8afb6f Reload should update preview window
Fix #2644
2021-10-23 01:06:15 +09:00
23 changed files with 837 additions and 305 deletions

View File

@@ -429,30 +429,34 @@ Admittedly, that was a silly example. Here's a practical one for browsing
Kubernetes pods.
```bash
#!/usr/bin/env bash
read -ra tokens < <(
kubectl get pods --all-namespaces |
fzf --info=inline --layout=reverse --header-lines=1 --border \
pods() {
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \
fzf --info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $'Press CTRL-O to open log in editor\n\n' \
--bind ctrl-/:toggle-preview \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --namespace {1} {2}) > /dev/tty' \
--preview-window up,follow \
--preview 'kubectl logs --follow --tail=100000 --namespace {1} {2}' "$@"
)
[ ${#tokens} -gt 1 ] &&
kubectl exec -it --namespace "${tokens[0]}" "${tokens[1]}" -- bash
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
--preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
}
```
![image](https://user-images.githubusercontent.com/700826/113473547-1d7a4880-94a5-11eb-98ef-9aa6f0ed215a.png)
- The preview window will *"log tail"* the pod
- Holding on to a large amount of log will consume a lot of memory. So we
limited the initial log amount with `--tail=100000`.
- With `execute` binding, you can press CTRL-O to open the log in your editor
without leaving fzf
- Select a pod (with an enter key) to `kubectl exec` into it
limited the initial log amount with `--tail=10000`.
- `execute` bindings allow you to run any command without leaving fzf
- Press enter key on a pod to `kubectl exec` into it
- Press CTRL-O to open the log in your editor
- Press CTRL-R to reload the pod list
- Press CTRL-/ repeatedly to to rotate through a different sets of preview
window options
1. `80%,border-bottom`
1. `hidden`
1. Empty string after `|` translates to the default options from `--preview-window`
Key bindings for git objects
----------------------------

View File

@@ -1,6 +1,37 @@
CHANGELOG
=========
0.29.0
------
- Added `change-preview(...)` action to change the `--preview` command
- cf. `preview(...)` is a one-off action that doesn't change the default
preview command
- Added `change-preview-window(...)` action
- You can rotate through the different options separated by `|`
```sh
fzf --preview 'cat {}' --preview-window right:40% \
--bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-top|hidden|)'
```
- Fixed rendering of the prompt line when overflow occurs with `--info=inline`
0.28.0
------
- Added `--header-first` option to print header before the prompt line
```sh
fzf --header $'Welcome to fzf\n▔▔▔▔▔▔▔▔▔▔▔▔▔▔' --reverse --height 30% --border --header-first
```
- Added `--scroll-off=LINES` option (similar to `scrolloff` option of Vim)
- You can set it to a very large number so that the cursor stays in the
middle of the screen while scrolling
```sh
fzf --scroll-off=5
fzf --scroll-off=999
```
- Fixed bug where preview window is not updated on `reload` (#2644)
- fzf on Windows will also use `$SHELL` to execute external programs
- See #2638 and #2647
- Thanks to @rashil2000, @vovcacik, and @janlazo
0.27.3
------
- Preview window is `hidden` by default when there are `preview` bindings but

View File

@@ -1,4 +1,4 @@
FROM archlinux/base:latest
FROM archlinux
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
RUN gem install --no-document -v 5.14.2 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc

View File

@@ -102,6 +102,7 @@ endif
grep -qF $(VERSION) install.ps1
# Make release note out of CHANGELOG.md
mkdir -p tmp
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note

View File

@@ -86,7 +86,7 @@ stuff.
### Using Homebrew
You can use [Homebrew](http://brew.sh/) (on macOS or Linux)
You can use [Homebrew](https://brew.sh/) (on macOS or Linux)
to install fzf.
```sh
@@ -131,6 +131,8 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
>
> Refer to the package documentation for more information. (e.g. `apt-cache show fzf`)
[![Packaging status](https://repology.org/badge/vertical-allrepos/fzf.svg)](https://repology.org/project/fzf/versions)
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also

View File

@@ -2,7 +2,7 @@
set -u
version=0.27.3
version=0.29.0
auto_completion=
key_bindings=
update_config=2

View File

@@ -1,4 +1,4 @@
$version="0.27.3"
$version="0.29.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -1,11 +1,11 @@
package main
import (
"github.com/junegunn/fzf/src"
fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.27"
var version string = "0.29"
var revision string = "devel"
func main() {

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
THE SOFTWARE.
..
.TH fzf-tmux 1 "Oct 2021" "fzf 0.27.3" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Dec 2021" "fzf 0.29.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
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
THE SOFTWARE.
..
.TH fzf 1 "Oct 2021" "fzf 0.27.3" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Dec 2021" "fzf 0.29.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -135,10 +135,14 @@ Enable cyclic scroll
Keep the right end of the line visible when it's too long. Effective only when
the query string is empty.
.TP
.BI "--scroll-off=" "LINES"
Number of screen lines to keep above or below when scrolling to the top or to
the bottom (default: 0).
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.BI "--hscroll-off=" "COL"
.BI "--hscroll-off=" "COLS"
Number of screen columns to keep to the right of the highlighted substring
(default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen.
@@ -295,6 +299,9 @@ are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
The first N lines of the input are treated as the sticky header. When
\fB--with-nth\fR is set, the lines are transformed just like the other
lines that follow.
.TP
.B "--header-first"
Print header before the prompt line
.SS Display
.TP
.B "--ansi"
@@ -803,76 +810,79 @@ e.g.
A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-prompt(...)\fR (change prompt to the given string)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange-prompt(...)\fR (change prompt to the given string)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBdeselect\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR
\fBpreview-page-up\fR
\fBpreview-half-page-down\fR
\fBpreview-half-page-up\fR
\fBpreview-bottom\fR
\fBpreview-top\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit)
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt)
\fBrefresh-preview\fR
\fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR
\fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION
@@ -962,7 +972,6 @@ commands in addition to the default preview command given by \fB--preview\fR
option.
e.g.
# Default preview command with an extra preview binding
fzf --preview 'file {}' --bind '?:preview:cat {}'
@@ -973,6 +982,22 @@ e.g.
# Preview window hidden by default, it appears when you first hit '?'
fzf --bind '?:preview:cat {}' --preview-window hidden
.SS CHANGE PREVIEW WINDOW ATTRIBUTES
\fBchange-preview-window\fR action can be used to change the properties of the
preview window. Unlike the \fB--preview-window\fR option, you can specify
multiple sets of options separated by '|' characters.
e.g.
# Rotate through the options using CTRL-/
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-horizontal|hidden|right)'
# The default properties given by `--preview-window` are inherited, so an empty string in the list is interpreted as the default
fzf --preview 'cat {}' --preview-window 'right,40%,border-left' --bind 'ctrl-/:change-preview-window(70%|down,border-top|hidden|)'
# This is equivalent to toggle-preview action
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(hidden|)'
.SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -444,6 +444,12 @@ function! s:use_sh()
return [shell, shellslash, shellcmdflag, shellxquote]
endfunction
function! s:writefile(...)
if call('writefile', a:000) == -1
throw 'Failed to write temporary file. Check if you can write to the path tempname() returns.'
endif
endfunction
function! fzf#run(...) abort
try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -471,7 +477,7 @@ try
let source_command = source
elseif type == 3
let temps.input = s:fzf_tempname()
call writefile(source, temps.input)
call s:writefile(source, temps.input)
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
else
throw 'Invalid source type'
@@ -515,7 +521,7 @@ try
call s:callback(dict, lines)
return lines
finally
if len(source_command)
if exists('source_command') && len(source_command)
if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command
else
@@ -660,7 +666,7 @@ function! s:execute(dict, command, use_height, temps) abort
endif
if s:is_win
let batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(command), batchfile)
call s:writefile(s:wrap_cmds(command), batchfile)
let command = batchfile
let a:temps.batchfile = batchfile
if has('nvim')
@@ -678,7 +684,7 @@ function! s:execute(dict, command, use_height, temps) abort
endif
elseif has('win32unix') && $TERM !=# 'cygwin'
let shellscript = s:fzf_tempname()
call writefile([command], shellscript)
call s:writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
let a:temps.shellscript = shellscript
endif
@@ -877,7 +883,7 @@ function! s:execute_term(dict, command, temps) abort
call s:pushd(a:dict)
if s:is_win
let fzf.temps.batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
call s:writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
let command = fzf.temps.batchfile
else
let command = a:command

View File

@@ -32,7 +32,7 @@ fi
###########################################################
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line'
bind '"\e[0n": redraw-current-line' 2> /dev/null
__fzf_comprun() {
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then

View File

@@ -41,7 +41,7 @@ __fzf_cd__() {
local cmd dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd -- %q' "$dir"
}
__fzf_history__() {

View File

@@ -87,7 +87,7 @@ function fzf_key_bindings
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ]
cd $result
cd -- $result
# Remove last token from commandline.
commandline -t ""

View File

@@ -79,7 +79,7 @@ fzf-cd-widget() {
return 0
fi
zle push-line # Clear buffer. Auto-restored on next prompt.
BUFFER="cd ${(q)dir}"
BUFFER="cd -- ${(q)dir}"
zle accept-line
local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion

View File

@@ -44,8 +44,10 @@ const usage = `usage: fzf [options]
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--cycle Enable cyclic scroll
--keep-right Keep the right end of the line visible on overflow
--scroll-off=LINES Number of screen lines to keep above or below when
scrolling to the top or to the bottom (default: 0)
--no-hscroll Disable horizontal scroll
--hscroll-off=COL Number of screen columns to keep to the right of the
--hscroll-off=COLS Number of screen columns to keep to the right of the
highlighted substring (default: 10)
--filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump and jump-accept
@@ -67,6 +69,7 @@ const usage = `usage: fzf [options]
--marker=STR Multi-select marker (default: '>')
--header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line
Display
--ansi Enable processing of ANSI color codes
@@ -173,6 +176,14 @@ type previewOpts struct {
headerLines int
}
func (a previewOpts) sameLayout(b previewOpts) bool {
return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden
}
func (a previewOpts) sameContentLayout(b previewOpts) bool {
return a.wrap == b.wrap && a.headerLines == b.headerLines
}
// Options stores the values of command-line options
type Options struct {
Fuzzy bool
@@ -200,6 +211,7 @@ type Options struct {
KeepRight bool
Hscroll bool
HscrollOff int
ScrollOff int
FileWord bool
InfoStyle infoStyle
JumpLabels string
@@ -212,7 +224,7 @@ type Options struct {
Filter *string
ToggleSort bool
Expect map[tui.Event]string
Keymap map[tui.Event][]action
Keymap map[tui.Event][]*action
Preview previewOpts
PrintQuery bool
ReadZero bool
@@ -222,6 +234,7 @@ type Options struct {
History *History
Header []string
HeaderLines int
HeaderFirst bool
Margin [4]sizeSpec
Padding [4]sizeSpec
BorderShape tui.BorderShape
@@ -261,6 +274,7 @@ func defaultOptions() *Options {
KeepRight: false,
Hscroll: true,
HscrollOff: 10,
ScrollOff: 0,
FileWord: false,
InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels,
@@ -273,7 +287,7 @@ func defaultOptions() *Options {
Filter: nil,
ToggleSort: false,
Expect: make(map[tui.Event]string),
Keymap: make(map[tui.Event][]action),
Keymap: make(map[tui.Event][]*action),
Preview: defaultPreviewOpts(""),
PrintQuery: false,
ReadZero: false,
@@ -283,6 +297,7 @@ func defaultOptions() *Options {
History: nil,
Header: make([]string, 0),
HeaderLines: 0,
HeaderFirst: false,
Margin: defaultMargin(),
Padding: defaultMargin(),
Unicode: true,
@@ -780,10 +795,10 @@ func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
}
func parseKeymap(keymap map[tui.Event][]action, str string) {
func parseKeymap(keymap map[tui.Event][]*action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
symbol := ":"
if strings.HasPrefix(src, "+") {
@@ -792,6 +807,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload"
} else if strings.HasPrefix(src[1:], "change-preview-window") {
prefix = symbol + "change-preview-window"
} else if strings.HasPrefix(src[1:], "change-preview") {
prefix = symbol + "change-preview"
} else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") {
@@ -835,7 +854,7 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
idx2 := len(pair[0]) + 1
specs := strings.Split(pair[1], "+")
actions := make([]action, 0, len(specs))
actions := make([]*action, 0, len(specs))
appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...)
}
@@ -974,6 +993,12 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
appendAction(actEnableSearch)
case "disable-search":
appendAction(actDisableSearch)
case "put":
if key.Type == tui.Rune && unicode.IsGraphic(key.Char) {
appendAction(actRune)
} else {
errorExit("unable to put non-printable character: " + pair[0])
}
default:
t := isExecuteAction(specLower)
if t == actIgnore {
@@ -989,6 +1014,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
offset = len("reload")
case actPreview:
offset = len("preview")
case actChangePreviewWindow:
offset = len("change-preview-window")
case actChangePreview:
offset = len("change-preview")
case actChangePrompt:
offset = len("change-prompt")
case actUnbind:
@@ -1004,17 +1033,22 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
if spec[offset] == ':' {
if specIndex == len(specs)-1 {
actionArg = spec[offset+1:]
actions = append(actions, action{t: t, a: actionArg})
actions = append(actions, &action{t: t, a: actionArg})
} else {
prevSpec = spec + "+"
continue
}
} else {
actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, action{t: t, a: actionArg})
actions = append(actions, &action{t: t, a: actionArg})
}
if t == actUnbind {
parseKeyChords(actionArg, "unbind target required")
} else if t == actChangePreviewWindow {
opts := previewOpts{}
for _, arg := range strings.Split(actionArg, "|") {
parsePreviewWindow(&opts, arg)
}
}
}
}
@@ -1040,6 +1074,10 @@ func isExecuteAction(str string) actionType {
return actUnbind
case "preview":
return actPreview
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
return actChangePreview
case "change-prompt":
return actChangePrompt
case "execute":
@@ -1052,7 +1090,7 @@ func isExecuteAction(str string) actionType {
return actIgnore
}
func parseToggleSort(keymap map[tui.Event][]action, str string) {
func parseToggleSort(keymap map[tui.Event][]*action, str string) {
keys := parseKeyChords(str, "key name required")
if len(keys) != 1 {
errorExit("multiple keys specified")
@@ -1354,6 +1392,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Hscroll = false
case "--hscroll-off":
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
case "--scroll-off":
opts.ScrollOff = nextInt(allArgs, &i, "scroll offset required")
case "--filepath-word":
opts.FileWord = true
case "--no-filepath-word":
@@ -1421,6 +1461,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--header-lines":
opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required"))
case "--header-first":
opts.HeaderFirst = true
case "--no-header-first":
opts.HeaderFirst = false
case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview":
@@ -1530,6 +1574,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match {
opts.ScrollOff = atoi(value)
} else if match, value := optString(arg, "--jump-labels="); match {
opts.JumpLabels = value
validateJumpLabels = true
@@ -1547,6 +1593,10 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("hscroll offset must be a non-negative integer")
}
if opts.ScrollOff < 0 {
errorExit("scroll offset must be a non-negative integer")
}
if opts.Tabstop < 1 {
errorExit("tab stop must be a positive integer")
}
@@ -1608,11 +1658,29 @@ func postProcessOptions(opts *Options) {
// Extend the default key map
keymap := defaultKeymap()
for key, actions := range opts.Keymap {
var lastChangePreviewWindow *action
for _, act := range actions {
if act.t == actToggleSort {
switch act.t {
case actToggleSort:
// To display "+S"/"-S" on info line
opts.ToggleSort = true
case actChangePreviewWindow:
lastChangePreviewWindow = act
}
}
// Re-organize actions so that we only keep the last change-preview-window
// and it comes first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+20)+preview(sleep 3; cat {})
if lastChangePreviewWindow != nil {
reordered := []*action{lastChangePreviewWindow}
for _, act := range actions {
if act.t != actChangePreviewWindow {
reordered = append(reordered, act)
}
}
actions = reordered
}
keymap[key] = actions
}
opts.Keymap = keymap

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"io/ioutil"
"math"
"os"
"os/signal"
"regexp"
@@ -104,85 +105,89 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output
type Terminal struct {
initDelay time.Duration
infoStyle infoStyle
spinner []string
prompt func()
promptLen int
pointer string
pointerLen int
pointerEmpty string
marker string
markerLen int
markerEmpty string
queryLen [2]int
layout layoutType
fullscreen bool
keepRight bool
hscroll bool
hscrollOff int
wordRubout string
wordNext string
cx int
cy int
offset int
xoffset int
yanked []rune
input []rune
multi int
sort bool
toggleSort bool
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]action
pressed string
printQuery bool
history *History
cycle bool
header []string
header0 []string
ansi bool
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
strong tui.Attr
unicode bool
borderShape tui.BorderShape
cleanExit bool
paused bool
border tui.Window
window tui.Window
pborder tui.Window
pwindow tui.Window
count int
progress int
reading bool
running bool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
selected map[int32]selectedItem
version int64
reqBox *util.EventBox
previewOpts previewOpts
previewer previewer
previewed previewed
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
prevLines []itemLine
suppress bool
sigstop bool
startChan chan bool
killChan chan int
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
initDelay time.Duration
infoStyle infoStyle
spinner []string
prompt func()
promptLen int
pointer string
pointerLen int
pointerEmpty string
marker string
markerLen int
markerEmpty string
queryLen [2]int
layout layoutType
fullscreen bool
keepRight bool
hscroll bool
hscrollOff int
scrollOff int
wordRubout string
wordNext string
cx int
cy int
offset int
xoffset int
yanked []rune
input []rune
multi int
sort bool
toggleSort bool
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]*action
pressed string
printQuery bool
history *History
cycle bool
headerFirst bool
headerLines int
header []string
header0 []string
ansi bool
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
strong tui.Attr
unicode bool
borderShape tui.BorderShape
cleanExit bool
paused bool
border tui.Window
window tui.Window
pborder tui.Window
pwindow tui.Window
count int
progress int
reading bool
running bool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
selected map[int32]selectedItem
version int64
reqBox *util.EventBox
initialPreviewOpts previewOpts
previewOpts previewOpts
previewer previewer
previewed previewed
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
prevLines []itemLine
suppress bool
sigstop bool
startChan chan bool
killChan chan int
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
}
type selectedItem struct {
@@ -213,6 +218,7 @@ const (
reqRefresh
reqReinit
reqRedraw
reqFullRedraw
reqClose
reqPrintQuery
reqPreviewEnqueue
@@ -283,6 +289,8 @@ const (
actTogglePreview
actTogglePreviewWrap
actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop
actPreviewBottom
actPreviewUp
@@ -321,9 +329,10 @@ type searchRequest struct {
}
type previewRequest struct {
template string
pwindow tui.Window
list []*Item
template string
pwindow tui.Window
scrollOffset int
list []*Item
}
type previewResult struct {
@@ -333,16 +342,16 @@ type previewResult struct {
spinner string
}
func toActions(types ...actionType) []action {
actions := make([]action, len(types))
func toActions(types ...actionType) []*action {
actions := make([]*action, len(types))
for idx, t := range types {
actions[idx] = action{t: t, a: ""}
actions[idx] = &action{t: t, a: ""}
}
return actions
}
func defaultKeymap() map[tui.Event][]action {
keymap := make(map[tui.Event][]action)
func defaultKeymap() map[tui.Event][]*action {
keymap := make(map[tui.Event][]*action)
add := func(e tui.EventType, a actionType) {
keymap[e.AsEvent()] = toActions(a)
}
@@ -413,7 +422,7 @@ func trimQuery(query string) []rune {
func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap {
for _, action := range actions {
if action.t == actPreview {
if action.t == actPreview || action.t == actChangePreview {
return true
}
}
@@ -493,69 +502,73 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
}
t := Terminal{
initDelay: delay,
infoStyle: opts.InfoStyle,
spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
keepRight: opts.KeepRight,
hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
wordRubout: wordRubout,
wordNext: wordNext,
cx: len(input),
cy: 0,
offset: 0,
xoffset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter,
expect: opts.Expect,
keymap: opts.Keymap,
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
padding: opts.Padding,
unicode: opts.Unicode,
borderShape: opts.BorderShape,
cleanExit: opts.ClearOnExit,
paused: opts.Phony,
strong: strongAttr,
cycle: opts.Cycle,
header: header,
header0: header,
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
running: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
printsep: opts.PrintSep,
merger: EmptyMerger,
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
previewed: previewed{0, 0, 0, false},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
sigstop: false,
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
killChan: make(chan int),
tui: renderer,
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
initDelay: delay,
infoStyle: opts.InfoStyle,
spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
keepRight: opts.KeepRight,
hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
scrollOff: opts.ScrollOff,
wordRubout: wordRubout,
wordNext: wordNext,
cx: len(input),
cy: 0,
offset: 0,
xoffset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter,
expect: opts.Expect,
keymap: opts.Keymap,
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
padding: opts.Padding,
unicode: opts.Unicode,
borderShape: opts.BorderShape,
cleanExit: opts.ClearOnExit,
paused: opts.Phony,
strong: strongAttr,
cycle: opts.Cycle,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
header: header,
header0: header,
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
running: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
printsep: opts.PrintSep,
merger: EmptyMerger,
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
initialPreviewOpts: opts.Preview,
previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
previewed: previewed{0, 0, 0, false},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
sigstop: false,
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
killChan: make(chan int),
tui: renderer,
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@@ -697,7 +710,7 @@ func (t *Terminal) sortSelected() []selectedItem {
}
func (t *Terminal) displayWidth(runes []rune) int {
width, _ := util.RunesWidth(runes, 0, t.tabstop, 0)
width, _ := util.RunesWidth(runes, 0, t.tabstop, math.MaxInt32)
return width
}
@@ -974,12 +987,23 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
return before, after
}
func (t *Terminal) promptLine() int {
if t.headerFirst {
max := t.window.Height() - 1
if !t.noInfoLine() {
max--
}
return util.Min(len(t.header0)+t.headerLines, max)
}
return 0
}
func (t *Terminal) placeCursor() {
t.move(0, t.promptLen+t.queryLen[0], false)
t.move(t.promptLine(), t.promptLen+t.queryLen[0], false)
}
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
t.move(t.promptLine(), 0, true)
t.prompt()
before, after := t.updatePromptOffset()
@@ -1001,22 +1025,23 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
func (t *Terminal) printInfo() {
pos := 0
line := t.promptLine()
switch t.infoStyle {
case infoDefault:
t.move(1, 0, true)
t.move(line+1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
}
t.move(1, 2, false)
t.move(line+1, 2, false)
pos = 2
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
if pos+len(" < ") > t.window.Width() {
return
}
t.move(0, pos, true)
t.move(line, pos, true)
if t.reading {
t.window.CPrint(tui.ColSpinner, " < ")
} else {
@@ -1059,11 +1084,20 @@ func (t *Terminal) printHeader() {
return
}
max := t.window.Height()
if t.headerFirst {
max--
if !t.noInfoLine() {
max--
}
}
var state *ansiState
for idx, lineStr := range t.header {
line := idx + 2
if t.noInfoLine() {
line--
line := idx
if !t.headerFirst {
line++
if !t.noInfoLine() {
line++
}
}
if line >= max {
continue
@@ -1164,7 +1198,7 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
// We start from the beginning to handle tab characters
width, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
_, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
if overflowIdx >= 0 {
return runes[:overflowIdx], true
}
@@ -1615,9 +1649,14 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
}
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
func (t *Terminal) evaluateScrollOffset() int {
if t.pwindow == nil {
return 0
}
// We only need the current item to calculate the scroll offset
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "")
t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
@@ -1628,20 +1667,21 @@ func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
}
base := -1
height := util.Max(0, t.pwindow.Height()-t.previewOpts.headerLines)
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") {
component = component[1:]
}
if component[0] == '/' {
denom := atoi(component[1:])
if denom == 0 {
return base
if denom != 0 {
base -= height / denom
}
return base - height/denom
break
}
base += atoi(component)
}
return base
return util.Max(0, base)
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
@@ -1740,8 +1780,10 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
})
}
func (t *Terminal) redraw() {
t.tui.Clear()
func (t *Terminal) redraw(clear bool) {
if clear {
t.tui.Clear()
}
t.tui.Refresh()
t.printAll()
}
@@ -1761,7 +1803,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.tui.Pause(true)
cmd.Run()
t.tui.Resume(true, false)
t.redraw()
t.redraw(true)
t.refresh()
} else {
t.tui.Pause(false)
@@ -1906,7 +1948,7 @@ func (t *Terminal) Loop() {
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
t.reqBox.Set(reqFullRedraw, nil)
}
}()
@@ -1945,12 +1987,14 @@ func (t *Terminal) Loop() {
var items []*Item
var commandTemplate string
var pwindow tui.Window
initialOffset := 0
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request := value.(previewRequest)
commandTemplate = request.template
initialOffset = request.scrollOffset
items = request.list
pwindow = request.pwindow
}
@@ -1962,11 +2006,9 @@ func (t *Terminal) Loop() {
if items[0] != nil {
_, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
initialOffset := 0
cmd := util.ExecCommand(command, true)
if pwindow != nil {
height := pwindow.Height()
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@@ -2101,7 +2143,7 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false)
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, list})
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
}
}
@@ -2156,9 +2198,11 @@ func (t *Terminal) Loop() {
t.suppress = false
case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw()
t.redraw(true)
case reqRedraw:
t.redraw()
t.redraw(false)
case reqFullRedraw:
t.redraw(true)
case reqClose:
exit(func() int {
if t.output() {
@@ -2226,13 +2270,15 @@ func (t *Terminal) Loop() {
}
}
}
togglePreview := func(enabled bool) {
togglePreview := func(enabled bool) bool {
if t.previewer.enabled != enabled {
t.previewer.enabled = enabled
t.tui.Clear()
// We need to immediately update t.pwindow so we don't use reqRedraw
t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader)
return true
}
return false
}
toggle := func() bool {
current := t.currentItem()
@@ -2269,12 +2315,12 @@ func (t *Terminal) Loop() {
}
}
actionsFor := func(eventType tui.EventType) []action {
actionsFor := func(eventType tui.EventType) []*action {
return t.keymap[eventType.AsEvent()]
}
var doAction func(action) bool
doActions := func(actions []action) bool {
var doAction func(*action) bool
doActions := func(actions []*action) bool {
for _, action := range actions {
if !doAction(action) {
return false
@@ -2282,7 +2328,7 @@ func (t *Terminal) Loop() {
}
return true
}
doAction = func(a action) bool {
doAction = func(a *action) bool {
switch a.t {
case actIgnore:
case actExecute, actExecuteSilent:
@@ -2300,7 +2346,7 @@ func (t *Terminal) Loop() {
if valid {
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.pwindow, list})
previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
}
}
}
@@ -2462,14 +2508,14 @@ func (t *Terminal) Loop() {
}
case actToggleIn:
if t.layout != layoutDefault {
return doAction(action{t: actToggleUp})
return doAction(&action{t: actToggleUp})
}
return doAction(action{t: actToggleDown})
return doAction(&action{t: actToggleDown})
case actToggleOut:
if t.layout != layoutDefault {
return doAction(action{t: actToggleDown})
return doAction(&action{t: actToggleDown})
}
return doAction(action{t: actToggleUp})
return doAction(&action{t: actToggleUp})
case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true)
@@ -2493,7 +2539,7 @@ func (t *Terminal) Loop() {
req(reqClose)
}
case actClearScreen:
req(reqRedraw)
req(reqFullRedraw)
case actClearQuery:
t.input = []rune{}
t.cx = 0
@@ -2642,7 +2688,7 @@ func (t *Terminal) Loop() {
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
if my == t.promptLine() && mx >= 0 {
// Prompt
t.cx = mx + t.xoffset
} else if my >= min {
@@ -2673,12 +2719,52 @@ func (t *Terminal) Loop() {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command
t.reading = true
t.version++
}
case actUnbind:
keys := parseKeyChords(a.a, "PANIC")
for key := range keys {
delete(t.keymap, key)
}
case actChangePreview:
if t.previewOpts.command != a.a {
togglePreview(len(a.a) > 0)
t.previewOpts.command = a.a
refreshPreview(t.previewOpts.command)
}
case actChangePreviewWindow:
currentPreviewOpts := t.previewOpts
// Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts
// Split window options
tokens := strings.Split(a.a, "|")
parsePreviewWindow(&t.previewOpts, tokens[0])
if len(tokens) > 1 {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
}
if t.previewOpts.hidden {
togglePreview(false)
} else {
// Full redraw
if !currentPreviewOpts.sameLayout(t.previewOpts) {
if togglePreview(true) {
refreshPreview(t.previewOpts.command)
} else {
req(reqRedraw)
}
} else if !currentPreviewOpts.sameContentLayout(t.previewOpts) {
t.previewed.version = 0
req(reqPreviewRefresh)
}
// Adjust scroll offset
if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.scroll {
scrollPreviewTo(t.evaluateScrollOffset())
}
}
}
return true
}
@@ -2686,7 +2772,7 @@ func (t *Terminal) Loop() {
if t.jumping == jumpDisabled {
actions := t.keymap[event.Comparable()]
if len(actions) == 0 && event.Type == tui.Rune {
doAction(action{t: actRune})
doAction(&action{t: actRune})
} else if !doActions(actions) {
continue
}
@@ -2748,9 +2834,26 @@ func (t *Terminal) constrain() {
t.cy = util.Constrain(t.cy, 0, count-1)
minOffset := t.cy - height + 1
minOffset := util.Max(t.cy-height+1, 0)
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
t.offset = util.Constrain(t.offset, minOffset, maxOffset)
if t.scrollOff == 0 {
return
}
scrollOff := util.Min(height/2, t.scrollOff)
for {
prevOffset := t.offset
if t.cy-t.offset < scrollOff {
t.offset = util.Max(minOffset, t.offset-1)
}
if t.cy-t.offset >= height-scrollOff {
t.offset = util.Min(maxOffset, t.offset+1)
}
if t.offset == prevOffset {
break
}
}
}
func (t *Terminal) vmove(o int, allowCycle bool) {

View File

@@ -288,6 +288,7 @@ func TestUnixCommands(t *testing.T) {
{give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
// 2) problematic examples
// (not necessarily unexpected)
// paths that need to expand some part of it won't work (special characters and variables)
{give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
@@ -315,6 +316,7 @@ func TestWindowsCommands(t *testing.T) {
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
// 2) problematic examples
// (not necessarily unexpected)
// notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
{give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
@@ -327,11 +329,97 @@ func TestWindowsCommands(t *testing.T) {
// the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
// the temp file contains: `cat "C:\test.txt"`
// TODO this should actually work
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
}
testCommands(t, tests)
}
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
func TestPowershellCommands(t *testing.T) {
if !util.IsWindows() {
t.SkipNow()
}
tests := []testCase{
// reference: give{template, query, items}, want{output OR match}
/*
You can read each line in the following table as a pipeline that
consist of series of parsers that act upon your input (col. 1) and
each cell represents the output value.
For example:
- exec.Command("program.exe", `\''`)
- goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
- powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
- native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
- some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)
Character⁰ CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
" empty string² missing argument error ... ... |
\" literal " unbalanced quote error ... ... |
'\"' literal '"' literal " empty string empty string (match all) | yes
'\\\"' literal '\"' literal \" literal " literal " |
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
\ transparent transparent transparent regex error |
'\' transparent literal \ literal \ regex error | yes
\\ transparent transparent transparent literal \ |
'\\' transparent literal \\ literal \\ literal \ |
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
' transparent unbalanced quote error ... ... |
\' transparent literal \ and unb. quote error ... ... |
\'' transparent literal \ and empty string literal \ regex error | no, but given as example above
''' transparent unbalanced quote error ... ... |
'''' transparent literal ' literal ' literal ' | yes
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
¹: native commands like grep, git grep, ripgrep
²: interpreted as a grouping quote, affects argument parser and gets removed from the result
[CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
[NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
*/
// 1) working examples
{give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},
// example of escaping single quotes
{give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},
// chaining powershells
{give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},
// 2) problematic examples
// (not necessarily unexpected)
// looking for a path string will only work with escaped backslashes
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
// looking for a literal double quote will only work with triple escaped double quotes
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},
// Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
// Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},
// the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
// to explorer, which will prompt user to pick editing program for the fzf-preview file
// the temp file contains: `cat "C:\test.txt"`
// TODO this should actually work
{give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}},
}
// to force powershell-style escaping we temporarily set environment variable that fzf honors
shellBackup := os.Getenv("SHELL")
os.Setenv("SHELL", "powershell")
testCommands(t, tests)
os.Setenv("SHELL", shellBackup)
}
/*
Test typical valid placeholders and parsing of them.

View File

@@ -21,10 +21,25 @@ func notifyOnCont(resizeChan chan<- os.Signal) {
}
func quoteEntry(entry string) string {
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
}
if strings.Contains(shell, "cmd") {
// backslash escaping is done here for applications
// (see ripgrep test case in terminal_test.go#TestWindowsCommands)
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
// caret is the escape character for cmd shell
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
} else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
escaped := strings.Replace(entry, `"`, `\"`, -1)
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
} else {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
}

View File

@@ -26,7 +26,7 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
}
width += w
if limit > 0 && width > limit {
if width > limit {
return width, idx
}
idx += len(rs)

View File

@@ -38,3 +38,19 @@ func TestOnce(t *testing.T) {
t.Error("Expected: false")
}
}
func TestRunesWidth(t *testing.T) {
for _, args := range [][]int{
{100, 5, -1},
{3, 4, 3},
{0, 1, 0},
} {
width, overflowIdx := RunesWidth([]rune("hello"), 0, 0, args[0])
if width != args[1] {
t.Errorf("Expected width: %d, actual: %d", args[1], width)
}
if overflowIdx != args[2] {
t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx)
}
}
}

View File

@@ -6,23 +6,57 @@ import (
"fmt"
"os"
"os/exec"
"strings"
"sync/atomic"
"syscall"
)
// ExecCommand executes the given command with cmd
var shellPath atomic.Value
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string, setpgid bool) *exec.Cmd {
return ExecCommandWith("cmd", command, setpgid)
var shell string
if cached := shellPath.Load(); cached != nil {
shell = cached.(string)
} else {
shell = os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
} else if strings.Contains(shell, "/") {
out, err := exec.Command("cygpath", "-w", shell).Output()
if err == nil {
shell = strings.Trim(string(out), "\n")
}
}
shellPath.Store(shell)
}
return ExecCommandWith(shell, command, setpgid)
}
// ExecCommandWith executes the given command with cmd. _shell parameter is
// ignored on Windows.
// ExecCommandWith executes the given command with the specified shell
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once.
func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd {
cmd := exec.Command("cmd")
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
var cmd *exec.Cmd
if strings.Contains(shell, "cmd") {
cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
CreationFlags: 0,
}
return cmd
}
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
} else {
cmd = exec.Command(shell, "-c", command)
}
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
CreationFlags: 0,
}
return cmd

View File

@@ -2069,6 +2069,145 @@ class TestGoFZF < TestBase
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
end
def test_reload_should_update_preview
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload:echo 4' --preview 'echo {}' --preview-window 'nohidden'", :Enter
tmux.until { |lines| assert_includes lines[1], '1' }
tmux.send_keys 'C-t'
tmux.until { |lines| assert_includes lines[1], '4' }
end
def test_scroll_off
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
height = tmux.until { |lines| lines }.first.to_i
tmux.send_keys :PgUp
tmux.until do |lines|
assert_equal height + 3, lines.first.to_i
assert_equal "> #{height}", lines[3].strip
end
tmux.send_keys :Up
tmux.until { |lines| assert_equal "> #{height + 1}", lines[3].strip }
tmux.send_keys 'l'
tmux.until { |lines| assert_equal '> 1000', lines.first.strip }
tmux.send_keys :PgDn
tmux.until { |lines| assert_equal "> #{1000 - height + 1}", lines.reverse[5].strip }
tmux.send_keys :Down
tmux.until { |lines| assert_equal "> #{1000 - height}", lines.reverse[5].strip }
end
def test_scroll_off_large
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=9999", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
height = tmux.until { |lines| lines }.first.to_i
tmux.send_keys :PgUp
tmux.until { |lines| assert_equal "> #{height}", lines[height / 2].strip }
tmux.send_keys :Up
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
tmux.send_keys :Up
tmux.until { |lines| assert_equal "> #{height + 2}", lines[height / 2].strip }
tmux.send_keys :Down
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
end
def test_header_first
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter
tmux.until do |lines|
expected = <<~OUTPUT
> 4
997/997
>
3
2
1
foobar
OUTPUT
assert_equal expected.chomp, lines.reverse.take(7).reverse.join("\n")
end
end
def test_header_first_reverse
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter
tmux.until do |lines|
expected = <<~OUTPUT
foobar
1
2
3
> < 997/997
> 4
OUTPUT
assert_equal expected.chomp, lines.take(6).join("\n")
end
end
def test_change_preview_window
tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --preview-window border-none --bind '" \
'a:change-preview(echo __{}__),' \
'b:change-preview-window(down)+change-preview(echo =={}==)+change-preview-window(up),' \
'c:change-preview(),d:change-preview-window(hidden),' \
"e:preview(printf ::%${FZF_PREVIEW_COLUMNS}s{})+change-preview-window(up),f:change-preview-window(up,wrap)'", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
tmux.until { |lines| assert_includes lines[0], '[[1]]' }
# change-preview action permanently changes the preview command set by --preview
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[0], '__1__' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[0], '__2__' }
# When multiple change-preview-window actions are bound to a single key,
# the last one wins and the updated options are immediately applied to the new preview
tmux.send_keys 'b'
tmux.until { |lines| assert_equal '==2==', lines[0] }
tmux.send_keys :Up
tmux.until { |lines| assert_equal '==3==', lines[0] }
# change-preview with an empty preview command closes the preview window
tmux.send_keys 'c'
tmux.until { |lines| refute_includes lines[0], '==' }
# change-preview again to re-open the preview window
tmux.send_keys 'a'
tmux.until { |lines| assert_equal '__3__', lines[0] }
# Hide the preview window with hidden flag
tmux.send_keys 'd'
tmux.until { |lines| refute_includes lines[0], '__3__' }
# One-off preview
tmux.send_keys 'e'
tmux.until do |lines|
assert_equal '::', lines[0]
refute_includes lines[1], '3'
end
# Wrapped
tmux.send_keys 'f'
tmux.until do |lines|
assert_equal '::', lines[0]
assert_equal ' 3', lines[1]
end
end
def test_change_preview_window_rotate
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
"a:change-preview-window(right|down|up|hidden|)'", :Enter
3.times do
tmux.until { |lines| lines[0].start_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| lines[0].end_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| lines[-1].start_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| assert_equal 'hello', lines[0] }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[0], 'hello' }
tmux.send_keys 'a'
end
end
end
module TestShell