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

Compare commits

...

11 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
18 changed files with 510 additions and 285 deletions

View File

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

View File

@@ -1,6 +1,19 @@
CHANGELOG 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 0.28.0
------ ------
- Added `--header-first` option to print header before the prompt line - Added `--header-first` option to print header before the prompt line

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 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 gem install --no-document -v 5.14.2 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc

View File

@@ -86,7 +86,7 @@ stuff.
### Using Homebrew ### 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. to install fzf.
```sh ```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`) > 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 ### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.28" var version string = "0.29"
var revision string = "devel" var revision string = "devel"
func main() { 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Nov 2021" "fzf 0.28.0" "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 .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 "Nov 2021" "fzf 0.28.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Dec 2021" "fzf 0.29.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -810,77 +810,79 @@ e.g.
A key or an event can be bound to one or more of the following actions. A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES): \fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR \fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) \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-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\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-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-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-prompt(...)\fR (change prompt to the given string) \fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBclear-screen\fR \fIctrl-l\fR \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBclear-selection\fR (clear multi-selection) \fBchange-prompt(...)\fR (change prompt to the given string)
\fBclose\fR (close preview window if open, abort fzf otherwise) \fBclear-screen\fR \fIctrl-l\fR
\fBclear-query\fR (clear query string) \fBclear-selection\fR (clear multi-selection)
\fBdelete-char\fR \fIdel\fR \fBclose\fR (close preview window if open, abort fzf otherwise)
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty) \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\fR
\fBdeselect-all\fR (deselect all matches) \fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality) \fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality) \fBenable-search\fR (enable search functionality)
\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-silent(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match) \fBfirst\fR (move to the first match)
\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
\fBjump\fR (EasyMotion-like 2-keystroke movement) \fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept) \fBjump-accept\fR (jump and accept)
\fBkill-line\fR \fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR \fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match) \fBlast\fR (move to the last match)
\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-down\fR
\fBhalf-page-up\fR \fBhalf-page-up\fR
\fBpreview(...)\fR (see below for the details) \fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR \fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR \fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR \fBpreview-page-down\fR
\fBpreview-page-up\fR \fBpreview-page-up\fR
\fBpreview-half-page-down\fR \fBpreview-half-page-down\fR
\fBpreview-half-page-up\fR \fBpreview-half-page-up\fR
\fBpreview-bottom\fR \fBpreview-bottom\fR
\fBpreview-top\fR \fBpreview-top\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt) \fBput\fR (put the character to the prompt)
\fBrefresh-preview\fR \fBrefresh-preview\fR
\fBreload(...)\fR (see below for the details) \fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR
\fBselect-all\fR (select all matches) \fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR) \fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches) \fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\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-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-preview-wrap\fR \fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality) \fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR \fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunbind(...)\fR (unbind bindings) \fBunbind(...)\fR (unbind bindings)
\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
.SS ACTION COMPOSITION .SS ACTION COMPOSITION
@@ -970,7 +972,6 @@ commands in addition to the default preview command given by \fB--preview\fR
option. option.
e.g. e.g.
# Default preview command with an extra preview binding # Default preview command with an extra preview binding
fzf --preview 'file {}' --bind '?:preview:cat {}' fzf --preview 'file {}' --bind '?:preview:cat {}'
@@ -981,6 +982,22 @@ e.g.
# Preview window hidden by default, it appears when you first hit '?' # Preview window hidden by default, it appears when you first hit '?'
fzf --bind '?:preview:cat {}' --preview-window hidden 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 .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

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

View File

@@ -41,7 +41,7 @@ __fzf_cd__() {
local cmd dir 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 \ 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-"}" -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__() { __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 eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]
cd $result cd -- $result
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""

View File

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

View File

@@ -176,6 +176,14 @@ type previewOpts struct {
headerLines int 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 // Options stores the values of command-line options
type Options struct { type Options struct {
Fuzzy bool Fuzzy bool
@@ -216,7 +224,7 @@ type Options struct {
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[tui.Event]string Expect map[tui.Event]string
Keymap map[tui.Event][]action Keymap map[tui.Event][]*action
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
@@ -279,7 +287,7 @@ func defaultOptions() *Options {
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[tui.Event]string), Expect: make(map[tui.Event]string),
Keymap: make(map[tui.Event][]action), Keymap: make(map[tui.Event][]*action),
Preview: defaultPreviewOpts(""), Preview: defaultPreviewOpts(""),
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
@@ -787,10 +795,10 @@ func init() {
// 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(
`(?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 { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
symbol := ":" symbol := ":"
if strings.HasPrefix(src, "+") { if strings.HasPrefix(src, "+") {
@@ -799,6 +807,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
prefix := symbol + "execute" prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") { if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "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") { } else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview" prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") { } else if strings.HasPrefix(src[1:], "unbind") {
@@ -842,7 +854,7 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
idx2 := len(pair[0]) + 1 idx2 := len(pair[0]) + 1
specs := strings.Split(pair[1], "+") specs := strings.Split(pair[1], "+")
actions := make([]action, 0, len(specs)) actions := make([]*action, 0, len(specs))
appendAction := func(types ...actionType) { appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...) actions = append(actions, toActions(types...)...)
} }
@@ -1002,6 +1014,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
offset = len("reload") offset = len("reload")
case actPreview: case actPreview:
offset = len("preview") offset = len("preview")
case actChangePreviewWindow:
offset = len("change-preview-window")
case actChangePreview:
offset = len("change-preview")
case actChangePrompt: case actChangePrompt:
offset = len("change-prompt") offset = len("change-prompt")
case actUnbind: case actUnbind:
@@ -1017,17 +1033,22 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
if spec[offset] == ':' { if spec[offset] == ':' {
if specIndex == len(specs)-1 { if specIndex == len(specs)-1 {
actionArg = spec[offset+1:] actionArg = spec[offset+1:]
actions = append(actions, action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} else { } else {
prevSpec = spec + "+" prevSpec = spec + "+"
continue continue
} }
} else { } else {
actionArg = spec[offset+1 : len(spec)-1] 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 { if t == actUnbind {
parseKeyChords(actionArg, "unbind target required") parseKeyChords(actionArg, "unbind target required")
} else if t == actChangePreviewWindow {
opts := previewOpts{}
for _, arg := range strings.Split(actionArg, "|") {
parsePreviewWindow(&opts, arg)
}
} }
} }
} }
@@ -1053,6 +1074,10 @@ func isExecuteAction(str string) actionType {
return actUnbind return actUnbind
case "preview": case "preview":
return actPreview return actPreview
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
return actChangePreview
case "change-prompt": case "change-prompt":
return actChangePrompt return actChangePrompt
case "execute": case "execute":
@@ -1065,7 +1090,7 @@ func isExecuteAction(str string) actionType {
return actIgnore 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") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
@@ -1633,11 +1658,29 @@ func postProcessOptions(opts *Options) {
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, actions := range opts.Keymap { for key, actions := range opts.Keymap {
var lastChangePreviewWindow *action
for _, act := range actions { for _, act := range actions {
if act.t == actToggleSort { switch act.t {
case actToggleSort:
// To display "+S"/"-S" on info line
opts.ToggleSort = true 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 keymap[key] = actions
} }
opts.Keymap = keymap opts.Keymap = keymap

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math"
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
@@ -104,88 +105,89 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
infoStyle infoStyle infoStyle infoStyle
spinner []string spinner []string
prompt func() prompt func()
promptLen int promptLen int
pointer string pointer string
pointerLen int pointerLen int
pointerEmpty string pointerEmpty string
marker string marker string
markerLen int markerLen int
markerEmpty string markerEmpty string
queryLen [2]int queryLen [2]int
layout layoutType layout layoutType
fullscreen bool fullscreen bool
keepRight bool keepRight bool
hscroll bool hscroll bool
hscrollOff int hscrollOff int
scrollOff int scrollOff int
wordRubout string wordRubout string
wordNext string wordNext string
cx int cx int
cy int cy int
offset int offset int
xoffset int xoffset int
yanked []rune yanked []rune
input []rune input []rune
multi int multi int
sort bool sort bool
toggleSort bool toggleSort bool
delimiter Delimiter delimiter Delimiter
expect map[tui.Event]string expect map[tui.Event]string
keymap map[tui.Event][]action keymap map[tui.Event][]*action
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
cycle bool cycle bool
headerFirst bool headerFirst bool
headerLines int headerLines int
header []string header []string
header0 []string header0 []string
ansi bool ansi bool
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
strong tui.Attr strong tui.Attr
unicode bool unicode bool
borderShape tui.BorderShape borderShape tui.BorderShape
cleanExit bool cleanExit bool
paused bool paused bool
border tui.Window border tui.Window
window tui.Window window tui.Window
pborder tui.Window pborder tui.Window
pwindow tui.Window pwindow tui.Window
count int count int
progress int progress int
reading bool reading bool
running bool running bool
failed *string failed *string
jumping jumpMode jumping jumpMode
jumpLabels string jumpLabels string
printer func(string) printer func(string)
printsep string printsep string
merger *Merger merger *Merger
selected map[int32]selectedItem selected map[int32]selectedItem
version int64 version int64
reqBox *util.EventBox reqBox *util.EventBox
previewOpts previewOpts initialPreviewOpts previewOpts
previewer previewer previewOpts previewOpts
previewed previewed previewer previewer
previewBox *util.EventBox previewed previewed
eventBox *util.EventBox previewBox *util.EventBox
mutex sync.Mutex eventBox *util.EventBox
initFunc func() mutex sync.Mutex
prevLines []itemLine initFunc func()
suppress bool prevLines []itemLine
sigstop bool suppress bool
startChan chan bool sigstop bool
killChan chan int startChan chan bool
slab *util.Slab killChan chan int
theme *tui.ColorTheme slab *util.Slab
tui tui.Renderer theme *tui.ColorTheme
executing *util.AtomicBool tui tui.Renderer
executing *util.AtomicBool
} }
type selectedItem struct { type selectedItem struct {
@@ -216,6 +218,7 @@ const (
reqRefresh reqRefresh
reqReinit reqReinit
reqRedraw reqRedraw
reqFullRedraw
reqClose reqClose
reqPrintQuery reqPrintQuery
reqPreviewEnqueue reqPreviewEnqueue
@@ -286,6 +289,8 @@ const (
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
actPreview actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop actPreviewTop
actPreviewBottom actPreviewBottom
actPreviewUp actPreviewUp
@@ -324,9 +329,10 @@ type searchRequest struct {
} }
type previewRequest struct { type previewRequest struct {
template string template string
pwindow tui.Window pwindow tui.Window
list []*Item scrollOffset int
list []*Item
} }
type previewResult struct { type previewResult struct {
@@ -336,16 +342,16 @@ type previewResult struct {
spinner string spinner string
} }
func toActions(types ...actionType) []action { func toActions(types ...actionType) []*action {
actions := make([]action, len(types)) actions := make([]*action, len(types))
for idx, t := range types { for idx, t := range types {
actions[idx] = action{t: t, a: ""} actions[idx] = &action{t: t, a: ""}
} }
return actions return actions
} }
func defaultKeymap() map[tui.Event][]action { func defaultKeymap() map[tui.Event][]*action {
keymap := make(map[tui.Event][]action) keymap := make(map[tui.Event][]*action)
add := func(e tui.EventType, a actionType) { add := func(e tui.EventType, a actionType) {
keymap[e.AsEvent()] = toActions(a) keymap[e.AsEvent()] = toActions(a)
} }
@@ -416,7 +422,7 @@ func trimQuery(query string) []rune {
func hasPreviewAction(opts *Options) bool { func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap { for _, actions := range opts.Keymap {
for _, action := range actions { for _, action := range actions {
if action.t == actPreview { if action.t == actPreview || action.t == actChangePreview {
return true return true
} }
} }
@@ -496,72 +502,73 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
} }
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
spinner: makeSpinner(opts.Unicode), spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0}, queryLen: [2]int{0, 0},
layout: opts.Layout, layout: opts.Layout,
fullscreen: fullscreen, fullscreen: fullscreen,
keepRight: opts.KeepRight, keepRight: opts.KeepRight,
hscroll: opts.Hscroll, hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff, hscrollOff: opts.HscrollOff,
scrollOff: opts.ScrollOff, scrollOff: opts.ScrollOff,
wordRubout: wordRubout, wordRubout: wordRubout,
wordNext: wordNext, wordNext: wordNext,
cx: len(input), cx: len(input),
cy: 0, cy: 0,
offset: 0, offset: 0,
xoffset: 0, xoffset: 0,
yanked: []rune{}, yanked: []rune{},
input: input, input: input,
multi: opts.Multi, multi: opts.Multi,
sort: opts.Sort > 0, sort: opts.Sort > 0,
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
margin: opts.Margin, margin: opts.Margin,
padding: opts.Padding, padding: opts.Padding,
unicode: opts.Unicode, unicode: opts.Unicode,
borderShape: opts.BorderShape, borderShape: opts.BorderShape,
cleanExit: opts.ClearOnExit, cleanExit: opts.ClearOnExit,
paused: opts.Phony, paused: opts.Phony,
strong: strongAttr, strong: strongAttr,
cycle: opts.Cycle, cycle: opts.Cycle,
headerFirst: opts.HeaderFirst, headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
header: header, header: header,
header0: header, header0: header,
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
reading: true, reading: true,
running: true, running: true,
failed: nil, failed: nil,
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
printer: opts.Printer, printer: opts.Printer,
printsep: opts.PrintSep, printsep: opts.PrintSep,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[int32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
previewOpts: opts.Preview, initialPreviewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""}, previewOpts: opts.Preview,
previewed: previewed{0, 0, 0, false}, previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
previewBox: previewBox, previewed: previewed{0, 0, 0, false},
eventBox: eventBox, previewBox: previewBox,
mutex: sync.Mutex{}, eventBox: eventBox,
suppress: true, mutex: sync.Mutex{},
sigstop: false, suppress: true,
slab: util.MakeSlab(slab16Size, slab32Size), sigstop: false,
theme: opts.Theme, slab: util.MakeSlab(slab16Size, slab32Size),
startChan: make(chan bool, 1), theme: opts.Theme,
killChan: make(chan int), startChan: make(chan bool, 1),
tui: renderer, killChan: make(chan int),
initFunc: func() { renderer.Init() }, tui: renderer,
executing: util.NewAtomicBool(false)} initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0) t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0) t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@@ -703,7 +710,7 @@ func (t *Terminal) sortSelected() []selectedItem {
} }
func (t *Terminal) displayWidth(runes []rune) int { 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 return width
} }
@@ -1191,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) { func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
// We start from the beginning to handle tab characters // 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 { if overflowIdx >= 0 {
return runes[:overflowIdx], true return runes[:overflowIdx], true
} }
@@ -1642,9 +1649,14 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list) 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( 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 { atoi := func(s string) int {
n, e := strconv.Atoi(s) n, e := strconv.Atoi(s)
@@ -1655,20 +1667,21 @@ func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
} }
base := -1 base := -1
height := util.Max(0, t.pwindow.Height()-t.previewOpts.headerLines)
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) { for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") { if strings.HasPrefix(component, "-/") {
component = component[1:] component = component[1:]
} }
if component[0] == '/' { if component[0] == '/' {
denom := atoi(component[1:]) denom := atoi(component[1:])
if denom == 0 { if denom != 0 {
return base base -= height / denom
} }
return base - height/denom break
} }
base += atoi(component) 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 { func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
@@ -1767,8 +1780,10 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
}) })
} }
func (t *Terminal) redraw() { func (t *Terminal) redraw(clear bool) {
t.tui.Clear() if clear {
t.tui.Clear()
}
t.tui.Refresh() t.tui.Refresh()
t.printAll() t.printAll()
} }
@@ -1788,7 +1803,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.tui.Pause(true) t.tui.Pause(true)
cmd.Run() cmd.Run()
t.tui.Resume(true, false) t.tui.Resume(true, false)
t.redraw() t.redraw(true)
t.refresh() t.refresh()
} else { } else {
t.tui.Pause(false) t.tui.Pause(false)
@@ -1933,7 +1948,7 @@ func (t *Terminal) Loop() {
go func() { go func() {
for { for {
<-resizeChan <-resizeChan
t.reqBox.Set(reqRedraw, nil) t.reqBox.Set(reqFullRedraw, nil)
} }
}() }()
@@ -1972,12 +1987,14 @@ func (t *Terminal) Loop() {
var items []*Item var items []*Item
var commandTemplate string var commandTemplate string
var pwindow tui.Window var pwindow tui.Window
initialOffset := 0
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.(previewRequest) request := value.(previewRequest)
commandTemplate = request.template commandTemplate = request.template
initialOffset = request.scrollOffset
items = request.list items = request.list
pwindow = request.pwindow pwindow = request.pwindow
} }
@@ -1989,11 +2006,9 @@ func (t *Terminal) Loop() {
if items[0] != nil { if items[0] != nil {
_, query := t.Input() _, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items) command := t.replacePlaceholder(commandTemplate, false, string(query), items)
initialOffset := 0
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
if pwindow != nil { if pwindow != nil {
height := pwindow.Height() height := pwindow.Height()
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
env := os.Environ() env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height) lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width()) columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@@ -2128,7 +2143,7 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.isPreviewEnabled() { if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, list}) t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
} }
} }
@@ -2183,9 +2198,11 @@ func (t *Terminal) Loop() {
t.suppress = false t.suppress = false
case reqReinit: case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop) t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw() t.redraw(true)
case reqRedraw: case reqRedraw:
t.redraw() t.redraw(false)
case reqFullRedraw:
t.redraw(true)
case reqClose: case reqClose:
exit(func() int { exit(func() int {
if t.output() { if t.output() {
@@ -2253,13 +2270,15 @@ func (t *Terminal) Loop() {
} }
} }
} }
togglePreview := func(enabled bool) { togglePreview := func(enabled bool) bool {
if t.previewer.enabled != enabled { if t.previewer.enabled != enabled {
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() t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader) req(reqPrompt, reqList, reqInfo, reqHeader)
return true
} }
return false
} }
toggle := func() bool { toggle := func() bool {
current := t.currentItem() current := t.currentItem()
@@ -2296,12 +2315,12 @@ func (t *Terminal) Loop() {
} }
} }
actionsFor := func(eventType tui.EventType) []action { actionsFor := func(eventType tui.EventType) []*action {
return t.keymap[eventType.AsEvent()] return t.keymap[eventType.AsEvent()]
} }
var doAction func(action) bool var doAction func(*action) bool
doActions := func(actions []action) bool { doActions := func(actions []*action) bool {
for _, action := range actions { for _, action := range actions {
if !doAction(action) { if !doAction(action) {
return false return false
@@ -2309,7 +2328,7 @@ func (t *Terminal) Loop() {
} }
return true return true
} }
doAction = func(a action) bool { doAction = func(a *action) bool {
switch a.t { switch a.t {
case actIgnore: case actIgnore:
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
@@ -2327,7 +2346,7 @@ func (t *Terminal) Loop() {
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.pwindow, list}) previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
} }
} }
} }
@@ -2489,14 +2508,14 @@ func (t *Terminal) Loop() {
} }
case actToggleIn: case actToggleIn:
if t.layout != layoutDefault { 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: case actToggleOut:
if t.layout != layoutDefault { 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: case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() { if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true) t.vmove(-1, true)
@@ -2520,7 +2539,7 @@ func (t *Terminal) Loop() {
req(reqClose) req(reqClose)
} }
case actClearScreen: case actClearScreen:
req(reqRedraw) req(reqFullRedraw)
case actClearQuery: case actClearQuery:
t.input = []rune{} t.input = []rune{}
t.cx = 0 t.cx = 0
@@ -2707,6 +2726,45 @@ func (t *Terminal) Loop() {
for key := range keys { for key := range keys {
delete(t.keymap, key) 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 return true
} }
@@ -2714,7 +2772,7 @@ func (t *Terminal) Loop() {
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
actions := t.keymap[event.Comparable()] actions := t.keymap[event.Comparable()]
if len(actions) == 0 && event.Type == tui.Rune { if len(actions) == 0 && event.Type == tui.Rune {
doAction(action{t: actRune}) doAction(&action{t: actRune})
} else if !doActions(actions) { } else if !doActions(actions) {
continue continue
} }

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") w = runewidth.StringWidth(s) + strings.Count(s, "\n")
} }
width += w width += w
if limit > 0 && width > limit { if width > limit {
return width, idx return width, idx
} }
idx += len(rs) idx += len(rs)

View File

@@ -38,3 +38,19 @@ func TestOnce(t *testing.T) {
t.Error("Expected: false") 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

@@ -2142,6 +2142,72 @@ class TestGoFZF < TestBase
assert_equal expected.chomp, lines.take(6).join("\n") assert_equal expected.chomp, lines.take(6).join("\n")
end end
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 end
module TestShell module TestShell